跳到內容

錯誤即功能

Spec: ISO 9241-110, §5.6.4 Evidence: Code-backed

NextPDF 將例外階層視為 API 介面,並以設計會丟出這些例外的方法時同等的用心來打造它。失敗是具體、具型別的,可以依你需要的粒度攔截,並為記錄檔帶上結構化的脈絡資訊。

本頁以引擎自身的原始碼呈現這個介面:基底型別、具型別的子類別、會把根本原因綁進訊息的具名建構式,以及每個 NextPDF 例外所揭露的結構化脈絡。

錯誤訊息是引擎在最糟糕的時刻對你說話:正式環境、凌晨 2 a.m.,而一份本該出貨的文件出了問題。訊息此時說了什麼,決定下一步是著手修正,還是一場漫長的調查。

一個一般的 RuntimeException: something went wrong 讓你無從下手。它告訴你引擎失敗了,卻沒說失敗的是什麼、在哪裡失敗,更別說該怎麼做。人因工程的指引對此態度明確。錯誤應該把自己解釋得足夠清楚,讓修正它成為順理成章的下一步,而不是一個研究專案( Spec: ISO 9241-110, §5.6.4.3 )。一個指明原因與補救方式的例外並非錦上添花。它是五分鐘修好與五小時修好之間的差別。

  • 每一個 NextPDF 失敗都繼承自同一個抽象基底 NextPdfException,因此你可以用單一型別攔截所有函式庫錯誤。
  • 其下是具體、具型別的子類別——找不到的字型、無效的設定、失敗的簽署操作——讓你能精準攔截你有辦法處理的那個失敗。
  • 每一個 NextPDF 例外都實作了 ContextAwareExceptionInterface,並揭露 getContext():一個結構化、可安全寫入記錄檔的對應表,因此你永遠不必為了取得診斷資訊而去剖析訊息字串。
  • 訊息是可操作的:具名建構式會把實際的根本原因(通常還有修正方式)綁定到訊息上,而不是套用一般範本。
  • 每一個例外類別都記載了誰能處理它——開發者、基礎設施,或函式庫的呼叫者——因此分流在你閱讀堆疊追蹤之前就已經開始。

這個階層很淺,而且是刻意為之。只有一個基底、一層領域專屬型別,以及它們每一個都遵守的一套契約。

單一基底,設計上就是全域攔截點。 NextPdfException 是抽象的,繼承自 RuntimeException,並實作 ContextAwareExceptionInterface

abstract class NextPdfException extends RuntimeException implements ContextAwareExceptionInterface
{
/** @return array<string, mixed> */
public function getContext(): array
{
return [];
}
}

抽象化是一個設計決定。你永遠不會意外攔截到那個模糊的基底,因為它從不被直接丟出。你會刻意攔截它,作為最後一道防線;而當你能做出具體處置時,你會去攔截某個具體的子類別。

具體、具型別的子類別。 缺少字型不是一般錯誤;它是 FontNotFoundException,並且攜帶你採取行動所需的資料:

final class FontNotFoundException extends NextPdfException
{
public function __construct(
private readonly string $fontName,
private readonly array $searchPaths,
private readonly bool $fallbackAttempted,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('Font "%s" not found. Searched: [%s].', $fontName, \implode(', ', $searchPaths)),
0,
$previous,
);
}
// getFontName(), getSearchPaths(), wasFallbackAttempted(), getContext()
}

訊息會指名字型,也會列出搜尋過的確切路徑。你不必去猜是哪個目錄遺漏了。例外會告訴你。

結構化脈絡,而不是剖析字串。 每一個例外都會回傳一個 snake_case、只含基本型別的對應表,可以安全地直接序列化進記錄檔或 APM 酬載:

public function getContext(): array
{
return [
'config_key' => $this->configKey,
'given_value' => $this->givenValue,
'expected_type' => $this->expectedType,
];
}

這份契約讓原因關係變得明確。記錄用的中介層可以對任何 NextPDF 例外呼叫 $logger->error($e->getMessage(), $e->getContext()),完全不必剖析訊息。訊息是給人看的。脈絡是給機器看的。兩者都不需要扮演對方的角色。

