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

Работа с событиями в OpenCart версии 2.2+ существенно изменилась. С документацией, по этому вопросу, пока плохо. Материал для статьи пришлось брать из своего опыта и разбора кода ядра OpenCart. Поэтому, возможны, неточности.
В OpenCart версии 2.2, события, в основном, не прописаны "жестко" в коде, а генерируются автоматически перед и после выполнения определенного метода определенного контроллера.

События в OpenCart хранятся в
1. В списке событий (массив $_['action_event']) файла конфигурации (system\config\catalog.php или system\config\admin.php для админки)
Тут перечислены служебные события.
2. В базе данных в таблице event. В БД заносятся пользовательские события, например установленные дополнениями (модулями).

Рассмотрим вариант хранения событий в БД.
Вызов проверки наличия обработчиков запускает метод
$this->event->trigger(название события, аргументы для действия)
который передает название события, выполняемое в данный момент.
Данный метод, в OC 2.2+ вызывается:

- в классе загрузчика Loader. Файл system\engine\loader.php
Тут метод trigger() вызывается для контроллеров (доступ к которым получаем с помощью загрузчика $this->load->controller()), моделей, видов, языковых файлов. Причем по два раза – до выполнения их кода (before) и после (after). А значит каждый раз, когда, например, в контроллере вызывается какая-то модель через загрузчик:
$this->load->model('account/address');
или какой-то языковой файл:
$this->load->language('checkout/cart');
и аналогично файл представление или другой контроллер, то выполнение скрипта проходит через класс Event, где сравниваются зарегистрированные обработчики событий с тем событием которое происходит сейчас.
Например в БД , в таблице событий (event) есть поле trigger со значением catalog/controller/product/category/before
Это значит, что перед выполнением контроллера ControllerProductCategory вызовется действие (метод определенного контроллера) которое указано в поле action.

- в классе ControllerStartupRouter. Файл catalog\controller\startup\router.php или аналогичный для админки.
В данном файле, каждый раз при загрузке страницы происходит разбор текущего URL из адресной строки перед выполнением указанного контроллера/метода. Обработчики вызываются так же 2 раза – до выполнения переданных в URL действий
$result = $this->event->trigger('controller/' . $route . '/before', array(&$route, &$data));
и аналогичного после.
А название события формируется в этой строке с учетом контроллера/метода из переменной $route, например:
'controller/product/category/before'
или
'controller/product/category/after'

Хоть проверка установленных обработчиков на события запускается именно в этих файлах (loader.php
и router.php), сами события считываются из БД и регистрируются еще до этого в файле catalog\controller\startup\event.php
<?php
class ControllerStartupEvent extends Controller {
    public function index() {
        // Add events from the DB
        $this->load->model('extension/event');

        $results = $this->model_extension_event->getEvents();
        foreach ($results as $result) {
            $this->event->register(substr($result['trigger'], strpos($result['trigger'], '/') + 1), new Action($result['action']));
        }
    }
}
где вызывается метод register() объекта Action (system\engine\event.php), сохраняющий их в массиве $data.

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

То есть, в OpenCart 2.2+ названия событий формируются динамически из класс/метод который выполняется, а не прописаны четко в коде. Хотя создавая события в своих модуля, можно и прописать «жестко».


Работа с событиями (добавление/удаление и тд.)

Для работы с событиями в админ-части существует модель admin/model/extension/event.php
Данная модель содержит такие методы:
- addEvent() - для добавления нового Обработчика.
public function addEvent($code, $trigger, $action, $status = 1) {
    $this->db->query("INSERT INTO `" . DB_PREFIX . "event` SET `code` = '" . $this->db->escape($code) . "', `trigger` = '" . $this->db->escape($trigger) . "', `action` = '" . $this->db->escape($action) . "', `status` = '" . (int)$status . "', `date_added` = now()");

    return $this->db->getLastId();
}
Метод получает 4 параметра:
$code - код (идентификатор) обработчика, обычно название модуля
$triger - само Событие, которое мы будем обрабатывать
$action - обработчик или метод, который будет вызван для обработки События.
$status – 1 – включено/ 0 -выключено
- deleteEvent() - для удаления Обработчика
а так же методы для получения события или списка событий, включение/выключение отдельного события, удаление события.

Пример работы с событиями в своем модуле:
Файлы модулей могут иметь стандартные методы:
- install() – выполнится при установке (включении) модуля;
- uninstall() – выполнится при удалении (выключении) модуля
в них и нужно работать с событиями, чтобы действия по ним выполнились только 1 раз.
Пример контроллера модуля:
<?php
class ControllerExtensionModuleKsl extends Controller {
    //Сработает только 1 раз при установке
    public function install() {
        $this->load->model('extension/event'); 
        $this->model_extension_event->addEvent('ksl-product-cat', 'catalog/controller/product/category/before', 'extension/ksl/my');
    }

