The reliable Symfony pattern is: render a Twig template to an HTML string, pass that string to KnpSnappyBundle’s KnpSnappyPdf service, and either write the generated file with generateFromHtml() or return the bytes with getOutputFromHtml(). wkhtmltopdf runs as a separate executable, so the binary path, temporary directory, asset URLs, JavaScript compatibility and security settings all matter in deployment.
What you need before writing code
- A Symfony application with Twig enabled.
- The
knplabs/knp-snappy-bundleComposer package. - A wkhtmltopdf executable installed in every environment that generates documents.
- A PDF Twig template designed for print rather than for an interactive browser application.
KnpSnappyBundle is an integration layer. Snappy wraps the external wkhtmltopdf and wkhtmltoimage command-line programs; the bundle is not a PDF renderer itself. The package is optional and is not part of Symfony’s core PDF functionality.
The upstream wkhtmltopdf repository has been archived and made read-only (January 2, 2023). Treat that maintenance status as a procurement and risk consideration for a new system. KnpSnappyBundle’s release listing currently shows 1.10.6 and mentions Symfony 8 support, while Symfony’s release page lists 8.1.7 as stable and 7.4.19 as LTS at the time of writing. These are separate release streams, not a compatibility guarantee: verify your Composer constraints, PHP version, binary build and operating system together.
Install the bundle and the renderer
Install the Symfony integration
composer require knplabs/knp-snappy-bundle
Install wkhtmltopdf using the method appropriate for your operating system or container image. Confirm that the PHP runtime—not only your interactive shell—can execute it:
#1 Best Overall
wkhtmltopdf --version
If PHP runs in a container, worker, queue process or separate production host, install the binary there as well. A path that works on a developer laptop does not automatically exist in production.
Configure the executable
Create or edit config/packages/knp_snappy.yaml:
knp_snappy:
pdf:
enabled: true
binary: '%env(WKHTMLTOPDF_BINARY)%'
options:
print-media-type: true
temporary_folder: '%kernel.project_dir%/var/snappy'
process_timeout: 90
Then define the environment variable for each deployment, for example:
WKHTMLTOPDF_BINARY=/usr/local/bin/wkhtmltopdf
Use the actual absolute path on the host. The bundle documents enabled, binary, options, temporary_folder (the system temporary directory by default) and process_timeout. Ensure the PHP user can execute the binary and read and write the temporary and output locations.
Rank #2
Create a print-oriented Twig template
Keep document markup deterministic and server-rendered where possible:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
{# templates/invoice/pdf.html.twig #}
Invoice {{ invoice.number }}
Invoice {{ invoice.number }}
{{ invoice.customerName }}
Description Amount
{% for line in invoice.lines %}
{{ line.description }} {{ line.amount|number_format(2) }}
{% endfor %}
Total: {{ invoice.total|number_format(2) }}
Use absolute asset URLs or inline critical CSS. A relative URL such as ../images/logo.svg is resolved by the converter’s process context, which may not be your web server’s document root. For dynamic applications, generate an absolute URL with Symfony’s URL generator and pass that URL to a converter method, or make the required assets available through a controlled, reachable location.
Return a PDF from a Symfony controller
Return PDF bytes directly
<?php
namespace AppController;
use KnpSnappyPdf;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpFoundationResponseHeaderBag;
use SymfonyComponentRoutingAttributeRoute;
final class InvoiceController extends AbstractController
{
#[Route('/invoices/{id}/pdf', name: 'invoice_pdf')]
public function pdf(int $id, Pdf $knpSnappyPdf): Response
{
$invoice = $this->getDoctrine()->getRepository('App\Entity\Invoice')->find($id);
if (!$invoice) {
throw $this->createNotFoundException();
}
$html = $this->renderView('invoice/pdf.html.twig', [
'invoice' => $invoice,
]);
$output = $knpSnappyPdf->getOutputFromHtml($html);
$response = new Response($output);
$response->headers->set('Content-Type', 'application/pdf');
$response->headers->set(
'Content-Disposition',
$response->headers->makeDisposition(
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
'invoice-' . $invoice->getNumber() . '.pdf'
)
);
return $response;
}
}
The repository lookup is illustrative; use your application’s current Doctrine injection conventions. The important sequence is renderView(), getOutputFromHtml(), then a PDF response. KnpSnappyBundle also provides a PdfResponse helper:
use KnpBundleSnappyBundleSnappyResponsePdfResponse;
$html = $this->renderView('invoice/pdf.html.twig', ['invoice' => $invoice]);
return new PdfResponse(
$knpSnappyPdf->getOutputFromHtml($html),
'invoice.pdf'
);
Write a file instead
$html = $this->renderView('invoice/pdf.html.twig', [
'invoice' => $invoice,
]);
$path = $this->getParameter('kernel.project_dir')
. '/var/generated/invoice-' . $invoice->getNumber() . '.pdf';
$knpSnappyPdf->generateFromHtml($html, $path);
return $this->file($path, 'invoice.pdf');
Create the destination directory and grant the worker user permission to write it. For large or slow documents, run generation in a queue and store the resulting file rather than holding a long-running HTTP request open.
Make URLs, CSS and images resolve
Prefer an absolute page URL when the template is already a route
If your PDF page can be rendered by a secured or signed route, generate an absolute URL and use the bundle’s URL-based output method. The converter then requests the page in its own process, so authentication, host names, TLS certificates and firewall rules must permit that request.
Use absolute asset references for HTML strings
For renderView() plus getOutputFromHtml(), references such as /build/app.css or images/logo.png may not resolve as they do in a browser. Use fully qualified URLs, inline required styles, or configure a controlled asset base. Test from the same container and user account that executes wkhtmltopdf.
Rank #4
Handle JavaScript conservatively
KnpSnappyBundle warns that JavaScript-heavy pages can fail because wkhtmltopdf is not fully compatible with ES6 APIs. Prefer server-rendered values in PDF templates. If a dependency requires missing APIs, add a tested polyfill; that is a compatibility workaround, not a promise that a modern browser application will render identically.
Security controls for untrusted content
Snappy’s documentation warns: “The --enable-local-file-access option in wkhtmltopdf can be risky if used with untrusted HTML or JavaScript. This may expose local files or lead to remote code execution.” Do not enable local-file access merely to make arbitrary user content render.
- Keep user-provided HTML separate from trusted templates.
- Sanitize and constrain data before inserting it into Twig.
- Allow only approved asset hosts and paths.
- Run the converter with a minimally privileged operating-system user.
- Use network egress controls where practical.
- Review any option that permits local files, custom headers, cookies or scripts.
If local images or stylesheets are unavoidable, restrict the accessible directory and assess the threat model before changing the option.
Best Value
- Used Book in Good Condition
Common failures and precise fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| “Unable to find wkhtmltopdf” | Binary missing or path is wrong for PHP. | Run the configured absolute path as the PHP worker user and correct knp_snappy.pdf.binary. |
| Empty or partly blank PDF | Template error, unsupported JavaScript, failed asset request or timeout. | Render the Twig HTML separately, remove client-side rendering, verify URLs from the runtime host and increase process_timeout only after fixing slow dependencies. |
| CSS or logo missing | Relative URL cannot be resolved in the converter process. | Use absolute URLs, inline critical CSS, or a controlled asset location. |
| Works locally, fails in production | Different binary build, permissions, fonts, DNS, TLS or temporary directory. | Compare versions and environment variables, test as the service account, and inspect the generated command’s stderr. |
| PDF request hangs | Page waits on an unreachable resource or JavaScript event. | Remove the dependency, set an appropriate process timeout, and move batch work to a queue. |
| Local-file-access warning | HTML needs local assets and an insecure option was enabled. | Prefer HTTP or inline assets; if unavoidable, allow only a tightly controlled directory and do not process untrusted HTML. |
Operational guidance
- Version together: record the wkhtmltopdf build, bundle version, PHP version and Symfony version in deployment artifacts.
- Test representative documents: include long tables, missing data, non-ASCII text, images, page breaks and slow assets.
- Watch resources: conversion uses a child process and temporary files; set worker memory, process limits and cleanup policies appropriately.
- Cache deliberately: invoice PDFs with immutable content can be stored by identifier and regenerated only when source data changes.
- Log failures: retain exit codes and stderr without logging secrets contained in headers, cookies or document data.
Or skip the browser setup
If your goal is a clean capture of a publicly reachable Symfony page—or a PDF through an API workflow—ScreenshotNeo removes the need to install and maintain a browser-rendering stack. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients.
For a direct request, see the ScreenshotNeo API documentation:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://your-symfony-site.example/invoices/123 -o invoice.webp
ScreenshotNeo includes 1,000 shots per month free with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan. Create a free ScreenshotNeo account.
Choosing this approach for a new project
wkhtmltopdf remains practical when an existing Symfony application already depends on KnpSnappyBundle and its HTML output is simple and stable. For a new system, explicitly weigh the archived upstream engine, required CSS and JavaScript fidelity, deployment burden, asset access model and treatment of untrusted HTML. Do not infer compatibility from a Symfony version number alone; validate the exact binary and package combination in a production-like environment.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Frequently Asked Questions
Can I use a Twig template without creating a public route?
Yes. Render it with Symfony’s renderView() and pass the resulting HTML string to getOutputFromHtml() or generateFromHtml(). Make asset URLs resolvable from the converter process.
Where should generated PDFs be created in a queue worker?
Use a writable application or object-storage staging directory, run the worker with the same configured binary and permissions as web requests, and persist the final file after conversion succeeds.
Is wkhtmltopdf a Symfony component?
No. wkhtmltopdf is an external command-line renderer; Snappy wraps it, and KnpSnappyBundle integrates that wrapper with Symfony.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minute