用具名建構式產生可操作的訊息。 這正是錯誤從偶發狀況變成設計產物的地方。SignatureException 不只是說「在 B-LT 等級簽署失敗」。它提供了具名建構式,把真正的根本原因(通常還有確切的補救方式)綁定到訊息上:

public static function tsaUrlEmpty(string $signatureLevel): self
{
return new self('', $signatureLevel, null,
'TSA endpoint URL is empty: pass a non-empty `tsaUrl` to the TsaClient '
. 'constructor (e.g. "https://timestamp.example.com/tsa") or remove the '
. 'TSA client wiring if no timestamping is required at this signature level');
}

訊息會說明哪裡出錯,以及該如何處理。還有一系列同類的建構式,分別對應缺少的能力套件、未提供的 HTTP 用戶端、誤選的純摘要演算法、與演算法不相符的金鑰類型等等。每一個都把一整類失敗變成一句開發者不必閱讀引擎原始碼就能據以行動的話。

刻意大聲的失敗。 有些例外存在的目的,正是要讓原本無聲的缺口清楚出聲。NotImplementedException 攜帶一個可供機器 grep 的 feature 標籤,以及一個 followUp 參照:

final class NotImplementedException extends NextPdfException
{
public function __construct(
public readonly string $feature,
public readonly string $followUp,
?Throwable $previous = null,
) {
parent::__construct(
\sprintf('%s is not implemented in this release. %s', $feature, $followUp),
0, $previous,
);
}
}

一條被執行到、卻尚未接通的路徑會丟出這個例外,而不是回傳一個看似合理的空操作。同樣的理念也驅動了 StrictModeViolation,其子類別會攜帶一個簡短、可 grep 的標籤來標示偏離的構造,外加選用的位置與引用脈絡。規格偏離因而成為一個具型別、帶脈絡的中止,而不是一次默默出錯的算繪。

分流中介資料就在類別本身裡。 每一個例外類別都在它的 docblock 裡指明誰能處理它。舉例來說,FontNotFoundException 是「開發者(核對字型路徑)或基礎設施(修正檔案權限)」。InvalidConfigException 是「開發者(在呼叫 NextPDF 之前修正設定)」。NotImplementedException 是「函式庫呼叫者——要嘛移除這個呼叫,要嘛釘選到未來的某個版本」。分流在堆疊追蹤之前就開始了,因為「這是我的問題,還是維運的問題?」這個問題已經有了答案。

下表總結了這項設計,以及每項特性為你帶來什麼。

設計特性在原始碼中它為你帶來什麼
單一抽象基底NextPdfException(抽象,實作脈絡介面)用單一型別攔截每一個函式庫錯誤,且永遠不會意外攔截到模糊的基底
具體、具型別的子類別FontNotFoundExceptionInvalidConfigExceptionSignatureException、…精準攔截你有辦法處理的那個失敗
結構化脈絡getContext()——只含 snake_case 基本型別可記錄或送往 APM,無須剖析訊息字串
可操作的訊息具名建構式綁定根本原因+補救方式一句你能據以行動的話,而非一個範本
刻意大聲NotImplementedExceptionStrictModeViolation讓無聲缺口變成具型別、可 grep 的中止
分流中介資料每個類別 docblock 裡的「Actionable by:」在讀追蹤之前就知道這是誰的問題

本頁是 Evidence: Code-backed :每一個類別、簽名與訊息形式都引用自引擎的例外命名空間,不是改寫而來。

  • 抽象基底及其 ContextAwareExceptionInterface 契約、具型別的子類別、getContext() 的形態,以及 SignatureException 的具名建構式,都逐字引用自原始碼。
  • 那些「Actionable by:」分流行就是同一批檔案中的類別 docblock 契約。
  • 人因工程的依據是 Spec: ISO 9241-110 ——§5.6.4.3,關於錯誤要把自己解釋到足以被修正,以及 §6 對使用錯誤的強健性原則。引擎把開發者視為使用者,把例外視為必須滿足那些條款的介面。

