Skip to content

NextPDF Symfony in production

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.

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 the PdfDocumentInterface / Document aliases) resolves to a fresh instance on every resolution. Under PSR-11 (PHP Standard Recommendation 11), a container may legitimately return a different value on each get() 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() (when preload_fonts is non-empty) the compiler pass calls lock(). 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.reset with method reset, so Symfony clears it between requests under runtimes that honor kernel.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.

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.

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-Length because 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-streamed download() or inline() 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-revalidate as the buffered variants.

Choose streaming for multi-megabyte reports and batch exports. Choose the buffered variants for small, latency-sensitive responses.

Offload generation to Messenger when requests must return quickly, or when rendering is processor-intensive.

  1. Implement PdfBuilderInterface for each document type.
  2. Register builders in a container.service_locator and wire it as GeneratePdfHandler’s $builderLocator.
  3. Route GeneratePdfMessage to a durable transport.
  4. Run workers with bounded lifetimes.

Recycle workers so a leaked allocation in a third-party dependency cannot grow without limit:

Terminal window
php bin/console messenger:consume async \
--limit=200 \
--memory-limit=256M \
--time-limit=3600

The 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.

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.

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.

  • Pin one nextpdf/core major in the application composer.json (the bundle accepts ^3.0 || ^5.2).
  • Make sure ext-mbstring and ext-zlib are enabled in the deployed PHP image (the bundle fails fast at boot otherwise).
  • Pre-populate preload_fonts with the fonts your documents use so the registry warms and locks at boot rather than on first request.
  • Point cache_path at 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:warmup during 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.
  • 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.reset not honored — Under a runtime that does not call kernel.reset, the image cache is bounded by image_cache_mb but not cleared between requests; size the cap accordingly.
  • Holding a document across requests — A captured Document from a prior request will carry stale state. Always resolve per request via PdfFactory.

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.

SpecClausereference_idClaim
PSR-11psr_11_container#1.1.2.p3.bNon-shared service: distinct value per resolution
PSR-3psr_3_logger#x3.p17Optional logger collaborator
  • /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.