Search by

himelali / pdf-generator

himelali

One fluent, driver-independent PDF API for Laravel on top of DomPDF, Snappy (wkhtmltopdf), mPDF and FPDF.

Package info

github.com/himelali/pdf-generator

pkg:composer/himelali/pdf-generator

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v2.0.0 2026-10-26 20:11 UTC

This package is auto-updated.

Last update: 2026-10-26 20:40:36 UTC


README

Latest Version Total Downloads Tests License Stars Forks

๐Ÿ“„ PdfGenerator for Laravel

One fluent PDF API for Laravel, four engines underneath. Write your PDF code once and switch between DomPDF, Snappy / wkhtmltopdf, mPDF and FPDF with a single config value.

use Himelali\PdfGenerator\Facades\Pdf;

return Pdf::loadView('invoices.show', ['invoice' => $invoice])
    ->setPaper('A4', 'landscape')
    ->setMargins(20, 15)
    ->setHeader('<strong>ACME Corp</strong>')
    ->setPageNumbers()
    ->setTitle("Invoice {$invoice->number}")
    ->download("invoice-{$invoice->number}.pdf");

The same chain produces the same page size, margins, header, footer, page numbers and metadata on every driver.

Table of contents

๐Ÿš€ Features

  • One generic API for DomPDF, Snappy (wkhtmltopdf), mPDF and FPDF
  • Paper sizes by name (A4, Letter, B5, ...) or in millimetres, rendered identically by every driver
  • Orientation, CSS-style margins, header, footer, footer text and Page {page} of {pages} numbering
  • The same default font and font size on every driver for text without its own CSS
  • Metadata: title, author, subject, keywords, creator
  • Load HTML, Blade views or HTML files, in any encoding
  • Download, display inline, get the raw bytes, save to a path or to any filesystem disk
  • Library-specific options when you need them (setOption()), without leaving the generic API
  • Shared document defaults in config, with per-driver overrides
  • Fast: library engines are configured once and reused; every document still starts fresh (Octane and queue safe)
  • Render log with time and memory per driver (Pdf::enableRenderLog()), off by default
  • Pdf::fake() with assertions for testing your own code
  • Package version via Pdf::VERSION / Pdf::version() and php artisan about
  • Fully typed, documented and tested (PHPStan level 8)

โœ… Requirements

Package Version
PHP 8.2 or higher
Laravel 10.x, 11.x, 12.x, 13.x

Each Laravel release has its own minimum PHP version (for example, Laravel 13 needs PHP 8.3+). Composer picks the right combination for you.

Required PHP extensions: dom, gd, mbstring, zlib.

The Snappy driver also needs a wkhtmltopdf binary (see Driver notes).

๐Ÿ“ฆ Installation

composer require himelali/pdf-generator

The service provider and the Pdf facade are registered automatically through Laravel package discovery.

Package discovery disabled?

Laravel 11 and newer, in bootstrap/providers.php:

return [
    // ...
    Himelali\PdfGenerator\PdfServiceProvider::class,
];

Laravel 10, in config/app.php:

'providers' => ServiceProvider::defaultProviders()->merge([
    Himelali\PdfGenerator\PdfServiceProvider::class,
])->toArray(),

'aliases' => Facade::defaultAliases()->merge([
    'Pdf' => Himelali\PdfGenerator\Facades\Pdf::class,
])->toArray(),

โš™๏ธ Configuration

Publish the config file to config/pdf-generator.php:

php artisan vendor:publish --tag=pdf-generator-config
return [
    // Driver used by the facade and by injected builders.
    'default' => env('PDF_DRIVER', 'dompdf'),

    // Starting settings of every new document, shared by all drivers.
    'defaults' => [
        'paper' => 'A4',                // name, or [width, height] in mm
        'orientation' => 'portrait',    // or "landscape"
        'margins' => 10,                // mm: 10, [10, 15], [10, 15, 20, 15] or ['top' => 20]
        'page_numbers' => false,        // true, false or a format like "{page} / {pages}"
        'default_font' => env('PDF_DEFAULT_FONT', 'sans-serif'),   // "sans-serif", "serif", "monospace" or a font name
        'default_font_size' => env('PDF_DEFAULT_FONT_SIZE', 12),   // points
        'font_dir' => storage_path('app/pdf/fonts'), // ignored until the directory exists
        'metadata' => ['creator' => env('APP_NAME')],
    ],

    // Records the time and memory of every render. Off by default.
    'log' => [
        'enabled' => env('PDF_LOG_ENABLED', false),
        'channel' => env('PDF_LOG_CHANNEL'),          // null = default log channel, false = memory only
        'level' => env('PDF_LOG_LEVEL', 'debug'),
    ],

    'drivers' => [
        'dompdf' => [
            'options' => [ /* DomPDF options, incl. 'chroot' => [public_path(), storage_path('app/public')] */ ],
        ],
        'snappy' => [
            'binary' => env('SNAPPY_PDF_BINARY', '/usr/local/bin/wkhtmltopdf'),
            'timeout' => 60,
            'options' => ['no-outline' => true, 'images' => true, 'disable-smart-shrinking' => true],
        ],
        'mpdf' => [
            'temp_dir' => storage_path('app/pdf/temp'),
            'options' => [ /* mPDF constructor configuration */ ],
        ],
        'fpdf' => [
            // "font" is the fallback for fonts FPDF does not have; null line_height follows the font size.
            'options' => ['font' => 'Helvetica', 'line_height' => null, 'compress' => true],
        ],
    ],
];

Font family and size are set once in defaults and apply to every driver (see Same fonts on every driver). A driver can still override any document default in its own defaults array:

'fpdf' => [
    'defaults' => ['paper' => 'Letter', 'default_font' => 'monospace', 'default_font_size' => 10],
    'options' => [/* ... */],
],

Precedence, from lowest to highest: defaults โ†’ drivers.{name}.defaults โ†’ the builder methods you call. For library options: drivers.{name}.options โ†’ setOption() / setOptions().

๐Ÿ”– Package version

The installed package version is available as a constant and as a method:

use Himelali\PdfGenerator\Facades\Pdf;
use Himelali\PdfGenerator\PdfManager;

Pdf::VERSION;          // "2.0.0" (constant on the facade)
Pdf::version();        // "2.0.0" (method)
PdfManager::VERSION;   // "2.0.0" (constant on the manager class)

From an injected factory:

use Himelali\PdfGenerator\Contracts\PdfFactoryInterface;

public function diagnostics(PdfFactoryInterface $pdf): array
{
    return ['pdf_generator' => $pdf->version()];
}

It is also listed by php artisan about, in the "PDF Generator" section:

php artisan about --only=pdf_generator
  PDF Generator ..............................................................
  Default Driver ...................................................... dompdf
  Drivers ......................................... dompdf, snappy, mpdf, fpdf
  Render Log ............................................................. OFF
  Version .............................................................. 2.0.0

Useful for diagnostics, for logging, or for stamping generated files:

Pdf::loadView('report')->setCreator('ACME Portal (PDF Generator ' . Pdf::version() . ')');

๐Ÿ›  Usage

Loading content

Pdf::loadHtml('<h1>Hello</h1>');                       // UTF-8 HTML
Pdf::loadHtml($latin1Html, 'ISO-8859-1');              // converted to UTF-8
Pdf::loadView('reports.sales', ['rows' => $rows]);     // Blade view
Pdf::loadFile(resource_path('pdf/terms.html'));        // HTML file

Both fragments (<p>...</p>) and complete documents (<!DOCTYPE html><html>...) are supported.

Layout

Pdf::loadHtml($html)
    ->setPaper('Letter')                  // A0โ€“A10, B0โ€“B10, C4โ€“C6, DL, Letter, Legal, Tabloid, Ledger, Executive, Folio
    ->setPaper([100, 150])                // custom size in mm; the orientation decides which side is the width
    ->setPaper('A4', 'landscape')         // size and orientation at once
    ->setOrientation('portrait')          // "portrait"/"P" or "landscape"/"L"
    ->landscape()                         // shortcut
    ->setMargins(15)                      // all sides, in mm
    ->setMargins(20, 15)                  // vertical, horizontal
    ->setMargins(20, 15, 25, 15);         // top, right, bottom, left

Header, footer and page numbers

Pdf::loadView('report')
    ->setHeader('<div style="text-align:right">ACME Corp</div>')
    ->setFooter('<small>123 Main Street, Springfield</small>')
    ->setFooterText('Confidential')       // plain text, centred, HTML-escaped
    ->setPageNumbers()                    // "Page 1 of 4"
    ->setPageNumbers('{page} / {pages}')  // custom format
    ->setPageNumbers(false);              // turn off

