契約 / 串流
串流領域包含兩個 experimental(實驗性)介面:StreamingWriterInterface 負責逐步輸出 PDF,CursorInterface 則負責頁面層級的內容組合。 Core 隨附一套 final 且已測試的引擎,並同時實作這兩個介面。 引擎類別屬於內部實作,因此你會透過公開的 experimental 合約使用其行為,而不是自行實作。 由於此層級屬於 experimental,合約可能在某個次要版本中變更,並會事先發出棄用通知。 在正式環境依賴它之前,請嚴格鎖定版本,或以你自己的轉接器將它包覆起來。
composer require nextpdf/core:^3概念總覽
標題為「概念總覽」的區段串流寫入器會在每一頁組合完成時將該頁序列化,並可在開始下一頁之前先輸出到輸出目標。 當工作負載的文件大小超出可用的記憶體預算時,這正是它的設計使用路徑。 記憶體內寫入器會保留整份文件;串流寫入器則不會。 StreamingWriterInterface 定義了一套嚴格的狀態機。 剛建立的實例處於 CLOSED 狀態。 open() 會將它移轉到 OPEN,並把 PDF 標頭寫入呼叫端提供的串流。 newPage() 會將它移轉到 PAGING,並回傳一個游標。 close() 會寫出交叉參照結構與尾段(trailer),並將它移轉到終端狀態 CLOSED。 交叉參照串流會把每個物件編號對映到它的位元組偏移量——ISO 32000-2 §7。 每個實例只會執行一次工作階段。 在 close() 之後,該實例即無法再使用。 串流資源由呼叫端持有。 寫入器會寫入其中,但絕不會關閉它。
CursorInterface 是頁面層級的寫入介面。 游標取得自 StreamingWriterInterface::newPage(),並會持續有效,直到發生下列任一情況:游標被最終化、下一次 newPage() 自動將它最終化,或 close() 使它失效。 失效是永久性的。 游標無法重新啟用。 在已失效的游標上呼叫任一方法都會擲出 LogicException。 游標會寫入原始的內容串流運算子、設定作用中的字型,並寫入定位文字。 內容串流會把頁面內容編碼為一連串繪圖運算子——ISO 32000-2 §8。 游標是低階介面:它不執行文字塑形、雙向重排、斷行,也不執行任何版面配置。 這些仍屬 Document 層級的職責。 單一游標不變式始終成立:任何時刻最多只有一個游標有效。
這兩個介面都是 experimental,而 Core 在它們背後隨附一套可運作的引擎——一個 final 的 StreamingWriterInterface 實作、對應的頁面游標,以及一個用於記憶體基準測試的捨棄式接收端。 這些引擎類別屬於內部實作,並非公開介面的一部分。 受支援的串流使用方式,是依賴 experimental 合約,並讓 Core 提供實作。 每個型別上的 PHPDoc 都指向串流寫入器的 ADR,說明其生命週期狀態機與範圍依據。 由於此層級屬於 experimental,合約簽章仍可能在某個次要版本中變更,並會事先發出棄用通知。 在正式環境依賴它之前,請嚴格鎖定版本,或以你自己的轉接器將它包覆起來。
API 介面
標題為「API 介面」的區段| 型別 | 種類 | 主要成員 | 穩定性 | 自版本 |
|---|---|---|---|---|
StreamingWriterInterface | 介面 | open(resource, Config)、newPage(?PageSize): CursorInterface、close() | experimental(實驗性,隨附引擎) | 3.1.0 |
CursorInterface | 介面 | writeContent(string)、setFont(string, string, float)、writeText(float, float, string)、finalizePage() | experimental(實驗性,隨附引擎) | 3.1.0 |
open() 若收到不可寫入的串流,會擲出 InvalidArgumentException;若寫入器已開啟,則會擲出 LogicException。 close() 不具冪等性。 重複關閉會擲出例外。
程式碼範例——快速上手
標題為「程式碼範例——快速上手」的區段<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;use NextPDF\Core\Config;
/** * Drive a streaming writer through one page. * * The parameter is the experimental contract; Core supplies the * implementation. Type-hint the interface and let the engine satisfy it. * * @param StreamingWriterInterface $writer A Core-supplied streaming writer. * @param resource $stream A writable, caller-owned stream. */function writeOnePage(StreamingWriterInterface $writer, $stream): void{ $writer->open($stream, new Config()); $cursor = $writer->newPage(); $cursor->setFont('helvetica', '', 12.0); $cursor->writeText(72.0, 720.0, 'Streamed page.'); $cursor->finalizePage(); $writer->close(); // The caller closes $stream after close() returns.}此函式是針對 experimental 介面撰寫的,因此會與引擎類別保持解耦。 Core 會在呼叫點注入一套可運作的實作。
程式碼範例——正式環境
標題為「程式碼範例——正式環境」的區段<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;use NextPDF\Core\Config;use NextPDF\ValueObjects\PageSize;use Psr\Log\LoggerInterface;
final readonly class LargeReportStreamer{ public function __construct( private StreamingWriterInterface $writer, private LoggerInterface $logger, ) {}
/** * Stream a multi-page report to a caller-owned file handle. * * @param resource $stream Writable file handle owned by the caller. * @param list<list<string>> $pages One list of text lines per page. */ public function stream($stream, array $pages): void { $this->writer->open($stream, new Config());
try { foreach ($pages as $lines) { $cursor = $this->writer->newPage(PageSize::A4()); $cursor->setFont('helvetica', '', 11.0);
$y = 760.0; foreach ($lines as $line) { $cursor->writeText(72.0, $y, $line); $y -= 14.0; }
$cursor->finalizePage(); } } finally { $this->writer->close(); } }}即使頁面迴圈擲出例外,finally 也能保證寫入器會關閉,且尾段(trailer)會寫出。 串流仍由呼叫端持有並負責關閉。
邊界情況與陷阱
標題為「邊界情況與陷阱」的區段- 請依賴介面,而非引擎類別。 實作這兩個合約的引擎屬於內部實作,並非公開介面的一部分。 請勿
new它,也不要以名稱參照它。 請以StreamingWriterInterface作為型別提示,並讓 Core 提供實作。 - 此合約為
experimental。 其簽章可能在某個次要版本中變更,並會事先發出棄用通知。 在正式環境依賴它之前,請嚴格鎖定版本,或以你自己的轉接器將它包覆起來。 - 游標會在下一次呼叫
newPage()或close()的當下立即失效。 持有過期游標並對它呼叫方法,會擲出LogicException。 為了清楚起見,請明確地最終化。 close()不具冪等性。 重複關閉是呼叫端的程式錯誤,而非可復原的情況。 合約會擲出例外。- 寫入器絕不會關閉串流。 在
close()回傳後忘記關閉呼叫端持有的控制代碼,會洩漏一個檔案描述符。 - 引擎會把每一頁最終化後的內容輸出,因此常駐記憶體不會隨頁數增長。 確切的記憶體使用輪廓屬於
experimental層級的特性,可能在不同次要版本間變動。 請勿從單次測量結果寫死任何假設。
串流設計會限制尖峰記憶體用量。 隨附的引擎會把每一頁完成後的內容輸出並釋放其緩衝區,因此常駐集不會隨頁數增長,這與記憶體內寫入器不同。 引擎會把交叉參照與頁面樹的記錄資料溢寫到具磁碟後援的暫存串流,藉此讓行程佔用量維持近乎固定。 具體的記憶體與耗時數字屬於 experimental 層級的特性,可能在不同次要版本間變動,因此此處不主張任何固定數值。 performance_budget 中的 1500 ms 耗時與 64 MB 尖峰,是 canvas 的範圍上限,而非合約保證。 可重現性是 bitwise 的:相同的內容與組態會產生完全相同的輸出,引擎會以黃金基準測試固定這一點。
安全注意事項
標題為「安全注意事項」的區段游標的 writeContent() 是一個低階逃生口。 它會把提供的位元組原封不動地附加到頁面內容串流,且不驗證運算子的語法或語意。 把不受信任的輸入傳給 writeContent(),會產生損毀或惡意的 PDF。 呼叫端必須把該方法視為只接受受信任輸入的介面,並對任何受呼叫端影響的文字優先使用 writeText()。 隨附的游標會依照 PDF 字面字串文法,對傳給 writeText() 的文字進行跳脫,但它並不會清理原始運算子。 由呼叫端持有串流的模型也是一項安全特性。 引擎會寫入串流,但絕不會關閉或重新開啟它,因此它無法重新導向輸出。 由於引擎會隨附出貨,其執行階段的攻擊面是真實存在的。 責任分界在於:呼叫端絕不可把不受信任的位元組餵給 writeContent(),而引擎則必須遵守合約的不變式。
符合性
標題為「符合性」的區段| 主張 | 標準 | 條款 | 佐證 |
|---|---|---|---|
| 內容串流會把頁面內容編碼為一連串繪圖運算子,並由游標附加上去。 | ISO 32000-2 | §8 | |
| 寫入器會在關閉時發出一個交叉參照結構,把每個物件編號對映到它的位元組偏移量。 | ISO 32000-2 | §7 |
這兩個條款都已釘選於詞彙表,並以改寫摘要呈現。 NextPDF 不重製任何規範性文字。 合約 PHPDoc 所參照的串流寫入器 ADR,記錄了生命週期與範圍的依據。
商業脈絡
標題為「商業脈絡」的區段一套已測試的串流引擎隨附於開源的 Core 中,位於這些 experimental 合約的背後。 引擎類別屬於內部實作,因此你是透過公開合約來使用串流,而不是透過具體類別名稱。 NextPDF Pro 與 NextPDF Enterprise 遵循相同的合約,因此在 Core 中針對 StreamingWriterInterface 撰寫的程式碼,在 Premium 針對同一合約的實作下仍然有效。 需要留意的是 experimental 層級——而非版本或供應狀態。 其簽章可能在某個次要版本中變更,並會事先發出棄用通知。
另請參閱
標題為「另請參閱」的區段- 合約:41 個公開介面(SPI)——SPI 總覽與穩定性層級。
- 合約/Document——與這些合約互補的記憶體內寫入器。
- Writer——PDF 物件與交叉參照的發出器。
- HTML/串流限制(ADR-001)——串流範圍的依據。
- 效能——串流輸出受記憶體限制的依據。