使用 NextPDF 例外階層處理錯誤
NextPDF 在例外狀態下會擲出具型別的例外。它絕不會把錯誤藏在 false 或 null 回傳值後面。每個領域例外都繼承同一個抽象基底 NextPdfException,並透過 ContextAwareExceptionInterface 公開結構化診斷脈絡。這則 recipe(範例)示範如何以適當粒度攔截,以及如何將結構化脈絡記錄到應用程式效能監控(APM)管線中。它也會指出哪些失敗是單一 catch-all 無法涵蓋的。
composer require nextpdf/core:^3無需額外擴充功能。
概念總覽
標題為「概念總覽」的區段階層如下:
RuntimeException └── NextPdfException (abstract, implements ContextAwareExceptionInterface) ├── InvalidConfigException ├── FontNotFoundException ├── FontParsingException ├── ImageProcessingException ├── WriterException ├── SignatureException ├── EncryptionException ├── HtmlParsingException ├── … (every domain exception under NextPDF\Exception) └── Strict\StrictModeViolation (abstract) ├── Strict\IncompatibleRenderingModeException └── Strict\OracleConformanceFailure這個階層有兩個實務影響,兩者都已對照原始碼驗證:
catch (NextPdfException $e)會攔截NextPDF\Exception下的每一個例外,包含 strict 模式違規。 它們全都繼承自抽象基底。- 它不會攔截函式庫可能擲出的所有例外。
NextPDF\Support\DegradedException直接繼承RuntimeException,並非NextPdfException。因此catch (NextPdfException $e)不會攔截到降級政策的拒絕。若要處理這種情況,請明確攔截DegradedException(或更廣的RuntimeException)。這則 recipe 會清楚標出這條界線,而不是假定單一 catch-all 涵蓋一切。
NextPdfException::getContext() 會回傳一個 array<string, mixed>,只包含 snake_case 鍵,以及基本型別(或基本型別清單)的值,因此可以直接序列化到 PSR-3 logger 的脈絡陣列。PSR-3 §1.3 會把例外放在 'exception' 這個脈絡鍵底下。NextPDF 的 getContext() 則會在同一個脈絡中補上領域細節,而不是放入例外物件本身。
API 介面
標題為「API 介面」的區段這裡的 API 介面來自 NextPDF\Exception\NextPdfException、NextPDF\Contracts\ContextAwareExceptionInterface、具體領域例外(例如 NextPDF\Exception\FontNotFoundException,帶有 getFontName() / getSearchPaths() / wasFallbackAttempted()),以及 NextPDF\Support\DegradedException(它帶有 Capability 與 DegradationPolicy)的 PHPDoc。下面用到的成員是 NextPdfException::getContext() 以及各例外專屬的存取器。
程式碼範例 — 快速上手
標題為「程式碼範例 — 快速上手」的區段<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Exception\NextPdfException;
try { $doc = Document::createStandalone(); $doc->addPage(); $doc->setFont('helvetica', '', 12); $doc->cell(0, 10, 'Hello'); $doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');} catch (NextPdfException $e) { // Every NextPDF\Exception\* (and strict-mode violation) lands here. // $e->getContext() is APM-safe structured detail. error_log($e->getMessage());}程式碼範例 — 正式環境
標題為「程式碼範例 — 正式環境」的區段完整範例示範精準攔截、結構化脈絡記錄,以及 DegradedException 這條界線。它會遵守 harness 的輸出通道。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Contracts\ContextAwareExceptionInterface;use NextPDF\Exception\FontNotFoundException;use NextPDF\Exception\NextPdfException;use NextPDF\Support\DegradedException;
/** * A minimal PSR-3-shaped sink. In production this is your real logger; * the exception goes under the 'exception' key (PSR-3 §1.3) and the * NextPDF structured context is merged in as domain detail. * * @param array<string, mixed> $context */function logError(string $message, array $context): void{ fwrite(STDERR, $message . ' ' . json_encode($context, JSON_THROW_ON_ERROR) . "\n");}
$doc = Document::createStandalone();$doc->setTitle('Exception handling patterns');
try { $doc->addPage(); $doc->setFont('helvetica', 'B', 16); $doc->cell(0, 12, 'Exception-aware error handling', newLine: true);
// This call succeeds; the catch blocks below show the SHAPE of handling. $doc->setFont('helvetica', '', 11); $doc->cell(0, 8, 'Catch specifically, then fall back to the base.', newLine: true);} catch (FontNotFoundException $e) { // Most specific first: actionable, typed accessors. logError('Font missing — using a fallback face', [ 'exception' => $e::class, 'font_name' => $e->getFontName(), 'searched' => $e->getSearchPaths(), 'fallback' => $e->wasFallbackAttempted(), ]);} catch (NextPdfException $e) { // Catch-all for every NextPDF\Exception\* including strict violations. $context = ['exception' => $e::class]; if ($e instanceof ContextAwareExceptionInterface) { $context += $e->getContext(); } logError($e->getMessage(), $context);} catch (DegradedException $e) { // BOUNDARY: DegradedException extends RuntimeException directly, NOT // NextPdfException. The catch above would NOT have caught it. This // explicit block (or a broader RuntimeException) is required. logError('Capability degraded under the active policy', [ 'exception' => $e::class, 'capability' => $e->capability->id, 'policy' => $e->policy->value, ]);}
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');
fwrite(STDERR, "Document built; handlers wired.\n");STDOUT 保留給 harness 使用;PDF 只會寫到 NEXTPDF_COOKBOOK_OUTPUT。
邊界情況與陷阱
標題為「邊界情況與陷阱」的區段- catch 區塊請依特定 → 一般的順序排列。 PHP 會比對第一個相容的
catch。如果把catch (NextPdfException $e)放在catch (FontNotFoundException $e)之前,會讓該特定區塊變成永遠執行不到的死碼。 DegradedException並不是NextPdfException。 經對照原始碼驗證,它繼承的是RuntimeException。單一的catch (NextPdfException $e)會讓嚴格降級的拒絕悄悄往外傳遞。啟用降級政策時,請明確攔截它(或RuntimeException)。getContext()依契約保證可安全用於 APM。 鍵都是 snake_case。值都是基本型別或基本型別清單,沒有巢狀物件,也沒有資源(resource)。你可以直接序列化它,而且它絕不會包含文件位元組。- 不要剖析例外訊息。 例外訊息是給人閱讀的,而且可能會變動。具型別的存取器(
getFontName()、capability->id等等)以及getContext()才是穩定的機器可讀介面。 - 留意陳舊計數。 較舊的資料可能會引用一個固定的「N 個領域例外」。這個階層會隨著各版本發布而成長。請依賴
NextPdfException這個基底型別與instanceof,絕對不要依賴寫死的計數。 - 含 PSR-3 預留位置(placeholder)的訊息要維持字串形式。 記錄時,請讓訊息保持為帶有
{placeholder}標記的字串,並把值放入脈絡陣列(PSR-3 §1.2)。不要把例外物件內插進訊息裡。
例外處理不會帶來任何穩態成本。NextPDF 只會在例外狀態時擲出例外,而且 getContext() 只會在需要時建構一個小陣列。這裡的 performance_budget(wall_ms: 2000、peak_mb: 96)限定的是這則 recipe 的 harness 執行範圍,而不是任意文件。
安全性注意事項
標題為「安全性注意事項」的區段getContext()設計上可安全用於記錄:只有基本型別,沒有文件酬載,也沒有檔案位元組。你仍然要為自己加進記錄脈絡的值負責。在任何使用者提供的資料(例如檔案路徑)抵達 sink 之前,請依你的記錄政策把它清理乾淨。- 不要把原始例外訊息直接回顯給終端使用者,以免洩漏檔案系統結構。請對外呈現一則一般性的訊息,並在伺服器端記錄結構化脈絡。
符合性
標題為「符合性」的區段| 陳述 | 規範 | 條款 | 參考 ID |
|---|---|---|---|
例外在 PSR-3 記錄脈絡中應放在 exception 鍵底下。 | PSR-3 | §1.3 | |
| 記錄訊息應維持字串形式;預留位置名稱對映到脈絡鍵。 | PSR-3 | §1.2 | |
| 修改日期會在每次儲存時重新產生,因此輸出在結構上(而非位元組上)是穩定的。 | ISO 32000-2 | §14.3 |
這則 recipe 以結構式可重現性設定檔驗證。輸出帶有一個 trailer /ID,以及一個會在每次儲存時重新產生的修改日期,所以無法達成位元組層級的相同。經 qpdf 正規化後的結構是穩定的。