The header and footer are drawn inside the top and bottom margins, so make those margins tall enough for their content. mPDF grows the margins for you.

Fonts

Pdf::setDefaultFont('serif')                      // sans-serif, serif, monospace, or a font name
    ->setDefaultFontSize(11)                      // points; for text without its own CSS
    ->setFontDirectory(storage_path('fonts'));    // custom font files, see Driver notes

See Same fonts on every driver for how this works per library.

Metadata

Pdf::loadHtml($html)
    ->setTitle('Annual Report 2026')
    ->setAuthor('Finance Team')
    ->setSubject('Results')
    ->setKeywords('finance, annual, report')
    ->setCreator('ACME Portal');

// or all at once
Pdf::loadHtml($html)->setMetadata(['title' => 'Annual Report', 'author' => 'Finance Team']);

Library-specific options

Anything the generic API does not cover can be passed straight to the active library:

Pdf::driver('dompdf')->setOption('dpi', 150)->loadHtml($html)->output();
Pdf::driver('snappy')->setOptions(['grayscale' => true, 'lowquality' => true])->loadHtml($html)->output();
Pdf::driver('mpdf')->setOptions(['autoScriptToLang' => true, 'autoLangToFont' => true])->loadHtml($html)->output();

Output

// HTTP responses (return them from a controller or route)
return Pdf::loadView('invoice', $data)->download('invoice.pdf');  // save dialog
return Pdf::loadView('invoice', $data)->stream('invoice.pdf');    // shown in the browser

// Raw bytes, e.g. for a mail attachment or an API payload
$bytes = Pdf::loadView('invoice', $data)->output();

// Files
Pdf::loadView('invoice', $data)->save(storage_path('app/invoices/7.pdf')); // local path, folders created
Pdf::loadView('invoice', $data)->save('invoices/7.pdf', 's3');             // any filesystem disk

Mail attachment example:

use Illuminate\Mail\Attachment;

public function attachments(): array
{
    return [
        Attachment::fromData(fn () => Pdf::loadView('invoice', $this->data)->output(), 'invoice.pdf')
            ->withMime('application/pdf'),
    ];
}

Picking a driver

Pdf::driver('mpdf')->loadHtml($html)->stream();   // one document
Pdf::setDefaultDriver('snappy');                  // the rest of this request/process

Every static call (Pdf::loadHtml(), Pdf::setPaper(), Pdf::driver(), ...) starts a new document. Keep chaining on the returned builder, or store it in a variable:

$pdf = Pdf::driver();
$pdf->loadView('report', $data);
if ($draft) {
    $pdf->setHeader('<p>DRAFT</p>');
}
return $pdf->download('report.pdf');

๐Ÿ”ค Same fonts on every driver

Each library has its own built-in stylesheet, so the same loadHtml('<p>Text</p>') used to come out differently. Measured in the generated PDFs with each library's own defaults:

Driver Library default With this package's defaults
DomPDF DejaVu Sans, 12 pt DejaVu Sans, 12 pt
mPDF DejaVu Serif Condensed, 11 pt DejaVu Sans, 12 pt
FPDF Helvetica, 12 pt Helvetica, 12 pt
Snappy WebKit default (16 px, rescaled to fit the page) CSS 12 pt, printed at its real size

The package now applies one default_font and default_font_size (config defaults, or setDefaultFont() / setDefaultFontSize()) to every driver:

// config/pdf-generator.php
'defaults' => [
    'default_font' => 'sans-serif',
    'default_font_size' => 12,
],
Pdf::driver('mpdf')->loadHtml('<p>Text</p>')->output();   // 12 pt sans-serif
Pdf::driver('fpdf')->loadHtml('<p>Text</p>')->output();   // 12 pt sans-serif
Pdf::driver('dompdf')->setDefaultFontSize(10)->loadHtml('<p>Text</p>')->output(); // 10 pt

Only unstyled text is affected. The defaults are added before your own CSS, so any font-family or font-size in your HTML (stylesheet, style attribute or class) still wins:

Pdf::loadHtml('<p>12 pt</p><p style="font-size: 8pt">8 pt</p>');

Use a generic family for portability. A specific font such as "DejaVu Sans" or "Arial" does not exist in every library. A generic family is mapped by each driver to the best font it has:

default_font DomPDF mPDF Snappy (system fonts, in order) FPDF
sans-serif DejaVu Sans DejaVu Sans DejaVu Sans, Arial, Helvetica Helvetica
serif DejaVu Serif DejaVu Serif DejaVu Serif, Times New Roman, Times Times
monospace DejaVu Sans Mono DejaVu Sans Mono DejaVu Sans Mono, Courier New, Courier Courier

The DejaVu fonts cover a wide Unicode range. FPDF's core fonts only cover Windows-1252 (see FPDF).

How each driver applies the defaults:

  • DomPDF, mPDF, Snappy: a base body rule (plus a table rule for engines where tables do not inherit fonts) is put first in <head>. DomPDF and mPDF also get the font as their library default.
  • FPDF: the font and size are used directly, and the line height follows the size (1.2 ร— size, like browsers) unless line_height is set.
  • Snappy: disable-smart-shrinking is on by default so that CSS sizes print at their real size instead of being rescaled by WebKit.

Set default_font and default_font_size to null to go back to each library's own defaults.

โฑ๏ธ Render log: time and memory

The render log records how long every PDF took to render, how much memory it used and how large the result is, per driver. It is off by default and adds no overhead while off.

use Himelali\PdfGenerator\Facades\Pdf;

Pdf::enableRenderLog();                 // start recording (and write to the configured log channel)

Pdf::driver('dompdf')->loadView('report', $data)->output();
Pdf::driver('mpdf')->loadView('report', $data)->output();

Pdf::getRenderLog();                    // every render
Pdf::getRenderLog('mpdf');              // only mPDF renders
Pdf::getRenderSummary();                // totals per driver, to compare drivers

Pdf::disableRenderLog();                // stop recording (entries are kept)
Pdf::flushRenderLog();                  // clear the entries
Pdf::isRenderLogEnabled();              // bool

Pdf::getRenderLog() returns one array per render:

[
    'driver' => 'mpdf',
    'time' => 45.12,               // milliseconds
    'memory' => 6815744,           // peak memory used while rendering, in bytes
    'memory_retained' => 12288,    // memory still in use afterwards, in bytes
    'size' => 18734,               // PDF size in bytes
    'html_size' => 5120,           // input HTML size in bytes
    'paper' => 'A4',
    'orientation' => 'portrait',
    'rendered_at' => '2026-10-27T10:15:03.123+00:00',
]

Pdf::getRenderSummary() groups them per driver, with readable units:

[
    'fpdf' => [
        'count' => 1,
        'total_time' => '1.23 ms',
        'average_time' => '1.23 ms',
        'max_time' => '1.23 ms',
        'max_memory' => '413.38 KB',
        'total_size' => '1.37 KB',
    ],
    'dompdf' => [ /* ... */ ],
]
Key Unit
count number of renders
total_time, average_time, max_time ms, or s from 1 second
max_memory, total_size B, KB or MB (1 KB = 1024 bytes)

The raw numbers of every render (milliseconds and bytes) are available from Pdf::getRenderLog().

Writing to a log file. Each render is also written to a Laravel log channel, as a readable line plus the data above:

[2026-10-27 10:15:03] local.DEBUG: PDF rendered with [mpdf] in 45.12 ms using 6.50 MB memory (18.30 KB PDF, 5.00 KB HTML, A4 portrait) {"driver":"mpdf",...}
Pdf::enableRenderLog();            // configured channel (log.channel; null = the app's default channel)
Pdf::enableRenderLog('daily');     // a specific channel

Configure it in config/pdf-generator.php or .env:

PDF_LOG_ENABLED=true      # record from the start of every request / command
PDF_LOG_CHANNEL=daily     # channel name; leave empty for the default channel
PDF_LOG_LEVEL=info        # default: debug

Set 'channel' => false to keep entries in memory only (read them with Pdf::getRenderLog()).

What is measured:

  • Only the PDF library. Time and memory cover just the call that turns the document into PDF bytes with DomPDF, Snappy, mPDF or FPDF. Loading the HTML or Blade view, building the download/stream response and saving the file are not included.

  • memory is the memory the library consumed while rendering: the peak above the memory already in use when rendering started. PHP's peak memory counter is reset before each render (memory_reset_peak_usage()) while the log is enabled.

  • The first render with each driver in a PHP process also loads that library's code, so it uses more time and memory than later ones. Measured with the same HTML:

    Driver 1st render 2nd render
    FPDF 1.97 MB 417 KB
    mPDF 13.36 MB 3.98 MB
    DomPDF 13.32 MB 4.80 MB

    Compare drivers using renders after the first one (or several renders, with average_time).

