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 Laravel Package

horstoeko/zugferd-laravel

Laravel integration for the horstoeko/zugferd library, making it easy to create, read, and validate ZUGFeRD/Factur-X electronic invoices. Provides service provider, facade, and configuration to generate PDF/A-3 files with embedded XML in Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require horstoeko/zugferd-laravel
php artisan vendor:publish --tag=zugferd

The service provider registers a Zugferd facade backed by the underlying horstoeko/zugferd builder. There are no migrations, no DB tables — everything lives in config/zugferd.php.

Where to look first

  1. config/zugferd.php — set default_profile. For EU/German public-sector recipients this is almost always ZugferdProfiles::PROFILE_XRECHNUNG_3; for general Factur-X use PROFILE_EN16931.
  2. The upstream horstoeko/zugferd README. The Laravel package is a thin DI/config/PDF-helper wrapper — 90% of the API surface you'll actually call lives in the base library (ZugferdDocumentBuilder, ZugferdDocumentReader, ZugferdDocumentPdfBuilder). Bookmark it.
  3. The package's own tests — they double as executable examples of the facade.

First use case: emit an XRechnung XML

Start with XML-only (skip PDF/A-3 until you need it — it's a separate rabbit hole).

use horstoeko\zugferd\Facades\Zugferd;
use horstoeko\zugferd\ZugferdProfiles;

$doc = Zugferd::getDocumentBuilder(ZugferdProfiles::PROFILE_XRECHNUNG_3);

$doc->setDocumentInformation('INV-2025-001', new \DateTimeImmutable('2025-03-15'), '380', 'EUR')
    ->setDocumentSeller('Acme GmbH')
    ->setDocumentSellerAddress('Hauptstr. 1', '10115', 'Berlin', 'DE')
    ->setDocumentBuyer('Customer AG', 'LEITWEG-123')
    ->setDocumentBuyerReference('04011000-12345-34')
    ->setDocumentSupplyDate(new \DateTimeImmutable('2025-03-10'));

$pos = $doc->addNewPosition('1');
$pos->setDocumentPositionProductDetails('Consulting')
    ->setDocumentPositionNetPrice(100.00)
    ->setDocumentPositionQuantity(1, 'H87')
    ->addDocumentPositionTax('S', 'VAT', 19.0)
    ->setDocumentPositionLineSum(100.00);

$doc->addDocumentTax('S', 'VAT', 19.00, 100.00, 19.00)
    ->setDocumentSummation(119.00, 119.00, 100.00, 0.0, 0.0, 100.00, 19.00);

Storage::put('invoices/INV-2025-001.xml', $doc->getContent());

Paste the output into the KoSIT validator before building anything else. If it passes there, it will pass with real customers.

Implementation Patterns

Wrap it in an action class, not a controller

The builder is stateful and mutates as you call it. Hide that behind a single entry point so you can reuse it from controllers, queued jobs, artisan commands, and mailables:

final class GenerateEInvoice
{
    public function __invoke(Invoice $invoice, string $format = 'xml'): EInvoiceResult
    {
        $doc = Zugferd::getDocumentBuilder($this->resolveProfile($invoice));
        $this->applyInvoice($doc, $invoice);
        $this->applyPositions($doc, $invoice);
        $this->applyTotals($doc, $invoice);

        return $format === 'pdf'
            ? new EInvoiceResult($this->renderPdf($doc, $invoice), 'pdf')
            : new EInvoiceResult($doc->getContent(), 'xml');
    }
}

Hybrid PDF (Factur-X / ZUGFeRD)

For an embedded-XML PDF, the wrapper exposes the PDF builder path: you give it the source PDF (your classic invoice layout) and the document builder, and it embeds the XML into a PDF/A-3. Pattern:

  • Render your normal invoice PDF first (DomPDF, Browsershot, whatever).
  • Load it into ZugferdDocumentPdfBuilder with the ZugferdDocumentBuilder, call generateDocument() then saveDocument($path).
  • Store both the PDF and the raw XML — accountants and auditors will ask for the XML, and some recipients can't open PDF/A-3.

Reader path for inbound invoices

You don't only emit — ZugferdDocumentReader lets you parse supplier invoices before import:

$reader = Zugferd::getDocumentReaderFromFile($path);
$reader->getDocumentInformation($invoiceNo, $invoiceDate, $type, $currency, $sum);
while ($reader->firstDocumentPosition() && $reader->nextDocumentPosition()) {
    // map to line items
}

Use it as a pre-flight gate: if the inbound XML doesn't parse, route to manual review instead of guessing.

Mail attachments

Don't Storage::get() and re-serialize — attach the exact bytes you generated and validated:

$mailable->attachData($result->content, "{$invoice->number}.xml", ['mime' => 'application/xml']);

For PDF, application/pdf. For XRechnung, XML only — recipients often reject PDFs for B2G.

Profile selection strategy

Don't hardcode a single profile. Resolve per recipient:

  • German public sector (Leitweg-ID present, buyer is a federal/state entity) → PROFILE_XRECHNUNG_3
  • EU B2B, EN16931-capable → PROFILE_EN16931
  • Legacy Factur-X retail → PROFILE_FACTURX_EN16931
  • Unknown → fail loudly, don't default silently.

Keep the resolution in one ProfileResolver class so tests can pin profiles.

Async for bulk

PDF generation (font embedding, PDF/A post-processing) is CPU-heavy. For nightly batch runs, dispatch a job per invoice, write the result to object storage, and record success/failure on the model. XML is cheap enough to do inline.

Gotchas and Tips

XRechnung mandatory fields bite first. setDocumentBuyerReference() (the Leitweg-ID) and seller contact details are required for XRechnung; omit them and getContent() throws with a cryptic validation message. Always run a fixture through KoSIT in CI.

Rounding will mismatch your totals. The builder trusts you on setDocumentSummation() and addDocumentTax() — it does not compute them. Calculate totals once from integer cents in your domain layer, then feed strings/floats into the builder. Half a cent of drift between line sums and document summation produces a hard validation failure.

Quantities need UN/ECE codes, not words. 'H87' (piece), 'C62' (unit), 'KGM', 'LTR'. Passing 'pcs' may serialize but fails validation on strict recipients.

Dates are \DateTimeInterface. Format is handled by the library; don't pre-format strings. But pay attention to timezone — issueDate should reflect the timezone of the seller, not the server.

Tax category codes (S, Z, E, AE, K, G, O) must match the tax scheme and rate. S + VAT + 19.0 for standard German VAT; E for exempt with 0.0 and a required exemption reason note via addDocumentNote().

PDF/A-3 is the hardest part of the whole package. Fonts must be embedded; transparency and certain color spaces break PDF/A conformance. The wrapper's PDF pipeline handles the XML embedding, but if your source PDF fails PDF/A, the result silently isn't compliant — validate with veraPDF or Mustang. If you can, generate your base invoice PDF via the same DomPDF/MPDF setup the package expects rather than merging arbitrary PDFs afterward.

Config caching in production. php artisan config:cache will freeze your default_profile. If profile is dynamic per tenant, don't read it from config at runtime — pass it explicitly into getDocumentBuilder(). In tests, config:clear between test cases that change profile, or you'll chase phantom failures.

The wrapper is thin. Some builder methods aren't surfaced on the facade. Don't fight it — call $doc->setDocumentX(...) directly on the ZugferdDocumentBuilder the facade hands back. The wrapper's job is config + service binding + PDF helpers; treat everything else as the upstream library.

Debugging validation failures. Catch the exception around getContent() (and saveDocument()), log the full message plus the $doc inputs. The library's error strings usually name the missing field. If they don't, $doc->getContent() on a minimal fixture and bisect the document — remove fields one at a time until it passes.

Locale. Set locale in config for German labels in PDF output, but never let your own number_format() run on amounts before passing them to the builder. Amounts are decimal values; the library serializes them per the standard.

Version pinning matters. Factur-X, ZUGFeRD, and XRechnung are all moving standards (XRechnung 3.0 → 3.0.1 → 3.1 differences are real). Pin the package version, read the changelog before upgrading, and keep the KoSIT validation fixture in your test suite so upgrades fail loudly instead of producing subtly non-conformant invoices.

Testing. Assert against a golden-file XML for one canonical invoice (careful with dates in the snapshot), and validate with KoSIT in a separate, slower CI job. Don't rely solely on the package's own validation — it does not replace the official validators.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource