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 вызывает эти вспомогательные методы в каждой точке жизненного цикла. Если диспетчер не задан, каждый вспомогательный метод ничего не делает.
Таксономия жизненного цикла включает следующие события:
| Событие | Пространство имён | Вызывается |
|---|---|---|
DocumentCreatedEvent | Event\Document | После полного построения документа |
PageAddedEvent | Event\Document | После инициализации страницы |
ContentRenderedEvent | Event\Content | После отрисовки HTML- или текстового содержимого на странице |
FontLoadedEvent | Event\Content | После разбора шрифта и помещения его в реестр |
EncryptionAppliedEvent | Event\Security | После настройки параметров шифрования |
SignatureAppliedEvent | Event\Security | После встраивания подписи |
PdfSerializedEvent | Event\Writer | После сериализации, перед выдачей результата |
DocumentOutputEvent | Event\Document | Перед передачей байтов PDF в место назначения |
Поверхность API
Заголовок раздела «Поверхность API»| Символ | Вид | Ключевые члены |
|---|---|---|
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 · останавливаемое событие · поставщик слушателей