magelan/htmltopdf-bundle
Symfony bundle for converting HTML markup into PDF documents, wrapping an HTML-to-PDF library with service configuration so you can generate downloadable or streamed PDFs from templates and controllers with minimal setup.
Reality check before anything else: magelan/htmltopdf-bundle is a Symfony bundle — *Bundle suffix, DependencyInjection/Extension + Configuration tree, and autoconfiguration via Symfony's kernel. It has no ServiceProvider, no config/*.php publish tag, no facade, and no extra.laravel auto-discovery. It will not composer require into a Laravel app and "just work." With 0 stars and 0 dependents, treat it as a reference implementation of wiring, not a runtime dependency.
Minimal path to first PDF:
Read three files in the repo before writing any code:
composer.json — the require block tells you the actual engine (likely spipu/html2pdf) and the PHP/Symfony constraints. If the constraints pin Symfony 3/4-era versions, they will conflict with a Laravel/latest-Symfony dependency graph.DependencyInjection/Configuration.php — the full option surface (page size, orientation, margins, fonts, encoding).DependencyInjection/MagelanHtmlToPdfExtension.php — which service ID is registered and how options map onto the engine.
That trio is the entire useful surface. Everything else is kernel boilerplate.Install only the engine, not the bundle. In a Laravel app:
composer require spipu/html2pdf
First use case — a Blade view → HTML string → PDF download for an invoice, statement, or admin report:
use Spipu\Html2Pdf\Html2Pdf;
$html = view('pdf.invoice', ['invoice' => $invoice])->render();
$pdf = new Html2Pdf('P', 'A4', 'en');
$pdf->writeHTML($html);
$pdf->output("invoice-{$invoice->id}.pdf", 'D'); // D=download, I=inline, F=file
Look first at the bundle's Configuration.php for the shape of defaults (margins, page size, fonts), then copy those defaults into your own config/pdf.php. You are porting configuration intent, not installing a package.
Translate the Symfony wiring into Laravel idioms before porting logic:
| Symfony bundle | Laravel equivalent |
|---|---|
Configuration.php tree |
config/pdf.php + config('pdf.*') |
Service via services.xml |
Singleton binding in a ServiceProvider |
%kernel.project_dir% |
base_path() |
Response helper (download/inline) |
response()->streamDownload(...) / response()->file(...) |
| Bundle-autowired generator service | app(PdfRenderer::class) behind an interface |
Always adopt behind an interface. This is the single most important pattern given the package's maintenance signal:
// app/Contracts/PdfRenderer.php
interface PdfRenderer {
public function render(string $html, array $options = []): string; // raw PDF bytes
}
// app/Services/Html2PdfRenderer.php
class Html2PdfRenderer implements PdfRenderer {
public function render(string $html, array $options = []): string
{
$pdf = new \Spipu\Html2Pdf\Html2Pdf(
$options['orientation'] ?? config('pdf.orientation', 'P'),
$options['paper'] ?? config('pdf.paper', 'A4'),
$options['lang'] ?? 'en',
);
$pdf->writeHTML($html);
return $pdf->output('', 'S'); // S = return as string
}
}
Bind it in AppServiceProvider::register():
$this->app->singleton(PdfRenderer::class, Html2PdfRenderer::class);
Separate rendering from responding. Controllers should not hold PDF bytes:
public function download(Invoice $invoice, PdfRenderer $renderer)
{
$html = view('pdf.invoice', compact('invoice'))->render();
$pdf = $renderer->render($html);
return response()->streamDownload(
fn () => print($pdf),
"invoice-{$invoice->id}.pdf",
['Content-Type' => 'application/pdf']
);
}
For inline preview, use response($pdf, 200, ['Content-Type' => 'application/pdf']).
Queue anything nontrivial. Rendering is CPU- and memory-bound; a synchronous render inside an HTTP request will time out on image-heavy templates. Pattern: dispatch a GenerateInvoicePdf job, store to the local or S3 disk, then email/notify.
dispatch(new GenerateInvoicePdf($invoice->id));
Keep views engine-friendly. The bundle's underlying engine is an old wkhtmltopdf/HTML2PDF class, so:
calc(), and modern CSS variables.charset in the layout <meta> if you handle non-Latin text.Test with a golden file. In a PdfRendererTest, assert the returned bytes start with %PDF- and contain the invoice number, so a refactor swapping the renderer still passes.
Do not install the bundle into Laravel. composer require magelan/htmltopdf-bundle will pull Symfony kernel pieces you don't need and may fail resolution against current Laravel. Extract the class, install the engine.
Verify the actual engine before quoting effort. The assessment docs flag that "whether it shells out to a binary (wkhtmltopdf-style) or drives a headless browser… determines the entire operational model." If it's wkhtmltopdf, you inherit archived software with poor modern-CSS support. If it wraps spipu/html2pdf (a pure-PHP lib), you get no shell/binary dependency — but limited CSS fidelity and slower large-document performance. Confirm which, in composer.json.
Fonts are the #1 silent failure. The engine looks up system fonts — if you render non-Latin scripts, ship the fonts into your Docker image (fonts-noto-cjk etc.) or characters render as tofu. Same for any custom font referenced in CSS.
The output() second argument is a footgun. 'D' downloads (sends headers), 'S' returns a string, 'F' writes a file, 'I' inlines. In Laravel you almost always want 'S' and let response() handle transport — this decouples rendering from HTTP.
Wrap output in try/catch for the engine's exception type. Html2Pdf throws Html2PdfException, not a generic \Exception. Map to a domain-level PdfGenerationException so the controller layer stays renderer-agnostic.
Timeout and memory ceilings from day one. Set max_execution_time and memory_limit for the worker/queue, and cap output size. A 50 MB PDF from a runaway template is a real failure mode.
The package is unowned and unmaintained. With 0 stars, 0 dependents, no issue history, no security review. Combined with HTML→PDF being a classic SSRF/local-file-inclusion surface (remote asset loading, file:// refs, embedded JS), never pass user-supplied HTML to it without sanitizing. Your own templates only.
The abstraction is the point. Because PdfRenderer is one interface with one implementation, swapping to spatie/browsershot or a Gotenberg client later is a one-file change (Html2PdfRenderer → BrowsershotRenderer), not a refactor. Do this from the first commit — the whole value of "borrowing wiring ideas from a 0-star Symfony bundle" is preserved only if the exit is cheap.
Extension point that actually matters: the options array in your PdfRenderer::render() signature. Model it on the bundle's Configuration.php keys (orientation, paper, margins, lang, default-CSS) so porting a config from the Symfony side is mechanical.
How can I help you explore Laravel packages today?