Перейти к содержимому

Триггеры действий и обработчики событий

NextPDF запускает события жизненного цикла через механизм в NextPDF\Event, совместимый с PHP Standards Recommendation 14 (PSR-14). Зарегистрируйте обработчик, выберите приоритет и реагируйте, когда создан документ, добавлена страница, загружен шрифт, выполняется подписание или шифрование, записываются байты или начинается вывод. Останавливайте цепочку только при необходимости.

Окно терминала
composer require nextpdf/core:^3

У системы событий две публичные части. ListenerProvider сопоставляет каждый класс события с отсортированным списком вызываемых обработчиков. EventDispatcher проходит по этому списку и вызывает каждый обработчик в порядке приоритета. Оба класса объявлены final, поэтому расширяйте поведение через композицию, а не наследование.

Оба класса совместимы с PSR-14 за счёт утиной типизации. EventDispatcher::dispatch() использует сигнатуру dispatch() из PSR-14 и возвращает событие после того, как отработают все обработчики. ListenerProvider::getListenersForEvent() использует сигнатуру провайдера из PSR-14. NextPDF не требует пакета PSR-14. Если в вашем проекте он установлен, интерфейсы всё равно совпадают.

Для авторов расширений важны два варианта поведения:

  • Прослушивание по шаблону. Чтобы разрешить обработчики, провайдер обходит родительские классы и интерфейсы события. Привяжите обработчик к базовому классу AbstractEvent, чтобы отслеживать каждое событие жизненного цикла. Привяжите обработчик к интерфейсу, чтобы перехватывать всё семейство событий.
  • Приоритет и распространение. Обработчик с более высоким приоритетом выполняется первым. При равных приоритетах сохраняется порядок регистрации. Каждое событие, наследующее AbstractEvent, можно остановить. Обработчик может вызвать stopPropagation(), и тогда диспетчер пропустит остальные.

У диспетчера есть быстрый путь без лишних затрат. Когда для класса события или любого его родителя не привязан ни один обработчик, dispatch() возвращается сразу после одной проверки hasListeners().

СобытиеПространство имёнСрабатывает, когдаСтабильность
DocumentCreatedEventNextPDF\Event\DocumentЗавершается построение документаэкспериментальная
PageAddedEventNextPDF\Event\DocumentСтраница полностью инициализированаэкспериментальная
ContentRenderedEventNextPDF\Event\ContentСодержимое отрисовано на страницеэкспериментальная
FontLoadedEventNextPDF\Event\ContentСемейство и начертание шрифта загружаются впервыеэкспериментальная
SignatureAppliedEventNextPDF\Event\SecurityВнедрены байты подписиэкспериментальная
EncryptionAppliedEventNextPDF\Event\SecurityНастроено шифрованиеэкспериментальная
PdfSerializedEventNextPDF\Event\WriterЗавершается сериализацияэкспериментальная
DocumentOutputEventNextPDF\Event\DocumentВот-вот начнётся доставка выводаэкспериментальная

Диспетчер, провайдер, маркерный интерфейс и базовый класс имеют статус stable (начиная с 3.0.0). Полезные данные событий имеют статус experimental. Их аргументы конструктора и свойства readonly могут измениться в минорном выпуске. Патч-выпуски только добавляют новое. Привязывайтесь к именам свойств полезных данных с учётом этого ограничения.

NextPDF\Event\ListenerProvider (стабильный, финальный):

МетодВозвращаетНазначение
addListener(string $eventClass, callable $listener, int $priority = 0)voidЗарегистрировать обработчик; более высокий приоритет выполняется первым. Выбрасывает InvalidConfigException, если класс пуст.
getListenersForEvent(EventInterface $event)list<callable>Разрешить обработчики, включая регистрации на родителях и интерфейсах.
hasListeners(string $eventClass)boolПроверить иерархию классов без накладных расходов.
getListenerCount(string $eventClass)intПодсчитать только прямые регистрации.
clearListeners()voidСбросить провайдер.

NextPDF\Event\EventDispatcher (стабильный, финальный):

МетодВозвращаетНазначение
dispatch(EventInterface $event)EventInterfaceВызвать обработчики в порядке приоритета, учесть остановку распространения и вернуть событие.
getListenerProvider()ListenerProviderПолучить доступ к провайдеру, чтобы добавлять обработчики во время выполнения.

