借助 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 规范化后的结构是稳定的。