Laravel + dompdf — debug triks

Profesjonelle fakturaer i Laravel med dompdf — og et debug-triks som sparte meg for mange timer

Da jeg begynte å bygge fakturagenerering inn i SaaS-en min, trodde jeg det ville ta en ettermiddag. Det tok betraktelig lenger tid enn det.

PDF-generering i PHP har alltid vært litt av en hodepine. Men etter å ha jobbet meg gjennom quirksene til dompdf i et Laravel-prosjekt, har jeg landet på et oppsett som fungerer pålitelig — og underveis bygde jeg et lite debug-verktøy som jeg nå bruker på hver eneste PDF-mal jeg skriver.

Dette innlegget handler om hvordan jeg satte det opp, hva som skapte problemer, og Blade-komponenten som gjør det mye mindre frustrerende å plassere elementer i dompdf.

Hvorfor dompdf

Det finnes flere alternativer for PDF-generering i Laravel: dompdf, TCPDF, mPDF, og nyere løsninger som headless Chrome via Browsershot. Jeg valgte dompdf fordi:

  • Det installeres som en enkel Composer-pakke (barryvdh/laravel-dompdf)
  • Det rendrer fra vanlig HTML og CSS — ingen proprietær markup
  • Det håndterer franske fakturakrav godt nok (UTF-8, euro-tegn, aksenttegn)
  • Det er raskt nok for on-demand-generering i en web-forespørsel

For en SaaS som genererer fakturaer én om gangen på brukerhandling er det riktig balanse mellom enkelhet og kvalitet på resultatet.

Grunnleggende oppsett

composer require barryvdh/laravel-dompdf

I controlleren din:

use Barryvdh\DomPDF\Facade\Pdf;

public function download(Invoice $invoice)
{
    $pdf = Pdf::loadView('pdf.invoice', [
        'invoice' => $invoice,
    ]);

    return $pdf->download("facture-{$invoice->number}.pdf");
}

Blade-malen kan ligge på resources/views/pdf/invoice.blade.php og er vanlig HTML — ingen spesiell syntaks kreves.

Struktur på fakturamalen

Det viktigste å forstå om dompdf er at det ikke støtter CSS fullt ut. Flexbox og Grid er ikke tilgjengelig. Du skriver i praksis HTML som om det er 2005 — tabeller for layout, absoluttposisjonering for elementer som må plasseres presist, og inline-stiler overalt.

Her er grunnmalen jeg bruker:

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: DejaVu Sans, sans-serif;
        }

        body {
            font-size: 11px;
            color: #1a1a2e;
        }

        .page {
            position: relative;
            width: 794px;
            padding: 40px 40px 40px 60px;
        }
    </style>
</head>
<body>
    <div class="page">

        <x-pdf-ruler />

        {{-- Header: firmainformasjon + fakturanummer --}}
        {{-- Kundeblokk --}}
        {{-- Varelinjertabell --}}
        {{-- Totaler --}}
        {{-- Juridisk bunntekst --}}

    </div>
</body>
</html>

Et par ting verdt å merke seg:

Bruk DejaVu Sans som font. Den følger med dompdf og håndterer franske aksenttegn, euro-tegnet og de fleste spesialtegn uten at du trenger å bygge inn en egendefinert font. Vil du ha en tilpasset font kan du bygge den inn, men det øker kompleksiteten og filstørrelsen.

Sett en eksplisitt bredde på rotelementet ditt. Jeg bruker 794px, som er A4 ved 96 dpi. Uten dette kan dompdf oppføre seg uforutsigbart med prosentverdier.

Bruk position: relative på side-wrapperen. Dette er nødvendig for alle absolutt-posisjonerte barn — inkludert debug-linjalen.

Det ingen snakker om: pikselposisjonering

Når du er forbi grunnleggende layout, begynner den virkelige frustrasjonen. Du må plassere elementer på eksakte posisjoner — en logo øverst til høyre, et «BETALT»-stempel lagt diagonalt over fakturaen, en bunntekst festet til bunnen av siden. Og dompdf rendrer ting litt annerledes enn en nettleser.

Den vanlige fremgangsmåten er å rendre PDF-en, se på den, gjette at noe er omtrent 120px fra toppen, oppdatere verdien, rendre igjen, sjekke igjen. Denne loopen kan ta 20–30 iterasjoner for en kompleks mal.

Så jeg bygde en debug-linjal.

Debug-linjalen som Blade-komponent

Ideen er enkel: en smal gul stripe festet til venstre kant av siden, som viser pikselsverdier for hver 10px. Du dropper den inn i malen mens du jobber, bruker den til å lese eksakte posisjoner, og så forsvinner den automatisk i produksjon.

