跳转到内容

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