Как работает ядро Joomla 6 на уровне Приложения (Application)? Сколько видов Приложений на самом деле есть в Joomla и в чём между ними разница? Какой жизненный цикл каждого Приложения Joomla, включая маршрутизацию, диспетчеризацию и события плагинов? На эти и другие вопросы попытается дать ответ эта статья, опирающаяся на кодовую базу Joomla 6.1.0.

Joomla 6 строит запрос вокруг объекта приложения, которое инициализирует окружение, разбирает запрос, передаёт управление компоненту, собирает документ и затем отправляет ответ. Плагины вмешиваются в работу в строго определённых точках этого цикла.

Для Joomla 3 хочу упомянуть статью Дмитрия Рекуна "Общая информация о принципе действия Joomla", которая довольно подробно описывает как Joomla работала раньше.

Что происходит в Joomla при создании HTML-страницы?

Обычный запрос HTML в Joomla начинается в index.php в корне сайта (если речь идёт о пользовательской части).

Сначала входной файл:

  1. проверяет минимальную версию PHP;
  2. определяет _JEXEC, чтобы внутренние файлы Joomla нельзя было выполнять напрямую;
  3. подключает defines.php из корня сайта (если он существует): его используют для предварительного определения или переопределения констант с путями при нестандартной структуре каталогов (Joomla находится за пределами web root);
  4. подключает includes/defines.php, где задаются основные JPATH_*-константы;
  5. передаёт управление в includes/app.php.

В includes/app.php Joomla сохраняет время старта и память, ещё раз поднимает определения путей, проверяет наличие обязательных зависимостей и подключает includes/framework.php. Во framework.php загружается libraries/bootstrap.php, проверяется состояние установки, читается configuration.php, настраиваются ошибки, JDEBUG и базовые параметры окружения.

После этого начинается уже загрузка Приложения Joomla 6:

  1. берётся DI-контейнер через Factory::getContainer();
  2. для сайта настраиваются алиасы веб-сессии;
  3. из контейнера создаётся SiteApplication;
  4. приложение записывается в Factory::$application;
  5. вызывается $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\AbstractApplication
    • Joomla\Application\AbstractWebApplication
      • Joomla\CMS\Application\WebApplication
        • Joomla\CMS\Application\CMSApplication
          • Joomla\CMS\Application\SiteApplication
          • Joomla\CMS\Application\AdministratorApplication
          • Joomla\CMS\Application\ApiApplication
          • Joomla\CMS\Installation\Application\InstallationApplication
    • Joomla\Console\Application
      • Joomla\CMS\Application\ConsoleApplication
      • Joomla\CMS\Installation\Application\CliInstallationApplication
    • Joomla\CMS\Application\CliApplication
      • Joomla\CMS\Application\DaemonApplication

Контракты приложений Joomla

  • Joomla\CMS\Application\CMSApplicationInterface
    • Joomla\CMS\Application\CMSApplication
    • Joomla\CMS\Application\ConsoleApplication
    • Joomla\CMS\Application\CliApplication
    • Joomla\CMS\Installation\Application\CliInstallationApplication
  • Joomla\CMS\Application\CMSWebApplicationInterface
    • Joomla\CMS\Application\CMSApplication
      • Joomla\CMS\Application\SiteApplication
      • Joomla\CMS\Application\AdministratorApplication
      • Joomla\CMS\Application\ApiApplication
      • Joomla\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) выполняет базовую загрузку:

  1. определяет константы путей;
  2. поднимает автозагрузку;
  3. создаёт DI-контейнер (подключение зависимостей);
  4. через сервис-провайдеры собирает объект приложения и внедряет в него диспетчер событий, логгер, сессию и другие сервисы;
  5. вызывает $app->execute().

С этого момента жизненным циклом управляет уже само приложение.

Общий цикл CMSApplication::execute()

Базовая логика живёт в libraries/src/Application/CMSApplication.php::execute().

