Bezpieczne renderowanie plików PDF w długo działającym procesie roboczym
W skrócie
Dział zatytułowany „W skrócie”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:
FontRegistryiImageRegistryprzechowują przeanalizowane tabele czcionek i bufory zdekodowanych obrazów. Utwórz te rejestry raz przy uruchamianiu procesu roboczego. - Jednorazowe, o czasie życia żądania: obiekt
Documentzwracany przezDocumentFactory::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.
Instalacja
Dział zatytułowany „Instalacja”composer require nextpdf/core:^3Wzorzec 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.
Omówienie koncepcyjne
Dział zatytułowany „Omówienie koncepcyjne”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
ImageRegistryz budżetemmaxCacheBytes. 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 gdymemoryUsage()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.
Powierzchnia API
Dział zatytułowany „Powierzchnia API”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().
Przykład kodu — szybki start
Dział zatytułowany „Przykład kodu — szybki start”<?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.Przykład kodu — środowisko produkcyjne
Dział zatytułowany „Przykład kodu — środowisko produkcyjne”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.
Przypadki brzegowe i pułapki
Dział zatytułowany „Przypadki brzegowe i pułapki”- 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żyjisLocked()jako asercji w kontroli stanu. reset()to nieunset().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ż
maxCacheBytesjest 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
Documentw 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łanieunset()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
/IDw 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ść”.
Wydajność
Dział zatytułowany „Wydajność”- 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
maxCacheBytespowiększone o zestaw roboczy jednego przetwarzanego dokumentu. Resetreset()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_budgetw 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.phpistnieje, atests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.phpdodaje brakującą asercję pamięci/GC (szczyt nie rośnie między cyklami po resecie).
Uwagi dotyczące bezpieczeństwa
Dział zatytułowany „Uwagi dotyczące bezpieczeństwa”- 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.
Zgodność
Dział zatytułowany „Zgodność”| Stwierdzenie | Specyfikacja | Klauzula | reference_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.