How to convert HTML to PDF in PHP and Laravel
PHP developers usually reach for Dompdf first, discover its CSS limits, then move to Browsershot and discover the cost of running Chromium next to PHP. This guide walks through both honestly and then shows the HTTP API approach with PDFOven.
Option 1: Dompdf
Dompdf is a pure-PHP renderer. No external binaries, installs with Composer, works on shared hosting.
composer require dompdf/dompdf
<?php
require 'vendor/autoload.php';
use Dompdf\Dompdf;
use Dompdf\Options;
$options = new Options();
$options->set('isRemoteEnabled', true);
$options->set('defaultFont', 'DejaVu Sans');
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
file_put_contents('invoice.pdf', $dompdf->output());
What to know:
- Dompdf implements most of CSS 2.1 and some CSS 3, but not flexbox or grid. Layouts built with them collapse. Tables and floats are the reliable tools.
- No JavaScript.
- Fonts must be TTF and registered via
@font-face; the bundled DejaVu fonts cover many scripts, but CJK needs extra fonts and large font files increase memory use. - Large tables can be slow and memory hungry, since everything happens inside the PHP process.
- Leave
isRemoteEnabledoff unless you need it, and never with user-controlled HTML, or the renderer can be used to fetch arbitrary URLs.
For simple receipts Dompdf is great. For anything designed in a modern browser, you will fight it.
Option 2: Browsershot (Puppeteer from PHP)
Spatie's Browsershot shells out to Node and Puppeteer, so you get real Chromium rendering.
composer require spatie/browsershot
npm install puppeteer
<?php
use Spatie\Browsershot\Browsershot;
Browsershot::html($html)
->format('A4')
->margins(12, 12, 12, 12)
->showBackground()
->waitUntilNetworkIdle()
->save('invoice.pdf');
$bytes = Browsershot::html($html)->format('A4')->pdf();
What to know:
- Your PHP servers now need Node, npm and a Chromium build plus its system libraries. On Forge, Vapor or shared hosts that is often the hard part. Laravel Vapor runs on Lambda, where Chromium does not fit without extra work.
- Each render starts a Node process and a browser, which is noticeably slower than Dompdf and uses hundreds of megabytes of memory. Run it in a queue worker rather than inline in a web request.
- Fonts have to be installed at the OS level on every server.
The PDFOven way: one HTTP call
With PDFOven, PHP only needs to make an HTTPS request. No binaries, no Node.
Guzzle
<?php
use GuzzleHttp\Client;
$client = new Client(['timeout' => 60]);
$response = $client->post('https://pdfoven.com/v1/pdf', [
'headers' => ['Authorization' => 'Bearer ' . getenv('PDFOVEN_KEY')],
'json' => [
'html' => $html,
'options' => [
'format' => 'A4',
'margin' => '0.5in',
'print_background' => true,
],
],
'http_errors' => false,
]);
if ($response->getStatusCode() !== 200) {
$error = json_decode((string) $response->getBody(), true)['error'];
throw new RuntimeException($error['code'] . ': ' . $error['message']);
}
file_put_contents('invoice.pdf', (string) $response->getBody());
Plain cURL
<?php
$ch = curl_init('https://pdfoven.com/v1/pdf');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PDFOVEN_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'html' => $html,
'options' => ['format' => 'Letter'],
]),
]);
$pdf = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException('PDF render failed: ' . $pdf);
}
Laravel controller
Render a Blade view to a string and return the PDF:
<?php
use Illuminate\Support\Facades\Http;
public function show(Invoice $invoice)
{
$html = view('invoices.pdf', ['invoice' => $invoice])->render();
$response = Http::withToken(config('services.pdfoven.key'))
->timeout(60)
->post('https://pdfoven.com/v1/pdf', [
'html' => $html,
'options' => ['format' => 'A4', 'print_background' => true],
'filename' => "invoice-{$invoice->number}.pdf",
]);
$response->throw();
return response($response->body(), 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="invoice-' . $invoice->number . '.pdf"',
]);
}
Because PDFOven renders with Chromium, the flexbox and grid layouts that broke in Dompdf work as they do in Chrome. If your Blade view loads charts with JavaScript, add 'wait_for_selector' => '#charts-ready'.
Other useful options
css: send styles separately from your markup.options.header_template/options.footer_template: repeating headers and footers, with<span class="pageNumber"></span>and<span class="totalPages"></span>for page numbers.options.landscape,options.scale, and formats A3, A4, A5, Letter, Legal and Tabloid.response: "base64"returns JSON withdata,content_typeandbytes, convenient for storing in a queue payload.POST /v1/imagerenders PNG (or JPEG with?type=jpeg).
Summary
| Dompdf | Browsershot | PDFOven | |
|---|---|---|---|
| Flexbox / grid | No | Yes | Yes |
| JavaScript | No | Yes | Yes |
| Server dependencies | None | Node, Chromium, fonts | None |
| Runs on serverless PHP | Yes | Difficult | Yes |
PDFOven's free plan covers 100 renders per month; paid plans are $12, $39 and $99 for 2,000, 15,000 and 60,000 renders. Errors are JSON ({ "error": { "code", "message" } }), and 429 quota_exceeded signals the monthly limit. Want to test a template first? Paste it into the free HTML to PDF tool.
Skip the browser plumbing
PDFOven runs Chromium for you. Send HTML, Markdown, a URL or JSON to a template and get a PDF back. 100 free renders every month.
Get a free API key