跳转到内容

契约 / 串流

串流领域包含两个 experimental(实验性)接口:StreamingWriterInterface 负责增量输出 PDF,CursorInterface 负责页面层级的内容组合。Core 随附一套 final 且已测试的引擎,同时实现这两个接口。引擎类属于内部实现,因此你应通过公开的 experimental 契约来使用其行为,而不是自行实现。由于这一层级为 experimental,契约可能在某个次要版本中变更,并会事先发出弃用通知。在正式环境依赖它之前,请严格锁定版本,或用你自己的适配器将它封装起来。

Terminal window
composer require nextpdf/core:^3

在每一页组合完成后,串流写入器会将其序列化,并可在开始下一页前先输出到目标。当工作负载生成的文件大小超出可用内存预算时,这正是它面向的使用路径。内存式写入器会保留整份文件;串流写入器不会。StreamingWriterInterface 定义了一套严格的状态机。新建实例处于 CLOSED 状态。open() 会将它转移到 OPEN,并把 PDF 标头写入调用方提供的串流。newPage() 会将它转移到 PAGING,并返回一个游标。close() 会写出交叉引用结构与尾部(trailer),并将它转移到终端状态 CLOSED。交叉引用串流会把每个对象编号映射到它的字节偏移量——ISO 32000-2 §7。每个实例只会执行一次会话。在 close() 之后,该实例即已用尽。串流资源由调用方持有。写入器只会写入它,绝不会关闭它。

CursorInterface 是页面层级的写入接口。游标由 StreamingWriterInterface::newPage() 返回,并在以下任一情况发生前保持有效:游标被最终化、下一次 newPage() 自动将它最终化,或 close() 使它失效。一旦失效即为永久,游标无法重新启用。在已失效的游标上调用任何方法都会抛出 LogicException。游标会写入原始内容串流运算子、设置当前字体,并写入带定位的文本。内容串流会把页面内容编码为一连串绘图运算子——ISO 32000-2 §8。游标是底层接口:它不执行文本塑形、双向重排、断行或任何版面布局。这些仍属 Document 层级的职责。单一游标不变量始终成立:任意时刻最多只有一个游标有效。

这两个接口都是 experimental,而 Core 在它们背后随附一套可正常工作的引擎——一个 final 的 StreamingWriterInterface 实现、它的页面游标,以及一个用于内存基准测试的丢弃式接收端。这些引擎类属于内部实现,并非公开接口的一部分。受支持的串流用法,是依赖 experimental 契约,并让 Core 提供实现。每个类型上的 PHPDoc 都指向串流写入器的 ADR,说明其生命周期状态机与范围依据。由于这一层级为 experimental,契约签名仍可能在某个次要版本中变更,并会事先发出弃用通知。在正式环境依赖它之前,请严格锁定版本,或用你自己的适配器将它封装起来。

类型种类主要成员稳定性自版本
StreamingWriterInterface接口open(resource, Config)newPage(?PageSize): CursorInterfaceclose()experimental(实验性,随附引擎)3.1.0
CursorInterface接口writeContent(string)setFont(string, string, float)writeText(float, float, string)finalizePage()experimental(实验性,随附引擎)3.1.0

当串流不可写入时,open() 会抛出 InvalidArgumentException;若写入器已开启,则会抛出 LogicExceptionclose() 不具备幂等性。重复关闭会抛出异常。

examples/contracts/streaming-quickstart.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;
use NextPDF\Core\Config;
/**
* Drive a streaming writer through one page.
*
* The parameter is the experimental contract; Core supplies the
* implementation. Type-hint the interface and let the engine satisfy it.
*
* @param StreamingWriterInterface $writer A Core-supplied streaming writer.
* @param resource $stream A writable, caller-owned stream.
*/
function writeOnePage(StreamingWriterInterface $writer, $stream): void
{
$writer->open($stream, new Config());
$cursor = $writer->newPage();
$cursor->setFont('helvetica', '', 12.0);
$cursor->writeText(72.0, 720.0, 'Streamed page.');
$cursor->finalizePage();
$writer->close();
// The caller closes $stream after close() returns.
}

此函数面向 experimental 接口编写,因此它与引擎类保持解耦。Core 会在调用点提供一套可正常工作的实现。

