拒绝猜测的 API
Spec: ISO/IEC 25010 ISO/IEC 25010 Spec: ISO 32000-2 ISO 32000-2 Evidence: Code-backed
NextPDF 要求你说清楚真正的意图。凡是意图会改变字节的地方——签名层级、输出目的地、符合性目标——它都会是一个必填且明确的参数,而不是由引擎从上下文推断出来。
本页面通过引擎自身的源代码呈现这一立场:方法签名、命名参数,以及那些在生成任何字节之前就拒绝含糊输入的位置。
为什么这很重要
标题为“为什么这很重要”的章节猜测,就是在不告知你的情况下替你做决定。对于一个文本字段来说,这顶多有点烦人。但对于 PDF 而言,这是一个潜在缺陷,因为你交付的往往是具有法律或封存效力的成品,其正确性会在日后由其他人用验证器检查。
以签名为例。其摘要是基于一个已声明的字节范围计算出来的,该范围特意排除了签名值本身( Spec: ISO 32000-2, §12.8 ISO 32000-2 §12.8 )。一个悄悄「帮忙」的 API——改写结构、推断层级、填补占位符——并不是真的在帮忙。它改变了签名本应保护的那些字节。那个在调用处看似贴心的猜测,数周后就会变成生产环境事故。它们是同一行代码。
简短版本
标题为“简短版本”的章节- 如果某个选择会改变输出且没有安全的默认值,NextPDF 就会把它设为必填参数,而不是靠推断得出。
- 读起来含糊不清的可选参数会被写出名称,因此调用处能陈述意图(
newLine: true,而非裸露的true)。 - 可能不安全的输入会在渲染之前先经过验证,并以一个指明原因的带类型异常加以拒绝。
- 文件实例是一次性使用的:构建、输出,然后丢弃。没有
reset(),所以也就没有「这东西被重复使用了吗?」的猜测。 - 引擎绝不会用一个看似合理的成品,替代你实际要求的那一个。它会选择拒绝。
NextPDF 如何处理这个问题
标题为“NextPDF 如何处理这个问题”的章节这套机制并不花哨,而这正是重点。它就是类型系统、命名参数、以枚举取代魔法字符串,以及少数刻意安排在输出之前的防护子句。
下表对比了几个含糊的输入。针对每一个输入,它都展示了一个会「帮忙」的库会推断出什么,以及 NextPDF 改为怎么做。每一项 NextPDF 行为,都来自本页面稍后所示的源代码。
| 含糊的输入 | 会猜测的库会怎么做 | NextPDF 会怎么做 |
|---|---|---|
像 "portait" 这样的方向字符串 | 回退到某个默认值并照样渲染 | addPage() 接受 Orientation 枚举,而非字符串——拼写错误是类型错误,而不是无声的默认值 |
传给 cell() 的一个末尾裸露的 true | 挑选它假设你想指的那个布尔参数位置 | 布尔值在调用处写出名称(newLine: true);没有名称的字面值正是这个 API 要消除的坏味道 |
传给 save() 的 php:// 包装器或目录穿越路径 | 「尽力而为」并写到某处 | 在 PDF 构建之前就被拒绝,并以一个带类型的 InvalidConfigException 指明键、值与预期类型 |
在高级签署器尚未接入时调用 setSignature() 接着调用 save() | 输出一个调用者误以为已签署、实则未签署的文件 | 在生成字节之前就抛出 NotImplementedException,并指明受支持的途径 |
重复使用同一个 Document 实例进行第二次渲染 | 猜测残留的状态是否仍然适用 | 没有 reset(),也没有重用路径——每个请求都通过 DocumentFactory 获得全新实例,因此根本没有残留状态可供猜测 |
意图是必填参数。 核心契约 PdfDocumentInterface 以强类型的值对象与枚举接受几何与对齐信息,而非松散的原始类型:
public function addPage( ?PageSize $size = null, Orientation $orientation = Orientation::Portrait,): static;
public function cell( float $width, float $height, string $text = '', bool|string $border = false, bool $newLine = false, Alignment $align = Alignment::Left, bool $fill = false,): static;Orientation 与 Alignment 都是枚举,因此调用方无法传入 "portait" 却让它静默地被当成「默认值」。凡是有默认值的地方,它都是一个安全的默认值(直向、靠左、无框线),而不是对你大概想要什么的猜测。
含糊的布尔值会在调用处写出名称。 在那些实质上充当 API 参考的示例中,同样的模式反复出现:
$document->cell(0, 15, 'Hello, NextPDF!', newLine: true);$document->setSignature(certInfo: $certInfo, level: SignatureLevel::PAdES_B_B);$pdf = $document->output(dest: OutputDestination::String);newLine: true 一目了然,不会被误解。而末尾裸露的 true 则不然。签名层级是 SignatureLevel::PAdES_B_B,一个枚举项——绝不是引擎必须去解读的字符串。输出目的地是 OutputDestination::String,所以「把字节给我,不要 HTTP 标头、不要文件」是被明确陈述的,而不是从是否传入文件名来推测。
不安全的输入会在写入任何一个字节之前就被拒绝。 save() 会在构建 PDF 之前先验证目标路径:
public function save(string $path): void{ // Reject stream wrappers and null bytes if (\str_contains($path, "\0") || \preg_match('#^[a-zA-Z]+://#', $path)) { throw new InvalidConfigException( configKey: 'output_path', givenValue: $path, expectedType: 'valid_path', ); } // Resolve the parent directory to prevent path traversal $dir = \dirname($path); $realDir = \realpath($dir); if ($realDir === false) { throw new InvalidConfigException( configKey: 'output_path', givenValue: $dir, expectedType: 'existing_directory', ); } // ... only now is the PDF built and written atomically}引擎不会对 php:// 包装器或目录穿越路径「尽力而为」。它会拒绝,而该异常会指明键、值,以及预期的内容。
引擎宁愿拒绝,也不输出具有误导性的成品。 拒绝猜测最强烈的形式,就是在输出会产生误导时,干脆完全不产生任何输出。当设定了高级签名,但真正负责签署的写入器接入点尚未接通时,构建路径会在生成字节之前抛出异常,而不是输出一个调用者误以为已签署的未签署文件:
if ($this->padesOrchestrator !== null) { throw new NotImplementedException( feature: 'Document::setSignature()->save()/output()/getPdfData()', followUp: 'The high-level PAdES writer seam is not yet wired ... ' . 'Produce a signed PDF via the direct two-phase ' . 'PadesOrchestrator::signDocument() then finalizeSignature() ' . 'buffer API ...', );}一个看起来已签署、实则未签署的 PDF,正是这个原则要防范的那种看似合理却错误的成品。同样的立场也出现在严格 CSS 路径中。未登记的规范偏差会在检测到时立即抛出 StrictModeViolation,而不是渲染出一个近似结果,并让该偏差继续潜伏。
一次性使用消除了一整类猜测。 Document 设计为用后即弃——构建、输出、丢弃。没有 reset(),也没有重用路径。长时间运行的工作进程会通过 DocumentFactory 为每个请求建立全新实例。引擎永远不必猜测前一份文件的残留状态是否仍有意义,因为从设计上就没有残留状态。
证据显示什么
标题为“证据显示什么”的章节本页面是 Evidence: Code-backed :上述每一种模式都引用自引擎自身的源代码及其示例,而不是根据意图转述。
- 这些带类型、带枚举的方法签名,就是
PdfDocumentInterface中的公开契约。命名参数的调用风格,是贯穿那些实质充当 API 参考的标准示例的一致形式。 - 那段渲染前的路径验证及其带类型的
InvalidConfigException,以及那道在输出前拒绝的NotImplementedException防护,都是逐字引用自文件外观(façade)的输出路径。 - 标准依据是 Spec: ISO/IEC 25010, §3.32 ISO/IEC 25010 §3.32 ——用户错误防护,正是一个拒绝猜测的 API 在调用处所要满足的质量属性。第二个依据是 Spec: ISO 32000-2, §12.8 ISO 32000-2 §12.8 ,这正是为什么在已签署文件相关场景中进行猜测绝不可能无害。该摘要覆盖的是一个已声明的字节范围,并且该范围排除了签名值;因此任何无声的改写都会使其失效。
实践示例
标题为“实践示例”的章节这是一个小而完整的示例程序。每一行可能含糊的代码都明确陈述了意图。唯一的不安全输入,会在任何工作开始之前被拒绝。
<?php
declare(strict_types=1);
use NextPDF\Contracts\OutputDestination;use NextPDF\Core\Document;use NextPDF\Exception\InvalidConfigException;use NextPDF\ValueObjects\PageSize;use NextPDF\Contracts\Orientation;
$document = Document::createStandalone();$document->setTitle('Quarterly Report');
// Intent is explicit: a typed page size and an Orientation enum case,// not a string the engine has to interpret.$document->addPage(PageSize::a4(), Orientation::Landscape);$document->setFont('helvetica', 'B', 16);
// Ambiguous boolean is named, so the call reads as intent.$document->cell(0, 12, 'Quarterly Report', newLine: true);
try { // Unsafe path is rejected before a byte is built. $document->save('php://output/report.pdf');} catch (InvalidConfigException $e) { // "Invalid configuration for key "output_path": expected valid_path, ..." error_log($e->getMessage());
// The String destination is explicit: bytes only, no HTTP headers, // no file side effect. Nothing is inferred from a missing filename. $bytes = $document->output(dest: OutputDestination::String);}这个程序没有任何一条执行路径会悄悄做错事。它要么陈述意图并继续执行,要么指明问题并停止。
常见误解
标题为“常见误解”的章节常见的反对意见是「这不过是啰嗦罢了」。这并非啰嗦,而是没有隐藏默认值。一个裸露的 true 比 newLine: true 短了多少,就正好抹去了多少清晰度。引擎以调用处多出的几个字符,换来的是消除一整类错误——也就是代码能编译、能执行、能生成文件,却是错的那一类。
一个相关的误解是,快速失败意味着「动不动就抛出异常」。在正常使用下,NextPDF 不会抛出任何异常。有效的输入会顺畅通过。这些防护只会由真正含糊或不安全的输入触发——正是那些你会想立即知道、而不想被猜测的输入。
限制与边界
标题为“限制与边界”的章节拒绝猜测适用于意图与安全,而不是每一项便利。NextPDF 仍有安全的默认值:直向方向、靠左对齐、无框线。原则是:只有在安全且不令人意外的地方才提供默认值,而在错误推断会产生错误文件的地方则绝不提供。
本页面在核心公开 API 接口(文件外观、其契约,以及输出路径)上演示了这项原则。各子系统有自己的入口,并分别记录其验证行为。此处引用的模式为本次审阅时的状态。它们是用来说明这个模式,并不是引擎中每一道防护的完整清单。
所描述的快速失败防护是正确性与安全性防护。它们本身并不构成安全边界。输入验证只是其中一层。设计理念与安全文档描述了更广泛的立场。
相关文档
标题为“相关文档”的章节- NextPDF 设计理念——将本页面所演示的原则放到其优先级语境中理解。
- 把错误当成一项功能——这些防护所抛出的带类型异常,旨在告诉你什么。
- 处处严格类型——类型系统如何让「陈述你的意图」成为可强制执行的要求,而不是仅供参考的建议。
术语表
标题为“术语表”的章节- 代码佐证(证据层级)——指其主张会对照引擎自身源代码或可执行示例加以核查,并以引用而非转述呈现的页面。
- 快速失败——在最早的时点,以明确的原因拒绝无效输入,而非继续执行并在稍后晦涩地失败。
- 命名参数——一种 PHP 调用处语法(
newLine: true),按名称将值绑定至参数,使原本含糊的字面值能够自我说明。 - 一次性使用生命周期——用后即弃的
Document契约:实例化、写入、保存、丢弃。没有reset(),不重复使用。工作进程会通过DocumentFactory为每个请求建立全新实例。 - PAdES——PDF 高级电子签名(PDF Advanced Electronic Signatures),用于 PDF 签署的 ETSI 规范系列。首次使用时展开全称;在签署相关页面有深入说明。