Skip to content

NextPDF Symfony quickstart

Inject PdfFactory, build a Document, and return it through PdfResponse. When you need background generation, dispatch a GeneratePdfMessage to a Messenger transport.

Inject NextPDF\Symfony\Service\PdfFactory. Its create() method returns a fresh NextPDF\Core\Document. It applies your configured defaults for the creator, the author, and the language. Return the document through NextPDF\Symfony\Http\PdfResponse.

src/Controller/InvoiceController.php
<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Http\PdfResponse;
use NextPDF\Symfony\Service\PdfFactory;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class InvoiceController
{
#[Route('/invoice/{number}', name: 'invoice_pdf')]
public function download(PdfFactory $pdf, string $number): Response
{
$doc = $pdf->create();
$doc->addPage();
$doc->cell(0, 10, "Invoice #{$number}", newLine: true);
$doc->cell(0, 10, 'Thank you for your business.');
return PdfResponse::download($doc, "invoice-{$number}.pdf");
}
}

PdfResponse::download() returns a Symfony\Component\HttpFoundation\Response. It sets Content-Type: application/pdf, an attachment disposition, Content-Length, and the bundle’s fixed security headers. Symfony documents the standard Response class and its header model (https://symfony.com/doc/current/components/http_foundation.html).

Use inline() when you want the browser to show the PDF instead of downloading it:

return PdfResponse::inline($doc, 'preview.pdf');

The disposition changes to inline. All other headers stay the same.

Use the streamed variants for large documents. They emit the PDF in 64 KB chunks, which reduces peak memory. They return a Symfony\Component\HttpFoundation\StreamedResponse and omit Content-Length.

return PdfResponse::streamDownload($doc, 'annual-report.pdf');

Use streamInline() for inline streaming. Symfony documents the StreamedResponse callback contract, which is a void callable that flushes output (https://symfony.com/doc/current/components/http_foundation.html).

When symfony/messenger is installed, you can move generation out of the request thread.

Implement NextPDF\Symfony\Message\PdfBuilderInterface. The handler gives you a fresh, pre-configured Document and the serializable context from the message.

src/Pdf/InvoicePdfBuilder.php
<?php
declare(strict_types=1);
namespace App\Pdf;
use NextPDF\Core\Document;
use NextPDF\Symfony\Message\PdfBuilderInterface;
final class InvoicePdfBuilder implements PdfBuilderInterface
{
public function build(Document $document, array $context): Document
{
$document->addPage();
$document->setFont('dejavusans', '', 12);
$document->cell(0, 10, 'Invoice #' . $context['invoice_id']);
return $document;
}
}

4b — Register the builder in the locator

Section titled “4b — Register the builder in the locator”

The handler resolves builders from a PHP Standard Recommendation 11 (PSR-11) service locator keyed by class name. Only registered builders are reachable. Add the builder to a locator in config/services.yaml:

services:
App\Pdf\InvoicePdfBuilder: ~
nextpdf.pdf_builder_locator:
class: Symfony\Component\DependencyInjection\ServiceLocator
arguments:
- 'App\Pdf\InvoicePdfBuilder': '@App\Pdf\InvoicePdfBuilder'
tags: ['container.service_locator']
NextPDF\Symfony\Message\GeneratePdfHandler:
arguments:
$builderLocator: '@nextpdf.pdf_builder_locator'

The handler asks the locator for the builder by its class-string id. In PSR-11, a container identifier is a string that uniquely identifies an entry (PSR-11 §1.1.2).

Inject Symfony\Component\Messenger\MessageBusInterface, then dispatch the message:

src/Controller/ReportController.php
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Pdf\InvoicePdfBuilder;
use NextPDF\Symfony\Message\GeneratePdfMessage;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
final class ReportController
{
#[Route('/invoice/{id}/queue', name: 'invoice_queue')]
public function queue(MessageBusInterface $bus, int $id): Response
{
$bus->dispatch(new GeneratePdfMessage(
builderClass: InvoicePdfBuilder::class,
outputPath: '/var/storage/invoices/' . $id . '.pdf',
builderContext: ['invoice_id' => $id],
));
return new Response('PDF generation queued.', 202);
}
}

GeneratePdfMessage is a readonly data transfer object (DTO). Its constructor rejects output paths that are empty, do not end in .pdf, include path-traversal segments, use stream-wrapper schemes, or contain null bytes. It also requires builderClass to be a syntactically valid class name. The handler validates the output path again at execution time, before it writes. If a path is safe at dispatch but unsafe at consume time, the handler still rejects it. The #[AsMessageHandler] attribute and the MessageBusInterface dispatch contract follow the standard Symfony Messenger model (https://symfony.com/doc/current/messenger.html).

In config/packages/messenger.yaml, route the message to a transport:

framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
NextPDF\Symfony\Message\GeneratePdfMessage: async

Then run a worker:

Terminal window
php bin/console messenger:consume async
Terminal window
php bin/console debug:container --tag=container.service_locator
php bin/console messenger:consume async --limit=1 -vv

The first command confirms that the builder locator is registered. The second command consumes one queued message and prints the handler’s progress.

  • /integrations/symfony/configuration/ — adjust defaults, fonts, and the document service.
  • /integrations/symfony/production-usage/ — review worker safety and streaming under load.
  • /integrations/symfony/troubleshooting/ — resolve common boot and runtime issues.

Each row is a normative claim made on this page and pinned to a full 64-hex reference_id from the gated standards development organization (SDO) corpus. _sidecars/rag-citations.yaml contains provenance, including the corpus manifest and retrieval transport.

SpecClausereference_idClaim
PSR-11psr_11_container#1.1.2.p4Container has()/get() identifier contract
  • /integrations/symfony/overview/ — review the capability summary.
  • /integrations/symfony/install/ — install and register the bundle.
  • /integrations/symfony/integration/ — review the end-to-end wiring reference.