События API¶
API вызывает события до и после каждой операции записи и использует события, чтобы плагины могли расширять интерфейс Admin2 (боковые панели, панели меню, виджеты, контекстные панели, панели настроек, компоненты пользовательских полей, пользовательские отчёты). Плагины реагируют на эти события, чтобы проверять и изменять данные, отменять операции или собирать интерфейс.
События страниц¶
| Событие | Когда | Данные |
|---|---|---|
onApiBeforePageCreate |
До создания страницы (изменяемое) | route, header, content, template, lang |
onApiPageCreated |
После создания страницы (также после копирования или пакетного копирования) | page, route, lang; при копировании вместо lang отправляются page, route, source_route и method (copy или batch) |
onApiBeforePageUpdate |
До обновления страницы (изменяемое) | page, data (тело запроса); для пакетного обновления method равно batch |
onApiPageUpdated |
После обновления страницы | page; previous_template, когда шаблон изменился; при пакетном обновлении отправляются page, route и method (batch) |
onApiBeforePageDelete |
До удаления страницы | page, плюс lang, когда удаляется один перевод, или method (batch) при пакетном удалении |
onApiPageDeleted |
После удаления страницы | route, плюс lang, когда удаляется один перевод, или method (batch) при пакетном удалении |
onApiPageMoved |
После перемещения страницы (также по одному разу на каждую перемещённую страницу после реорганизации) | page, old_route, new_route; method равно reorganize, когда событие приходит из POST /pages/reorganize |
onApiBeforePageTranslate |
До создания перевода (изменяемое) | page, lang, header, content |
onApiPageTranslated |
После создания перевода | page, route, lang |
onApiBeforePageAdoptLanguage |
До принятия базового файла как файла конкретного языка | page, route, lang, from_file, to_file |
onApiPageLanguageAdopted |
После переименования базового файла в имя файла конкретного языка | page, route, lang |
onApiBeforePageSync |
До перезаписи перевода из другого языка (изменяемое) | page, source_lang, target_lang, header, content |
onApiPageSynced |
После перезаписи перевода | page, route, source_lang, target_lang |
onApiBeforePagesReorder |
До изменения порядка дочерних страниц | parent, order |
onApiPagesReordered |
После изменения порядка дочерних страниц | parent, order |
onApiBeforePagesReorganize |
До массового перемещения или изменения порядка | operations (разрешённый список) |
onApiPagesReorganized |
После массового перемещения или изменения порядка | operations |
События медиафайлов¶
| Событие | Когда | Данные |
|---|---|---|
onApiBeforeMediaUpload |
До загрузки каждого файла | page, filename, type, size; для медиафайлов сайта добавляется path |
onApiMediaUploaded |
После завершения загрузки | page, filenames; для медиафайлов сайта добавляется path |
onApiBeforeMediaDelete |
До удаления медиафайла | page, filename; для медиафайлов сайта добавляется path |
onApiMediaDeleted |
После удаления медиафайла | page, filename; для медиафайлов сайта добавляется path |
onApiMediaMetadataUpdated |
После сохранения метаданных медиафайла | page, filename для медиафайлов страницы; path, filename для медиафайлов сайта |
onApiMediaMetadataDeleted |
После удаления метаданных медиафайла | page, filename для медиафайлов страницы; path, filename для медиафайлов сайта |
Для медиафайлов сайта (эндпоинты /media) страницы нет, поэтому page равно null, а path содержит путь к файлу или папке относительно user://media. Слушателю, который читает page без проверки на null, нужно это учитывать.
События конфигурации¶
| Событие | Когда | Данные |
|---|---|---|
onApiConfigUpdated |
После сохранения конфигурации | scope, data |
События аутентификации и пользователей¶
| Событие | Когда | Данные |
|---|---|---|
onApiUserLogin |
После успешного входа по паролю, через 2FA или SSO | user, method (password/2fa/sso), ip, request; для SSO добавляется provider |
onApiUserLoginFailure |
После любой неудачной попытки входа | username, reason (password/2fa/disabled/no_api_access), ip |
onApiUserLogout |
После /auth/revoke с действительным токеном |
user, ip, request |
onApiPasswordReset |
После успешного /auth/reset-password |
user, ip |
onApiUserCreated |
После создания пользователя (через /users, /auth/setup или принятое приглашение) |
user |
onApiUserUpdated |
После обновления пользователя | user |
onApiBeforeUserDelete |
До удаления пользователя | user |
onApiUserDeleted |
После удаления пользователя | username |
onApiUser2faEnabled |
После включения 2FA | user |
onApiUser2faDisabled |
После выключения 2FA | user, forced_by_admin (bool) |
onApiSetupComplete |
После того как первичная настройка создала первого суперадминистратора | user |
onApiInvitationAccepted |
После принятия приглашения и создания учётной записи по нему | user, invitation |
onApiGroupCreated |
После создания группы пользователей | groupname, group |
onApiGroupUpdated |
После обновления группы пользователей | groupname, group |
onApiGroupDeleted |
После удаления группы пользователей | groupname |
События единого входа¶
Они позволяют плагину входа добавить провайдера на экран входа Admin2. API владеет маршрутами и обработкой сессии; плагин общается только с провайдером.
| Событие | Эндпоинт | Данные |
|---|---|---|
onApiLoginProviders |
GET /auth/sso/providers |
providers (изменяемый список) |
onApiLoginStart |
GET /auth/sso/{provider}/start |
provider, request, callback_url, return_to, redirect (устанавливается в URL авторизации провайдера), error |
onApiLoginCallback |
GET /auth/sso/{provider}/callback |
provider, request, callback_url, user (устанавливается в пользователя Grav, выполнившего вход), error |
После того как onApiLoginCallback вернёт пользователя, API выполняет те же проверки доступа к API, блокировки учётной записи и 2FA, что и при входе по паролю, а затем вызывает onApiUserLogin с method, равным sso.
События GPM¶
| Событие | Когда | Данные |
|---|---|---|
onApiBeforePackageInstall |
До установки | package, type (plugin/theme) |
onApiPackageInstalled |
После установки | package, type, dependencies |
onApiBeforePackageRemove |
До удаления | package, type |
onApiPackageRemoved |
После удаления | package, type |
onApiBeforePackageUpdate |
До обновления одного пакета | package, type |
onApiPackageUpdated |
После обновления пакета | package, type, dependencies |
onApiBeforeGravUpgrade |
До обновления ядра Grav | current_version, available_version |
onApiGravUpgraded |
После обновления ядра Grav | previous_version, new_version |
События схем¶
| Событие | Когда | Данные |
|---|---|---|
onApiBlueprintResolved |
После сериализации схемы для Admin2 (изменяемое fields) |
context, fields, user, плюс template, plugin, theme или page_id в зависимости от схемы |
onAdminPageTypes / onAdminModularPageTypes |
Пока GET /blueprints/pages строит список типов страниц |
types (по ссылке) |
context сообщает слушателю, какую схему он видит: page (с template), plugin (с plugin), theme (с theme), account, group или group_new (с template), config (только для конфигурации учётных записей, с template, равным accounts) или plugin-page (с plugin и page_id). Проверяйте его перед изменением полей, чтобы слушатель, предназначенный для схем страниц, не трогал остальные.
События интеграции с Admin2¶
Они вызываются, пока Admin2 собирает интерфейс. Плагины добавляют элементы, виджеты и панели в данные события, чтобы зарегистрировать свои интеграции. Текущий пользователь передаётся, чтобы слушатели могли ограничивать регистрацию по разрешениям, кроме onApiGenerateReports и onApiLogFiles, которые не несут user.
| Событие | Эндпоинт | Данные |
|---|---|---|
onApiSidebarItems |
GET /sidebar/items |
items, user |
onApiMenubarItems |
GET /menubar/items |
items, user |
onApiMenubarAction |
POST /menubar/actions/{plugin}/{action} |
plugin, action, body, user, result (изменяемое) |
onApiFloatingWidgets |
GET /floating-widgets |
widgets, user |
onApiContextPanels |
GET /context-panels |
panels, user |
onApiAdminSettingsPanels |
GET /settings/panels |
panels, user |
onApiPluginPageInfo |
GET /gpm/plugins/{slug}/page |
plugin, definition (изменяемое), user |
onApiGenerateReports |
GET /reports |
reports |
onApiDashboardNotifications |
GET /dashboard/notifications |
notifications (изменяемое, сгруппированное по месту), user, force |
onApiDashboardWidgets |
GET /dashboard/widgets |
widgets, user |
onApiMarkdownEditorButtons |
GET /editor/toolbar-buttons |
buttons, user |
onApiLogFiles |
GET /system/logs/files |
files (изменяемый список {file, label}) |
onApiUserListFilters |
GET /users/filters |
filters, defaultFilter, showAll, user |
onApiUserListFilter |
GET /users?filter={id} |
filter, collection (изменяемая), query, user |
onApiUserListColumns |
GET /users/columns |
columns, user |
onApiUserListColumnData |
GET /users |
usernames, data (изменяемое), user |
onApiUserListRowActions |
GET /users/row-actions |
actions, user |
onApiUserListRowAction |
POST /users/{username}/row-action |
id, plugin, action, username, user, result (изменяемое) |
Вкладки фильтров списка пользователей¶
Эти два события позволяют плагину добавить в список пользователей вкладку, показывающую отфильтрованный набор учётных записей, — аналог вкладок страницы пользователей из классической админки в Admin Next.
onApiUserListFilters собирает вкладки для строки навигации. Добавьте описание вкладки в массив filters; основной эндпоинт ставит первым встроенную вкладку all («All Users») и отбрасывает любую вкладку, на которую вызывающая сторона не авторизована. Событие также несёт две политики уровня страницы — defaultFilter (вкладка, на которой клиент открывается, когда в URL нет filter) и showAll (установите false, чтобы скрыть вкладку «All Users», когда показ всех учётных записей не является полезным стартовым представлением):
public function onApiUserListFilters(Event $event): void
{
// У события нет offsetGet по ссылке — добавляйте через чтение-изменение-запись,
// тем же приёмом, что в onApiSidebarItems / onApiMenubarItems.
$filters = $event['filters'];
$filters[] = [
'id' => 'active', // выбирается через ?filter=active; 'all' зарезервирован
'plugin' => 'my-plugin',
'label' => 'Active', // обычный текст, не ключ перевода
'icon' => 'fa-bolt', // необязательно
'priority' => 10, // необязательно, большее значение сортируется раньше
'authorize' => 'api.users.read', // необязательно, строка или массив «любое из»
];
$event['filters'] = $filters;
// Необязательная политика уровня страницы.
$event['defaultFilter'] = 'active'; // открываться здесь, когда в URL нет ?filter
$event['showAll'] = false; // скрыть встроенную вкладку "All Users"
}
Поле icon принимает общий формат IconSpec: сокращение Font Awesome (fa-bolt, fa-regular:clock, fa-brands:github), любые CSS-классы иконок, которые Admin2 уже загружает (class:ti ti-user), или безопасный структурированный объект SVG (['type' => 'svg', 'path' => '…']). Необработанная разметка SVG не принимается — иконки SVG собираются заново из разрешённых тегов и атрибутов.
defaultFilter учитывается, только когда он соответствует вкладке, которую вызывающая сторона действительно видит, иначе используется первая вкладка в строке. showAll => false убирает «All Users», только когда после авторизации остаётся хотя бы одна собственная вкладка плагина, поэтому строка никогда не окажется пустой.
onApiUserListFilter выполняется, пока GET /users?filter={id} строит список, — после поиска и сортировки, но до фильтрации по разрешениям и группам и постраничной разбивки. Плагин, владеющий активным id filter, сужает коллекцию и присваивает её обратно; основной эндпоинт по-прежнему применяет область доступа вызывающей стороны и разбивает на страницы, поэтому вкладка может только сузить уже видимое, но никогда не расширить:
public function onApiUserListFilter(Event $event): void
{
if ($event['filter'] !== 'active') {
return;
}
// filter() работает с обоими бэкендами учётных записей; см. ниже.
$event['collection'] = $event['collection']->filter(
fn ($user) => $user->get('state', 'enabled') === 'enabled'
);
}
Вкладки фильтров работают с обоими бэкендами учётных записей, и событие несёт одинаковые ключи для каждого. На бэкенде учётных записей Flex (по умолчанию в Grav) collection — это FlexCollectionInterface, и слушатель должен присвоить обратно коллекцию Flex, иначе его результат игнорируется. В файловом хранилище учётных записей collection — это Grav ArrayCollection пользователей с ключами по имени пользователя, поддерживающая filter(), matching() и перебор, но не имеющая методов Flex вроде filterBy(); слушатель может присвоить обратно любой перебираемый набор пользователей, и сохранятся только учётные записи, уже присутствующие в списке. filter() с замыканием работает с обоими. Чтобы использовать метод только для Flex, сначала проверьте тип:
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
$collection = $event['collection'];
$event['collection'] = $collection instanceof FlexCollectionInterface
? $collection->filterBy(['state' => 'enabled'])
: $collection->filter(fn ($user) => $user->get('state', 'enabled') === 'enabled');
Столбцы списка пользователей¶
Эти два события позволяют плагину добавить собственные столбцы в список пользователей — ссылку на WhatsApp, дату окончания подписки, число входов — не трогая разметку таблицы. Контракт намеренно узкий: плагин объявляет столбец и поставляет скалярные значения, но рендерингом, макетом, обновлениями и состоянием списка владеет Admin2. У плагина нет способа внедрить разметку или рендерер.
onApiUserListColumns собирает дополнительные столбцы, доступные через GET /users/columns (по аналогии с /users/filters, под разрешением api.users.read). Добавьте описание столбца в массив columns:
public function onApiUserListColumns(Event $event): void
{
// У события нет offsetGet по ссылке — добавляйте через чтение-изменение-запись,
// тем же приёмом, что в onApiUserListFilters / onApiSidebarItems.
$columns = $event['columns'];
$columns[] = [
'id' => 'my-plugin-valid-till', // обязательно, уникально
'plugin' => 'my-plugin',
'label' => 'Valid until', // обычный текст, не ключ перевода
'field' => 'subscription.valid_till', // ключ в карте данных каждого пользователя
'formatter' => 'datetime', // один из встроенных форматтеров ниже
'sortable' => false, // необязательно; на клиенте, только текущая страница
'priority' => 50, // необязательно, большее значение сортируется раньше
'authorize' => 'api.users.read', // необязательно, строка или массив «любое из»
];
$event['columns'] = $columns;
}
formatter проверяется по фиксированному белому списку — text, link, date, datetime, boolean, number, badge — и неизвестное значение откатывается к text. Сервер лишь называет форматтер; рендерит его клиент, поэтому по сети не передаются ни функция рендеринга, ни HTML. Значение link проверяется на клиенте как href (только относительный или http(s); всё остальное выводится как инертный текст). Проверка authorize повторно выполняется на сервере, а поле удаляется из ответа.
onApiUserListColumnData поставляет значения для каждого пользователя. Он вызывается один раз на ответ списка, и его список usernames содержит только учётные записи, уже выбранные после поиска, фильтрации, ограничения по разрешениям и постраничной разбивки, поэтому плагин получает данные для текущей страницы, а не для всех учётных записей. Верните карту username => [ field => value ]:
public function onApiUserListColumnData(Event $event): void
{
$data = $event['data'];
foreach ($event['usernames'] as $username) { // только текущая страница
$data[$username] = [
'subscription.valid_till' => $this->validTill($username),
];
}
$event['data'] = $data;
}
Эти значения объединяются с каждым сериализованным пользователем в карту extra в GET /users, с ключами по field столбца, поэтому нет второго эндпоинта для вызова и нет соединения на клиенте. После объединения выживают только скаляры (и null) — массивы, объекты и слишком большие значения отбрасываются, — а событие изолировано, поэтому слушатель, выбросивший исключение, приводит к отсутствующим значениям, а не к поломке списка. Столбцы работают и с бэкендом учётных записей Flex, и с файловым хранилищем учётных записей.
Действия в строке списка пользователей¶
Эти два события позволяют плагину добавить в список пользователей кнопку действия для пользователя — «Impersonate», «Send reset link», «Sync subscription», — которая выполняет серверную операцию над одной учётной записью. Они заменяют старый обходной путь с внедрением кнопки в отрендеренную таблицу через MutationObserver: Admin2 владеет таблицей, идентификацией строки и размещением кнопки, а плагин получает имя целевого пользователя на сервере. Модель повторяет элементы и действия панели меню, разделяясь на событие объявления и событие выполнения.
onApiUserListRowActions собирает кнопки, доступные через GET /users/row-actions (под разрешением api.users.read). Добавьте описание действия в массив actions:
public function onApiUserListRowActions(Event $event): void
{
// У события нет offsetGet по ссылке — добавляйте через чтение-изменение-запись.
$actions = $event['actions'];
$actions[] = [
'id' => 'impersonate-user', // обязательно, уникально — ключ выполнения
'plugin' => 'impersonate',
'label' => 'Impersonate', // обычный текст, не ключ перевода
'icon' => 'fa-user-secret', // необязательный класс Font Awesome
'action' => 'start', // необязательный глагол, передаваемый обратно обработчику
'confirm' => 'Impersonate this user?', // необязательное подтверждение на клиенте
'priority' => 80, // необязательно, большее значение сортируется раньше
'authorize' => ['admin.impersonate', 'admin.super'], // необязательно, строка или массив «любое из»
];
$event['actions'] = $actions;
}
Как и столбцы, описание не содержит форматтера: authorize повторно проверяется на сервере и удаляется из ответа, список ограничен по длине, а по сети не передаются ни HTML, ни функция рендеринга. authorize управляет только тем, какие кнопки отображаются, — это не граница безопасности.
onApiUserListRowAction выполняет действие. Admin2 вызывает его через POST /users/{username}/row-action с телом { "id": "<action-id>" }; API повторно проверяет authorize объявленного действия для вызывающей стороны (неизвестный или неавторизованный id даёт неотличимый 404), а затем вызывает событие с разрешённым именем целевого пользователя. Проверяйте $event['plugin'] — каждый слушатель получает каждый вызов, — повторно авторизуйте для конкретной цели и верните результат:
public function onApiUserListRowAction(Event $event): void
{
if (($event['plugin'] ?? '') !== 'impersonate') {
return;
}
$username = (string) $event['username'];
// Независимо перепроверьте разрешение для ЭТОЙ цели — фильтр объявления
// был только UX. Выбросьте ForbiddenException, чтобы вернуть настоящий 403.
$url = $this->createImpersonationUrl($username);
$event['result'] = [
'status' => 'success', // 'success' | 'error'
'message' => "Impersonating {$username}", // показывается во всплывающем уведомлении
'url' => $url, // необязательно — открывается в новой вкладке
];
}
Результат нормализуется до { status, message, url }. url проверяется на сервере, прежде чем достигнет клиента: выживает только путь от корня (не протокол-относительная форма //host) или абсолютный URL того же источника — схема javascript:/data: или URL другого источника отбрасывается, поэтому поток «вернуть URL и перейти туда» не может стать открытым перенаправлением. Admin2 показывает message как всплывающее уведомление и открывает выживший url в новой вкладке с noopener. Обработчик, выбросивший исключение, приводит к всплывающему уведомлению об ошибке, а не к поломке списка пользователей; выброшенный ForbiddenException распространяется как 403. Действия в строке требуют api.users.read, а вызывающая сторона, не являющаяся суперпользователем, не может выполнить его для учётной записи суперпользователя (403). Как и вкладки фильтров и столбцы, они работают с обоими бэкендами учётных записей.
Метки и перевод¶
Метки элементов для боковой панели, панели меню, вкладок фильтров, столбцов и действий в строке могут быть либо обычным отображаемым текстом ('Active'), либо ключом перевода ('PLUGIN_MYPLUGIN.ACTIVE'). Когда метка выглядит как ключ, API разрешает её на сервере на язык Admin Next вошедшего пользователя и возвращает переведённую строку, поэтому плагин должен регистрировать исходный ключ и не должен сам вызывать Language::translate(). Перевод в плагине использует активный контентный язык сайта, что даёт смешанный язык в админке (например, русскую метку внутри английского интерфейса), когда эти два языка различаются. Регистрируйте ключ; пусть API его локализует.
Сопоставление событий с вебхуками¶
Диспетчер исходящих вебхуков слушает часть перечисленных выше событий и преобразует их в события вебхуков со следующими именами:
| Событие API | Событие вебхука |
|---|---|
onApiPageCreated |
page.created |
onApiPageUpdated |
page.updated |
onApiPageDeleted |
page.deleted |
onApiPageMoved |
page.moved |
onApiPageTranslated |
page.translated |
onApiPagesReordered |
pages.reordered |
onApiMediaUploaded |
media.uploaded |
onApiMediaDeleted |
media.deleted |
onApiUserCreated |
user.created |
onApiUserUpdated |
user.updated |
onApiUserDeleted |
user.deleted |
onApiConfigUpdated |
config.updated |
onApiPackageInstalled |
gpm.installed |
onApiPackageRemoved |
gpm.removed |
onApiGravUpgraded |
grav.upgraded |
Создание подписок на вебхуки и управление ими см. в разделе Эндпоинты вебхуков. Вебхуки срабатывают после события API, поэтому слушатели, изменяющие состояние через события onApi*, увидят свои изменения в исходящем теле.
События, совместимые с админкой (onAdmin*)¶
API также вызывает те же события, что и классический плагин Admin в Grav. Это гарантирует, что сторонние плагины, подписанные на события админки (SEO Magic, Auto Date, Mega Frontmatter и т. д.), работают корректно независимо от того, приходят ли изменения из классического интерфейса админки или из API/Admin2.
Оба семейства событий срабатывают для каждой операции — сначала события админки, затем события API.
| Событие | Когда | Данные |
|---|---|---|
onAdminCreatePageFrontmatter |
Создание страницы (до сохранения) | header, data |
onAdminSave |
До сохранения (страница, пользователь, конфигурация) | object (по ссылке), page |
onAdminAfterSave |
После сохранения | object, page |
onAdminAfterDelete |
После удаления страницы | object, page |
onAdminAfterSaveAs |
После перемещения или переименования страницы | path |
onAdminAfterAddMedia |
После загрузки медиафайла | object, page |
onAdminAfterDelMedia |
После удаления медиафайла | object, page, media, filename |
Пример порядка событий¶
Для операции создания страницы события срабатывают в таком порядке:
onApiBeforePageCreate— событие API «до»onAdminCreatePageFrontmatter— внедрение метаданных страницы админкойonAdminSave— сохранение в админке «до» (плагины могут изменить страницу)onAdminAfterSave— сохранение в админке «после» (индексация, уведомления)onApiPageCreated— событие API «после» (запускает вебхуки)
Подписка из плагина¶
Не ставьте подписку на эти совместимые с админкой события за проверку isAdmin() в onPluginsInitialized(). Когда сохранение приходит через API (именно так Admin2 сохраняет всё), запрос не обрабатывается классическим плагином Admin, поэтому $grav['admin'] регистрируется позже, во время диспетчеризации запроса, через AdminProxy плагина API. На момент onPluginsInitialized() он ещё не установлен, поэтому isAdmin() по-прежнему возвращает false, и любые обработчики, зарегистрированные внутри блока isAdmin(), никогда не подписываются. В результате плагин, чья логика сохранения и удаления молча никогда не выполняется под Admin2.
Примечание
Распространённый приём эпохи 1.7 — регистрировать onAdminAfterSave только когда isAdmin() равно true. Для записей через API/Admin2 это больше не работает. Эти события в любом случае срабатывают только во время операций записи админки/API, поэтому подписываться на них безусловно безопасно.
Регистрируйте их напрямую в getSubscribedEvents() или безусловно в onPluginsInitialized():
public static function getSubscribedEvents(): array
{
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0],
// Регистрируются всегда — они срабатывают только при записи из админки/API:
'onAdminAfterSave' => ['onObjectSave', 0],
'onAdminAfterDelete' => ['onObjectDelete', 0],
'onAdminAfterSaveAs' => ['onObjectMove', 0],
'onFlexObjectAfterSave' => ['onObjectSave', 0],
'onFlexObjectAfterDelete' => ['onObjectDelete', 0],
];
}
Учтите, что объекты Flex (и Flex Pages) вызывают onFlexObjectAfterSave / onFlexObjectAfterDelete — варианты с *After*, — а не имена onFlexObjectSave / onFlexObjectDelete без приставки. Подписывайтесь на вариант, соответствующий нужным вам данным.
Регистрация маршрутов¶
Плагины расширяют API пользовательскими эндпоинтами через onApiRegisterRoutes. Два связанных события позволяют плагину открыть маршруты для публики и добавить инструменты MCP:
| Событие | Когда | Данные |
|---|---|---|
onApiRegisterRoutes |
Во время инициализации маршрутизатора | routes (ApiRouteCollector) |
onApiCollectPublicRoutes |
Один раз на запрос, до аутентификации | api_base, prefixes (изменяемое), exact (изменяемое) |
onApiMcpTools |
Пока GET /mcp/tools собирает определения инструментов MCP |
tools (объект McpToolCollector; вызывайте ->add($plugin, [...])) |
onApiCollectPublicRoutes позволяет плагину пометить собственные маршруты как публичные, чтобы они пропускали аутентификацию. Добавьте полные пути (включая api_base) в prefixes для совпадений по префиксу или в exact для точных совпадений. Запись можно ограничить одним методом, записав её как "GET /api/v1/my-plugin/feed", что оставляет записи по тому же пути аутентифицированными.
Подробности см. в руководстве Интеграция плагинов с API.
Системные события¶
| Событие | Когда | Данные |
|---|---|---|
onApiLogCleared |
После очистки файла журнала через DELETE /system/logs |
file, bytes, user |
onApiDemoBaselineCaptured |
После создания базового снимка демо-контента | status |
onApiDemoReset |
После сброса демо-контента к базовому снимку | status |
Использование событий в плагине¶
public static function getSubscribedEvents(): array
{
return [
'onApiPageCreated' => ['onPageChanged', 0],
'onApiPageUpdated' => ['onPageChanged', 0],
'onApiPageDeleted' => ['onPageChanged', 0],
];
}
public function onPageChanged(Event $event): void
{
// Перестроить поисковый индекс, очистить кэш CDN и т. п.
$page = $event['page'] ?? null;
$route = $event['route'] ?? $page?->route();
$this->rebuildIndex($route);
}