Порядок работы такой:

  1. Проверка опасных системных переменных.
  2. Настройка журнала.
  3. Построение карты пространств имён (неймспейсов) расширений.
  4. Загрузка плагинов группы behaviour (в ядре Joomla это: плагин обратной совместимости, версионность и поддержка тегов com_tags).
  5. Загрузка плагинов группы system.
  6. Вызов onBeforeExecute.
  7. Выполнение прикладной части через doExecute().
  8. Если создан документ, вызов render().
  9. При включённом GZIP-сжатии в конфиге Joomla - вызов compress() и затем onAfterCompress.
  10. Вызов onBeforeRespond.
  11. Отправка ответа через respond().
  12. Вызов onAfterRespond.

Итого:

  • onBeforeExecute — самая ранняя общая точка для системных плагинов;
  • onBeforeRespond — последняя точка до отправки заголовков и тела ответа;
  • onAfterRespond — заключительный этап после отдачи заголовков и тела ответа браузеру: журналирование, отладка, побочные действия.

Отдельно важно: в WebApplication существует ещё onAfterExecute, но в обычном запросе Joomla CMS он не вызывается, потому что CMSApplication переопределяет execute() своим циклом.

Инициализация

Инициализация проходит внутри CMSApplication::initialiseApp(), а у SiteApplication и AdministratorApplication есть свои дополнения.

На этом этапе Joomla:

  1. в приложениях сайта и административной части уточняет язык и, при необходимости, группы доступа пользователя;
  2. создаёт объект языка и загружает его в приложение;
  3. подключает файлы локализации (ini-файлы языков);
  4. выбирает редактор для текущего пользователя;
  5. вызывает onAfterInitialise.

Важно не смешивать этот этап с более ранней сборкой приложения. Объекты сессии, диспетчера событий, журнала и другие сервисы внедряются ещё при создании приложения в сервис-провайдере libraries/src/Service/Provider/Application.php. Текущий пользователь подтягивается из сессии через WebApplication::afterSessionStart(), а не создаётся внутри initialiseApp().

Именно после onAfterInitialise можно считать, что у приложения уже есть в рабочем состоянии:

  • сессия;
  • язык;
  • пользователь;
  • конфигурация;
  • диспетчер событий.

Это хороший момент для ранней настройки среды: язык уже выбран, редактор известен, базовые сервисы доступны, но к рендеру компонента Joomla ещё не приступила, поэтому логика, зависящая от url, здесь ещё преждевременна.

Маршрутизация и проверка доступа

Публичная часть

В SiteApplication::route():

  1. создаётся или берётся текущий Uri;
  2. роутер разбирает адрес;
  3. найденные переменные записываются во входные данные приложения;
  4. вызывается onAfterRoute;
  5. выполняется проверка доступа к текущему Itemid (id пункта меню) через authorise().

После этого Joomla уже знает, какой компонент, представление, шаблон, макет и пункт меню связаны с запросом, и может сразу отреагировать на запрет доступа.

Административная часть

В AdministratorApplication::route() логика проще:

  1. при необходимости выполняется перевод на HTTPS;
  2. проверяется состояние страниц многофакторной проверки;
  3. вызывается onAfterRoute.

В административной части маршрутизация в меньшей степени завязана на человекопонятные адреса, но событие то же самое.

API

В ApiApplication::route() порядок другой:

  1. загружаются плагины группы webservices;
  2. вызывается onBeforeApiRoute;
  3. маршрутизатор API подбирает маршрут;
  4. Joomla согласует формат ответа по заголовку Accept;
  5. переменные маршрута записываются во входные данные;
  6. вызывается onAfterApiRoute;
  7. при необходимости выполняется вход пользователя для закрытых маршрутов.

Для API важен именно onBeforeApiRoute: через него можно добавить или поправить роуты REST API Joomla до собственно роутинга.

Отдельно: как работает DaemonApplication

На демон-процессы обычно вешают задачи, которые должны жить долго, работать в фоне и не зависеть от HTTP-запроса или разового запуска CLI-команды.

