Renderizar PDF de forma segura en un worker de larga duración
De un vistazo
Sección titulada «De un vistazo»Un worker de PHP de larga duración (RoadRunner, Swoole, Laravel Octane) mantiene vivo el proceso durante muchas solicitudes. Volver a analizar las mismas fuentes y volver a decodificar las mismas imágenes en cada solicitud desperdicia CPU y aumenta la memoria residente. Para evitarlo, NextPDF separa dos ciclos de vida:
- Ciclo de vida del proceso, compartido:
FontRegistryeImageRegistrymantienen las tablas de fuentes analizadas y las cachés de imágenes decodificadas. Crearlos una sola vez al arrancar el worker. - Ciclo de vida de la solicitud, desechable: el
Documentque devuelveDocumentFactory::create(). Construirlo, escribirlo y dejar que salga del ámbito. Entonces, el recolector de basura recupera todo el grafo de objetos.
Esta recipe proporciona la secuencia de arranque del worker, la lógica por solicitud y el reinicio por ciclo que mantiene estable el consumo máximo de memoria.
Instalación
Sección titulada «Instalación»composer require nextpdf/core:^3El patrón de worker en sí no requiere ninguna extensión adicional, y un runtime de worker (RoadRunner / Swoole / Octane) es opcional. El mismo patrón de fábrica funciona en un bucle for de CLI simple, que es exactamente lo que ejercita el arnés.
Descripción conceptual
Sección titulada «Descripción conceptual»El punto de entrada recomendado para workers es DocumentFactory. Construirlo una sola vez con un FontRegistry e ImageRegistry compartidos:
FontRegistry::warmup()analiza los archivos de fuentes que especifiques y guarda en caché las tablas analizadas. A continuación,FontRegistry::lock()congela el registro, de modo que ningún código por solicitud pueda mutar el conjunto de fuentes compartido.isLocked()informa del estado actual. Una vez bloqueado, el registro se puede compartir de forma segura entre corrutinas concurrentes.- Crear
ImageRegistrycon un presupuestomaxCacheBytes. Cuando se supera el presupuesto, expulsa las entradas menos usadas recientemente. Una sola imagen mayor que el presupuesto evita la caché por completo en lugar de saturarla. ImageRegistry::reset()expulsa todas las imágenes en caché y mantiene el registro plenamente funcional. La siguiente solicitud vuelve a poblarlo bajo demanda. Llamarlo con una cadencia (cada N solicitudes, o cuandomemoryUsage()cruce un umbral) devuelve el máximo histórico a la línea base.
Cada documento que crea la fábrica es un PDF independiente. ISO 32000-2 §7.5.5 define el tráiler de un archivo nunca actualizado como uno que no tiene entrada Prev, y cada solicitud del worker emite exactamente un archivo de primera generación de ese tipo. De este modo, las solicitudes no comparten estado de documento, aunque sí compartan las cachés de fuentes y de imágenes. La etiqueta BaseFont de la fuente subconjunto (ISO 32000-2 §9.6.4) permanece estable entre solicitudes porque la fuente analizada vive en el registro compartido.
Superficie de la API
Sección titulada «Superficie de la API»La superficie de la API de esta recipe se genera a partir del PHPDoc de NextPDF\Core\DocumentFactory, NextPDF\Typography\FontRegistry, NextPDF\Graphics\ImageRegistry y NextPDF\Support\MemoryReport. Los miembros clave que se usan más adelante son DocumentFactory::create(), FontRegistry::warmup() / lock() / isLocked() / memoryUsage(), ImageRegistry::reset() / memoryUsage(), y MemoryReport::$currentBytes / $peakBytes / $entryCount / utilizationPercent().
Ejemplo de código — Inicio rápido
Sección titulada «Ejemplo de código — Inicio rápido»<?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.Ejemplo de código — Producción
Sección titulada «Ejemplo de código — Producción»El ejemplo completo respeta el canal de salida del arnés. Muestra la secuencia de arranque, un bucle de solicitudes acotado, el reset() por ciclo y una aserción sobre el máximo histórico de memoria. Este es el script que ejecuta dos veces el arnés de reproducibilidad.
<?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 queda reservado para el arnés; el texto de progreso se envía a STDERR. El PDF se escribe únicamente en NEXTPDF_COOKBOOK_OUTPUT; nunca se imprime con echo.
Casos límite y trampas
Sección titulada «Casos límite y trampas»- Bloquear antes de compartir. Llamar a
FontRegistry::lock()durante el arranque. Un registro que sigue siendo mutable cuando dos corrutinas lo tocan es una condición de carrera de datos. UsarisLocked()como aserción en una comprobación de salud. reset()no esunset().ImageRegistry::reset()expulsa los datos binarios en caché, pero mantiene el registro utilizable, así que es la llamada periódica adecuada. Destruir y reconstruir el registro en cada solicitud anula por completo el propósito de la caché compartida.- Omisión de imágenes de gran tamaño. Una imagen mayor que
maxCacheBytesse decodifica cada vez que se usa y nunca se almacena en caché, así que no puede expulsar el conjunto de trabajo. Esto es intencional. Dimensionar el presupuesto para las imágenes habituales, no para la rara imagen grande. - El documento debe salir del ámbito. Mantener el
Documenten una variable estática, un enlace de contenedor de larga duración o un closure capturado por el worker mantiene vivo todo el grafo de objetos y anula la recolección por solicitud. Llamar aunset()o permitir que salga del ámbito es obligatorio. - Colocación de
gc_collect_cycles(). El recolector de ciclos de PHP no conoce los límites entre solicitudes. Llamarlo después de la cadencia de reinicio, no en cada solicitud. Esto mantiene acotado el máximo histórico sin pagar el costo de la recolección en la ruta caliente. - Advertencia sobre el determinismo. Las marcas de tiempo del documento y el
/IDdel tráiler se regeneran en cada guardado (ISO 32000-2 §14.3). Por tanto, el PDF capturado se compara con el perfil semántico (AST estructural más metadatos, nunca bytes volátiles). Consulta «Conformidad».
Rendimiento
Sección titulada «Rendimiento»- El registro compartido convierte el análisis repetido de fuentes y la decodificación repetida de imágenes en un costo de arranque que se paga una sola vez. A partir de ahí, el trabajo por solicitud se reduce al diseño y la serialización.
- El pico de memoria residente está acotado por
maxCacheBytesmás el conjunto de trabajo de un único documento en curso. Elreset()por ciclo devuelve la caché a la línea base, de modo que un worker de larga duración no muestra un diente de sierra con tendencia ascendente. - El front-matter
performance_budget(wall_ms: 4000,peak_mb: 192) acota la ejecución del arnés del bucle de 12 solicitudes. El arnés lo impone; no es una garantía para documentos arbitrarios. - Esta recipe aporta la cobertura de «memoria/GC» de la lista de carencias §4.3 para #31. Existe el
examples/14-worker-factory.phpde respaldo, y el nuevotests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.phpañade la aserción de memoria/GC que faltaba (el pico no crece entre ciclos tras el reinicio).
Notas de seguridad
Sección titulada «Notas de seguridad»- El patrón de worker procesa un documento por solicitud y comparte únicamente cachés de fuentes analizadas y de imágenes decodificadas. Ningún contenido de documento cruza el límite entre solicitudes. Una solicitud no puede leer los datos de documento de otra solicitud a través de los registros compartidos.
- La entrada no confiable sigue fluyendo por los límites de entrada normales de NextPDF, y el patrón de worker no relaja ninguna validación. Tratar la entrada HTML y de activos de cada solicitud como no confiable, exactamente igual que se haría en un proceso por solicitud.
Conformidad
Sección titulada «Conformidad»| Declaración | Especificación | Cláusula | reference_id |
|---|---|---|---|
| La fecha de modificación del documento se regenera en cada guardado; por eso, la salida por solicitud no es estable byte a byte. | ISO 32000-2 | §14.3 | |
Cada documento del worker es un archivo nunca actualizado (sin Prev en el tráiler); las solicitudes no comparten estado de documento. | ISO 32000-2 | §7.5.5 | |
| El prefijo de la etiqueta de la fuente subconjunto se mantiene estable entre solicitudes porque la fuente analizada vive en el registro compartido. | ISO 32000-2 | §9.6.4 |
Como el /ID del tráiler y la fecha de modificación se regeneran en cada guardado, esta recipe se verifica con el perfil de reproducibilidad semántico (igualdad de AST estructural más una comparación solo de metadatos). Una afirmación bit a bit o estructural no sería veraz para la salida del worker.