Event: PSR-14 lifecycle event taxonomy
At a glance
Section titled “At a glance”The Event module dispatches typed lifecycle events at each stage of PDF generation. Listeners can observe or transform a document without changing engine internals. The dispatcher follows PHP Standards Recommendation 14 (PSR-14), so existing PSR-14 tooling stays compatible.
Install
Section titled “Install”composer require nextpdf/core:^3The Event module ships with the core package. It has no extra dependencies:
the EventInterface and StoppableEventInterface types mirror the PSR-14
contracts without requiring the psr/event-dispatcher package.
Conceptual overview
Section titled “Conceptual overview”The module has three parts: a dispatcher, a listener provider, and a fixed set of lifecycle event classes.
EventDispatcher takes an EventInterface instance, asks the
ListenerProvider for matching listeners, and calls each one in priority
order. The dispatch() method returns the same event object. A listener can
read state the engine put into the event and write back state the engine reads
later. This matches the PSR-14 dispatcher model.
ListenerProvider maps an event class to a list of callables in priority
order. Registration is instance-scoped, with no static state, so a worker
process can hold its own provider instances. The provider also walks the event
class tree and its interfaces. A listener on AbstractEvent sees every
lifecycle event. A listener on an interface sees every event that implements
it.
StoppableEventInterface adds propagation control. A listener can call
stopPropagation(), and the dispatcher then stops calling later listeners for
that cycle. A stoppable event is a special event that carries its own way to
halt the listener chain. PSR-14 uses the same model for stoppable events
(PSR-14 psr_14_event#x4). AbstractEvent implements the interface through
StoppableEventTrait, so every lifecycle event is stoppable by default.
The dispatcher has a zero-overhead fast path. It checks for a listener on the
event class or any ancestor. When none exists, hasListeners() returns
false, and dispatch() returns at once. A document with no listeners pays
one boolean check per lifecycle point.
EventAwareDocumentTrait is the integration point. It holds an optional
EventDispatcher and exposes protected dispatch helpers. The Document class
calls these helpers at each lifecycle point. When no dispatcher is set, every
helper is a no-op.
The lifecycle taxonomy includes these events:
| Event | Namespace | Fired |
|---|---|---|
DocumentCreatedEvent | Event\Document | After a document is fully constructed |
PageAddedEvent | Event\Document | After a page is initialized |
ContentRenderedEvent | Event\Content | After HTML or text content is rendered to a page |
FontLoadedEvent | Event\Content | When a font is parsed into the registry |
EncryptionAppliedEvent | Event\Security | After encryption parameters are configured |
SignatureAppliedEvent | Event\Security | After a signature is embedded |
PdfSerializedEvent | Event\Writer | After serialization, before output delivery |
DocumentOutputEvent | Event\Document | Before PDF bytes reach the destination |
API surface
Section titled “API surface”| Symbol | Kind | Key members |
|---|---|---|
NextPDF\Event\EventInterface | interface | getEventName(): non-empty-string |
NextPDF\Event\StoppableEventInterface | interface | isPropagationStopped(): bool |
NextPDF\Event\StoppableEventTrait | trait | isPropagationStopped(), stopPropagation() |
NextPDF\Event\AbstractEvent | abstract class | getEventName(); implements StoppableEventInterface |
NextPDF\Event\EventDispatcher | final class | dispatch(EventInterface): EventInterface, getListenerProvider() |
NextPDF\Event\ListenerProvider | final class | addListener(), getListenersForEvent(), hasListeners(), getListenerCount(), clearListeners() |
NextPDF\Event\EventAwareDocumentTrait | trait | setEventDispatcher(), getEventDispatcher() |
NextPDF\Event\Document\DocumentCreatedEvent | final class | $document, $config |
NextPDF\Event\Document\PageAddedEvent | final class | $document, $pageIndex, $pageSize, $orientation |
NextPDF\Event\Document\DocumentOutputEvent | final class | getPdfData(), setPdfData(), getByteSize(), $filename, $destination |
NextPDF\Event\Content\ContentRenderedEvent | final class | $document, $pageIndex, $contentType, $content |
NextPDF\Event\Content\FontLoadedEvent | final class | $family, $style, $fontType, $filePath |
NextPDF\Event\Security\EncryptionAppliedEvent | final class | $document, $algorithm, $allowPrint, $allowCopy, $allowModify |
NextPDF\Event\Security\SignatureAppliedEvent | final class | $document, $signatureLevel, $signerName, $reason, $location |
NextPDF\Event\Writer\PdfSerializedEvent | final class | $byteSize, $objectCount, $pageCount, $pdfVersion, $isLinearized, $isEncrypted |
addListener() throws NextPDF\Exception\InvalidConfigException when the event
class string is empty.
Code sample — Quick start
Section titled “Code sample — Quick start”Register a listener, then dispatch a lifecycle event.
<?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);The Document class calls the dispatch helpers internally after a dispatcher
is attached through setEventDispatcher().
Code sample — Production
Section titled “Code sample — Production”Wire the dispatcher into a document, set listener priority, and stop propagation from a gate listener.
<?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).Edge cases & gotchas
Section titled “Edge cases & gotchas”- Wildcard registration uses the class hierarchy. A listener on
AbstractEventreceives every lifecycle event because every event extends it. Scope listeners to concrete classes when you want only one event. - Listener priority sorts highest first. Equal priorities keep insertion order (stable sort).
stopPropagation()halts only the current dispatch cycle. The next dispatched event starts a fresh cycle.- The sorted listener cache invalidates on any
addListener()call because a new parent or interface registration can change resolution for several event classes. $documenton document-scoped events is typedobject, not theDocumentclass, to keep the Event module free of a hardCoredependency.DocumentOutputEvent::setPdfData()expects a non-empty string. Replacing the payload with an empty string produces an invalid document.- The dispatch helpers on
EventAwareDocumentTraitare no-ops until a dispatcher is set, so listener-free runs add no measurable cost.
Performance
Section titled “Performance”Dispatch with no listeners is O(1) for a leaf event class: one
hasListeners() boolean check, then an immediate return. With listeners,
getListenersForEvent() walks the event ancestry once, sorts the collected
entries, and caches the sorted list per event class until the next mutation.
A repeat dispatch of the same class is therefore O(k) over k matched
listeners. The default performance_budget for this reference page is
wall_ms: 1500, peak_mb: 64.
Security notes
Section titled “Security notes”Listeners run inside the generation pipeline with the same privileges as the
caller. Treat listener code as trusted code. DocumentOutputEvent exposes the
final PDF binary and lets a listener replace it. An audit or integrity listener
should run before any listener that transforms the bytes. Use a higher
priority. The security-scoped events (EncryptionAppliedEvent,
SignatureAppliedEvent) report applied parameters for audit logging. They do
not change the cryptographic outcome.
Conformance
Section titled “Conformance”| Spec | Clause | Topic |
|---|---|---|
| PSR-14 (PHP-FIG) | psr_14_event#x4 | Stoppable event halts further listeners |
The dispatch() signature, the listener-provider split, and the
stoppable-event model follow PSR-14. NextPDF declares its own
EventInterface and StoppableEventInterface. The package has no PSR-14
runtime dependency, while remaining duck-type compatible.
See also
Section titled “See also”/modules/core/contracts/— public interface surface/modules/core/observability/— telemetry and metrics hooks/modules/core/audit/— audit-trail integration/modules/core/config/—Configpassed onDocumentCreatedEvent/modules/core/exception/—InvalidConfigExceptionfromaddListener()
Glossary: PSR-14 · stoppable event · listener provider