Get started
PDF generation in Symfony: every working option in 2026

PDF generation in Symfony: every working option in 2026

Generate PDFs in Symfony: KnpSnappyBundle with wkhtmltopdf, Dompdf and mPDF for HTML to PDF, Gotenberg for Chromium, plus a hosted PDF4.dev API.

13 min read

Generating a PDF in Symfony comes down to two steps: render a Twig template to an HTML string, then convert that HTML to PDF with an engine. The best engine in 2026 depends on your CSS: for modern layouts (flexbox, grid, web fonts) use a Chromium renderer like Gotenberg (self-hosted) or the PDF4.dev API (hosted). For simple invoices with basic CSS, mPDF runs in pure PHP with no external binary. KnpSnappyBundle with wkhtmltopdf still works but the underlying tool is unmaintained.

This guide covers every option that works today, with real controller code for each, and a decision table so you can pick fast.

Which Symfony PDF option should you pick?

The right choice depends on three things: how much CSS your documents use, whether you can run extra infrastructure, and how much maintenance you want to own. The table below compares the five working options against those criteria.

OptionCSS supportJS supportInfra weightMaintainedBest for
mPDFModerate (no flexbox/grid)NonePure PHP, noneYesInvoices, reports, basic CSS
DompdfLimited (table layouts)NonePure PHP, noneYesSimple one-page documents
KnpSnappyBundle (wkhtmltopdf)Old WebKit subsetLimited, deprecatedBinary installNo (archived 2023)Legacy projects only
GotenbergFull (Chromium)FullDocker serviceYesSelf-hosted full fidelity
PDF4.dev APIFull (Chromium)FullNone (HTTPS call)YesFull fidelity, no infrastructure

Every option starts the same way in Symfony: render a Twig template to a string. The engines differ only in what they do with that HTML string. So the controller pattern below is reusable across all five.

How do you render a Twig template to an HTML string in Symfony?

Use renderView() instead of render(). A standard Symfony controller returns a Response from render(), but for PDF generation you need the raw HTML as a string so you can feed it to a PDF engine. AbstractController::renderView() compiles the Twig template with your data and returns that string.

<?php
// src/Controller/InvoiceController.php
namespace App\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
 
class InvoiceController extends AbstractController
{
    #[Route('/invoice/{id}/pdf', name: 'invoice_pdf')]
    public function pdf(int $id): Response
    {
        // renderView returns compiled HTML as a string (not a Response)
        $html = $this->renderView('invoice/pdf.html.twig', [
            'invoice_number' => 'INV-001',
            'company' => 'Acme Corp',
            'total' => '1,500.00',
        ]);
 
        // $html now holds the full document, ready for any PDF engine below
        return new Response($html); // replaced per engine in the next sections
    }
}

Keep the CSS inline or in a <style> block inside the template. External stylesheet links resolve differently per engine (some need absolute file paths, some need a base URL), so inlining removes a class of bugs.

How do you use mPDF in Symfony?

mPDF is a pure-PHP library that converts HTML to PDF with no external binary, and it supports more CSS than Dompdf (including some @page rules, named page sizes, and CSS for headers/footers). Install it with composer require mpdf/mpdf, then pass your rendered Twig string to WriteHTML().

<?php
// src/Controller/InvoiceController.php
use Mpdf\Mpdf;
use Symfony\Component\HttpFoundation\Response;
 
#[Route('/invoice/{id}/pdf', name: 'invoice_pdf')]
public function pdf(int $id): Response
{
    $html = $this->renderView('invoice/pdf.html.twig', [
        'invoice_number' => 'INV-001',
        'company' => 'Acme Corp',
        'total' => '1,500.00',
    ]);
 
    $mpdf = new Mpdf([
        'format' => 'A4',
        'margin_top' => 20,
        'margin_bottom' => 20,
        'tempDir' => sys_get_temp_dir(), // writable temp dir is required
    ]);
    $mpdf->WriteHTML($html);
 
    // 'S' returns the PDF as a string instead of streaming it
    $pdf = $mpdf->Output('', \Mpdf\Output\Destination::STRING_RETURN);
 
    return new Response($pdf, 200, [
        'Content-Type' => 'application/pdf',
        'Content-Disposition' => 'inline; filename="invoice.pdf"',
    ]);
}

When to use it: invoices, quotes, receipts, and reports where the layout is tables and simple blocks. mPDF handles right-to-left scripts, basic Unicode fonts, and repeating table headers well.

Caveats: no flexbox, no CSS grid, no JavaScript. mPDF needs a writable tempDir and uses noticeably more memory on large documents (a 50-page report can push past the default PHP memory_limit, so raise it for batch jobs). Custom fonts require registering them in the mPDF font config.

How do you use Dompdf in Symfony?

Dompdf is the lightest pure-PHP option: install composer require dompdf/dompdf, load the HTML, and stream the output. It targets HTML 4 and CSS 2.1 with a few CSS 3 additions, so it works for one-page documents with table-based layouts but breaks on anything that needs flexbox or grid.