Типичные задачи:

  • постоянная обработка очереди: email, SMS, webhooks, экспорт/импорт, генерация файлов;
  • воркеры фоновых заданий, где нужно быстро подхватывать новые задачи без запуска PHP с нуля каждый раз;
  • слушатели внешних событий: сокеты, очереди сообщений, брокеры, long polling, системные события;
  • периодическая фоновая работа с собственным циклом: очистка, синхронизация, пересчёт агрегатов, мониторинг;
  • процессы, которым нужно держать состояние в памяти: кэш подключений, открытые соединения, подготовленные ресурсы;
  • интеграции с внешними сервисами, где нужно постоянно принимать или отправлять события;
  • сервисные процессы, где важны PID-файл, сигналы SIGTERM/SIGHUP, мягкая остановка и перезапуск.

Для Joomla-практики чаще всего это были бы не обычные задачи расширений, а инфраструктурные вещи: воркер очереди, постоянный импорт, синхронизация с CRM/маркетплейсом, обработчик вебхуков из очереди, индексация, массовая рассылка, мониторинг состояния. Если задача может спокойно запускаться раз в минуту по cron и завершаться, демон обычно не нужен. Демон оправдан, когда процесс должен быть постоянно запущен, быстро реагировать, держать ресурсы открытыми или управляться через сигналы и PID.

Поэтому DaemonApplication живёт по другому сценарию, нежели сайт, админка или API. Это не разовый запрос-ответ, а долгоживущий процесс, который:

  1. стартует из CLI;
  2. при необходимости уходит в фон;
  3. пишет PID-файл;
  4. подключает обработчики Unix-сигналов;
  5. бесконечно гоняет doExecute() в цикле.

Требования к окружению для демона на базе Joomla

Конструктор DaemonApplication проверяет наличие расширения PCNTL и наличие функций POSIX. Если их нет, приложение падает ещё в __construct().

Какие параметры он готовит?

Метод loadConfiguration() нормализует и подготавливает служебные параметры демона:

  • author_name
  • author_email
  • application_name
  • application_description
  • application_executable
  • application_directory
  • application_pid_file
  • application_uid
  • application_gid
  • application_require_identity
  • max_execution_time
  • max_memory_limit

По умолчанию DaemonApplication строит PID-файл в абсолютном системном Unix-каталоге /tmp, по шаблону /tmp/<application_name>/<application_name>.pid. Это не временная папка конкретной Joomla-установки. Если очень нужно, путь можно переопределить через конфигурационный параметр application_pid_file.

Как выглядит жизненный цикл демона на Joomla?

Порядок в DaemonApplication::execute() такой:

  1. onBeforeExecute
  2. включение сборки мусора gc_enable()
  3. попытка перейти в режим демона через daemonize()
  4. если переход удался:
    1. включение declare(ticks=1) для мониторинга сигналов
    2. бесконечный цикл:
      1. gc()
      2. usleep(1000)
      3. doExecute()
  5. onAfterExecute вызывается только если запуск не удался или если выполнение вообще вышло из цикла

На заметку:

для DaemonApplication событие onAfterExecute существует реально, но при нормальной бесконечной работе демона до него обычно не доходят.

Это важное отличие Joomla Daemon от веб-цикла Joomla:

  • у демона нет onAfterInitialise, onAfterRoute, onAfterDispatch, onBeforeRender, onAfterRender, onBeforeRespond, onAfterRespond;
  • у него есть только ранний onBeforeExecute, поздний onAfterExecute и специфические onFork и onReceiveSignal.

Что делает daemonize()

Метод daemonize() последовательно:

  1. проверяет через isActive(), не запущен ли уже такой демон;
  2. сбрасывает внутренние флаги процесса;
  3. если не передан флаг -f, вызывает detach() и уходит в фон;
  4. если -f передан, остаётся на переднем плане;
  5. пишет PID-файл через writeProcessIdFile();
  6. пытается сменить пользователя и группу через changeIdentity();
  7. подключает обработчики сигналов через setupSignalHandlers();
  8. меняет рабочую директорию на application_directory.

Что значит флаг -f

Этот флаг не позволяет уйти демону в фоновый процесс. Если у CLI-ввода есть флаг -f, демон продолжает работать с консолью и выводить информацию (если предполагается) в неё. Это удобно для отладки, запуска под внешним диспетчером процессов, различных видов тестирования.

Как работает fork

Уход в фон делается в detach(), а само разделение процесса — в fork().

