Статья нашего сотрудника из его личного блога.
В статье речь идет об OpenCart версии> =2.3, а именно рассматриваются 2.3 и 3.0
Система событий в 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 2.3+?
Как мы определили ранее, заранее определенного списка событий нет. Однако предполагаемые события можно узнать в файле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
При загрузке каждого такого объекта(кроме конфигов)движка генерируются события:
-
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;
Иными словами,
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событии может быть критичным для следующих обработчиков, а добавление данных может не иметь смысла если обработчики событий не знают этих данных!
Хранение обработчиков событий в 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.0setting/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 :)
Автор: Виталий Бутурлин