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

Event: таксономия событий жизненного цикла PSR-14

Модуль Event отправляет типизированные события жизненного цикла на каждом этапе генерации PDF. Слушатели могут наблюдать за документом или преобразовывать его, не меняя внутреннюю реализацию движка. Диспетчер следует PHP Standards Recommendation 14 (PSR-14), поэтому существующие инструменты PSR-14 остаются совместимыми.

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

Модуль Event поставляется вместе с пакетом ядра. У него нет дополнительных зависимостей: типы EventInterface и StoppableEventInterface повторяют контракты PSR-14 и не требуют пакета psr/event-dispatcher.

Модуль состоит из трёх частей: диспетчера, поставщика слушателей и фиксированного набора классов событий жизненного цикла.

EventDispatcher принимает экземпляр EventInterface, запрашивает у ListenerProvider соответствующих слушателей и вызывает их по очереди в порядке приоритета. Метод dispatch() возвращает тот же объект события. Слушатель может прочитать состояние, которое движок поместил в событие, и записать состояние, которое движок прочитает позже. Это соответствует модели диспетчера PSR-14.

ListenerProvider сопоставляет класс события со списком вызываемых объектов в порядке приоритета. Регистрация хранится в экземпляре и не использует статическое состояние, поэтому каждый рабочий процесс может иметь собственные экземпляры поставщика. Поставщик также обходит дерево классов события и его интерфейсы. Слушатель, зарегистрированный для AbstractEvent, видит каждое событие жизненного цикла. Слушатель, зарегистрированный для интерфейса, видит каждое событие, которое его реализует.