Ключевая деталь ядра:

  1. вызывается pcntl_fork();
  2. и в родительском, и в дочернем процессе вызывается postFork();
  3. postFork() отправляет событие onFork.

То есть:

onFork срабатывает после разделения процесса и проходит в обоих потоках выполнения, родительском и дочернем.

Это хорошая точка для post-fork инициализации:

  • переподключить сокеты;
  • заново открыть файловые дескрипторы;
  • переинициализировать соединения, которые нельзя безопасно делить после fork.

Как работает onReceiveSignal

Статический обработчик DaemonApplication::signal($signal):

  1. пишет сигнал в журнал;
  2. берёт текущий экземпляр приложения из static::$instance;
  3. отправляет событие onReceiveSignal;
  4. только потом выполняет встроенную реакцию ядра.

Это значит, что обработчик плагина получает сигнал до штатной реакции класса.

Какие сигналы обрабатываются встроенно

Ядро по умолчанию вешает обработчик на длинный список POSIX-сигналов из static::$signals.

Практически важные встроенные ветки такие:

  • SIGINT, SIGTERM — мягкое завершение через shutdown();
  • SIGHUP — завершение с последующим перезапуском через shutdown(true);
  • SIGCHLD, SIGCLD — уборка завершившихся дочерних процессов через pcntlWait().

Для остальных сигналов ядро только отправляет onReceiveSignal, а специального встроенного поведения не добавляет.

Как он определяет, что демон уже работает?

Метод isActive():

  1. читает PID-файл;
  2. проверяет, что PID корректен;
  3. делает posix_kill($pid, 0) как проверку живого процесса;
  4. если процесс не отвечает, удаляет устаревший PID-файл.

То есть здесь используется именно проверка существования процесса, а не простое наличие файла.

Что делает shutdown()

shutdown():

  1. ставит флаг завершения;
  2. если демон ещё не был полностью поднят, просто завершает текущий процесс;
  3. если это главный процесс, читает PID из файла;
  4. удаляет PID-файл;
  5. при restart=true перезапускает ту же команду;
  6. иначе завершает процесс.

Отдельная хронология для 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 ядро даёт всего четыре действительно системных точки:

  • onBeforeExecute
  • onAfterExecute
  • onFork
  • onReceiveSignal

Но именно для фоновых процессов это и есть главные точки расширения.

При этом Daemon приложение Joomla само не импортирует какие-либо группы плагинов по умолчанию (на данный момент -Joomla 6.1). Поскольку это в любом случае довольно специфичный сценарий - вы импортируете нужные группы плагинов самостоятельно. Также помните, что вы можете здесь использовать и классы фреймворка Joomla и библиотек, входящих в состав ядра Joomla.

Диспетчеризация

После маршрутизации приложение передаёт управление компоненту.

Публичная и административная часть

В SiteApplication::dispatch() и AdministratorApplication::dispatch() порядок такой:

  1. создаётся документ;
  2. настраиваются метаданные, шаблон и реестры ресурсов;
  3. вызывается onAfterInitialiseDocument;
  4. компонент отрисовывается через ComponentHelper::renderComponent();
  5. результат кладётся в буфер документа;
  6. вызывается onAfterDispatch.

onAfterInitialiseDocument — это точка до выполнения компонента: документ уже существует, но компонент ещё не отработал. Здесь удобно править тип документа, метаданные, реестры стилей и сценариев. Помните, что это событие появилось только в Joomla 5.0.0.

onAfterDispatch — это точка после выполнения компонента. Здесь уже можно работать с буферами документа и итогом работы компонента.

API

В ApiApplication::dispatch() смысл тот же:

  1. создаётся документ;
  2. вызывается onAfterInitialiseDocument;
  3. вызывается диспетчер компонента веб-служб.
  4. вызывается onAfterDispatch.

Сборка документа

Для фронтенда (SiteApplication) и админки это обычно HTML. Хотя нужно упомянуть, что внутри компонента за формат рендера отвечает View и View может отдавать и txt, и json, и XML - что угодно, не только HTML. Но рассмотрим стандартный путь, когда сборка ответа в HTML делает CMSApplication::render().

Порядок такой:

  1. заполняются параметры шаблона;
  2. выполняется $this->document->parse(...);
  3. вызывается onBeforeRender;
  4. документ формирует итоговую строку ответа;
  5. строка записывается в тело ответа через setBody();
  6. вызывается onAfterRender.

Здесь есть важная деталь: во время сборки HTML-заголовка libraries/src/Document/Renderer/Html/MetasRenderer.php вызывает onBeforeCompileHead.

То есть:

  • onBeforeRender — общий поздний этап перед окончательной сборкой страницы;
  • onBeforeCompileHead — специальная точка именно для <head>;
  • onAfterRender — этап, когда вся HTML-строка уже собрана и её можно переписать целиком.

Вывод ответа

После сборки документа CMSApplication::execute() завершает запрос:

  1. при включённом сжатии вызывает onAfterCompress;
  2. вызывает onBeforeRespond;
  3. отправляет заголовки и тело;
  4. вызывает 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 во время запроса

Ниже уже не укрупнённая схема, а именно хронология. Важно помнить две вещи:

  1. не все ветви выполняются в каждом запросе;
  2. часть событий внутри формирования HTML зависит от конкретного шаблона и от того, в каком месте шаблон вызывает jdoc:include.

Самый первый триггер в жизненном цикле Joomla

Ещё до onBeforeExecute ядро успевает загрузить группы behaviour и system.

Для каждого плагина, который загружается через PluginHelper::importPlugin(), перед созданием экземпляра и сразу после него проходят:

  1. onBeforeExtensionBoot
  2. onAfterExtensionBoot

Это касается:

  • плагинов behaviour и system в начале запроса;
  • плагинов webservices в API;
  • плагинов content, user, installer, extension, finder, privacy, quickicon, sampledata и других в более поздних ветках;
  • компонентов и модулей, когда они загружаются через bootComponent() и bootModule().

Поэтому onBeforeExtensionBoot и onAfterExtensionBoot — самые ранние реально доступные точки вмешательства в жизненный цикл расширения.

Полный порядок для обычной HTML-страницы сайта

Ниже порядок для типового запроса фронтенда с HTML-документом.

Базовая последовательность

  1. Для каждого загружаемого плагина группы behaviour: onBeforeExtensionBoot -> onAfterExtensionBoot
  2. Для каждого загружаемого плагина группы system: onBeforeExtensionBoot -> onAfterExtensionBoot
  3. onBeforeExecute
  4. onAfterInitialise
  5. onAfterRoute
  6. onAfterInitialiseDocument
  7. Для загружаемого компонента: onBeforeExtensionBoot -> onAfterExtensionBoot
  8. Внутренняя логика компонента
  9. onAfterDispatch
  10. onBeforeRender
  11. Внутренняя сборка документа и шаблона
  12. onAfterRender
  13. При включённом gzip и совместимых настройках PHP: compress() -> onAfterCompress
  14. onBeforeRespond
  15. onAfterRespond

Что может произойти внутри пункта «внутренняя логика компонента»

Если компонент использует HTML-представление на базе Joomla\CMS\MVC\View\HtmlView, то внутри его display() дополнительно идут:

  1. onBeforeDisplay
  2. загрузка шаблона представления
  3. onAfterDisplay

Но и это ещё не всё. Многие компоненты вызывают свои события до parent::display().

Например, у com_content в представлении статьи порядок такой:

  1. для каждого плагина группы content: onBeforeExtensionBoot -> onAfterExtensionBoot
  2. onContentPrepare
  3. onContentAfterTitle
  4. onContentBeforeDisplay
  5. onContentAfterDisplay
  6. onBeforeDisplay
  7. onAfterDisplay

У категорий и тегов логика похожая, но события onContentPrepare, onContentAfterTitle, onContentBeforeDisplay, onContentAfterDisplay могут вызываться по каждому элементу списка, а не один раз на страницу.

Что может произойти внутри пункта «внутренняя сборка документа и шаблона»

Вот здесь порядок уже зависит от шаблона.

Если шаблон первым делом выводит <head>, то раньше всего сработает onBeforeCompileHead. Если шаблон затем выводит позиции модулей, то для первой же позиции Joomla запустит такую ветку:

  1. onPrepareModuleList
  2. onAfterModuleList
  3. onAfterCleanModuleList
  4. для каждого загружаемого модуля: onBeforeExtensionBoot -> onAfterExtensionBoot
  5. для каждого модуля: onRenderModule
  6. внутренняя логика модуля
  7. для каждого модуля: onAfterRenderModule
  8. после завершения всей позиции: onAfterRenderModules

При этом внутренняя логика конкретного модуля тоже может загрузить свои плагины и вызвать свои события. Пример из ядра: mod_articles_news внутри вывода модуля вызывает:

  1. onContentPrepare
  2. onContentAfterTitle
  3. onContentBeforeDisplay
  4. onContentAfterDisplay

То есть в одной HTML-странице события content могут сработать:

  • в компоненте;
  • в одном или нескольких модулях;
  • в произвольном порядке относительно позиций модулей, потому что это уже зависит от шаблона.

Полный порядок триггеров плагинов для HTML-страницы в админке Joomla

Базовый порядок почти тот же:

  1. для плагинов behaviour: onBeforeExtensionBoot -> onAfterExtensionBoot
  2. для плагинов system: onBeforeExtensionBoot -> onAfterExtensionBoot
  3. onBeforeExecute
  4. onAfterInitialise
  5. onAfterRoute
  6. onAfterInitialiseDocument
  7. для компонента: onBeforeExtensionBoot -> onAfterExtensionBoot
  8. внутренняя логика компонента и представления
  9. onAfterDispatch
  10. onBeforeRender
  11. события головы страницы, модулей и меню в зависимости от шаблона
  12. onAfterRender
  13. при включённом gzip и совместимых настройках PHP: compress() -> onAfterCompress
  14. onBeforeRespond
  15. onAfterRespond

Но в административной части есть ещё несколько условных точек, которые часто забывают.

Если строится меню для модулей меню в панели администратора Joomla, то во время обработки узлов меню могут вызываться onPreprocessMenuItems. Причём это событие может пройти несколько раз, по уровням дерева. Контексты в ядре com_menus.administrator.module и administrator.module.mod_submenu.

Если открыт список пунктов меню в com_menus, Перед выводом представления Items вызывается onBeforeRenderMenuItems. Если модель собирает типы меню дополнительно может вызваться onAfterGetMenuTypeOptions. То есть для административной части полная хронология часто содержит не только системные и модульные события, но и отдельные меню-события внутри конкретных экранов.

Полный порядок для API-запроса

У API жизненный цикл другой, и это важно.

Базовая последовательность

  1. Для плагинов behaviour: onBeforeExtensionBoot -> onAfterExtensionBoot
  2. Для плагинов system: onBeforeExtensionBoot -> onAfterExtensionBoot
  3. onBeforeExecute
  4. onAfterInitialise
  5. Для плагинов webservices: onBeforeExtensionBoot -> onAfterExtensionBoot
  6. onBeforeApiRoute
  7. разбор маршрута API
  8. onAfterApiRoute
  9. onAfterInitialiseDocument
  10. Для компонента API: onBeforeExtensionBoot -> onAfterExtensionBoot
  11. логика контроллера, модели и представления API
  12. onAfterDispatch
  13. render() через переопределённый ApiApplication::render()
  14. при включённом gzip и совместимых настройках PHP: compress() -> onAfterCompress
  15. onBeforeRespond
  16. onAfterRespond

Чего здесь обычно нет

В обычном API-запросе через ApiApplication нет общего HTML-цикла:

  • нет onBeforeRender из CMSApplication::render();
  • нет onAfterRender;
  • нет onBeforeCompileHead;
  • нет модульных событий формирования вывода.

Причина простая: ApiApplication переопределяет render() и не использует HTML-сборку документа.

Какие API-события добавляются внутри представления и сериализации

Если ответ строится через JsonApiView и сериализатор Joomla, дополнительно могут идти:

  1. onApiGetFields
  2. onGetApiAttributes
  3. onGetApiRelation

Это уже не системная часть жизненного цикла приложения, а события слоя сериализации API.

