Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Zugferd Laravel Package

horstoeko/zugferd

A PHP library for creating, reading, and validating ZUGFeRD and Factur-X electronic invoices, including XML data and PDF/A-3 hybrid invoice documents for compliant e-invoicing workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer (PHP 8.1+ is required by current releases):

composer require horstoeko/zugferd

First, read ZugferdProfiles (the profile enum-ish constants: PROFILE_MINIMUM, PROFILE_BASIC, PROFILE_EN16931, PROFILE_EXTENDED, PROFILE_XRECHNUNG_3, …). Everything else hangs off four classes:

  • ZugferdDocumentBuilder — creates XML
  • ZugferdDocumentReader — parses XML or PDF (it extracts the embedded XML for you)
  • ZugferdDocumentPdfBuilder — attaches the XML to a PDF/A-3 file
  • ZugferdDocumentValidator — runs the EN16931/XRechnung rules (shells out to Java)

First use case: build an EN16931 (Factur-X COMFORT) invoice from an Eloquent Order.

use horstoeko\zugferd\ZugferdDocumentBuilder;
use horstoeko\zugferd\ZugferdProfiles;

$doc = ZugferdDocumentBuilder::createNew(ZugferdProfiles::PROFILE_EN16931);

$doc->setDocumentInformation(
        'RE-2024-0001', '380', new \DateTimeImmutable('2024-05-01'), 'EUR'
    )
    ->setDocumentSeller('Acme GmbH', 'HRB 12345')
    ->addDocumentSellerTaxRegistration('VA', 'DE123456789')
    ->setDocumentBuyer('Customer AG', 'K-42')
    ->setDocumentBuyerReference('04011000-12345-67');

$doc->addDocumentTax('S', 'VAT', 19.0, 100.00, 19.00);

$doc->addNewPosition('1')
    ->setDocumentPositionProductDetails('Widget', 'A widget', 'WID-1')
    ->setDocumentPositionNetPrice(100.00)
    ->setDocumentPositionQuantity(1, 'H87')
    ->addDocumentPositionTax('S', 'VAT', 19.0)
    ->setDocumentPositionLineSummation(100.00);

$doc->setDocumentSummation(119.00, 119.00, 100.00, 0.00, 0.00, 100.00, 19.00);

$xml = $doc->getContent();                 // string
// or $doc->writeFile($path);

For a PDF, generate the visual invoice with whatever you already use (DomPDF, Browsershot, Snappy), then embed:

use horstoeko\zugferd\ZugferdDocumentPdfBuilder;

ZugferdDocumentPdfBuilder::fromPdfFile($doc, $sourcePdfPath)
    ->generateDocument()
    ->saveDocument($outputPdfPath);

The critical first lesson: the library does not make an arbitrary PDF PDF/A-3 compliant. It attaches the XML and sets the AF relationship/metadata; the input PDF must already be PDF/A-3 (embedded fonts, correct color space, XMP). Budget time for that separately.

Implementation Patterns

Wrap it in a service, not in controllers. The builder is a mutable, chained object and profile rules differ per tenant. A single InvoiceXmlFactory / ZugferdInvoiceService that takes a DTO (or a well-scoped Eloquent model) and returns {xml, profile, hash} keeps the static-factory API out of your domain code and makes it testable.

Map to DTOs first. Build an InvoiceData value object (seller, buyer, lines, taxes, totals) once, then have a thin adapter translate that into ZugferdDocumentBuilder calls. This is essential if you later support XRechnung and Factur-X side by side — only the mapping layer changes.

Profile per tenant/recipient, not per app. Store the profile (EN16931 for Factur-X COMFORT, XRECHNUNG_3 for German public-sector recipients) on the customer or tenant settings row. Public-sector German recipients need XRechnung; French/B2B Factur-X is usually EN16931; MINIMUM is only useful for legacy readers.

Persist the XML, don't regenerate it. Store the generated XML (and the attached PDF) in storage/ with the invoice hash, exactly as sent. Regenerating from Eloquent months later with a different library version produces a different document — that's an audit problem. Treat the XML as an immutable artifact.

Queue it. Building a 200-line invoice and stamping a PDF takes hundreds of milliseconds to seconds. A GenerateZugferdInvoice job on the documents queue, writing to Storage::disk('invoices'), is a good default. Chain validation as a second job.

