Aller au contenu

Event : taxonomie PSR-14 des événements du cycle de vie

Le module Event répartit des événements typés du cycle de vie à chaque étape de la génération du PDF. Les écouteurs observent ou transforment un document sans toucher aux rouages internes du moteur. Comme le répartiteur suit le modèle PSR-14, l’outillage PSR-14 existant reste compatible.

Fenêtre de terminal
composer require nextpdf/core:^3

Le module Event est livré avec le package core. Il n’a besoin d’aucune dépendance supplémentaire : les types EventInterface et StoppableEventInterface reproduisent les contrats PSR-14 sans exiger le package psr/event-dispatcher.

Le module s’articule autour de trois éléments : un répartiteur, un fournisseur d’écouteurs et un ensemble fixe de classes d’événements du cycle de vie.

EventDispatcher reçoit une instance de EventInterface. Il demande au ListenerProvider les écouteurs correspondants, puis les appelle chacun dans l’ordre de priorité. La méthode dispatch() renvoie le même objet événement. Ainsi, un écouteur peut lire l’état que le moteur a placé dans l’événement. Il peut aussi y écrire un nouvel état que le moteur relira plus tard. C’est le schéma du répartiteur PSR-14.

ListenerProvider associe une classe d’événement à une liste de callables dans l’ordre de priorité. Les enregistrements restent limités à l’instance. Il n’y a aucun état statique. Un processus worker peut donc conserver ses propres instances de fournisseur. Le fournisseur parcourt aussi la hiérarchie de classes de l’événement ainsi que ses interfaces. Ainsi, un écouteur sur AbstractEvent voit chaque événement du cycle de vie. Un écouteur sur une interface voit tout événement qui l’implémente.

StoppableEventInterface ajoute le contrôle de propagation. Un écouteur peut appeler stopPropagation(). Le répartiteur cesse alors d’appeler les écouteurs suivants pour ce cycle. Un événement arrêtable est un cas particulier : il porte sa propre logique d’interruption de la chaîne d’écouteurs. PSR-14 décrit le même modèle pour les événements arrêtables (PSR-14 psr_14_event#x4). AbstractEvent implémente l’interface via StoppableEventTrait. Chaque événement du cycle de vie est donc arrêtable par défaut.

Le répartiteur dispose d’un chemin rapide sans surcoût inutile. Il vérifie la présence d’un écouteur sur la classe de l’événement ou sur l’un de ses ancêtres. Lorsqu’il n’y en a aucun, hasListeners() renvoie false et dispatch() retourne aussitôt. Un document sans écouteur n’encourt qu’une seule vérification booléenne par point du cycle de vie.

EventAwareDocumentTrait est le point d’intégration. Il détient un EventDispatcher facultatif. Il expose des assistants de répartition protégés. La classe Document appelle ces assistants à chaque point du cycle de vie. Quand aucun répartiteur n’est défini, chaque assistant est une opération sans effet.

La taxonomie des événements du cycle de vie :

ÉvénementEspace de nomsDéclenchement
DocumentCreatedEventEvent\DocumentAprès la construction complète d’un document
PageAddedEventEvent\DocumentAprès l’initialisation d’une page
ContentRenderedEventEvent\ContentAprès le rendu d’un contenu HTML ou texte sur une page
FontLoadedEventEvent\ContentLorsqu’une police est analysée et ajoutée au registre
EncryptionAppliedEventEvent\SecurityAprès la configuration des paramètres de chiffrement
SignatureAppliedEventEvent\SecurityAprès l’intégration d’une signature
PdfSerializedEventEvent\WriterAprès la sérialisation, avant la livraison de la sortie
DocumentOutputEventEvent\DocumentAvant que les octets du PDF n’atteignent leur destination
SymboleTypeMembres clés
NextPDF\Event\EventInterfaceinterfacegetEventName(): non-empty-string
NextPDF\Event\StoppableEventInterfaceinterfaceisPropagationStopped(): bool
NextPDF\Event\StoppableEventTraittraitisPropagationStopped(), stopPropagation()
NextPDF\Event\AbstractEventclasse abstraitegetEventName() ; implémente StoppableEventInterface
NextPDF\Event\EventDispatcherclasse finaledispatch(EventInterface): EventInterface, getListenerProvider()
NextPDF\Event\ListenerProviderclasse finaleaddListener(), getListenersForEvent(), hasListeners(), getListenerCount(), clearListeners()
NextPDF\Event\EventAwareDocumentTraittraitsetEventDispatcher(), getEventDispatcher()
NextPDF\Event\Document\DocumentCreatedEventclasse finale$document, $config
NextPDF\Event\Document\PageAddedEventclasse finale$document, $pageIndex, $pageSize, $orientation
NextPDF\Event\Document\DocumentOutputEventclasse finalegetPdfData(), setPdfData(), getByteSize(), $filename, $destination
NextPDF\Event\Content\ContentRenderedEventclasse finale$document, $pageIndex, $contentType, $content
NextPDF\Event\Content\FontLoadedEventclasse finale$family, $style, $fontType, $filePath
NextPDF\Event\Security\EncryptionAppliedEventclasse finale$document, $algorithm, $allowPrint, $allowCopy, $allowModify
NextPDF\Event\Security\SignatureAppliedEventclasse finale$document, $signatureLevel, $signerName, $reason, $location
NextPDF\Event\Writer\PdfSerializedEventclasse finale$byteSize, $objectCount, $pageCount, $pdfVersion, $isLinearized, $isEncrypted

addListener() lève NextPDF\Exception\InvalidConfigException lorsque la chaîne de classe d’événement est vide.

Enregistre un écouteur, puis répartis un événement du cycle de vie.

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

La classe Document appelle les assistants de répartition en interne dès qu’un répartiteur lui est attaché avec setEventDispatcher().

Raccorde le répartiteur à un document, utilise la priorité des écouteurs et arrête la propagation au niveau d’un écouteur de type gate.

<?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).
  • L’enregistrement avec joker s’appuie sur la hiérarchie de classes. Un écouteur sur AbstractEvent reçoit chaque événement du cycle de vie, car chaque événement en hérite. Limite les écouteurs aux classes concrètes quand tu ne veux écouter qu’un seul événement.
  • La priorité des écouteurs trie du plus élevé au plus faible. Les priorités égales conservent l’ordre d’insertion (tri stable).
  • stopPropagation() interrompt uniquement le cycle de répartition courant. L’événement réparti suivant démarre un nouveau cycle.
  • Le cache des écouteurs triés est invalidé à chaque appel de addListener(), car un nouvel enregistrement sur un parent ou une interface peut modifier la résolution pour plusieurs classes d’événements.
  • $document dans les événements limités au document est typé object, et non la classe Document, afin de garder le module Event libre de toute dépendance forte envers Core.
  • DocumentOutputEvent::setPdfData() attend une chaîne non vide. Remplacer la charge utile par une chaîne vide produit un document invalide.
  • Les assistants de répartition sur EventAwareDocumentTrait sont des opérations sans effet tant qu’aucun répartiteur n’est défini ; les exécutions sans écouteur n’ajoutent donc aucun coût mesurable.

