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 Bundle Laravel Package

orkestra/pdf-bundle

Orkestra PDF Bundle brings configurable PDF generation to PHP applications, offering a centralized service for converting templates or HTML into downloadable documents. Ideal for invoices, reports, and exports, it keeps PDF setup reusable and easy to integrate within existing workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Read these three things first, in this order:

  1. composer.json — confirm the Symfony version constraint and what engines it actually pulls in. This tells you within 60 seconds whether you're going to try installing it or just port it.
  2. Pdf/PdfManager.php — the whole public contract. It exposes rendering through a manager that resolves a configured engine and forwards render() / output() style calls to it.
  3. DependencyInjection/Configuration.php — the config tree: which engine is selected by default, and what per-engine options (page size, orientation, margins) flow through.

Everything else in the bundle is an adapter (WkhtmltopdfPdf, TcpdfPdf, ZendPdf) plus Symfony plumbing (bundle class, extension, service definitions). That's the entire surface area — roughly a day of reading, not a week.

Reality check before you write a line of app code. This is a Symfony bundle, not a Laravel package. There is no ServiceProvider, no config/pdf.php you can vendor:publish, no facade, no auto-discovery. In a Laravel app you have two options:

  • Port the pattern (recommended). Lift PdfManager + the one adapter you need into app/Services/Pdf/. It's a few hundred lines and removes a dependency with 2 stars and zero downstream users. You keep the useful idea — a single interface in front of a swappable engine — and own it.
  • Bridge it. Register the bundle's services manually in a ServiceProvider, map a Laravel config array into the bundle's expected tree shape, and resolve PdfManager out of the container. Doable, but you're maintaining a container adapter for an abandoned package.

