Skip to content
Devix Open Source

Guide

Getting started

composer require devix-labs/invoice-pdf
use Devix\Invoice\{Invoice, Line, Party, Renderer};

$invoice = new Invoice(
    number: 'INV-2026-0042',
    seller: new Party('Devix Technologies FZ-LLC', '100123456700003', 'Dubai Internet City', nameAr: 'شركة ديفكس للتقنية'),
    buyer: new Party('Al Noor Trading LLC', '100999888700003', 'Sharjah, UAE'),
    issuedAt: now(),
    currency: 'AED',
    country: 'AE',
);

$invoice->add(
    new Line('Website design', '18500.00', 1, 500, descriptionAr: 'تصميم موقع'),
    new Line('Hosting', '450.00', 12, 500, descriptionAr: 'استضافة'),
);

$pdf = (new Renderer(['language' => 'ar']))->pdf($invoice);

That is a correct, right-to-left Arabic tax invoice out of dompdf — the engine 5.7 million Laravel applications already have, which on its own cannot join a single Arabic letter.

Why every other PHP invoice is broken in Arabic

Arabic is cursive. A letter's shape depends on its neighbours: ع alone is ع, at the start of a word ع‍, in the middle ‍ع‍, at the end ‍ع. Unicode stores the letter once and leaves the choosing to the renderer.

dompdf, tcpdf and most others do not choose. They draw the isolated form of every letter, in the order given, left to right. The text is all there and completely unreadable.

So this package does the two passes a browser would:

use Devix\Invoice\Arabic\Shaper;

Shaper::shape('لا');                        // ﻻ — one glyph, never two
Shaper::shape('سلام');                      // ﺳﻼﻡ
Shaper::render('المجموع 1,234.56 درهم');    // reordered, with the number left alone

Shaping picks each letter's form and replaces lam + alef with the single glyph it is always written as. Reordering turns right-to-left runs around while leaving numbers and Latin words running the right way. After both, there is nothing left for the engine to get wrong.

Vowels ride along with their letter: مُحَمَّد stays one joined word, which is the bug that turns a name into six separate letters.

Urdu's extra letters — پ چ ٹ ڈ ڑ گ ک ہ ے — are there too.

Does the invoice pass?

$invoice->problems();
// []                      — it carries everything the UAE requires
// ['exchange-rate']       — it is in USD and does not show the rate used
// ['seller-tax-number']   — then it is not a tax invoice at all

Reason codes, per jurisdiction, so a missing field is caught by a test rather than by an auditor. country: 'AE' checks Federal Decree-Law No. 8 of 2017, Article 59; 'SA' checks what ZATCA asks for.

The one people forget: a UAE invoice not in dirhams must show the exchange rate used. Set exchangeRate: '3.6725'.

Money is never a float

use Devix\Invoice\Money;

Money::toMinor('1234.56');          // 123456 — whole fils
Money::toMinor('1.234', 'KWD');     // 1234 — the dinar has three places
Money::taxWithin(11500, 1500);      // 1500 — the tax inside 115.00, not 17.25

The same rules as Devix VAT Calculator, so a total computed in the browser and the one printed here are the same number. A hundred lines of 0.30 come to exactly what they should.

Tax rates are basis points: 500 is 5%, 1500 is 15%. Not a float, because 5% is not 0.05000000000000000277.

Treatments are not rates

new Line('Consulting', '1000.00', 1, 500);                     // standard
new Line('Export',     '2000.00', 1, 500, 'zero-rated');
new Line('Rent',       '3000.00', 1, 500, 'exempt');
new Line('Services',   '4000.00', 1, 500, 'reverse-charge');

All four of those are nought per cent of tax and none of them is the same thing: one recovers input tax, one does not, and reverse charge moves the liability entirely. breakdown() keeps them apart, because a VAT return does.

Where to go next

Updated 15 Sep 2026