跳到內容

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 上的派送輔助方法都是無作用,因此沒有監聽器的執行不會增加可量測的成本。

對於葉節點事件類別,沒有監聽器的派送是 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