Skip to content
Devix Open Source

Reference

The invoice, the renderer and the shaper

Invoice

new Invoice(
    number: 'INV-2026-0042',
    seller: $party,
    buyer: $party,
    issuedAt: new DateTimeImmutable,
    currency: 'AED',
    country: 'AE',              // AE, SA, PK
    suppliedAt: null,
    dueAt: null,
    type: 'standard',           // or 'simplified'
    notes: null,
    poNumber: null,
    exchangeRate: null,
);
$invoice->add(Line ...$lines);

$invoice->netMinor();     // 620000
$invoice->taxMinor();     // 31000 — summed per line, which is what is required
$invoice->grossMinor();   // 651000
$invoice->format(651000); // '6,510.00'
$invoice->breakdown();    // net and tax per treatment and rate
$invoice->problems();     // reason codes
$invoice->isCompliant();

Tax is summed per line, never taken on the total. They are different numbers — ten lines of 0.30 at 5% give 0.20 per line and 0.15 on the total — and per line is what both the FTA and ZATCA specify.

Reason codes

no-lines · seller-tax-number · seller-address · buyer-name · buyer-address · buyer-tax-number · date-of-supply · exchange-rate · seller-name-arabic

Line

new Line(
    description: 'Website design',
    unitPrice: '18500.00',      // a decimal string, never a float
    quantity: 1,                // may be fractional — hours, kilos
    taxBasisPoints: 500,        // 5%
    treatment: 'standard',
    descriptionAr: 'تصميم موقع',
    unit: 'month',
    discount: '0',
    currency: 'AED',
);

Party

new Party(
    name: 'Devix Technologies FZ-LLC',
    taxNumber: '100123456700003',
    address: "Dubai Internet City\nDubai",
    nameAr: 'شركة ديفكس للتقنية',
    addressAr: 'مدينة دبي للإنترنت',
    country: 'AE', email: null, phone: null,
);

Renderer

$renderer = new Renderer([
    'language' => 'ar',          // turns the whole invoice round
    'font' => 'DejaVu Sans, sans-serif',
    'accent' => '#0d9488',
    'paper' => 'A4',
    'orientation' => 'portrait',
    'qr' => 'data:image/png;base64,…',
    'amountInWords' => 'فقط ستة آلاف وخمسمائة وعشرة دراهم',
]);

$renderer->html($invoice);   // self-contained HTML
$renderer->pdf($invoice);    // the bytes, via dompdf

The HTML has no <link>, no @import and no <script> — there is a test for that — because a PDF engine cannot fetch anything and fails quietly when asked to.

language: 'ar' also reverses the table columns, because dompdf lays them out in source order whatever dir says. Without it the description ends up on the left of an Arabic invoice, which no reader would accept. It was found by looking at a rendered page.

Shaper

use Devix\Invoice\Arabic\Shaper;

Shaper::shape($text);     // presentation forms, joins and ligatures
Shaper::reorder($text);   // right-to-left runs turned around
Shaper::render($text);    // both
Shaper::toPlain($shaped); // exactly back again

toPlain() is the escape hatch. Shaping writes presentation forms into the PDF, so copying out of a reader gives shaped characters — that is the trade every PHP Arabic invoice makes. When you need the original string for an XML attachment, a search index or an email subject, toPlain() gives it back, and a test proves the round trip is exact.

The font

It must contain Arabic Presentation Forms-B, U+FE70–U+FEFF. DejaVu Sans, which dompdf ships with, does — which is why none of this needs a font installed. Amiri and Cairo look better; register them with your engine and set font.

A font without that block renders boxes. That is the one failure mode left, and it is immediately obvious.

Updated 15 Sep 2026