Ir al contenido

Event: taxonomía de eventos del ciclo de vida PSR-14

El módulo Event despacha eventos tipados del ciclo de vida en cada etapa de la generación del PDF. Los listeners observan o transforman un documento sin modificar las partes internas del motor. El dispatcher sigue el modelo PSR-14, por lo que las herramientas PSR-14 existentes siguen siendo compatibles.

Ventana de terminal
composer require nextpdf/core:^3

El módulo Event está incluido en el paquete del núcleo. No tiene dependencias adicionales: los tipos EventInterface y StoppableEventInterface reflejan los contratos PSR-14 sin requerir el paquete psr/event-dispatcher.

El módulo consta de tres partes: un dispatcher, un listener provider y un conjunto fijo de clases de eventos del ciclo de vida.

EventDispatcher recibe una instancia de EventInterface. Le solicita al ListenerProvider los listeners coincidentes. Luego llama a cada uno en orden de prioridad. El método dispatch() devuelve el mismo objeto de evento. De este modo, un listener puede leer el estado que el motor colocó en el evento. También puede escribir en él un estado que el motor lee más adelante. Así funciona el dispatcher PSR-14.

ListenerProvider asigna una clase de evento a una lista de callables en orden de prioridad. El registro está acotado a la instancia. No hay estado estático. Un proceso de trabajo puede tener sus propias instancias del provider. El provider también recorre la jerarquía de clases del evento y sus interfaces. Así, un listener sobre AbstractEvent ve cada evento del ciclo de vida. Un listener sobre una interfaz ve cada evento que la implementa.

StoppableEventInterface añade control de propagación. Un listener puede llamar a stopPropagation(). Entonces el dispatcher deja de llamar a más listeners en ese ciclo. Un evento detenible es un caso especial: incorpora su propio mecanismo para detener la cadena de listeners. PSR-14 describe el mismo modelo para eventos detenibles (PSR-14 psr_14_event#x4). AbstractEvent implementa la interfaz mediante StoppableEventTrait. Así, cada evento del ciclo de vida es detenible de forma predeterminada.

El dispatcher tiene una ruta rápida sin sobrecarga. Comprueba si hay algún listener registrado en la clase de evento o en cualquier ancestro. Cuando no hay ninguno, hasListeners() devuelve false y dispatch() retorna de inmediato. Un documento sin listeners solo paga una comprobación booleana por punto de ciclo de vida.

EventAwareDocumentTrait es el punto de integración. Contiene un EventDispatcher opcional. Expone helpers de despacho protegidos. La clase Document llama a estos helpers en cada punto del ciclo de vida. Cuando no hay ningún dispatcher configurado, cada helper se comporta como una operación nula.

La taxonomía de eventos de ciclo de vida:

EventoNamespaceSe dispara
DocumentCreatedEventEvent\DocumentDespués de construir por completo un documento
PageAddedEventEvent\DocumentDespués de inicializar una página
ContentRenderedEventEvent\ContentDespués de renderizar contenido HTML o de texto en una página
FontLoadedEventEvent\ContentCuando se analiza una fuente y se incorpora al registro
EncryptionAppliedEventEvent\SecurityDespués de configurar los parámetros de cifrado
SignatureAppliedEventEvent\SecurityDespués de incrustar una firma
PdfSerializedEventEvent\WriterDespués de la serialización, antes de la entrega de la salida
DocumentOutputEventEvent\DocumentAntes de que los bytes del PDF lleguen al destino
SímboloTipoMiembros clave
NextPDF\Event\EventInterfaceinterfacegetEventName(): non-empty-string
NextPDF\Event\StoppableEventInterfaceinterfaceisPropagationStopped(): bool
NextPDF\Event\StoppableEventTraittraitisPropagationStopped(), stopPropagation()
NextPDF\Event\AbstractEventclase abstractagetEventName(); implements StoppableEventInterface
NextPDF\Event\EventDispatcherclase finaldispatch(EventInterface): EventInterface, getListenerProvider()
NextPDF\Event\ListenerProviderclase finaladdListener(), getListenersForEvent(), hasListeners(), getListenerCount(), clearListeners()
NextPDF\Event\EventAwareDocumentTraittraitsetEventDispatcher(), getEventDispatcher()
NextPDF\Event\Document\DocumentCreatedEventclase final$document, $config
NextPDF\Event\Document\PageAddedEventclase final$document, $pageIndex, $pageSize, $orientation
NextPDF\Event\Document\DocumentOutputEventclase finalgetPdfData(), setPdfData(), getByteSize(), $filename, $destination
NextPDF\Event\Content\ContentRenderedEventclase final$document, $pageIndex, $contentType, $content
NextPDF\Event\Content\FontLoadedEventclase final$family, $style, $fontType, $filePath
NextPDF\Event\Security\EncryptionAppliedEventclase final$document, $algorithm, $allowPrint, $allowCopy, $allowModify
NextPDF\Event\Security\SignatureAppliedEventclase final$document, $signatureLevel, $signerName, $reason, $location
NextPDF\Event\Writer\PdfSerializedEventclase final$byteSize, $objectCount, $pageCount, $pdfVersion, $isLinearized, $isEncrypted

addListener() lanza NextPDF\Exception\InvalidConfigException cuando la cadena de la clase de evento está vacía.

Registrar un listener y despachar un evento del ciclo de vida.

<?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 clase Document llama internamente a los helpers de despacho una vez que se adjunta un dispatcher mediante setEventDispatcher().

Conectar el dispatcher a un documento, usar la prioridad de los listeners y detener la propagación desde un listener de control.

<?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).
  • El registro con comodín usa la jerarquía de clases. Un listener sobre AbstractEvent recibe cada evento del ciclo de vida porque cada evento lo extiende. Acotar los listeners a clases concretas cuando se necesite un solo evento.
  • La prioridad de los listeners ordena de mayor a menor. Las prioridades iguales conservan el orden de inserción (ordenación estable).
  • stopPropagation() detiene únicamente el ciclo de despacho actual. El siguiente evento despachado inicia un ciclo nuevo.
  • La caché de listeners ordenados se invalida con cualquier llamada a addListener(), porque un nuevo registro de un padre o de una interfaz puede cambiar la resolución de varias clases de eventos.
  • $document en los eventos con alcance de documento está tipado como object, no como la clase Document, para mantener el módulo Event libre de una dependencia fuerte de Core.
  • DocumentOutputEvent::setPdfData() espera una cadena no vacía. Reemplazar la carga útil por una cadena vacía produce un documento no válido.
  • Los helpers de despacho de EventAwareDocumentTrait son operaciones nulas hasta que se configura un dispatcher, por lo que las ejecuciones sin listeners no añaden ningún costo medible.

El despacho sin listeners es O(1) para una clase de evento hoja: una comprobación booleana con hasListeners() y, a continuación, un retorno inmediato. Con listeners, getListenersForEvent() recorre una vez la ascendencia del evento, ordena las entradas recopiladas y almacena en caché la lista ordenada por clase de evento hasta la siguiente mutación. Por lo tanto, el despacho repetido de la misma clase es O(k) sobre k listeners coincidentes. El performance_budget predeterminado para esta página de referencia es wall_ms: 1500, peak_mb: 64.

Los listeners se ejecutan dentro de la canalización de generación con los mismos privilegios que el código llamador. El código de los listeners debe tratarse como código de confianza. DocumentOutputEvent expone el binario final del PDF y permite que un listener lo reemplace. Un listener de auditoría o de integridad debería ejecutarse antes que cualquier listener que transforme los bytes (usar una prioridad más alta). Los eventos con alcance de seguridad (EncryptionAppliedEvent, SignatureAppliedEvent) informan los parámetros aplicados para el registro de auditoría. No cambian el resultado criptográfico.

EspecificaciónCláusulaTema
PSR-14 (PHP-FIG)psr_14_event#x4El evento detenible detiene los listeners posteriores

La firma de dispatch(), la separación entre dispatcher y listener provider y el modelo de evento detenible siguen PSR-14. NextPDF declara su propia EventInterface y StoppableEventInterface. El paquete no tiene ninguna dependencia de PSR-14 en tiempo de ejecución y mantiene la compatibilidad por duck typing.

  • /modules/core/contracts/ — superficie de interfaces públicas
  • /modules/core/observability/ — hooks de telemetría y métricas
  • /modules/core/audit/ — integración del registro de auditoría
  • /modules/core/config/Config pasado en DocumentCreatedEvent
  • /modules/core/exception/InvalidConfigException lanzado desde addListener()

Glosario: PSR-14 · evento detenible · listener provider