    //Сработает только 1 раз при удалении (удалит действие)
    public function uninstall() {
        $this->load->model('extension/event');
        $this->model_extension_event->deleteEvent('ksl-product-cat');
    }
}
Данное событие сработает перед обработкой текущего URL, выводящего страницу категорий товаров. В результате будет вызван метод index (т.к. не указан другой) контроллера 'extension/ksl/my
Файл catalog\controller\extension\ksl\my.php:
<?php
class ControllerExtensionKslMy extends Controller {    
    public function index(&$route, &$data) {
    $data[0]['city'] = 'Kiev';

    /*
    * То, что возвращает метод вызванный по событию,
    * заменяет собой то, что возвращает метод который это событие вызвал
    */
    //return $value;    
    }
}
В зависимости от типа события, своему, выполняемому методу, можно передать несколько аргументов (указаны в файле загрузчика system\engine\loader.php):
- для контроллера (&$route, &$data, &$output);
- для контроллера, при разборе URL в файле router.php (BEFORE) (&$route, &$data);
- для загрузки модели (&$route);
- при вызове отдельного метода модели (&$route, &$args, &$output);
- для вида (&$route, &$data, &$output);
- для языкового файла (&$route, &$output)

$route - путь к файлу вызвавшему событие (для контроллера и модели это путь к их классу, может так-же содержать метод этого класса, который вызывает событие);
$data - аргументы данного метода (массив);
$output – при установке обработчика события на AFTER (после выполнения стандартного метода), с данным аргументом передается результат выполнения этого метода, с которым можно работать (менять). Если установить значение данной переменной в своем коде, установленном на BEFORE, то до выполнения стандартного класса/метода (который вызвал событие) выполнение кода не дойдет, данное значение будет использовано вместо результата работы стандартного класса/метода.

Аргументы $data и $output могут быть не переданы, если имеют значение NULL. Поэтому при передаче аргументов в свой метод, на всякий случай, можно задать им значение по-умолчанию = null, что бы метод отработал:
public function index(&$route, &$data=null, &$output=null) {}

Данные аргументы передают значения по-ссылке, поэтому можно их изменить.
Если изменить значение $route, то можно сделать перенаправление на другой контроллер/метод или просто вернуть результат его выполнения.
Если изменить элементы массива $data, то метод вызвавший событие получит эти измененные данные так же в качестве аргумента.
Если изменить значение $output, то для controller и view, это:
во-первых, предотвратит выполнение самого класса/метода который вызвал данное событие;
во-вторых, данное значение заменит результат, который должен был вернуть этот класс/метод вызвавший данное событие.

Если пользовательский метод, вызванный по событию, что-то возвращает, то для контроллера, вида и метода модели- возвращаемый результат заменит то, что возвращает класс/метод вызвавший данное событие. Это можно использовать для изменения кода самого метода вызывающего события.

Как я уже писал, события можно вешать и на модели, виды, языковые файлы. Примеры есть в списке стандартных событий OC (файл system\config\catalog.php или system\config\admin.php в массиве $_['action_event']):
'model/extension/shipping/*/before'     => 'event/compatibility/beforeModel',
'view/shipping/*/before'                => 'event/compatibility/view',
…
тут так же можно заметить, что есть возможность использовать * для подмены пути или названия файла.

Для работы с событиями в frontend существует своя модель - ModelExtensionEvent, файл
catalog\model\extension\event.php
Она имеет всего 1 метод, получающий список событий из таблицы event. Поэтому зарегистрировать событие выполнив какой-то код из директории “Catalog” вы не сможете (по-умолчанию).


Коснемся так же первого пункта - списка служебных событий.
Хоть этот способ и не рассчитывался разработчиками для работы с пользовательскими событиями, в принципе, вызов нужного метода контроллера можно установить на событие, указав его в списке событий конфигурационного файла
system\config\catalog.php
или
system\config\admin.php
в массиве
$_['action_event'] = array(…

Данные списки событий добавляются еще на этапе загрузки библиотек OC в файле system\framework.php:
if ($config->has('action_event')) {
    foreach ($config->get('action_event') as $key => $value) {
$event->register($key, new Action($value));
    }
}
и будут выполнены самыми первыми, в таком же порядке как перечислены.

Например так, можно установить обработчик события для своего дополнения:
'controller/product/category/before'     => 'extension/ksl/my',
В результате выполнится метод index() контроллера ControllerExtensionKslMy
Можно указать конкретный метод, например get():
'controller/product/category/before'     => 'extension/ksl/my/get',
Можно использовать звездочку для подмены любого названия:
'controller/product/*/before'     => 'extension/ksl/my',

Ознакомиться с примерами работы с событиями разных типов, можно в моей заметке Примеры работы с событиями разных типов в OpenCart 2.2+.