E-invoicing

A typed UBL document, XML tools, certificates and signatures: each one works alone, and they work together.

A business document, from code to signed XML

An electronic invoice, a quotation or a despatch advice is an XML with a strict schema, and often a signature. Derafu UBL gives you every UBL document as typed classes, Derafu XML works with the XML, and Derafu Certificate and Derafu Signature sign it.

The packages are generic on purpose: they do not carry the rules of a country or of a network. Those rules belong in a package built on top, and the one for Peppol, Derafu Peppol, is planned.

How it works

Four steps, from an object to a signed document.

Build the document

Every document type of UBL 2.1 to 2.5 is a class, with typed setters. Or start from an array.

Write it, and check it

As XML, JSON or YAML, and validated against the official schemas.

Work with the XML

Convert it to arrays and back, read it with XPath, validate it with an XSD and canonicalize it.

Sign it

Load a certificate and sign the whole document, or one element, with an XML digital signature.

Four packages, one flow

Each one is installed on its own.

From code to a signed document

One short example for each step.

// Every UBL type is a class. This is a Quotation of UBL 2.1 (the V21 namespace),
// built with typed setters.
$quotation = (new Quotation())
    ->setId((new Identifier())->setValue('Q-001'))
    ->setIssueDate(new DateTimeImmutable('2026-01-15'))
    ->setSellerSupplierParty(
        (new SupplierParty())->setParty(
            (new Party())->setPartyNames([
                (new PartyName())->setName((new Text())->setValue('Acme Corp')),
            ])
        )
    )
    ->setQuotedMonetaryTotal(
        (new MonetaryTotal())->setPayableAmount(
            (new Amount())->setValue('100')->setCurrencyID('EUR')
        )
    );
    // ...and the lines of the quotation, with setQuotationLines([...]).

// Or start from a plain array, in the shape that the JSON and YAML formats carry:
$quotation = Quotation::fromArray([
    'ID' => ['_' => 'Q-001'],
    'IssueDate' => ['_' => '2026-01-15'],
    // ...
]);
// The same document, in three formats.
$serializer = new Serializer();
$xml  = $serializer->serialize($quotation, 'xml');    // the official UBL XML
$json = $serializer->serialize($quotation, 'json');   // the "UBL JSON Alternative Representation" of OASIS
$yaml = $serializer->serialize($quotation, 'yaml');   // the same data, in YAML

// And back. The XML does not say which UBL version it is, so you choose it
// (the default is 2.1):
$deserializer = new Deserializer();
$quotation = $deserializer->deserialize($xml, 'xml', ['version' => '2.3']);

// Validate it while it is read, against the official schemas:
$quotation = $deserializer->deserialize($json, 'json', ['validate' => true]);

// Or validate an XML on its own. If it is not valid, it throws a ValidationException
// with the errors that libxml reported.
try {
    (new XmlSchemaValidator())->validate($xml);
} catch (ValidationException $e) {
    foreach ($e->getErrors() as $error) {
        printf("Line %d: %s\n", $error->line, trim($error->message));
    }
}
// XML to a PHP array and back. Attributes go in "@attributes" and the text in "@value".
$document = new XmlDocument();
$document->loadXml($xml);

$array = (new XmlDecoder())->decode($document);
$xml   = (new XmlEncoder())->encode($array)->saveXml();

// Or read the document without converting it, with XPath and with types.
// The text is '0021': the typed reads say what you want it to be.
$reader = $document->getReader();
$reader->requireString('//seller/name');   // 'Seller name'
$reader->getAsInt('//info/number');        // 21
$reader->getString('//info/number');       // '0021'

// The package also validates a document with an XSD, with the errors of libxml
// explained and translated, and gives the canonical XML (C14N).
$loader = new CertificateLoader();
$certificate = $loader->loadFromFile('/path/to/certificate.p12', 'password');

$certificate->getId();               // The identity, for example "12345678-9".
$certificate->getName();
$certificate->getEmail();
$certificate->isActive();            // It is not expired.
$certificate->getExpirationDays();

// Check that it has an ID, in the format that you expect (a regular expression).
// If it does not, it throws a CertificateException that says why.
(new CertificateValidator('/^\d+-[\dK]$/'))->validate($certificate);

