Skip to content

Action triggers and event listeners

NextPDF fires lifecycle events through a system in NextPDF\Event that is compatible with PHP Standards Recommendation 14 (PSR-14). Register a listener, choose a priority, and react when a document is created, a page is added, a font loads, signing or encryption runs, bytes are written, or output begins. Stop the chain only when you need to.

Terminal window
composer require nextpdf/core:^3

The event system has two public parts. ListenerProvider maps each event class to a sorted list of listener callables. EventDispatcher walks that list and calls each listener in priority order. Both classes are final, so extend the behavior with composition, not subclassing.

Both classes match PSR-14 by duck typing. EventDispatcher::dispatch() uses the PSR-14 dispatch() signature and returns the event after every listener runs. ListenerProvider::getListenersForEvent() uses the PSR-14 provider signature. NextPDF does not require the PSR-14 package. If your project installs it, the interfaces still line up.

Two behaviors matter for extension authors:

  • Wildcard listening. To resolve listeners, the provider walks the event’s parent classes and interfaces. Bind a listener to the AbstractEvent base class to watch every lifecycle event. Bind one to an interface to catch a family.
  • Priority and propagation. Higher priority runs first. Equal priorities keep registration order. Every event that extends AbstractEvent is stoppable. A listener can call stopPropagation(), and the dispatcher then skips the rest.

The dispatcher has a zero-cost fast path. When no listener is bound for an event class or any parent, dispatch() returns immediately after one hasListeners() check.

EventNamespaceFired whenStability
DocumentCreatedEventNextPDF\Event\DocumentDocument construction finishesexperimental
PageAddedEventNextPDF\Event\DocumentA page is fully initializedexperimental
ContentRenderedEventNextPDF\Event\ContentContent is rendered to a pageexperimental
FontLoadedEventNextPDF\Event\ContentA font family and style load for the first timeexperimental
SignatureAppliedEventNextPDF\Event\SecuritySignature bytes are embeddedexperimental
EncryptionAppliedEventNextPDF\Event\SecurityEncryption is configuredexperimental
PdfSerializedEventNextPDF\Event\WriterSerialization completesexperimental
DocumentOutputEventNextPDF\Event\DocumentOutput delivery is about to beginexperimental

The dispatcher, provider, marker interface, and base class are stable (since 3.0.0). The event payloads are experimental. Their constructor arguments and readonly properties may change in a minor release. Patch releases only add. Bind to payload property names with that constraint in mind.

NextPDF\Event\ListenerProvider (stable, final):

MethodReturnsPurpose
addListener(string $eventClass, callable $listener, int $priority = 0)voidRegister a listener; higher priority runs first. Throws InvalidConfigException when the class is empty.
getListenersForEvent(EventInterface $event)list<callable>Resolve listeners, including registrations on parents and interfaces.
hasListeners(string $eventClass)boolCheck the class hierarchy with zero overhead.
getListenerCount(string $eventClass)intCount direct registrations only.
clearListeners()voidReset the provider.

NextPDF\Event\EventDispatcher (stable, final):

MethodReturnsPurpose
dispatch(EventInterface $event)EventInterfaceInvoke listeners in priority order, respect propagation stops, and return the event.
getListenerProvider()ListenerProviderAccess the provider to add listeners at runtime.

Documents that fire events use NextPDF\Event\EventAwareDocumentTrait. Its setEventDispatcher() method wires a dispatcher into one document. With no dispatcher, every dispatch helper does nothing.

<?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);

This production audit listener uses high priority so it runs first, writes structured logs, and adds a catch-all on the base class for completeness.

<?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);
}
}
  • Final classes. EventDispatcher and ListenerProvider are final. Compose them; do not subclass them.
  • Empty event class throws. addListener('', ...) throws InvalidConfigException. Always pass a class-string constant.
  • Wildcard cost. A listener on AbstractEvent fires for every event. Give catch-all listeners low priority, and keep them cheap.
  • Output mutation. DocumentOutputEvent carries Portable Document Format (PDF) bytes. The engine reads them back after dispatch. If you change those bytes, you get broad control and broad risk. A wrong byte offset corrupts the PDF and can break a signature. Prefer to observe unless you own the result for determinism and signatures.
  • No dispatcher, no events. A document with no dispatcher set through EventAwareDocumentTrait fires no events. This is the intended zero-cost path, not a setup error.

The fast path is one hasListeners() parent-chain check. With no listeners, dispatch is almost free. The provider caches the sorted listener list per event class and clears that cache only when listeners change. Keep listeners non-blocking because they run inline on the render path.

SignatureAppliedEvent and EncryptionAppliedEvent are the audit anchors. Register high-priority listeners to log signing and encryption into a tamper-evident store. Do not stop the chain on a security event unless you intend to mute later listeners. Stopping it can quietly disable audit hooks that run afterward.

This page makes no normative claims beyond PSR-14 compatibility. That compatibility is duck-type only and does not require the PSR-14 package.

NextPDF Enterprise ships audited listeners for signature and encryption events that feed a tamper-evident audit log. Because the listener contract is the public event application programming interface (API), your own listeners can coexist with the Enterprise ones on the same provider.

The glossary defines the event listener, event dispatcher, listener provider, and stoppable event terms used on this page. See the published glossary for canonical definitions.