E-Rechnung in Laravel: XRechnung und ZUGFeRD mit KoSIT-Validierung
Veröffentlicht am · Marten Ackermann
Kurzantwort: Eine belastbare E-Rechnungs-Integration in Laravel baut keine eigenen Prüfregeln, sondern lässt den offiziellen KoSIT-Validator entscheiden. Eingehende XRechnungen und ZUGFeRD-Dateien werden in ein eigenes, formatneutrales Modell überführt, das Original bleibt unverändert das Rechtsdokument, und jede Verarbeitung landet in einem hash-verketteten Journal. Ausgehende Rechnungen durchlaufen denselben Validator, bevor sie festgeschrieben werden.
Seit dem 1. Januar 2025 müssen Unternehmen in Deutschland E-Rechnungen im B2B-Verkehr empfangen können; ab 2027 bzw. 2028 auch ausstellen. Für PEX habe ich beide Richtungen gebaut: Empfang mit Validierung, Archiv und DATEV-Export, Versand als XRechnung 3.0 und ZUGFeRD 2.x. Diese sechs Entscheidungen haben das Projekt getragen.
1. Der Validator ist der Schiedsrichter — nie eigene Regeln nachbauen
Die XRechnung-Regeln bestehen aus XSD-Schemas und Hunderten Schematron-Regeln (BR-DE-…, BR-CO-…, BR-CL-…), und sie ändern sich mit jeder Release. Wer sie in PHP nachprogrammiert, betreibt ab dem ersten Tag eine Kopie, die veraltet.
PEX nutzt deshalb das offizielle KoSIT-validationtool mit der offiziellen XRechnung-Konfiguration — beide auf eine exakte Version gepinnt. Der eigene Code interpretiert nur den Prüfbericht: Urteil, Fundstellen, Schweregrad. Die Regel-Inhalte bleiben Sache des Validators.
Betrieben wird er als dauerhaft laufender Hintergrundprozess auf dem Server, ein CLI-Adapter dient als Fallback. Installiert wird über ein Skript mit Prüfsummen und Smoke-Test — ohne Docker, ohne Root, ein unveränderliches Verzeichnis pro Version.
2. Ein eigenes kanonisches Modell statt XML durch die App zu reichen
E-Rechnungen kommen in zwei Syntaxen (UBL und CII), teils eingebettet in PDFs (ZUGFeRD/Factur-X). Würde jede Stelle der App — Oberfläche, Suche, DATEV-Export, Webhooks — direkt XML lesen, gäbe es überall zwei Code-Pfade.
Stattdessen wird jedes Dokument in ein syntaxneutrales Modell nach EN 16931 überführt. Zum Lesen nutze ich bewährte Bibliotheken (josemmo/einvoicing für UBL, horstoeko/zugferd für CII, die PDF-Extraktion und Spezialfelder) — aber das Zielmodell ist mein eigenes. Damit bleibt jede Bibliothek austauschbar.
3. Das Original ist das Rechtsdokument
Das kanonische Modell ist das Arbeits-Dokument. Rechtlich zählt die empfangene Datei, Byte für Byte. Sie wird nie aus dem Modell neu erzeugt, nie „bereinigt“, nie überschrieben.
Daraus folgt auch die Idempotenz: Beim Eingang wird ein SHA-256 über die Rohdaten gebildet, und ein Unique-Constraint in der Datenbank — nicht eine Prüfung im Anwendungscode — entscheidet, ob ein Dokument schon existiert. Doppelte Uploads sind normale Wiederholungen, kein Fehler: Sie bekommen das bestehende Dokument zurück.
4. GoBD: Archiv, das sich nicht ändern lässt, und ein Journal, das es beweist
Originale, Prüfberichte und Modell-Snapshots liegen in einem eigenen S3-Bucket mit Object Lock im Compliance-Modus — Aufbewahrungsfrist pro Objekt, standardmäßig zehn Jahre. Nicht einmal ein Admin-Zugang kann sie vorher löschen.
Jeder Verarbeitungsschritt landet zusätzlich in einem Append-only-Journal, dessen Einträge hash-verkettet sind:
// hash = sha256(vorheriger_hash ∥ kanonisches_json(event))
public static function hash(string $prevHash, array $fields): string
{
return hash('sha256', $prevHash.self::canonicalJson($fields));
}
Das kanonische JSON ist rekursiv nach Schlüsseln sortiert, damit der Hash nicht von der Reihenfolge im Array abhängt. Ein Artisan-Befehl verifiziert die gesamte Kette — wird ein Eintrag nachträglich verändert, bricht sie an genau dieser Stelle.
5. Eine Pipeline aus kleinen, wiederholbaren Jobs
Parsen, Validieren, Normalisieren, Archivieren: vier Schritte, vier eigene Queue-Jobs auf einer eigenen Horizon-Queue. Jeder Job ist idempotent und kann einzeln neu angestoßen werden. Ein langsamer Validator-Lauf blockiert so weder andere Dokumente noch den Rest der App.
Der Status eines Dokuments ist ein PHP-Enum mit expliziter Übergangstabelle — illegale Übergänge werfen eine Exception. Dokumente, die nicht verarbeitet werden können, landen in einer Quarantäne-Ansicht mit dem genauen Grund, statt still zu verschwinden.
6. Ausgehende Rechnungen: korrigieren verboten, prüfen Pflicht
Für den Versand mappt PEX die eigene Rechnung auf dasselbe kanonische Modell und erzeugt daraus XRechnung (UBL) oder ein ZUGFeRD-PDF/A-3. Zwei Regeln:
- Nichts stillschweigend korrigieren. Eine Position mit Steuerkategorie
Sbei 0 % wird als genau das übertragen. Die Gesamtsummen werden aus den Positionen in ganzzahligen Cent neu berechnet und mit den gespeicherten Werten verglichen; Abweichungen werden gemeldet, nie überschrieben. - Vor der Festschreibung prüfen. Die erzeugte Datei läuft durch denselben KoSIT-Validator. Fehler sieht der Nutzer, bevor die Rechnung den Empfänger erreicht — nicht als Rückläufer aus dessen Buchhaltung.
Architektur: ein eigenes Paket mit klaren Grenzen
Der gesamte E-Rechnungs-Teil ist ein eigenes Composer-Paket im Repository, aufgebaut in vier Schichten (Domain, Application, Infrastructure, Laravel). Domain und Application sind reines PHP; ein Pest-Architekturtest stellt sicher, dass dort weder Illuminate noch Symfony importiert werden. Das Paket hat rund 180 eigene Tests, dazu Vertragstests gegen den echten KoSIT-Validator in CI.
Neue XRechnung-Versionen sind so eine Konfigurationsfrage: neue Validator-Konfiguration, ein Eintrag in der Ruleset-Liste — kein neuer Code. Jede Prüfung speichert, gegen welche Regelversion sie lief.
Fazit
E-Rechnung wirkt wie ein Format-Thema, ist aber ein Nachweis-Thema: Welche Datei kam an, wer hat sie wann geprüft, gegen welche Regeln, und ist seitdem etwas verändert worden? Wer diese Fragen in der Architektur beantwortet, statt in Prozessdokumenten, hat die eigentliche Arbeit erledigt.
Sie brauchen das für Ihre eigene Laravel-Anwendung? Genau dafür gibt es mein Festpreis-Paket „E-Rechnung aus individueller PHP/Laravel-B2B-Software“. Das Gesamtsystem zeigt die PEX Case Study.