Zum Inhalt springen

Contracts / Streaming

Die Streaming-Domäne enthält zwei experimental-Schnittstellen: StreamingWriterInterface für inkrementelle PDF-Ausgabe und CursorInterface für die Komposition von Seiteninhalten. Core stellt eine finale, getestete Engine bereit, die beide implementiert. Die Engine-Klassen sind intern; Sie nutzen das Verhalten daher über den öffentlichen experimental-Vertrag, statt ihn selbst zu implementieren. Da die Stufe experimental ist, kann sich der Vertrag in einem Minor-Release mit vorheriger Deprecation-Ankündigung ändern. Pinnen Sie ihn eng oder kapseln Sie ihn hinter einem eigenen Adapter, bevor Sie sich in Produktion darauf verlassen.

Terminal-Fenster
composer require nextpdf/core:^3

Ein Streaming-Writer serialisiert jede Seite, sobald sie komponiert ist, und kann sie noch vor Beginn der nächsten Seite in die Ausgabe flushen. Das ist der vorgesehene Weg für eine Arbeitslast, bei der das Dokument das verfügbare Speicherbudget übersteigt. Der In-Memory-Writer hält das gesamte Dokument vor; ein Streaming-Writer tut das nicht. StreamingWriterInterface definiert einen strikten Zustandsautomaten. Eine neue Instanz ist CLOSED. open() überführt sie nach OPEN und schreibt den PDF-Header in einen vom Aufrufer bereitgestellten Stream. newPage() überführt sie nach PAGING und gibt einen Cursor zurück. close() schreibt die Cross-Reference-Struktur und den Trailer und überführt sie in ein terminales CLOSED. Ein Cross-Reference-Stream bildet jede Objektnummer auf ihren Byte-Offset ab — ISO 32000-2 §7. Pro Instanz läuft nur eine Sitzung. Nach close() ist die Instanz verbraucht. Die Stream-Ressource gehört dem Aufrufer; der Writer schreibt hinein, schließt sie aber nie.

CursorInterface ist die Schreiboberfläche auf Seitenebene. Ein Cursor stammt aus StreamingWriterInterface::newPage() und bleibt gültig, bis er finalisiert wird, der nächste newPage() ihn automatisch finalisiert oder close() ihn ungültig macht. Die Invalidierung ist dauerhaft. Ein Cursor lässt sich nicht reaktivieren. Jeder Methodenaufruf auf einem invalidierten Cursor wirft eine LogicException. Der Cursor schreibt rohe Content-Stream-Operatoren, setzt die aktive Schriftart und schreibt positionierten Text. Ein Content-Stream kodiert Seiteninhalt als Folge von Grafik-Operatoren — ISO 32000-2 §8. Der Cursor ist eine Low-Level-Oberfläche: Er führt kein Text-Shaping, keine bidirektionale Umsortierung, keinen Zeilenumbruch und kein Layout aus. Das bleibt Aufgabe der Document-Ebene. Die Single-Cursor-Invariante gilt durchgängig: Zu jedem Zeitpunkt ist höchstens ein Cursor gültig.

Beide Schnittstellen sind experimental, und Core liefert dafür eine funktionierende Engine — eine finale Implementierung von StreamingWriterInterface, ihren Seiten-Cursor und einen Discard-Sink für Speicher-Benchmarking. Diese Engine-Klassen sind intern und nicht Teil der öffentlichen Fläche. Der unterstützte Weg zur Nutzung von Streaming besteht darin, vom experimental-Vertrag abzuhängen und Core die Implementierung liefern zu lassen. Der PHPDoc-Kommentar jedes Typs verweist für den Lifecycle-Zustandsautomaten und die Scope-Begründung auf die Streaming-Writer-ADR. Da die Stufe experimental ist, kann sich die Vertragssignatur in einem Minor-Release mit vorheriger Deprecation-Ankündigung noch ändern. Pinnen Sie den Vertrag eng oder kapseln Sie ihn hinter einem eigenen Adapter, bevor Sie sich in Produktion darauf verlassen.

TypArtWichtige MitgliederStabilitätSeit
StreamingWriterInterfaceinterfaceopen(resource, Config), newPage(?PageSize): CursorInterface, close()experimental (ausgelieferte Engine)3.1.0
CursorInterfaceinterfacewriteContent(string), setFont(string, string, float), writeText(float, float, string), finalizePage()experimental (ausgelieferte Engine)3.1.0

open() wirft bei einem nicht beschreibbaren Stream eine InvalidArgumentException und eine LogicException, wenn der Writer bereits offen ist. close() ist nicht idempotent. Ein zweiter Close-Aufruf wirft.

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.
}

Die Funktion ist gegen die experimental-Schnittstelle geschrieben und bleibt damit von der Engine-Klasse entkoppelt. Core injiziert an der Aufrufstelle eine funktionierende Implementierung.

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();
}
}
}