<?php
// src/Controller/ReportController.php
use Dompdf\Dompdf;
use Dompdf\Options;
use Symfony\Component\HttpFoundation\Response;
 
#[Route('/report/{id}/pdf', name: 'report_pdf')]
public function pdf(int $id): Response
{
    $html = $this->renderView('report/pdf.html.twig', [
        'title' => 'Monthly report',
    ]);
 
    $options = new Options();
    $options->set('defaultFont', 'DejaVu Sans'); // ships with Unicode glyphs
    $options->set('isRemoteEnabled', true);       // allow remote images if needed
 
    $dompdf = new Dompdf($options);
    $dompdf->loadHtml($html);
    $dompdf->setPaper('A4', 'portrait');
    $dompdf->render();
 
    return new Response($dompdf->output(), 200, [
        'Content-Type' => 'application/pdf',
        'Content-Disposition' => 'attachment; filename="report.pdf"',
    ]);
}

When to use it: the simplest documents, where you control the markup and can write table-based layouts. Dompdf has zero external dependencies, so it deploys anywhere PHP runs.

Caveats: the CSS subset is the narrowest of all options here. Emoji and non-Latin scripts need an explicit Unicode font (DejaVu Sans is bundled). Remote images and stylesheets are disabled by default for security, so you opt in with isRemoteEnabled. For complex layouts, expect to fight the renderer.

Is KnpSnappyBundle still a good choice?

KnpSnappyBundle still installs and integrates cleanly with Symfony, but it wraps wkhtmltopdf, which its maintainers archived in 2023 and no longer patch. The bundle gives you a Knp\Snappy\Pdf service and a PdfResponse, but the rendering engine underneath is an old WebKit fork that lacks modern CSS and stopped receiving security fixes.

<?php
// src/Controller/LegacyPdfController.php
use Knp\Snappy\Pdf;
use Symfony\Component\HttpFoundation\Response;
 
#[Route('/legacy/{id}/pdf', name: 'legacy_pdf')]
public function pdf(int $id, Pdf $knpSnappyPdf): Response
{
    $html = $this->renderView('legacy/pdf.html.twig', []);
 
    // getOutputFromHtml runs the wkhtmltopdf binary and returns bytes
    $pdf = $knpSnappyPdf->getOutputFromHtml($html);
 
    return new Response($pdf, 200, [
        'Content-Type' => 'application/pdf',
        'Content-Disposition' => 'inline; filename="document.pdf"',
    ]);
}

When to use it: an existing project already built on it, where rewriting the templates is not worth the effort yet. The Symfony integration auto-wires the Pdf service, so the developer experience is good.

wkhtmltopdf was archived by its maintainers in 2023 and gets no security or rendering updates. It also requires installing a system binary, which complicates Docker images and serverless deploys. For new Symfony projects, prefer mPDF, Gotenberg, or a hosted Chromium API instead.

How do you use Gotenberg with Symfony for full Chromium fidelity?

Gotenberg is a Docker service that wraps headless Chromium (and LibreOffice) behind an HTTP API, so your PDF renders with the exact engine Chrome uses: flexbox, grid, web fonts, and JavaScript all work. From Symfony you render the Twig template, post the HTML to the Gotenberg container with HttpClient, and stream the bytes back.

The cleanest path is the official gotenberg/gotenberg-php client (composer require gotenberg/gotenberg-php), but you can also call the endpoint directly with Symfony HttpClient.

<?php
// src/Controller/ContractController.php
use Gotenberg\Gotenberg;
use Gotenberg\Stream;
use Symfony\Component\HttpFoundation\Response;
 
#[Route('/contract/{id}/pdf', name: 'contract_pdf')]
public function pdf(int $id): Response
{
    $html = $this->renderView('contract/pdf.html.twig', [
        'party' => 'Acme Corp',
    ]);
 
    // GOTENBERG_URL points at the running container, e.g. http://gotenberg:3000
    $request = Gotenberg::chromium($_ENV['GOTENBERG_URL'])
        ->pdf()
        ->paperSize(8.27, 11.7) // A4 in inches
        ->margins(0.4, 0.4, 0.4, 0.4)
        ->html(Stream::string('index.html', $html));
 
    $response = Gotenberg::send($request);
 
    return new Response($response->getBody()->getContents(), 200, [
        'Content-Type' => 'application/pdf',
        'Content-Disposition' => 'attachment; filename="contract.pdf"',
    ]);
}

When to use it: you need full CSS and JS fidelity, you can run Docker, and you want the renderer inside your own infrastructure (data never leaves your network). Gotenberg is open source under the MIT license. See the Gotenberg documentation for the full route list.

Caveats: you own the container. That means CPU and memory budgeting (Chromium is heavy under concurrency), health checks, version upgrades, and scaling the service separately from your Symfony app. For a few PDFs a day this is overkill; for steady volume it pays off.

How do you generate PDFs in Symfony with the PDF4.dev API (no infrastructure)?