Документы, запускающие события, используют NextPDF\Event\EventAwareDocumentTrait. Его метод setEventDispatcher() подключает диспетчер к одному документу. Без диспетчера каждый вспомогательный метод диспетчеризации ничего не делает.

<?php
declare(strict_types=1);
use NextPDF\Event\EventDispatcher;
use NextPDF\Event\ListenerProvider;
use NextPDF\Event\Security\SignatureAppliedEvent;
$listeners = new ListenerProvider();
$listeners->addListener(
SignatureAppliedEvent::class,
static function (SignatureAppliedEvent $event): void {
\error_log("Signed at level {$event->signatureLevel} by {$event->signerName}");
},
priority: 100,
);
$dispatcher = new EventDispatcher($listeners);

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

<?php
declare(strict_types=1);
use NextPDF\Event\AbstractEvent;
use NextPDF\Event\EventDispatcher;
use NextPDF\Event\ListenerProvider;
use NextPDF\Event\Security\EncryptionAppliedEvent;
use NextPDF\Event\Security\SignatureAppliedEvent;
use Psr\Log\LoggerInterface;
final class SecurityAuditSubscriber
{
public function __construct(private readonly LoggerInterface $logger) {}
public function register(ListenerProvider $listeners): EventDispatcher
{
$listeners->addListener(
SignatureAppliedEvent::class,
function (SignatureAppliedEvent $event): void {
$this->logger->info('signature.applied', [
'level' => $event->signatureLevel,
'signer' => $event->signerName,
]);
},
priority: 1000,
);
$listeners->addListener(
EncryptionAppliedEvent::class,
function (EncryptionAppliedEvent $event): void {
$this->logger->info('encryption.applied', [
'algorithm' => $event->algorithm,
]);
},
priority: 1000,
);
// Catch-all: observe every lifecycle event for trace completeness.
$listeners->addListener(
AbstractEvent::class,
fn (AbstractEvent $event): mixed
=> $this->logger->debug('lifecycle', ['event' => $event->getEventName()]),
priority: -1000,
);
return new EventDispatcher($listeners);
}
}
  • Финальные классы. EventDispatcher и ListenerProvider объявлены final. Используйте их через композицию; не наследуйтесь от них.
  • Пустой класс события выбрасывает исключение. addListener('', ...) выбрасывает InvalidConfigException. Всегда передавайте константу class-string.
  • Стоимость прослушивания по шаблону. Обработчик на AbstractEvent срабатывает для каждого события. Задавайте универсальным обработчикам низкий приоритет и сохраняйте их недорогими.
  • Изменение вывода. DocumentOutputEvent несёт байты Portable Document Format (PDF). Движок считывает их обратно после диспетчеризации. Если вы измените эти байты, вы получите широкий контроль и такой же широкий риск. Неверное смещение байта повреждает PDF и может нарушить подпись. Предпочтительнее наблюдать, если только вы не отвечаете за результат ради детерминизма и подписей.
  • Нет диспетчера — нет событий. Документ без диспетчера, заданного через EventAwareDocumentTrait, не запускает события. Это предусмотренный путь без лишних затрат, а не ошибка настройки.

Быстрый путь — это одна проверка hasListeners() по цепочке родителей. Без обработчиков диспетчеризация почти ничего не стоит. Провайдер кэширует отсортированный список обработчиков для каждого класса события и очищает этот кэш только при изменении обработчиков. Делайте обработчики неблокирующими, потому что они выполняются прямо на пути отрисовки.

SignatureAppliedEvent и EncryptionAppliedEvent — это точки привязки аудита. Регистрируйте обработчики с высоким приоритетом, чтобы записывать подписание и шифрование в защищённое от подделки хранилище. Не останавливайте цепочку на событии безопасности, если только вы не намерены заглушить последующие обработчики. Остановка цепочки может незаметно отключить хуки аудита, которые выполняются после.

Эта страница не делает нормативных утверждений сверх совместимости с PSR-14. Эта совместимость основана только на утиной типизации и не требует пакета PSR-14.

NextPDF Enterprise поставляет проверенные обработчики для событий подписания и шифрования, которые наполняют защищённый от подделки журнал аудита. Поскольку контракт обработчика — это публичный API событий (application programming interface, API), ваши собственные обработчики могут сосуществовать с обработчиками Enterprise в одном провайдере.

Глоссарий определяет термины event listener, event dispatcher, listener provider и stoppable event, используемые на этой странице. Канонические определения смотрите в опубликованном глоссарии.