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.