跳转到内容

错误即功能

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 规范使用。