Pro édition
Stream
Le module Stream rend des lots de documents de façon durable et concurrente, avec un commit local exactement-une-fois vers des stores durables mono-hôte (l’exactement-une-fois inter-hôtes est la frontière de Stream Enterprise). Il scinde le travail en deux responsabilités proprement séparées : un moteur de rendu qui transforme des manifestes validés en octets (et rien d’autre), et un ensemble de stores durables — committer, jalon, idempotence, lettre morte — qui publient ces octets en toute sécurité et permettent à une exécution de reprendre après une panne sans re-publier la sortie déjà commitée.
Disponibilité et licence
Section intitulée « Disponibilité et licence »Cette fonctionnalité est livrée dans NextPDF Pro (nextpdf/pro) et s’active avec une enveloppe de licence de palier Pro. Un déploiement sans ce droit ne charge pas les classes de la fonctionnalité. Compare les éditions et obtiens une licence.
Il n’existe pas d’indicateur de licence par fonctionnalité distinct. La concurrence (nombre de workers), la taille de lot, le budget de nouvelles tentatives et le backend de store (en mémoire ou système de fichiers durable) sont des paramètres au runtime, pas des bascules de licence.
Installer
Section intitulée « Installer »composer require nextpdf/pro:^3Le code se trouve sous l’espace de noms NextPDF\Pro\Stream.
Vue d’ensemble conceptuelle
Section intitulée « Vue d’ensemble conceptuelle »Stream s’organise autour d’une jointure figée — NextPDF\Pro\Stream\Engine\RenderEngineInterface — qui sépare le moteur de débit de la sémantique de flux :
- Le moteur de rendu détient la concurrence et la mémoire bornée. Il rend une fenêtre de manifestes pré-validés et pré-dédupliqués via
renderBatch()et renvoie unEngineRenderResultpar manifeste, dans l’ordre d’entrée. Surtout, le moteur est sans effet de bord vis-à-vis de la sortie finale : il renvoie les octets rendus accompagnés de leur empreinte sha-256, sans jamais écrire vers une clé d’objet finale. C’est cette pureté qui rend possible la livraison exactement-une-fois. - Les collaborateurs de flux détiennent la livraison. Le committer, le store de jalons, le store d’idempotence (déduplication) et le store de lettres mortes décident où atterrissent les octets, comment une exécution reprend, quel travail est une rediffusion et ce qu’il advient des défaillances terminales.
Un échec de rendu par manifeste est rapporté comme un résultat Failed (ou Timeout) par élément ; il n’interrompt jamais le lot. L’enveloppe de lot réussit toujours, avec des issues par élément.
Concepts clés
Section intitulée « Concepts clés »Moteurs de rendu et exécuteurs
Section intitulée « Moteurs de rendu et exécuteurs »InProcessRenderEngineest la référence de correction synchrone, mono-processus. Il valide chaque manifeste de façon verrouillée via leRenderManifestValidatorlivré avant de le rendre via leSingleDocumentRendererdu Core, si bien qu’un manifeste invalide devient un échec par élément (code d’erreurSPEC-MANIFEST-INVALID) au lieu d’atteindre le renderer.ConcurrentRenderEnginedistribue un lot vers unRenderUnitExecutorInterfaceet rétablit l’ordre déterministe du lot par index d’unité. La sortie est identique octet pour octet à un rendu séquentiel, quel que soit l’ordre d’achèvement ; un achèvement manquant, dupliqué ou inconnu est un échec dur, jamais un abandon silencieux.- Les exécuteurs sont la jointure de concurrence.
InlineRenderUnitExecutorest la référence déterministe ;ProcessPoolRenderUnitExecutorrépartit un lot sur jusqu’à N sous-processus workerphpqui rendent en parallèle, puis collecte et vérifie l’intégrité de leurs résultats.
Commit durable, sans effet de bord
Section intitulée « Commit durable, sans effet de bord »OutputCommitterInterface::commit() publie les octets rendus vers leur destination finale exactement une fois : de façon atomique (aucun objet partiel n’est jamais observé), idempotente (re-commiter un contenu identique octet pour octet n’effectue aucune écriture et renvoie un CommitReceipt avec idempotentReuse = true — un reçu neuf, pas l’original), sans écrasement silencieux (des octets divergents vers une clé occupée sans overwrite lève un conflit) et vérifiée en intégrité (le committer recalcule l’empreinte avant d’écrire). Le LocalFilesystemCommitter met cela en œuvre pour le système de fichiers local.
Reprise sur jalon
Section intitulée « Reprise sur jalon »Un RunCheckpoint est une barrière durable qui enregistre combien d’éléments une exécution a commités, plus un instantané d’état clavé. À la reprise, le processor avance rapidement au-delà de l’offset commité et restaure l’état clavé, si bien qu’une panne en cours d’exécution reprend sans re-publier la sortie déjà commitée. FilesystemCheckpointStore persiste chaque barrière de façon atomique.
Déduplication par idempotence, nouvelles tentatives et lettres mortes
Section intitulée « Déduplication par idempotence, nouvelles tentatives et lettres mortes »Le store d’idempotence est le chemin rapide qui permet au processor de court-circuiter avant de rendre un manifeste rediffusé ; la comparaison d’empreinte du committer reste la garantie durable exactement-une-fois, si bien qu’un enregistrement de déduplication perdu provoque au pire un re-rendu gaspillé que le committer déduplique. RetryPolicy fournit un backoff exponentiel borné et déterministe pour les défaillances transitoires (timeout) ; un job qui épuise son budget est capturé dans un DeadLetterStoreInterface plutôt que perdu. Chaque store livre une variante en mémoire (portée mono-exécution / test) et une variante système de fichiers durable.
Marqueur de durabilité
Section intitulée « Marqueur de durabilité »Les stores dont l’état survit à un redémarrage de processus implémentent le marqueur DurableCapability. Une exécution résistante aux pannes exige que chaque collaborateur soit durable, de sorte qu’elle échoue rapidement plutôt que de promettre une sémantique exactement-une-fois qu’un store en mémoire ne peut tenir à travers un redémarrage.
Exemple de code — Démarrage rapide
Section intitulée « Exemple de code — Démarrage rapide »Rends un manifeste et commite ses octets exactement une fois. Le moteur renvoie les octets accompagnés d’une empreinte ; le committer les publie.
<?php
declare(strict_types=1);
use NextPDF\Manifest\OutputObjectKey;use NextPDF\Manifest\Render\SingleDocumentRenderer;use NextPDF\Manifest\RenderManifestBuilder;use NextPDF\Manifest\TemplateRef;use NextPDF\Pro\Stream\Commit\LocalFilesystemCommitter;use NextPDF\Pro\Stream\Engine\InProcessRenderEngine;
$outputRoot = __DIR__ . '/out';\is_dir($outputRoot) || \mkdir($outputRoot, 0o775, true);
// The engine renders bytes only — it never writes the final object.$engine = new InProcessRenderEngine(SingleDocumentRenderer::standalone());
$target = OutputObjectKey::file('out', 'invoices/1001.pdf');
$manifest = RenderManifestBuilder::create('invoice-1001') ->withInlineInput('<h1>Invoice 1001</h1><p>Amount due: 42.00</p>') ->withTemplate(TemplateRef::html()) ->withOutputKey($target) ->build();
$result = $engine->renderBatch([$manifest])[0];
// A durable committer publishes the rendered bytes exactly once.$committer = new LocalFilesystemCommitter($outputRoot);
if ($result->isRendered()) { $receipt = $committer->commit($result->jobId, $target, $result->bytes, $result->sha256); echo $receipt->target->toUri(), ' (', $receipt->bytesWritten, " bytes)\n";}Exemple de code — Production
Section intitulée « Exemple de code — Production »Rends un lot, route les timeouts vers la politique de nouvelles tentatives et met en lettre morte les défaillances terminales. Le commit refuse d’écraser des octets divergents, si bien qu’une collision de clé est détectée et capturée plutôt que perdue.
<?php
declare(strict_types=1);
use DateTimeImmutable;use NextPDF\Manifest\OutputObjectKey;use NextPDF\Manifest\Render\SingleDocumentRenderer;use NextPDF\Manifest\RenderManifest;use NextPDF\Manifest\RenderManifestBuilder;use NextPDF\Manifest\TemplateRef;use NextPDF\Pro\Stream\Commit\LocalFilesystemCommitter;use NextPDF\Pro\Stream\Engine\EngineRenderStatus;use NextPDF\Pro\Stream\Engine\InProcessRenderEngine;use NextPDF\Pro\Stream\Exception\OutputCommitConflictException;use NextPDF\Pro\Stream\Retry\DeadLetterRecord;use NextPDF\Pro\Stream\Retry\InMemoryDeadLetterStore;use NextPDF\Pro\Stream\Retry\RetryPolicy;
$outputRoot = __DIR__ . '/out';\is_dir($outputRoot) || \mkdir($outputRoot, 0o775, true);
$engine = new InProcessRenderEngine(SingleDocumentRenderer::standalone(), maxBatchSize: 64);$committer = new LocalFilesystemCommitter($outputRoot);$deadLetter = new InMemoryDeadLetterStore();$retry = RetryPolicy::default(); // 3 attempts, 100ms base, 30s cap.
/** * Build one manifest and remember its output target for the commit stage. * * @return array{RenderManifest, OutputObjectKey} */$makeJob = static function (string $jobId, string $html): array { $target = OutputObjectKey::file('out', 'invoices/' . $jobId . '.pdf'); $manifest = RenderManifestBuilder::create($jobId) ->withInlineInput($html) ->withTemplate(TemplateRef::html()) ->withOutputKey($target) ->build();
return [$manifest, $target];};
/** @var array<non-empty-string, OutputObjectKey> $targets */$targets = [];$manifests = [];foreach (['inv-2001' => '<h1>2001</h1>', 'inv-2002' => '<h1>2002</h1>'] as $id => $html) { [$manifest, $target] = $makeJob($id, $html); $manifests[] = $manifest; $targets[$id] = $target;}
foreach ($engine->renderBatch($manifests) as $result) { // A timeout is transient — the policy decides whether to re-enqueue it. if ($result->status === EngineRenderStatus::Timeout && $retry->shouldRetry(1)) { // Re-enqueue on the caller's work queue after delayMsForAttempt(1) ms. continue; }
if (!$result->isRendered()) { $deadLetter->add(new DeadLetterRecord( jobId: $result->jobId, idempotencyKeyValue: $result->jobId, attempts: $retry->maxAttempts, lastErrorCode: $result->errorCode ?? 'SPEC-RENDER-EXCEPTION', lastErrorMessage: $result->errorMessage ?? '', failedAt: new DateTimeImmutable(), ));
continue; }
try { // overwrite=false: identical bytes are an idempotent no-op; divergent // bytes to an occupied key raise SPEC-COMMIT-409 instead of clobbering. $receipt = $committer->commit( $result->jobId, $targets[$result->jobId], $result->bytes, $result->sha256, ); } catch (OutputCommitConflictException $e) { $deadLetter->add(new DeadLetterRecord( jobId: $result->jobId, idempotencyKeyValue: $result->jobId, attempts: 1, lastErrorCode: $e->specCode(), lastErrorMessage: $e->getMessage(), failedAt: new DateTimeImmutable(), ));
continue; }
echo $receipt->idempotentReuse ? "reused {$receipt->target->toUri()}\n" : "committed {$receipt->target->toUri()}\n";}
if ($deadLetter->count() > 0) { \fwrite(\STDERR, $deadLetter->count() . " job(s) dead-lettered\n");}Quand l’utiliser
Section intitulée « Quand l’utiliser »- Rendu de lots à fort volume où le débit profite d’une exécution concurrente (pool de processus).
- Exécutions de longue durée qui doivent survivre à une panne et reprendre sans double publication de la sortie.
- Pipelines qui doivent garantir la livraison exactement-une-fois de chaque document rendu vers sa cible.
Pour un document ponctuel isolé, rends directement avec le module Writer ; la valeur de Stream réside dans les lots durables, reprenables et concurrents.
Performance
Section intitulée « Performance »Le débit évolue avec le nombre de workers dans ProcessPoolRenderUnitExecutor (borné par maxWorkers et maxBatchSize), tandis que le moteur garde une sortie de rendu identique octet pour octet à la référence séquentielle. Un délai d’expiration en temps d’horloge plafonne chaque lot parallèle, de sorte qu’un worker bloqué ne puisse pas bloquer indéfiniment. Il n’existe pas de chiffre de débit fixe publié ; il dépend de la complexité des documents et du parallélisme de l’hôte. Mesure avec des documents représentatifs.
Notes de sécurité
Section intitulée « Notes de sécurité »Les manifestes sont validés de façon verrouillée avant le rendu. Le committer rejette la traversée de chemin, les octets nuls, les schémas de stream-wrapper, les cibles en lien symbolique et les vecteurs de flux de données alternatifs NTFS (deux-points), et résout chaque clé sous une unique racine configurée. Les résultats de worker inter-processus sont re-hachés et comparés à l’empreinte rapportée par le worker, de sorte qu’un worker corrompu ne puisse pas altérer silencieusement la sortie. Ce module ne journalise aucun contenu de document.
Note sur la frontière Enterprise
Section intitulée « Note sur la frontière Enterprise »Les stores durables de Stream sont ici adossés au système de fichiers et mono-hôte. Le commit exactement-une-fois concurrent inter-hôtes vers la même clé, ainsi que la déduplication durable entre exécutions, relèvent des committers et stores Enterprise sur stockage objet ; le processor de flux de jobs documentaires qui pilote ces collaborateurs est une affaire Enterprise. Pro fournit le moteur, les contrats et les implémentations durables locales.
Repli / alternative dans le Core
Section intitulée « Repli / alternative dans le Core »Sans Pro, rends les documents un par un avec le writer de NextPDF Core ; le streaming de lots durable, l’exécution concurrente et le commit exactement-une-fois sont des ajouts de Pro. Voir /modules/writer/.
Frontière de publication
Section intitulée « Frontière de publication »Cette page documente uniquement le comportement observable de l’extérieur et la surface d’API publique prise en charge. Les chemins d’espaces de noms internes, les classes utilitaires, les tables de mécanismes, les noms de fichiers de runbook et les préfixes de tickets sont hors périmètre.