跳到內容

使用 NextPDF 例外階層處理錯誤

NextPDF 在例外狀態下會擲出具型別的例外。它絕不會把錯誤藏在 falsenull 回傳值後面。每個領域例外都繼承同一個抽象基底 NextPdfException,並透過 ContextAwareExceptionInterface 公開結構化診斷脈絡。這則 recipe(範例)示範如何以適當粒度攔截,以及如何將結構化脈絡記錄到應用程式效能監控(APM)管線中。它也會指出哪些失敗是單一 catch-all 無法涵蓋的。

Terminal window
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

這個階層有兩個實務影響,兩者都已對照原始碼驗證:

  1. catch (NextPdfException $e) 會攔截 NextPDF\Exception 下的每一個例外,包含 strict 模式違規。 它們全都繼承自抽象基底。
  2. 它不會攔截函式庫可能擲出的所有例外。 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 介面來自 NextPDF\Exception\NextPdfExceptionNextPDF\Contracts\ContextAwareExceptionInterface、具體領域例外(例如 NextPDF\Exception\FontNotFoundException,帶有 getFontName() / getSearchPaths() / wasFallbackAttempted()),以及 NextPDF\Support\DegradedException(它帶有 CapabilityDegradationPolicy)的 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_budgetwall_ms: 2000peak_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 正規化後的結構是穩定的。