cleverit/ubl_invoice
A Laravel/PHP package for building UBL invoice XML documents. cleverit/ubl_invoice provides an object-oriented API to assemble invoice data and output standards-compliant UBL XML for electronic invoicing workflows.
The package is unmaintained (last release Feb 2019, no public repository linked), so treat this as a vendor-locked dependency to isolate and wrap, not a foundation to build on. Install it and confirm what it actually pulls in before writing application code.
composer require cleverit/ubl_invoice
composer show cleverit/ubl_invoice # inspect require constraints + autoload
php artisan vendor:publish --provider="CleverIt\UBLInvoice\UBLInvoiceServiceProvider"
Look in this order:
composer.json — PHP and illuminate/* constraints. If it caps laravel/framework below ^8, you cannot install it on Laravel 9+ without a fork or --ignore-platform-reqs (the latter is a stopgap, not a deployment answer).config/ubl_invoice.php (after publishing) — seller details, currency defaults, XML namespace/prefix options.src/ — the generator entry point (likely a facade or a bound service singleton). Read the constructor and any toXml()/save() method; that is your entire usable API surface.tests/ and examples/ if shipped — the fastest way to learn the expected input shape is usually a fixture invoice.First use case: generate one valid UBL 2.1 invoice for a single hardcoded buyer/seller and one line item, write it to storage/app/invoices/, and validate it against the UBL XSD with xmllint. Do this before wiring anything to Eloquent — you want to know the package produces schema-valid output at all before debugging your own data mapping on top of it.
$invoice = app(UBLInvoice::class) // or the facade from the README
->setSeller($sellerData)
->setBuyer($buyerData)
->addLine($lineData);
Storage::put("invoices/{$number}.xml", $invoice->toXml());
Wrap it, don't call it. The highest-value pattern with an abandoned package is a one-class seam:
interface InvoiceXmlGenerator { public function generate(InvoiceData $data): string; }
final class UblGenerator implements InvoiceXmlGenerator
{
public function generate(InvoiceData $data): string { /* package calls live only here */ }
}
Everything else in your app depends on InvoiceXmlGenerator. When you eventually migrate to easybill/ubl-invoice, horstoeko/orderx, or a hand-rolled builder, one file changes.
DTO → mapper → generator. Keep your domain models clean. Have a dedicated InvoiceData value object (seller, buyer, lines, tax breakdown, totals) and a single OrderToInvoiceData mapper per source model. This is where 90% of your maintenance cost lands, so make it explicit and unit-testable rather than sprinkling array assembly into controllers.
Persist the XML, not just the result. Invoices are legal documents and must be immutable. Store the generated file with a content hash and the metadata you'll need to prove it:
$xml = $generator->generate($data);
$hash = hash('sha256', $xml);
Invoice::create([
'number' => $number, 'buyer_vat' => $data->buyerVat,
'total' => $data->total, 'xml_path' => $path, 'sha256' => $hash,
'generated_at' => now(),
]);
Storage::disk('invoices')->put($path, $xml); // write-once
Once sent, never regenerate. Re-rendering from mutable Eloquent rows is how invoice numbers and totals drift from what the customer received.
Trigger from domain events. Bind generation to OrderPaid / InvoiceRequested via a queued listener, not to a controller. Bulk re-issues, month-end runs, and retries all become trivial. Use a per-entity sequence number (legal requirement), not id.
Validate before transport. The package only produces XML — it does not deliver. Validate with the UBL 2.1 XSD, then hand off to a Peppol Access Point or SDI connector:
$dom = new DOMDocument();
$dom->loadXML($xml);
$dom->schemaValidate(storage_path('schemas/UBL-Invoice-2.1.xsd')); // throws on failure
Golden-file tests. Snapshot the generated XML with frozen dates, IDs, and seller config. Normalize whitespace and XML declaration before asserting, and store fixtures as .xml files rather than inline strings so diffs are reviewable.
$this->assertXmlStringEqualsXmlString(
file_get_contents(base_path('tests/fixtures/invoice-basic.xml')),
$generator->generate($data)
);
Multi-tenant seller data. Put seller identity in a SellerProfile model or config keyed per tenant, not in the published config file — config is cached (config:cache) and won't survive runtime tenant switching.
illuminate/support ^9/^10/^11. Check composer why-not laravel/framework early. Realistic options: (a) fork, bump constraints, run tests, (b) pin your app to an old Laravel in a dedicated microservice, (c) replace it. Don't discover this at deploy time.DOMDocument::loadXML or SimpleXML on incoming XML, ensure libxml_disable_entity_loader(true) semantics (PHP 8 default, but audit) and never parse untrusted XML with the package's helpers.cbc:ID must precede cbc:IssueDate, tax totals must come before cac:InvoiceLine, etc. Array-to-XML builders that emit keys in insertion order will fail validation. If you're injecting extra fields, test against the XSD, not just by eyeballing.bcmul/string formatting with the mandated number of decimal places. Floats produce totals that fail Peppol's rounding rules, especially with VAT category codes S, Z, E, and AE, and with per-line vs per-document rounding differences.cbc:TaxExemptionReason and the right cbc:ID code; omitting it is the most common rejection reason.cac:/cbc: prefixes, but many validators and downstream parsers do. Keep the package's default consistent across all documents, and pin it in config.id. Generate cbc:UUID and cbc:IssueDate deterministically in tests, or your snapshots break hourly.DOMDocument::$encoding = 'UTF-8' and no leading whitespace in the template.xmllint --noout --schema UBL-Invoice-2.1.xsd invoice.xml → diff against a known-good sample from your access point → only then inspect the package call. The error messages will point at the schema line, which is usually your mapper, not the package.composer patch (cweagans/composer-patches) rather than editing vendor/.How can I help you explore Laravel packages today?