拒絕猜測的 API
Spec: ISO/IEC 25010 ISO/IEC 25010 Spec: ISO 32000-2 ISO 32000-2 Evidence: Code-backed
NextPDF 要求你明確說出真正的意圖。凡是意圖會改變位元組之處——簽章層級、輸出目的地、符合性目標——都必須以明確的必填引數表示,而不是讓引擎從上下文推斷。
本頁透過引擎自身的原始碼呈現這個立場:方法簽名、具名引數,以及在產生任何位元組之前就拒絕含糊輸入的各個位置。
為什麼這很重要
標題為「為什麼這很重要」的區段猜測就是在不告知你的情況下,代替你做決定。對於一個文字欄位來說,這頂多有點惱人。但對於 PDF 而言,這是一個潛伏缺陷,因為你交付的往往是具有法律或歸檔效力的成品,其正確性日後會由其他人以驗證器檢查。
以簽章為例。簽章摘要是針對一個宣告的位元組範圍計算而得,該範圍刻意排除了簽章值本身( Spec: ISO 32000-2, §12.8 ISO 32000-2 §12.8 )。一個悄悄「幫忙」的 API——改寫結構、推斷層級、填補佔位符——其實沒有幫上忙。它改變了簽章本應保護的那些位元組。呼叫處那個看似貼心的猜測,數週後就會變成生產環境事故。它們是同一行程式碼。
簡短版本
標題為「簡短版本」的區段- 如果某個選擇會改變輸出且沒有安全的預設值,NextPDF 就會把它設為必填引數,而不是由推斷得出。
- 讀起來含糊不清的選填引數會被具名化,因此呼叫處能陳述意圖(
newLine: true,而非裸露的true)。 - 可能不安全的輸入會在算繪之前先經過驗證,並以指明原因的具型別例外拒絕。
- 文件實例是一次性使用的:建構、輸出,然後丟棄。沒有
reset(),所以也就沒有「這東西被重複使用了嗎?」的猜測。 - 引擎絕不會以一個看似合理的成品,替代你實際要求的那一個。它會選擇拒絕。
NextPDF 如何處理這個問題
標題為「NextPDF 如何處理這個問題」的區段這套機制並不花俏,而這正是重點。它就是型別系統、具名引數、以列舉取代魔術字串,以及少數刻意安排在輸出之前的防護子句。
下表對比了幾種含糊的輸入。每一列都呈現一個會「幫忙」的函式庫可能推斷出什麼,以及 NextPDF 改為怎麼做。每一個 NextPDF 欄位,都是從本頁稍後所示的原始碼中引用的行為。
| 含糊的輸入 | 會猜測的函式庫會怎麼做 | NextPDF 會怎麼做 |
|---|---|---|
像 "portait" 這樣的方向字串 | 退回到某個預設值並繼續算繪 | addPage() 接受 Orientation 列舉,而非字串——拼錯字是型別錯誤,而非無聲的預設值 |
傳給 cell() 的一個裸露的尾端 true | 挑一個它猜測你想指定的布林參數位置 | 布林值在呼叫處被具名化(newLine: true);未具名的字面值正是這個 API 所消除的壞味道 |
傳給 save() 的 php:// 包裝器或目錄穿越路徑 | 「盡力而為」並寫到某個位置 | 在 PDF 建構之前就被拒絕,並以一個具型別的 InvalidConfigException 指明鍵、值與預期型別 |
在高階簽署器尚未接通時呼叫 setSignature() 接著呼叫 save() | 輸出一個呼叫者誤以為已簽署、實則未簽署的檔案 | 在產生位元組之前就擲出 NotImplementedException,並指明受支援的途徑 |
重複使用同一個 Document 實例進行第二次算繪 | 猜測殘留的狀態是否仍然適用 | 沒有 reset(),也沒有重複使用路徑——每個請求都透過 DocumentFactory 取得全新實例,因此根本沒有殘留狀態可供猜測 |
意圖是必填引數。 核心契約 PdfDocumentInterface 以具型別的值物件與列舉接受幾何與對齊資訊,而不是鬆散的原始型別:
public function addPage( ?PageSize $size = null, Orientation $orientation = Orientation::Portrait,): static;
public function cell( float $width, float $height, string $text = '', bool|string $border = false, bool $newLine = false, Alignment $align = Alignment::Left, bool $fill = false,): static;Orientation 與 Alignment 都是列舉,因此呼叫無法傳入 "portait",還讓它無聲地被當成「預設值」。凡是有預設值之處,它都是一個安全的預設值(直向、靠左、無框線),而不是在猜測你大概想要什麼。
含糊的布林值會在呼叫處被具名化。 在那些實質上充當 API 參考的範例中,同一種形式反覆出現:
$document->cell(0, 15, 'Hello, NextPDF!', newLine: true);$document->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);$pdf = $document->output(dest: OutputDestination::String);newLine: true 一目瞭然,不會被誤解。一個裸露的尾端 true 則不然。簽章層級是 SignatureLevel::PAdES_B_B,一個列舉案例——絕不是引擎必須去解讀的字串。輸出目的地是 OutputDestination::String,所以「把位元組給我,不要 HTTP 標頭、不要檔案」是被明確陳述的,而不是從是否傳入檔名來推測。
不安全的輸入會在寫入任何一個位元組之前就被拒絕。 save() 會在建構 PDF 之前先驗證目的地路徑:
public function save(string $path): void{ // Reject stream wrappers and null bytes if (\str_contains($path, "\0") || \preg_match('#^[a-zA-Z]+://#', $path)) { throw new InvalidConfigException( configKey: 'output_path', givenValue: $path, expectedType: 'valid_path', ); } // Resolve the parent directory to prevent path traversal $dir = \dirname($path); $realDir = \realpath($dir); if ($realDir === false) { throw new InvalidConfigException( configKey: 'output_path', givenValue: $dir, expectedType: 'existing_directory', ); } // ... only now is the PDF built and written atomically}引擎不會對 php:// 包裝器或目錄穿越路徑「盡力而為」。它會拒絕,而該例外會指明鍵、值與預期內容。
引擎寧可拒絕,也不輸出具誤導性的成品。 拒絕猜測最明確的形式,就是在輸出會失實時,乾脆完全不產生任何輸出。當設定了高階簽章,但真正負責簽署的寫入器接縫尚未接通時,建構路徑會在產生位元組之前擲出例外,而不是輸出一個呼叫者誤以為已簽署、實則未簽署的檔案:
if ($this->padesOrchestrator !== null) { throw new NotImplementedException( feature: 'Document::setSignature()->save()/output()/getPdfData()', followUp: 'The high-level PAdES writer seam is not yet wired ... ' . 'Produce a signed PDF via the direct two-phase ' . 'PadesOrchestrator::signDocument() then finalizeSignature() ' . 'buffer API ...', );}一個看起來已簽署、實則未簽署的 PDF,正是這個原則要防範的那種看似合理卻錯誤的成品。同樣的立場也出現在嚴格 CSS 路徑中。未經登錄的規格偏差會在偵測到的當下擲出 StrictModeViolation,而不是算繪出一個近似結果,讓該偏差未被偵測。
一次性使用消除了一整類猜測。 Document 是可拋棄的——建構、輸出、丟棄。沒有 reset(),也沒有重複使用路徑。長時間執行的工作行程會透過 DocumentFactory 為每個請求建立全新實例。引擎永遠不必猜測前一份文件的殘留狀態是否仍有意義,因為設計上根本就沒有殘留狀態。
證據怎麼說
標題為「證據怎麼說」的區段本頁是 Evidence: Code-backed :上述每一種模式都引用自引擎自身的原始碼及其範例,而不是只依意圖轉述。
- 這些具型別、帶有列舉的方法簽名,就是
PdfDocumentInterface中的公開契約。具名引數的呼叫風格,是貫穿那些實質充當 API 參考的標準範例的一致形式。 - 那段算繪前的路徑驗證及其具型別的
InvalidConfigException,以及那道在輸出前拒絕的NotImplementedException防護,都是逐字引用自文件外觀(façade)的輸出路徑。 - 標準的依據是 Spec: ISO/IEC 25010, §3.32 ISO/IEC 25010 §3.32 ——使用者錯誤防護,正是一個拒絕猜測的 API 在呼叫處所要滿足的品質屬性。第二個依據是 Spec: ISO 32000-2, §12.8 ISO 32000-2 §12.8 ,這也說明了為什麼在已簽署的文件周邊進行猜測絕不可能是無害的。該摘要涵蓋一個排除了簽章值的宣告位元組範圍,因此任何無聲的改寫都會使其失效。
實務範例
標題為「實務範例」的區段一個小巧而完整的程式。每一行原本可能含糊的程式碼,都明確陳述了它的意圖。唯一一個不安全的輸入,會在進行任何工作之前就被拒絕。
<?php
declare(strict_types=1);
use NextPDF\Contracts\OutputDestination;use NextPDF\Core\Document;use NextPDF\Exception\InvalidConfigException;use NextPDF\ValueObjects\PageSize;use NextPDF\Contracts\Orientation;
$document = Document::createStandalone();$document->setTitle('Quarterly Report');
// Intent is explicit: a typed page size and an Orientation enum case,// not a string the engine has to interpret.$document->addPage(PageSize::a4(), Orientation::Landscape);$document->setFont('helvetica', 'B', 16);
// Ambiguous boolean is named, so the call reads as intent.$document->cell(0, 12, 'Quarterly Report', newLine: true);
try { // Unsafe path is rejected before a byte is built. $document->save('php://output/report.pdf');} catch (InvalidConfigException $e) { // "Invalid configuration for key "output_path": expected valid_path, ..." error_log($e->getMessage());
// The String destination is explicit: bytes only, no HTTP headers, // no file side effect. Nothing is inferred from a missing filename. $bytes = $document->output(dest: OutputDestination::String);}這個程式沒有任何一條路徑會悄悄做錯事。它不是陳述意圖並繼續執行,就是指明問題並停止。
常見誤解
標題為「常見誤解」的區段常見的反對意見是「這不過是囉嗦罷了」。這並非囉嗦。這是刻意消除隱藏預設值。一個裸露的 true 比 newLine: true 短了多少,就正好抹去了多少清晰度。引擎以呼叫處多出的幾個字元,換來一整類錯誤被消除——也就是程式碼能編譯、能執行、能產生檔案,卻是錯的那一類。
一個相關的誤解是,快速失敗意味著「動不動就擲出例外」。在正常使用下,NextPDF 不會擲出任何例外。有效的輸入會順暢通過。這些防護只會對真正含糊或不安全的輸入啟動——正是那些你會想立即知道,而不是交給引擎猜測的輸入。
限制與邊界
標題為「限制與邊界」的區段拒絕猜測適用於意圖與安全,而不是每一項便利。NextPDF 仍有安全的預設值:直向、靠左對齊、無框線。原則是:唯有在安全且不令人意外之處才提供預設值;在錯誤推斷會產生錯誤文件之處,則絕不提供。
本頁在核心公開 API 介面(文件外觀、其契約,以及輸出路徑)上演示了這項原則。各子系統有自己的進入點,也各自記載其驗證行為。此處引用的模式為本次審閱時的現況。它們是用來說明這個模式,並非引擎中每一道防護的完整清單。
所描述的快速失敗防護是正確性與安全性防護。它們本身並不構成安全邊界。輸入驗證只是其中一層。設計理念與安全文件描述了更廣泛的立場。
相關文件
標題為「相關文件」的區段- NextPDF 設計理念——將本頁所演示的原則,放回其優先順序脈絡之中。
- 把錯誤當成一項功能——這些防護所擲出的具型別例外,要告訴你什麼。
- 處處嚴格型別——型別系統如何讓「陳述你的意圖」成為可強制執行的要求,而不是僅供參考的建議。
術語表
標題為「術語表」的區段- 程式碼佐證(證據層級)——指主張會對照引擎自身原始碼或可執行範例加以查核,並以引用而非轉述呈現的頁面。
- 快速失敗——在最早的時點,以明確的原因拒絕無效輸入,而不是繼續執行並在稍後以晦澀方式失敗。
- 具名引數——一種 PHP 呼叫處語法(
newLine: true),依名稱將值繫結至參數,使原本含糊的字面值能自我說明。 - 一次性使用生命週期——可拋棄的
Document契約:實例化、寫入、儲存、丟棄。沒有reset(),不重複使用。工作行程會透過DocumentFactory為每個請求建立全新實例。 - PAdES——PDF 進階電子簽章(PDF Advanced Electronic Signatures),用於 PDF 簽署的 ETSI 規範系列。於首次使用時展開全稱;在簽署相關頁面有深入說明。