daniel-jorg-schuppelius/php-erechnung-toolkit

PHP library for creating, parsing and validating electronic invoices (XRechnung, ZUGFeRD/Factur-X) according to EN 16931.

Maintainers

Package info

github.com/Daniel-Jorg-Schuppelius/php-erechnung-toolkit

Language:XSLT

pkg:composer/daniel-jorg-schuppelius/php-erechnung-toolkit

Transparency log

Statistics

Installs: 2 872

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.13 2026-08-26 15:19 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

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

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 (java on PATH) — availability is checked through the Common-Toolkit executable configuration; the call runs via Java::execute()
  • The KoSIT validator jar — configured as a javaExecutables entry in config/erechnung_executables.json (default tools/kosit/validator.jar). Point path at your own jar there, or override per call with an explicit path or the KOSIT_VALIDATOR_JAR env 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