コンテンツにスキップ

Event: PSR-14 ライフサイクルイベントの分類体系

Event モジュールは、PDF 生成の各段階で型付けされたライフサイクルイベントをディスパッチします。リスナーは、エンジン内部を変更せずにドキュメントを監視または変換できます。ディスパッチャーは PSR-14 モデルに従うため、既存の PSR-14 ツールとの互換性が保たれます。

Terminal window
composer require nextpdf/core:^3

Event モジュールは core パッケージに同梱されています。追加の依存関係はありません。EventInterface 型と StoppableEventInterface 型は、psr/event-dispatcher パッケージを必要とせずに PSR-14 のコントラクトを反映します。

このモジュールは、ディスパッチャー、リスナープロバイダー、固定されたライフサイクルイベントクラスのセットという 3 つの部分で構成されています。

EventDispatcherEventInterface インスタンスを受け取り、一致するリスナーを ListenerProvider に問い合わせ、各リスナーを優先度順に呼び出します。dispatch() メソッドは同じイベントオブジェクトを返します。これにより、リスナーはエンジンがイベントに格納した状態を読み取ることができます。また、エンジンが後で読み取る状態を書き戻すこともできます。これが PSR-14 ディスパッチャーの形式です。

ListenerProvider は、イベントクラスを優先度順の callable のリストにマッピングします。登録はインスタンス単位でスコープされ、静的な状態はありません。ワーカープロセスは、それぞれ独自のプロバイダーインスタンスを保持できます。プロバイダーは、イベントクラスのツリーとそのインターフェイスもたどります。そのため、AbstractEvent に対するリスナーは、すべてのライフサイクルイベントを受け取ります。インターフェイスに対するリスナーは、それを実装するすべてのイベントを受け取ります。

StoppableEventInterface は伝播制御を追加します。リスナーは stopPropagation() を呼び出すことができます。呼び出されると、ディスパッチャーはそのサイクルにおける以降のリスナー呼び出しを停止します。停止可能なイベントは特別なケースの 1 つであり、リスナーチェーンを停止するための独自の手段を備えています。PSR-14 は、停止可能なイベントについて同じモデルを説明しています(PSR-14 psr_14_event#x4)。AbstractEvent は、StoppableEventTrait を通じてこのインターフェイスを実装します。そのため、すべてのライフサイクルイベントはデフォルトで停止可能です。

ディスパッチャーは、オーバーヘッドのない高速パスを備えています。イベントクラスまたはその先祖クラスにリスナーがあるかどうかを確認し、リスナーが存在しない場合、hasListeners()false を返し、dispatch() はただちに戻ります。リスナーのないドキュメントでは、ライフサイクルポイントごとにブール値のチェックを 1 回行うだけです。

EventAwareDocumentTrait は統合の接点です。オプションの EventDispatcher を保持し、protected なディスパッチヘルパーを提供します。Document クラスは、各ライフサイクルポイントでこれらのヘルパーを呼び出します。ディスパッチャーが設定されていない場合、すべてのヘルパーは no-op です。

ライフサイクルイベントの分類体系:

イベントネームスペース発火タイミング
DocumentCreatedEventEvent\Documentドキュメントが完全に構築された後
PageAddedEventEvent\Documentページが初期化された後
ContentRenderedEventEvent\ContentHTML またはテキストコンテンツがページにレンダリングされた後
FontLoadedEventEvent\Contentフォントが解析されレジストリに反映された時
EncryptionAppliedEventEvent\Security暗号化パラメーターが構成された後
SignatureAppliedEventEvent\Security署名が埋め込まれた後
PdfSerializedEventEvent\Writerシリアライズの後、出力配信の前
DocumentOutputEventEvent\DocumentPDF バイトが宛先に到達する前
シンボルタイプ主なメンバー
NextPDF\Event\EventInterfaceinterfacegetEventName(): non-empty-string
NextPDF\Event\StoppableEventInterfaceinterfaceisPropagationStopped(): bool
NextPDF\Event\StoppableEventTraittraitisPropagationStopped(), stopPropagation()
NextPDF\Event\AbstractEventabstract classgetEventName();実装するインターフェイスは 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);

Document クラスは、ディスパッチャーが setEventDispatcher() を通じてアタッチされると、内部でディスパッチヘルパーを呼び出します。

ディスパッチャーをドキュメントに組み込み、リスナーの優先度を使用し、ゲートリスナーで伝播を停止します。

<?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 を継承しているため、これに対するリスナーはすべてのライフサイクルイベントを受け取ります。1 つのイベントのみを対象にしたい場合は、リスナーを具象クラスにスコープします。
  • リスナーの優先度は、高いものから順にソートされます。優先度が等しい場合は、挿入順が保持されます(安定ソート)。
  • stopPropagation() は、現在のディスパッチサイクルのみを停止します。次にディスパッチされるイベントでは、新しいサイクルが開始されます。
  • 新しい親またはインターフェイスを登録すると複数のイベントクラスの resolve(解決)が変わる可能性があるため、ソート済みリスナーキャッシュは addListener() 呼び出しのたびに無効化されます。
  • $document(ドキュメントスコープのイベント上)は object として型付けされており、Document クラスではありません。これは、Event モジュールが Core への強い依存を持たないようにするためです。
  • DocumentOutputEvent::setPdfData() は、空でない文字列を期待します。ペイロードを空の文字列に置き換えると、無効なドキュメントが生成されます。
  • ディスパッチヘルパー(EventAwareDocumentTrait 上)は、ディスパッチャーが設定されるまで no-op であるため、リスナーのない実行では測定可能なコストは追加されません。

リスナーのないディスパッチは、リーフのイベントクラスでは O(1) です。すなわち、hasListeners() によるブール値チェックを 1 回行い、その後ただちに戻ります。リスナーがある場合、getListenersForEvent() はイベントの先祖を 1 回たどり、収集したエントリをソートして、次の変更までイベントクラスごとにソート済みリストをキャッシュします。そのため、同じクラスの繰り返しのディスパッチは、一致した k 個のリスナーに対して O(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 · 停止可能イベント · リスナープロバイダー