跳到內容

契約 / 文件

document 領域涵蓋你建構 PDF 時需遵循的契約:PdfDocumentInterface 負責內容,DocumentFactoryInterface 負責適用於 worker 的安全建立流程,另有 font 與 image 註冊表契約,以及用於輸出與版面的三個列舉。全部自 1.0.0 或 1.7.0 起即為 stable(穩定)。

Terminal window
composer require nextpdf/core:^3

PdfDocumentInterface 是主要 API 介面。它定義頁面管理、字型選擇、cell 與 multi-cell 文字版面、HTML 繪製、影像嵌入,以及最終輸出。每個方法都回傳 static,因此呼叫可以串接。Document::createStandalone() 會回傳滿足該介面的具體實例。在你自己的服務中,型別提示請指向這個介面;如此一來,引擎內部仍可隨時抽換實作。

建立文件有兩條路徑。在傳統的 PHP-FPM 請求中,createStandalone() 會建立一份自我包含、帶有私有註冊表的文件。長時間執行的 worker 則走另一條路徑,這類情境包含 RoadRunner、Swoole 與 Laravel Octane。此時,DocumentFactoryInterface::create() 會回傳一份全新、用過即丟的 Document。這份文件會從行程生命週期的註冊表讀取資料,但絕不會改動它們。工廠持有 FontRegistryInterfaceImageRegistryInterface 這兩個單例(singleton)。每份文件都會取得各自的繪製 context 與 writer。這就是故障隔離:一份文件無法破壞另一份文件所依賴的共享狀態。

註冊表契約正是 worker 能維持高速的原因。FontRegistryInterface 只會剖析字型檔一次,並在整個行程生命週期內快取已剖析的中繼資料。暖機後即可將它鎖定,避免生產環境流量再變更它。ImageRegistryInterface 會在有界的最近最少使用(LRU)策略下,快取已解碼的影像二進位資料。即使二進位資料已被逐出,影像中繼資料仍常駐於記憶體。兩者都提供 memoryUsage() 供容量規劃使用。ImageRegistryInterface 繼承 ResettableService。該契約會逐出快取資料,但不會摧毀結構性中繼資料。worker 可以在記憶體吃緊時丟棄影像快取,並持續提供服務。

另有三個列舉補齊這個領域。OutputDestination 用來選擇行內顯示、強制下載、寫入檔案系統,或回傳原始字串。Orientation 用來選擇直向或橫向。Alignment 用來選擇靠左、置中、靠右或左右對齊的文字。每個列舉都以舊版 TCPDF 代碼作為列舉值,支撐各個 case,因此 compat-tcpdf 橋接層能乾淨地對映。這些列舉的向後相容承諾屬於累加性質,不會移除任何 case。新的 case 可能會在次要版本中加入。

型別種類主要成員穩定性自版本
PdfDocumentInterface介面addPage(), setMargins(), setFont(), cell(), multiCell(), writeHtml(), image(), output(), save()穩定1.0.0
DocumentFactoryInterface介面create(?Config): Document穩定1.7.0
ResettableService介面reset(): void穩定1.7.0
FontRegistryInterface介面register(), get(), warmup(), lock(), isLocked(), registerFromBinary(), memoryUsage()穩定1.7.0
ImageRegistryInterface介面load(), loadFromString(), getMetadata(), memoryUsage()(繼承 ResettableService穩定2.0.0
OutputDestination列舉(字串)Inline, Download, File, String穩定1.0.0
Orientation列舉(字串)Portrait, Landscape穩定1.0.0
Alignment列舉(字串)Left, Center, Right, Justify穩定1.0.0

FontRegistryInterfaceImageRegistryInterface 在 typography 頁有完整說明;本 document 頁則聚焦於它們在建立生命週期中的角色。

examples/01-hello-world.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('Hello World');
$doc->addPage();
$doc->setFont('helvetica', '', 24);
$doc->cell(0, 15, 'Hello, NextPDF!', newLine: true);
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, 'This is a minimal PDF generated with NextPDF.', newLine: true);
$doc->save(__DIR__ . '/output/01-hello-world.pdf');
examples/02-pdf-factory.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\PdfFactory;
use NextPDF\ValueObjects\{Margin, PageSize};
$factory = PdfFactory::new()
->withPageSize(PageSize::A4())
->withMargins(new Margin(15.0, 15.0, 15.0, 15.0))
->withCompress(true)
->withLang('en');
// The same configured factory creates independent documents.
$doc = $factory->create();
$doc->setTitle('PdfFactory Example');
$doc->setAuthor('NextPDF');
$doc->addPage();
$doc->setFont('helvetica', '', 16);
$doc->cell(0, 12, 'Created via PdfFactory', newLine: true);
$doc2 = $factory->create();
$doc2->addPage();
$doc2->setFont('helvetica', '', 12);
$doc2->cell(0, 10, 'Second document from the same factory.');
$doc->save(__DIR__ . '/output/02-pdf-factory.pdf');

PdfFactory 是不可變的建構器;每次 with*() 都會回傳一個新實例。它在底層組合出 DocumentFactoryInterface,因此總覽中提到的 worker 註冊表模型不需要額外串接即可套用。

  • createStandalone() 會建立私有註冊表。在 worker 迴圈中,這會導致每次請求都重新剖析每個字型。請改用搭配共享註冊表的 DocumentFactoryInterface
  • 一份 Document 在設計上就是用過即丟。跨多份邏輯文件重複使用同一個實例會洩漏狀態。請為每份文件呼叫一次 create(),並讓垃圾回收機制回收它。
  • FontRegistryInterface::lock() 會使 register()addFontDirectory()warmup() 拋出 LogicException。請在暖機後鎖定,絕不要在處理請求時鎖定。
  • OutputDestination::File 會寫入伺服器檔案系統,並回傳原始位元組。save() 則明確指定檔案路徑。同一份文件請勿混用這兩者。
  • cell() 為了相容 TCPDF,邊框引數接受 bool|string;空字串並不等同於 false。請傳入你真正要表達的型別值。

font 與 image 註冊表讓 document 領域成為一個記憶體有界的系統,而不是逐請求重建的系統。首次請求的字型剖析占去大部分時間。在 worker 範例中,跨三份文件的 performance_budget 為 1500 ms 牆鐘時間與 64 MB 尖峰記憶體。這份預算幾乎全部都花在首次字型剖析上。暖機後,每份文件可歸因於契約的工作量為 O(1) — 一次註冊表查詢加上一次 context 配置。對任一註冊表呼叫 memoryUsage(),都會回傳一份可供即時容量規劃使用的 MemoryReportResettableService::reset() 會在持續負載下將尖峰記憶體控制在界限之內。

document 契約本身不帶任何密碼學介面,但仍有兩項維運風險需要留意。第一,image() 接受路徑或 URL。在不可信輸入的情境下,請透過 ExternalResourcePolicyInterface(見 security-policy 頁)約束遠端抓取,而不是直接傳入使用者可控的 URL。第二,writeHtml() 是 HTML 管線的進入點。不可信的 markup(標記內容)在繪製前必須先通過 HtmlSecurityPolicyInterface。document 層本身並不會進行淨化。這是 security-policy 領域的職責;而且它是一個契約,因此你可以提供更嚴格的政策,無需分叉。

document 契約實作 ISO 32000-2 定義的 PDF 2.0 文件結構。輸出、頁面與字型處理會依 ISO 32000-2 §7 產生間接物件與一個交叉參照串流(cross-reference stream)。內容依引擎層契約(ADR-010)透過 writer 層發出。本頁除了結構符合性之外,不主張任何條款層級的宣稱。PDF/A 與 PDF/UA 符合性記載於 extraction 與 accessibility 兩頁,規範性表格也在那裡。