Система событий в OpenCart достаточно интересна, она не является заранее предопределенным списком событий. Внутренность движка устроена таким образом, что почти каждый метод контроллера, который реагирует на определенный роут, загружает какие-то сущности/файлы (другие контроллеры, модели, представления, переводы).
Чтобы полностью ознакомиться с разработкой модулей для OpenCart рекомендую прочитать выделенную статью.
Система событий 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 нет. Однако предполагаемые события можно узнать в файле system/engine/loader.php
. $this->load
и есть объект Loader
.
Просматривая файл можно увидеть что события генерируются ($this->registry->get('event')->trigger
) при загрузке:
Нас интересуют не все объекты. Так как OpenCart построен по MVCl архитектуре, то в нем есть 4 вида загружаемых файлов, на основании загрузки которых можно изменить/добавить логику движку.
MVCl вкратце:
admin/model
или catalog/model
admin/view
или catalog/view
admin/controller
или catalog/controller
admin/language
или catalog/language
При загрузке каждого такого объекта (кроме конфигов) движок OpenCart генерируют события:
То есть, мы можем изменить/заменить логику при загрузке файла.
В момент когда мы открываем страницу админки, или клиент просматривает товар, или со страницы сайта происходит 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;
OpenCart 2.3+ позволяет полностью переопределить поведение запроса при
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
событии может быть критичным для следующих обработчиков, а добавление данных может не иметь смысла если обработчики событий не знают этих данных!
Системные обработчики хранятся в 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" . $relPath . $fileName
где relPath
это относительный путь до файла, а fileName
это имя файла. Для данного примера имя класса контроллера будет ControllerExtensionModuleProductmarkedfield
status
- статус вкл/выкл, по умолчанию включенsort_order
- порядок сортировки (OpenCart 3.0), чем меньше значение тем выше приоритет выполнения, по умолчанию 0Отдельно стоит рассказать про отношение trigger
и action
. trigger
указывается полным путем (почти) до файла (и в случае контроллера или модели еще и указанием выполняемого метода) вместе с контекстом admin
или catalog
, а action
указывается относительным, без admin
или catalog
.
Например:
admin/view/catalog/product_form/after
, action - extension/module/productmarkedfield/eventProductFormAfter
, полный путь до файла обработчика admin/controller/extension/module/productmarkedfield.php
метод ControllerExtensionModuleProductmarkedfield::eventProductFormAfter
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');