コンテンツにスキップ

契約 / ストリーミング

ストリーミングドメインには、2 つの experimental インターフェイスがあります。増分 PDF 出力向けの StreamingWriterInterface と、ページ単位のコンテンツ構成向けの CursorInterface です。Core は、その両方を実装する final かつテスト済みのエンジンを同梱しています。エンジンクラスは internal であるため、自前で実装するのではなく、パブリックな experimental 契約を介してその動作を利用します。このティアは experimental であるため、契約は事前の非推奨告知を伴い、マイナーリリースで変更される可能性があります。本番環境で依存する前に、バージョンを厳密に固定するか、独自のアダプターでラップしてください。

Terminal window
composer require nextpdf/core:^3

ストリーミングライターは、各ページを構成しながらシリアライズし、次のページが始まる前に出力へフラッシュすることがあります。これは、ドキュメントが利用可能なメモリ予算を超えるワークロード向けに設計された経路です。インメモリライターはドキュメント全体を保持しますが、ストリーミングライターは保持しません。StreamingWriterInterface は厳密なステートマシンを定義します。新規インスタンスは CLOSED です。open() はそれを OPEN へ遷移させ、呼び出し側が指定したストリームに PDF ヘッダーを書き込みます。newPage() はそれを PAGING へ遷移させ、カーソルを返します。close() は相互参照構造とトレーラーを書き込み、それを終端状態の CLOSED へ遷移させます。相互参照ストリームは、各オブジェクト番号をそのバイトオフセットへ対応付けます(ISO 32000-2 §7)。1 インスタンスにつき実行できるセッションは 1 つだけです。close() の後、そのインスタンスは使用済みです。呼び出し側がストリームリソースを所有します。ライターはそこへ書き込みますが、クローズはしません。

CursorInterface はページ単位の書き込みサーフェスです。カーソルは StreamingWriterInterface::newPage() から取得され、明示的にファイナライズされるか、次の newPage() によって自動的にファイナライズされるか、close() によって無効化されるまで有効です。無効化は恒久的です。カーソルを再アクティブ化することはできません。無効化されたカーソルのすべてのメソッドは LogicException をスローします。カーソルは生のコンテンツストリーム演算子を書き込み、アクティブなフォントを設定し、位置指定されたテキストを書き込みます。コンテンツストリームは、ページのコンテンツをグラフィックス演算子の並びとしてエンコードします(ISO 32000-2 §8)。カーソルは低レベルのサーフェスです。テキストシェーピング、双方向の並べ替え、行分割、その他のレイアウトは行いません。それらは Document レベルの関心事のままです。単一カーソルの不変条件は全体を通じて成立します。いかなる時点でも、有効なカーソルは最大 1 つです。

どちらのインターフェイスも experimental であり、Core はそれらの背後で動作するエンジンを同梱しています。final な StreamingWriterInterface 実装、そのページカーソル、メモリベンチマークに使用される破棄シンクです。これらのエンジンクラスは internal であり、パブリックサーフェスの一部ではありません。ストリーミングを使用するサポート済みの方法は、experimental 契約に依存し、Core に実装を供給させることです。各型の PHPDoc は、ライフサイクルのステートマシンとスコープの根拠について streaming-writer ADR を参照しています。このティアは experimental であるため、契約のシグネチャは事前の非推奨告知を伴い、なおマイナーリリースで変更される可能性があります。本番環境で依存する前に、バージョンを厳密に固定するか、独自のアダプターでラップしてください。

種別主なメンバー安定性導入バージョン
StreamingWriterInterfaceinterfaceopen(resource, Config), newPage(?PageSize): CursorInterface, close()experimental(同梱エンジン)3.1.0
CursorInterfaceinterfacewriteContent(string), setFont(string, string, float), writeText(float, float, string), finalizePage()experimental(同梱エンジン)3.1.0