Notes:

  • Only successful renders are recorded. output(), download(), stream() and save() all render once.
  • Entries stay in memory until flushed. In long-running processes (queue workers, Octane), call Pdf::flushRenderLog() or leave the log disabled.
  • php artisan about --only=pdf_generator shows whether the render log is on.

๐Ÿ“š API reference

Builder (Himelali\PdfGenerator\Contracts\PdfBuilderInterface)

Every setter returns the builder, so calls can be chained.

Method Description
loadHtml(string $html, string $encoding = 'UTF-8') Load HTML, converting it to UTF-8
loadView(string $view, array $data = [], array $mergeData = []) Render a Blade view and load it
loadFile(string $path, string $encoding = 'UTF-8') Load HTML from a file
setPaper(string|array|PageSize $paper, string|Orientation|null $orientation = null) Paper by name or [width, height] in mm
setOrientation(string|Orientation $orientation) portrait/P or landscape/L
landscape() / portrait() Orientation shortcuts
setMargins($top, $right = null, $bottom = null, $left = null) Margins in mm, CSS shorthand (or a Margins object)
setHeader(?string $html) HTML repeated at the top of each page (null removes it)
setFooter(?string $html) HTML repeated at the bottom of each page
setFooterText(?string $text) Plain, centred footer text
setPageNumbers(bool|string $format = true) Page numbers, with {page} and {pages} placeholders
setFontDirectory(string $path) Directory with custom fonts
setDefaultFont(string $family) Font for unstyled text: sans-serif, serif, monospace (same on every driver) or a font name
setDefaultFontSize(float|int $points) Font size in points for unstyled text, on every driver
setMetadata(array $metadata) title, author, subject, keywords, creator
setTitle() / setAuthor() / setSubject() / setKeywords() / setCreator() Single metadata values
setOption(string $key, mixed $value) / setOptions(array $options) Library-specific options
getDocument(): Document The immutable document built so far
output(): string Render and return the PDF bytes
save(string $path, ?string $disk = null) Render and write to a path or a disk
download(string $filename = 'document.pdf'): Response Render as an attachment response
stream(string $filename = 'document.pdf'): Response Render as an inline response

The builder is macroable, so you can add your own shortcuts:

use Himelali\PdfGenerator\PdfBuilder;

// In a service provider's boot() method:
PdfBuilder::macro('letterhead', function () {
    return $this->setPaper('A4')->setMargins(30, 20)->setHeader(view('pdf.letterhead')->render());
});

Pdf::letterhead()->loadView('letters.welcome', $data)->download();

Manager (Pdf facade, Himelali\PdfGenerator\PdfManager)

Method Description
VERSION (constant) / version(): string The package version, e.g. "2.0.0"
driver(?string $name = null): PdfBuilderInterface New document on the given (or default) driver
renderer(?string $name = null): PdfRendererInterface The shared renderer of a driver
extend(string $name, string|Closure $renderer) Register or replace a driver
getDefaultDriver() / setDefaultDriver(string $name) Read or change the default driver
getDrivers(): array / hasDriver(string $name): bool Available drivers
forgetRenderers(string ...$names) Rebuild renderers, e.g. after changing config at runtime
enableRenderLog(?string $channel = null) Start recording render time and memory (and writing them to a log channel)
disableRenderLog() / isRenderLogEnabled(): bool Stop recording / check the state
getRenderLog(?string $driver = null): array Recorded renders, optionally for one driver
getRenderSummary(): array Count, time, memory and size totals per driver, with readable units (1.23 ms, 413.38 KB)
flushRenderLog() Clear the recorded renders
fake(): PdfFake (facade only) Swap in the testing fake

๐Ÿงฉ Choosing a driver

DomPDF Snappy mPDF FPDF
Package dompdf/dompdf knplabs/knp-snappy mpdf/mpdf setasign/fpdf
External binary โ€“ wkhtmltopdf โ€“ โ€“
HTML / CSS CSS 2.1, some CSS 3 Full WebKit (CSS 3, JS) Good CSS 2.1, tables Text only
Header / footer HTML โœ… โœ… โœ… As plain text
Page numbers โœ… โœ… โœ… โœ…
Metadata All fields Title only All fields All fields
Unicode โœ… โœ… โœ… (incl. RTL) Windows-1252 only
Speed Medium Fast for complex pages Medium Very fast
Best for General use, no binaries Pixel-perfect modern layouts Multilingual / RTL documents Simple, high-volume text

