Une facture Factur-X est un PDF ordinaire à deux couches : la couche visuelle (lisible par un humain) et une couche données, un fichier XML au format CII (Cross Industry Invoice EN 16931) embarqué en pièce jointe dans le PDF. C'est cette seconde couche qui intéresse les logiciels de comptabilité, les vérificateurs de conformité et les pipelines d'archivage.

Cet article montre comment extraire ce XML en PHP, le parser, et l'exploiter.

🔎 Si vous construisez une facture Factur-X de zéro plutôt que de la lire, consultez le testeur disponible sur /sdk/FactureX/.


Prérequis et rappels

  • Factur-X est définie par la norme EN 16931 ; le fichier XML s'appelle factur-x.xml (ou zugferd-invoice.xml pour les anciennes factures ZUGFeRD 1.x).
  • Plusieurs profils existent : MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED. Le profil détermine quels champs XML sont obligatoires ou optionnels.
  • L'extraction du XML ne dépend pas du profil ; le parsing, lui, doit en tenir compte.

Articles connexes :


Méthode 1 — Avec la librairie atgp/factur-x

Installation

composer require atgp/factur-x

La librairie dépend de smalot/pdfparser pour accéder aux pièces jointes du PDF.

Extraire le XML

Approche via la classe Facturx :

<?php

require 'vendor/autoload.php';

use Atgp\FacturX\Facturx;

$pdfBinary = file_get_contents('/chemin/vers/facture.pdf');

$facturx = new Facturx();
$xml = $facturx->getFacturxXmlFromPdf($pdfBinary);

// $xml est une chaîne UTF-8 contenant le XML CII brut
file_put_contents('/tmp/factur-x.xml', $xml);

Approche alternative via la classe Reader :

<?php

require 'vendor/autoload.php';

use Atgp\FacturX\Reader;

$pdfBinary = file_get_contents('/chemin/vers/facture.pdf');

$reader = new Reader();
$xml = $reader->extractXML($pdfBinary);

Les deux formes renvoient la même chaîne XML brute. Choisissez selon votre style ou l'API que vous avez déjà en place.

Parser le XML obtenu

Le XML CII utilise plusieurs espaces de noms. Sans les déclarer explicitement, les requêtes XPath ne renvoient rien — c'est le piège le plus fréquent.

Les principaux préfixes à connaître :

Préfixe URI
rsm urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100
ram urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100
udt urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100

Avec SimpleXML

<?php

$sxe = simplexml_load_string($xml);

$rsm = $sxe->children('urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100');
$header = $rsm->ExchangedDocument
               ->children('urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');

$numeroFacture = (string) $header->ID;
$dateRaw       = (string) $header->IssueDateTime
                          ->children('urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100')
                          ->DateTimeString;

echo "Numéro : $numeroFacture\n";
echo "Date   : $dateRaw\n";

Avec DOMDocument et XPath (plus robuste pour les requêtes complexes)

<?php

$dom = new DOMDocument();
$dom->loadXML($xml);

$xpath = new DOMXPath($dom);

