daniel-jorg-schuppelius / php-erechnung-toolkit
PHP library for creating, parsing and validating electronic invoices (XRechnung, ZUGFeRD/Factur-X) according to EN 16931.
Package info
github.com/Daniel-Jorg-Schuppelius/php-erechnung-toolkit
Language:XSLT
pkg:composer/daniel-jorg-schuppelius/php-erechnung-toolkit
Requires
- php: >=8.2 <8.6
- ext-dom: *
- ext-libxml: *
- dschuppelius/php-common-toolkit: ^1.36 || ^2.0
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
Requires (Dev)
- daniel-jorg-schuppelius/php-pdf-toolkit: ^0.8.0
- laravel/pint: ^1.27
- nyholm/psr7: ^1.8
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.3
Suggests
- ext-openssl: Required to read certificate details of Peppol SMP endpoints (SmpEndpoint::getCertificateInfo())
- daniel-jorg-schuppelius/php-pdf-toolkit: Required for ZUGFeRD/Factur-X PDF generation with embedded XML invoices
- psr/http-client-implementation: A PSR-18 client (e.g. guzzlehttp/guzzle) for Peppol SMP lookups via Psr18SmpClient
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v0.15.1
- v0.15.0
- v0.14.1
- v0.14
- v0.13
- v0.12
- v0.11.1
- v0.11
- v0.10.5
- v0.10.4
- v0.10.3
- v0.10.2
- v0.10.1
- v0.10
- v0.9.2
- v0.9.1
- v0.9
- v0.8
- v0.7.1
- v0.7
- v0.6
- v0.5.1
- v0.5
- v0.4.4
- v0.4.3
- v0.4.2
- v0.4.1
- v0.4
- v0.3.1
- v0.3
- v0.2
- v0.1.12
- v0.1.11.1
- v0.1.11
- v0.1.10
- v0.1.9
- v0.1.8
- v0.1.7
- v0.1.6
- v0.1.5
- v0.1.4
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1
This package is auto-updated.
Last update: 2026-09-20 10:55:50 UTC
README
PHP library for creating, parsing and validating electronic invoices (XRechnung, ZUGFeRD/Factur-X) according to EN 16931.
Features
- XRechnung: German e-invoicing standard for public sector
- ZUGFeRD 2.x / Factur-X: Hybrid PDF/A-3 with embedded XML
- EN 16931: European e-invoicing standard
- XBestellung / Peppol BIS Order: UBL purchase orders (docs)
- Order-X: CII purchase orders + hybrid PDF/A-3 (order-side Factur-X)
- Despatch Advice: UBL delivery notes (Peppol BIS Despatch Advice)
- Peppol: participant/document identifiers, SBDH envelope, SML/SMP lookup, BIS core rules (no AS4 stack)
- UBL 2.1: Universal Business Language
- UN/CEFACT CII D16B: Cross Industry Invoice
Beyond E-Invoicing
The toolkit also covers the exchange formats that surround an invoice in the construction and wholesale trades. None of these were listed here before, although parsers and generators have shipped for some time:
- GAEB: DA XML 3.x (
GaebDaXmlParser/GaebDaXmlGenerator), the older families GAEB 90 (d8x) and GAEB 2000 (p8x), plus quantity/costing phases (GaebCostingParser) — bills of quantities, tenders and awards (docs) - DATANORM 4/5:
DatanormParser/DatanormGeneratorfor article and price catalogues, including the DA-11 price-update variant (Da11Parser/Da11Generator) - BMEcat:
BmecatParserfor supplier catalogues - UGL:
UglParser,UglInvoiceParserandUglGenerator— the German trade data exchange used between wholesalers and craft businesses - openTRANS:
OpenTransOrderParser/OpenTransOrderGeneratorfor orders and order responses
Installation
composer require dschuppelius/php-erechnung-toolkit
Requirements
- PHP >= 8.2
- ext-dom
- ext-libxml
- dschuppelius/php-common-toolkit ^1.0
Quick Start
Creating an Invoice
use ERechnungToolkit\Builders\ERechnungDocumentBuilder; use DateTimeImmutable; // Create a basic invoice $invoice = ERechnungDocumentBuilder::create('INV-2026-001') ->withIssueDate(new DateTimeImmutable()) ->withSeller('Muster GmbH', 'DE123456789') ->withSellerAddress('Musterstraße 1', '12345', 'Berlin') ->withBuyer('Kunde AG') ->withBuyerAddress('Kundenweg 2', '54321', 'München') ->addLine('Beratungsleistung', 10, 150.00) ->build(); // Generate UBL XML (XRechnung) $ublXml = $invoice->toUblXml(); // Generate CII XML (ZUGFeRD) $ciiXml = $invoice->toCiiXml();
Creating an XRechnung
$leitwegId = '04011000-12345-67'; $xrechnung = ERechnungDocumentBuilder::xrechnung('XR-2026-001', $leitwegId) ->withIssueDate(new DateTimeImmutable()) ->withSeller('Verkäufer GmbH', 'DE123456789') ->withSellerAddress('Verkäuferstraße 1', '10115', 'Berlin') ->withSellerEndpoint('seller@example.com', 'EM') ->withBuyer('Öffentliche Verwaltung') ->withBuyerAddress('Amtsweg 1', '80333', 'München') ->withBuyerLeitwegId($leitwegId) ->addLine('Dienstleistung', 1, 1000.00) ->build();
Parsing an Invoice
use ERechnungToolkit\Parsers\ERechnungParser; $parser = new ERechnungParser(); // Parse from XML string $document = $parser->parse($xmlContent); // Parse from file $document = $parser->parseFile('/path/to/invoice.xml'); // Access invoice data echo $document->getId(); echo $document->getSeller()->getName(); foreach ($document->getLines() as $line) { echo $line->getItemName() . ': ' . $line->getNetAmount(); }
Supported Profiles
| Profile | Description |
|---|---|
| MINIMUM | ZUGFeRD 2.x MINIMUM |
| BASIC_WL | ZUGFeRD 2.x BASIC WL |
| BASIC | ZUGFeRD 2.x BASIC |
| EN16931 | EN 16931 (COMFORT) |
| EXTENDED | ZUGFeRD 2.x EXTENDED |
| XRECHNUNG | XRechnung 3.0 |
| XRECHNUNG_EXTENSION | XRechnung 3.0 Extension |
Order Profiles (XBestellung / Peppol BIS Order)
| Profile | Description |
|---|---|
| PEPPOL_ORDER_ONLY | Peppol BIS Order only 3 (international) |
| XBESTELLUNG | XBestellung v1.0 (German CIUS) |
Creating an Order (XBestellung)
use ERechnungToolkit\Builders\OrderBuilder; use ERechnungToolkit\Enums\UnitCode; $order = OrderBuilder::xbestellung('ORD-2026-001') ->withBuyer('Stadt Musterstadt') // ordering party (sender) ->withBuyerAddress('Rathausplatz 1', '12345', 'Musterstadt') ->withBuyerEndpoint('04011000-12345-67', '0204') // Leitweg-ID ->withSeller('Lieferant GmbH', 'DE123456789') // supplier (recipient) ->withSellerAddress('Lieferweg 2', '54321', 'Lieferstadt') ->withSellerEndpoint('DE123456789', '9930') ->addLine('Bürostuhl', 5, 120.00, UnitCode::PIECE, 'ART-4711') ->build(); $xml = $order->toUblXml(); // Parsing back use ERechnungToolkit\Parsers\OrderParser; $parsed = (new OrderParser)->parse($xml);
The bundled KoSIT validator is scenario-driven and can validate orders once the official XBestellung validation artifacts are added to
data/kosit/scenarios.xml. See docs/XBestellung for details.
ZUGFeRD/Factur-X PDF Generation
Generate PDF/A-3 invoices with embedded XML for automated processing. Requires dschuppelius/php-pdf-toolkit:
composer require daniel-jorg-schuppelius/php-pdf-toolkit
use ERechnungToolkit\Builders\ERechnungDocumentBuilder; use ERechnungToolkit\Generators\ZugferdPdfGenerator; // Create invoice $invoice = ERechnungDocumentBuilder::zugferd('ZF-2026-001') ->withIssueDate(new DateTimeImmutable()) ->withSeller('Verkäufer GmbH', 'DE123456789') ->withSellerAddress('Musterstraße 1', '12345', 'Berlin') ->withSellerBankAccount('DE89370400440532013000', 'COBADEFFXXX') ->withBuyer('Käufer AG') ->withBuyerAddress('Kundenweg 2', '54321', 'München') ->addLine('Beratung', 10, 150.00) ->build(); // Generate ZUGFeRD PDF with embedded XML $pdfGenerator = new ZugferdPdfGenerator(); $pdfGenerator->generateToFile($invoice, '/path/to/invoice.pdf'); // Or get PDF as bytes $pdfBytes = $pdfGenerator->generate($invoice); // With custom HTML template $customHtml = '<html>...</html>'; $pdfBytes = $pdfGenerator->generate($invoice, $customHtml);
The generated PDF:
- Is PDF/A-3 compliant for long-term archiving
- Contains the embedded XML invoice (CII format)
- Can be processed automatically by accounting software
- Is visually readable as a normal PDF invoice
Validation
Two layers:
XSD schema (pure PHP, no Java) — UblSchemaValidator validates UBL documents
(Invoice, CreditNote, Order, DespatchAdvice) against the bundled official OASIS
UBL 2.1 schemas via libxml. Catches structure / data type / element-order errors.
use ERechnungToolkit\Validators\UblSchemaValidator; $errors = (new UblSchemaValidator)->validate($document->toUblXml()); // [] = valid
CiiSchemaValidator does the same for UN/CEFACT CII (rsm:CrossIndustryInvoice,
XRechnung-CII and ZUGFeRD/Factur-X) against the bundled CII D16B schema. D16B is
the superset of the Factur-X profile schemas, so a narrow profile (MINIMUM,
BASIC WL) can still carry elements it does not allow.
use ERechnungToolkit\Validators\CiiSchemaValidator; $errors = (new CiiSchemaValidator)->validate($document->toCiiXml()); // [] = valid
XRechnung in both syntaxes. generateUbl() and generateCii() produce an
XRechnung that the KoSIT validator accepts without warnings — including credit
notes (BT-25), line and document allowances and tax-exempt invoices
(tests/Generators/XRechnungConformanceTest.php). UBL stays the common choice;
use CII when a recipient asks for it.
Small businesses without a VAT ID (§ 19 UStG). EN 16931 rule BR-CO-26 requires the seller identifier (BT-29), the legal registration identifier (BT-30) or the VAT identifier (BT-31). A tax number alone (BT-32) is not enough — KoSIT rejects the invoice. Pass the tax number as seller identifier as well:
$document = ERechnungDocumentBuilder::xrechnung('XR-2026-0818', $leitwegId) ->withSeller('Kleinbetrieb Anna Schmidt', '', '201/987/65432') // no VAT ID, tax number BT-32 ->withSellerIdentifier('201/987/65432') // BT-29 ->withTaxExemptionReason('Kein Ausweis von Umsatzsteuer, da Kleinunternehmer gemäß § 19 UStG') // … ->build();
Business rules (EN16931 / XRechnung) — validate UBL/CII against XML Schema, the
EN16931 Schematron rules and the XRechnung CIUS using the official KoSIT validator.
Both the validator jar (tools/kosit/validator.jar) and its configuration
(scenarios + schemas, data/kosit/) ship with this package, so validation works
out of the box.
Because the Schematron rules are distributed as XSLT 2.0 (not executable by PHP's XSLT 1.0 engine), the validator runs as an external Java process.
Validation Requirements
- A Java runtime (
javaonPATH) — availability is checked through the Common-Toolkit executable configuration; the call runs viaJava::execute() - The KoSIT validator jar — configured as a
javaExecutablesentry inconfig/erechnung_executables.json(defaulttools/kosit/validator.jar). Pointpathat your own jar there, or override per call with an explicit path or theKOSIT_VALIDATOR_JARenv var
use ERechnungToolkit\Validators\KositValidator; $validator = new KositValidator(); // uses the bundled jar + Java from PATH if ($validator->isAvailable()) { $result = $validator->validate($ublXml); // or ->validateFile('/path/to/invoice.xml') if ($result->isValid()) { echo 'Konform: ' . $result->getScenarioName(); } else { foreach ($result->getErrors() as $error) { echo $error->getCode() . ': ' . $error->getText() . PHP_EOL; } } }
ValidationResult exposes the conformance verdict (isValid()), the
accept/reject recommendation (isAccepted()), all rule messages by severity
(getErrors(), getWarnings(), getMessages()) and the raw KoSIT report
(getRawReport()).
Peppol
Format-neutral building blocks for the Peppol network — identifiers, envelope,
participant lookup and the BIS core rules. The toolkit deliberately does not
implement an AS4 access point: PKI, certification and operations stay with a
certified provider. AccessPointClientInterface is the seam an application
implements for its provider.
Identifiers
use ERechnungToolkit\Peppol\{DocumentTypeId, ParticipantId}; $seller = ParticipantId::germanVatId('DE123456789'); // 9930:DE123456789 $buyer = ParticipantId::leitwegId('04011000-12345-67'); // 0204:... $buyer->canonical(); // iso6523-actorid-upis::0204:04011000-12345-67 $buyer->urlEncoded(); // for SMP URLs $buyer->getIcdLabel(); // "DE:LWID (Leitweg-ID)" $documentTypeId = DocumentTypeId::peppolBisBillingInvoice(); $fromDocument = DocumentTypeId::fromUbl($invoiceXml); // derived from root element + CustomizationID
Participant identifiers are validated against the Peppol identifier policy (4-digit ICD, max. 50 characters) and compared case-insensitively.
SBDH envelope (Peppol Business Message Envelope 2.0)
use ERechnungToolkit\Peppol\Sbdh; $sbdh = Sbdh::forUbl($invoiceXml, $seller, $buyer, 'DE'); // COUNTRY_C1 = sender country $envelope = $sbdh->envelope($invoiceXml); $parsed = Sbdh::parse($envelope); // header data $payload = Sbdh::payloadOf($envelope); // the UBL document
Document type, standard, root element, syntax version and the process id
(cbc:ProfileID) are derived from the document; instance identifier defaults to
a UUID v4.
SML / SMP lookup
use ERechnungToolkit\Enums\SmlZone; use ERechnungToolkit\Peppol\Http\Psr18SmpClient; use ERechnungToolkit\Peppol\SmpLookup; // DNS name of the participant (NAPTR/BDXL: base32(sha256), lowercase, unpadded) SmpLookup::dnsName($buyer, SmlZone::PRODUCTION); // legacy CNAME scheme: "B-" + md5 SmpLookup::legacyDnsName($buyer, SmlZone::LEGACY_PRODUCTION); $lookup = new SmpLookup(new Psr18SmpClient($psr18Client, $psr17RequestFactory)); $group = $lookup->fetchServiceGroup($buyer, 'https://smp.example.org'); if ($group->supports($documentTypeId)) { $endpoint = $lookup->resolveEndpoint($buyer, $documentTypeId, 'https://smp.example.org'); $endpoint?->getUrl(); // AS4 delivery address of the receiver $endpoint?->getCertificateInfo(); // subject/issuer/validity (needs ext-openssl) }
HTTP and DNS stay outside the toolkit: SmpHttpClientInterface (PSR-18 adapter
included) and DnsNaptrResolverInterface (SystemNaptrResolver uses
dns_get_record) are injected, so lookups are testable without network access.
Not implemented: verifying the XML signature of SMP responses and Redirect
entries — both belong to the access point provider.
BIS core rules
use ERechnungToolkit\Peppol\BisValidator; $result = (new BisValidator)->validate($invoiceXml); $result = (new BisValidator)->validateEnvelope($envelope); // envelope + payload
BisValidator is a documented subset, not a conformance statement: it covers
the EN 16931 mandatory fields (BR-01..BR-16, BR-62/BR-63), the sum rules
BR-CO-10/BR-CO-15 and the Peppol rules PEPPOL-EN16931-R001/-R003/-R004/-R010/
-R020/-R053 — the rules that are decidable without Schematron. For the full rule
set inject a Schematron validator (new BisValidator($kositValidator)); its
messages are merged into the same ValidationResult.
Sending and receiving
use ERechnungToolkit\Contracts\AccessPointClientInterface; final class MyProviderClient implements AccessPointClientInterface { public function isAvailable(): bool { /* ... */ } public function send(string $sbdhEnvelopeXml): TransportReceipt { /* ... */ } public function receive(int $limit = 50): array { /* list<InboundDocument> */ } public function acknowledge(string $messageId): bool { /* ... */ } }
TransportReceipt carries the technical delivery status (PeppolTransportStatus),
message id, timestamp and the raw provider response; InboundDocument wraps a
received SBDH envelope and exposes header and payload.
License
AGPL-3.0-or-later
Author
Daniel Jörg Schuppelius - schuppelius.org
Versions
Releases are tagged in Git; git tag --sort=-v:refname lists them (latest: v0.13). There is no separate changelog file — the tags and the commit history are the record.