Aller au contenu

Contrats / Streaming

Le domaine du streaming regroupe deux interfaces experimental : StreamingWriterInterface, pour la sortie PDF incrémentale, et CursorInterface, pour composer le contenu au niveau de la page. Core livre un moteur final testé qui implémente les deux. Les classes du moteur sont internes : tu utilises donc le comportement via le contrat public experimental, au lieu de l’implémenter toi-même. Comme le niveau est experimental, le contrat peut changer dans une version mineure, après un avis préalable de dépréciation. Épingle-le strictement ou place-le derrière ton propre adaptateur avant d’en dépendre en production.

Fenêtre de terminal
composer require nextpdf/core:^3

Un writer en streaming sérialise chaque page au fil de sa composition et peut la vider vers la sortie avant le début de la page suivante. C’est la voie de conception prévue pour les charges de travail dont le document dépasse le budget mémoire disponible. Le writer en mémoire conserve le document entier. Ce n’est pas le cas d’un writer en streaming. StreamingWriterInterface définit une machine à états stricte. Une instance neuve est CLOSED. open() la fait passer à OPEN et écrit l’en-tête PDF dans un flux fourni par l’appelant. newPage() la fait passer à PAGING et renvoie un curseur. close() écrit la structure de références croisées et le trailer, puis la fait passer à l’état terminal CLOSED. Un flux de références croisées associe chaque numéro d’objet à son décalage en octets — ISO 32000-2 §7. Une seule session s’exécute par instance. Après close(), l’instance est épuisée. La ressource de flux appartient à l’appelant. Le writer y écrit, mais ne la ferme jamais.

CursorInterface est la surface d’écriture au niveau de la page. Un curseur est obtenu depuis StreamingWriterInterface::newPage() et reste valide jusqu’à sa finalisation, jusqu’à ce que le prochain newPage() le finalise automatiquement, ou jusqu’à ce que close() l’invalide. L’invalidation est définitive. Un curseur ne peut pas être réactivé. Toute méthode appelée sur un curseur invalidé lève LogicException. Le curseur écrit des opérateurs bruts dans le flux de contenu, définit la police active et écrit du texte positionné. Un flux de contenu encode le contenu de la page sous forme d’une séquence d’opérateurs graphiques — ISO 32000-2 §8. Le curseur est une surface bas niveau : il n’effectue ni mise en forme du texte (shaping), ni réordonnancement bidirectionnel, ni césure de lignes, ni mise en page. Cela reste du ressort du niveau Document. L’invariant du curseur unique vaut tout au long du cycle : au plus un curseur est valide à un instant donné.

Les deux interfaces sont experimental, et Core livre un moteur fonctionnel derrière elles — une implémentation finale de StreamingWriterInterface, son curseur de page et un puits de rejet utilisé pour le benchmark mémoire. Ces classes de moteur sont internes et ne font pas partie de la surface publique. Le mode d’utilisation pris en charge pour le streaming consiste à dépendre du contrat experimental et à laisser Core fournir l’implémentation. La PHPDoc de chaque type pointe vers l’ADR du streaming-writer pour la machine à états du cycle de vie et la justification du périmètre. Comme le niveau est experimental, la signature du contrat peut encore changer dans une version mineure, après un avis préalable de dépréciation. Épingle-le strictement ou place-le derrière ton propre adaptateur avant d’en dépendre en production.

TypeNatureMembres clésStabilitéDepuis
StreamingWriterInterfaceinterfaceopen(resource, Config), newPage(?PageSize): CursorInterface, close()experimental (moteur livré)3.1.0
CursorInterfaceinterfacewriteContent(string), setFont(string, string, float), writeText(float, float, string), finalizePage()experimental (moteur livré)3.1.0

open() lève InvalidArgumentException pour un flux non accessible en écriture et LogicException si le writer est déjà ouvert. close() n’est pas idempotent. Une double fermeture lève une exception.

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

Comme la fonction est écrite contre l’interface experimental, elle reste découplée de la classe du moteur. Core injecte une implémentation fonctionnelle au point d’appel.

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

