コンテンツにスキップ

アクショントリガーとイベントリスナー

NextPDF は、NextPDF\Event 内の PSR-14 互換システムを通じてライフサイクルイベントを発火します。リスナーは優先順位付きで登録できます。ドキュメントの作成、新規ページ、フォントの読み込み、署名、暗号化、書き出し、出力に応答し、必要に応じてチェーンを停止できます。

Terminal window
composer require nextpdf/core:^3

イベントシステムは、公開された 2 つの部分で構成されます。ListenerProvider は、イベントクラスをソート済みのリスナー callable リストにマッピングします。EventDispatcher はそのリストをたどり、各リスナーを優先順位順に呼び出します。どちらのクラスも final であるため、サブクラス化ではなくコンポジションで拡張してください。

どちらのクラスもダックタイピングで PSR-14 に適合します。EventDispatcher::dispatch() は PSR-14 の dispatch() シグネチャに適合し、すべてのリスナーの実行後にイベントを返します。ListenerProvider::getListenersForEvent() は PSR-14 のプロバイダーシグネチャに適合します。NextPDF は PSR-14 パッケージを必須とはしませんが、プロジェクトに導入されている場合でも、インターフェイスは引き続き一致します。

拡張機能の作成者が押さえるべき動作が 2 つあります。

  • ワイルドカードリスニング。 プロバイダーはリスナー解決時に、イベントの親クラスとそのインターフェイスをたどります。すべてのライフサイクルイベントを監視するには、リスナーを AbstractEvent 基底クラスにバインドします。関連する一群をまとめて捕捉するには、インターフェイスにバインドします。
  • 優先順位と伝播。 優先順位の高いリスナーから先に実行されます。同じ優先順位では登録順が維持されます。AbstractEvent を継承するすべてのイベントは停止可能です。リスナーは stopPropagation() を呼び出せます。その場合、ディスパッチャーは残りのリスナーをスキップします。

ディスパッチャーにはゼロコストの高速パスがあります。イベントクラスまたはいずれかの親にリスナーがバインドされていない場合、dispatch() は 1 回の hasListeners() チェックの後にただちに戻ります。

イベント名前空間発火タイミング安定性
DocumentCreatedEventNextPDF\Event\Documentドキュメント構築完了時experimental
PageAddedEventNextPDF\Event\Documentページ完全初期化時experimental
ContentRenderedEventNextPDF\Event\Contentページへのコンテンツレンダリング時experimental
FontLoadedEventNextPDF\Event\Contentフォントファミリーとスタイルの初回読み込み時experimental
SignatureAppliedEventNextPDF\Event\Security署名バイト埋め込み時experimental
EncryptionAppliedEventNextPDF\Event\Security暗号化構成時experimental
PdfSerializedEventNextPDF\Event\Writerシリアル化完了時experimental
DocumentOutputEventNextPDF\Event\Document出力配信前experimental

ディスパッチャー、プロバイダー、マーカーインターフェイス、基底クラスは、3.0.0 以降 stable です。イベントペイロードは experimental です。これらのコンストラクター引数と readonly プロパティは、マイナーリリースで変更される可能性があります。パッチリリースでは追加のみが行われます。この点を踏まえて、ペイロードのプロパティ名にバインドしてください。

NextPDF\Event\ListenerProvider(stable、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プロバイダーのリセット。

NextPDF\Event\EventDispatcher(stable、final):

メソッド戻り値目的
dispatch(EventInterface $event)EventInterfaceリスナーの優先順位順の呼び出し、伝播停止の尊重、イベントの返却。
getListenerProvider()ListenerProvider実行時にリスナーを追加するためのプロバイダーアクセス。

イベントを発火するドキュメントでは NextPDF\Event\EventAwareDocumentTrait を使用します。その setEventDispatcher() メソッドは、1 つのドキュメントにディスパッチャーを接続します。ディスパッチャーがない場合、すべてのディスパッチヘルパーは何もしません。

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

これは本番環境向けの監査リスナーです。最初に実行されるよう高い優先順位を設定し、構造化形式でログを記録し、トレースの網羅性のために基底クラスへキャッチオールを追加します。

<?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 クラス。 EventDispatcherListenerProviderfinal です。サブクラス化せず、コンポジションを使用してください。
  • 空のイベントクラスはスローします。 addListener('', ...)InvalidConfigException をスローします。常に class-string 定数を渡してください。
  • ワイルドカードのコスト。 AbstractEvent に登録したリスナーは、すべてのイベントで呼び出されます。キャッチオールリスナーには低い優先順位を設定し、処理は軽量に保ってください。
  • 出力の変更。 DocumentOutputEvent は PDF バイトを保持します。エンジンはディスパッチ後にそのバイトを読み戻します。このバイトを変更すると広範な制御が可能になりますが、実際のリスクも伴います。バイトオフセットを誤ると PDF が破損し、署名が壊れる可能性があります。決定性と署名のために結果を自身で管理する場合を除き、観測にとどめることをお勧めします。
  • ディスパッチャーがなければイベントもありません。 EventAwareDocumentTrait を通じてディスパッチャーが設定されていないドキュメントは、イベントを発火しません。これは意図されたゼロコストのパスであり、セットアップエラーではありません。

高速パスは、hasListeners() による親チェーンの 1 回のチェックです。リスナーがなければ、ディスパッチのコストはほぼ発生しません。プロバイダーは、ソート済みのリスナーリストをイベントクラスごとにキャッシュします。このキャッシュは、変更があったときにのみクリアされます。リスナーはノンブロッキングに保ってください。リスナーはレンダリングパス上でインライン実行されます。

SignatureAppliedEventEncryptionAppliedEvent は監査の起点です。署名と暗号化を改ざん検知可能なストアに記録するために、優先順位の高いリスナーを登録してください。後続のリスナーを無効にする意図がない限り、セキュリティイベントでチェーンを停止しないでください。停止すると、後続の監査フックが気づかないうちに無効になることがあります。

このページでは、PSR-14 互換性を超える規範的な主張は行いません。ここでの PSR-14 互換性はダックタイピングのみによるもので、PSR-14 パッケージを必要としません。

NextPDF Enterprise は、署名イベントと暗号化イベントを改ざん検知可能な監査ログへ送る監査済みリスナーを提供します。リスナーのコントラクトは公開イベント API であるため、独自のリスナーは Enterprise のリスナーと同じプロバイダー上で共存できます。

用語集では、このページで使用しているイベントリスナーイベントディスパッチャーリスナープロバイダー停止可能イベントを定義しています。正式な定義については、公開されている用語集を参照してください。