Contratti / Streaming
In sintesi
Sezione intitolata “In sintesi”Il dominio dello streaming contiene due interfacce experimental: StreamingWriterInterface per l’output PDF incrementale e CursorInterface per la composizione dei contenuti a livello di pagina. Core include un motore finale e testato che le implementa entrambe. Le classi del motore sono interne, quindi l’uso passa dal contratto pubblico experimental, anziché da un’implementazione diretta. Poiché il livello è experimental, il contratto può cambiare in una versione minore con preavviso di deprecazione. Vincolarne rigidamente la versione o incapsularlo dietro un adattatore personalizzato prima di dipenderne in produzione.
Installazione
Sezione intitolata “Installazione”composer require nextpdf/core:^3Panoramica concettuale
Sezione intitolata “Panoramica concettuale”Un writer in streaming serializza ogni pagina mentre viene composta e può inviarla all’output prima che inizi la pagina successiva. È il percorso previsto per carichi di lavoro in cui il documento supera il budget di memoria disponibile. Il writer in memoria conserva l’intero documento; un writer in streaming no. StreamingWriterInterface definisce una macchina a stati rigorosa. Un’istanza appena creata è CLOSED. open() la porta in OPEN e scrive l’intestazione PDF in un flusso fornito dal chiamante. newPage() la porta in PAGING e restituisce un cursore. close() scrive la struttura di riferimento incrociato e il trailer, quindi la porta nello stato terminale CLOSED. Un flusso di riferimento incrociato associa ogni numero di oggetto al relativo offset in byte — ISO 32000-2 §7. Per ogni istanza viene eseguita una sola sessione. Dopo close() l’istanza è esaurita. La risorsa del flusso resta di proprietà del chiamante. Il writer vi scrive, ma non lo chiude mai.
CursorInterface è la superficie di scrittura a livello di pagina. Un cursore si ottiene da StreamingWriterInterface::newPage() e resta valido finché non viene finalizzato, finché la successiva newPage() non lo finalizza automaticamente oppure finché close() non lo invalida. L’invalidazione è permanente: un cursore non può essere riattivato. Ogni metodo chiamato su un cursore invalidato genera LogicException. Il cursore scrive operatori grezzi nel flusso di contenuto, imposta il font attivo e scrive testo posizionato. Un flusso di contenuto codifica il contenuto della pagina come una sequenza di operatori grafici — ISO 32000-2 §8. Il cursore è una superficie di basso livello: non esegue text shaping, riordinamento bidirezionale, interruzione di riga né alcun layout. Questi aspetti restano di competenza del livello Document. L’invariante del cursore singolo vale sempre: al massimo un cursore è valido in qualsiasi momento.
Entrambe le interfacce sono experimental e Core fornisce un motore funzionante che le supporta: un’implementazione finale di StreamingWriterInterface, il relativo cursore di pagina e un sink di scarto usato per il benchmarking della memoria. Queste classi del motore sono interne e non fanno parte della superficie pubblica. Il modo supportato per usare lo streaming è dipendere dal contratto experimental e lasciare che Core fornisca l’implementazione. Il PHPDoc di ogni tipo rimanda all’ADR dello streaming writer per la macchina a stati del ciclo di vita e la motivazione dell’ambito. Poiché il livello è experimental, la firma del contratto può ancora cambiare in una versione minore con preavviso di deprecazione. Vincolarne rigidamente la versione o incapsularlo dietro un adattatore personalizzato prima di dipenderne in produzione.
Superficie API
Sezione intitolata “Superficie API”| Tipo | Genere | Membri principali | Stabilità | Da |
|---|---|---|---|---|
StreamingWriterInterface | interface | open(resource, Config), newPage(?PageSize): CursorInterface, close() | experimental (motore fornito) | 3.1.0 |
CursorInterface | interface | writeContent(string), setFont(string, string, float), writeText(float, float, string), finalizePage() | experimental (motore fornito) | 3.1.0 |
open() genera InvalidArgumentException se il flusso non è scrivibile e LogicException se il writer è già aperto. close() non è idempotente: una seconda chiusura genera un’eccezione.
Esempio di codice — Avvio rapido
Sezione intitolata “Esempio di codice — Avvio rapido”<?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.}La funzione è tipizzata sull’interfaccia experimental, quindi resta disaccoppiata dalla classe del motore. Core inietta un’implementazione funzionante al punto di chiamata.
Esempio di codice — Produzione
Sezione intitolata “Esempio di codice — Produzione”<?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(); } }}Il blocco finally garantisce la chiusura del writer e la scrittura del trailer anche quando un ciclo sulle pagine genera un’eccezione. Il chiamante resta comunque proprietario del flusso e si occupa di chiuderlo.
Casi limite e insidie
Sezione intitolata “Casi limite e insidie”- Dipendere dall’interfaccia, non dalla classe del motore. Il motore che implementa entrambi i contratti è interno e non fa parte della superficie pubblica. Non istanziarlo con
newné referenziarlo per nome. Usare il type hintStreamingWriterInterfacee lasciare che Core fornisca l’implementazione. - Il contratto è
experimental. La sua firma può cambiare in una versione minore, con preavviso di deprecazione. Vincolarne rigidamente la versione o incapsularlo dietro un adattatore personalizzato prima di dipenderne in produzione. - Un cursore viene invalidato quando viene chiamato il successivo
newPage()oclose(). Conservare un cursore obsoleto e richiamarne un metodo generaLogicException. Finalizzarlo esplicitamente per chiarezza. close()non è idempotente. Una seconda chiusura è un bug del chiamante, non una condizione recuperabile. Il contratto genera un’eccezione.- Il writer non chiude mai il flusso. Dimenticare di chiudere un handle di proprietà del chiamante dopo il ritorno di
close()provoca una perdita di descrittori di file. - Il motore scarica ogni pagina finalizzata in modo che la memoria residente non cresca con il numero di pagine. Il profilo di memoria esatto è una proprietà di livello
experimentale può variare tra versioni minori. Non codificare in modo rigido un’assunzione basata su una sola misurazione.
Prestazioni
Sezione intitolata “Prestazioni”La progettazione in streaming limita la memoria di picco. Il motore fornito scarica ogni pagina completata e rilascia il relativo buffer, quindi il resident set non cresce con il numero di pagine, a differenza del writer in memoria. Il motore sposta la gestione del riferimento incrociato e dell’albero delle pagine su flussi temporanei su disco per mantenere quasi costante l’impronta del processo. Le cifre concrete di memoria e tempo reale sono una proprietà di livello experimental e possono variare tra versioni minori; per questo qui non si dichiara alcun valore fisso. Il performance_budget di 1500 ms di tempo reale e 64 MB di picco è il limite del canvas, non una garanzia contrattuale. La riproducibilità è bitwise: gli stessi contenuti e la stessa configurazione producono un output identico byte per byte, verificato dai test golden-baseline del motore.
Note sulla sicurezza
Sezione intitolata “Note sulla sicurezza”Il metodo writeContent() del cursore è una via di fuga di basso livello. Accoda letteralmente i byte forniti al flusso di contenuto della pagina e non convalida né la sintassi né la semantica degli operatori. Un input non attendibile passato a writeContent() produce un PDF corrotto o malevolo. Il chiamante deve trattare quel metodo come una superficie riservata a input attendibile e preferire writeText() per qualsiasi testo influenzato dal chiamante. Il cursore fornito esegue l’escape del testo passato a writeText() secondo la grammatica delle stringhe letterali PDF, ma non sanifica gli operatori grezzi. Anche il modello in cui il flusso appartiene al chiamante è una proprietà di sicurezza. Il motore scrive nel flusso, ma non lo chiude né lo riapre mai, quindi non può reindirizzare l’output. La superficie di attacco a runtime è concreta perché il motore viene fornito. La responsabilità ricade sui chiamanti, che non devono mai fornire byte non attendibili a writeContent(), e sul motore, che deve rispettare gli invarianti del contratto.
Conformità
Sezione intitolata “Conformità”| Asserzione | Standard | Clausola | Evidenza |
|---|---|---|---|
| Un flusso di contenuto codifica il contenuto della pagina come una sequenza di operatori grafici, che il cursore accoda. | ISO 32000-2 | §8 | |
| Alla chiusura il writer emette una struttura di riferimento incrociato che associa ogni numero di oggetto al relativo offset in byte. | ISO 32000-2 | §7 |
Entrambe le clausole sono ancorate al glossario e parafrasate. NextPDF non riproduce alcun testo normativo. L’ADR dello streaming writer, richiamato dal PHPDoc del contratto, contiene la motivazione del ciclo di vita e dell’ambito.
Contesto commerciale
Sezione intitolata “Contesto commerciale”Un motore di streaming testato viene fornito con il Core open source dietro questi contratti experimental. Le classi del motore sono interne, quindi lo streaming si usa tramite il contratto pubblico, non tramite un nome di classe concreto. NextPDF Pro e NextPDF Enterprise seguono lo stesso contratto, quindi il codice scritto su StreamingWriterInterface in Core resta valido con un’implementazione Premium dello stesso contratto. L’avvertenza riguarda il livello experimental, non l’edizione o la disponibilità. La firma può cambiare in una versione minore con preavviso di deprecazione.
Vedere anche
Sezione intitolata “Vedere anche”- Contract: 41 interfacce pubbliche (SPI) — panoramica della SPI e dei livelli di stabilità.
- Contract / Document — il writer in memoria che questi contratti completano.
- Writer — l’emettitore di oggetti PDF e riferimenti incrociati.
- HTML / Vincoli di streaming (ADR-001) — la motivazione dell’ambito per lo streaming.
- Prestazioni — la motivazione legata ai limiti di memoria per l’output in streaming.