webchemistry/invoice
PHP invoice library for generating invoices with PDF output, QR payment codes, and configurable invoice data. Simplifies creating, rendering, and exporting invoices in PHP applications.
Install via Composer; the package pulls in mPDF for rendering, so you get a %PDF binary out of the box with no headless browser:
composer require webchemistry/invoice
Minimal first invoice — the three concepts you need are party (who), item (what), and invoice (the document):
use WebChemistry\Invoice\Invoice;
use WebChemistry\Invoice\InvoiceParty;
use WebChemistry\Invoice\InvoiceItem;
$invoice = new Invoice();
$invoice->setNumber('2026-0001');
$invoice->setCompany(new InvoiceParty(
'Acme s.r.o.', 'Hlavní 1', 'Praha', '110 00', 'CZ', '12345678', 'CZ12345678'
));
$invoice->setCustomer(new InvoiceParty(
'Client a.s.', 'Vedlejší 2', 'Brno', '602 00', 'CZ', '87654321', 'CZ87654321'
));
$invoice->addItem(new InvoiceItem('Consulting', 10, 1500.00, 21)); // qty, unit price, VAT %
$invoice->addItem(new InvoiceItem('Hosting', 1, 299.00, 21));
$invoice->setDateOfIssue(new \DateTimeImmutable());
$invoice->setDateOfDue(new \DateTimeImmutable('+14 days'));
$pdf = $invoice->generate(); // string
file_put_contents(storage_path('invoices/2026-0001.pdf'), $pdf);
Where to look first: the examples/ directory in the repo (if present) is the fastest source of truth for the exact constructor/method signatures for your installed version — the API is small but has shifted between minor releases. Then read src/Invoice.php and the bundled template under src/templates/ to understand what data the PDF actually renders. If you're on Nette, check the DI extension (usually WebChemistry\Invoice\DI\InvoiceExtension) so you don't hand-wire objects.
First real use case: generating a downloadable invoice at the end of a checkout or from an admin "issue invoice" button. Don't try to email it on day one — get the file on disk, eyeball it, then automate storage and delivery.
Never scatter new Invoice() across controllers. Build one factory that takes your domain objects and returns a configured invoice:
final class InvoiceFactory
{
public function __construct(
private string $templatePath,
private string $sellerIco,
// ... company config
) {}
public function fromOrder(Order $order): Invoice
{
$invoice = new Invoice();
$invoice->setTemplate($this->templatePath);
$invoice->setNumber($order->invoice_number);
$invoice->setCompany($this->sellerParty());
$invoice->setCustomer($this->customerParty($order->customer));
foreach ($order->items as $line) {
$invoice->addItem(new InvoiceItem(
$line->description,
$line->quantity,
$line->unit_price, // keep as decimal, not cents, here
$line->vat_rate,
));
}
return $invoice;
}
}
This is the seam that makes the package testable and swappable. Your controllers depend on InvoiceFactory, not on WebChemistry\Invoice\Invoice.
Invoices are legal documents. Generate once, store the bytes (local disk, S3), and record the path plus a hash on the order row:
$pdf = $this->invoiceFactory->fromOrder($order)->generate();
Storage::disk('invoices')->put($path = "{$order->invoice_number}.pdf", $pdf);
$order->update(['invoice_path' => $path, 'invoice_generated_at' => now()]);
Subsequent downloads stream the stored file. This avoids re-rendering cost, protects against the package changing output after an upgrade, and gives you a deterministic artifact for audits.
The single biggest correctness risk is duplicate or skipped invoice numbers. Reserve the number before rendering, under a lock:
DB::transaction(function () use ($order) {
$seq = InvoiceSequence::lockForUpdate()->first();
$order->invoice_number = sprintf('%s-%04d', $seq->year, ++$seq->last);
$seq->save();
$order->save();
});
// render after the transaction commits
Then render in a separate step (or a queued job) so a PDF failure doesn't roll back the number — you want a number reserved and a retryable render, not a gap you can't explain to an auditor.
Generating hundreds of PDFs inline will time out. Push a GenerateInvoicePdf job per order onto a queue, with tries = 3 and a unique job key on the invoice number so retries don't double-render. mPDF is CPU-bound and single-threaded; horizontal workers scale it fine.
The clean extension point is the HTML template that mPDF renders. Copy the bundled template into resources/views/invoices/default.html, point setTemplate() at it, and restyle with CSS that mPDF understands (tables, @page, inline-ish CSS — no flexbox). Keep your branding there, keep your logic in InvoiceFactory. If you subclass Invoice you inherit maintenance debt on upgrade; template overrides survive minor bumps.
Assert the factory produces the right totals and that generate() returns a non-empty string starting with %PDF:
$pdf = $this->invoiceFactory->fromOrder($order)->generate();
$this->assertStringStartsWith('%PDF', $pdf);
$this->assertGreaterThan(1000, strlen($pdf));
If the package exposes the intermediate HTML, snapshot that — it's diffable and stable, unlike PDF bytes which change with every mPDF bump.
round($v, 2, PHP_ROUND_HALF_UP) before handing values to InvoiceItem. A cent of drift on a PDF is a support ticket.DateTimeImmutable in your app timezone; mPDF renders whatever you pass. Set issue/due/tax dates explicitly rather than relying on defaults, and be deliberate about the tax date (DUZP) if you're in the EU — it's separate from the issue date in many regimes.default_font in mPDF config or embed a Unicode font (DejaVu Sans is the safe default). Bump memory_limit if you render large item tables — mPDF is memory-hungry.setTemplate() wants a filesystem path, not a Laravel view name. Resolve it with resource_path() / base_path() and fail loudly if the file is missing — the error message from mPDF for a missing template is unhelpfully vague.generate() returns a string, not a response. Don't echo it. Wrap it in your framework's download/stream response (response($pdf)->header('Content-Type', 'application/pdf')) or write it to storage.composer.json, and if you upgrade, run your invoice tests plus a visual diff of one sample PDF.InvoiceFactory wrapper, (3) subclassing Invoice, (4) forking the package. Stay in (1) and (2) unless you hit a wall; the package is small (≈98 stars, single maintainer) so upstream fixes may be slow — vendor a fork only if you truly need it.How can I help you explore Laravel packages today?