examples/contracts/streaming-production.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\StreamingWriterInterface;
use NextPDF\Core\Config;
use NextPDF\ValueObjects\PageSize;
use Psr\Log\LoggerInterface;
final readonly class LargeReportStreamer
{
public function __construct(
private StreamingWriterInterface $writer,
private LoggerInterface $logger,
) {}
/**
* Stream a multi-page report to a caller-owned file handle.
*
* @param resource $stream Writable file handle owned by the caller.
* @param list<list<string>> $pages One list of text lines per page.
*/
public function stream($stream, array $pages): void
{
$this->writer->open($stream, new Config());
try {
foreach ($pages as $lines) {
$cursor = $this->writer->newPage(PageSize::A4());
$cursor->setFont('helvetica', '', 11.0);
$y = 760.0;
foreach ($lines as $line) {
$cursor->writeText(72.0, $y, $line);
$y -= 14.0;
}
$cursor->finalizePage();
}
} finally {
$this->writer->close();
}
}
}

即使页面循环抛出异常,finally 也能保证写入器被关闭,并写出尾部(trailer)。串流仍由调用方持有,并由调用方负责关闭。

  • 请依赖接口,而非引擎类。实现这两个契约的引擎属于内部实现,并非公开接口的一部分。请勿 new 它,也不要按名称引用它。请以 StreamingWriterInterface 作类型提示,并让 Core 提供实现。
  • 此契约为 experimental。其签名可能在某个次要版本中变更,并会事先发出弃用通知。在正式环境依赖它之前,请严格锁定版本,或用你自己的适配器将它封装起来。
  • 游标会在下一次调用 newPage()close() 时立即失效。持有过期的游标并在其上调用方法,会抛出 LogicException。为了清晰起见,请明确地最终化。
  • close() 不具备幂等性。重复关闭是调用方的编程错误,而非可恢复的情况。契约会抛出异常。
  • 写入器绝不会关闭串流。在 close() 返回后忘记关闭由调用方持有的句柄,会造成文件描述符泄漏。
  • 引擎会把每一页最终化后的内容输出,因此常驻内存不会随页数增长。确切的内存特征属于 experimental 层级的特性,可能在不同次要版本间变动。请勿根据单次测量结果写死任何假设。

串流设计会限制峰值内存用量。随附的引擎会把每一页完成后的内容输出并释放其缓冲区,因此常驻集不会随页数增长,这与内存式写入器不同。引擎会把交叉引用与页面树的记账数据溢写到以磁盘为后备的临时串流中,借此让进程占用量保持近乎恒定。具体的内存与耗时数字属于 experimental 层级的特性,可能在不同次要版本间变动,因此此处不声明任何固定数值。1500 ms 耗时与 64 MB 峰值的 performance_budget 是 canvas 的范围上限,而非契约上的保证。可重现性是 bitwise 的:相同的内容与配置会产生完全相同的输出,引擎的黄金基准测试会固定验证这一点。

游标的 writeContent() 是一个底层逃逸口。它会把提供的字节原封不动地附加到页面内容串流,且不验证运算子的语法或语义。把不可信的输入传给 writeContent(),会生成损坏或恶意的 PDF。调用方必须将该方法视为仅接受可信输入的接口,并对任何可能受调用方影响的文本优先使用 writeText()。随附的游标会按 PDF 字面字符串语法转义传给 writeText() 的文本,但它并不会清理原始运算子。串流由调用方持有的模型也是一项安全特性。引擎会写入串流,但绝不会关闭或重新开启它,因此它无法重定向输出。由于引擎随附交付,其运行时攻击面是真实存在的。责任边界是:调用方绝不可把不可信的字节传给 writeContent(),而引擎必须遵守契约的不变量。

主张标准条款佐证
内容串流会把页面内容编码为一连串绘图运算子,并由游标附加上去。ISO 32000-2§8
写入器会在关闭时发出一个交叉引用结构,把每个对象编号映射到它的字节偏移量。ISO 32000-2§7

这两个条款都已固定到词汇表,并以改写摘要呈现。NextPDF 不复制任何规范性文本。契约 PHPDoc 所引用的串流写入器 ADR,记录了生命周期与范围的依据。

一套已测试的串流引擎随附于开源 Core 中,作为这些 experimental 契约背后的实现。引擎类属于内部实现,因此你是通过公开契约来使用串流,而不是通过具体的类名称。NextPDF Pro 与 NextPDF Enterprise 遵循相同的契约,因此在 Core 中针对 StreamingWriterInterface 编写的代码,使用 Premium 对同一契约的实现时仍然有效。需要留意的是 experimental 层级——而非版本或供应状态。其签名可能在某个次要版本中变更,并会事先发出弃用通知。