Содержание
В статье речь идет об OpenCart версии >=2.3, а именно рассматриваются 2.3 и 3.0
Система событий в OpenCart достаточно интересна, она не является заранее предопределенным списком событий. Внутренность движка устроена таким образом, что почти каждый метод контроллера, который реагирует на определенный роут, загружает какие-то файлы (другие контроллеры, модели, представления, переводы).
Например, рассмотрим контроллер admin/controller/catalog/product.php
у которого на адрес /admin/index.php?route=catalog/product
будет вызван метод index
:
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
) при загрузке:
- контроллеров
- моделей
- представлений
- конфигов
- переводов
- Model - файлы моделей, те что работают с БД,
admin/model
илиcatalog/model
- View - файлы представлений, интерфейс/верстка,
admin/view
илиcatalog/view
- Controller - файлы обработчики роутов (путей по которым админы ходят в админке, а клиенты в клиентской части),
admin/controller
илиcatalog/controller
- language - файлы переводов,
admin/language
илиcatalog/language
- 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):
// 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;
Иными словами:
before
или after
событии.Для загрузчиков файлов действует другая логика.
Если в событии before
один из обработчиков возвращает не null
, тогда загрузка файла не будет происходить, и вместо результата загрузки файла будет результат выполнения обработчика вернувшего не null
. При этом событие after
будет сгенерировано и если один из обработчиков вернет не null
тогда результат его работы заменит предыдущий. Это можно увидеть на примере загрузки контроллера (основная логика аналогична и для представлений/моделей/переводов):
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):
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
это путь до файла обработчика, относительно того контекста для которого устанавливается обработчик, при этом в указанном файле должен быть класс имя которого формируется из"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 будет выглядеть так:
$this->load->model('extension/event'); //событие "после загрузки формы товара" - для показа дополнительного поля товара (обязательна маркировка или нет) $this->model_extension_event->addEvent( 'productmarkedfield', //название модуля 'admin/view/catalog/product_form/after', //событие 'extension/module/productmarkedfield/eventProductFormAfter' //обработчик );
А для OpenCart 3.0 так:
$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
:
$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):
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):
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 будет выглядеть так:
$this->load->model('extension/event'); $this->model_extension_event->deleteEvent('productmarkedfield');
А для OpenCart 3.0:
$this->load->model('setting/event'); $this->model_setting_event->deleteEvent('productmarkedfield');
Итог
Система событий OpenCart достаточна интересна, она позволяет многое и гибко, но не без недостатков. Больше всего смущает тот факт, что для изменения интерфейса (события загрузки представлений) необходимо вручную работать с DOM.
Для понимания содержимого аргументов события нужно изучать исходный код загружаемых файлов, а в случае представлений также необходимо изучать контроллер, который передает в это представление данные. Однако со временем этот факт перерастает из "недостатка в достоинство" раскрывая прелесть движка OpenCart :)