Дополнительное поле в карточке товара OpenCart

16.12.2020

Разрабатывая модуль для OpenCart, возникла необходимость сделать дополнительное поле в карточке товара, которое должно быть видно только в админке. Это должно быть поле с логическим значением "маркирован товар или нет". Вот так в итоге:

Дополнительное поле в карточке товара OpenCart
Дополнительное поле в карточке товара OpenCart

Недолго поковыряв админку выяснил, что из коробки OpenCart не поддерживает произвольные поля, однако есть платные модули, например вот. Значит нужно сделать кастомное поле в OpenCart самому.

Варианты решения задачи:

  • заюзать неиспользуемые поля (sku, upc, ean, jan, isbn, mpn) - почти мгновенно, но возможно кто-то из наших клиентов будет использовать занятое нами поле
  • вмешаться в код движка - быстро и мало кода, однако тогда решение не переносимо и при обновлении движка придется обновлять и само решение, так как изменения будут потеряны при обновлении движка
  • разработать модуль - дольше, больше кода путь джедая, переносимо на другие сайты с той же версией OpenCart, не зависит от обновления движка (за исключением мажорных версий)
Для реализации дополнительного поля в карточке товара будем разрабатывать модуль ProductMarkedField (отдельный репозиторий с примером).

В коде этого модуля нет настроек, но в другой статье можно почитать о том, как создать страницу настроек модуля.

В общем схема выглядит так:

  • изменить таблицу базы данных при установке модуля и прибраться в ней после удаления нашего модуля
  • показать дополнительное поле в карточке товара в админке OpenCart
  • сохранить поле при отправке формы
Последние два пункта должны быть выполнены используя события OpenCart показа карточки товара и изменения товара.

Про детали разработки модулей для opencart поговорим в другой раз, а пока предположим что читатель имеет минимальный опыт в этом деле.

Основная логика модуля умещается в один файл контроллера admin/controller/extension/module/productmarkedfield.php (закроем глазки на верстку и sql внутри контроллера).

Для того чтобы модуль отобразился в разделе "Расширений" (чтобы его можно было инсталлировать/деинсталлировать) надо создать языковой файл admin/language/ru-ru/extension/module/productmarkedfield.php с таким содержимым:

•••
php
<?php $_['heading_title'] = 'Кастомное поле в карточке товара "Маркировка"';

Установка модуля

В методе install нужно модифицировать таблицу product:

•••
php
$this->db->query("ALTER TABLE `".DB_PREFIX."product` ADD `marked` TINYINT UNSIGNED NOT NULL DEFAULT '0';");

В ocStore 2.3.x все нормально, но в ocStore 3.0.2.0 при использовании MySQL 8, запрос добавления нового столбца в таблицу заканчивался ошибкой:

Для решения этой проблемы изменим значение по умолчанию для столбца date_available:

•••
php
$this->db->query("ALTER TABLE `".DB_PREFIX."product` CHANGE `date_available` `date_available` DATE NOT NULL;");

Теперь надо добавить обработчики событий, чтобы с их помощью мы могли изменять верстку карточки товара и сохранять значение нашего дополнительного поля (для ocStore 2.3.x):

•••
php
$this->load->model('extension/event'); //событие "после загрузки формы товара" - для показа дополнительного поля товара (обязательна маркировка или нет) $this->model_extension_event->addEvent( 'productmarkedfield', //код, в данном случае название модуля 'admin/view/catalog/product_form/after', //событие 'extension/module/productmarkedfield/eventProductFormAfter' //обработчик ); //событие "после редактирования товара" - для сохранения статуса маркировки $this->model_extension_event->addEvent( 'productmarkedfield', 'admin/model/catalog/product/editProduct/after', 'extension/module/productmarkedfield/eventProductEditAfter' );

Для ocStore 3.0.x модель событий загружается таким образом:

•••
php
$this->load->model('setting/event');

И вместо объекта model_extension_event нужно использовать model_setting_event соответственно.

Показ поля в карточке товара

Выводить наше поле в карточку товара мы будем после загрузки admin/view/template/catalog/product_form.twig. наш метод будет принимать 3 аргумента:

•••
php
public function eventProductFormAfter( &$route, &$args, //переданные аргументы в этот шаблон &$output//html верстка страницы )

Нас интересует третий аргумент &$output, именно его мы и будем модифицировать вставляя туда верстку со значением нашего нового поля.

Для модификации формы нам понадобится Simple HTML DOM, и краткий мануал. Скачиваем и ложим его в system/library, а в коде подгружаем расположенный внутри класс таим образом (@ чтобы не выводить ошибки, так как оригинальная версия этой библиотеки загружается с некритичными ошибками):

•••
php
@$this->load->library('simple_html_dom');

Затем используем регулярные выражения для того чтобы получить id товара. (Искал другие варианты, но id в нормальном виде в $args массиве не нашел):

•••
php
preg_match("/product_id=(\d+)/", $args["action"], $aMatch); $idProdict = $aMatch[1];

Загружаем модель описывающую товар и получаем необходимую информацию о товаре (данные из таблиц product и product_description):

•••
php
$this->load->model('catalog/product'); $aProduct = $this->model_catalog_product->getProduct($idProdict);

Теперь мы имеем доступ к данным товара, среди которых наше дополнительное поле - значение маркировки.

Однако, этот код будет работать только для редактирования уже существующего товара, но при создании нового товара будут ошибки, так как не будет найден id еще несуществующего товара. Изменим вышеприведенный код:

•••
php
$isMarked = false; if(preg_match("/product_id=(\d+)/", $args["action"], $aMatch)) { $idProduct = $aMatch[1]; $this->load->model('catalog/product'); $aProduct = $this->model_catalog_product->getProduct($idProduct); $isMarked = $aProduct["marked"]; }

То есть мы создали новую переменную isMarked, которая будет хранить в себе значение по умолчанию false и если удастся найти id товара, тогда в isMarked будет записано значение из нашего произвольного поля в карточке товара OpenCart.

Теперь при помощи Simple HTML DOM найдем вкладку "Данные" и вставим в самое начало наше поле маркировки, предварительно подсмотрев на верстку необходимого gui элемента в dmin/view/template/catalog/product_form.twig (в ocStore 2.3.x tpl расширение, и внутри нет Twig):

•••
php
$html = str_get_html($output); $html->find('div#tab-data', 0)->innertext = '<div class="form-group"> <label class="col-sm-2 control-label">Маркирован</label> <div class="col-sm-10"> <label class="radio-inline"> <input type="radio" name="marked" value="1" '.($aProduct["marked"] ? 'checked="checked"' : "").'>Да </label> <label class="radio-inline"> <input type="radio" name="marked" value="0" '.(!$aProduct["marked"] ? 'checked="checked"' : "").'>Нет </label> </div> </div>' . $html->find('div#tab-data', 0)->innertext;
Код рабочего метода целиком:
•••
php
public function eventProductFormAfter(&$route, &$args, &$output) { @$this->load->library('simple_html_dom'); $isMarked = false; if(preg_match("/product_id=(\d+)/", $args["action"], $aMatch)) { $idProduct = $aMatch[1]; $this->load->model('catalog/product'); $aProduct = $this->model_catalog_product->getProduct($idProduct); $isMarked = $aProduct["marked"]; } $html = str_get_html($output); $html->find('div#tab-data', 0)->innertext = '<div class="form-group"> <label class="col-sm-2 control-label">Маркирован</label> <div class="col-sm-10"> <label class="radio-inline"> <input type="radio" name="marked" value="1" '.($isMarked ? 'checked="checked"' : "").'>Да </label> <label class="radio-inline"> <input type="radio" name="marked" value="0" '.(!$isMarked ? 'checked="checked"' : "").'>Нет </label> </div> </div>' . $html->find('div#tab-data', 0)->innertext; $output = $html->outertext; }

Сохранение значения дополнительного поля

Для сохранения результатов в момент когда администратор нажимает кнопку "Сохранить", необходимо вручную (с помощью обработчика события) внести изменения в базу данных, так как модель catalog/product при редактировании товара (ModelCatalogProduct::editProduct) сохраняет только определенный набор данных, и наше кастомное поле не входит в этот набор.

Для этого мы уже зарегистрировали ранее обработчик события "после редактирования товара":

•••
php
public function eventProductEditAfter(&$route, &$args) { //в $args[0] лежит id товара $sSql = "UPDATE " . DB_PREFIX . "product SET marked = " . $this->db->escape($args[1]['marked']) . " WHERE product_id = '" . (int)$args[0] . "'"; $this->db->query($sSql); }

Удаление модуля

При деинсталяции модуля надо удалить столбец marked из таблицы product и удалить обработчики событий установленные нашим модулем. Все это делается в методе uninstall.

Удалить столбец из таблицы товаров:

•••
php
$this->db->query("ALTER TABLE `".DB_PREFIX."product` DROP `marked`");

Удалить все сообщения (для ocStore 2.3.x):

•••
php
$this->load->model('extension/event'); $this->model_extension_event->deleteEvent('productmarkedfield');

Удалить все сообщения (ocStore 3.0.x):

•••
php
$this->load->model('setting/event'); $this->model_setting_event->deleteEventByCode('productmarkedfield');

Послесловие

В целом не так уж и трудно, однако кажется немного странным тот факт что верстку надо менять руками, так как нет удобного встроенного инструмента для изменения интерфейса.