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

Twig2Pdf Bundle Laravel Package

uga/twig2pdf-bundle

uga/twig2pdf-bundle is a Symfony bundle that bridges Twig templates and PDF generation, letting applications render Twig/HTML views into PDF documents or responses. It simplifies producing invoices, reports, and other downloadable PDFs from familiar Twig templates.

View on GitHub
Deep Wiki
Context7

Getting Started

This is not a Laravel package. uga/twig2pdf-bundle is a Symfony 2.8 LTS bundle that registers a Twig2Pdf service in the Symfony DI container and wraps the spipu/html2pdf (TCPDF-based) renderer. It has no service provider, no facade, no publishable config, and its composer.json requires Symfony 2.8 components — so composer require uga/twig2pdf-bundle into a modern Laravel app will almost certainly fail dependency resolution against the symfony/* 6.x/7.x packages Laravel already ships.

Treat it as a reference implementation, not a dependency. The minimal path:

  1. Read the bundle's Resources/config/services.yml and the Twig2Pdf class first — that is the entire package. You are looking for two things: how it renders a Twig template to an HTML string, and how it hands that string to Html2Pdf and builds a Response.
  2. Reproduce that flow with Blade instead of Twig and whatever PDF engine you actually want.
  3. First use case: an invoice or report endpoint that renders a Blade view and streams a PDF.

If you insist on the same underlying engine, require it directly:

composer require spipu/html2pdf
// app/Http/Controllers/InvoicePdfController.php
public function __invoke(Invoice $invoice)
{
    $html = view('pdf.invoice', ['invoice' => $invoice])->render();

    $pdf = new \Spipu\Html2Pdf\Html2Pdf('P', 'A4', 'en');
    $pdf->writeHTML($html);

    return response($pdf->output('invoice-'.$invoice->id.'.pdf', 'S'), 200, [
        'Content-Type'        => 'application/pdf',
        'Content-Disposition' => 'inline; filename="invoice.pdf"',
    ]);
}

If you specifically want the Laravel-native route, barryvdh/laravel-dompdf or spatie/laravel-pdf (Browsershot) will get you further in an afternoon than porting this bundle ever will.

Implementation Patterns

Extract the service, keep the shape. The bundle's value is the shape: render(template, data) → html → pdf → response. Mirror it with a small injectable class so controllers, jobs, and mailables all share one code path:

class PdfRenderer
{
    public function render(string $view, array $data, array $options = []): string
    {
        $html = view($view, $data)->render();
        $pdf  = new Html2Pdf($options['orientation'] ?? 'P', 'A4', 'en', true);
        $pdf->writeHTML($html);
        return $pdf->output('', 'S'); // 'S' = return as string
    }
}

Return strings, not responses, from the renderer. The Symfony bundle tends to return a Response directly. That couples you to HTTP and makes the renderer useless for queued jobs and mail attachments. Let the controller decide between response()->streamDownload() and Storage::put().

Separate the layout from the content view. One pdf.layout Blade file holding <style> and page setup, with @yield('content'). TCPDF-era engines ignore most modern CSS, so keep the layout table-based and inline the CSS rather than relying on a stylesheet link.

Queue anything that isn't a single page. Render is CPU-bound and synchronous; a 30-page report will hold a PHP-FPM worker for seconds. Dispatch a job, write to storage/app/private/pdfs, then expose a signed download URL.

Integration tip for mail: attach via $message->attachData($renderer->render(...), 'invoice.pdf', ['mime' => 'application/pdf']). Never attach a Response object.

If you must run the original bundle, do it in a separate Symfony 2.8 microservice behind an HTTP call, not inside the Laravel app. It's the only way the Symfony 2.8 constraints won't poison your dependency tree.

Gotchas and Tips

  • Dependency hell is the first wall. Symfony 2.8 is EOL since 2018. Even if Composer resolves, you're installing unpatched components alongside Laravel's. There is no scenario where this belongs in require.
  • Twig ≠ Blade. Template syntax from the bundle's examples ({{ var }}, {% for %}, filters like |raw) will not run through Blade. Any template you copy has to be rewritten; {{ }} happens to overlap, {% %} does not.
  • Output buffering. TCPDF-based renderers emit warnings and stray whitespace straight into the PDF binary. Wrap generation in ob_start() / ob_get_clean(), or the downloaded file will be corrupt and open with "cannot display PDF."
  • Asset paths. Relative image and font URLs resolve against the filesystem, not the web root. Use public_path('images/logo.png') or embed as base64. The Symfony bundle's asset() calls have no Blade equivalent — swap for asset() helper output with an absolute URL, or base64.
  • Memory and timeouts. Html2Pdf routinely exceeds 128M on image-heavy pages. Set ini_set('memory_limit', '512M') and set_time_limit(120) around the call, or raise it in the job's middleware.
  • No JavaScript, limited CSS. Flexbox, grid, and web fonts are unreliable. Design PDF views like it's 2009: tables, floats, explicit page breaks (page-break-before), pt-based sizing.
  • Debugging render failures is painful because exceptions surface after partial output. Catch Spipu\Html2Pdf\Exception\Html2PdfException, log $e->getMessage() plus the HTML to a temp file, and inspect that HTML in a browser — most failures are unbalanced tags.
  • Charset. Pass 'UTF-8' explicitly to the constructor; the default is ISO-8859-1 on older versions and will mangle accents and currency symbols.
  • Extension point that matters: put the renderer behind an interface (PdfRendererContract) with Html2PdfRenderer and BrowsershotRenderer implementations bound in a service provider. You will want to swap engines the first time a stakeholder asks for a modern-looking PDF, and that swap should be a one-line config change.
  • Testing: assert on Content-Type: application/pdf and the %PDF- magic bytes rather than snapshotting the whole binary; byte output varies by engine version and makes tests flaky.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core