๐Ÿ“ Driver notes

DomPDF

  • is_php_enabled (runs PHP embedded in the HTML) and is_remote_enabled (lets the HTML fetch any URL) are off by default. Only enable them for HTML you fully trust.
  • Local files (images, CSS) are only read from the chroot directories. The default is public_path() and storage_path('app/public'), the target of the public/storage link, so <img src="{{ public_path('logo.png') }}"> works. Add more directories to drivers.dompdf.options.chroot when needed. Files outside the chroot are silently skipped.
  • Custom fonts: call setFontDirectory() with a writable directory and declare the font with @font-face in your CSS. The directory is also added to DomPDF's chroot, so the font files can be read.
  • Options accept camelCase (isRemoteEnabled) or snake_case (is_remote_enabled). See DomPDF options.

Snappy (wkhtmltopdf)

Install wkhtmltopdf from wkhtmltopdf.org (use a build with patched Qt for header/footer HTML), or pull in a static Linux build through Composer:

composer require h4cc/wkhtmltopdf-amd64
SNAPPY_PDF_BINARY=/var/www/html/vendor/h4cc/wkhtmltopdf-amd64/bin/wkhtmltopdf-amd64
  • The binary may also be a command name such as wkhtmltopdf, which is looked up on PATH.
  • A missing binary throws BinaryNotFoundException the first time the driver is used, not while the app boots.
  • wkhtmltopdf 0.12.6+ blocks local files (images, CSS) by default. Allow them with setOption('enable-local-file-access', true) or in drivers.snappy.options, but only for HTML you trust.
  • Custom fonts: use @font-face in your CSS or install the font on the system. The default font and size are applied with a base CSS rule.
  • disable-smart-shrinking is enabled in the config so CSS sizes print at their real size. Remove it if you rely on WebKit shrinking wide pages to fit.
  • Any wkhtmltopdf option can go in options or setOption() (see wkhtmltopdf --extended-help).

mPDF

  • The temp_dir is created automatically and must be writable.

  • Custom fonts: put the .ttf files in the font directory and register them under fontdata. They are added on top of mPDF's bundled fonts:

    'mpdf' => [
        'defaults' => ['font_dir' => resource_path('fonts'), 'default_font' => 'inter'],
        'options' => [
            'fontdata' => ['inter' => ['R' => 'Inter-Regular.ttf', 'B' => 'Inter-Bold.ttf']],
        ],
    ],
  • Any mPDF configuration variable can go in options or setOption().

FPDF

  • FPDF does not lay out HTML. Markup is reduced to text, block elements become line breaks, and <script>/<style> are dropped.
  • The core fonts (Courier, Helvetica/Arial, Times, Symbol, ZapfDingbats) only cover Windows-1252, so other characters print as ?.
  • Custom fonts: generate definition files with FPDF's makefont utility (.json for FPDF 1.9+, .php for older versions), put them in the font directory, and use the font's name with setDefaultFont(). The core fonts stay available.
  • If the default font is not available to FPDF (for example DejaVu Sans shared with the HTML drivers), FPDF falls back to its configured font instead of failing.
  • Options: font and font_size (fallbacks when the document sets no default font/size), line_height in mm (null follows the font size) and compress.

๐Ÿ’‰ Dependency injection

Type-hint the contracts instead of using the facade:

Contract Resolves to
Himelali\PdfGenerator\Contracts\PdfFactoryInterface The manager (singleton): driver(), renderer(), version()
Himelali\PdfGenerator\Contracts\PdfBuilderInterface A new document on the default driver
Himelali\PdfGenerator\Contracts\PdfRendererInterface The default driver's shared renderer
use Himelali\PdfGenerator\Contracts\PdfFactoryInterface;

final class InvoicePdf
{
    public function __construct(private readonly PdfFactoryInterface $pdf)
    {
    }

    public function render(Invoice $invoice): string
    {
        return $this->pdf->driver('mpdf')
            ->loadView('invoices.show', ['invoice' => $invoice])
            ->setTitle("Invoice {$invoice->number}")
            ->output();
    }
}
use Himelali\PdfGenerator\Contracts\PdfBuilderInterface;

