События в OpenCart

23.12.2020

В статье речь идет об OpenCart версии >=2.3, а именно рассматриваются 2.3 и 3.0

Система событий в OpenCart достаточно интересна, она не является заранее предопределенным списком событий. Внутренность движка устроена таким образом, что почти каждый метод контроллера, который реагирует на определенный роут, загружает какие-то файлы (другие контроллеры, модели, представления, переводы).

Система событий OpenCart это генерируемые события до и после загрузки файлов движка/модулей

Например, рассмотрим контроллер admin/controller/catalog/product.php у которого на адрес /admin/index.php?route=catalog/product будет вызван метод index:

•••
php
public function index() { $this->load->language('catalog/product'); $this->document->setTitle($this->language->get('heading_title')); $this->load->model('catalog/product'); $this->getList(); }

В этом методе используется загрузка файла перевода и модели catalog/product, и на оба факта загрузки можно установить свои обработчики изменяющие данные.

Какие события есть в OpenCart 2.3+?

Как мы определили ранее, заранее определенного списка events в OpenCart нет. Однако предполагаемые события можно узнать в файле system/engine/loader.php. $this->load и есть объект Loader.

Просматривая файл можно увидеть что события генерируются ($this->registry->get('event')->trigger) при загрузке:

  • контроллеров
  • моделей
  • представлений
  • конфигов
  • переводов
Нас интересуют не все объекты. Так как OpenCart построен по MVCl архитектуре, то в нем есть 4 вида загружаемых файлов, на основании загрузки которых можно изменить/добавить логику движку. MVCl вкратце:
  • Model - файлы моделей, те что работают с БД, admin/model или catalog/model
  • View - файлы представлений, интерфейс/верстка, admin/view или catalog/view
  • Controller - файлы обработчики роутов (путей по которым админы ходят в админке, а клиенты в клиентской части), admin/controller или catalog/controller
  • language - файлы переводов, admin/language или catalog/language
При загрузке каждого такого объекта (кроме конфигов) движок OpenCart генерируют события:
  • before - до загрузки
  • after - после загрузки
То есть, мы можем изменить (или вообще заменить, но об этом позже) логику при загрузке файла.

Логика работы событий

В момент когда мы открываем страницу админки, или клиент просматривает товар, или со страницы сайта происходит ajax запрос, движок запускает первый контроллер startup/router, который в свою очередь на основании get параметра route выполняет action целевого контроллера (путь которого указан в route).

Однако, контроллер startup/router не выполняет загрузку через $this->load, а самостоятельно генерирует событие before, получая от него результат, и если этот результат null, тогда целевой контроллер будет выполнен и наступит событие after (куcок кода из admin/controller/startup/router.php OpenCart 3.0):

•••
php
// Trigger the pre events $result = $this->event->trigger('controller/' . $route . '/before', array(&$route, &$data)); if (!is_null($result)) { return $result; } // We dont want to use the loader class as it would make an controller callable. $action = new Action($route); // Any output needs to be another Action object. $output = $action->execute($this->registry); // Trigger the post events $result = $this->event->trigger('controller/' . $route . '/after', array(&$route, &$data, &$output)); if (!is_null($result)) { return $result; } return $output;

Иными словами:

OpenCart 2.3+ позволяет полностью переопределить поведение запроса при before или after событии.

Для загрузчиков файлов действует другая логика.

Если в событии before один из обработчиков возвращает не null, тогда загрузка файла не будет происходить, и вместо результата загрузки файла будет результат выполнения обработчика вернувшего не null. При этом событие after будет сгенерировано и если один из обработчиков вернет не null тогда результат его работы заменит предыдущий. Это можно увидеть на примере загрузки контроллера (основная логика аналогична и для представлений/моделей/переводов):

•••
php
public function controller($route, $data = array()) { // Sanitize the call $route = preg_replace('/[^a-zA-Z0-9_\/]/', '', (string)$route); // Keep the original trigger $trigger = $route; file_put_contents($_SERVER['DOCUMENT_ROOT']."/loader-controller.txt", $trigger."\n", FILE_APPEND); // Trigger the pre events $result = $this->registry->get('event')->trigger('controller/' . $trigger . '/before', array(&$route, &$data)); // Make sure its only the last event that returns an output if required. if ($result != null && !$result instanceof Exception) { $output = $result; } else { $action = new Action($route); $output = $action->execute($this->registry, array(&$data)); } // Trigger the post events $result = $this->registry->get('event')->trigger('controller/' . $trigger . '/after', array(&$route, &$data, &$output)); if ($result && !$result instanceof Exception) { $output = $result; } if (!$output instanceof Exception) { return $output; } }

Любой из обработчиков события before или after, может переопределить результат выполнения события.

В system/engine/event.php Event::trigger определено: если какой-либо обработчик события before или after) возвращает не null, тогда после него не будут запущены другие обработчики этого события (для before или after).

Аргументы обработчиков событий

Для четырех видов файлов (контроллер, представление, модель, перевод) набор аргументов и основная логика загрузки одинаковая.

Аргументы событий передаются по ссылке, а значит изменения значений аргументов будут видны во всех местах где они используются. При загрузке MVCl файлов аргументы:

  • при before событии: &$route и &$data
  • при after событии: &$route, &$data и &$output
Описание аргументов (дополнительно можно посмотреть здесь system/engine/loader.php):
  • &$route содержит данные о пути данного события, без controller|view|model|language и без before|after, например для события catalog/model/checkout/order/addOrderHistory/after в &$route будет checkout/order/addOrderHistory
  • &$data содержит массив данных для работы события (либо пустой массив), например для файлов представления это данные для подстановки в tpl/twig файлах
  • &$output содержит результат работы самого события (или обработчика, который определил возвращаемое значение), например для файлов представления это обработанное содержимое файла представления

Для видов в &$data передается ассоциативный массив для использования в tpl/twig файлах видов. В &$output верстка загруженного вида, где данные из &$data уже вставленны. Изменения &$data при before событии могут не иметь смысла, так как данные уже обработаны. Это относится ко всем загружаемым файлам.

Изменять &$output при after событии представления можно различными способами, один из которых используя библиотеку Simple Html DOM.

Удаление данных из &$data при before событии может быть критичным для следующих обработчиков, а добавление данных может не иметь смысла если обработчики событий не знают этих данных!

Хранение обработчиков событий в OpenCart

Системные обработчики хранятся в php файлах system/config/admin.php и system/config/catalog.php в ассоциативном массиве $_['action_event'], где ключ это загружаемый файл, а значение обработчик. Как видно из ключей этого массива, вместо полного пути загружаемого файла, можно указывать * в качестве реакции "на все". Таким образом часть событий проходит через "движковые обработчики".

В OpenCart 3.0 появились приоритеты работы обработчиков (чем меньше значение тем выше приоритет) и объекты моделей больше не обрабатываются "движковыми" обработчиками.

Пользовательские (от модулей) обработчики событий хранятся в БД в таблице event:

  • event_id - идентификатор (автоинкремент)
  • code - код обработчика, один и тот же код может быть у нескольких обработчиков, сюда записывается название модуля
  • trigger - событие, например admin/view/catalog/product_form/after - после загрузки формы товара
  • action - обработчик, например extension/module/productmarkedfield/eventProductFormAfter
  • status - включен или нет обработчик (1/0)
  • sort_order - порядок сортировки (приоритет выполнения обработчика)

Добавление обработчиков событий

Работа с событиями заключается в:

  • регистрации обработчиков событий при инсталяции модуля (метод install)
  • удалении всех обработчиков событий при деинсталяции модуля (метод uninstall)
Обязательно надо удалять обработчики событий при удалении модуля, иначе движок будет запускать обработчики из модуля, а при отсутствии файлов удаленного модуля будет ошибка!

Для работы с событиями на стороне админки, для OpenCart 2.3 есть модель extension/event, а для OpenCart 3.0 setting/event

Метод регистрации в обоих версиях одинаковый, за исключением параметра $sort_order (кусок кода из модели event для OpenCart 3.0):

•••
php
public function addEvent($code, $trigger, $action, $status = 1, $sort_order = 0) { $this->db->query("INSERT INTO `" . DB_PREFIX . "event` SET `code` = '" . $this->db->escape($code) . "', `trigger` = '" . $this->db->escape($trigger) . "', `action` = '" . $this->db->escape($action) . "', `sort_order` = '" . (int)$sort_order . "', `status` = '" . (int)$status . "'"); return $this->db->getLastId(); }

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