open() は、書き込み不可のストリームに対しては InvalidArgumentException を、ライターがすでに開かれている場合は LogicException をスローします。close() は冪等ではありません。二重クローズは例外をスローします。

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 により、ページループが例外をスローした場合でも、ライターがクローズされ、トレーラーが書き込まれることが保証されます。ストリームを所有しクローズするのは、引き続き呼び出し側です。

  • エンジンクラスではなく、インターフェイスに依存してください。両方の契約を実装するエンジンは internal であり、パブリックサーフェスの一部ではありません。それを new したり、名前で参照したりしないでください。StreamingWriterInterface を型ヒントとして指定し、Core に実装を供給させてください。
  • この契約は experimental です。そのシグネチャは、事前の非推奨告知を伴い、マイナーリリースで変更される可能性があります。本番環境で依存する前に、バージョンを厳密に固定するか、独自のアダプターでラップしてください。
  • カーソルは、次の newPage() または close() が呼び出された瞬間に無効化されます。古くなったカーソルを保持してそのメソッドを呼び出すと、LogicException がスローされます。明確さのため、明示的にファイナライズしてください。
  • close() は冪等ではありません。二重クローズは呼び出し側のバグであり、回復可能な状態ではありません。契約は例外をスローします。
  • ライターがストリームをクローズすることはありません。close() が返った後に呼び出し側が所有するハンドルをクローズし忘れると、ファイルディスクリプターがリークします。
  • エンジンはファイナライズされた各ページをフラッシュするため、常駐メモリがページ数に応じて増加することはありません。正確なメモリプロファイルは experimental ティアの特性であり、マイナーリリース間で変動する可能性があります。1 回の計測から得た想定をハードコードしないでください。

ストリーミング設計はピークメモリを制限します。同梱エンジンは完成した各ページをフラッシュしてそのバッファを解放するため、インメモリライターとは異なり、常駐セットがページ数に応じて増加することはありません。エンジンは、プロセスのフットプリントをほぼ一定に保つため、相互参照とページツリーの管理情報をディスクにバックされた一時ストリームへ退避します。具体的なメモリと実時間の数値は experimental ティアの特性であり、マイナーリリース間で変化する可能性があるため、ここでは固定値を明言しません。実時間 1500 ms、ピーク 64 MB という performance_budget は、キャンバスのエンベロープであり、契約上の保証ではありません。再現性は bitwise です。同じコンテンツと構成はバイト単位で同一の出力を生成し、それはエンジンのゴールデンベースラインテストによって固定されます。

カーソルの writeContent() は低レベルのエスケープハッチです。これは、指定されたバイト列をページコンテンツストリームにそのまま追加し、演算子の構文やセマンティクスを検証しません。writeContent() に渡された信頼できない入力は、破損した PDF または悪意のある PDF を生成します。呼び出し側は、このメソッドを信頼できる入力専用のサーフェスとして扱い、呼び出し側の影響を受けるテキストには writeText() を優先しなければなりません。同梱カーソルは、writeText() に渡されたテキストを PDF のリテラル文字列文法に合わせてエスケープしますが、生の演算子をサニタイズすることはありません。呼び出し側がストリームを所有するモデルも、セキュリティ上の特性です。エンジンはストリームに書き込みますが、それをクローズしたり再オープンしたりしないため、出力をリダイレクトすることはできません。エンジンが同梱される以上、ランタイムの攻撃対象領域は現実に存在します。信頼できないバイト列を決して writeContent() に渡さない義務は呼び出し側にあり、契約の不変条件を守る義務はエンジンにあります。

主張規格箇条エビデンス
コンテンツストリームは、ページのコンテンツをグラフィックス演算子の並びとしてエンコードし、カーソルはそれを追加します。ISO 32000-2§8
ライターはクローズ時に、各オブジェクト番号をそのバイトオフセットへ対応付ける相互参照構造を出力します。ISO 32000-2§7

どちらの箇条も用語集で固定され、本文では言い換えています。NextPDF は規範的なテキストを一切複製しません。契約の PHPDoc から参照される streaming-writer ADR が、ライフサイクルとスコープの根拠を保持しています。

テスト済みのストリーミングエンジンは、これらの experimental 契約の背後で、オープンソースの Core に同梱されています。エンジンクラスは internal であるため、具体的なクラス名ではなく、パブリックな契約を介してストリーミングを利用します。NextPDF Pro と NextPDF Enterprise は同じ契約に従うため、Core で StreamingWriterInterface に対して記述されたコードは、同じ契約の Premium 実装に対しても有効なままです。注意点は、エディションや提供状況ではなく、experimental ティアです。シグネチャは、事前の非推奨告知を伴い、マイナーリリースで変更される可能性があります。