Route::get('/report', fn (PdfBuilderInterface $pdf) => $pdf->loadView('report')->stream('report.pdf'));

๐Ÿ”Œ Custom drivers

A driver is a renderer: one class that turns a Document into PDF bytes. It gets the whole builder API (loading, layout, output, testing) for free.

use Himelali\PdfGenerator\Contracts\PdfRendererInterface;
use Himelali\PdfGenerator\Domain\Document;

final class GotenbergRenderer implements PdfRendererInterface
{
    public function __construct(private readonly array $config = [])
    {
    }

    public function render(Document $document): string
    {
        [$width, $height] = $document->paper->dimensions($document->orientation); // mm

        // $document->html, ->margins, ->header, ->footer, ->footerText, ->pageNumbering,
        // ->metadata->toPdfInfo(), ->defaultFont, ->fontDirectory, ->options ...

        return $pdfBytes;
    }
}

Register it in a service provider's boot() method:

use Himelali\PdfGenerator\Facades\Pdf;

// Class name: resolved from the container with config('pdf-generator.drivers.gotenberg') as $config
Pdf::extend('gotenberg', GotenbergRenderer::class);

// Or a closure receiving the driver config and the container
Pdf::extend('gotenberg', fn (array $config, $app) => new GotenbergRenderer($config));

Pdf::driver('gotenberg')->loadHtml('<h1>Custom</h1>')->download();

extend() can also replace a built-in driver, for example to build DomPDF your own way.

๐Ÿงช Testing your application

Pdf::fake() swaps every driver for an in-memory renderer. No PDF library runs, the outputs are a tiny valid PDF, and every rendered document is recorded:

use Himelali\PdfGenerator\Domain\Document;
use Himelali\PdfGenerator\Facades\Pdf;

public function test_invoices_can_be_downloaded(): void
{
    $pdf = Pdf::fake();

    $this->get('/invoices/7/pdf')
        ->assertOk()
        ->assertHeader('Content-Type', 'application/pdf');

    $pdf->assertRenderedCount(1);
    $pdf->assertRendered(fn (Document $document) =>
        $document->metadata->title === 'Invoice 7'
        && str_contains($document->html, 'ACME Corp')
        && $document->orientation->isLandscape()
    );
}
Method Description
assertRendered(?Closure $callback = null) At least one document was rendered (and matched the callback)
assertRenderedCount(int $count) Exactly $count documents were rendered
assertNothingRendered() No document was rendered
rendered(?Closure $callback = null): Document[] The recorded documents

The fake also covers code that type-hints the contracts, and it keeps drivers registered with Pdf::extend() as well as a default driver changed at runtime.

โ— Exceptions

Every exception implements Himelali\PdfGenerator\Exceptions\PdfException, so you can catch them all at once:

use Himelali\PdfGenerator\Exceptions\PdfException;

try {
    $bytes = Pdf::loadView('report')->output();
} catch (PdfException $e) {
    report($e);
}
Exception Extends Thrown when
InvalidDocumentException InvalidArgumentException An invalid paper size, orientation, margin, page number format or metadata key is given
InvalidDriverException InvalidArgumentException An unknown driver is requested, or a custom driver does not implement PdfRendererInterface
BinaryNotFoundException PdfGenerationException The wkhtmltopdf binary cannot be found
PdfGenerationException RuntimeException A file, view or font directory cannot be read, or a PDF cannot be saved
RenderException RuntimeException The underlying library fails; the original error is available from getPrevious()

๐Ÿ› Architecture

