Przejdź do głównej zawartości

Bezpieczne renderowanie plików PDF w długo działającym procesie roboczym

Długo działający proces roboczy PHP (PHP: Hypertext Preprocessor), taki jak RoadRunner, Swoole lub Laravel Octane, utrzymuje ten sam proces przy życiu przez wiele żądań. Jeśli przy każdym żądaniu analizujesz te same czcionki i dekodujesz te same obrazy, niepotrzebnie zużywasz czas procesora i zwiększasz zużycie pamięci rezydentnej. NextPDF eliminuje ten koszt, rozdzielając dwa czasy życia:

  • Współdzielone, o czasie życia procesu: FontRegistry i ImageRegistry przechowują przeanalizowane tabele czcionek i bufory zdekodowanych obrazów. Utwórz te rejestry raz przy uruchamianiu procesu roboczego.
  • Jednorazowe, o czasie życia żądania: obiekt Document zwracany przez DocumentFactory::create(). Zbuduj go, zapisz i pozwól mu opuścić zasięg. Mechanizm odśmiecania pamięci PHP może wtedy odzyskać cały graf obiektów.

Ten przepis pokazuje sekwencję uruchamiania procesu roboczego, logikę obsługi pojedynczego żądania oraz reset wykonywany w każdym cyklu, dzięki któremu szczytowe zużycie pamięci pozostaje stałe.

Okno terminala
composer require nextpdf/core:^3

Wzorzec procesu roboczego nie wymaga żadnego dodatkowego rozszerzenia; środowisko uruchomieniowe procesu roboczego (RoadRunner / Swoole / Octane) jest opcjonalne. Ten sam wzorzec fabryki możesz uruchomić w pętli for w interfejsie wiersza poleceń (CLI), co dokładnie sprawdza uprząż testowa.

W kodzie procesu roboczego zacznij od DocumentFactory. Utwórz ją raz ze współdzielonymi FontRegistry i ImageRegistry:

  • FontRegistry::warmup() analizuje dostarczone pliki czcionek i buforuje przeanalizowane tabele. FontRegistry::lock() zamraża rejestr, aby kod obsługujący pojedyncze żądanie nie mógł zmienić współdzielonego zestawu czcionek. isLocked() zwraca bieżący stan. Po zablokowaniu rejestru można go bezpiecznie współdzielić między współbieżnymi koprocedurami.
  • Skonstruuj ImageRegistry z budżetem maxCacheBytes. Po przekroczeniu budżetu usuwa najdawniej używane wpisy. Obraz większy niż budżet omija bufor, zamiast powodować nadmierne wypieranie wpisów.
  • ImageRegistry::reset() usuwa wszystkie buforowane obrazy, a rejestr pozostaje gotowy do użycia. Następne żądanie wypełnia go ponownie na żądanie. Wywołuj go w ustalonym rytmie (co N żądań lub gdy memoryUsage() przekroczy próg), aby sprowadzić maksymalne wykorzystanie z powrotem do wartości bazowej.

Każdy dokument tworzony przez fabrykę jest niezależnym plikiem Portable Document Format (PDF). ISO 32000-2 §7.5.5 określa zwiastun pliku, który nigdy nie był aktualizowany, jako niezawierający wpisu Prev, a każde żądanie procesu roboczego emituje taki plik — pierwszej generacji. Żądania nie współdzielą więc stanu dokumentu, mimo że współdzielą bufory czcionek i obrazów. Znacznik BaseFont czcionki podzbiorowej (ISO 32000-2 §9.6.4) pozostaje stabilny między żądaniami, ponieważ przeanalizowana czcionka znajduje się we współdzielonym rejestrze.

Ten przepis korzysta z powierzchni API wygenerowanej z PHPDoc dla NextPDF\Core\DocumentFactory, NextPDF\Typography\FontRegistry, NextPDF\Graphics\ImageRegistry oraz NextPDF\Support\MemoryReport. Kluczowe elementy to DocumentFactory::create(), FontRegistry::warmup() / lock() / isLocked() / memoryUsage(), ImageRegistry::reset() / memoryUsage() oraz 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.

Kompletny przykład uwzględnia kanał wyjściowy uprzęży testowej. Pokazuje sekwencję uruchamiania, ograniczoną pętlę żądań, reset reset() wykonywany w każdym cyklu oraz asercję szczytowego zużycia pamięci. To skrypt, który uprząż weryfikująca powtarzalność uruchamia dwukrotnie.

