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

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

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

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

    Ответить