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(ouzugferd-invoice.xmlpour 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
- Code source et issues : github.com/atgp/factur-x
- Référence des champs CII : Les champs EN 16931 expliqués
- Générer une Factur-X depuis PHP : /blog/generer-factur-x-php/
- Testeur en ligne : /sdk/FactureX/