Komponentklassenapp/View/Components/PdfRuler.php:

<?php

namespace App\View\Components;

use Illuminate\View\Component;

class PdfRuler extends Component
{
    public function __construct(
        public int    $max   = 297,
        public int    $step  = 10,
        public int    $width = 30,
        public string $side  = 'left',
    ) {}

    public function render()
    {
        return view('components.pdf-ruler');
    }

    public function shouldRender(): bool
    {
        return config('app.debug', false);
    }
}

Blade-malenresources/views/components/pdf-ruler.blade.php:

<div style="
    position: absolute;
    {{ $side === 'right' ? 'right: 0;' : 'left: 0;' }}
    top: 0;
    bottom: 0;
    width: {{ $width }}px;
    background: #ffeb3b;
    font-size: 7px;
    font-family: monospace;
    color: #000;
    line-height: 1;
    padding: 0 2px;
    z-index: 9999;
    box-sizing: border-box;
">
    @for ($i = 0; $i <= $max; $i += $step)
        <div style="
            {{ $i === 0 ? '' : "margin-top: {$step}px;" }}
            border-top: 1px solid rgba(0,0,0,0.2);
            padding-top: 1px;
        ">{{ $i }}</div>
    @endfor
</div>

Bruk i PDF-malen din:

<div class="page">

    {{-- Synlig kun når APP_DEBUG=true --}}
    <x-pdf-ruler />

    {{-- Eller på høyre side --}}
    <x-pdf-ruler side="right" />

    {{-- Begge sider samtidig --}}
    <x-pdf-ruler side="left" />
    <x-pdf-ruler side="right" />

    {{-- Finere oppløsning --}}
    <x-pdf-ruler :step="5" />

    ... innholdet ditt her ...

</div>

shouldRender()-metoden er det viktige her. Når APP_DEBUG=false — som det alltid er i produksjon — hopper Laravel over komponenten helt. Ingen HTML sendes ut, ingen prosessering skjer. Det er ingenting som ved en feil kan havne i en PDF kunden ser.

Typiske A4-mål å ha i bakhodet

Når du bruker linjalen er disse referansepunktene nyttige:

ReferansePikselverdi (96 dpi)
A4 sidehøyde1123 px
A4 sidebredde794 px
Typisk toppmargin40 px
Typisk bunnmargin40 px
Brukbar høyde~1043 px

Jeg setter :max="1123" når jeg trenger å sjekke posisjoner helt ned til bunnen av siden.

Andre ting jeg har lært om dompdf

Tabeller er din venn. Selv om det føles som et steg tilbake, er <table> det mest pålitelige layoutverktøyet i dompdf. Varelinjer, totalblokken, headeren med firmainformasjon til venstre og fakturanummer til høyre — alt i tabeller.

Inline-stiler er mer pålitelige enn stilark. dompdf sitt CSS-cascade-støtte har sine quirks. Når noe ikke rendrer riktig, løser det seg ofte umiddelbart ved å flytte stilen inline.

Bilder bør base64-enkodes. Hvis du bygger inn en logo, konverter den til en base64 data-URI. dompdf kan slite med filstioppløsning, særlig i Docker-miljøer.

$logo = base64_encode(file_get_contents(public_path('images/logo.png')));
$logoSrc = 'data:image/png;base64,' . $logo;

Sideskift krever eksplisitt CSS. Hvis fakturaen kan strekke seg over flere sider, legg til page-break-inside: avoid på varelinjeradene så en rad aldri deles over to sider.

tr { page-break-inside: avoid; }

Resultatet

Debug-linjalen høres ut som en liten ting, men den hjalp meg genuint i arbeidet med PDF-maler. I stedet for en kjedelig rendre-gjett-juster-loop åpner jeg PDF-en én gang, leser av verdiene jeg trenger, og skriver de riktige tallene med en gang.

Komponent-tilnærmingen betyr også at den er gjenbrukbar på tvers av alle PDF-maler i prosjektet — tilbud, følgesedler, kvitteringer — med én linje kode og null risiko for at den dukker opp i produksjon.

Hvis du bygger fakturagenerering inn i din Laravel SaaS og vil spare noen timer med frustrasjon, håper jeg dette er nyttig. Kopier gjerne komponentkoden — det er den typen ting som bare burde finnes i ethvert Laravel dompdf-prosjekt.

Jeg bygger SaaS-verktøy og skriver om de tekniske valgene bak dem. Fant du dette nyttig, finnes det en kortere LinkedIn-versjon av innlegget som kan være verdt å dele med nettverket ditt.