Zum Inhalt springen

Event: PSR-14-Taxonomie für Lebenszyklus-Events

Das Event-Modul löst in jeder Phase der PDF-Erzeugung typisierte Lebenszyklus-Events aus. Listener beobachten oder transformieren ein Dokument, ohne die internen Abläufe der Engine zu verändern. Der Dispatcher folgt dem PSR-14-Modell; dadurch bleibt bestehendes PSR-14-Tooling kompatibel.

Terminal-Fenster
composer require nextpdf/core:^3

Das Event-Modul wird mit dem Core-Paket ausgeliefert. Es bringt keine zusätzliche Abhängigkeit mit: Die Typen EventInterface und StoppableEventInterface bilden die PSR-14-Verträge nach, ohne das Paket psr/event-dispatcher vorauszusetzen.

Das Modul besteht aus drei Teilen: einem Dispatcher, einem Listener-Provider und einem festen Satz von Lebenszyklus-Event-Klassen.

EventDispatcher nimmt eine Instanz von EventInterface entgegen, fragt beim ListenerProvider die passenden Listener ab und ruft sie anschließend in Prioritätsreihenfolge auf. Die Methode dispatch() gibt dasselbe Event-Objekt zurück. Dadurch kann ein Listener Zustand lesen, den die Engine im Event abgelegt hat, und Zustand zurückschreiben, den die Engine später ausliest. Das entspricht der Form eines PSR-14-Dispatchers.

ListenerProvider bildet eine Event-Klasse auf eine Liste von Callables in Prioritätsreihenfolge ab. Die Registrierung ist instanzgebunden; es gibt keinen statischen Zustand. Ein Worker-Prozess kann eigene Provider-Instanzen halten. Der Provider durchläuft außerdem die Klassenhierarchie des Events und seine Interfaces. So erhält ein Listener für AbstractEvent jedes Lebenszyklus-Event. Ein Listener für ein Interface erhält jedes Event, das dieses Interface implementiert.

StoppableEventInterface ergänzt eine Steuerung der Weitergabe. Ein Listener kann stopPropagation() aufrufen. Der Dispatcher ruft dann für diesen Zyklus keine weiteren Listener mehr auf. Ein stoppbares Event ist ein Sonderfall und bringt seinen eigenen Mechanismus mit, um die Listener-Kette anzuhalten. PSR-14 beschreibt dasselbe Modell für stoppbare Events (PSR-14 psr_14_event#x4). AbstractEvent implementiert das Interface über StoppableEventTrait. Damit ist jedes Lebenszyklus-Event standardmäßig stoppbar.

Der Dispatcher hat einen Schnellpfad ohne Overhead. Er prüft, ob es einen Listener für die Event-Klasse oder einen beliebigen Vorfahren gibt. Ist keiner vorhanden, liefert hasListeners() den Wert false, und dispatch() kehrt sofort zurück. Ein Dokument ohne Listener verursacht an jedem Lebenszyklus-Punkt nur eine boolesche Prüfung.

EventAwareDocumentTrait bildet die Integrationsstelle und hält einen optionalen EventDispatcher. Es stellt geschützte Dispatch-Helfer bereit. Die Klasse Document ruft diese Helfer an jedem Lebenszyklus-Punkt auf. Wenn kein Dispatcher gesetzt ist, ist jeder Helfer ein No-Op.

Die Lebenszyklus-Event-Taxonomie:

EventNamespaceAusgelöst
DocumentCreatedEventEvent\DocumentNachdem ein Dokument vollständig konstruiert wurde
PageAddedEventEvent\DocumentNachdem eine Seite initialisiert wurde
ContentRenderedEventEvent\ContentNachdem HTML- oder Textinhalt auf eine Seite gerendert wurde
FontLoadedEventEvent\ContentWenn eine Schrift in die Registry geparst wird
EncryptionAppliedEventEvent\SecurityNachdem die Verschlüsselungsparameter konfiguriert wurden
SignatureAppliedEventEvent\SecurityNachdem eine Signatur eingebettet wurde
PdfSerializedEventEvent\WriterNach der Serialisierung, vor der Auslieferung der Ausgabe
DocumentOutputEventEvent\DocumentBevor die PDF-Bytes ihr Ziel erreichen
SymbolArtWichtige Mitglieder
NextPDF\Event\EventInterfaceinterfacegetEventName(): non-empty-string
NextPDF\Event\StoppableEventInterfaceinterfaceisPropagationStopped(): bool
NextPDF\Event\StoppableEventTraittraitisPropagationStopped(), stopPropagation()
NextPDF\Event\AbstractEventabstract classgetEventName(); implementiert 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() wirft NextPDF\Exception\InvalidConfigException, wenn der Event-Klassenstring leer ist.

Registrieren Sie einen Listener und lösen Sie ein Lebenszyklus-Event aus.

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

Die Klasse Document ruft die Dispatch-Helfer intern auf, sobald ein Dispatcher über setEventDispatcher() angebunden wurde.

Verdrahten Sie den Dispatcher mit einem Dokument, nutzen Sie Listener-Priorität und stoppen Sie die Weitergabe aus einem Gate-Listener heraus.

<?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).
  • Die Wildcard-Registrierung nutzt die Klassenhierarchie. Ein Listener für AbstractEvent empfängt jedes Lebenszyklus-Event, weil jedes Event davon erbt. Beschränken Sie Listener auf konkrete Klassen, wenn Sie nur ein einziges Event adressieren möchten.
  • Die Listener-Priorität sortiert die höchste Priorität zuerst. Gleiche Prioritäten behalten die Einfügereihenfolge (stabile Sortierung).
  • stopPropagation() hält nur den aktuellen Dispatch-Zyklus an. Das nächste ausgelöste Event beginnt einen frischen Zyklus.
  • Der sortierte Listener-Cache wird bei jedem Aufruf von addListener() ungültig, da eine neue Eltern- oder Interface-Registrierung die Auflösung für mehrere Event-Klassen ändern kann.
  • $document auf den dokumentbezogenen Events ist als object typisiert, nicht als Klasse Document, um das Event-Modul von einer harten Abhängigkeit von Core freizuhalten.
  • DocumentOutputEvent::setPdfData() erwartet einen nicht leeren String. Wird die Nutzlast durch einen leeren String ersetzt, entsteht ein ungültiges Dokument.
  • Die Dispatch-Helfer in EventAwareDocumentTrait sind No-Ops, bis ein Dispatcher gesetzt ist, sodass Läufe ohne Listener keine messbaren Kosten verursachen.

