Skip to content

Event: PSR-14 lifecycle event taxonomy

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.

Terminal window
composer require nextpdf/core:^3

The 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.

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:

EventNamespaceFired
DocumentCreatedEventEvent\DocumentAfter a document is fully constructed
PageAddedEventEvent\DocumentAfter a page is initialized
ContentRenderedEventEvent\ContentAfter HTML or text content is rendered to a page
FontLoadedEventEvent\ContentWhen a font is parsed into the registry
EncryptionAppliedEventEvent\SecurityAfter encryption parameters are configured
SignatureAppliedEventEvent\SecurityAfter a signature is embedded
PdfSerializedEventEvent\WriterAfter serialization, before output delivery
DocumentOutputEventEvent\DocumentBefore PDF bytes reach the destination
SymbolKindKey members
NextPDF\Event\EventInterfaceinterfacegetEventName(): non-empty-string
NextPDF\Event\StoppableEventInterfaceinterfaceisPropagationStopped(): bool
NextPDF\Event\StoppableEventTraittraitisPropagationStopped(), stopPropagation()
NextPDF\Event\AbstractEventabstract classgetEventName(); implements StoppableEventInterface
NextPDF\Event\EventDispatcherfinal classdispatch(EventInterface): EventInterface, getListenerProvider()
NextPDF\Event\ListenerProviderfinal classaddListener(), getListenersForEvent(), hasListeners(), getListenerCount(), clearListeners()
NextPDF\Event\EventAwareDocumentTraittraitsetEventDispatcher(), getEventDispatcher()
NextPDF\Event\Document\DocumentCreatedEventfinal class$document, $config
NextPDF\Event\Document\PageAddedEventfinal class$document, $pageIndex, $pageSize, $orientation
NextPDF\Event\Document\DocumentOutputEventfinal classgetPdfData(), setPdfData(), getByteSize(), $filename, $destination
NextPDF\Event\Content\ContentRenderedEventfinal class$document, $pageIndex, $contentType, $content
NextPDF\Event\Content\FontLoadedEventfinal class$family, $style, $fontType, $filePath
NextPDF\Event\Security\EncryptionAppliedEventfinal class$document, $algorithm, $allowPrint, $allowCopy, $allowModify
NextPDF\Event\Security\SignatureAppliedEventfinal class$document, $signatureLevel, $signerName, $reason, $location
NextPDF\Event\Writer\PdfSerializedEventfinal class$byteSize, $objectCount, $pageCount, $pdfVersion, $isLinearized, $isEncrypted

addListener() throws NextPDF\Exception\InvalidConfigException when the event class string is empty.

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().

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).
  • Wildcard registration uses the class hierarchy. A listener on AbstractEvent receives 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.
  • $document on document-scoped events is typed object, not the Document class, to keep the Event module free of a hard Core dependency.
  • DocumentOutputEvent::setPdfData() expects a non-empty string. Replacing the payload with an empty string produces an invalid document.
  • The dispatch helpers on EventAwareDocumentTrait are no-ops until a dispatcher is set, so listener-free runs add no measurable cost.

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.

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.

SpecClauseTopic
PSR-14 (PHP-FIG)psr_14_event#x4Stoppable 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.

  • /modules/core/contracts/ — public interface surface
  • /modules/core/observability/ — telemetry and metrics hooks
  • /modules/core/audit/ — audit-trail integration
  • /modules/core/config/Config passed on DocumentCreatedEvent
  • /modules/core/exception/InvalidConfigException from addListener()

Glossary: PSR-14 · stoppable event · listener provider