Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Pdf Generation Bundle Laravel Package

sylius/pdf-generation-bundle

Sylius PDF Generation Bundle adds PDF document creation to Sylius stores, enabling configurable invoices, order confirmations, and other printable documents through flexible templates and service-based integration.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the bundle and one adapter. The bundle only orchestrates PDF generation; it does not ship a PDF engine. Pick the simplest adapter for your environment:

composer require sylius/pdf-generation-bundle

# Option A — easiest local setup, pure PHP
composer require dompdf/dompdf

# Option B — HTML/CSS fidelity, needs the wkhtmltopdf binary
composer require knplabs/knp-snappy-bundle
# and install wkhtmltopdf on the host/container

# Option C — remote rendering service
composer require gotenberg/gotenberg-php

Enable Sylius\PdfGenerationBundle\SyliusPdfGenerationBundle in config/bundles.php if Symfony Flex did not do it.

2. Add the minimum config. Start with dompdf unless you already run wkhtmltopdf or Gotenberg:

# config/packages/sylius_pdf_generation.yaml
sylius_pdf_generation:
    default:
        adapter: dompdf
        storage:
            type: filesystem
            directory: '%kernel.project_dir%/var/pdf'

default is always defined and is the fallback context. storage is optional if you only render bytes and handle persistence yourself.

3. First use case: render a Twig template to PDF bytes. Inject the high-level renderer, not an adapter:

use Sylius\PdfGenerationBundle\Core\Renderer\TwigToPdfRendererInterface;

final class InvoicePdfController
{
    public function __construct(private TwigToPdfRendererInterface $renderer) {}

    public function __invoke(Invoice $invoice): Response
    {
        $pdf = $this->renderer->render(
            'pdf/invoice.html.twig',
            ['invoice' => $invoice],
            'default',
        );

        return new Response($pdf, 200, [
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => sprintf('inline; filename="invoice-%s.pdf"', $invoice->getNumber()),
        ]);
    }
}

Where to look first. Read these in order:

  • src/Core/Renderer/TwigToPdfRenderer.php and HtmlToPdfRenderer.php — the request flow and context selection.
  • src/Adapter/Dompdf/, src/Adapter/KnpSnappy/, src/Adapter/Gotenberg/ — what each adapter actually does and which options processors exist.
  • src/DependencyInjection/Configuration.php — the real config schema, including storage cascading.
  • config/services.php and config/adapter/*.php — service IDs, tags, and what is abstract vs. concrete.
  • The repository’s agents/AGENTS.md — unusually good internal documentation for a package this small.

First meaningful integration. Render bytes with TwigToPdfRendererInterface; persist with PdfFileGeneratorInterface only if you want the bundle’s storage abstraction. For a first production use case, generate an invoice PDF on demand, stream it in the response, and avoid storing it until you know your retention requirements.

Implementation Patterns

Use one context per document type. Contexts are the package’s main abstraction. Configure them per business document rather than per controller:

sylius_pdf_generation:
    default:
        adapter: dompdf
        storage:
            type: filesystem
            directory: '%kernel.project_dir%/var/pdf'
    contexts:
        invoice:
            adapter: knp_snappy
            storage:
                type: flysystem
                filesystem: 's3.storage'
                prefix: 'invoices'
                local_cache_directory: '%kernel.project_dir%/var/pdf_cache'
        receipt:
            adapter: dompdf
            storage:
                type: filesystem
                directory: '%kernel.project_dir%/private/receipts'

Then pass 'invoice' or 'receipt' as the third argument. This lets you change the engine for one document type without touching callers. Do not make the context a user-controlled string unless you validate it against your configured contexts.

Render Twig when layout matters; render raw HTML when you already have markup. TwigToPdfRendererInterface is the normal path. Use HtmlToPdfRendererInterface for emails, stored HTML snapshots, or HTML produced by a non-Twig templating layer:

$pdf = $this->htmlToPdfRenderer->render($html, 'invoice');

Keep rendering and storage separate. The renderer returns a PDF byte string. If you need the bundle’s storage layer, wrap the bytes with PdfFileGeneratorInterface:

use Sylius\PdfGenerationBundle\Core\Generator\PdfFileGeneratorInterface;
use Sylius\PdfGenerationBundle\Core\Filesystem\Manager\PdfFileManagerInterface;

$pdf = $this->twigToPdfRenderer->render('pdf/invoice.html.twig', ['invoice' => $invoice], 'invoice');
$file = $this->pdfFileGenerator->generate(
    sprintf('invoice-%s.pdf', $invoice->getNumber()),
    $pdf,
    'invoice',
);

// Later, retrieve it
$stored = $this->pdfFileManager->get($filename, 'invoice');
$localPath = $this->pdfFileManager->resolveLocalPath($filename, 'invoice');

Use resolveLocalPath() when you need to hand the file to a mailer attachment, a ZIP command, or an external process. For Flysystem/Gaufrette storage this may materialize a local cache copy, so treat it as a temporary path rather than permanent storage.

Configure generator options with options processors, not YAML. Adapter-specific settings such as page size, margins, and allowed files belong in an OptionsProcessorInterface:

use Sylius\PdfGenerationBundle\Core\Attribute\AsPdfOptionsProcessor;
use Sylius\PdfGenerationBundle\Core\Processor\OptionsProcessorInterface;

#[AsPdfOptionsProcessor(adapter: 'knp_snappy', context: 'invoice', priority: 10)]
final class InvoiceSnappyOptionsProcessor implements OptionsProcessorInterface
{
    public function process(object $generator, string $context = 'default'): void
    {
        /** @var \Knp\Snappy\AbstractGenerator $generator */
        $generator->setOption('page-size', 'A4');
        $generator->setOption('margin-top', 10);
        $generator->setOption('margin-bottom', 10);
    }
}