Ein Dispatch ohne Listener ist O(1) für eine Blatt-Event-Klasse: eine boolesche Prüfung mit hasListeners(), danach die sofortige Rückkehr. Mit Listenern durchläuft getListenersForEvent() die Event-Abstammung einmal, sortiert die gesammelten Einträge und cacht die sortierte Liste pro Event-Klasse bis zur nächsten Mutation. Wiederholtes Dispatching derselben Klasse ist daher O(k) über k passende Listener. Das Standard-performance_budget für diese Referenzseite ist wall_ms: 1500, peak_mb: 64.

Listener laufen innerhalb der Erzeugungs-Pipeline mit denselben Rechten wie der Aufrufer. Behandeln Sie Listener-Code als vertrauenswürdigen Code. DocumentOutputEvent legt die finale PDF-Binärdatei offen und lässt einen Listener sie ersetzen. Ein Audit- oder Integritäts-Listener sollte vor jedem Listener laufen, der die Bytes transformiert (verwenden Sie eine höhere Priorität). Die sicherheitsbezogenen Events (EncryptionAppliedEvent, SignatureAppliedEvent) melden die angewendeten Parameter für die Audit-Protokollierung. Sie ändern das kryptografische Ergebnis nicht.

SpezifikationKlauselThema
PSR-14 (PHP-FIG)psr_14_event#x4Stoppbares Event hält weitere Listener an

Die Signatur von dispatch(), die Aufteilung in Listener-Provider und das Modell für stoppbare Events folgen PSR-14. NextPDF deklariert sein eigenes EventInterface und StoppableEventInterface. Das Paket hat keine PSR-14-Laufzeitabhängigkeit und bleibt zugleich duck-type-kompatibel.

  • /modules/core/contracts/ — öffentliche Interface-Oberfläche
  • /modules/core/observability/ — Telemetrie- und Metrik-Hooks
  • /modules/core/audit/ — Integration des Audit-Trails
  • /modules/core/config/Config, weitergereicht über DocumentCreatedEvent
  • /modules/core/exception/InvalidConfigException aus addListener()

Glossar: PSR-14 · stoppbares Event · Listener-Provider