Renderize PDFs com segurança em um worker de longa duração
Visão geral
Seção intitulada “Visão geral”Um worker PHP (PHP: Hypertext Preprocessor) de longa duração (RoadRunner, Swoole, Laravel Octane) mantém um único processo ativo por muitas requisições. Se você analisar as mesmas fontes e decodificar as mesmas imagens a cada requisição, desperdiça tempo de processador e aumenta a memória residente. O NextPDF evita esse custo separando dois ciclos de vida:
- Ciclo de vida do processo, compartilhado:
FontRegistryeImageRegistrymantêm tabelas de fontes analisadas e caches de imagens decodificadas. Crie os registros uma única vez quando o worker for inicializado. - Ciclo de vida da requisição, descartável: o
Documentretornado porDocumentFactory::create(). Construa-o, salve-o e deixe-o sair de escopo. O coletor de lixo do PHP pode então recuperar todo o grafo de objetos.
Esta receita mostra a sequência de inicialização do worker, o corpo executado a cada requisição e o reset por ciclo que mantém o pico de memória estável.
Instalação
Seção intitulada “Instalação”composer require nextpdf/core:^3O padrão de worker não requer nenhuma outra extensão, e um runtime de worker (RoadRunner / Swoole / Octane) é opcional. Você pode executar o mesmo padrão de fábrica em um loop for pela interface de linha de comando (CLI), que é o que o harness testa.
Visão conceitual
Seção intitulada “Visão conceitual”Em código de worker, comece com DocumentFactory. Construa-o uma única vez com um FontRegistry e um ImageRegistry compartilhados:
FontRegistry::warmup()analisa os arquivos de fonte que você fornece e armazena em cache as tabelas analisadas.FontRegistry::lock()congela o registro para que o código executado por requisição não possa modificar o conjunto de fontes compartilhado.isLocked()informa o estado atual. Depois de bloquear o registro, é seguro compartilhá-lo entre corrotinas concorrentes.- Construa o
ImageRegistrycom um orçamento demaxCacheBytes. Quando o orçamento é excedido, ele descarta as entradas usadas menos recentemente. Uma imagem maior que o orçamento ignora o cache em vez de sobrecarregá-lo. ImageRegistry::reset()descarta todas as imagens em cache enquanto mantém o registro pronto para uso. A próxima requisição o repopula sob demanda. Chame-o em uma cadência (a cada N requisições, ou quandomemoryUsage()cruza um limite) para trazer o pico máximo de volta à linha de base.
Cada documento criado pela fábrica é um arquivo Portable Document Format (PDF) independente. A ISO 32000-2 §7.5.5 define o trailer de um arquivo nunca atualizado como um trailer sem entrada Prev, e cada requisição do worker emite esse tipo de arquivo de primeira geração. Portanto, as requisições não compartilham estado de documento, mesmo que compartilhem os caches de fontes e imagens. A tag BaseFont de fonte de subconjunto (ISO 32000-2 §9.6.4) permanece estável entre requisições porque a fonte analisada reside no registro compartilhado.
Superfície da API
Seção intitulada “Superfície da API”Esta receita usa a superfície da API gerada a partir do PHPDoc em NextPDF\Core\DocumentFactory, NextPDF\Typography\FontRegistry, NextPDF\Graphics\ImageRegistry e NextPDF\Support\MemoryReport. Os membros principais são DocumentFactory::create(), FontRegistry::warmup() / lock() / isLocked() / memoryUsage(), ImageRegistry::reset() / memoryUsage() e MemoryReport::$currentBytes / $peakBytes / $entryCount / utilizationPercent().
Exemplo de código — Início rápido
Seção intitulada “Exemplo de código — Início 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.Exemplo de código — Produção
Seção intitulada “Exemplo de código — Produção”O exemplo completo respeita o canal de saída do harness. Ele mostra a sequência de inicialização, um loop limitado de requisições, o reset() por ciclo e uma asserção de pico máximo de memória. Este é o script que o harness de reprodutibilidade executa duas vezes.
<?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 permanece livre para o harness; o texto de progresso vai para STDERR. O PDF é gravado somente em NEXTPDF_COOKBOOK_OUTPUT; ele nunca é exibido na saída.
Casos extremos e armadilhas
Seção intitulada “Casos extremos e armadilhas”- Bloqueie antes de compartilhar. Chame
FontRegistry::lock()na inicialização. Um registro que ainda é mutável quando duas corrotinas o acessam é uma condição de corrida de dados. UseisLocked()como a asserção em uma verificação de integridade. reset()não éunset().ImageRegistry::reset()descarta os dados binários em cache e mantém o registro utilizável, por isso é a chamada periódica correta. Se você destruir e reconstruir o registro a cada requisição, perde o benefício do cache compartilhado.- Desvio de imagem superdimensionada. Uma imagem maior que
maxCacheBytesé decodificada a cada uso e nunca armazenada em cache, de modo que não pode expulsar o conjunto de trabalho. Isso é intencional. Dimensione o orçamento para as imagens comuns, não para a rara imagem grande. - O documento deve sair de escopo. Se você mantiver o
Documentem uma propriedade estática, em um binding de container de longa duração ou em uma closure capturada pelo worker, todo o grafo de objetos permanece vivo e a coleta por requisição não funciona. Uma chamada aunset()ou a saída de escopo é obrigatória. - Posicionamento de
gc_collect_cycles(). O coletor de ciclos do PHP não conhece os limites de requisição. Chame-o após a cadência de reset, não a cada requisição. Isso limita o pico máximo sem adicionar custo de coleta ao caminho crítico. - Ressalva sobre determinismo. Os carimbos de data/hora do documento e o
/IDdo trailer são regenerados a cada gravação (ISO 32000-2 §14.3). Portanto, o PDF capturado é comparado com o perfil semântico (árvore de sintaxe abstrata (AST) estrutural mais metadados, nunca bytes voláteis). Consulte “Conformidade”.
Desempenho
Seção intitulada “Desempenho”- O registro compartilhado transforma a análise repetida de fontes e a decodificação de imagens em um custo único de inicialização. Depois disso, o trabalho por requisição passa a ser layout e serialização.
- O pico de memória residente é limitado por
maxCacheBytesmais o conjunto de trabalho de um documento em andamento. Oreset()por ciclo retorna o cache à linha de base, de modo que um worker de longa duração não apresente um padrão dente de serra com tendência de crescimento. - O front-matter
performance_budget(wall_ms: 4000,peak_mb: 192) limita a execução do harness no loop de 12 requisições. O harness impõe esse orçamento; ele não é uma garantia para documentos arbitrários. - Esta receita fornece a cobertura “memory/GC” da lista de lacunas da §4.3 para a #31. O
examples/14-worker-factory.phpde apoio existe, etests/Cookbook/Php/WorkerSafeBatchRenderingRecipeTest.phpadiciona a asserção de memory/GC que faltava (o pico não cresce entre ciclos depois do reset).
Notas de segurança
Seção intitulada “Notas de segurança”- O padrão de worker processa um documento por requisição e compartilha apenas os caches de fontes analisadas e imagens decodificadas. O conteúdo do documento não cruza o limite da requisição. Uma requisição não pode ler os dados de documento de outra requisição por meio dos registros compartilhados.
- A entrada não confiável continua passando pelos limites de entrada normais do NextPDF, e o padrão de worker não flexibiliza a validação. Trate a entrada de HyperText Markup Language (HTML) e os assets de cada requisição como não confiáveis, exatamente como faria em um processo por requisição.
Conformidade
Seção intitulada “Conformidade”| Declaração | Especificação | Cláusula | reference_id |
|---|---|---|---|
| A data de modificação do documento é regenerada a cada gravação, de modo que a saída por requisição não é estável byte a byte. | ISO 32000-2 | §14.3 | |
Cada documento do worker é um arquivo nunca atualizado (sem Prev no trailer); as requisições não compartilham estado de documento. | ISO 32000-2 | §7.5.5 | |
| O prefixo da tag de fonte de subconjunto é estável entre requisições porque a fonte analisada reside no registro compartilhado. | ISO 32000-2 | §9.6.4 |
Como o /ID do trailer e a data de modificação são regenerados a cada gravação, esta receita é verificada com o perfil de reprodutibilidade semântico (igualdade de árvore de sintaxe abstrata (AST) estrutural mais uma comparação apenas de metadados). Uma afirmação byte a byte ou estrutural seria imprecisa para a saída do worker.