跳转到内容

动作触发器与事件监听器

NextPDF 在 NextPDF\Event 中通过兼容 PSR-14 的系统触发生命周期事件。 你可以按指定优先级注册监听器,响应文档创建、添加页面、字体加载、签署、加密、写入或输出等动作,并在需要时中断整条链。

Terminal window
composer require nextpdf/core:^3

事件系统包含两个公开部分。 ListenerProvider 将事件类映射到一组已排序的监听器 callable 列表。 EventDispatcher 遍历该列表,并按优先级调用每个监听器。 这两个类都是 final,因此请通过组合而不是继承来扩展它们。

这两个类都通过 duck typing 符合 PSR-14。 EventDispatcher::dispatch() 符合 PSR-14 的 dispatch() 签名,并会在所有监听器执行完毕后返回该事件。 ListenerProvider::getListenersForEvent() 符合 PSR-14 的 provider 签名。 NextPDF 不需要 PSR-14 包;但如果你的项目已安装它,这些接口仍然可以对齐。

有两个行为对扩展作者很重要:

  • 通配符监听。 解析监听器时,provider 会遍历事件的父类及其接口。 将监听器绑定到 AbstractEvent 基类,即可监看每一个生命周期事件。 绑定到某个接口,即可拦截一整族事件。
  • 优先级与传播。 优先级较高者先执行。 优先级相同时,保持注册顺序。 每个继承 AbstractEvent 的事件都是可停止的。 监听器可以调用 stopPropagation()。 dispatcher 随后会跳过其余监听器。

dispatcher 有一条零成本快速路径。 当某个事件类及其所有父类都没有绑定监听器时,dispatch() 只做一次 hasListeners() 检查后就立即返回。

事件命名空间触发时机稳定性
DocumentCreatedEventNextPDF\Event\Document文档构造完成实验性
PageAddedEventNextPDF\Event\Document页面初始化完成实验性
ContentRenderedEventNextPDF\Event\Content内容已渲染到页面实验性
FontLoadedEventNextPDF\Event\Content某个字体家族与样式首次加载实验性
SignatureAppliedEventNextPDF\Event\Security签章字节已嵌入实验性
EncryptionAppliedEventNextPDF\Event\Security加密设置完成实验性
PdfSerializedEventNextPDF\Event\Writer序列化完成实验性
DocumentOutputEventNextPDF\Event\Document输出交付之前实验性

dispatcher、provider、标记接口与基类都是 stable(自 3.0.0 起)。 事件负载则是 experimental。 它们的构造函数参数与 readonly 属性可能在 minor 版本中变动。 patch 版本只会新增。 绑定负载属性名称时,请记住这一点。

NextPDF\Event\ListenerProvider(稳定、final):

方法返回用途
addListener(string $eventClass, callable $listener, int $priority = 0)void注册监听器;优先级较高者先执行。 若传入空类,会抛出 InvalidConfigException
getListenersForEvent(EventInterface $event)list<callable>解析监听器,包括父类与接口上的注册。
hasListeners(string $eventClass)bool跨整个类层级的零开销检查。
getListenerCount(string $eventClass)int只计算直接注册的数量。
clearListeners()void重设 provider。

NextPDF\Event\EventDispatcher(稳定、final):

方法返回用途
dispatch(EventInterface $event)EventInterface按优先级调用监听器;遵守传播停止;返回该事件。
getListenerProvider()ListenerProvider访问 provider,即可在运行时新增监听器。

会触发事件的文档会使用 NextPDF\Event\EventAwareDocumentTrait。 它的 setEventDispatcher() 方法会把一个 dispatcher 接入单个文档。 若没有 dispatcher,每个 dispatch 辅助方法都不会执行任何操作。

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

这是一个生产环境的审计监听器。 它使用较高优先级以便最先执行,以结构化形式记录,并在基类上添加一个 catch-all 监听器,以保证覆盖完整。

<?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 类。 EventDispatcherListenerProvider 都是 final。 请使用组合,不要使用继承。
  • 空事件类会抛出异常。 addListener('', ...) 会抛出 InvalidConfigException。 请始终传入 class-string 常量。
  • 通配符的成本。 绑定在 AbstractEvent 上的监听器会对每一个事件触发。 请为 catch-all 监听器设置较低优先级,并让它们保持轻量。
  • 输出变更。 DocumentOutputEvent 携带 PDF 字节。 引擎会在 dispatch 之后读回这些字节。 变更这些字节可以获得广泛控制权,但也有实际风险。 错误的字节偏移量会让 PDF 损坏,并可能破坏签章。 除非你为了确定性与签章而拥有最终结果,否则请以观察为主。
  • 没有 dispatcher,就没有事件。 没有通过 EventAwareDocumentTrait 设置 dispatcher 的文档不会触发任何事件。 这是有意设计的零成本路径,并不是配置错误。

快速路径是一次沿父类链进行的 hasListeners() 检查。 没有监听器时,dispatch 几乎没有成本。 provider 会针对每个事件类缓存已排序的监听器列表。 只有在发生变更时,它才会清除该缓存。 请让监听器保持非阻塞。 它们会在渲染路径上以内联方式执行。

SignatureAppliedEventEncryptionAppliedEvent 是审计锚点。 请注册高优先级监听器,将签署与加密记录到防篡改的存储区。 除非你有意让后续监听器静默,否则不要在安全事件上中断整条链。 中断它可能会悄然关闭后续执行的审计挂钩。

本页除了兼容 PSR-14 之外不做任何规范性主张;该兼容性仅为 duck-type,并不需要 PSR-14 包。

NextPDF Enterprise 随附经过审计的监听器,用于签章与加密事件,并写入防篡改的审计记录。 监听器契约就是公开的事件 API,因此你自己的监听器可以与 Enterprise 的监听器共存于同一个 provider 上。

词汇表定义了本页使用的 event listenerevent dispatcherlistener providerstoppable event 等术语。 请参阅已发布的词汇表,了解每个术语的规范定义。