Inbound invoices: normalize with the reader. ZugferdDocumentReader::readAndGuessFromFile($path) handles both .xml and .pdf and extracts the embedded XML. Note the getters use by-reference out-parameters, so wrap a small method per field group and return an array/DTO:

$doc = ZugferdDocumentReader::readAndGuessFromFile($path);
$doc->getDocumentInformation($no, $type, $date, $currency, $lang, $notes);
$doc->getDocumentSummation($grandTotal, $duePayable, /* ... */);

Guard every out-param — they stay uninitialized/null when the field is absent.

Validation in CI and in a job. ZugferdDocumentValidator::fromFile($path)->validate() then inspect getValidationResult(). Run it as a queued job in production against every generated invoice, and as a PHPUnit test against golden fixtures in CI.

Testing. Keep a handful of real XRechnung/ZUGFeRD XML fixtures from the repo's tests/assets and assert with assertXmlFileEqualsXmlFile or a normalized snapshot. For Eloquent-backed builders, freeze invoice numbers and dates so XML is deterministic.

Laravel wiring. No service provider is required, but a small one that calls ZugferdSettings in boot() (temp folder, attachment/export folder, Java/Mustang paths) is the cleanest place for environment-specific config. Use storage_path() on the local disk, not the system /tmp, so multi-container deployments don't cross wires.

Gotchas and Tips

  • Java is a hard dependency of the validator. ZugferdDocumentValidator shells out to the Mustang/KoSIT validator and may download a JAR on first use. In Docker, install a JRE, pre-bake the JAR, and point ZugferdSettings at it; otherwise validation silently fails or hangs in CI. Do not let this run in a web request.
  • Attachment ≠ PDF/A-3 compliance. If your DomPDF/Browsershot output isn't PDF/A-3, some recipients' viewers will complain even though the XML is valid. Validate the PDF (e.g. veraPDF) separately from the XML.
  • XRechnung mandatory fields bite. BT-10 buyer reference (setDocumentBuyerReference), BT-30 seller VAT ID or tax number, BT-34/BT-49 electronic addresses, seller contact (name/phone/email), and payment means with IBAN. EN16931 validation passing does not imply XRechnung passing — always validate against the target profile.
  • Profile constraints. MINIMUM/BASIC WL cannot express line items or full taxes; don't start with MINIMUM and expect to grow. XRechnung has no MINIMUM/BASIC variant at all.
  • Rounding and totals. setDocumentSummation must reconcile with line sums and per-rate tax sums; EN16931 is strict. Compute in bcmath or integer cents, round half-up to 2 decimals (amounts) / 4 decimals (quantities and unit prices), and format with number_format($v, 2, '.', '') before handing floats to the builder. One cent off = validation failure.
  • Tax category codes must be consistent. S, Z, E, AE, K, G, O — exempt/zero-rated categories require an exemption reason; reverse charge requires the buyer's VAT ID. One addDocumentTax per category/rate pair, and matching addDocumentPositionTax on each line.
  • Unique, stable line IDs. addNewPosition('1') — IDs must be unique and ideally stable (SKU + index), because credit notes and corrections reference them.
  • Sanitize free text. Descriptions and notes go into XML with length limits and no control characters; strip/replace before calling product details and addDocumentNote. Ampersands and non-ASCII are handled by the serializer, but bad UTF-8 from the DB is not.
  • Don't mutate after getContent(). The builder is stateful; serialize once, then hand the string around. Calling getContent() repeatedly is not guaranteed to be free of side effects.
  • ZugferdSettings is global static state. In long-running processes (Octane, queue workers) set it once during boot, not per invoice, or reset it between jobs.
  • Pin the library version. Profile constants and mapping methods track ZUGFeRD 2.3 / XRechnung 3.x. A minor bump can change required fields; lock it, upgrade deliberately, and re-run the golden-file tests.
  • Large invoices / memory. Hundreds of lines are fine, but PDF merging plus an embedded XML of megabytes can spike memory in a worker — raise memory_limit on that queue or split very large invoices.
  • Extension points: subclass the PDF builder if you need custom XMP/metadata, wrap ZugferdDocumentBuilder rather than patching it, and look at the companion orderx package if you need to ingest XRechnung orders. For unsupported CIUS rules, post-process the XML rather than forking.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource