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

Html2Pdf Bundle Laravel Package

obtao/html2pdf-bundle

ObtaoHtml2pdfBundle brings HTML-to-PDF conversion to Symfony apps, letting you render templates or raw HTML as PDF documents through a simple service. Configure it once and reuse it across controllers and commands.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the honest framing: this is not a Laravel package. obtao/html2pdf-bundle is a Symfony 2.x bundle — a Bundle subclass plus a DI extension and services.xml. Laravel's container never invokes any of it. If you add it to a Laravel composer.json you get dead code and, most likely, unresolved platform requirements on PHP 8.1+. There is no service provider to discover, no config/ to publish, no facade.

What the bundle does give you is a clean reference for how to wrap an HTML→PDF engine behind a service. The engine it wraps is spipu/html2pdf (the HTML2PDF class over TCPDF). That is the thing you composer require in Laravel:

composer require spipu/html2pdf

Where to look in the repo, in order — treat these as design reference, not code you install:

  1. DependencyInjection/Configuration.php — the config tree (orientation, format, margins, language, default font, font directory). This is the genuinely reusable artifact; mirror it into config/html2pdf.php.
  2. Resources/config/services.xml — how the wrapper is wired and which options are injected vs passed per-call.
  3. The Html2Pdf-style wrapper class — a thin factory that constructs the engine with config and exposes one render method. Reproduce this.
  4. ObtaoHtml2PdfBundle.php — boilerplate, ignore entirely.

Minimal Laravel port — config:

// config/html2pdf.php
return [
    'orientation' => 'P',
    'format'      => 'A4',
    'lang'        => 'en',
    'margins'     => [10, 10, 10, 10], // top, right, bottom, left (mm)
    'font_dir'    => storage_path('fonts'),
];

Service:

// app/Services/Html2PdfService.php
namespace App\Services;

use Spipu\Html2Pdf\Html2Pdf;

class Html2PdfService
{
    public function __construct(private array $config) {}

    public function render(string $html, array $overrides = []): string
    {
        $c = $overrides + $this->config;

        $pdf = new Html2Pdf(
            $c['orientation'],
            $c['format'],
            $c['lang'],
            true,        // unicode
            'UTF-8',
            $c['margins']
        );

        $pdf->writeHTML($html);

        return $pdf->output('', 'S'); // 'S' returns the PDF as a string
    }
}

Bind it once so you never new the engine in a controller:

// app/Providers/AppServiceProvider.php (boot or register)
$this->app->singleton(\App\Services\Html2PdfService::class, function ($app) {
    return new \App\Services\Html2PdfService($app['config']['html2pdf']);
});

First viable use case — a Blade view rendered to a downloadable PDF:

public function invoice(Invoice $invoice, Html2PdfService $pdf)
{
    $html = view('pdf.invoice', compact('invoice'))->render();

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

That is the whole day-to-day value: one config file, one two-method wrapper, and a Blade view. Everything the bundle added beyond that is Symfony plumbing you rebuild in ~40 lines.

Implementation Patterns

Config-first, options-last. The one genuinely good idea from the bundle: document formatting lives in config, not scattered through controllers. Keep orientation/format/margins/font in config/html2pdf.php, inject as a singleton, and expose per-document overrides as a second argument (render($html, ['orientation' => 'L'])) so landscape receipts don't require a second service.

Three output modes, one method. Html2Pdf::output($filename, $mode) is your whole response strategy. 'S' returns a string (stream through a controller), 'I' sends inline to the browser (avoid in Laravel — it bypasses the response pipeline), 'D' triggers a download header, 'F' writes to disk. For Laravel, always use 'S' and let response()/Storage handle the destination:

// Download
return response($pdf->render($html), 200, [
    'Content-Type'        => 'application/pdf',
    'Content-Disposition' => 'attachment; filename="report.pdf"',
]);

// Store
Storage::put("invoices/{$invoice->id}.pdf", $pdf->render($html));

// Queue an email attachment
Mail::raw('Attached.', fn ($m) => $m->attachData($pdf->render($html), 'invoice.pdf'));

Queue anything user-facing. PDF generation is CPU- and memory-bound and blocks the request. For anything that isn't a small single-page document, dispatch a job:

class GenerateInvoicePdf implements ShouldQueue
{
    public $timeout = 120;

    public function __construct(public int $invoiceId) {}

    public function handle(Html2PdfService $pdf): void
    {
        $invoice = Invoice::findOrFail($this->invoiceId);
        $html    = view('pdf.invoice', compact('invoice'))->render();
        Storage::put("invoices/{$invoice->id}.pdf", $pdf->render($html));
        $invoice->update(['pdf_path' => "invoices/{$invoice->id}.pdf"]);
    }
}

Use Blade partials for headers/footers, not engine headers. The engine supports <page_header> / <page_footer> tags, but the sane Laravel pattern is a single Blade layout that repeats them, and reserve the engine's own header/footer for page numbering. @include('pdf.partials.header') inside a pdf.layout parent keeps everything in one templating system your team already knows.

Keep the contract narrow behind an interface. Given the engine's age, isolate it so you can swap to browsershot/dompdf/Gotenberg without touching feature code:

interface PdfRenderer { public function render(string $html, array $opts = []): string; }

final class Html2PdfRenderer implements PdfRenderer { /* wraps spipu */ }

Bind the interface in AppServiceProvider; inject PdfRenderer everywhere. This is the single highest-leverage decision because it turns an abandoned dependency into a swappable detail.

Test with extracted text, not bytes. PDF bytes are non-deterministic (timestamps, compression). Assert on structural outcomes instead:

$pdf = app(Html2PdfService::class)->render(view('pdf.invoice', compact('invoice'))->render());

$this->assertStringStartsWith('%PDF-', $pdf);
$this->assertGreaterThan(1000, strlen($pdf));
// for content assertions, shell out:
$text = shell_exec('pdftotext -q - - <<<'.escapeshellarg($pdf));
$this->assertStringContainsString($invoice->number, $text);

Sanity-check the environment in a feature test. Add one test that boots the service, renders a trivial <p>ok</p>, and asserts a %PDF- prefix. This catches the missing-gd/missing-font/mbstring failures that otherwise first appear in production.

Gotchas and Tips

Do not install the bundle itself. On PHP 8.1+ composer require obtao/html2pdf-bundle will fail resolution or install with deprecation notices from the Symfony 2 constraints and TCPDF's PHP 5-era code. The correct dependency is spipu/html2pdf alone, and even that is legacy — check its current release against your PHP version before committing (there are maintained forks; treat compatibility claims as unverified until your own smoke test passes).

Memory is the #1 failure. The TCPDF lineage builds the whole document in memory. Large tables, embedded images, or long reports hit Allowed memory size exhausted. Mitigate with set_time_limit(120) and a raised memory_limit inside the job, chunk long tables across pages, and downscale images before embedding. Never raise limits globally — scope them to the PDF job.

CSS support is pre-HTML5, not Chrome. No flexbox, no grid, limited position, rough border-radius, fragile background handling. If the design demands modern CSS, spipu is the wrong tool — use a Chromium renderer. Test your actual template, don't assume; browsers and this engine disagree constantly.

Fonts and Unicode bite silently. Non-Latin scripts and special glyphs render as ? or boxes if the font isn't registered. Set font_dir to a writable path, register TTF fonts with $pdf->addFont(...), and pass true for the unicode flag. Keep a test that renders a Cyrillic/CJK/RTL sample so regressions surface early.

Margins are per-side and in millimetres, order matters. The constructor takes [$top, $right, $bottom, $left]. A wrong order produces clipped content that looks like a CSS bug. Verify with a template that draws content to the edges.

Images need absolute or resolvable paths. Relative image URLs resolve against the working directory, not public/. Convert to base64 or use Storage::path(...)/public_path(...). Remote URLs are unreliable — fetch and inline them.

output() echoes in some modes. 'I' and 'D' write headers and stream directly, which corrupts Laravel responses and breaks JSON tests. Always use 'S' and return a proper Response. If PDF garbage shows up before your HTML, something called output('x', 'I').

Debug by writing to disk, not the browser. When writeHTML throws (it throws rather than returning errors), dump the HTML to storage/app/debug/ first, render the same HTML in a browser to isolate templating vs engine issues, and only then instrument. The exception messages are terse; the HTML is where the bug is.

Extension points you'll actually use. Html2Pdf supports extensions (e.g. Html2Pdf\Extension\HtmlExtension) via $pdf->addExtension(...) and custom tags via $pdf->addTag(...). Reserve these for genuinely needed features — each extension is another abandoned-code surface to own.

Fork responsibly or don't. With 2 stars and 0 dependents there is no upstream fix channel. If you adopt spipu directly, either pin the version hard in composer.json, or budget time to vendor/fork. If you keep the PdfRenderer interface from the patterns above, that fork cost stays contained and a future swap to browsershot/Gotenberg is a one-class change rather than a rewrite.

Laravel-specific integration tip. Register the service in AppServiceProvider and set 'timeout'/'tries' on any queued PDF job; also consider a dedicated queue (onQueue('pdf')) so a burst of invoice generation can't starve your default queue.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console