Хронология при открытии и отправке формы Joomla

Открытие формы редактирования

Типовой порядок:

  1. общий цикл приложения до onAfterRoute
  2. загрузка компонента
  3. onContentPrepareData
  4. onContentPrepareForm
  5. onBeforeDisplay
  6. onAfterDisplay
  7. onAfterDispatch
  8. дальше общая HTML-сборка страницы

Отправка формы и сохранение записи

Типовой порядок:

  1. общий цикл приложения до маршрута и диспетчеризации
  2. onContentNormaliseRequestData
  3. onContentBeforeValidateData
  4. onTableBeforeLoad и onTableAfterLoad, если запись не новая
  5. onTableBeforeBind
  6. onTableAfterBind
  7. onTableCheck
  8. onContentBeforeSave
  9. onTableBeforeStore
  10. onTableAfterStore
  11. onContentCleanCache
  12. onContentAfterSave
  13. обычное завершение запроса через onBeforeRespond и onAfterRespond

Если это удаление:

  1. onTableBeforeLoad
  2. onTableAfterLoad
  3. onContentBeforeDelete
  4. onTableBeforeDelete
  5. onTableAfterDelete
  6. onContentAfterDelete
  7. onContentCleanCache

Если это смена состояния (опубликовано, не опубликовано, в архиве, в корзине):

  1. onContentBeforeChangeState
  2. onTableBeforePublish
  3. onTableAfterPublish
  4. onContentChangeState
  5. onContentCleanCache

Хронология триггеров плагинов Joomla для входа, выхода и восстановления доступа пользователя

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

Вход пользователя

  1. для плагинов user: onBeforeExtensionBoot -> onAfterExtensionBoot
  2. onUserAuthenticate
  3. onUserAuthorisation
  4. при отказе: onUserAuthorisationFailure
  5. при продолжении входа: onUserLogin
  6. при успехе: onUserAfterLogin
  7. при неудаче: onUserLoginFailure

Выход пользователя

  1. для плагинов user: onBeforeExtensionBoot -> onAfterExtensionBoot
  2. onUserLogout
  3. при успехе: onUserAfterLogout
  4. при неудаче: onUserLogoutFailure

Напоминание логина пользователя

onUserAfterRemind - Вызывается при отправке формы "забыли логин" в com_users Joomla.

Запрос на сброс пароля

События вызываются при отправке формы "забыли пароль" в com_users Joomla:

  1. onUserBeforeResetRequest
  2. сохранение пользователя (там свои события onUserAfterSave и т.д.)
  3. onUserAfterResetRequest

Завершение сброса пароля

  1. onUserBeforeResetComplete
  2. сохранение пользователя
  3. onUserAfterResetComplete

Хронология установки расширения Joomla через штатный установщик

Если запрос идёт через стандартный установщик Joomla, то внутри него порядок событий такой:

  1. для плагинов группы installer: onBeforeExtensionBoot -> onAfterExtensionBoot
  2. onInstallerBeforeInstallation
  3. при скачивании пакета по url адресу с сайта разработчика: onInstallerBeforePackageDownload
  4. onInstallerBeforeInstaller
  5. для плагинов extension: onBeforeExtensionBoot -> onAfterExtensionBoot
  6. onExtensionBeforeInstall
  7. установка расширения
  8. onExtensionAfterInstall
  9. onInstallerAfterInstaller

Для обновления:

  1. onInstallerBeforePackageDownload, если пакет нужно скачать
  2. onExtensionBeforeUpdate
  3. обновление
  4. onExtensionAfterUpdate

Для удаления:

  1. onExtensionBeforeUninstall
  2. удаление
  3. 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'), происходит следующее:

  1. из таблицы #__extensions выбираются только включённые плагины;
  2. выборка сортируется по полю ordering;
  3. плагин загружается и создаётся;
  4. его обработчики регистрируются в диспетчере событий.

Отсюда следует первое правило порядка:

если приоритет слушателей не задан явно, раньше будет вызван тот плагин, который раньше загружен, а загружается он по 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().