Le finally garantit que le writer est fermé et que le trailer est écrit, même si une boucle sur les pages lève une exception. L’appelant reste propriétaire du flux et le ferme lui-même.

  • Dépends de l’interface, pas de la classe du moteur. Le moteur qui implémente les deux contrats est interne et ne fait pas partie de la surface publique. Ne fais pas de new dessus et ne le référence pas par son nom. Type-hinte StreamingWriterInterface et laisse Core fournir l’implémentation.
  • Le contrat est experimental. Sa signature peut changer dans une version mineure, après un avis préalable de dépréciation. Épingle-le strictement ou place-le derrière ton propre adaptateur avant d’en dépendre en production.
  • Un curseur s’invalide dès que le prochain newPage() ou close() est appelé. Conserver un curseur périmé et appeler une méthode dessus lève LogicException. Finalise-le explicitement, pour plus de clarté.
  • close() n’est pas idempotent. Une double fermeture est un bug de l’appelant, pas une condition récupérable. Le contrat lève une exception.
  • Le writer ne ferme jamais le flux. Oublier de fermer un handle détenu par l’appelant après le retour de close() provoque une fuite de descripteur de fichier.
  • Le moteur vide chaque page finalisée afin que la mémoire résidente ne croisse pas avec le nombre de pages. Le profil mémoire exact relève du niveau experimental et peut évoluer d’une version mineure à l’autre. Ne code pas en dur une hypothèse tirée d’une seule mesure.

La conception en streaming borne le pic de mémoire. Le moteur livré vide chaque page terminée et libère son tampon, de sorte que la mémoire résidente ne croît pas avec le nombre de pages, contrairement au writer en mémoire. Le moteur déporte ses structures de suivi des références croisées et de l’arbre de pages vers des flux temporaires adossés au disque, afin de maintenir l’empreinte du processus quasi constante. Les chiffres concrets de mémoire et de temps d’exécution relèvent du niveau experimental et peuvent évoluer d’une version mineure à l’autre ; aucun chiffre fixe n’est donc avancé ici. Le performance_budget de 1500 ms d’exécution et 64 Mo de pic est l’enveloppe du canevas, pas une garantie contractuelle. La reproductibilité est bitwise : le même contenu et la même configuration produisent une sortie identique octet pour octet, ce que verrouillent les tests de référence dorée du moteur.

Le writeContent() du curseur est une trappe de sortie bas niveau. Il ajoute tels quels les octets fournis au flux de contenu de la page et ne valide ni la syntaxe ni la sémantique des opérateurs. Une entrée non fiable passée à writeContent() produit un PDF corrompu ou malveillant. L’appelant doit traiter cette méthode comme une surface réservée aux entrées de confiance et préférer writeText() pour tout texte influencé par l’appelant. Le curseur livré échappe le texte passé à writeText() pour la grammaire des chaînes littérales PDF, mais il n’assainit pas les opérateurs bruts. Le modèle de flux appartenant à l’appelant est aussi une propriété de sécurité. Le moteur écrit dans le flux, mais ne le ferme ni ne le rouvre jamais ; il ne peut donc pas rediriger la sortie. La surface d’attaque à l’exécution est réelle parce que le moteur est livré. Il revient aux appelants de ne jamais transmettre d’octets non fiables à writeContent(), et au moteur d’honorer les invariants du contrat.

AffirmationNormeClausePreuve
Un flux de contenu encode le contenu de la page sous forme d’une séquence d’opérateurs graphiques, que le curseur ajoute.ISO 32000-2§8
Le writer émet à la fermeture une structure de références croisées qui associe chaque numéro d’objet à son décalage en octets.ISO 32000-2§7

Les deux clauses sont référencées dans le glossaire et paraphrasées. NextPDF ne reproduit aucun texte normatif. L’ADR du streaming-writer référencé par la PHPDoc du contrat contient la justification du cycle de vie et du périmètre.

Un moteur de streaming testé est livré dans le Core open source derrière ces contrats experimental. Les classes du moteur sont internes : tu utilises donc le streaming via le contrat public, pas par un nom de classe concret. NextPDF Pro et NextPDF Enterprise suivent le même contrat ; du code écrit contre StreamingWriterInterface dans Core reste donc valide avec une implémentation Premium du même contrat. La réserve porte sur le niveau experimental, pas sur l’édition ni sur la disponibilité. La signature peut changer dans une version mineure, après un avis préalable de dépréciation.