Пишем свой модуль для OpenCart. Advanced Level. Продолжение

Работа сис. админа
10 мин. на чтение

В первой части статьи мы написали полностью компоненты для работы с новой Сущностью в 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

На этом разработку этого модуля можно считать законченной.

Ihor Chyshkala

Пишу статьи про ИТ в свободное от работы время.

Оцените автора
Авторский блог Игоря Чишкалы
Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

  1. Слава

    Окмода нет? В админке все получилось, на фронте ошибка о отсутствии переменной $category_function_groups

    Ответить
    1. Ihor Chyshkala автор

      Решили в привате. Если у кого-то еще есть вопросы — пишите.
      Ошибка у Славы была в том, что он не скопировал метод getMainCategoryId с админской модели в клиентскую.

      Ответить
  2. Слава

    Теперь еще возник вопрос, как эти преимущества, не переносить в карточку продукта, а вывести на странице категорий?

    Ответить
  3. Boris https://ya.ru
    Ответить
  4. Диванный аналитик

    Автор, отображение иконок подправь на demo сайте к статье.

    Ответить
    1. Ihor Chyshkala автор

      Лень, точнее времени нет.

      Ответить