跳转到内容

Event(事件):PSR-14 生命周期事件分类

Event 模块会在 PDF 生成的每个阶段分发类型明确的生命周期事件。监听器(listener)可以在不修改引擎内部实现的情况下观察或转换文件。分发器遵循 PSR-14 模型,因此现有的 PSR-14 工具仍可兼容运行。

Terminal window
composer require nextpdf/core:^3

Event 模块随核心包一同提供。它没有额外依赖:EventInterfaceStoppableEventInterface 这两个类型对应 PSR-14 的合约,无须引入 psr/event-dispatcher 包。

这个模块由三个部分组成:一个分发器、一个监听器提供器(listener provider),以及一组固定的生命周期事件类。

EventDispatcher 接收一个 EventInterface 实例。它会向 ListenerProvider 查询匹配的监听器,然后按优先级逐一调用每个监听器。dispatch() 方法会返回同一个事件对象。因此,监听器可以读取引擎放入事件中的状态,也可以回写状态,供引擎稍后读取。这就是 PSR-14 分发器的形态。

ListenerProvider 会将事件类映射到一份按优先级排列的可调用条目列表。注册范围仅限于实例,不包含任何静态状态。一个 worker 进程可以持有自己的提供器实例。提供器也会遍历事件类的继承树及其接口。因此,注册在 AbstractEvent 上的监听器会看到每个生命周期事件;注册在某个接口上的监听器,会看到所有实现该接口的事件。

StoppableEventInterface 加入了传播控制能力。监听器可以调用 stopPropagation(),随后分发器会停止在这一轮中调用后续监听器。可停止事件是一种特殊情况,它自带终止监听器链的方式。PSR-14 对可停止事件描述了相同模型(PSR-14 psr_14_event#x4)。AbstractEvent 通过 StoppableEventTrait 实现该接口。因此,每个生命周期事件默认都是可停止的。

分发器有一条零额外开销的快速路径。它会检查事件类或任何祖先类上是否注册了监听器。当完全没有监听器时,hasListeners() 会返回 false,而 dispatch() 会立即返回。没有监听器的文件在每个生命周期点只需一次布尔检查的成本。

EventAwareDocumentTrait 是集成接点。它持有一个可选的 EventDispatcher,并公开受保护的分发辅助方法。Document 类会在每个生命周期点调用这些辅助方法。未设置分发器时,每个辅助方法都是空操作(no-op)。

生命周期事件分类:

事件命名空间触发时机
DocumentCreatedEventEvent\Document在文件完整构建完成后
PageAddedEventEvent\Document在页面初始化后
ContentRenderedEventEvent\Content在 HTML 或文本内容绘制到某一页之后
FontLoadedEventEvent\Content当字体完成解析并载入注册表时
EncryptionAppliedEventEvent\Security在加密参数设置完成后
SignatureAppliedEventEvent\Security在签名嵌入后
PdfSerializedEventEvent\Writer在序列化之后、输出交付之前
DocumentOutputEventEvent\Document在 PDF 字节送达目标位置之前
符号种类主要成员
NextPDF\Event\EventInterface接口getEventName(): non-empty-string
NextPDF\Event\StoppableEventInterface接口isPropagationStopped(): bool
NextPDF\Event\StoppableEventTraittraitisPropagationStopped(), stopPropagation()
NextPDF\Event\AbstractEvent抽象类getEventName();实现 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() 会在事件类字符串为空时抛出 NextPDF\Exception\InvalidConfigException

注册一个监听器并分发一个生命周期事件。

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

当通过 setEventDispatcher()Document 类挂载分发器后,它会在内部调用分发辅助方法。

将分发器接入文件,使用监听器优先级,并通过一个 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).
  • 通配符注册会利用类继承层级。由于每个事件都继承自 AbstractEvent,注册在它上面的监听器会收到每个生命周期事件。当你只想处理单个事件时,请将监听器范围限定为具体类。
  • 监听器优先级由高到低排序。优先级相同者保留插入顺序(稳定排序)。
  • stopPropagation() 只会中止当前这一轮分发。下一个被分发的事件会开启一轮全新的循环。
  • 每次调用 addListener() 都会使已排序的监听器缓存失效,因为新增的父类或接口注册可能改变多个事件类的 resolve(解析)结果。
  • $document 在文件范围事件中的类型声明为 object,而不是 Document 类,以避免 Event 模块对 Core 产生硬依赖。
  • DocumentOutputEvent::setPdfData() 预期接收非空字符串。用空字符串替换载荷会生成一份无效文件。
  • 在设置分发器之前,EventAwareDocumentTrait 上的分发辅助方法都是空操作(no-op),因此没有监听器时执行不会增加可测量的成本。

对于叶子事件类,没有监听器时的分发是 O(1):执行一次 hasListeners() 布尔检查,然后立即返回。有监听器时,getListenersForEvent() 会遍历一次事件的继承关系,对收集到的条目排序,并按事件类缓存已排序列表,直到下一次变更。因此,重复分发同一类的成本为 O(k),其中 k 为匹配的监听器数量。本参考页的默认 performance_budgetwall_ms: 1500peak_mb: 64

监听器在生成流水线内以与调用方相同的权限执行。请将监听器代码视为受信任代码。DocumentOutputEvent 会暴露最终的 PDF 二进制内容,并允许监听器将其替换。审计或完整性监听器应在任何会转换字节的监听器之前执行(使用更高的优先级)。安全范围事件(EncryptionAppliedEventSignatureAppliedEvent)会报告已应用的参数,供审计记录使用。它们不会改变密码学结果。

规范条款主题
PSR-14(PHP-FIG)psr_14_event#x4可停止事件会中止后续监听器

本包的 dispatch() 签名、监听器提供器的拆分以及可停止事件模型均遵循 PSR-14。NextPDF 声明了自己的 EventInterfaceStoppableEventInterface。本包没有 PSR-14 的运行时依赖,同时仍保持鸭子类型兼容。

  • /modules/core/contracts/ — 公开接口面
  • /modules/core/observability/ — 遥测与指标钩子
  • /modules/core/audit/ — 审计轨迹集成
  • /modules/core/config/Config,随 DocumentCreatedEvent 提供
  • /modules/core/exception/InvalidConfigException,由 addListener() 抛出

词汇表:PSR-14 · stoppable event · listener provider