Das finally garantiert, dass der Writer geschlossen und der Trailer geschrieben wird, selbst wenn eine Seitenschleife wirft. Der Stream gehört weiterhin dem Aufrufer; auch das Schließen übernimmt er.

  • Hängen Sie von der Schnittstelle ab, nicht von der Engine-Klasse. Die Engine, die beide Verträge implementiert, ist intern und nicht Teil der öffentlichen Fläche. Erzeugen Sie sie nicht mit new und referenzieren Sie sie nicht über ihren Namen. Verwenden Sie StreamingWriterInterface als Type Hint und lassen Sie Core die Implementierung liefern.
  • Der Vertrag ist experimental. Seine Signatur kann sich in einem Minor-Release ändern, mit vorheriger Deprecation-Ankündigung. Pinnen Sie ihn eng oder kapseln Sie ihn hinter einem eigenen Adapter, bevor Sie sich in Produktion darauf verlassen.
  • Ein Cursor wird ungültig, sobald der nächste newPage() oder close() aufgerufen wird. Wenn Sie einen veralteten Cursor halten und eine Methode darauf aufrufen, wirft das eine LogicException. Finalisieren Sie der Klarheit halber explizit.
  • close() ist nicht idempotent. Ein doppelter Close-Aufruf ist ein Aufruferfehler, kein behebbarer Zustand. Der Vertrag wirft.
  • Der Writer schließt den Stream nie. Wenn Sie vergessen, ein Handle des Aufrufers zu schließen, nachdem close() zurückgekehrt ist, bleibt ein File-Descriptor offen.
  • Die Engine flusht jede finalisierte Seite, sodass der residente Speicher nicht mit der Seitenzahl wächst. Das genaue Speicherprofil ist eine Eigenschaft der experimental-Stufe und kann sich zwischen Minor-Releases verschieben. Verdrahten Sie keine Annahme aus einer einzelnen Messung fest.

Das Streaming-Design begrenzt den Spitzenspeicher. Die ausgelieferte Engine flusht jede fertige Seite und gibt ihren Puffer frei, sodass der Resident Set anders als beim In-Memory-Writer nicht mit der Seitenzahl wächst. Die Engine lagert ihre Cross-Reference- und Page-Tree-Buchführung in plattengestützte temporäre Streams aus, um den Prozess-Footprint nahezu konstant zu halten. Konkrete Speicher- und Laufzeitwerte sind eine Eigenschaft der experimental-Stufe und können sich zwischen Minor-Releases verschieben; daher wird hier keine feste Zahl zugesichert. Das performance_budget von 1500 ms Laufzeit und 64 MB Spitze ist die Canvas-Hülle, keine vertragliche Garantie. Die Reproduzierbarkeit ist bitwise: Derselbe Inhalt und dieselbe Konfiguration erzeugen bytegenau dieselbe Ausgabe, die die Golden-Baseline-Tests der Engine festpinnen.

Das writeContent() des Cursors ist ein Low-Level-Escape-Hatch. Es hängt die übergebenen Bytes wortwörtlich an den Page-Content-Stream an und validiert weder Operator-Syntax noch -Semantik. Nicht vertrauenswürdige Eingabe, die an writeContent() übergeben wird, erzeugt ein korruptes oder bösartiges PDF. Ein Aufrufer muss diese Methode als Oberfläche ausschließlich für vertrauenswürdige Eingabe behandeln und für jeden vom Aufrufer beeinflussten Text writeText() bevorzugen. Der ausgelieferte Cursor escapt Text, der an writeText() übergeben wird, für die PDF-Literal-String-Grammatik, sanitisiert aber keine rohen Operatoren. Das Modell, bei dem der Stream im Besitz des Aufrufers bleibt, ist ebenfalls eine Sicherheitseigenschaft. Die Engine schreibt in den Stream, schließt oder öffnet ihn aber nie erneut und kann die Ausgabe daher nicht umleiten. Die Angriffsfläche zur Laufzeit ist real, weil die Engine ausgeliefert wird. Die Verantwortung liegt bei den Aufrufern, nie nicht vertrauenswürdige Bytes an writeContent() zu füttern, und bei der Engine, die Invarianten des Vertrags einzuhalten.

AussageStandardKlauselNachweis
Ein Content-Stream kodiert Seiteninhalt als Folge von Grafik-Operatoren, die der Cursor anhängt.ISO 32000-2§8
Der Writer gibt beim Schließen eine Cross-Reference-Struktur aus, die jede Objektnummer auf ihren Byte-Offset abbildet.ISO 32000-2§7

Beide Klauseln sind im Glossar gepinnt und paraphrasiert. NextPDF gibt keinen normativen Text wieder. Die von der Vertrags-PHPDoc referenzierte Streaming-Writer-ADR enthält die Begründung zu Lifecycle und Scope.

Eine getestete Streaming-Engine wird im quelloffenen Core hinter diesen experimental-Verträgen ausgeliefert. Die Engine-Klassen sind intern; Sie nutzen Streaming daher über den öffentlichen Vertrag statt über einen konkreten Klassennamen. NextPDF Pro und NextPDF Enterprise folgen demselben Vertrag, sodass Code, der in Core gegen StreamingWriterInterface geschrieben ist, auch gegen eine Premium-Implementierung desselben Vertrags gültig bleibt. Der Vorbehalt betrifft die experimental-Stufe — nicht Edition oder Verfügbarkeit. Die Signatur kann sich in einem Minor-Release mit vorheriger Deprecation-Ankündigung ändern.