Skip to content

HTML to PDF in PHP and Laravel

, 5 min read

PHP has no headless browser of its own, so HTML-to-PDF usually means a wkhtmltopdf binary, a Chrome install, or an HTTP API. This guide does the last, first in plain PHP with cURL, then with Laravel’s HTTP client.

Plain PHP

render.php
<?php
$ch = curl_init("https://api.pagenull.dev/v1/pdf");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 60,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("PAGENULL_API_KEY"),
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "html" => "<h1>Invoice #1001</h1><p>Total: €120.00</p>",
        "format" => "A4",
        "margin" => ["top" => "20mm", "bottom" => "20mm"],
    ]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 200) {
    $error = json_decode($body, true)["error"];
    throw new RuntimeException($error["code"] . ": " . $error["message"]);
}
file_put_contents("invoice.pdf", $body);

Laravel

Put the key in .env as PAGENULL_API_KEY and read it through config/services.php:

config/services.php
'pagenull' => [
    'key' => env('PAGENULL_API_KEY'),
],
app/Http/Controllers/InvoicePdfController.php
<?php

namespace App\Http\Controllers;

use App\Models\Invoice;
use Illuminate\Support\Facades\Http;

class InvoicePdfController extends Controller
{
    public function show(Invoice $invoice)
    {
        $html = view('invoices.pdf', ['invoice' => $invoice])->render();

        $res = Http::withToken(config('services.pagenull.key'))
            ->timeout(60)
            ->post('https://api.pagenull.dev/v1/pdf', ['html' => $html, 'format' => 'A4']);

        abort_unless($res->successful(), 502, $res->json('error.message'));

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

A Blade view renders the HTML, so the invoice layout lives with your other views. Inline CSS or a <style> block in the view is simplest; public stylesheet URLs also work.

Generating in a queue

For batches such as month-end invoices, render in a queued job and store the file under your own retention rules. pagenull keeps no copy, so this is the copy:

app/Jobs/RenderInvoicePdf.php (excerpt)
public function handle(): void
{
    $res = Http::withToken(config('services.pagenull.key'))
        ->withHeaders(['Idempotency-Key' => 'invoice-'.$this->invoice->id])
        ->timeout(60)
        ->post('https://api.pagenull.dev/v1/pdf', ['html' => view('invoices.pdf', ['invoice' => $this->invoice])->render()]);

    $res->throw();
    Storage::put("invoices/{$this->invoice->number}.pdf", $res->body());
    $this->invoice->update(['pdf_receipt' => $res->header('pagenull-receipt')]);
}

With the Idempotency-Key, a retried job is rendered again but billed once. Laravel retries failed jobs for you; with this header that is safe. The receipt is worth keeping: here’s why.