NextPDF Symfony in production
At a glance
Section titled “At a glance”Use the bundle in long-running PHP runtimes. It creates non-shared documents, locks the font registry after warmup, and resets the image cache between requests. Stream large Portable Document Format (PDF) files, and offload heavy jobs to Messenger workers.
Worker-safe service lifecycle
Section titled “Worker-safe service lifecycle”Long-running runtimes keep the container alive across requests, so per-request
state must stay isolated. FrankenPHP, RoadRunner, and Messenger workers follow
this model. The bundle’s services.php defines the lifecycle below, verified
against the service definitions:
- Document — non-shared.
nextpdf.document(and thePdfDocumentInterface/Documentaliases) resolves to a fresh instance on every resolution. Under PSR-11 (PHP Standard Recommendation 11), a container may legitimately return a different value on eachget()for the same id (PSR-11 §1.1.2). Resolve a document per request. Never hold it across requests. - FontRegistry — shared and locked. The registry is a singleton for the
process lifetime. After
warmup()(whenpreload_fontsis non-empty) the compiler pass callslock(). The lock prevents runtime mutation and cross-request font-state pollution. - ImageRegistry — shared, reset per request. The bounded least recently used
(LRU) image cache is shared, but it is tagged
kernel.resetwith methodreset, so Symfony clears it between requests under runtimes that honorkernel.reset. - EInvoice contracts — non-shared. When Premium implementations are present, the embedder, validator, profile, and schematron services are registered as non-shared. Parser context stays scoped to each call and never leaks across requests.
Recommended injection pattern
Section titled “Recommended injection pattern”Inject PdfFactory, a shared, stateless configuration holder, and call
create() per request:
public function __construct(private readonly PdfFactory $pdf) {}
public function action(): Response{ $doc = $this->pdf->create(); // fresh, disposable // ... build ... return PdfResponse::inline($doc, 'document.pdf');}Do not inject Document or nextpdf.document into a shared service that is
held across requests. Resolve it inside the request-scoped method instead.
Streaming large documents
Section titled “Streaming large documents”PdfResponse::streamDownload() and streamInline() return a
StreamedResponse. The callback emits the PDF body in 64 KB chunks and
flushes after each chunk, which bounds the response buffer for large documents.
The behaviors below are verified against PdfResponse:
- The streamed variants intentionally omit
Content-Lengthbecause the response object does not know the body size up front. Download progress bars and some proxies work better with a known length. Use the non-streameddownload()orinline()when the document is small enough to hold in memory and a content length is desirable. - The streamed variants emit the same security headers and the same
Cache-Control: private, max-age=0, must-revalidateas the buffered variants.
Choose streaming for multi-megabyte reports and batch exports. Choose the buffered variants for small, latency-sensitive responses.
Asynchronous generation at scale
Section titled “Asynchronous generation at scale”Offload generation to Messenger when requests must return quickly, or when rendering is processor-intensive.
- Implement
PdfBuilderInterfacefor each document type. - Register builders in a
container.service_locatorand wire it asGeneratePdfHandler’s$builderLocator. - Route
GeneratePdfMessageto a durable transport. - Run workers with bounded lifetimes.
Bounded worker lifetime
Section titled “Bounded worker lifetime”Recycle workers so a leaked allocation in a third-party dependency cannot grow without limit:
php bin/console messenger:consume async \ --limit=200 \ --memory-limit=256M \ --time-limit=3600The bundle’s messenger.timeout and messenger.retries configuration keys
record the intended per-message timeout and retry budget. Enforce the same
behavior through Symfony’s retry strategy and worker flags.
Output path safety in workers
Section titled “Output path safety in workers”GeneratePdfMessage validates the output path at construction.
GeneratePdfHandler re-validates it at execution time, before writing to disk.
This two-stage check matters for async work. A message can sit in a queue
between dispatch and consumption, so the handler does not blindly trust the
queued path. Restrict worker filesystem permissions to the intended output
directory as defense in depth.
Observability
Section titled “Observability”The FontRegistry and ImageRegistry services accept an optional
Psr\Log\LoggerInterface (bound with nullOnInvalid()). When the application
provides a logger, the registries can emit diagnostics through it. The logger is
an optional, swappable collaborator under the PSR-3 logger contract (PSR-3).
For request-level visibility, log around PdfFactory::create() and the
Messenger handler in your application code. Use messenger:consume -vv during
incident triage.
Deployment checklist
Section titled “Deployment checklist”- Pin one
nextpdf/coremajor in the applicationcomposer.json(the bundle accepts^3.0 || ^5.2). - Make sure
ext-mbstringandext-zlibare enabled in the deployed PHP image (the bundle fails fast at boot otherwise). - Pre-populate
preload_fontswith the fonts your documents use so the registry warms and locks at boot rather than on first request. - Point
cache_pathat a writable, persistent location if you rely on cached artifacts across deploys. Otherwise, the%kernel.cache_dir%default is fine. - Run
php bin/console cache:warmupduring deploy so the compiled container (including the optional-extension probes) is built ahead of traffic. - Use a durable Messenger transport (not
sync) for production async work, and recycle workers with--limit/--memory-limit/--time-limit.
Edge cases and gotchas
Section titled “Edge cases and gotchas”- Streamed responses behind a buffering proxy — A proxy that buffers the full body negates the memory benefit. Configure the proxy to stream PDF responses, or use buffered responses there.
kernel.resetnot honored — Under a runtime that does not callkernel.reset, the image cache is bounded byimage_cache_mbbut not cleared between requests; size the cap accordingly.- Holding a document across requests — A captured
Documentfrom a prior request will carry stale state. Always resolve per request viaPdfFactory.
Conformance
Section titled “Conformance”Each row is a normative claim on this page, pinned to a full 64-hex
reference_id from the gated standards development organization (SDO) corpus.
The provenance for the corpus manifest and retrieval transport is in
_sidecars/rag-citations.yaml.
| Spec | Clause | reference_id | Claim |
|---|---|---|---|
| PSR-11 | psr_11_container#1.1.2.p3.b | Non-shared service: distinct value per resolution | |
| PSR-3 | psr_3_logger#x3.p17 | Optional logger collaborator |
See also
Section titled “See also”- /integrations/symfony/configuration/ — service lifecycle and parameters.
- /integrations/symfony/security-and-operations/ — response headers, path validation, key handling.
- /integrations/symfony/troubleshooting/ — boot and runtime diagnostics.
- /integrations/symfony/quickstart/ — the minimal async setup.