用廣泛攔截作為最後防線;在你能採取行動的地方精準攔截,並把結構化脈絡直接餵給你的記錄器——無須剖析訊息。

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Exception\FontNotFoundException;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
function renderInvoice(LoggerInterface $logger): ?string
{
try {
$document = Document::createStandalone();
$document->setTitle('Invoice 2026-0042');
$document->addPage();
$document->setFont('BrandSans', '', 12);
$document->cell(0, 10, 'Thank you for your business.', newLine: true);
return $document->getPdfData();
} catch (FontNotFoundException $e) {
// Specific: we can recover — fall back to a built-in font.
// getContext() is log-safe structured data, not a parsed string.
$logger->warning($e->getMessage(), $e->getContext());
return null; // caller re-renders with 'helvetica'
} catch (NextPdfException $e) {
// Backstop: any other NextPDF failure, still with structured context.
$logger->error($e->getMessage(), $e->getContext());
return null;
}
}

那個具體的 catch 能夠復原,是因為例外型別告訴它復原是可能的。最後防線則替其他所有失敗記錄結構化脈絡。應用程式在任何時候都不必去讀訊息,才能得知發生了什麼。

常見的誤讀是:認為深層的例外樹是過度設計,而單一錯誤型別會更簡單。那對引擎來說會更簡單,對你來說卻更糟。單一型別意味著每一個失敗都是一般的堆疊追蹤,而復原邏輯只能靠字串比對。那種比對很脆弱,下一次訊息改寫就會把它弄壞。一個小而具體的階層,會把那份知識移進型別系統,讓編譯器與你的 catch 區塊得以運用它。

第二個誤解是:認為訊息與脈絡是多餘重複的。它們並非如此。訊息是給閱讀記錄行的人看的文字。脈絡是供程式碼路由、警示或儀表板使用的具型別對應表。把兩者混為一談,正是 getContext() 契約要消除的字串剖析陷阱。

這個階層刻意保持淺層。NextPDF 並非為每一種可想像的失敗建立一個獨立的例外類別。只有當「具體地攔截那個失敗」是呼叫者會合理去做的事時,它才會建立一個。過度切分只會把字串剖析的問題換成一份龐雜的 catch 清單。

getContext() 是為記錄檔與 APM 設計的結構,因此依契約它只回傳基本型別與基本型別的清單,不含巢狀物件。它是診斷脈絡,而非引擎內部狀態的序列化快照。它也不是一個可供你據以建構外部結構描述的穩定傳輸格式。

本頁描述的是例外的設計介面。確切的例外集合及其欄位會隨引擎演進。此處引用的類別與形態是截至本次審閱時的現況,用來說明契約,而非一份凍結目錄。至於契約——單一基底、具型別的子類別、結構化脈絡、可操作的訊息——才是穩定的部分。

  • 拒絕猜測的 API——讓這些例外一開始就被丟出的快速失敗防護。
  • NextPDF 的設計哲學——為什麼「錯誤即一個 API 介面」是一項一等原則。
  • 管線模型——這些失敗在文件流經引擎時於何處浮現,以及它們如何被觀測。
  • 程式碼佐證(證據等級)——頁面的主張會對照引擎自身的原始碼加以核對,並以引用而非改寫的方式呈現。
  • 脈絡感知例外——一個實作 ContextAwareExceptionInterface 並揭露 getContext() 的 NextPDF 例外。該方法會回傳一個由基本診斷欄位構成的 snake_case 對應表,可安全地序列化進記錄檔或 APM 酬載,而無須剖析訊息字串。
  • 具名建構式——一個靜態工廠方法(例如 SignatureException::tsaUrlEmpty()),它會建構一個例外,其訊息綁定到某個具體的根本原因,且通常還綁定其補救方式。
  • PAdES——PDF Advanced Electronic Signatures,是用於 PDF 簽署的 ETSI 規範家族。 首次使用時展開說明;在簽署相關頁面中有深入介紹。
  • TSA——Time-Stamping Authority(時間戳記憑證機構),是核發 RFC 3161 時間戳記的受信任服務,供較高的 PAdES 規範使用。