$xpath->registerNamespace('rsm', 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100');
$xpath->registerNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100');
$xpath->registerNamespace('udt', 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100');

// Numéro de facture
$numero = $xpath->evaluate('string(//rsm:ExchangedDocument/ram:ID)');

// Date d'émission
$date = $xpath->evaluate('string(//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString)');

// Total TTC
$totalTTC = $xpath->evaluate('string(//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount)');

echo "Numéro  : $numero\n";
echo "Date    : $date\n";
echo "Total   : $totalTTC €\n";

Méthode 2 — Sans librairie PHP

En ligne de commande avec pdfdetach (poppler-utils)

poppler-utils expose l'utilitaire pdfdetach qui liste et extrait les pièces jointes d'un PDF sans avoir besoin de PHP.

# Installer poppler-utils (Debian/Ubuntu)
apt-get install poppler-utils

# Lister les pièces jointes
pdfdetach -list facture.pdf

# Extraire toutes les pièces jointes dans le répertoire courant
pdfdetach -saveall facture.pdf

La commande produit factur-x.xml (ou zugferd-invoice.xml) dans le répertoire courant. Vous pouvez ensuite le lire et le parser comme n'importe quel XML.

# Vérification rapide
xmllint --format factur-x.xml | head -40

En PHP avec smalot/pdfparser seul

Si vous ne voulez pas la surcouche atgp/factur-x mais souhaitez rester dans PHP, smalot/pdfparser permet d'accéder aux objets embarqués. Le XML Factur-X est stocké dans le dictionnaire /EmbeddedFiles du PDF.

<?php

require 'vendor/autoload.php';

use Smalot\PdfParser\Parser;

$parser = new Parser();
$pdf    = $parser->parseContent(file_get_contents('facture.pdf'));

foreach ($pdf->getObjectElements() as $object) {
    $details = $object->getDetails();
    if (isset($details['F']) && str_contains(strtolower($details['F']), 'factur-x')) {
        $xml = $object->getContent();
        file_put_contents('/tmp/factur-x.xml', $xml);
        break;
    }
}

Cette approche est plus bas niveau et moins fiable que atgp/factur-x (qui encapsule correctement ces cas). Elle convient surtout si vous avez déjà smalot/pdfparser en dépendance et n'avez besoin que d'un cas simple.


Cas d'usage

Import automatique en comptabilité. Le XML CII contient les montants, la TVA décomposée, les références fournisseur et client. Un pipeline peut lire ce fichier à la réception d'un PDF et pré-remplir une écriture comptable sans saisie manuelle.

Contrôle de cohérence PDF vs XML. La couche visuelle peut diverger de la couche XML (erreur de génération, PDF retouché). Extraire le XML et comparer le total TTC avec celui affiché dans le PDF permet de détecter ces écarts avant paiement.

Archivage des données structurées. Conserver le XML à côté du PDF dans un GED (ou en base) permet d'interroger les archives par numéro, date, fournisseur, montant — sans OCR.

Validation de conformité. Après extraction, vous pouvez valider le XML contre le schéma XSD officiel ou via les règles Schematron EN 16931 (voir Factur-X : comprendre et valider).


Pièges courants

Les namespaces XML. C'est la cause n°1 des bugs silencieux. Un XPath comme //ExchangedDocument/ID ne renvoie rien si les namespaces ne sont pas enregistrés. Toujours utiliser registerNamespace() avec DOMXPath, ou naviguer avec ->children('uri') en SimpleXML.

L'encodage UTF-8. Le XML CII est en UTF-8. Si votre base ou votre buffer n'est pas en UTF-8, les caractères accentués seront corrompus.

Le profil détermine les champs disponibles. En profil MINIMUM, seuls quelques champs sont présents (numéro, date, montant TTC, identifiants des parties). Les détails de ligne n'apparaissent qu'à partir du profil EN 16931. Le profil est lisible dans //rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID.

Le nom du fichier embarqué. Les factures récentes utilisent factur-x.xml ; les anciennes ZUGFeRD 1.x utilisent zugferd-invoice.xml. La librairie gère les deux ; si vous extrayez manuellement, testez les deux noms.


FAQ

Peut-on modifier le XML et le réemballer dans le PDF ? Oui, atgp/factur-x expose aussi une API de génération. Voir Générer une Factur-X en PHP. Attention : modifier le XML sans régénérer le PDF visuel crée une incohérence entre les deux couches.

La librairie fonctionne-t-elle avec les factures ZUGFeRD allemandes ? Oui. ZUGFeRD 2.x est aligné sur Factur-X ; seul le nom du fichier embarqué peut différer. ZUGFeRD 1.x utilise un schéma différent.

getFacturxXmlFromPdf() retourne null ou lève une exception ? Si aucun XML n'est trouvé (PDF non Factur-X), la méthode peut lever une exception. Encapsulez l'appel dans un try/catch en production.

Faut-il valider le XML après extraction ? Recommandé pour détecter des factures malformées. La validation XSD et Schematron est détaillée dans Factur-X : comprendre et valider.


Pour aller plus loin