Разберем значение аргументов:

  • code - идентификатор группы обработчиков, обычно это название модуля
  • trigger - обрабатываемое событие, например admin/view/catalog/product_form/after
  • action - контроллер обработчик события, например extension/module/productmarkedfield/eventProductFormAfter это путь до файла обработчика, относительно того контекста для которого устанавливается обработчик, при этом в указанном файле должен быть класс имя которого формируется из
    •••
    php
    "Controller" . $sRelPath . $sFileName
    где sRelPath это относительный путь до файла, а sFileName это имя файла. Для данного примера имя класса контроллера будет ControllerExtensionModuleProductmarkedfield
  • status - статус вкл/выкл, по умолчанию включен
  • sort_order - порядок сортировки (OpenCart 3.0), чем меньше значение тем выше приоритет выполнения, по умолчанию 0
Отдельно стоит рассказать про отношение trigger и action. trigger указывается полным путем (почти) до файла (и в случае контроллера или модели еще и указанием выполняемого метода) вместе с контекстом admin или catalog, а action указывается относительным, без admin или catalog. Например:
  • trigger - admin/view/catalog/product_form/after, action - extension/module/productmarkedfield/eventProductFormAfter, полный путь до файла обработчика admin/controller/extension/module/productmarkedfield.php метод ControllerExtensionModuleProductmarkedfield::eventProductFormAfter
  • trigger - catalog/model/checkout/order/addOrderHistory/after, action - extension/module/productmarkedfield/eventaddOrderHistoryAfter, полный путь до файла обработчика catalog/controller/extension/module/productmarkedfield.php метод ControllerExtensionModuleProductmarkedfield::eventaddOrderHistoryAfter
Для примера работы с функциями добавления/удаления обработчиков, возьмем модуль дополнительного поля в карточке товара из предыдущей статьи.

Код регистрации обработчика событий для OpenCart 2.3 будет выглядеть так:

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

А для OpenCart 3.0 так:

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

Если количество обязательных аргументов обработчика события превышает количество передаваемых аргументов роутером, то обработчик не будет запущен!

Количество обязательных аргументов обработчика имеет значение. system/engine/action.php Action::execute при помощи рефлексии определяет количество необходимых аргументов и если их в обработчике больше чем может передать объект action тогда ожидаем Exception:

•••
php
$reflection = new ReflectionClass($class); if ($reflection->hasMethod($this->method) && $reflection->getMethod($this->method)->getNumberOfRequiredParameters() <= count($args)) { return call_user_func_array(array($controller, $this->method), $args); } else { return new \Exception('Error: Could not call ' . $this->route . '/' . $this->method . '!'); }

Удаление обработчиков событий

Методы удаления уже имеют значительную разницу.

В OpenCart 2.3 у модели extension/event метод deleteEvent удаляет все обработчики событий модуля по коду (кусок кода из OpenCart 2.3):

•••
php
public function deleteEvent($code) { $this->db->query("DELETE FROM `" . DB_PREFIX . "event` WHERE `code` = '" . $this->db->escape($code) . "'"); }

OpenCart 3.0 предоставляет немного больше. Метод deleteEvent удаляет обработчик события по его идентификатору, а метод deleteEventByCode удаляет все обработчики события по коду, как deleteEvent в OpenCart 2.3 (кусок кода OpenCart 3.0):

•••
php
public function deleteEvent($event_id) { $this->db->query("DELETE FROM `" . DB_PREFIX . "event` WHERE `event_id` = '" . (int)$event_id . "'"); } public function deleteEventByCode($code) { $this->db->query("DELETE FROM `" . DB_PREFIX . "event` WHERE `code` = '" . $this->db->escape($code) . "'"); }

Таким образом удаление обработчиков событий для OpenCart 2.3 будет выглядеть так:

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

А для OpenCart 3.0:

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

Итог

Система событий OpenCart достаточна интересна, она позволяет многое и гибко, но не без недостатков. Больше всего смущает тот факт, что для изменения интерфейса (события загрузки представлений) необходимо вручную работать с DOM.

Для понимания содержимого аргументов события нужно изучать исходный код загружаемых файлов, а в случае представлений также необходимо изучать контроллер, который передает в это представление данные. Однако со временем этот факт перерастает из "недостатка в достоинство" раскрывая прелесть движка OpenCart :)