В первой части статьи мы написали полностью компоненты для работы с новой Сущностью в OpenCart – Функциями. В этой части мы доделаем до конца поставленное перед нами ТЗ, а это значит, что добавим новые методы в Модель и Контроллер категорий, добавим новую вкладку в Представление формы редактирования Категории и последним пунктом доработаем пользовательскую часть, чтобы наши Функции выводились на странице товара. И конечно не забудем за файлы Языков, не забудем про Языки :)
Давайте сразу решим вопрос с Языками. Идем в admin/language/ru-ru/ru-ru.php
Добавим туда название Таба для Функций в Представлении категорий:
$_['tab_functions'] = 'Функции';
И собственно в admin/language/ru-ru/catalog/category.php
$_['entry_functions_icons'] = 'Иконка функции'; $_['entry_function'] = 'Название функции';
Ну и конечно объявить это все добро в Контроллере Функции admin/controller/catalog/category.php
$data['tab_functions'] = $this->language->get('tab_functions'); $data['entry_functions_icons'] = $this->language->get('entry_functions_icons'); $data['entry_function'] = $this->language->get('entry_function');
Модель, Контроллер и Представление категорий
Не смотря на то, что у нас есть Модель для работы с Функциями, я рекомендую добавить новые методы именно в Модель категорий – admin/model/catalog/category.php
, чтобы все было логично.
Нам нужно добавить условия в методы addCategory()
, editCategory
, deleteCategory()
и создать всего один новый метод — getCategoryFunctions()
, который работает всего с одной таблицей в БД, связывающей function_id
и category_id
.
Ваш опыт и логика могут подсказать, что логичнее хранить такие данные в формате json вместо построчного. В данном случае это не совсем удобно. Например, если вы захотите удалить какую-то Функцию, проверять ее привязку к Категории товаров станет в разы сложнее.
Добавляем новое условие в метод addCategory()
:
if (isset($data['function_category'])) { foreach ($data['function_category'] as $function_id) { $this->db->query("INSERT INTO " . DB_PREFIX . "function_to_category SET category_id = '" . (int)$category_id . "', function_id = '" . (int)$function_id . "'"); } }
Почти похожее в editCategory()
. Однако в этом случае нужно сначала зачистить все относящиеся к этой категорий привязки во избежание конфликтов и задвоений, а уже потом добавлять данные, если они пришли через POST.
$this->db->query("DELETE FROM " . DB_PREFIX . "function_to_category WHERE category_id = '" . (int)$category_id . "'"); if (isset($data['category_functions'])) { foreach ($data['category_functions'] as $function_id) { $this->db->query("INSERT INTO " . DB_PREFIX . "function_to_category SET category_id = '" . (int)$category_id . "', function_id = '" . (int)$function_id . "'"); } }
В метод deleteCategory()
нужно просто вставить одну строку из editCategory()
, которая удалит записи привязки для этой категории:
$this->db->query("DELETE FROM " . DB_PREFIX . "function_to_category WHERE category_id = '" . (int)$category_id . "'");
Ну и собственно, сам метод getCategoryFunctions()
, получающий пока только список привязок Функций к Категории.
public function getCategoryFunctions($category_id) { $category_function_data = array(); $query = $this->db->query("SELECT * FROM " . DB_PREFIX . "function_to_category WHERE category_id = '" . (int)$category_id . "'"); foreach ($query->rows as $result) { $category_function_data[] = $result['function_id']; } return $category_function_data; }
Нам он по сути нужен только для <input type="checkbox>
» в Представлении категории, но об этом позже. Сам список Функций, их Группы и описание мы будем получать методом, который уже описан в Моделе admin/model/catalog/function.php
С моделью разобрались, дальше двигаемся к Контроллеру категории – admin/controller/catalog/category.php
Т.к. мы встроили все необходимые запросы к БД в существующие методы Модели категории, то нас в этом Контроллере будет интересовать исключительно один метод getForm()
.
Найдите это место в коде метода:
$this->load->model('tool/image'); if (isset($this->request->post['image']) && is_file(DIR_IMAGE . $this->request->post['image'])) { $data['thumb'] = $this->model_tool_image->resize($this->request->post['image'], 100, 100); } elseif (!empty($category_info) && is_file(DIR_IMAGE . $category_info['image'])) { $data['thumb'] = $this->model_tool_image->resize($category_info['image'], 100, 100); } else { $data['thumb'] = $this->model_tool_image->resize('no_image.png', 100, 100); }
Тут принципиальный момент исключительно в $this->load->model('tool/image');
, т.к. этот метод Модели нам нужен для обработки (ресайза) картинок. Моя версия ocStore корректно «простила» мой полуночный кодинг, когда я повторно подключил этот метод, но безусловно это не корректно. После этого кода вставляем наш:
if (isset($this->request->post['category_functions'])) { $data['category_functions'] = $this->request->post['category_functions']; } elseif (isset($this->request->get['category_id'])) { $data['category_functions'] = $this->model_catalog_category->getCategoryFunctions($this->request->get['category_id']); } else { $data['category_functions'] = array(); } $this->load->model('catalog/function'); $data['functions'] = array(); $results = $this->model_catalog_function->getFunctions(); foreach ($results as $result) { if (is_file(DIR_IMAGE . $result['image'])) { $image = $this->model_tool_image->resize($result['image'], 40, 40); } else { $image = $this->model_tool_image->resize('no_image.png', 40, 40); } $data['functions'][] = array( 'function_id' => $result['function_id'], 'image' => $image, 'name' => $result['name'], 'function_group' => $result['function_group'], 'sort_order' => $result['sort_order'] ); }
Собственно из того, что тут стоит прокомментировать: в Представление нам передается простой массив $data['category_functions']
, который содержит ID Функций, привязанных к текущей категории, а многомерный массив $data['functions'][]
содержит уже данные всех существующих Функций.
Допишем немного в Представление категории. Для этого идем в admin/view/template/catalog/category_form.tpl
Во-первых, нужно создать еще одну вкладку. Находим:
<ul class="nav nav-tabs"> <li class="active"><a href="#tab-general" data-toggle="tab"><?php echo $tab_general; ?></a></li> <li><a href="#tab-data" data-toggle="tab"><?php echo $tab_data; ?></a></li> <li><a href="#tab-design" data-toggle="tab"><?php echo $tab_design; ?></a></li> </ul>
Изменяем на:
<ul class="nav nav-tabs"> <li class="active"><a href="#tab-general" data-toggle="tab"><?php echo $tab_general; ?></a></li> <li><a href="#tab-data" data-toggle="tab"><?php echo $tab_data; ?></a></li> <li><a href="#tab-functions" data-toggle="tab"><?php echo $tab_functions; ?></a></li> <li><a href="#tab-design" data-toggle="tab"><?php echo $tab_design; ?></a></li> </ul>
Осталось добавить только контент самой вкладки tab_functions
:
<div class="tab-pane" id="tab-functions"> <div class="table-responsive"> <table id="images" class="table table-striped table-bordered table-hover"> <thead> <tr> <td style="width: 1px;" class="text-center"><input type="checkbox" onclick="$('input[name*=\'category_functions\']').prop('checked', this.checked);" /></td> <td class="text-left"><?php echo $entry_functions_icons; ?></td> <td class="text-left"><?php echo $entry_function; ?></td> <td class="text-left"><?php echo $entry_description; ?></td> <td class="text-right"><?php echo $entry_sort_order; ?></td> </tr> </thead> <tbody> <?php foreach ($functions as $function) { ?> <tr> <td class="text-center"><?php if (in_array($function['function_id'], $category_functions)) { ?> <input type="checkbox" name="category_functions[]" value="<?php echo $function['function_id']; ?>" checked="checked" /> <?php } else { ?> <input type="checkbox" name="category_functions[]" value="<?php echo $function['function_id']; ?>" /> <?php } ?></td> <td class="text-center"><?php if ($function['image']) { ?> <img src="<?php echo $function['image']; ?>" style="width:50px;" alt="<?php echo $function['name']; ?>" class="img-thumbnail" /> <?php } else { ?> <span class="img-thumbnail list"><i class="fa fa-camera fa-2x"></i></span> <?php } ?></td> <td class="text-left"><?php echo $function['name']; ?></td> <td class="text-left"><?php echo $function['function_group']; ?></td> <td class="text-right"><?php echo $function['sort_order']; ?></td> </tr> <?php } ?> </tbody> </table> </div> </div>
Из интересного для учащегося разработчика тут разве что использование встроенного метода PHP in_array()
, который проверяет содержится ли ID выводимой через цикл foreach
функции в массиве $category_functions
. Если да, то он меняет значение <input type="checkbox">
на checked
, в противном случае просто выводит <input type="checkbox">
. Указанные input
у нас как раз отвечает за привязку Функции к Категории.
Если вы все сделали верно, то у вас должна получиться такая аккуратная вкладка:
На этом Админка нашего модуля полностью закончена, с чем я вас и поздравляю.
Пользовательская часть модуля
Все конечно круто, но теперь хочется посмотреть как это все будет выглядеть в пользовательской части. Мы столько написали для Категорий, а выводится это должно для Товаров, да еще и в двух местах в разном виде да еще и сортировки учитывать, а как задумаешься, что там еще у Функций и Группы есть, так вообще ужас )) На самом деле как и с Админкой все прекрасно реализуемо, если немного думать и разбивать сложную задачу на простые составные.
Тут нужно сделать всего одну важную оговорку. Указанный модуль можно реализовать в принципе только для ocStore, т.к. там есть такая прекрасная вещь как Главная категория для товара:
Это создает однозначную связь между товаров и его родительской категорией, при это не мешает выводить товар в любых других категориях. Основа нашего модуля – Главная категория, ID
которой мы можем получить из Модели через Метод getProductMainCategoryId()
, в которой в качестве входящего параметра указывается ID
текущего товара. Этот Метод (почему-то) есть только в Административной части, давайте просто скопируем его в Пользовательскую модель. Для этого вставляем в catalog/model/catalog/product.php
public function getProductMainCategoryId($product_id) { $query = $this->db->query("SELECT category_id FROM " . DB_PREFIX . "product_to_category WHERE product_id = '" . (int)$product_id . "' AND main_category = '1' LIMIT 1"); return ($query->num_rows ? (int)$query->row['category_id'] : 0); }
Так же давайте сразу закроем вопрос с файлами языка, чтобы не возвращаться к ним в будущем. Идем в catalog/language/ru-ru/product/product.php
и добавим там:
$_['tab_functions'] = 'Функции';
и в сам Контроллер catalog/controller/product/product.php
сразу пропишем где-то с остальными языковыми переменными:
$data['tab_functions'] = $this->language->get('tab_functions');
Все, можно считать, языки мы учли. Перейдем к Модели товара, она у нас в принципе самое интересное, что вообще есть в этом модуле. По сложности я потратил на нее примерно 35-40% всего времени, которое было отведено на этот модуль. В модель товара catalog/model/catalog/product.php
добавляем новый Метод:
public function getCategoryFunctions($category_id) { $category_function_group_data = array(); $category_function_group_query = $this->db->query("SELECT fg.sort_order, fg.function_group_id, fgd.name FROM " . DB_PREFIX . "function_group fg LEFT JOIN " . DB_PREFIX . "function_group_description fgd ON (fg.function_group_id = fgd.function_group_id) ORDER BY fg.sort_order"); foreach ($category_function_group_query->rows as $category_function_group) { $category_function_data = array(); $category_function_query = $this->db->query("SELECT ftc.function_id, fd.name, fd.description, fd.image FROM " . DB_PREFIX . "function_to_category ftc LEFT JOIN " . DB_PREFIX . "function_description fd ON (ftc.function_id = fd.function_id) LEFT JOIN " . DB_PREFIX . "function f ON (ftc.function_id = f.function_id) WHERE f.function_group_id = '" . (int)$category_function_group['function_group_id'] . "' AND ftc.category_id = '" . (int)$category_id . "' ORDER BY f.sort_order"); if ($category_function_query->num_rows) { foreach ($category_function_query->rows as $category_function) { $this->load->model('tool/image'); $image = $this->model_tool_image->resize($category_function['image'], 40, 40); $category_function_data[] = array( 'function_id' => $category_function['function_id'], 'name' => $category_function['name'], 'description' => $category_function['description'], 'image' => $image ); } $category_function_group_data[] = array( 'function_group_id' => $category_function_group['function_group_id'], 'name' => $category_function_group['name'], 'function' => $category_function_data ); } } return $category_function_group_data; }
Давайте остановимся чуток подробнее на том, что тут спроектировано. Метод getCategoryFunctions()
получил $category_id
, откуда$category_id
взялась в модели Продукта мы узнаем из Контроллера продукта чуть позже;)
Далее мы объявляем массив $category_function_group_data = array();
, который в последствие будет возвращен как результат работы Метода модели в Контроллер.
Этот код получает из БД все существующие Группы функций, а именно их порядок сортировки, ID, а также Имя, отсортировав их по указанному в админке порядку сортировки (такого было ТЗ).
$category_function_group_query = $this->db->query("SELECT fg.sort_order, fg.function_group_id, fgd.name FROM " . DB_PREFIX . "function_group fg LEFT JOIN " . DB_PREFIX . "function_group_description fgd ON (fg.function_group_id = fgd.function_group_id) ORDER BY fg.sort_order");
Прекрасно, у нас есть ID
Групп Функций (ну и не только). Пришло время получить по ним Функции, а еще разложить их «по полочкам», т.е. в многомерным массиве, где первым уровнем идут Группы Функций со своими полями, а следующим принадлежащие им Функции так же со своими полями:
foreach ($category_function_group_query->rows as $category_function_group) { $category_function_data = array(); $category_function_query = $this->db->query("SELECT ftc.function_id, fd.name, fd.description, fd.image FROM " . DB_PREFIX . "function_to_category ftc LEFT JOIN " . DB_PREFIX . "function_description fd ON (ftc.function_id = fd.function_id) LEFT JOIN " . DB_PREFIX . "function f ON (ftc.function_id = f.function_id) WHERE f.function_group_id = '" . (int)$category_function_group['function_group_id'] . "' AND ftc.category_id = '" . (int)$category_id . "' ORDER BY f.sort_order");
Условно говоря тут написано: дайка мне в цикле foreach
все Функции и их поля (JOIN разных таблиц), у которых ID
Группы функции = $category_function_group['function_group_id']
, т.е. текущего в цикле foreach
элемента массива $category_function_group
, в котором содержится ID
Группы функции.
Дальше подключим немного мозгов и магии. Т.к. возможны ситуации, что не все Группы функций активны (содержат хоть 1 присвоенную Категории товаров Функцию), давайте добавим проверку, что массив $category_function_query
получился не пустой для текущего значения $category_function_group['function_group_id']
. Не поверите, но для этого в OpenCart существует специальный Метод с говорящим названием num_rows()
, который возвращает true
, если есть хоть один элемент и false
если массив пуст.
if ($category_function_query->num_rows) {
Внутри этого условия, если у нас массив оказался не пуст, возьмем из него данные и сложим в массив Функций $category_function_data[]
, который потом положим уже в многомерный массив Групп функций.
foreach ($category_function_query->rows as $category_function) { $this->load->model('tool/image'); $image = $this->model_tool_image->resize($category_function['image'], 40, 40); $category_function_data[] = array( 'function_id' => $category_function['function_id'], 'name' => $category_function['name'], 'description' => $category_function['description'], 'image' => $image ); }
Тут можно акцентировать внимание, что OpenCart разрешает вызывать одни Модели внутри других. Например тут я подключаю Модель $this->load->model('tool/image');
, которая используется для изменения размера картинок, как видно у меня, мне нужны квадратные иконки размером 40х40 пикселей. Еще этот Метод создает отдельную копию нашей картинки с указанными размерами, а в переменную $image
положит путь уже к измененной картинке.
Ну и последний кусочек этого Метода модели:
$category_function_group_data[] = array( 'function_group_id' => $category_function_group['function_group_id'], 'name' => $category_function_group['name'], 'function' => $category_function_data ); } } return $category_function_group_data; }
Как я уже говорил в возвращаемый в Контроллер многомерный массив $category_function_group_data
попадут только те Группы и их Функции, у которых есть хоть одна Функция, присвоенная Родительской категории текущего товара.
Давайте допишем наш ужасный Контроллер. Предупреждаю! Слабонервным лучше отойти от экранов, слабых на здоровье прекратить читать. Я предупредил.
Идем в catalog/controller/product/product.php
и дописываем этот огромный код:
$main_category_id = $this->model_catalog_product->getProductMainCategoryId($product_id); $data['category_function_groups'] = $this->model_catalog_product->getCategoryFunctions($main_category_id);
Т.к. всю работу мы сделали на стороне Модели, код в Контроллере у нас очень простой :). Сначала мы получили ID
Главной категории товара через Метод getProductMainCategoryId();
. Потом получили привязанные к этой ID
категории Группы функций и сами Функции.
Остался «последний бастион», а именно .tpl
нашего товара, где мы всю эту красоту должны вывести. Идем в catalog/view/theme/
ваша-тема
/template/product/product.tpl
. В качестве примера у нас будет стандартная тема, она же будет и в демо этой доработки.
Если помните еще ТЗ, то выводить иконки нужно было два раза. 1-й раз где-нибудь в основной информации о товаре блоком, в котором выводятся просто иконки без описания, причем их максимальное количество должно быть ограничено 30-ю.
Для этого в нужно месте нашего шаблона выведем:
<?php if($category_function_groups) { ?> <div class="icons"> <?php $i = 0; ?> <?php foreach ($category_function_groups as $category_function_group) { ?> <?php foreach ($category_function_group['function'] as $function) { ?> <div class="product-page__daikin-icons"><img src="<?php echo $function['image']; ?>" alt="<?php echo $function['name']; ?>" class="product-page__img-icon" data-toggle="tooltip" data-placement="top" title="<?php echo $function['name']; ?>"></div> <?php $i++;?> <?php if($i > 31) { ?> <?php break 2; ?> <?php } ?> <?php } ?> <?php } ?> </div> <a href="#tab-functions">Все функции</a> <?php } ?>
Для реализации задумки клиента был применен грязный хак в виде break 2;
, т.к. у нас Функции лежали в многомерном массиве $category_function_groups
, где в Группе может быть от 1 до 100 000 Функций, а нам нужно только 30, то нужно использовать классику в виде счетчика $i++, а также цикл foreach
запущенный внутри другого цикла foreach
. Когда у нас насобирается 30 Функций, нам нужно прекратить выполнение сразу двух циклов, поэтому break 2
;
Уже можно посмотреть на результат:
Тут и дальше я безусловно поработал напильником с CSS, у меня используются SVG иконки, у которых тоже своя особенность, однако т.к. это выходит за рамки нашей задачи, листинги CSS я не привожу, оставляя эту часть на вкус читателя.
Теперь давайте выведем наши Функции более подробно и в полном «обмундировании» в отдельной вкладке Функции. Добавим в tpl
новую таб, для этого в секции <ul class="nav nav-tabs">
вставляем:
<?php if($category_function_groups) { ?> <li><a href="#tab-functions" data-toggle="tab"><?php echo $tab_functions; ?></a></li> <?php } ?>
Думаю, тут все согласятся, что если у товара нет никаких Функций то и не стоит выводить пустой таб, поэтому стоит проверка.
Давайте допишем сам код контента таба (так же с условием на существование массива):
<?php if($category_function_groups) { ?> <div class="tab-pane" id="tab-functions"> <?php if($category_function_groups) { ?> <?php foreach ($category_function_groups as $category_function_group) { ?> <h2 style="margin: 20px 0"><?php echo $category_function_group['name']; ?></h2> <div class="product-page__daikin-list-item"> <div class="function-list"> <?php foreach ($category_function_group['function'] as $function) { ?> <div class="function-item" style="display: block;"> <img src="<?php echo $function['image']; ?>" alt="<?php echo $function['name']; ?>" class="product-page__img-function" data-toggle="tooltip" data-placement="top" style="width: 40px;" title="<?php echo $function['name']; ?>"> <div class="function_text"><strong><?php echo $function['name']; ?></strong> <?php echo $function['description']; ?></div> </div> <?php } ?> </div> </div> <?php } ?> <?php } ?> </div> <?php } ?>
Если вы все сделали верно, то у вас должно быть как на моей демке https://oc.netsh.pp.ua/desktops/mac/imac
На этом разработку этого модуля можно считать законченной.
Окмода нет? В админке все получилось, на фронте ошибка о отсутствии переменной $category_function_groups
Решили в привате. Если у кого-то еще есть вопросы — пишите.
Ошибка у Славы была в том, что он не скопировал метод getMainCategoryId с админской модели в клиентскую.
Теперь еще возник вопрос, как эти преимущества, не переносить в карточку продукта, а вывести на странице категорий?
Boris https://ya.ru
Автор, отображение иконок подправь на demo сайте к статье.
Лень, точнее времени нет.