Порядок там такой:

  1. onContentPrepare
  2. onContentAfterTitle
  3. onContentBeforeDisplay
  4. вывод основного содержимого материала
  5. 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() порядок такой:

  1. строится объект формы;
  2. вызывается onContentNormaliseRequestData;
  3. вызывается onContentBeforeValidateData;
  4. форма фильтрует и проверяет данные;
  5. затем уже вызывается сохранение модели.

Это очень полезное разделение:

  • onContentNormaliseRequestData — для приведения сырых данных к нужному виду;
  • onContentBeforeValidateData — для правки данных прямо перед правилами формы;
  • onContentPrepareForm — для изменения самой формы, а не значений.

События сохранения и удаления на уровне модели

Основная логика находится в libraries/src/MVC/Model/AdminModel.php.

Порядок при сохранении записи

Если запись уже существует, типовой путь такой:

  1. onTableBeforeLoad
  2. Table::load()
  3. onTableAfterLoad
  4. onTableBeforeBind
  5. Table::bind()
  6. onTableAfterBind
  7. prepareTable()
  8. Table::check(), внутри базовой реализации — onTableCheck
  9. onContentBeforeSave
  10. Table::store(), внутри него — onTableBeforeStore и onTableAfterStore
  11. onContentCleanCache
  12. onContentAfterSave

Ключевой нюанс:

к моменту onContentBeforeSave объект Table уже связан с входными данными и уже прошёл check().

То есть:

  • для работы с ещё не связанными данными лучше перехватывать форму и валидацию;
  • для работы с итоговой строкой записи лучше брать onContentBeforeSave или onTableBeforeStore.

Порядок при удалении записи

В AdminModel::delete():

  1. onTableBeforeLoad
  2. Table::load()
  3. onTableAfterLoad
  4. onContentBeforeDelete
  5. onTableBeforeDelete
  6. Table::delete()
  7. onTableAfterDelete
  8. onContentAfterDelete
  9. onContentCleanCache

Порядок при смене состояния

В AdminModel::publish():

  1. onContentBeforeChangeState
  2. onTableBeforePublish
  3. Table::publish()
  4. onTableAfterPublish
  5. onContentChangeState
  6. onContentCleanCache

Основные события модели, наследующей 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.

Порядок на уровне списка модулей:

  1. onPrepareModuleList
  2. если событие не дало свой список, Joomla строит список сама
  3. onAfterModuleList
  4. Joomla чистит список от дублей и недопустимых элементов
  5. onAfterCleanModuleList

Порядок на уровне отдельного модуля:

  1. onRenderModule
  2. отрисовка модуля
  3. onAfterRenderModule

Порядок на уровне позиции:

  1. все модули позиции отрисованы
  2. 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.

Порядок при входе пользователя

  1. onUserAuthenticate
  2. onUserAuthorisation
  3. при отказе onUserAuthorisationFailure
  4. onUserLogin
  5. при успехе onUserAfterLogin
  6. при неудаче onUserLoginFailure

Порядок при выходе

  1. onUserLogout
  2. при успехе onUserAfterLogout
  3. при неудаче onUserLogoutFailure

Порядок при сохранении пользователя

В User::save():

  1. onUserBeforeSave
  2. запись в таблицу пользователя
  3. onUserAfterSave

Порядок при удалении пользователя

В User::delete():

  1. onUserBeforeDelete
  2. удаление из таблицы
  3. 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 мне действительно нужно вмешаться».

Об авторе

Толкачев Сергей Юрьевич

Толкачев Сергей Юрьевич

Joomla-разработчик. Контрибьютер ядра Joomla. Один из ведущих Telegram-канала русскоязычного Joomla-сообщества JoomlaFeed, один из модераторов чата русскоязычного Joomla-сообщества. Мои расширения в официальном маркетплейсе расширений Joomla - Joomla Extensions Directory. Имею публикации в официальном журнале международного Joomla-сообщества - Joomla Community Magazine и на официальном сайте русскоязычного Joomla-сообщества.

Муж. Отец 3 детей.

Россия, Саратов.

Расширения Joomla WebTolk

107 Всего расширений
12 Категорий
547 Выпущено версий
768869 Всего скачиваний