PDF's veilig renderen in een langlopende worker
In één oogopslag
Sectie met titel “In één oogopslag”Een langlopende PHP-worker (PHP: Hypertext Preprocessor) (RoadRunner, Swoole, Laravel Octane) houdt één proces actief voor veel verzoeken. Als u voor elk verzoek dezelfde lettertypen parseert en dezelfde afbeeldingen decodeert, verspilt u processortijd en verhoogt u het residente geheugen. NextPDF vermijdt die kosten door twee soorten levensduur te scheiden:
- Proceslevensduur, gedeeld:
FontRegistryenImageRegistrybevatten geparseerde lettertypetabellen en gedecodeerde afbeeldingscaches. Maak de registers eenmalig aan tijdens het opstarten van de worker. - Verzoeklevensduur, wegwerpbaar: het
DocumentdatDocumentFactory::create()retourneert. Bouw het op, schrijf het weg en laat het buiten bereik raken. De PHP-garbagecollector kan dan de volledige objectgraaf vrijgeven.
Dit recipe laat zien hoe u de worker opstart, elk verzoek verwerkt en de reset per cyclus uitvoert die het piekgeheugen stabiel houdt.
Installatie
Sectie met titel “Installatie”composer require nextpdf/core:^3Het workerpatroon vereist geen aanvullende extensie en een worker-runtime (RoadRunner / Swoole / Octane) is optioneel. U kunt hetzelfde factorypatroon uitvoeren in een command-line interface-lus (CLI) met for, zoals de harness ook test.
Conceptueel overzicht
Sectie met titel “Conceptueel overzicht”Gebruik voor workercode DocumentFactory als uitgangspunt. Maak die eenmalig aan met een gedeelde FontRegistry en ImageRegistry:
FontRegistry::warmup()parseert de lettertypebestanden die u aanlevert en zet de geparseerde tabellen in de cache.FontRegistry::lock()bevriest het register zodat code per verzoek de gedeelde lettertypeset niet kan wijzigen.isLocked()rapporteert de huidige status. Zodra u het register hebt vergrendeld, kunt u het veilig delen tussen gelijktijdige coroutines.- Construeer
ImageRegistrymet eenmaxCacheBytes-budget. Wanneer het budget wordt overschreden, verwijdert het register de minst recent gebruikte items. Een afbeelding die groter is dan het budget omzeilt de cache in plaats van die te overbelasten. ImageRegistry::reset()verwijdert elke gecachete afbeelding zonder het register onbruikbaar te maken. Het volgende verzoek vult de cache op aanvraag opnieuw. Roep dit op een vast ritme aan (elke N verzoeken, of wanneermemoryUsage()een drempel overschrijdt) om de geheugenpiek terug te brengen naar de uitgangswaarde.
Elk document dat de factory aanmaakt, is een onafhankelijk Portable Document Format-bestand (PDF). ISO 32000-2 §7.5.5 bepaalt dat de trailer van een nooit-bijgewerkt bestand geen Prev-vermelding heeft, en elk workerverzoek produceert zo’n eerste-generatiebestand. Verzoeken delen daarom geen documentstatus, ook al delen ze de lettertype- en afbeeldingscaches. De BaseFont-tag van het subset-lettertype (ISO 32000-2 §9.6.4) blijft stabiel tussen verzoeken omdat het geparseerde lettertype in het gedeelde register staat.
API-oppervlak
Sectie met titel “API-oppervlak”Dit recipe gebruikt het API-oppervlak dat uit PHPDoc is gegenereerd voor NextPDF\Core\DocumentFactory, NextPDF\Typography\FontRegistry, NextPDF\Graphics\ImageRegistry en NextPDF\Support\MemoryReport. De belangrijkste members zijn DocumentFactory::create(), FontRegistry::warmup() / lock() / isLocked() / memoryUsage(), ImageRegistry::reset() / memoryUsage(), en MemoryReport::$currentBytes / $peakBytes / $entryCount / utilizationPercent().
Codevoorbeeld — Snelstart
Sectie met titel “Codevoorbeeld — Snelstart”<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// --- Worker boot (run ONCE, before the request loop) ---------------------$fonts = new FontRegistry();$fonts->lock(); // freeze the shared font set$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$factory = new DocumentFactory($fonts, $images);
// --- Per request ---------------------------------------------------------$doc = $factory->create();$doc->setTitle('Worker output');$doc->addPage();$doc->setFont('helvetica', 'B', 16);$doc->cell(0, 12, 'Generated in a shared-registry worker', newLine: true);$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf');// $doc leaves scope here → GC reclaims the whole document tree.Codevoorbeeld — Productie
Sectie met titel “Codevoorbeeld — Productie”Het volledige voorbeeld respecteert het uitvoerkanaal van de harness. Het toont de opstartvolgorde, een begrensde verzoeklus, de reset() per cyclus en een assertie op de geheugenpiek. Dit is het script dat de reproduceerbaarheidsharness tweemaal uitvoert.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// --- Worker boot: shared, process-lifetime registries --------------------$fonts = new FontRegistry();$fonts->lock(); // share-safe once locked$images = new ImageRegistry(maxCacheBytes: 50 * 1024 * 1024);$factory = new DocumentFactory($fonts, $images);
$resetEvery = 4; // reset cadence in requests$peakAfterReset = 0;
// --- Simulated request loop ---------------------------------------------for ($request = 1; $request <= 12; $request++) { $doc = $factory->create(); $doc->setTitle("Worker Request #{$request}"); $doc->addPage(); $doc->setFont('helvetica', 'B', 16); $doc->cell(0, 12, "Worker Request #{$request}", newLine: true); $doc->setFont('helvetica', '', 11); $doc->cell(0, 8, 'Shared FontRegistry / ImageRegistry across requests.', newLine: true);
// The harness captures the LAST request's PDF via the side channel. if ($request === 12) { $doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/out.pdf'); } else { $doc->getPdfData(); // force render, then drop }
unset($doc); // explicit end-of-request
// Bound the cache high-water mark on a fixed cadence. if ($request % $resetEvery === 0) { $images->reset(); \gc_collect_cycles(); $report = $images->memoryUsage(); $peakAfterReset = \max($peakAfterReset, $report->currentBytes); }}
$final = $images->memoryUsage();
fwrite(STDERR, \sprintf( "fonts.locked=%s images.entries=%d images.current=%dB peak_after_reset=%dB\n", $fonts->isLocked() ? 'yes' : 'no', $final->entryCount, $final->currentBytes, $peakAfterReset,));STDOUT blijft vrij voor de harness; voortgangstekst gaat naar STDERR. De PDF wordt uitsluitend naar NEXTPDF_COOKBOOK_OUTPUT geschreven en wordt nooit weergegeven.
Randgevallen en valkuilen
Sectie met titel “Randgevallen en valkuilen”- Vergrendel voordat u deelt. Roep
FontRegistry::lock()aan bij het opstarten. Een register dat nog wijzigbaar is wanneer twee coroutines het benaderen, is een datarace. GebruikisLocked()als assertie in een health check. reset()is nietunset().ImageRegistry::reset()verwijdert gecachete binaire gegevens en houdt het register bruikbaar, dus dit is de juiste periodieke aanroep. Als u het register voor elk verzoek vernietigt en opnieuw opbouwt, verliest u het voordeel van de gedeelde cache.- Omzeiling van te grote afbeeldingen. Een afbeelding die groter is dan
maxCacheByteswordt per gebruik gedecodeerd en nooit gecachet, zodat ze de werkset niet kan verdringen. Dit is opzettelijk. Stem het budget af op uw veelvoorkomende afbeeldingen, niet op de zeldzame grote. - Het document moet buiten bereik raken. Als u het
Documentvasthoudt in een static, een langlevende container-binding of een door de worker vastgehouden closure, blijft de volledige objectgraaf in leven en kan opruiming per verzoek niet werken. Eenunset()-aanroep of het verlaten van het bereik is verplicht. - Plaatsing van
gc_collect_cycles(). De cycle collector van PHP weet niets van verzoekgrenzen. Roep deze aan na het resetritme, niet bij elk verzoek. Dit begrenst de piek zonder opruimkosten toe te voegen aan het hot path. - Kanttekening bij determinisme. Documenttijdstempels en de
/IDin de trailer worden bij elke opslag opnieuw gegenereerd (ISO 32000-2 §14.3). De opgeslagen PDF wordt daarom vergeleken met het semantische profiel (structurele abstract syntax tree (AST) plus metadata, nooit vluchtige bytes). Zie ‘Conformiteit’.
Prestaties
Sectie met titel “Prestaties”- Het gedeelde register maakt herhaald lettertypeparseren en afbeeldingsdecoderen tot eenmalige opstartkosten. Het werk per verzoek bestaat dan uit lay-out en serialisatie.
- Het residente piekgeheugen wordt begrensd door
maxCacheBytesplus de werkset van één document dat in behandeling is. Dereset()per cyclus brengt de cache terug naar de uitgangswaarde, zodat een langlevende worker geen stijgend zaagtandpatroon vertoont. - De
performance_budget-frontmatter (wall_ms: 4000,peak_mb: 192) begrenst de harness-uitvoering van de lus van 12 verzoeken. De harness dwingt dit budget af; het is geen garantie voor willekeurige documenten. - Dit recipe levert de ‘memory/GC’-dekking uit de gatlijst in §4.3 voor #31. Het onderliggende
examples/14-worker-factory.phpbestaat, entests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.phpvoegt de ontbrekende memory/GC-assertie toe (de piek groeit na een reset niet over cycli heen).
Beveiligingsopmerkingen
Sectie met titel “Beveiligingsopmerkingen”- Het workerpatroon verwerkt één document per verzoek en deelt alleen caches van geparseerde lettertypen en gedecodeerde afbeeldingen. Documentinhoud overschrijdt de verzoekgrens niet. Een verzoek kan via de gedeelde registers niet de documentgegevens van een ander verzoek lezen.
- Niet-vertrouwde invoer loopt nog steeds door de normale invoergrenzen van NextPDF, en het workerpatroon versoepelt de validatie niet. Behandel de HyperText Markup Language (HTML)-invoer en asset-invoer van elk verzoek als niet-vertrouwd, net zoals u in een proces per verzoek zou doen.
Conformiteit
Sectie met titel “Conformiteit”| Bewering | Specificatie | Artikel | reference_id |
|---|---|---|---|
| De wijzigingsdatum van het document wordt bij elke opslag opnieuw gegenereerd, dus de uitvoer per verzoek is niet byte-stabiel. | ISO 32000-2 | §14.3 | |
Elk workerdocument is een nooit-bijgewerkt bestand (geen Prev in de trailer); verzoeken delen geen documentstatus. | ISO 32000-2 | §7.5.5 | |
| Het tag-prefix van het subset-lettertype is stabiel over verzoeken heen omdat het geparseerde lettertype in het gedeelde register staat. | ISO 32000-2 | §9.6.4 |
Omdat de /ID in de trailer en de wijzigingsdatum bij elke opslag opnieuw worden gegenereerd, wordt dit recipe geverifieerd met het semantische reproduceerbaarheidsprofiel (gelijkheid van de structurele abstract syntax tree (AST) plus een vergelijking van alleen metadata). Een bitsgewijze of structurele bewering zou onnauwkeurig zijn voor worker-uitvoer.