// For tests, you can make a self-signed certificate instead of asking for one.
$selfSigned = new SelfSignedCertificate();
$certificate = $loader->loadFromArray($selfSigned->toArray());
// Sign the whole XML, or only the element that has a given ID.
$signedXml = $signatureService->signXml($xml, $certificate);
$signedXml = $signatureService->signXml($xml, $certificate, 'elementId');

// Validate a signed XML. You get one result for each signature in the document, and
// you check each one. It throws a SignatureException only if the XML is malformed
// or if it has no signature at all.
foreach ($signatureService->validateXml($signedXml) as $signature) {
    if (!$signature->isValid()) {
        echo $signature->getError()?->getMessage();
    }
}

// The XML signature is an enveloped signature, with canonical XML 1.0, RSA-SHA1
// and a SHA1 digest. The certificate goes inside the signature.

// Plain data can be signed too, with any algorithm that OpenSSL offers.
$signature = $signatureService->sign($data, $privateKey, OPENSSL_ALGO_SHA256);
$valid = $signatureService->validate($data, $signature, $publicKey, OPENSSL_ALGO_SHA256);

Frequently asked questions

What it does, and what it leaves to you.

Not in Derafu UBL, on purpose. It knows nothing about a fixed seller, a tax rate, a currency or an e-invoicing profile. That logic belongs in a package built on top of it, for the network or the country that you need. The one for Peppol, Derafu Peppol, is planned and will use Derafu UBL as its base.

The one that you need, from 2.1 to 2.5: each has its own classes. The XML of a document does not say its version, so you choose it, and the default is 2.1, the version of Peppol BIS Billing 3.0. UBL versions only add, so what is valid in one is valid in the later ones. JSON and YAML can be validated against the official schemas only for 2.1, 2.2 and 2.3, because OASIS has not published JSON schemas for 2.4 and 2.5.

Yes. The XML is read and written with xterr/php-ubl, and the classes are generated from the official XSD with the generator of that project. Derafu UBL includes fixes for some known defects of it, as a script that patches it in vendor/. Composer does not run the scripts of a dependency, so your project has to add that script to its own composer.json; otherwise the original behavior stays, without a message. The documentation of UBL explains how.

The XML signature is RSA-SHA1 with a SHA1 digest, as an enveloped signature with canonical XML 1.0. If the authority that you work with asks for another algorithm in the XML signature, this package does not produce it. XAdES and XML encryption are not included. Signing plain data does accept any algorithm of OpenSSL.

Yes. UBL does not need the others. Signature uses Certificate and XML, because it signs XML documents with a certificate.

PHP 8.5 or later, with the openssl and mbstring extensions for certificates and signatures. YAML and the validation of JSON need symfony/yaml and opis/json-schema, which you add only if you use them. The Derafu packages are published as dev-main for now.

Where it fits

Other good tools exist. Choose by how you work.

Approach Examples Choose it when Derafu
An invoice for a profile num-num/ubl-invoice, josemmo/einvoicing You need a ready invoice for Peppol or EN 16931, with its rules. A generic model of every UBL document, without profile rules. You build the rules on top.
A library for one tax authority greenter (Peru), facturae-php (Spain) You issue in that country and want its rules and its signature. Generic pieces. The profile of a country is yours to write.
XML security robrichards/xmlseclibs You need more algorithms in the XML signature, or XML encryption. A smaller XML signature, typed and with translatable errors, made to work with Derafu XML.
Public key infrastructure sop/x509, spomky-labs/pki-framework You need a full PKI: ASN.1 and the validation of a certification path. A layer over OpenSSL for the certificate of a person or a company: PKCS#12, its identity, its expiration and self-signed ones for tests.

Install it

Install only the packages that you use.

# The Derafu packages are published as dev-main for now, so your composer.json needs:
#   "minimum-stability": "dev", "prefer-stable": true
composer require derafu/ubl
composer require derafu/xml
composer require derafu/certificate derafu/signature

# Optional for UBL: the YAML format and the validation of JSON and YAML.
composer require symfony/yaml opis/json-schema

# Derafu UBL also asks for a script in the composer.json of your project:
# see its documentation.

Next: the UBL, XML, Certificate and Signature documentation.

Ready to build your first document?

Start with the UBL documentation: the document types, the versions and the formats.