Avec l'obligation de recevoir des factures électroniques pour toutes les entreprises dès le 1er septembre 2026 (impots.gouv.fr), savoir lire un Factur-X côté réception devient aussi important que savoir le générer. Un développeur doit pouvoir ouvrir un PDF/A-3, en extraire le XML embarqué et récupérer les données structurées pour automatiser le rapprochement comptable.
Ce guide technique, neutre et sans promotion d'API, montre comment lire un Factur-X en PHP : extraction du fichier factur-x.xml, chargement avec horstoeko/zugferd, lecture de l'en-tête, des totaux et des lignes, puis normalisation vers un objet métier. Pour la génération, voyez d'abord notre tuto Factur-X en PHP / Laravel.
L'essentiel en 5 points
- Le XML est embarqué dans le PDF/A-3 sous le nom
factur-x.xml(ouzugferd-invoice.xml,xrechnung.xml). - 2 lecteurs :
ZugferdDocumentPdfReaderpour un PDF,ZugferdDocumentReaderpour un XML brut. - Lecture par getters : en-tête, totaux, puis itération des lignes via
firstDocumentPosition()/nextDocumentPosition(). - Profils légers (MINIMUM, BASIC WL) ne contiennent pas le détail des lignes : prévoyez ce cas.
- Bonne pratique : normaliser vers un DTO métier et contrôler la cohérence PDF visuel / XML.
Que contient réellement un fichier Factur-X reçu ?
Un Factur-X reçu est un PDF/A-3. À l'intérieur, un fichier embarqué (embedded file selon la norme PDF) contient le XML structuré au format Cross Industry Invoice (CII) de l'UN/CEFACT. Ce fichier porte une relation d'association AFRelationship et un nom normalisé. Selon l'émetteur, vous rencontrerez trois noms.
| Nom du fichier embarqué | Format | Origine |
|---|---|---|
| factur-x.xml | CII | Factur-X (France) |
| zugferd-invoice.xml | CII | ZUGFeRD (Allemagne, anciennes versions) |
| xrechnung.xml | CII ou UBL | Secteur public allemand |
Votre code de réception doit donc accepter ces variantes. La bonne nouvelle : horstoeko/zugferd gère ces trois noms et devine automatiquement le profil. Inutile de coder la détection à la main.
Comment extraire le XML d'un PDF Factur-X en PHP ?
La voie la plus simple est ZugferdDocumentPdfReader. Il ouvre le PDF, localise le fichier embarqué, l'extrait et retourne un lecteur prêt à interroger. La méthode readAndGuessFromFile() devine le profil dans la foulée.
use horstoeko\zugferd\ZugferdDocumentPdfReader; // Ouvre le PDF/A-3, extrait factur-x.xml, devine le profil $document = ZugferdDocumentPdfReader::readAndGuessFromFile( __DIR__ . "/facture-recue-facturx.pdf" ); if ($document === null) { throw new \RuntimeException("Aucun XML Factur-X trouvé dans ce PDF."); } // $document est un ZugferdDocumentReader prêt à interroger
Si vous recevez directement un XML CII brut (sans PDF, fréquent quand l'échange passe par une plateforme), chargez-le avec ZugferdDocumentReader. La lecture des champs est ensuite strictement identique.
use horstoeko\zugferd\ZugferdDocumentReader; $document = ZugferdDocumentReader::readAndGuessFromFile( __DIR__ . "/facture-recue.xml" );
Comment lire l'en-tête et les totaux d'un Factur-X ?
Les getters de la librairie utilisent un passage par référence : on déclare les variables, on les passe, elles sont remplies. On récupère ainsi le numéro, le type, la date, la devise, puis les sommes (HT, TVA, TTC, net à payer).
// En-tête $document->getDocumentInformation( $documentNo, $documentType, $documentDate, $invoiceCurrency, $taxCurrency, $documentName, $documentLanguage, $effectiveSpecifiedPeriod ); // Vendeur et acheteur $document->getDocumentSeller($sellerName, $sellerIds, $sellerDescription); $document->getDocumentBuyer($buyerName, $buyerIds, $buyerDescription); // Totaux : HT, TVA, TTC, net à payer... $document->getDocumentSummation( $grandTotalAmount, // TTC $duePayableAmount, // net à payer $lineTotalAmount, // HT $chargeTotalAmount, $allowanceTotalAmount, $taxBasisTotalAmount, $taxTotalAmount, // total TVA $roundingAmount, $totalPrepaidAmount ); echo "Facture {$documentNo} — TTC {$grandTotalAmount} {$invoiceCurrency}";
Comment itérer les lignes d'une facture Factur-X ?
Les lignes ne sont présentes que dans les profils qui les portent (BASIC, EN 16931, EXTENDED). On se positionne sur la première ligne, on lit, puis on avance tant qu'il en reste.
$lignes = []; if ($document->firstDocumentPosition()) { do { $document->getDocumentPositionProductDetails( $name, $description, $sellerAssignedId, $buyerAssignedId, $globalIdType, $globalId ); $document->getDocumentPositionQuantity($quantity, $unitCode); $document->getDocumentPositionLineSummation($lineTotal, $totalAllowance, $totalCharge); $lignes[] = [ "libelle" => $name, "quantite" => $quantity, "total_ht" => $lineTotal, ]; } while ($document->nextDocumentPosition()); }
Attention aux profils légers : sur un profil MINIMUM ou BASIC WL, firstDocumentPosition() renvoie false car le XML ne contient aucune ligne. Votre code doit gérer ce cas sans planter et se rabattre sur les totaux d'en-tête.
Comment normaliser vers un objet métier ?
En production, on ne disperse pas les getters dans toute l'application. On encapsule la lecture dans un service qui retourne un DTO (Data Transfer Object) propre, consommé par le reste du code. Cela isole la dépendance à la librairie et facilite les tests.
final class FacturXReader { public function read(string $pdfPath): array { $doc = ZugferdDocumentPdfReader::readAndGuessFromFile($pdfPath); if ($doc === null) { throw new \RuntimeException("Factur-X invalide."); } $doc->getDocumentInformation($no, $type, $date, $cur, ...); $doc->getDocumentSummation($ttc, $due, $ht, $c, $a, $base, $tva, ...); return [ "numero" => $no, "date" => $date, "devise" => $cur, "total_ht"=> $ht, "total_tva" => $tva, "total_ttc" => $ttc, ]; } }
Et sans la librairie : extraction manuelle du XML
Si vous voulez comprendre le mécanisme bas niveau ou éviter la dépendance, vous pouvez extraire le fichier embarqué avec un parseur PDF (smalot/pdfparser ou setasign/fpdi) puis lire le XML avec DOMDocument et XPath en déclarant les namespaces CII. C'est instructif, mais vous réimplémentez la gestion des profils et des chemins — rarement rentable en production.
- Localiser l'EmbeddedFile dans la structure
/Names /EmbeddedFilesdu PDF. - Décompresser le flux du fichier (souvent FlateDecode) pour obtenir le XML.
- Charger en DOMDocument et enregistrer les namespaces
rsm,ram,udtdu standard CII. - Interroger en XPath les noeuds de montant, TVA et lignes selon le profil.
Quelles erreurs éviter en lecture Factur-X ?
La réception automatisée est sensible : une facture mal lue, c'est un rapprochement comptable faux. Quatre pièges reviennent systématiquement.
| Erreur | Conséquence | Parade |
|---|---|---|
| Supposer un profil EN 16931 | Crash sur facture MINIMUM | Lire le profil détecté |
| Ignorer la devise | Montants en mauvaise unité | Toujours lire $invoiceCurrency |
| Confiance au PDF visuel | Données non automatisables | Traiter le XML, contrôler l'écart |
| Pas de gestion des avoirs | Montants négatifs ignorés | Tester le type 381 (avoir) |
Comment brancher la lecture sur un logiciel existant ?
Lire le fichier n'est qu'un début. La valeur réside dans le rapprochement : associer la facture reçue à un bon de commande, déclencher un workflow de validation, alimenter la compta. C'est un travail d'intégration sur le logiciel métier. Sur ce volet, lisez notre guide connecter un logiciel métier à une PDP sans devenir PDP, et côté éditeurs, solution compatible vs plateforme agréée.
FAQ : lire et extraire un Factur-X en PHP
Comment extraire le XML d'un PDF Factur-X en PHP ?
Le XML est un fichier embarqué du PDF/A-3 nommé factur-x.xml. Avec horstoeko/zugferd, ZugferdDocumentPdfReader l'extrait automatiquement et devine le profil. On peut aussi l'extraire manuellement via les EmbeddedFiles du PDF, mais la librairie gère tous les noms (factur-x.xml, zugferd-invoice.xml, xrechnung.xml).
Quelle différence entre lire un PDF Factur-X et un XML CII brut ?
Un PDF Factur-X contient le XML embarqué : il faut d'abord l'extraire. Un XML CII brut se charge directement. horstoeko/zugferd fournit ZugferdDocumentPdfReader pour le PDF et ZugferdDocumentReader pour le XML seul. La lecture des champs est ensuite identique.
Comment récupérer les lignes d'une facture Factur-X ?
Après chargement, on appelle firstDocumentPosition() puis on boucle avec nextDocumentPosition(). getDocumentPositionProductDetails() donne le libellé, getDocumentPositionQuantity() la quantité, getDocumentPositionLineSummation() le montant. Les profils MINIMUM et BASIC WL ne contiennent pas de lignes.
Peut-on lire un Factur-X sans horstoeko/zugferd ?
Oui. On extrait le fichier embarqué avec smalot/pdfparser ou FPDI puis on parse le XML avec DOMDocument ou SimpleXML selon les namespaces CII. Mais on réinvente la gestion des profils et des chemins XPath. La librairie reste plus fiable et plus rapide à mettre en oeuvre.
Faut-il vérifier la cohérence entre le PDF visuel et le XML ?
Oui. L'administration impose la cohérence entre données visibles et données structurées. Côté réception, on se fie au XML pour l'automatisation, mais un contrôle d'écart (TTC, numéro, date) entre le rendu et le XML détecte les fichiers mal formés.
Comment intégrer la lecture Factur-X dans un logiciel existant ?
On encapsule la lecture dans un service qui retourne un DTO normalisé consommé par le reste de l'application. C'est le type de mission d'intégration que réalise ComAll Agency : brancher la réception et le rapprochement des e-factures sur un logiciel métier sur-mesure. Devis gratuit sous 24h.
Conclusion : maîtriser la réception avant l'échéance
Lire un Factur-X en PHP se résume à trois gestes : extraire le XML embarqué, charger le document, interroger les getters. La librairie horstoeko/zugferd absorbe la complexité des profils et des variantes de nommage. Le reste — gestion des profils légers, des avoirs, normalisation métier — relève d'un code défensif et bien testé.
Si vous devez fiabiliser la réception d'e-factures sur un logiciel existant avant le 1er septembre 2026, nous intervenons en mission d'intégration. Demandez un devis gratuit sous 24h.
Brancher la réception d'e-factures sur votre logiciel ?
Mission d'intégration : lecture, rapprochement et raccordement PDP sur-mesure. Devis sous 24h.
Demander un devis gratuit →