Ga naar inhoud

PDF's veilig renderen in een langlopende worker

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: FontRegistry en ImageRegistry bevatten geparseerde lettertypetabellen en gedecodeerde afbeeldingscaches. Maak de registers eenmalig aan tijdens het opstarten van de worker.
  • Verzoeklevensduur, wegwerpbaar: het Document dat DocumentFactory::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.

Terminal window
composer require nextpdf/core:^3

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

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 ImageRegistry met een maxCacheBytes-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 wanneer memoryUsage() 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.

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().

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

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.

  • 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. Gebruik isLocked() als assertie in een health check.
  • reset() is niet unset(). 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 maxCacheBytes wordt 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 Document vasthoudt 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. Een unset()-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 /ID in 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’.
  • 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 maxCacheBytes plus de werkset van één document dat in behandeling is. De reset() 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.php bestaat, en tests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.php voegt de ontbrekende memory/GC-assertie toe (de piek groeit na een reset niet over cycli heen).
  • 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.
BeweringSpecificatieArtikelreference_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.