PDF4.dev is a hosted HTML-to-PDF API: you POST your rendered Twig HTML (or a stored template id plus data) and get a PDF back, rendered by headless Chromium on the server side. There is no binary to install, no Docker container to run, and no Chromium cold-start to manage. From Symfony you call it with the same HttpClient you already use.

The render endpoint is POST https://pdf4.dev/api/v1/render with an Authorization: Bearer p4_live_xxx header. A minimal body is { "html": "<h1>Hello</h1>", "data": {}, "delivery": "url" }. Because the engine is real Chromium, flexbox, grid, web fonts, and JavaScript render exactly as they do in Chrome.

<?php
// src/Controller/InvoiceApiController.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\Routing\Attribute\Route;
 
class InvoiceApiController extends AbstractController
{
    public function __construct(private HttpClientInterface $client) {}
 
    #[Route('/invoice/{id}/pdf', name: 'invoice_api_pdf')]
    public function pdf(int $id): Response
    {
        // Render the Twig template to HTML, same as every option above
        $html = $this->renderView('invoice/pdf.html.twig', [
            'invoice_number' => 'INV-001',
            'company' => 'Acme Corp',
            'total' => '1,500.00',
        ]);
 
        $resp = $this->client->request('POST', 'https://pdf4.dev/api/v1/render', [
            'auth_bearer' => $_ENV['PDF4_API_KEY'], // p4_live_xxx
            'json' => [
                'html' => $html,
                'data' => [],
                'format' => ['preset' => 'a4'],
                'delivery' => 'base64',
            ],
        ]);
 
        $pdf = base64_decode($resp->toArray()['pdf_base64']);
 
        return new Response($pdf, 200, [
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => 'attachment; filename="invoice.pdf"',
        ]);
    }
}

When to use it: you want full Chromium fidelity without running Chromium yourself. Store a template once and render it with template_id plus data, so your Twig stays in one place. Use delivery: "url" for large files (the response is a signed URL valid 24 hours) so you never hold a multi-megabyte payload in PHP memory.

Caveats: it is a network call, so add a timeout and a retry on the HttpClient request, and handle the failure path. Rendering happens off your servers, which is the point if you do not want to run infrastructure, but means your documents transit an external API over HTTPS. You can prototype the same HTML conversion with the free HTML to PDF toolTry it free, or convert a live page with the Webpage To PdfTry it free tool, before wiring the API into your controller.

Whichever engine you pick, send the rendered PDF back with Content-Type: application/pdf. Use Content-Disposition: attachment; filename="invoice.pdf" to force a download, or inline to open it in the browser tab. For large files, return a StreamedResponse so PHP does not buffer the whole document in memory.

How do you stream a large PDF from a Symfony controller?

Return a StreamedResponse instead of a plain Response when the PDF is large or generated on the fly, so the bytes flush to the client as they are produced and PHP never buffers the full document in memory. Set the same application/pdf and Content-Disposition headers on the streamed response.

<?php
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
 
#[Route('/report/{id}/download', name: 'report_download')]
public function download(int $id): StreamedResponse
{
    $pdfBytes = $this->generatePdf($id); // any engine from above
 
    $response = new StreamedResponse(function () use ($pdfBytes) {
        echo $pdfBytes;
    });
 
    $response->headers->set('Content-Type', 'application/pdf');
    $disposition = $response->headers->makeDisposition(
        ResponseHeaderBag::DISPOSITION_ATTACHMENT,
        'report.pdf'
    );
    $response->headers->set('Content-Disposition', $disposition);
 
    return $response;
}

makeDisposition() builds a correctly encoded Content-Disposition header, including a safe ASCII fallback for filenames with accents or spaces. For very large jobs (hundreds of pages, or batch exports), generate the PDF in a Symfony Messenger worker and store the file or a signed URL, then let the controller redirect to it. That keeps the request fast and moves heavy rendering off the web process.

Which option should you choose?

Match the option to your constraints rather than chasing one universal answer. The summary below covers the common Symfony scenarios.

  • Simple invoices, receipts, reports with basic CSS, no extra infra: use mPDF. Pure PHP, no binary, handles tables and Unicode well. Choose Dompdf only if you want the lightest possible dependency and accept a narrower CSS subset.
  • Modern CSS (flexbox, grid, web fonts), self-hosted, steady volume: run Gotenberg. You own the Docker container and the scaling, but you get exact Chromium output inside your network.
  • Modern CSS with zero infrastructure to run or scale: call the PDF4.dev API from Symfony HttpClient. You POST HTML or a template id and get a PDF back, rendered by headless Chromium, with no binary, no Docker, and no cold-start to manage.
  • Existing project on KnpSnappyBundle: it still works, but plan a migration. wkhtmltopdf is archived and unmaintained, so do not start new projects on it.

For most teams in 2026, the decision is between mPDF (simple documents, pure PHP) and a Chromium renderer (Gotenberg if you self-host, PDF4.dev if you do not). Both Chromium options produce identical output because they use the same engine. Pick based on whether you want to run the infrastructure yourself.

Free tools mentioned:

Html To PdfTry it freeWebpage To PdfTry it free

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.