Как работает ядро Joomla 6 на уровне Приложения (Application)? Сколько видов Приложений на самом деле есть в Joomla и в чём между ними разница? Какой жизненный цикл каждого Приложения Joomla, включая маршрутизацию, диспетчеризацию и события плагинов? На эти и другие вопросы попытается дать ответ эта статья, опирающаяся на кодовую базу Joomla 6.1.0.
Joomla 6 строит запрос вокруг объекта приложения, которое инициализирует окружение, разбирает запрос, передаёт управление компоненту, собирает документ и затем отправляет ответ. Плагины вмешиваются в работу в строго определённых точках этого цикла.
Для Joomla 3 хочу упомянуть статью Дмитрия Рекуна "Общая информация о принципе действия Joomla", которая довольно подробно описывает как Joomla работала раньше.
Что происходит в Joomla при создании HTML-страницы?
Обычный запрос HTML в Joomla начинается в index.php в корне сайта (если речь идёт о пользовательской части).
Сначала входной файл:
- проверяет минимальную версию PHP;
- определяет
_JEXEC, чтобы внутренние файлы Joomla нельзя было выполнять напрямую; - подключает
defines.phpиз корня сайта (если он существует): его используют для предварительного определения или переопределения констант с путями при нестандартной структуре каталогов (Joomla находится за пределами web root); - подключает
includes/defines.php, где задаются основныеJPATH_*-константы; - передаёт управление в
includes/app.php.
В includes/app.php Joomla сохраняет время старта и память, ещё раз поднимает определения путей, проверяет наличие обязательных зависимостей и подключает includes/framework.php. Во framework.php загружается libraries/bootstrap.php, проверяется состояние установки, читается configuration.php, настраиваются ошибки, JDEBUG и базовые параметры окружения.
После этого начинается уже загрузка Приложения Joomla 6:
- берётся DI-контейнер через
Factory::getContainer(); - для сайта настраиваются алиасы веб-сессии;
- из контейнера создаётся
SiteApplication; - приложение записывается в
Factory::$application; - вызывается
$app->execute().
Дальше управление переходит к CMSApplication::execute(): проверяются опасные системные переменные, настраивается журнал, строится карта пространств имён расширений (которая потом кэшируется в autoload_psr4.php), загружаются плагины behaviour и system, вызывается onBeforeExecute, затем приложение выполняет свой основной цикл.
Для HTML-страницы сайта этот цикл кратко выглядит следующим образом:
index.php
-> includes/defines.php
-> includes/app.php
-> includes/framework.php
-> DI-контейнер
-> SiteApplication
-> CMSApplication::execute()
-> onBeforeExecute
-> initialiseApp()
-> onAfterInitialise
-> route()
-> onAfterRoute
-> dispatch()
-> onAfterInitialiseDocument
-> компонент
-> onAfterDispatch
-> render()
-> onBeforeRender
-> сборка HTML-документа и шаблона
-> onAfterRender
-> compress() [если включён gzip]
-> onAfterCompress [если включён gzip]
-> onBeforeRespond
-> respond()
-> onAfterRespond
Для каждого типа приложений Joomla используется своя точка входа. А REST API свой рендер - JSON, вместо HTML - и свой роутинг. У CLI - вывод в консоль. Ниже эти различия разобраны подробнее.
Типы приложений
В Joomla 6 есть несколько типов Приложений, каждое из которых создано для определённого круга задач.
Приложение административной части
Класс: Joomla\CMS\Application\AdministratorApplication.
Отвечает за /administrator, вход администратора, меню панели управления, формы редактирования и внутренние сервисы панели управления.
Приложение публичной части сайта
Класс: Joomla\CMS\Application\SiteApplication.
Создаёт привычные нам страницы сайта: статьи, категории, меню, модули, шаблон сайта, мультиязычность, SEF URL и т.д.
API-приложение
Класс: Joomla\CMS\Application\ApiApplication.
Joomla REST API с точкой входа в /api. Тут согласуется формат ответа, роуты, проверяются права доступа к API.
Консольное приложение
Класс: Joomla\CMS\Application\ConsoleApplication.
CLI интерфейс Joomla, точка входа: cli/joomla.php. Здесь нет HTML, применяется обычно для "тяжёлых задач" и для "тру админов".
Демон
Класс: Joomla\CMS\Application\DaemonApplication.
Это долгоживущее консольное приложение для фоновых процессов. Вы можете запустить его с php как серверный процесс, чтоб он батрачил сутками, выполняя какую-нибудь работу. В обычной разработке расширений встречается редко.
Daemon приложение Joomla наследуется от CliApplication, а не от CMSApplication, поэтому у него свой жизненный цикл. В ядре Joomla готовых примеров самого DaemonApplication нет. Но это хороший повод для экспериментов. CliApplication - устаревший класс, в дальнейшем Daemon скорее всего будет наследоваться от нового ConsoleApplication.
Иерархия классов Application Joomla
Joomla\Application\AbstractApplicationJoomla\Application\AbstractWebApplicationJoomla\CMS\Application\WebApplicationJoomla\CMS\Application\CMSApplicationJoomla\CMS\Application\SiteApplicationJoomla\CMS\Application\AdministratorApplicationJoomla\CMS\Application\ApiApplicationJoomla\CMS\Installation\Application\InstallationApplication
Joomla\Console\ApplicationJoomla\CMS\Application\ConsoleApplicationJoomla\CMS\Installation\Application\CliInstallationApplication
Joomla\CMS\Application\CliApplicationJoomla\CMS\Application\DaemonApplication
Контракты приложений Joomla
Joomla\CMS\Application\CMSApplicationInterfaceJoomla\CMS\Application\CMSApplicationJoomla\CMS\Application\ConsoleApplicationJoomla\CMS\Application\CliApplicationJoomla\CMS\Installation\Application\CliInstallationApplication
Joomla\CMS\Application\CMSWebApplicationInterfaceJoomla\CMS\Application\CMSApplicationJoomla\CMS\Application\SiteApplicationJoomla\CMS\Application\AdministratorApplicationJoomla\CMS\Application\ApiApplicationJoomla\CMS\Installation\Application\InstallationApplication
Обычные веб-клиенты Joomla, то есть site, administrator и api, наследуются через цепочку WebApplication → CMSApplication.
ConsoleApplication реализует CMS-контракт, но не является веб-приложением.
DaemonApplication не наследуется от CMSApplication; он идёт через CliApplication, поэтому у него отдельный жизненный цикл.
Пример порядка выполнения веб-приложения
Ниже описан общий цикл для SiteApplication и AdministratorApplication. Для API он почти такой же, но со своим роутингом.
Подготовка к выполнению приложения
Точка входа (index.php, administrator/index.php или api/index.php) выполняет базовую загрузку:
- определяет константы путей;
- поднимает автозагрузку;
- создаёт DI-контейнер (подключение зависимостей);
- через сервис-провайдеры собирает объект приложения и внедряет в него диспетчер событий, логгер, сессию и другие сервисы;
- вызывает
$app->execute().
С этого момента жизненным циклом управляет уже само приложение.
Общий цикл CMSApplication::execute()
Базовая логика живёт в libraries/src/Application/CMSApplication.php::execute().
Порядок работы такой:
- Проверка опасных системных переменных.
- Настройка журнала.
- Построение карты пространств имён (неймспейсов) расширений.
- Загрузка плагинов группы
behaviour(в ядре Joomla это: плагин обратной совместимости, версионность и поддержка теговcom_tags). - Загрузка плагинов группы
system. - Вызов
onBeforeExecute. - Выполнение прикладной части через
doExecute(). - Если создан документ, вызов
render(). - При включённом GZIP-сжатии в конфиге Joomla - вызов
compress()и затемonAfterCompress. - Вызов
onBeforeRespond. - Отправка ответа через
respond(). - Вызов
onAfterRespond.
Итого:
onBeforeExecute— самая ранняя общая точка для системных плагинов;onBeforeRespond— последняя точка до отправки заголовков и тела ответа;onAfterRespond— заключительный этап после отдачи заголовков и тела ответа браузеру: журналирование, отладка, побочные действия.
Отдельно важно: в WebApplication существует ещё onAfterExecute, но в обычном запросе Joomla CMS он не вызывается, потому что CMSApplication переопределяет execute() своим циклом.
Инициализация
Инициализация проходит внутри CMSApplication::initialiseApp(), а у SiteApplication и AdministratorApplication есть свои дополнения.
На этом этапе Joomla:
- в приложениях сайта и административной части уточняет язык и, при необходимости, группы доступа пользователя;
- создаёт объект языка и загружает его в приложение;
- подключает файлы локализации (ini-файлы языков);
- выбирает редактор для текущего пользователя;
- вызывает
onAfterInitialise.
Важно не смешивать этот этап с более ранней сборкой приложения. Объекты сессии, диспетчера событий, журнала и другие сервисы внедряются ещё при создании приложения в сервис-провайдере libraries/src/Service/Provider/Application.php. Текущий пользователь подтягивается из сессии через WebApplication::afterSessionStart(), а не создаётся внутри initialiseApp().
Именно после onAfterInitialise можно считать, что у приложения уже есть в рабочем состоянии:
- сессия;
- язык;
- пользователь;
- конфигурация;
- диспетчер событий.
Это хороший момент для ранней настройки среды: язык уже выбран, редактор известен, базовые сервисы доступны, но к рендеру компонента Joomla ещё не приступила, поэтому логика, зависящая от url, здесь ещё преждевременна.
Маршрутизация и проверка доступа
Публичная часть
В SiteApplication::route():
- создаётся или берётся текущий
Uri; - роутер разбирает адрес;
- найденные переменные записываются во входные данные приложения;
- вызывается
onAfterRoute; - выполняется проверка доступа к текущему
Itemid(id пункта меню) черезauthorise().
После этого Joomla уже знает, какой компонент, представление, шаблон, макет и пункт меню связаны с запросом, и может сразу отреагировать на запрет доступа.
Административная часть
В AdministratorApplication::route() логика проще:
- при необходимости выполняется перевод на HTTPS;
- проверяется состояние страниц многофакторной проверки;
- вызывается
onAfterRoute.
В административной части маршрутизация в меньшей степени завязана на человекопонятные адреса, но событие то же самое.
API
В ApiApplication::route() порядок другой:
- загружаются плагины группы
webservices; - вызывается
onBeforeApiRoute; - маршрутизатор API подбирает маршрут;
- Joomla согласует формат ответа по заголовку
Accept; - переменные маршрута записываются во входные данные;
- вызывается
onAfterApiRoute; - при необходимости выполняется вход пользователя для закрытых маршрутов.
Для API важен именно onBeforeApiRoute: через него можно добавить или поправить роуты REST API Joomla до собственно роутинга.
Отдельно: как работает DaemonApplication
На демон-процессы обычно вешают задачи, которые должны жить долго, работать в фоне и не зависеть от HTTP-запроса или разового запуска CLI-команды.
Типичные задачи:
- постоянная обработка очереди: email, SMS, webhooks, экспорт/импорт, генерация файлов;
- воркеры фоновых заданий, где нужно быстро подхватывать новые задачи без запуска PHP с нуля каждый раз;
- слушатели внешних событий: сокеты, очереди сообщений, брокеры, long polling, системные события;
- периодическая фоновая работа с собственным циклом: очистка, синхронизация, пересчёт агрегатов, мониторинг;
- процессы, которым нужно держать состояние в памяти: кэш подключений, открытые соединения, подготовленные ресурсы;
- интеграции с внешними сервисами, где нужно постоянно принимать или отправлять события;
- сервисные процессы, где важны PID-файл, сигналы SIGTERM/SIGHUP, мягкая остановка и перезапуск.
Для Joomla-практики чаще всего это были бы не обычные задачи расширений, а инфраструктурные вещи: воркер очереди, постоянный импорт, синхронизация с CRM/маркетплейсом, обработчик вебхуков из очереди, индексация, массовая рассылка, мониторинг состояния. Если задача может спокойно запускаться раз в минуту по cron и завершаться, демон обычно не нужен. Демон оправдан, когда процесс должен быть постоянно запущен, быстро реагировать, держать ресурсы открытыми или управляться через сигналы и PID.
Поэтому DaemonApplication живёт по другому сценарию, нежели сайт, админка или API. Это не разовый запрос-ответ, а долгоживущий процесс, который:
- стартует из CLI;
- при необходимости уходит в фон;
- пишет PID-файл;
- подключает обработчики Unix-сигналов;
- бесконечно гоняет
doExecute()в цикле.
Требования к окружению для демона на базе Joomla
Конструктор DaemonApplication проверяет наличие расширения PCNTL и наличие функций POSIX. Если их нет, приложение падает ещё в __construct().
Какие параметры он готовит?
Метод loadConfiguration() нормализует и подготавливает служебные параметры демона:
author_nameauthor_emailapplication_nameapplication_descriptionapplication_executableapplication_directoryapplication_pid_fileapplication_uidapplication_gidapplication_require_identitymax_execution_timemax_memory_limit
По умолчанию DaemonApplication строит PID-файл в абсолютном системном Unix-каталоге /tmp, по шаблону /tmp/<application_name>/<application_name>.pid. Это не временная папка конкретной Joomla-установки. Если очень нужно, путь можно переопределить через конфигурационный параметр application_pid_file.
Как выглядит жизненный цикл демона на Joomla?
Порядок в DaemonApplication::execute() такой:
onBeforeExecute- включение сборки мусора
gc_enable() - попытка перейти в режим демона через
daemonize() - если переход удался:
- включение
declare(ticks=1)для мониторинга сигналов - бесконечный цикл:
gc()usleep(1000)doExecute()
- включение
onAfterExecuteвызывается только если запуск не удался или если выполнение вообще вышло из цикла
На заметку:
для
DaemonApplicationсобытиеonAfterExecuteсуществует реально, но при нормальной бесконечной работе демона до него обычно не доходят.
Это важное отличие Joomla Daemon от веб-цикла Joomla:
- у демона нет
onAfterInitialise,onAfterRoute,onAfterDispatch,onBeforeRender,onAfterRender,onBeforeRespond,onAfterRespond; - у него есть только ранний
onBeforeExecute, позднийonAfterExecuteи специфическиеonForkиonReceiveSignal.
Что делает daemonize()
Метод daemonize() последовательно:
- проверяет через
isActive(), не запущен ли уже такой демон; - сбрасывает внутренние флаги процесса;
- если не передан флаг
-f, вызываетdetach()и уходит в фон; - если
-fпередан, остаётся на переднем плане; - пишет PID-файл через
writeProcessIdFile(); - пытается сменить пользователя и группу через
changeIdentity(); - подключает обработчики сигналов через
setupSignalHandlers(); - меняет рабочую директорию на
application_directory.
Что значит флаг -f
Этот флаг не позволяет уйти демону в фоновый процесс. Если у CLI-ввода есть флаг -f, демон продолжает работать с консолью и выводить информацию (если предполагается) в неё. Это удобно для отладки, запуска под внешним диспетчером процессов, различных видов тестирования.
Как работает fork
Уход в фон делается в detach(), а само разделение процесса — в fork().
Ключевая деталь ядра:
- вызывается
pcntl_fork(); - и в родительском, и в дочернем процессе вызывается
postFork(); postFork()отправляет событиеonFork.
То есть:
onForkсрабатывает после разделения процесса и проходит в обоих потоках выполнения, родительском и дочернем.
Это хорошая точка для post-fork инициализации:
- переподключить сокеты;
- заново открыть файловые дескрипторы;
- переинициализировать соединения, которые нельзя безопасно делить после
fork.
Как работает onReceiveSignal
Статический обработчик DaemonApplication::signal($signal):
- пишет сигнал в журнал;
- берёт текущий экземпляр приложения из
static::$instance; - отправляет событие
onReceiveSignal; - только потом выполняет встроенную реакцию ядра.
Это значит, что обработчик плагина получает сигнал до штатной реакции класса.
Какие сигналы обрабатываются встроенно
Ядро по умолчанию вешает обработчик на длинный список POSIX-сигналов из static::$signals.
Практически важные встроенные ветки такие:
SIGINT,SIGTERM— мягкое завершение черезshutdown();SIGHUP— завершение с последующим перезапуском черезshutdown(true);SIGCHLD,SIGCLD— уборка завершившихся дочерних процессов черезpcntlWait().
Для остальных сигналов ядро только отправляет onReceiveSignal, а специального встроенного поведения не добавляет.
Как он определяет, что демон уже работает?
Метод isActive():
- читает PID-файл;
- проверяет, что PID корректен;
- делает
posix_kill($pid, 0)как проверку живого процесса; - если процесс не отвечает, удаляет устаревший PID-файл.
То есть здесь используется именно проверка существования процесса, а не простое наличие файла.
Что делает shutdown()
shutdown():
- ставит флаг завершения;
- если демон ещё не был полностью поднят, просто завершает текущий процесс;
- если это главный процесс, читает PID из файла;
- удаляет PID-файл;
- при
restart=trueперезапускает ту же команду; - иначе завершает процесс.
Отдельная хронология для DaemonApplication
Если собрать всё вместе, жизненный цикл выглядит так:
Запуск CLI
-> new DaemonApplication(...)
-> проверка PCNTL и POSIX
-> loadConfiguration()
-> execute()
-> onBeforeExecute
-> daemonize()
-> isActive()
-> detach() / foreground mode
-> fork()
-> onFork
-> writeProcessIdFile()
-> changeIdentity()
-> setupSignalHandlers()
-> declare(ticks=1)
-> while (true)
-> gc()
-> usleep(1000)
-> doExecute()
-> при поступлении сигнала
-> onReceiveSignal
-> shutdown()/restart()/reap children
-> onAfterExecute
Важные детали
Для демона Joomla ядро даёт всего четыре действительно системных точки:
onBeforeExecuteonAfterExecuteonForkonReceiveSignal
Но именно для фоновых процессов это и есть главные точки расширения.
При этом Daemon приложение Joomla само не импортирует какие-либо группы плагинов по умолчанию (на данный момент -Joomla 6.1). Поскольку это в любом случае довольно специфичный сценарий - вы импортируете нужные группы плагинов самостоятельно. Также помните, что вы можете здесь использовать и классы фреймворка Joomla и библиотек, входящих в состав ядра Joomla.
Диспетчеризация
После маршрутизации приложение передаёт управление компоненту.
Публичная и административная часть
В SiteApplication::dispatch() и AdministratorApplication::dispatch() порядок такой:
- создаётся документ;
- настраиваются метаданные, шаблон и реестры ресурсов;
- вызывается
onAfterInitialiseDocument; - компонент отрисовывается через
ComponentHelper::renderComponent(); - результат кладётся в буфер документа;
- вызывается
onAfterDispatch.
onAfterInitialiseDocument — это точка до выполнения компонента: документ уже существует, но компонент ещё не отработал. Здесь удобно править тип документа, метаданные, реестры стилей и сценариев. Помните, что это событие появилось только в Joomla 5.0.0.
onAfterDispatch — это точка после выполнения компонента. Здесь уже можно работать с буферами документа и итогом работы компонента.
API
В ApiApplication::dispatch() смысл тот же:
- создаётся документ;
- вызывается
onAfterInitialiseDocument; - вызывается диспетчер компонента веб-служб.
- вызывается
onAfterDispatch.
Сборка документа
Для фронтенда (SiteApplication) и админки это обычно HTML. Хотя нужно упомянуть, что внутри компонента за формат рендера отвечает View и View может отдавать и txt, и json, и XML - что угодно, не только HTML. Но рассмотрим стандартный путь, когда сборка ответа в HTML делает CMSApplication::render().
Порядок такой:
- заполняются параметры шаблона;
- выполняется
$this->document->parse(...); - вызывается
onBeforeRender; - документ формирует итоговую строку ответа;
- строка записывается в тело ответа через
setBody(); - вызывается
onAfterRender.
Здесь есть важная деталь: во время сборки HTML-заголовка libraries/src/Document/Renderer/Html/MetasRenderer.php вызывает onBeforeCompileHead.
То есть:
onBeforeRender— общий поздний этап перед окончательной сборкой страницы;onBeforeCompileHead— специальная точка именно для<head>;onAfterRender— этап, когда вся HTML-строка уже собрана и её можно переписать целиком.
Вывод ответа
После сборки документа CMSApplication::execute() завершает запрос:
- при включённом сжатии вызывает
onAfterCompress; - вызывает
onBeforeRespond; - отправляет заголовки и тело;
- вызывает
onAfterRespond.
onAfterCompress важно понимать буквально. В CMSApplication::execute() он вызывается только после $this->compress() и только если включён параметр gzip, при этом PHP не использует zlib.output_compression и текущий output_handler не равен ob_gzhandler. Это событие относится к CMS-циклу SiteApplication, AdministratorApplication и ApiApplication; у базового WebApplication::execute() сжатие тоже есть, но события onAfterCompress после него нет.
На практике:
onBeforeRespond— последняя безопасная точка для заголовков и тела ответа;onAfterRespond— не место для изменения ответа, а место для побочных действий после того, как HTML отдан в браузер.
Схема цикла выполнения
Публичная часть
index.php
-> SiteApplication::execute()
-> onBeforeExecute
-> initialiseApp()
-> onAfterInitialise
-> route()
-> onAfterRoute
-> dispatch()
-> onAfterInitialiseDocument
-> renderComponent()
-> onAfterDispatch
-> render()
-> onBeforeRender
-> onBeforeCompileHead
-> onAfterRender
-> compress() [если включён gzip]
-> onAfterCompress [если включён gzip]
-> onBeforeRespond
-> respond()
-> onAfterRespond
Админка Joomla
administrator/index.php
-> AdministratorApplication::execute()
-> onBeforeExecute
-> initialiseApp()
-> onAfterInitialise
-> route()
-> onAfterRoute
-> dispatch()
-> onAfterInitialiseDocument
-> renderComponent()
-> onAfterDispatch
-> render()
-> onBeforeRender
-> onBeforeCompileHead
-> onAfterRender
-> compress() [если включён gzip]
-> onAfterCompress [если включён gzip]
-> onBeforeRespond
-> respond()
-> onAfterRespond
REST API Joomla
api/index.php
-> ApiApplication::execute()
-> onBeforeExecute
-> initialiseApp()
-> onAfterInitialise
-> route()
-> onBeforeApiRoute
-> parseApiRoute()
-> onAfterApiRoute
-> dispatch()
-> onAfterInitialiseDocument
-> component dispatcher
-> onAfterDispatch
-> render()
-> compress() [если включён gzip]
-> onAfterCompress [если включён gzip]
-> onBeforeRespond
-> respond()
-> onAfterRespond
Полная хронологическая карта триггеров Joomla во время запроса
Ниже уже не укрупнённая схема, а именно хронология. Важно помнить две вещи:
- не все ветви выполняются в каждом запросе;
- часть событий внутри формирования HTML зависит от конкретного шаблона и от того, в каком месте шаблон вызывает
jdoc:include.
Самый первый триггер в жизненном цикле Joomla
Ещё до onBeforeExecute ядро успевает загрузить группы behaviour и system.
Для каждого плагина, который загружается через PluginHelper::importPlugin(), перед созданием экземпляра и сразу после него проходят:
onBeforeExtensionBootonAfterExtensionBoot
Это касается:
- плагинов
behaviourиsystemв начале запроса; - плагинов
webservicesв API; - плагинов
content,user,installer,extension,finder,privacy,quickicon,sampledataи других в более поздних ветках; - компонентов и модулей, когда они загружаются через
bootComponent()иbootModule().
Поэтому onBeforeExtensionBoot и onAfterExtensionBoot — самые ранние реально доступные точки вмешательства в жизненный цикл расширения.
Полный порядок для обычной HTML-страницы сайта
Ниже порядок для типового запроса фронтенда с HTML-документом.
Базовая последовательность
- Для каждого загружаемого плагина группы
behaviour:onBeforeExtensionBoot->onAfterExtensionBoot - Для каждого загружаемого плагина группы
system:onBeforeExtensionBoot->onAfterExtensionBoot onBeforeExecuteonAfterInitialiseonAfterRouteonAfterInitialiseDocument- Для загружаемого компонента:
onBeforeExtensionBoot->onAfterExtensionBoot - Внутренняя логика компонента
onAfterDispatchonBeforeRender- Внутренняя сборка документа и шаблона
onAfterRender- При включённом gzip и совместимых настройках PHP:
compress()->onAfterCompress onBeforeRespondonAfterRespond
Что может произойти внутри пункта «внутренняя логика компонента»
Если компонент использует HTML-представление на базе Joomla\CMS\MVC\View\HtmlView, то внутри его display() дополнительно идут:
onBeforeDisplay- загрузка шаблона представления
onAfterDisplay
Но и это ещё не всё. Многие компоненты вызывают свои события до parent::display().
Например, у com_content в представлении статьи порядок такой:
- для каждого плагина группы
content:onBeforeExtensionBoot->onAfterExtensionBoot onContentPrepareonContentAfterTitleonContentBeforeDisplayonContentAfterDisplayonBeforeDisplayonAfterDisplay
У категорий и тегов логика похожая, но события onContentPrepare, onContentAfterTitle, onContentBeforeDisplay, onContentAfterDisplay могут вызываться по каждому элементу списка, а не один раз на страницу.
Что может произойти внутри пункта «внутренняя сборка документа и шаблона»
Вот здесь порядок уже зависит от шаблона.
Если шаблон первым делом выводит <head>, то раньше всего сработает onBeforeCompileHead. Если шаблон затем выводит позиции модулей, то для первой же позиции Joomla запустит такую ветку:
onPrepareModuleListonAfterModuleListonAfterCleanModuleList- для каждого загружаемого модуля:
onBeforeExtensionBoot->onAfterExtensionBoot - для каждого модуля:
onRenderModule - внутренняя логика модуля
- для каждого модуля:
onAfterRenderModule - после завершения всей позиции:
onAfterRenderModules
При этом внутренняя логика конкретного модуля тоже может загрузить свои плагины и вызвать свои события. Пример из ядра: mod_articles_news внутри вывода модуля вызывает:
onContentPrepareonContentAfterTitleonContentBeforeDisplayonContentAfterDisplay
То есть в одной HTML-странице события content могут сработать:
- в компоненте;
- в одном или нескольких модулях;
- в произвольном порядке относительно позиций модулей, потому что это уже зависит от шаблона.
Полный порядок триггеров плагинов для HTML-страницы в админке Joomla
Базовый порядок почти тот же:
- для плагинов
behaviour:onBeforeExtensionBoot->onAfterExtensionBoot - для плагинов
system:onBeforeExtensionBoot->onAfterExtensionBoot onBeforeExecuteonAfterInitialiseonAfterRouteonAfterInitialiseDocument- для компонента:
onBeforeExtensionBoot->onAfterExtensionBoot - внутренняя логика компонента и представления
onAfterDispatchonBeforeRender- события головы страницы, модулей и меню в зависимости от шаблона
onAfterRender- при включённом gzip и совместимых настройках PHP:
compress()->onAfterCompress onBeforeRespondonAfterRespond
Но в административной части есть ещё несколько условных точек, которые часто забывают.
Если строится меню для модулей меню в панели администратора Joomla, то во время обработки узлов меню могут вызываться onPreprocessMenuItems. Причём это событие может пройти несколько раз, по уровням дерева. Контексты в ядре com_menus.administrator.module и administrator.module.mod_submenu.
Полный порядок для API-запроса
У API жизненный цикл другой, и это важно.
Базовая последовательность
- Для плагинов
behaviour:onBeforeExtensionBoot->onAfterExtensionBoot - Для плагинов
system:onBeforeExtensionBoot->onAfterExtensionBoot onBeforeExecuteonAfterInitialise- Для плагинов
webservices:onBeforeExtensionBoot->onAfterExtensionBoot onBeforeApiRoute- разбор маршрута API
onAfterApiRouteonAfterInitialiseDocument- Для компонента API:
onBeforeExtensionBoot->onAfterExtensionBoot - логика контроллера, модели и представления API
onAfterDispatchrender()через переопределённыйApiApplication::render()- при включённом gzip и совместимых настройках PHP:
compress()->onAfterCompress onBeforeRespondonAfterRespond
Чего здесь обычно нет
В обычном API-запросе через ApiApplication нет общего HTML-цикла:
- нет
onBeforeRenderизCMSApplication::render(); - нет
onAfterRender; - нет
onBeforeCompileHead; - нет модульных событий формирования вывода.
Причина простая: ApiApplication переопределяет render() и не использует HTML-сборку документа.
Какие API-события добавляются внутри представления и сериализации
Если ответ строится через JsonApiView и сериализатор Joomla, дополнительно могут идти:
onApiGetFieldsonGetApiAttributesonGetApiRelation
Это уже не системная часть жизненного цикла приложения, а события слоя сериализации API.
Хронология при открытии и отправке формы Joomla
Открытие формы редактирования
Типовой порядок:
- общий цикл приложения до
onAfterRoute - загрузка компонента
onContentPrepareDataonContentPrepareFormonBeforeDisplayonAfterDisplayonAfterDispatch- дальше общая HTML-сборка страницы
Отправка формы и сохранение записи
Типовой порядок:
- общий цикл приложения до маршрута и диспетчеризации
onContentNormaliseRequestDataonContentBeforeValidateDataonTableBeforeLoadиonTableAfterLoad, если запись не новаяonTableBeforeBindonTableAfterBindonTableCheckonContentBeforeSaveonTableBeforeStoreonTableAfterStoreonContentCleanCacheonContentAfterSave- обычное завершение запроса через
onBeforeRespondиonAfterRespond
Если это удаление:
onTableBeforeLoadonTableAfterLoadonContentBeforeDeleteonTableBeforeDeleteonTableAfterDeleteonContentAfterDeleteonContentCleanCache
Если это смена состояния (опубликовано, не опубликовано, в архиве, в корзине):
onContentBeforeChangeStateonTableBeforePublishonTableAfterPublishonContentChangeStateonContentCleanCache
Хронология триггеров плагинов Joomla для входа, выхода и восстановления доступа пользователя
Эти события не являются обязательной частью каждого запроса, но если запрос делает именно это действие, порядок будет таким.
Вход пользователя
- для плагинов
user:onBeforeExtensionBoot->onAfterExtensionBoot onUserAuthenticateonUserAuthorisation- при отказе:
onUserAuthorisationFailure - при продолжении входа:
onUserLogin - при успехе:
onUserAfterLogin - при неудаче:
onUserLoginFailure
Выход пользователя
- для плагинов
user:onBeforeExtensionBoot->onAfterExtensionBoot onUserLogout- при успехе:
onUserAfterLogout - при неудаче:
onUserLogoutFailure
Напоминание логина пользователя
onUserAfterRemind - Вызывается при отправке формы "забыли логин" в com_users Joomla.
Запрос на сброс пароля
События вызываются при отправке формы "забыли пароль" в com_users Joomla:
onUserBeforeResetRequest- сохранение пользователя (там свои события
onUserAfterSaveи т.д.) onUserAfterResetRequest
Завершение сброса пароля
onUserBeforeResetComplete- сохранение пользователя
onUserAfterResetComplete
Хронология установки расширения Joomla через штатный установщик
Если запрос идёт через стандартный установщик Joomla, то внутри него порядок событий такой:
- для плагинов группы
installer:onBeforeExtensionBoot->onAfterExtensionBoot onInstallerBeforeInstallation- при скачивании пакета по url адресу с сайта разработчика:
onInstallerBeforePackageDownload onInstallerBeforeInstaller- для плагинов
extension:onBeforeExtensionBoot->onAfterExtensionBoot onExtensionBeforeInstall- установка расширения
onExtensionAfterInstallonInstallerAfterInstaller
Для обновления:
onInstallerBeforePackageDownload, если пакет нужно скачатьonExtensionBeforeUpdate- обновление
onExtensionAfterUpdate
Для удаления:
onExtensionBeforeUninstall- удаление
onExtensionAfterUninstall
Карта уровней событий Joomla
Не все события Joomla находятся в верхнем цикле Application. Часть событий возникает при загрузке расширений, часть — внутри компонента, формы, модели, таблицы или шаблона. Поэтому точку внедрения в ход работы Joomla лучше выбирать не по названию события, а по уровню, на котором уже есть нужные данные.
| Уровень | Где возникает | Примеры событий | Когда туда идти |
|---|---|---|---|
| Приложение | Общий цикл CMSApplication, SiteApplication, AdministratorApplication, ApiApplication |
onBeforeExecute, onAfterInitialise, onAfterRoute, onAfterDispatch, onBeforeRender, onAfterRender, onBeforeRespond, onAfterRespond |
Когда задача относится ко всему запросу: окружение, маршрут, документ, заголовки, финальное тело ответа |
| Загрузка расширений | Когда Joomla создаёт плагин, компонент или модуль через механизм загрузки расширений | onBeforeExtensionBoot, onAfterExtensionBoot |
Когда нужно отследить или повлиять на момент создания расширения, а не на его бизнес-логику |
| Компонент и представление | Внутри MVC-компонента и HTML-представления | onBeforeDisplay, onAfterDisplay, а также собственные события компонента |
Когда данные уже выбраны компонентом, но страница ещё не собрана целиком |
| Контент | В com_content, модулях материалов и вспомогательной обработке текста. Эти же события могут вызываться и в других компонента, следующих "Joomla way" в разработке. |
onContentPrepare, onContentAfterTitle, onContentBeforeDisplay, onContentAfterDisplay |
Когда нужно менять текст, вставки вокруг материала или результат вывода статьи |
| Формы и данные формы | При построении формы и перед валидацией отправленных данных | onContentPrepareData, onContentPrepareForm, onContentNormaliseRequestData, onContentBeforeValidateData |
Когда нужно добавить поля, изменить XML-форму или подготовить сырые данные до проверки |
| Модель сохранения | В AdminModel и наследниках перед записью, после записи, при удалении и смене состояния |
onContentBeforeSave, onContentAfterSave, onContentBeforeDelete, onContentAfterDelete, onContentBeforeChangeState, onContentChangeState |
Когда нужно применить бизнес-правило компонента или отменить действие на уровне модели |
| Таблица | В Table и Nested прямо перед низкоуровневой операцией с записью |
onTableBeforeBind, onTableAfterBind, onTableCheck, onTableBeforeStore, onTableAfterStore, onTableBeforeDelete, onTableAfterDelete |
Когда нужна самая близкая к базе точка и уже есть объект таблицы |
| Модули | При сборке списка модулей, выводе отдельного модуля и позиции | onPrepareModuleList, onAfterModuleList, onAfterCleanModuleList, onRenderModule, onAfterRenderModule, onAfterRenderModules |
Когда нужно подменить список модулей, атрибуты модуля или HTML всей позиции |
| Пользователи и вход | В аутентификации, авторизации входа, сохранении и удалении пользователя | onUserAuthenticate, onUserAuthorisation, onUserLogin, onUserAfterLogin, onUserBeforeSave, onUserAfterSave |
Когда задача относится к подлинности, разрешению входа, пользовательской сессии или данным пользователя |
| Установка и обновление | В com_installer, InstallerHelper и Installer |
onInstallerBeforePackageDownload, onInstallerBeforeInstallation, onExtensionBeforeInstall, onExtensionAfterInstall, onExtensionBeforeUpdate, onExtensionAfterUpdate |
Когда нужно вмешаться в скачивание, установку, обновление или удаление расширения |
| Административное меню | В отдельных экранах и модулях административной части | onPreprocessMenuItems, onBeforeRenderMenuItems, onAfterGetMenuTypeOptions |
Когда задача касается дерева меню, списка пунктов меню или выбора типа пункта меню |
| API-сериализация | Внутри API-представления и сериализатора | onApiGetFields, onGetApiAttributes, onGetApiRelation |
Когда роут API уже обработан и нужно изменить поля, атрибуты или связи JSON:API-ответа |
| Аварийная ветка - обработка ошибок | При исключении внутри CMSApplication::execute() |
onError |
Когда нужна реакция на ошибку до передачи исключения обработчику |
Технически часть этих событий вызывается из трейтов или базовых классов, но для выбора точки вмешательства полезнее мыслить уровнем жизненного цикла. Например, onBeforeExtensionBoot относится к загрузке расширения, onTableBeforeStore — к уровню таблицы, а onContentBeforeSave — к уровню модели и бизнес-правил компонента.
Аварийная ветка
Если в основном цикле до отправки ответа вылетает исключение, CMSApplication::execute() вызывает onError. После этого управление передаётся обработчику исключений. Это не штатная ветка, но это тоже часть хронологии запроса.
Как в Joomla 6 устроены триггеры плагинов
Как плагины загружаются
Базовый механизм находится в libraries/src/Plugin/PluginHelper.php.
Когда ядро вызывает PluginHelper::importPlugin('system') или PluginHelper::importPlugin('content'), происходит следующее:
- из таблицы
#__extensionsвыбираются только включённые плагины; - выборка сортируется по полю
ordering; - плагин загружается и создаётся;
- его обработчики регистрируются в диспетчере событий.
Отсюда следует первое правило порядка:
если приоритет слушателей не задан явно, раньше будет вызван тот плагин, который раньше загружен, а загружается он по
ordering.
Как задаётся порядок вызова
В Joomla 6 работают сразу два слоя порядка.
Порядок загрузки плагинов группы
Он определяется ordering в таблице расширений.
Приоритет слушателя
Если плагин реализует SubscriberInterface, он может вернуть карту событий из getSubscribedEvents() и указать приоритет:
<?php
namespace Acme\Plugin\System\Example\Extension;
use Joomla\Event\Priority;
use Joomla\Event\SubscriberInterface;
final class Example implements SubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'onAfterRoute' => ['onAfterRoute', Priority::HIGH],
'onAfterRender' => ['onAfterRender', Priority::LOW],
];
}
}
Что это значит для разработчика:
Priority::HIGH— выполнить раньше большинства;Priority::NORMAL— обычный порядок;Priority::LOW— выполнить ближе к концу.
Если у нескольких обработчиков одинаковый приоритет, сохранится порядок их регистрации, то есть учитывается обычно снова ordering.
Какой объект приходит в обработчик
В Joomla 6 ядро почти везде передаёт не россыпь позиционных аргументов, а объект события $event.
Пример:
<?php
namespace Acme\Plugin\System\Example\Extension;
use Joomla\CMS\Event\Application\AfterRouteEvent;
final class Example
{
public function onAfterRoute(AfterRouteEvent $event): void
{
$app = $event->getApplication();
}
}
Старый стиль с методами вроде onContentPrepare($context, &$item, &$params, $page = 0) ещё поддерживается через прослойку совместимости, но это уже устаревающий путь. Поэтому разработчикам рекомендуется провести рефакторинг своих плагинов и поднимать им потихоньку системные требования.
Что в аргументах можно менять
Это самая важная часть для разработчика плагинов.
1. Можно менять переданный объект
Если событие несёт объект, Joomla потом использует этот же объект дальше по цепочке.
Чаще всего это:
- приложение;
- документ;
- форма;
- таблица;
- роутер;
- объект материала / контакта / товара и т.д.;
- объект ответа аутентификации.
Именно поэтому формально неизменяемое событие всё равно позволяет менять поведение ядра: вы меняете не сам контейнер события, а состояние объекта внутри него.
2. Для массивов и строк ищите update*()
Во многих типизированных событиях есть специальные методы:
updateData();updateAttributes();updateContent();updateModules();updateUrl();updateHeaders().
Если они есть, пользоваться нужно именно ими.
3. Для собираемого результата используется result
Многие события накапливают результат не прямой заменой аргумента, а через список значений.
Типичные случаи:
onContentAfterTitle,onContentBeforeDisplay,onContentAfterDisplayсобирают строки;onContentBeforeSave,onUserLogin,onUserLogoutсобирают логические значения;- наличие хотя бы одного
falseчасто останавливает дальнейшее действие ядра.
4. Не рассчитывайте на произвольный setArgument()
Большая часть событий ядра наследуется от AbstractImmutableEvent. Это значит:
- произвольная замена аргументов запрещена;
- менять нужно объект внутри аргумента или использовать предусмотренный
update*(); - старые трюки с прямой подменой аргументов надо считать устаревшими.
Быстрое правило выбора точки вмешательства
Если нужно:
| Задача | Нужная точка |
|---|---|
| Подправить окружение до основного цикла | onBeforeExecute |
| Реагировать на уже разобранный адрес | onAfterRoute |
| Добавить стили, сценарии, метаданные | onAfterInitialiseDocument, onBeforeCompileHead, onBeforeRender |
| Переписать готовый HTML целиком | onAfterRender |
| Изменить форму редактирования | onContentPrepareForm |
| Изменить данные формы до проверки | onContentNormaliseRequestData, onContentBeforeValidateData |
| Проверить или отменить сохранение | onContentBeforeSave, onUserBeforeSave |
| Вмешаться на самом низком уровне записи в таблицу | onTableBeforeBind, onTableCheck, onTableBeforeStore |
| Добавить вставки вокруг текста материала | onContentAfterTitle, onContentBeforeDisplay, onContentAfterDisplay |
| Изменить список модулей позиции | onPrepareModuleList, onAfterModuleList, onAfterCleanModuleList |
Основные группы событий и реальные точки вызова
Системные события приложения
| Событие | Где вызывается | Когда | Аргументы | Что реально менять | Зачем применять |
|---|---|---|---|---|---|
onBeforeExecute |
CMSApplication::execute() |
После загрузки behaviour и system, до основного цикла |
subject = приложение, в CMS ещё container |
Состояние приложения, сервисы контейнера, ранние проверки | Очень ранняя инициализация |
onAfterInitialise |
CMSApplication::initialiseApp() |
После выбора языка, загрузки библиотечных языковых файлов и выбора редактора | subject = приложение |
Состояние приложения, язык, редактор, пользовательские состояния | Ранняя настройка среды |
onAfterRoute |
SiteApplication::route(), AdministratorApplication::route() |
После разбора маршрута; в site ещё до проверки доступа к Itemid |
subject = приложение |
Входные данные, перенаправления, языковую логику, кеширование | Логика, зависящая от маршрута |
onBeforeApiRoute |
ApiApplication::route() |
До разбора маршрута API | subject = приложение, router |
Сам маршрутизатор API | Регистрация и правка маршрутов API |
onAfterApiRoute |
ApiApplication::route() |
После разбора маршрута API, но до API-логина для непубличного маршрута | subject = приложение |
Входные данные и подготовку перед API-логином; саму проверку доступа запускает ApiApplication::route() после события |
Поздняя подготовка API-запроса |
onAfterInitialiseDocument |
SiteApplication::dispatch(), AdministratorApplication::dispatch(), ApiApplication::dispatch() |
Документ уже создан, компонент ещё не отработал | subject = приложение, document |
Документ, метаданные, ресурсы, тип документа | Подготовка документа |
onAfterDispatch |
dispatch() в приложениях |
Компонент уже дал вывод, буферы заполнены | subject = приложение |
Буферы документа, состояние документа | Постпроцессор компонента |
onBeforeRender |
CMSApplication::render() |
Перед окончательной сборкой документа | subject = приложение |
Документ, буферы, активы | Последняя общая точка до сборки HTML в site и administrator |
onBeforeCompileHead |
Document\\Renderer\\Html\\MetasRenderer::render() |
Во время сборки <head> |
subject = приложение, document |
Только содержимое документа и <head> |
Подключение ресурсов и метаданных |
onAfterRender |
CMSApplication::render() |
После сборки итоговой строки ответа | subject = приложение |
Тело ответа через объект приложения | Полная перепись HTML в site и administrator |
onAfterCompress |
CMSApplication::execute() |
После compress(), только если включён gzip и PHP не сжимает вывод сам |
subject = приложение |
Тело уже прошло сжатие; править его здесь обычно не нужно | Диагностика, служебные действия |
onError |
CMSApplication::execute() |
Только при исключении | subject = исключение, application = приложение |
Обычно журналирование, подмена исключения, аварийная реакция | Обработка ошибок |
onBeforeRespond |
CMSApplication::execute() |
Прямо перед отправкой ответа | subject = приложение |
Заголовки, cookies, тело ответа | Последняя точка перед отправкой |
onAfterRespond |
CMSApplication::execute() |
Сразу после отправки | subject = приложение |
Ответ клиенту уже не изменить | Журналирование, отладка |
События вывода материала
Главная точка для статьи: components/com_content/src/View/Article/HtmlView.php::display().
Порядок там такой:
onContentPrepareonContentAfterTitleonContentBeforeDisplay- вывод основного содержимого материала
onContentAfterDisplay
Общий набор аргументов:
context— строка видаcom_content.article;subject— объект материала;params— параметры;page— номер страницы или смещение.
| Событие | Где вызывается | Что можно менять | Для чего нужно |
|---|---|---|---|
onContentPrepare |
com_content и HTML\\Helpers\\Content::prepare() |
Сам объект материала, прежде всего text |
Подстановка меток, замена шорткодов вида {SHORT_CODE}, автосвязи, фильтрация текста |
onContentAfterTitle |
com_content |
Возвращаемые строки через result |
Вставка блока сразу после заголовка |
onContentBeforeDisplay |
com_content |
Возвращаемые строки через result |
Вставка перед основным текстом |
onContentAfterDisplay |
com_content |
Возвращаемые строки через result |
Вставка после текста |
Например, если вам нужно:
- изменить сам текст статьи, используйте
onContentPrepare; - добавить отдельный HTML-блок до или после текста, используйте
result-события; - трогать поля записи, категории, изображения и другие свойства материала, меняйте объект
subject.
События подготовки формы и данных
Эта группа отвечает не за показ страницы как таковой, а за подготовку формы редактирования и начальных данных.
| Событие | Где вызывается | Аргументы | Что можно менять | Для чего нужно |
|---|---|---|---|---|
onContentPrepareData |
FormBehaviorTrait::preprocessData(), обычно из loadFormData() конкретной модели |
context, data, служебный subject |
data через updateData() или по ссылке совместимости |
Предварительно заполнить или поправить исходные данные формы |
onContentPrepareForm |
FormBehaviorTrait::preprocessForm() |
subject = Form, data |
Сам объект формы | Добавить, убрать, скрыть, переименовать поля и вкладки |
onContentNormaliseRequestData |
FormController::normalizeRequestData() |
context, data, subject = Form |
Объект data |
Нормализация сырых данных формы до проверки |
onContentBeforeValidateData |
FormModel::validate() |
subject = Form, data |
data через updateData() |
Последняя правка перед фильтрацией и правилами |
Важно понимать, что при открытии формы обычно сначала компонент вызывает onContentPrepareData, потом строит XML-форму и вызывает onContentPrepareForm. А при отправке формы в FormController::save() порядок такой:
- строится объект формы;
- вызывается
onContentNormaliseRequestData; - вызывается
onContentBeforeValidateData; - форма фильтрует и проверяет данные;
- затем уже вызывается сохранение модели.
Это очень полезное разделение:
onContentNormaliseRequestData— для приведения сырых данных к нужному виду;onContentBeforeValidateData— для правки данных прямо перед правилами формы;onContentPrepareForm— для изменения самой формы, а не значений.
События сохранения и удаления на уровне модели
Основная логика находится в libraries/src/MVC/Model/AdminModel.php.
Порядок при сохранении записи
Если запись уже существует, типовой путь такой:
onTableBeforeLoadTable::load()onTableAfterLoadonTableBeforeBindTable::bind()onTableAfterBindprepareTable()Table::check(), внутри базовой реализации —onTableCheckonContentBeforeSaveTable::store(), внутри него —onTableBeforeStoreиonTableAfterStoreonContentCleanCacheonContentAfterSave
Ключевой нюанс:
к моменту
onContentBeforeSaveобъект Table уже связан с входными данными и уже прошёлcheck().
То есть:
- для работы с ещё не связанными данными лучше перехватывать форму и валидацию;
- для работы с итоговой строкой записи лучше брать
onContentBeforeSaveилиonTableBeforeStore.
Порядок при удалении записи
В AdminModel::delete():
onTableBeforeLoadTable::load()onTableAfterLoadonContentBeforeDeleteonTableBeforeDeleteTable::delete()onTableAfterDeleteonContentAfterDeleteonContentCleanCache
Порядок при смене состояния
В AdminModel::publish():
onContentBeforeChangeStateonTableBeforePublishTable::publish()onTableAfterPublishonContentChangeStateonContentCleanCache
Основные события модели, наследующей AdminModel
| Событие | Аргументы | Что можно менять | Как ядро использует результат |
|---|---|---|---|
onContentBeforeSave |
context, subject = таблица, isNew, data |
Объект таблицы; можно вернуть false через result |
При любом false сохранение отменяется |
onContentAfterSave |
Те же аргументы | Обычно только побочные действия | Результат не отменяет уже выполненное сохранение |
onContentBeforeDelete |
context, subject = таблица |
Объект таблицы; можно вернуть false |
Удаление отменяется |
onContentAfterDelete |
context, subject = таблица |
Обычно только побочные действия | Это уже постфактум |
onContentBeforeChangeState |
context, subject = список первичных ключей, value |
Можно вернуть false |
Изменение состояния отменяется |
onContentChangeState |
Те же аргументы | Обычно побочные действия | Состояние уже изменено |
onBeforeBatch |
Зависит от команды партии | Таблицу и результат | Тонкая настройка пакетных операций в списке элементов в админке |
onContentCleanCache |
defaultgroup, cachebase, result |
Обычно только чтение результата очистки | Удобно для реакций после сброса кеша |
События таблиц (классы Table) Joomla
Классы Table нужны для работы с базой данных. Классы Table в Joomla описывают строку / запись в базы данных как объект: загружают запись, связывают входные данные с полями, проверяют, сохраняют, удаляют и меняют состояние. Они нужны как низкоуровневый слой работы с БД под моделями, чтобы компоненты не писали однотипный SQL для CRUD-операций вручную. Самый низкий уровень находится в libraries/src/Table/Table.php и libraries/src/Table/Nested.php.
Здесь события ближе всего к реальной записи в базу. Они особенно полезны, когда нужно вмешаться не в поведение компонента, а в сам объект таблицы.
Здесь нужно учитывать, что объект Table для базы данных может быть использован разными компонентами, библиотеками или плагинами, поэтому внедрение в процесс на данном этапе может проявить себя порой в самых неожиданных местах. Не забывайте грамотно ограничивать действие своих плагинов.
| Событие | Где вызывается | Аргументы | Что менять на практике |
|---|---|---|---|
onTableObjectCreate |
Конструктор таблицы | subject = таблица |
Первичная настройка объекта таблицы |
onTableBeforeLoad / onTableAfterLoad |
Table::load() |
ключи загрузки, флаг reset, затем данные таблицы |
Реакция на чтение записи |
onTableBeforeBind / onTableAfterBind |
Table::bind() |
src, ignore, subject |
Наиболее ранняя правка полей таблицы |
onTableCheck |
Table::check() |
subject = таблица |
Проверка и досчёт полей до записи |
onTableBeforeStore / onTableAfterStore |
Table::store() |
updateNulls, ключ, subject |
Последняя правка перед записью и реакция после неё |
onTableBeforeDelete / onTableAfterDelete |
Table::delete() |
первичный ключ, subject |
Низкоуровневый контроль удаления |
onTableBeforePublish / onTableAfterPublish |
Table::publish() |
список ключей, новое состояние, userId |
Реакция на публикацию и снятие с публикации |
onTableBeforeCheckout / onTableAfterCheckout |
Table::checkout() |
пользователь, таблица | Контроль блокировки записи |
onTableBeforeCheckin / onTableAfterCheckin |
Table::checkin() |
таблица | Контроль снятия блокировки |
onTableBeforeHit / onTableAfterHit |
Table::hit() |
таблица | Счётчики просмотров |
onTableBeforeMove / onTableAfterMove |
Table::move() |
смещение и условия | Порядок сортировки |
onTableBeforeReorder / onTableAfterReorder |
Table::reorder() |
запрос и условия | Перестройка порядка |
onTableBeforeReset / onTableAfterReset |
Table::reset() |
таблица | Сброс состояния объекта |
onTableSetNewTags |
AdminModel::batchTags() |
newTags, replaceTags, removeTags, subject |
Специальная обработка пакетного назначения меток |
При работе с событиями Joomla Table:
- меняйте поля у объекта
subject; - не рассчитывайте на широкую подмену служебных аргументов;
- если логика относится к конкретному бизнес-правилу компонента, часто лучше использовать событие модели, а не таблицы.
События модулей
Эта группа событий для плагинов живёт в libraries/src/Helper/ModuleHelper.php и libraries/src/Document/Renderer/Html/ModulesRenderer.php.
Порядок на уровне списка модулей:
onPrepareModuleList- если событие не дало свой список, Joomla строит список сама
onAfterModuleList- Joomla чистит список от дублей и недопустимых элементов
onAfterCleanModuleList
Порядок на уровне отдельного модуля:
onRenderModule- отрисовка модуля
onAfterRenderModule
Порядок на уровне позиции:
- все модули позиции отрисованы
onAfterRenderModules
| Событие | Аргументы | Что можно менять | Для чего нужно |
|---|---|---|---|
onPrepareModuleList |
modules |
Полностью заменить список модулей через updateModules() |
Свой источник модулей |
onAfterModuleList |
modules |
Править уже собранный список | Фильтрация и перестановка |
onAfterCleanModuleList |
modules |
Править очищенный список | Финальная чистка |
onRenderModule |
subject = модуль, attributes |
Параметры и сам объект модуля | Подмена стиля, атрибутов и содержимого |
onAfterRenderModule |
subject = уже отрисованный модуль, attributes |
Обычно только содержимое модуля | Постобработка одного модуля |
onAfterRenderModules |
content, attributes |
Общую строку позиции через updateContent() |
Обёртки, агрегаты, массовая правка HTML |
События пользователей и аутентификации
Логика раскидана по:
libraries/src/Authentication/Authentication.php;libraries/src/Application/CMSApplication.php;libraries/src/User/User.php.
Порядок при входе пользователя
onUserAuthenticateonUserAuthorisation- при отказе
onUserAuthorisationFailure onUserLogin- при успехе
onUserAfterLogin - при неудаче
onUserLoginFailure
Порядок при выходе
onUserLogout- при успехе
onUserAfterLogout - при неудаче
onUserLogoutFailure
Порядок при сохранении пользователя
В User::save():
onUserBeforeSave- запись в таблицу пользователя
onUserAfterSave
Порядок при удалении пользователя
В User::delete():
onUserBeforeDelete- удаление из таблицы
onUserAfterDelete
| Событие | Аргументы | Что можно менять | Для чего нужно |
|---|---|---|---|
onUserAuthenticate |
credentials, options, subject = AuthenticationResponse |
Объект ответа аутентификации | Подтвердить или отклонить подлинность пользователя |
onUserAuthorisation |
subject = AuthenticationResponse, options |
Обычно добавляется результат | Решить, можно ли пустить уже проверенного пользователя |
onUserLogin |
subject = массив ответа аутентификации, options |
Возврат false через result отменяет вход |
Поднять пользовательскую сессию в своём хранилище |
onUserAfterLogin |
subject, options |
Обычно побочные действия | Журналы, внешние сервисы, одноразовые метки |
onUserBeforeSave |
subject = старые данные пользователя, isNew, data = новые данные |
Можно вернуть false |
Бизнес-правила перед сохранением |
onUserAfterSave |
subject = сохранённые данные, isNew, savingResult, errorMessage |
Обычно побочные действия | Синхронизация после сохранения |
onUserBeforeDelete |
subject = массив пользователя |
Обычно проверка и исключение | Запрет удаления |
onUserAfterDelete |
subject, deletingResult, errorMessage |
Обычно побочные действия | Уборка следов пользователя |
Здесь есть принципиальное отличие от событий контента:
onUserAuthenticate и onUserAuthorisation работают с объектом AuthenticationResponse, то есть меняют не HTML и не таблицу, а состояние решения о входе.
События установки и обновления расширений
Эта группа особенно важна для систем сопровождения, зеркал, внутренних каталогов расширений и нестандартной доставки пакетов расширений Joomla.
| Событие | Где вызывается | Аргументы | Что можно менять |
|---|---|---|---|
onInstallerBeforePackageDownload |
InstallerHelper::downloadPackage() |
url, headers |
Адрес и заголовки через updateUrl() и updateHeaders() |
onInstallerBeforeInstallation |
com_installer |
Контекст установки и result |
Можно прервать установку |
onExtensionBeforeInstall |
Installer::install() |
объект установщика, пакет | Обычно проверки перед установкой |
onExtensionAfterInstall |
Installer::install() |
объект установщика, eid, result |
Реакция после установки |
onExtensionBeforeUpdate |
Installer::update() |
объект установщика, пакет | Проверки перед обновлением |
onExtensionAfterUpdate |
Installer::update() |
объект установщика, eid, result |
Реакция после обновления |
onExtensionBeforeUninstall |
Installer::uninstall() |
объект установщика, идентификатор | Проверки перед удалением |
onExtensionAfterUninstall |
Installer::uninstall() |
объект установщика, идентификатор, result |
Уборка после удаления |
Вместо заключения
В Joomla 6+ система плагинов стала строже и понятнее, чем в старых Joomla 2.5 / Joomla 3:
- события имеют типизированные классы;
- аргументы названы явно;
- для изменения данных всё чаще есть специальные методы;
- основная точка расширения — не «магия триггера», а точное понимание стадии жизненного цикла.
Именно поэтому хорошая разработка под Joomla начинается не с вопроса «какой есть триггер», а с вопроса «на какой стадии жизненного цикла приложения Joomla мне действительно нужно вмешаться».