События в OpenCart

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

Система событий в 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?

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

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

Нас интересуют не все объекты. Так как OpenCart построен по MVCl архитектуре, то в нем есть 4 вида загружаемых файлов, на основании загрузки которых можно изменить/добавить логику движку.

MVCl вкратце:

При загрузке каждого такого объекта (кроме конфигов) движок 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 файлов аргументы:

Описание аргументов (дополнительно можно посмотреть здесь system/engine/loader.php):

Для видов в &$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:

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

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

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

Для работы с событиями на стороне админки, для 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();
}

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

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

Отдельно стоит рассказать про отношение trigger и action. trigger указывается полным путем (почти) до файла (и в случае контроллера или модели еще и указанием выполняемого метода) вместе с контекстом admin или catalog, а action указывается относительным, без admin или catalog.

Например:

Код регистрации обработчика событий для 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');
В телеграм канале DevOps от первого лица можно оставить комментарий или почитать интересные истории из практики DevOps