Sans écouteur, la répartition est en O(1) pour une classe d’événement feuille : une vérification booléenne hasListeners(), puis un retour immédiat. Avec des écouteurs, getListenersForEvent() parcourt une fois l’ascendance de l’événement, trie les entrées collectées et met en cache la liste triée par classe d’événement jusqu’à la prochaine mutation. La répartition répétée de la même classe est donc en O(k) pour les k écouteurs concernés. Le performance_budget par défaut de cette page de référence est wall_ms: 1500, peak_mb: 64.

Les écouteurs s’exécutent dans le pipeline de génération avec les mêmes privilèges que l’appelant. Considère le code des écouteurs comme du code de confiance. DocumentOutputEvent expose le binaire PDF final et permet à un écouteur de le remplacer. Un écouteur d’audit ou d’intégrité doit s’exécuter avant tout écouteur qui transforme les octets (utilise une priorité plus élevée). Les événements limités à la sécurité (EncryptionAppliedEvent, SignatureAppliedEvent) rapportent les paramètres appliqués pour la journalisation d’audit. Ils ne modifient pas le résultat cryptographique.

SpécificationClauseThème
PSR-14 (PHP-FIG)psr_14_event#x4L’événement arrêtable interrompt les écouteurs suivants

La signature de dispatch(), la séparation fournisseur-écouteurs et le modèle d’événement arrêtable suivent PSR-14. NextPDF déclare ses propres EventInterface et StoppableEventInterface. Le package n’a aucune dépendance d’exécution PSR-14 tout en restant compatible grâce au duck typing.

  • /modules/core/contracts/ — surface des interfaces publiques
  • /modules/core/observability/ — hooks de télémétrie et de métriques
  • /modules/core/audit/ — intégration de la piste d’audit
  • /modules/core/config/Config transmis sur DocumentCreatedEvent
  • /modules/core/exception/InvalidConfigException issu de addListener()

Glossaire : PSR-14 · événement arrêtable · fournisseur d’écouteurs