The context attribute is optional and defaults to default. Processors tagged for default run for every render using that adapter; context-specific processors run afterward. Higher priority runs first. Use this to centralize security settings such as disabling remote assets or restricting local file access.

Use a custom adapter when the bundled engines do not fit. For example, a wrapper around an internal PDF service:

use Sylius\PdfGenerationBundle\Core\Adapter\PdfGenerationAdapterInterface;
use Sylius\PdfGenerationBundle\Core\Attribute\AsPdfGenerationAdapter;

#[AsPdfGenerationAdapter('internal_pdf_service')]
final class InternalPdfServiceAdapter implements PdfGenerationAdapterInterface
{
    public function __construct(private HttpClientInterface $http) {}

    public function generate(string $html): string
    {
        $response = $this->http->request('POST', 'https://pdf.internal/render', [
            'body' => $html,
        ]);

        return $response->getContent();
    }
}

Then set adapter: internal_pdf_service in config. Custom adapters are user-managed services: the bundle does not create them and does not pass context or options into generate(). If you need per-context behavior in a custom adapter, inject the needed services and resolve behavior inside generate() or create separate adapter services.

Use a custom generator provider only when generator lifecycle matters. The bundled providers create fresh Knp\Snappy\Pdf and Dompdf\Dompdf instances per render. If you have an expensive client or a pool, implement GeneratorProviderInterface and tag it with sylius_pdf_generation.generator_provider. This is an advanced extension point; most applications should not need it.

Generate asynchronously for large or slow PDFs. PDF generation is synchronous and returns a full string. For invoices triggered by checkout or batch jobs, dispatch a Messenger message, generate in the worker, store the file, and persist only the filename in your entity. Avoid storing PDF bytes in the database unless you have a specific reason.

Test at the interface boundary. Inject TwigToPdfRendererInterface into application services and replace it with a stub in tests. For integration tests of your templates, configure a dedicated test context pointing at a custom adapter that records the HTML it receives:

#[AsPdfGenerationAdapter('test_recording')]
final class RecordingAdapter implements PdfGenerationAdapterInterface
{
    public array $html = [];

    public function generate(string $html): string
    {
        $this->html[] = $html;
        return '%PDF-1.4 fake';
    }
}

Then assert on the recorded HTML rather than trying to assert PDF binary content. Keep one real smoke test per adapter to catch missing binaries or misconfigured options.

Treat storage as environment-specific. Use filesystem for local development and small deployments; use flysystem with S3 or another remote filesystem for multi-node deployments. Set local_cache_directory when using remote storage and you need resolveLocalPath(). Ensure the prefix does not collide with other application files, and generate filenames with UUIDs rather than user input.

Gotchas and Tips

default is reserved. You cannot define contexts.default; the bundle already owns it. If you try, configuration validation should fail or produce surprising overrides. Use named contexts for everything else and keep default as a safe fallback.

Unknown adapter names are deferred, not immediately rejected. The extension stores unknown adapters in an internal parameter and a compiler pass resolves them later by matching sylius_pdf_generation.adapter tags. A typo in adapter: knp_snpy can therefore survive config validation and fail at runtime when no adapter is found for the context. After changing adapter config, run:

bin/console cache:clear
bin/console debug:container --tag=sylius_pdf_generation.adapter
bin/console debug:container --tag=sylius_pdf_generation.options_processor

If a custom adapter is missing, confirm its tag key exactly matches the configured adapter name.

Built-in adapters require their packages. knp_snappy, dompdf, and gotenberg each need the corresponding Composer package and, for KnpSnappy, the wkhtmltopdf binary. The bundle throws a LogicException with a composer require hint when the class is missing. That error usually means the package is installed in a different Composer root, the binary is absent from the runtime container, or the adapter package was installed after the container was compiled.

Options processor context matching is exact. A processor with context: 'invoice' will not run for Invoice or invoices. A processor with no context runs only for the default context, not for all contexts. The adapter attribute is required. When debugging why an option is not applied, check both the adapter name and the context string, then check priority ordering: default processors run before context-specific ones, and higher priority runs first within a group.

Adapters are context-unaware by design. PdfGenerationAdapterInterface::generate(string $html): string receives only HTML. Do not try to pass context, filenames, or tenant IDs through the adapter interface. If an adapter needs per-request state, use a custom adapter with injected request-scoped services or separate adapter services per tenant/context.

Do not hold onto generators across requests. KnpSnappy’s provider deliberately clones the knp_snappy.pdf definition as a non-shared prototype so each call gets a fresh generator. Dompdf and Gotenberg providers also create fresh instances. Injecting Knp\Snappy\Pdf or Dompdf\Dompdf directly and reusing it across requests can produce stale options, leaked state, or memory growth. Inject the bundle interfaces instead.

wkhtmltopdf asset and security issues are the most common production failure. Templates that reference /assets/..., relative URLs, or local files often render blank or fail because wkhtmltopdf cannot resolve them. Use absolute URLs served by your application, or configure allowed_files through a KnpSnappy options processor. Also be careful with untrusted HTML: wkhtmltopdf and Dompdf can fetch remote resources and read local files depending on options. For invoice/receipt content derived from user input, disable remote assets, restrict allowed files, and run generation in a sandboxed worker or container.

Dompdf defaults are conservative. Remote images and stylesheets may not load unless you enable remote access in a Dompdf options processor. Fonts must be available to Dompdf; custom fonts often need to be registered or referenced with a base path. Test with the exact PHP version and Dompdf version used in production, because CSS support varies.

Gotenberg requires a reachable service. The gotenberg.base_url setting defaults to http://localhost:3000. In Docker Compose this is usually a service hostname, not localhost. Network timeouts and 5xx responses surface as exceptions from the adapter; wrap generation in your own retry or fallback logic if PDF delivery is business-critical. The Gotenberg adapter has no bundled options processor in the documented file list, so adapter-specific behavior is limited to the base URL plus whatever you implement in a custom adapter or processor where applicable.

Storage paths need care. SymfonyFilesystemPdfStorage writes to the configured directory; ensure it exists, is writable by the PHP user, and is outside the public web root unless you intend direct downloads. For Flysystem/Gaufrette, resolveLocalPath() may lazily copy the file into local_cache_directory; that directory must be writable and should be cleaned up periodically. PdfFile has a mutable storagePath, so avoid sharing the same PdfFile instance across contexts or requests.

Filename safety is your responsibility. PdfFileManagerInterface::get(), has(), remove(), and resolveLocalPath() accept filenames. Do not pass raw user input. Generate names with UUIDs or validated invoice numbers, and reject path separators. A filename collision silently overwrites in filesystem storage.

PDFs are returned as full strings. There is no built-in streaming response or chunked generation. A very large template can exhaust memory in the PHP process, especially with Dompdf. For large documents, lower concurrency in workers, increase memory limits deliberately, or use a custom adapter that streams from an external service and writes directly to storage.

Exceptions propagate from Twig and the adapter. Missing templates, Twig syntax errors, wkhtmltopdf process failures, and Dompdf rendering warnings can all surface as exceptions or as generated PDFs with error output. In development, log the HTML passed to HtmlToPdfRendererInterface for failing requests. In production, catch adapter exceptions at the application boundary and return a controlled error rather than a 500 during checkout or invoice download.

Version maturity should shape your risk tolerance. This is an MIT-licensed Sylius package with very low public adoption (3 stars, 0 dependents at the time of assessment) and a recent release. The API is small and clean, but expect less community testing than a mainstream Symfony bundle. Pin the version in composer.json, add an integration test that renders a real PDF through your chosen adapter, and review the changelog before upgrades. The interfaces are simple enough that wrapping them behind your own application service is cheap insurance against future BC changes.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route