<?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 pozostaje dostępny dla uprzęży testowej; tekst informujący o postępie trafia do STDERR. Plik PDF jest zapisywany wyłącznie do NEXTPDF_COOKBOOK_OUTPUT; nigdy nie jest wypisywany na wyjście.

  • Zablokuj, zanim zaczniesz współdzielić. Wywołaj FontRegistry::lock() przy uruchamianiu. Rejestr, który nadal można modyfikować, gdy korzystają z niego dwie koprocedury, jest źródłem wyścigu danych. Użyj isLocked() jako asercji w kontroli stanu.
  • reset() to nie unset(). ImageRegistry::reset() usuwa buforowane dane binarne i pozostawia rejestr gotowy do użycia, więc jest właściwym wywołaniem okresowym. Jeśli niszczysz i odbudowujesz rejestr przy każdym żądaniu, tracisz korzyść ze współdzielonego bufora.
  • Omijanie zbyt dużych obrazów. Obraz większy niż maxCacheBytes jest dekodowany przy każdym użyciu i nigdy nie jest buforowany, więc nie może wyprzeć zestawu roboczego. Jest to zamierzone. Dobierz budżet pod kątem typowych obrazów, a nie rzadkiego dużego obrazu.
  • Dokument musi opuścić zasięg. Jeśli przechowujesz Document w polu statycznym, w długo żyjącym powiązaniu kontenera lub w domknięciu przechwyconym przez proces roboczy, cały graf obiektów pozostaje przy życiu i odzyskiwanie pamięci dla pojedynczego żądania nie może zadziałać. Wywołanie unset() lub wyjście z zasięgu jest obowiązkowe.
  • Umiejscowienie gc_collect_cycles(). Kolektor cykli PHP nie wie o granicach żądań. Wywołuj go zgodnie z wyznaczonym rytmem resetowania, a nie przy każdym żądaniu. Ogranicza to maksymalny poziom wykorzystania bez dodawania kosztu odśmiecania do ścieżki krytycznej.
  • Zastrzeżenie dotyczące determinizmu. Sygnatury czasowe dokumentu oraz wpis /ID w zwiastunie są generowane ponownie przy każdym zapisie (ISO 32000-2 §14.3). Przechwycony plik PDF jest zatem porównywany w profilu semantycznym (strukturalne drzewo składni abstrakcyjnej (AST) wraz z metadanymi, a nigdy z lotnymi bajtami). Zobacz „Zgodność”.
  • Współdzielony rejestr zamienia powtarzane analizowanie czcionek i dekodowanie obrazów w jednorazowy koszt uruchomienia. Praca przypadająca na pojedyncze żądanie sprowadza się wtedy do układu i serializacji.
  • Szczytowe zużycie pamięci rezydentnej jest ograniczone przez maxCacheBytes powiększone o zestaw roboczy jednego przetwarzanego dokumentu. Reset reset() wykonywany w każdym cyklu przywraca bufor do wartości bazowej, więc długo działający proces roboczy nie tworzy narastającego przebiegu piłokształtnego.
  • Wpis performance_budget w metadanych frontmatter (wall_ms: 4000, peak_mb: 192) wyznacza limit dla uruchomienia pętli 12 żądań w uprzęży testowej. Uprząż egzekwuje ten budżet; nie jest to gwarancja dla dowolnych dokumentów.
  • Ten przepis zapewnia pokrycie „pamięć/GC” z listy luk §4.3 dla #31. Plik bazowy examples/14-worker-factory.php istnieje, a tests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.php dodaje brakującą asercję pamięci/GC (szczyt nie rośnie między cyklami po resecie).
  • Wzorzec procesu roboczego przetwarza jeden dokument na żądanie i współdzieli tylko bufory przeanalizowanych czcionek i zdekodowanych obrazów. Treść dokumentu nie wychodzi poza granicę żądania. Żądanie nie może odczytać danych dokumentu innego żądania przez współdzielone rejestry.
  • Niezaufane dane wejściowe nadal przechodzą przez standardowe granice wejściowe NextPDF, a wzorzec procesu roboczego nie rozluźnia walidacji. Traktuj dane wejściowe HyperText Markup Language (HTML) oraz zasoby każdego żądania jako niezaufane, dokładnie tak jak w procesie obsługującym jedno żądanie.
StwierdzenieSpecyfikacjaKlauzulareference_id
Data modyfikacji dokumentu jest generowana ponownie przy każdym zapisie, więc wynik pojedynczego żądania nie jest stabilny bajtowo.ISO 32000-2§14.3
Każdy dokument procesu roboczego to plik, który nigdy nie był aktualizowany (brak Prev w zwiastunie); żądania nie współdzielą stanu dokumentu.ISO 32000-2§7.5.5
Prefiks znacznika czcionki podzbiorowej jest stabilny między żądaniami, ponieważ przeanalizowana czcionka znajduje się we współdzielonym rejestrze.ISO 32000-2§9.6.4

Ponieważ wpis /ID w zwiastunie oraz data modyfikacji są generowane ponownie przy każdym zapisie, ten przepis jest weryfikowany w profilu powtarzalności semantycznym (równość strukturalnego drzewa składni abstrakcyjnej (AST) wraz z porównaniem wyłącznie metadanych). Twierdzenie o powtarzalności bitowej lub strukturalnej byłoby niedokładne dla wyniku procesu roboczego.