StoppableEventInterface добавляет управление распространением. Слушатель может вызвать stopPropagation(), после чего диспетчер перестаёт вызывать последующих слушателей в этом цикле. Останавливаемое событие содержит собственный механизм прерывания цепочки слушателей. PSR-14 использует ту же модель для останавливаемых событий (PSR-14 psr_14_event#x4). AbstractEvent реализует интерфейс через StoppableEventTrait, поэтому каждое событие жизненного цикла по умолчанию является останавливаемым.

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

EventAwareDocumentTrait — точка интеграции. Он хранит необязательный EventDispatcher и предоставляет защищённые вспомогательные методы отправки. Класс Document вызывает эти вспомогательные методы в каждой точке жизненного цикла. Если диспетчер не задан, каждый вспомогательный метод ничего не делает.

Таксономия жизненного цикла включает следующие события:

СобытиеПространство имёнВызывается
DocumentCreatedEventEvent\DocumentПосле полного построения документа
PageAddedEventEvent\DocumentПосле инициализации страницы
ContentRenderedEventEvent\ContentПосле отрисовки HTML- или текстового содержимого на странице
FontLoadedEventEvent\ContentПосле разбора шрифта и помещения его в реестр
EncryptionAppliedEventEvent\SecurityПосле настройки параметров шифрования
SignatureAppliedEventEvent\SecurityПосле встраивания подписи
PdfSerializedEventEvent\WriterПосле сериализации, перед выдачей результата
DocumentOutputEventEvent\DocumentПеред передачей байтов PDF в место назначения
СимволВидКлючевые члены
NextPDF\Event\EventInterfaceинтерфейсgetEventName(): non-empty-string
NextPDF\Event\StoppableEventInterfaceинтерфейсisPropagationStopped(): bool
NextPDF\Event\StoppableEventTraitтрейтisPropagationStopped(), stopPropagation()
NextPDF\Event\AbstractEventабстрактный классgetEventName(); реализует StoppableEventInterface
NextPDF\Event\EventDispatcherфинальный классdispatch(EventInterface): EventInterface, getListenerProvider()
NextPDF\Event\ListenerProviderфинальный классaddListener(), getListenersForEvent(), hasListeners(), getListenerCount(), clearListeners()
NextPDF\Event\EventAwareDocumentTraitтрейтsetEventDispatcher(), getEventDispatcher()
NextPDF\Event\Document\DocumentCreatedEventфинальный класс$document, $config
NextPDF\Event\Document\PageAddedEventфинальный класс$document, $pageIndex, $pageSize, $orientation
NextPDF\Event\Document\DocumentOutputEventфинальный классgetPdfData(), setPdfData(), getByteSize(), $filename, $destination
NextPDF\Event\Content\ContentRenderedEventфинальный класс$document, $pageIndex, $contentType, $content
NextPDF\Event\Content\FontLoadedEventфинальный класс$family, $style, $fontType, $filePath
NextPDF\Event\Security\EncryptionAppliedEventфинальный класс$document, $algorithm, $allowPrint, $allowCopy, $allowModify
NextPDF\Event\Security\SignatureAppliedEventфинальный класс$document, $signatureLevel, $signerName, $reason, $location
NextPDF\Event\Writer\PdfSerializedEventфинальный класс$byteSize, $objectCount, $pageCount, $pdfVersion, $isLinearized, $isEncrypted

addListener() выбрасывает NextPDF\Exception\InvalidConfigException, если строка класса события пуста.

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

<?php
declare(strict_types=1);
use NextPDF\Event\Document\PageAddedEvent;
use NextPDF\Event\EventDispatcher;
use NextPDF\Event\ListenerProvider;
$provider = new ListenerProvider();
$provider->addListener(
PageAddedEvent::class,
static function (PageAddedEvent $event): void {
\printf("Page %d added (%s)\n", $event->pageIndex, $event->pageSize->name);
},
);
$dispatcher = new EventDispatcher($provider);

Класс Document сам вызывает вспомогательные методы отправки после подключения диспетчера через setEventDispatcher().

Подключите диспетчер к документу, задайте приоритет слушателей и остановите распространение в слушателе-шлюзе.

<?php
declare(strict_types=1);
use NextPDF\Event\Document\DocumentOutputEvent;
use NextPDF\Event\Document\PageAddedEvent;
use NextPDF\Event\EventDispatcher;
use NextPDF\Event\ListenerProvider;
$provider = new ListenerProvider();
// Higher priority runs first. A licensing gate observes every page.
$provider->addListener(
PageAddedEvent::class,
static function (PageAddedEvent $event): void {
if ($event->pageIndex >= 100) {
// Stop later page listeners for this dispatch cycle.
$event->stopPropagation();
}
},
priority: 100,
);
// Last-chance hook: replace the PDF bytes before delivery.
$provider->addListener(
DocumentOutputEvent::class,
static function (DocumentOutputEvent $event): void {
$optimized = \gzencode($event->getPdfData(), 0);
if ($optimized !== false) {
$event->setPdfData($optimized);
}
},
);
$dispatcher = new EventDispatcher($provider);
// Pass $dispatcher to a Document via setEventDispatcher($dispatcher).
  • Регистрация по общему типу использует иерархию классов. Слушатель, зарегистрированный для AbstractEvent, получает каждое событие жизненного цикла, потому что каждое событие его расширяет. Ограничивайте слушателей конкретными классами, когда вам нужно только одно событие.
  • Слушатели сортируются по приоритету от наибольшего к наименьшему. При равных приоритетах сохраняется порядок вставки (устойчивая сортировка).
  • stopPropagation() прерывает только текущий цикл отправки. Следующее отправленное событие начинает новый цикл.
  • Кеш отсортированных слушателей сбрасывается при каждом вызове addListener(), потому что регистрация нового родителя или интерфейса может изменить результат разрешения для нескольких классов событий.
  • $document в событиях уровня документа имеет тип object, а не класс Document, чтобы модуль Event не был жёстко зависим от Core.
  • DocumentOutputEvent::setPdfData() ожидает непустую строку. Замена полезной нагрузки пустой строкой делает документ недопустимым.
  • Вспомогательные методы отправки в EventAwareDocumentTrait ничего не делают, пока не задан диспетчер, поэтому запуски без слушателей не добавляют ощутимых затрат.

Отправка без слушателей имеет сложность O(1) для конечного класса события: одна булева проверка hasListeners(), затем немедленный возврат. При наличии слушателей getListenersForEvent() один раз обходит предков события, сортирует собранные записи и кеширует отсортированный список для каждого класса события до следующего изменения регистрации. Поэтому повторная отправка того же класса имеет сложность O(k) для k подходящих слушателей. Бюджет по умолчанию performance_budget для этой справочной страницы — wall_ms: 1500, peak_mb: 64.

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

СтандартПунктТема
PSR-14 (PHP-FIG)psr_14_event#x4Останавливаемое событие прерывает дальнейших слушателей

Сигнатура dispatch(), разделение на диспетчер и поставщика слушателей, а также модель останавливаемого события следуют PSR-14. NextPDF объявляет собственные EventInterface и StoppableEventInterface. У пакета нет зависимости от PSR-14 во время выполнения, но он сохраняет совместимость за счёт утиной типизации.

  • /modules/core/contracts/ — публичные интерфейсы
  • /modules/core/observability/ — перехватчики телеметрии и метрик
  • /modules/core/audit/ — интеграция с журналом аудита
  • /modules/core/config/Config, передаваемый в DocumentCreatedEvent
  • /modules/core/exception/InvalidConfigException из addListener()

Глоссарий: PSR-14 · останавливаемое событие · поставщик слушателей