src/
โ”œโ”€โ”€ Domain/              Pure, immutable model of a PDF, independent of any library
โ”‚   โ”œโ”€โ”€ Document.php         aggregate root: content, layout, decorations, metadata, options
โ”‚   โ”œโ”€โ”€ PageSize.php         named or custom size in mm
โ”‚   โ”œโ”€โ”€ Orientation.php      enum
โ”‚   โ”œโ”€โ”€ Margins.php          CSS-style shorthand
โ”‚   โ”œโ”€โ”€ PageNumbering.php    "{page} of {pages}" format
โ”‚   โ”œโ”€โ”€ GenericFont.php      enum: sans-serif, serif, monospace
โ”‚   โ””โ”€โ”€ Metadata.php         title, author, subject, keywords, creator
โ”œโ”€โ”€ Contracts/           PdfFactoryInterface, PdfBuilderInterface, PdfRendererInterface
โ”œโ”€โ”€ Renderers/           One adapter per library (Strategy pattern)
โ”‚   โ”œโ”€โ”€ DomPdfRenderer.php
โ”‚   โ”œโ”€โ”€ SnappyRenderer.php
โ”‚   โ”œโ”€โ”€ MpdfRenderer.php
โ”‚   โ””โ”€โ”€ FpdfRenderer.php
โ”œโ”€โ”€ Output/              PdfResponseFactory (HTTP), PdfWriter (local path / disk)
โ”œโ”€โ”€ Support/Html.php     HTML composition shared by the renderers (base font CSS, footer, ...)
โ”œโ”€โ”€ Profiling/           RenderProfiler (render log), RenderMetrics (one entry), HumanReadable (units)
โ”œโ”€โ”€ Testing/             PdfFake + FakeRenderer
โ”œโ”€โ”€ PdfBuilder.php       Fluent API; collects settings into a Document and delegates
โ”œโ”€โ”€ PdfManager.php       Driver registry and factory; caches renderers
โ”œโ”€โ”€ PdfServiceProvider.php
โ””โ”€โ”€ Facades/Pdf.php

How a call flows: Pdf::loadView() โ†’ PdfManager::driver() creates a PdfBuilder with the driver's cached renderer and the configured default Document โ†’ each setter replaces the immutable Document โ†’ download() asks the renderer for bytes and PdfResponseFactory for the response.

Design principles:

  • Single responsibility: the builder only collects settings; renderers only render; PdfWriter only stores; PdfResponseFactory only builds responses; RenderProfiler only measures and logs.
  • Open/closed: new libraries are added with extend() and one renderer class, without touching the package.
  • Liskov and interface segregation: every renderer honours the same small render(Document): string contract, and the same tests run against each one.
  • Dependency inversion and injection: the builder depends on contracts (PdfRendererInterface, Laravel's view and filesystem contracts), injected by the container. No global helpers are used in src/.
  • DRY: page sizes, margins, fonts, page number formats and footer composition are defined once and translated by each renderer.
  • Domain-driven design: the Domain layer is the shared language (Document, PageSize, Margins, ...). It is immutable, validated and free of framework or library code.
  • Performance: renderers are stateless, so they are built once per process. That includes DomPDF options, the Snappy generator and mPDF's font tables. Default documents are immutable and shared too. Each document still gets a fresh builder.

โฌ†๏ธ Upgrading from 1.x

2.0 is a rewrite around a driver-independent document model.

1.x 2.x
setPageSize('A4') setPaper('A4')
setFonts($dir) setFontDirectory($dir)
PdfDriverInterface PdfBuilderInterface (fluent API) and PdfRendererInterface (custom drivers)
Drivers\*Driver classes Renderers\*Renderer classes
Pdf::extend('x', CustomDriver::class) Pdf::extend('x', CustomRenderer::class); closures receive (array $config, Container $app)
Per-driver format, orientation, margin_*, font_dir keys Shared defaults (plus optional drivers.{name}.defaults)
mpdf.mode, mpdf.default_font_size, ... Move them under drivers.mpdf.options
fpdf.unit Removed: all sizes are in millimetres
Snappy binary checked when the app boots Checked when the driver is first used
--tag=config --tag=pdf-generator-config

New: loadFile(), setPaper(..., $orientation), landscape()/portrait(), CSS-style setMargins(), custom page number formats, metadata setters, setDefaultFont() / setDefaultFontSize() shared by all drivers, setOption(s)(), the render log (Pdf::enableRenderLog()), Pdf::fake(), Pdf::VERSION / Pdf::version() and the php artisan about section, the PdfException interface and InvalidDocumentException.

๐Ÿค Contributing

composer test       # PHPUnit
composer analyse    # PHPStan (level 8)
composer fix        # PHP CS Fixer

Set SNAPPY_PDF_BINARY to a wkhtmltopdf binary to also run the real Snappy rendering test.

Releasing a version

  1. Update PdfManager::VERSION in src/PdfManager.php (the facade's Pdf::VERSION follows it).
  2. Add a matching ## x.y.z heading to CHANGELOG.md. A test fails if the two differ.
  3. Tag the release with the same number: git tag v2.0.0.

License

This package is open-sourced software licensed under the MIT license.

It wraps the following libraries, each under its own license:

Make sure to review and comply with each library's license when you use this package in your projects.