First use case to spike. A single internal document — a monthly statement or a back-office export — rendered from a Blade view (port the Twig calls; it's view()->render() in place of $twig->render()), streamed back with response()->streamDownload(). If that works end-to-end in a scratch app on your target PHP version, you have a viable path. If the install fails on PHP 8.2+ because of the Zend\Pdf constraint, you've learned it cheaply and you port.

Implementation Patterns

Wrap it behind your own contract, always. Even if you keep the vendor code, the call sites should never touch PdfManager directly.

namespace App\Contracts;

interface PdfRenderer
{
    /** @param array<string,mixed> $data
     *  @param array<string,mixed> $options
     */
    public function render(string $view, array $data = [], array $options = []): string;
}

Then a single adapter:

final class OrkestraRenderer implements PdfRenderer
{
    public function __construct(private PdfManager $manager) {}

    public function render(string $view, array $data = [], array $options = []): string
    {
        $html = view($view, $data)->render();

        return $this->manager
            ->getPdf($options['engine'] ?? null)   // engine chosen at call time, not hardcoded
            ->render($html, $options);
    }
}

Bind it in a ServiceProvider ($this->app->bind(PdfRenderer::class, OrkestraRenderer::class)) and every controller, command, and queued job gets consistent PDF output for free. This is the entire value proposition of the package: centralized configuration so PDF output stays consistent across controllers, commands, and background jobs.

Resolve the engine per call, not per install. The manager exposes engine selection, so $options['engine'] should be a decision your app makes — per tenant, per document type, or per feature flag. This is the one genuinely useful capability the bundle offers and the one thing you cannot get from a single-engine package like barryvdh/laravel-dompdf. Use it deliberately:

$engine = $tenant->prefers_legacy_renderer ? 'tcpdf' : 'wkhtmltopdf';
return $this->pdf->render('invoices.show', compact('invoice'), ['engine' => $engine]);

Keep engine-specific options in an options bag, never in the interface. Page size (A4, Letter), orientation, margins, and DPI look uniform but aren't — wkhtmltopdf and TCPDF interpret them differently, and wkhtmltopdf adds things (--enable-local-file-access, header/footer templates) the others don't have. Document the lowest common denominator in your contract's docblock, and pass anything engine-only through a $engineOptions array the adapter forwards untouched. Otherwise your "portable" interface leaks the moment someone needs a footer with a page number.

Push generation off the request path. wkhtmltopdf shells out and costs tens to hundreds of MB per process; TCPDF runs in-process and hits memory_limit on large documents. Neither is queue-aware, so you own batching, retries, and backpressure:

final class GenerateInvoicePdf implements ShouldQueue
{
    public function handle(PdfRenderer $pdf, Filesystem $disk): void
    {
        $bytes = $pdf->render('invoices.show', ['invoice' => $this->invoice]);
        $disk->put("invoices/{$this->invoice->id}.pdf", $bytes);
    }
}

Keep a synchronous path only for small, single-document, interactive downloads.

Pre-render HTML, verify before rendering. Render the Blade view to a string first, then feed the string to the engine. This gives you a seam to log or Storage::put() the HTML on failure, which is far easier to debug than a half-written PDF.

Test against golden files. Once output is stable, snapshot one PDF per document type and compare byte-for-byte (or page count + extracted text) in CI. This is what catches an engine swap or a binary upgrade silently changing pagination.

Gotchas and Tips

The bundle doesn't exist in Laravel. No auto-discovery, no facade alias, no publishable config. If you go the bridge route you must write a ServiceProvider that registers PdfManager, its factory, and each engine service, plus a config mapper — the bundle expects a Symfony-style nested tree, not a flat Laravel array. Budget a day for the bridge alone, and expect it to break on the next Symfony-less composer update.

wkhtmltopdf is EOL and will bite you on modern images. Archived since ~2020, Qt-WebKit based, no flexbox or grid, unreliable web fonts. It also needs --enable-local-file-access for anything local, and needs libxrender1 / font packages installed in your Docker image or you get blank or tofu-glyph pages that look like success. Pin the binary version in your Dockerfile and check its presence in a health endpoint or boot-time validation:

if (! shell_exec('command -v wkhtmltopdf')) {
    throw new RuntimeException('wkhtmltopdf binary missing from container.');
}

Watch for orphaned processes and hangs. Remote asset fetches inside templates (a logo on a slow CDN, an unreachable font) will hang wkhtmltopdf indefinitely. Always set a timeout, inline or base64 the assets, and never render untrusted HTML through it. Argument injection is a real surface if any user data reaches the command line.

Zend\Pdf is a dead namespace. ZF2 was archived and migrated to Laminas. On PHP 8.2+ composer will either fail resolving the constraint or install an abandoned package; if it does install alongside Laminas packages you can get autoload collisions. If you must keep one engine, keep TCPDF and drop the Zend adapter outright.

TCPDF means in-process memory. Large tables or many pages will hit memory_limit and fatal out — which in a queued job looks like a silent failure unless you log it. Raise the limit for that worker only (php -d memory_limit=512M artisan queue:work) and set a queue timeout; don't raise it globally.

Configuration shape is the sneaky failure. The bundle reads page size and orientation from its config tree at container build time, so changing them per-document via config won't work — they must be passed through the render options instead. Mixing the two up silently produces Letter-sized output for an A4 template with no error.

Debugging workflow that actually works: dump the rendered HTML to disk first, open it in a browser to confirm the markup is right, then and only then run the engine. Nine of ten "the PDF looks wrong" bugs are HTML/CSS problems or missing assets, not engine problems. For the tenth, check the binary (path, version, permissions) and then the fonts.

Output drift is the tax on the abstraction. Two engines will produce different pagination, different CSS support, and different font metrics for the same template. If you use per-tenant engine selection, your golden-file tests need a per-engine baseline, and any "let's swap engines" change becomes a design review, not a config tweak.

Extension points are thin. The useful one is writing your own adapter implementing the same render call and registering it with the manager — this is how you'd introduce a modern backend (Browsershot, Gotenberg, spatie/laravel-pdf) without touching call sites. That work is worth doing in your own App\Services\Pdf namespace with the package as a reference, not as a runtime dependency.

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.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle