Transparence : cet article est réalisé dans le cadre d’un partenariat rémunéré avec SensioLabs, créateur de Symfony et mainteneur de GotenbergBundle. Les opinions et retours techniques exprimés restent les miens.
Introduction
Generer des PDF en PHP, c’est souvent un parcours du combattant. Entre les bibliotheques obsoletes comme wkhtmltopdf (deprecie depuis 2023), les solutions comme Dompdf qui peinent a reproduire fidellement du CSS moderne, ou encore les approches “coordonnees” ou vous placez chaque element pixel par pixel… On est loin d’une experience developpeur agreable.
Et si je vous disais qu’il existe une solution qui vous permet de generer des PDF a partir de vos templates Twig, avec un rendu pixel-perfect, le tout en quelques lignes de code ?
C’est exactement ce que propose GotenbergBundle, maintenu par SensioLabs (le createur de Symfony). Dans ce tutoriel, nous allons partir de zero et construire deux fonctionnalites concretes :
Ce que nous allons construire
- Une generation de PDF a partir d’un template Twig (facture classique)
- Une facture au format Factur-X (standard europeen PDF + XML)
Pourquoi GotenbergBundle ?
| Critere | Anciennes solutions | GotenbergBundle |
|---|---|---|
| Rendu CSS | Partiel (CSS 2.1 max) | Complet (Chromium) |
| Installation | Extensions PHP, binaires systeme | Docker uniquement |
| Maintenance | Souvent abandonnes | SensioLabs (createur de Symfony) |
| Integration Symfony | Inexistante ou bricolee | Native (Twig, Profiler, AssetMapper) |
| Formats d’entree | HTML basique | HTML, Twig, Markdown, URL, Office |
| Scalabilite | Limitee | Docker = scalable par nature |
C’est quoi Gotenberg ?
Avant de parler du bundle Symfony, comprenons ce qu’est Gotenberg lui-meme.
Gotenberg est une API conteneurisee (Docker-first) qui convertit des documents en PDF. Concretement, c’est un conteneur Docker qui embarque :
- Chromium : pour convertir du HTML, des URLs ou du Markdown en PDF (avec un rendu identique a un navigateur moderne)
- LibreOffice : pour convertir des documents Office (Word, Excel, PowerPoint) en PDF
Vous envoyez vos fichiers via une requete HTTP (multipart/form-data), et Gotenberg vous renvoie un PDF. C’est aussi simple que ca.
Votre App Symfony --> requete HTTP --> Gotenberg (Docker) --> PDF
(Twig) (multipart/form) (Chromium/LibreOffice)
Le GotenbergBundle est le pont entre Symfony et cette API. Il fournit :
- Une interface fluide (Builder Pattern) pour construire vos requetes
- L’integration avec Twig (
gotenberg_asset()pour vos CSS/images) - Un panneau dans le Symfony Profiler pour debugger vos generations
- Une gestion optimisee de la memoire (streaming des reponses)
- Un kit de test pour vos tests PHPUnit
- Le support des webhooks pour la generation asynchrone
Installation
1. Prerequis
Pour ce tutoriel, vous avez besoin de :
- PHP 8.2+ (nous utilisons PHP 8.4)
- Symfony 8.0 (fonctionne aussi avec 6.4 et 7.x)
- Docker et Docker Compose
- Le CLI Symfony (
symfony)
GotenbergBundle est garanti compatible avec toutes les versions actuellement supportees par Symfony (sauf la 5.4).
2. Creation du projet Symfony
Si vous partez de zero, creez un nouveau projet Symfony :
symfony new GotenbergDemo --webapp
cd GotenbergDemo
L’option
--webappinstalle tous les packages necessaires (Twig, Doctrine, Profiler, etc.).
3. Installation du GotenbergBundle
composer require sensiolabs/gotenberg-bundle
Grace a Symfony Flex, cette commande fait automatiquement plusieurs choses :
- Enregistre le bundle dans
config/bundles.php - Cree le fichier de configuration
config/packages/sensiolabs_gotenberg.yaml - Met a jour le
compose.yamlpour ajouter le service Gotenberg - Ajoute la variable
GOTENBERG_DSNdans.env
4. Verification du Docker Compose
Apres l’installation, votre compose.yaml devrait contenir un service Gotenberg. Si ce n’est pas le cas, ajoutez-le :
services:
# ... votre base de donnees existante ...
###> sensiolabs/gotenberg-bundle ###
gotenberg:
image: gotenberg/gotenberg:8
ports:
- "3000:3000"
###< sensiolabs/gotenberg-bundle ###
5. Verification du fichier .env
Verifiez que la variable GOTENBERG_DSN est presente dans votre .env :
###> sensiolabs/gotenberg-bundle ###
GOTENBERG_DSN=http://localhost:3000
###< sensiolabs/gotenberg-bundle ###
6. Demarrage des services
docker compose up -d
symfony serve -d
Vous pouvez verifier que Gotenberg est bien demarre :
curl http://localhost:3000/health
Si tout est OK, vous devriez recevoir une reponse avec un status up.
Exemple 1 : Generer un PDF depuis un template Twig
C’est le cas d’usage le plus courant : vous avez un template Twig (une facture, un rapport, un bon de commande…) et vous voulez le convertir en PDF.
1. Creation du controleur
Creez le fichier src/Controller/PdfController.php :
<?php
namespace App\Controller;
use Sensiolabs\GotenbergBundle\GotenbergPdfInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class PdfController extends AbstractController
{
#[Route('/pdf/invoice', name: 'app_pdf_invoice')]
public function invoice(GotenbergPdfInterface $gotenberg): Response
{
return $gotenberg->html()
->content('pdf/invoice.html.twig', [
'invoice_number' => 'FACT-2026-001',
'invoice_date' => new \DateTime(),
'customer' => [
'name' => 'Acme Corp',
'address' => '42 rue de la Paix, 75002 Paris',
'email' => 'contact@acme.fr',
],
'items' => [
['name' => 'Prestation de developpement', 'qty' => 5, 'unit_price' => 600.00],
['name' => 'Hebergement annuel', 'qty' => 1, 'unit_price' => 240.00],
['name' => 'Maintenance mensuelle', 'qty' => 3, 'unit_price' => 150.00],
],
])
->generate()
->stream();
}
}
Explication ligne par ligne :
GotenbergPdfInterface $gotenberg: Le service est injecte automatiquement par Symfony (autowiring)->html(): On utilise le builder HTML (Chromium convertira notre HTML en PDF)->content('pdf/invoice.html.twig', [...]): Le template Twig avec ses variables, exactement comme un$this->render()->generate(): Envoie la requete a Gotenberg et recupere le PDF->stream(): Retourne uneStreamedResponsequi envoie le PDF au navigateur
C’est tout. 5 lignes pour generer un PDF depuis un template Twig.
2. Creation du template Twig
Creez le fichier templates/pdf/invoice.html.twig :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>Facture {{ invoice_number }}</title>
<link rel="stylesheet" href="{{ gotenberg_asset('css/invoice.css') }}" />
</head>
<body>
<header>
<div class="company">
<img src="{{ gotenberg_asset('img/logo.png') }}" alt="Logo" class="logo" />
<h1>Ma Societe SAS</h1>
<p>123 avenue des Developpeurs<br>69000 Lyon</p>
</div>
<div class="invoice-info">
<h2>FACTURE</h2>
<p><strong>N° :</strong> {{ invoice_number }}</p>
<p><strong>Date :</strong> {{ invoice_date|date('d/m/Y') }}</p>
</div>
</header>
<section class="customer">
<h3>Facturer a :</h3>
<p>
<strong>{{ customer.name }}</strong><br>
{{ customer.address }}<br>
{{ customer.email }}
</p>
</section>
<table>
<thead>
<tr>
<th>Description</th>
<th>Quantite</th>
<th>Prix unitaire HT</th>
<th>Total HT</th>
</tr>
</thead>
<tbody>
{% set total_ht = 0 %}
{% for item in items %}
{% set line_total = item.qty * item.unit_price %}
{% set total_ht = total_ht + line_total %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.qty }}</td>
<td>{{ item.unit_price|number_format(2, ',', ' ') }} EUR</td>
<td>{{ line_total|number_format(2, ',', ' ') }} EUR</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
{% set tva = total_ht * 0.20 %}
{% set total_ttc = total_ht + tva %}
<tr>
<td colspan="3">Total HT</td>
<td>{{ total_ht|number_format(2, ',', ' ') }} EUR</td>
</tr>
<tr>
<td colspan="3">TVA (20%)</td>
<td>{{ tva|number_format(2, ',', ' ') }} EUR</td>
</tr>
<tr class="total">
<td colspan="3"><strong>Total TTC</strong></td>
<td><strong>{{ total_ttc|number_format(2, ',', ' ') }} EUR</strong></td>
</tr>
</tfoot>
</table>
<footer>
<p>Ma Societe SAS - SIRET : 123 456 789 00001 - TVA : FR12345678900</p>
</footer>
</body>
</html>
Points cles :
{{ gotenberg_asset('css/invoice.css') }}: Cette fonction Twig est fournie par le bundle. Elle permet d’inclure des fichiers statiques (CSS, images, fonts) dans le PDF. Les fichiers sont automatiquement envoyes a Gotenberg avec la requete. Ils sont resolus depuis le dossier configure dansassets_directory(icigotenberg/).- Le template est du HTML/CSS classique : tout ce que Chromium sait rendre, Gotenberg sait le convertir en PDF. Flexbox, Grid,
@media print… tout fonctionne ! - Le template n’herite pas de
base.html.twig: c’est un document HTML autonome, car c’est Chromium (cote Gotenberg) qui va le rendre, pas le navigateur de l’utilisateur.
3. Creation de la feuille de styles
AssetMapper et gotenberg_asset() : un piege a connaitre
Par defaut, GotenbergBundle resout les assets depuis le dossier assets/ de votre projet. La fonction Twig gotenberg_asset() cherche le fichier sur le disque pour l’envoyer a Gotenberg.
Le probleme : si vous utilisez AssetMapper (le systeme d’assets par defaut depuis Symfony 6.4), celui-ci versionne les noms de fichiers en ajoutant un hash (ex: invoice-VT_oO7v.css). Quand gotenberg_asset() passe par AssetMapper, il recoit un chemin versionne… mais le fichier physique sur le disque s’appelle toujours invoice.css. Resultat : fichier introuvable, erreur 500.
La solution : creer un dossier dedie gotenberg/ en dehors du scope d’AssetMapper, et configurer le bundle pour l’utiliser.
Dans config/packages/gotenberg.yaml, ajoutez la directive assets_directory :
sensiolabs_gotenberg:
http_client: 'gotenberg.client'
assets_directory:
- '%kernel.project_dir%/gotenberg'
Puis creez le dossier et le fichier CSS :
mkdir -p gotenberg/css
Creez le fichier gotenberg/css/invoice.css :
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
color: #333;
padding: 40px;
}
header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #2563eb;
}
.logo {
max-width: 80px;
margin-bottom: 10px;
}
.company h1 {
font-size: 20px;
color: #2563eb;
}
.invoice-info {
text-align: right;
}
.invoice-info h2 {
font-size: 28px;
color: #2563eb;
margin-bottom: 10px;
}
.customer {
margin-bottom: 30px;
padding: 15px;
background-color: #f8fafc;
border-radius: 8px;
}
.customer h3 {
color: #64748b;
font-size: 12px;
text-transform: uppercase;
margin-bottom: 8px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
thead {
background-color: #2563eb;
color: white;
}
th, td {
padding: 12px 15px;
text-align: left;
}
th {
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
}
tbody tr:nth-child(even) {
background-color: #f8fafc;
}
tbody tr:hover {
background-color: #e2e8f0;
}
tfoot td {
padding: 10px 15px;
border-top: 1px solid #e2e8f0;
}
tfoot tr.total td {
border-top: 2px solid #2563eb;
font-size: 16px;
}
footer {
position: fixed;
bottom: 20px;
left: 40px;
right: 40px;
text-align: center;
font-size: 11px;
color: #94a3b8;
border-top: 1px solid #e2e8f0;
padding-top: 10px;
}
Pour ajouter un logo, placez une image PNG dans
gotenberg/img/logo.pnget ajoutez<img src="{{ gotenberg_asset('img/logo.png') }}" />dans le template.
4. Test
Ouvrez votre navigateur a l’adresse : https://localhost:8000/pdf/invoice
Votre PDF est genere et affiche directement dans le navigateur. Vous pouvez le telecharger, l’imprimer… C’est un vrai PDF, genere par Chromium, avec un rendu parfait.
5. Aller plus loin : options de personnalisation
Le builder HTML offre de nombreuses options pour personnaliser votre PDF :
use Sensiolabs\GotenbergBundle\Enumeration\PaperSize;
use Sensiolabs\GotenbergBundle\Enumeration\Unit;
return $gotenberg->html()
->content('pdf/invoice.html.twig', $variables)
// Format du papier
->paperStandardSize(PaperSize::A4)
// Marges personnalisees
->margins(10, 10, 10, 10, Unit::Millimeters)
// Orientation paysage
->landscape()
// Imprimer les couleurs de fond
->printBackground()
// En-tete et pied de page
->header('pdf/header.html.twig', ['company' => 'Ma Societe'])
->footer('pdf/footer.html.twig')
// Metadonnees du PDF
->metadata(['Author' => 'Ma Societe', 'Title' => 'Facture'])
// Nom du fichier en telechargement
->fileName('facture-2026-001')
->generate()
->stream();
6. Le Profiler Symfony
Un des gros atouts du bundle : chaque requete vers Gotenberg apparait dans le Symfony Profiler. Vous pouvez y voir :
- Le template utilise
- Les fichiers envoyes (CSS, images)
- Le temps de generation
- Les eventuelles erreurs
C’est un outil precieux pour debugger vos generations de PDF.
Exemple 2 : Factur-X (facture electronique europeenne)
C’est quoi Factur-X ?
Factur-X (aussi connu sous le nom ZUGFeRD) est un standard europeen de facturation electronique. Le principe est simple : c’est un PDF hybride qui contient a la fois :
- Le PDF lisible par un humain (votre facture classique)
- Un fichier XML structure embarque dans le PDF, lisible par une machine
Factur-X = PDF classique + XML embarque
(humain) (machine)
Pourquoi c’est important ?
- C’est en train de devenir obligatoire dans l’Union Europeenne pour les transactions B2B
- Ca permet l’automatisation : les logiciels comptables peuvent lire le XML directement
- Ca reduit les erreurs de saisie : plus besoin de re-taper les montants
Comment GotenbergBundle gere Factur-X ?
Normalement, injecter un XML dans un PDF est complexe : il faut manipuler la structure interne du PDF, ajouter des metadonnees, respecter le standard PDF/A-3…
Avec GotenbergBundle, c’est beaucoup plus simple : le builder permet d’embarquer des fichiers directement dans le PDF genere grace a la methode ->embeds(). Gotenberg se charge de l’attachement du XML dans le PDF.
Passage en version de developpement
La methode ->embeds() n’est pas encore disponible dans la derniere version stable (v1.1.1). Elle fait partie de la prochaine release, actuellement sur la branche 1.x. Pour l’utiliser des maintenant, il faut passer sur la version de developpement :
composer require sensiolabs/gotenberg-bundle:1.x-dev
C’est un bon signe : le bundle est en developpement actif, et l’equipe SensioLabs travaille a enrichir le support de Factur-X. Si vous n’avez pas besoin de l’embed XML, restez sur la v1.1.1 stable.
1. Creation du template Twig pour le XML Factur-X
Dans un vrai projet, le XML Factur-X n’est pas un fichier statique : il est genere dynamiquement a partir des donnees de votre facture (client, montants, numero…). Exactement comme vous generez du HTML avec Twig, vous pouvez generer du XML avec Twig.
Le XML Factur-X suit la norme EN 16931. Voici un template simplifie au profil “Minimum”.
Creez le fichier templates/pdf/facturx.xml.twig :
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:factur-x.eu:1p0:minimum</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>{{ invoice_number }}</ram:ID>
<ram:TypeCode>380</ram:TypeCode>
<ram:IssueDateTime>
<udt:DateTimeString format="102">{{ invoice_date|date('Ymd') }}</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
<rsm:SupplyChainTradeTransaction>
<ram:ApplicableHeaderTradeAgreement>
<ram:SellerTradeParty>
<ram:Name>{{ seller.name }}</ram:Name>
<ram:SpecifiedLegalOrganization>
<ram:ID schemeID="0002">{{ seller.siret }}</ram:ID>
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress>
<ram:CountryID>FR</ram:CountryID>
</ram:PostalTradeAddress>
</ram:SellerTradeParty>
<ram:BuyerTradeParty>
<ram:Name>{{ customer.name }}</ram:Name>
</ram:BuyerTradeParty>
</ram:ApplicableHeaderTradeAgreement>
<ram:ApplicableHeaderTradeDelivery/>
<ram:ApplicableHeaderTradeSettlement>
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
<ram:TaxBasisTotalAmount>{{ total_ht|number_format(2, '.', '') }}</ram:TaxBasisTotalAmount>
<ram:TaxTotalAmount currencyID="EUR">{{ tva|number_format(2, '.', '') }}</ram:TaxTotalAmount>
<ram:GrandTotalAmount>{{ total_ttc|number_format(2, '.', '') }}</ram:GrandTotalAmount>
<ram:DuePayableAmount>{{ total_ttc|number_format(2, '.', '') }}</ram:DuePayableAmount>
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
</ram:ApplicableHeaderTradeSettlement>
</rsm:SupplyChainTradeTransaction>
</rsm:CrossIndustryInvoice>
Explication du XML :
TypeCode 380: Code UN/CEFACT pour “Facture commerciale”format="102": Format de date AAAAMMJJschemeID="0002": Identifiant SIRET- Les variables Twig (
{{ invoice_number }},{{ total_ht }}, etc.) sont injectees dynamiquement, comme dans n’importe quel template
Twig ne sert pas qu’a generer du HTML ! Ici on l’utilise pour generer du XML. On pourrait aussi generer du CSV, du JSON, du YAML… Twig est un moteur de templates generique.
2. Mise a jour du controleur
Voici la logique complete. Le principe est simple :
- On prepare les donnees de la facture (en vrai, elles viendraient de Doctrine)
- On calcule les totaux
- On genere le XML dynamiquement avec Twig
- On ecrit le XML dans un fichier temporaire
- On passe ce fichier a
embeds()avec un chemin absolu - On nettoie le fichier temporaire apres l’envoi du PDF
Ajoutez une nouvelle methode dans src/Controller/PdfController.php :
use Sensiolabs\GotenbergBundle\Enumeration\PdfFormat;
use Twig\Environment;
#[Route('/pdf/facturx', name: 'app_pdf_facturx')]
public function facturx(
GotenbergPdfInterface $gotenberg,
Environment $twig,
): Response {
// 1. Les donnees de la facture (en vrai, elles viendraient de la BDD)
$invoiceData = [
'invoice_number' => 'FACT-2026-001',
'invoice_date' => new \DateTime(),
'customer' => [
'name' => 'Acme Corp',
'address' => '42 rue de la Paix, 75002 Paris',
'email' => 'contact@acme.fr',
],
'seller' => [
'name' => 'Ma Societe SAS',
'siret' => '12345678900001',
],
'items' => [
['name' => 'Prestation de developpement', 'qty' => 5, 'unit_price' => 600.00],
['name' => 'Hebergement annuel', 'qty' => 1, 'unit_price' => 240.00],
['name' => 'Maintenance mensuelle', 'qty' => 3, 'unit_price' => 150.00],
],
];
// 2. Calculer les totaux
$totalHt = array_sum(array_map(
fn ($item) => $item['qty'] * $item['unit_price'],
$invoiceData['items'],
));
$tva = $totalHt * 0.20;
$totalTtc = $totalHt + $tva;
// 3. Generer le XML Factur-X dynamiquement via Twig
$xmlContent = $twig->render('pdf/facturx.xml.twig', [
...$invoiceData,
'total_ht' => $totalHt,
'tva' => $tva,
'total_ttc' => $totalTtc,
]);
// 4. Ecrire le XML dans un fichier temporaire nomme "factur-x.xml"
// Le standard Factur-X EXIGE que le fichier embarque s'appelle "factur-x.xml"
$tempDir = sys_get_temp_dir() . '/facturx_' . uniqid();
mkdir($tempDir);
$tempXmlPath = $tempDir . '/factur-x.xml';
file_put_contents($tempXmlPath, $xmlContent);
try {
// 5. Generer le PDF avec le XML embarque
return $gotenberg->html()
->content('pdf/invoice.html.twig', $invoiceData)
->embeds($tempXmlPath)
->pdfFormat(PdfFormat::Pdf3b)
->metadata([
'Author' => $invoiceData['seller']['name'],
'Title' => 'Facture ' . $invoiceData['invoice_number'],
'Subject' => 'Factur-X Invoice',
])
->fileName('facture-facturx-2026-001')
->generate()
->stream();
} finally {
// 6. Nettoyer le fichier temporaire et son dossier
@unlink($tempXmlPath);
@rmdir($tempDir);
}
}
Decortiquons les etapes :
| Etape | Ce qui se passe | Pourquoi |
|---|---|---|
| 1. Donnees | On prepare un tableau avec toutes les infos de la facture | En vrai, ces donnees viendraient d’une entite Doctrine ($invoice->getCustomer(), etc.) |
| 2. Totaux | On calcule HT, TVA et TTC | Les montants du XML doivent correspondre exactement a ceux du PDF |
| 3. Rendu Twig | $twig->render() genere le XML en string | On reutilise Twig, un outil que vous connaissez deja, pour generer du XML |
| 4. Fichier temp | On ecrit le XML dans /tmp/.../factur-x.xml | embeds() attend un chemin de fichier, pas une string. Le fichier doit s’appeler factur-x.xml (exigence du standard) |
| 5. Generation | GotenbergBundle envoie le HTML + le XML a Gotenberg | Gotenberg genere le PDF et y embarque le XML comme piece jointe |
| 6. Nettoyage | finally garantit que le fichier temp est supprime | Meme si une erreur survient, le fichier est nettoye. Pas de fuite de fichiers |
Points cles :
- Le fichier XML doit s’appeler
factur-x.xml: c’est une exigence du standard Factur-X. On cree un sous-dossier temporaire unique (/tmp/facturx_xyz123/) pour eviter les collisions, puis on y ecritfactur-x.xml. Le bundle utilise le basename du fichier comme nom de la piece jointe dans le PDF. embeds($tempXmlPath): on passe un chemin absolu (commence par/tmp/). Le bundle detecte automatiquement que c’est un chemin absolu et ne cherche pas dans le dossiergotenberg/.pdfFormat(PdfFormat::Pdf3b): genere un PDF au format PDF/A-3b, prerequis du standard Factur-X (c’est ce format qui autorise l’embarquement de fichiers).- Le bloc
try/finally: c’est une bonne pratique pour garantir le nettoyage des ressources temporaires, meme en cas d’erreur. Le@devantunlink/rmdirsupprime les warnings si le fichier n’existe plus. - Le spread operator
...$invoiceData: permet de “decompresser” le tableau dans un autre tableau. Pratique pour ne pas repeter les variables.
3. Test
Ouvrez votre navigateur a l’adresse : https://localhost:8000/pdf/facturx
Le PDF genere est visuellement identique a la facture classique, mais il contient desormais le fichier XML embarque. Vous pouvez le verifier en ouvrant le PDF avec Adobe Acrobat ou un lecteur compatible : le XML apparait dans les pieces jointes du document.
4. Validation Factur-X : transparence sur l’etat actuel
Pour verifier la conformite de notre PDF, on peut utiliser le validateur officiel FNFE-MPE : services.fnfe-mpe.org.
Voici le resultat avec notre code :
| Verification | Statut |
|---|---|
| Profil detecte (Minimum) | OK |
| XML valide contre le XSD | OK |
| XML valide contre le Schematron | OK |
Fichier nomme factur-x.xml | OK |
| Conformite PDF/A-3 | KO |
| Metadonnees XMP Factur-X | KO |
Le XML est parfaitement valide. C’est GotenbergBundle qui fait le travail : generation du PDF via Chromium, embedding du XML, format PDF/A-3b.
Les erreurs restantes concernent la structure interne du PDF : metadonnees XMP specifiques a Factur-X (fx:DocumentType, fx:Version…) et attributs PDF/A-3 sur les fichiers embarques (AFRelationship, MIME type). Ces elements ne sont pas geres par Gotenberg lui-meme a ce jour. C’est un sujet ouvert sur le GitHub de Gotenberg.
En resume : GotenbergBundle gere le plus gros du travail (rendu PDF, embedding XML). Pour une conformite Factur-X stricte en production (reforme e-invoicing 2026), un post-traitement avec une librairie dediee comme
atgp/factur-xsera necessaire pour completer les metadonnees PDF. Le GotenbergBundle est en developpement actif et le support complet pourrait arriver dans une future version.
Les plus de GotenbergBundle
Au-dela de la generation de PDF, le bundle offre plusieurs fonctionnalites qui ameliorent la DX (Developer Experience) :
Gestion optimisee de la memoire
Le bundle utilise le streaming pour retourner les PDF. Le fichier n’est jamais charge entierement en memoire : il est envoye progressivement au navigateur. C’est crucial pour les gros documents.
Kit de test PHPUnit
Le bundle fournit des utilitaires pour tester vos generations de PDF sans avoir besoin d’un serveur Gotenberg en marche :
use Sensiolabs\GotenbergBundle\Test\GotenbergTestTrait;
class PdfControllerTest extends WebTestCase
{
use GotenbergTestTrait;
public function testInvoiceGeneration(): void
{
$client = static::createClient();
$client->request('GET', '/pdf/invoice');
$this->assertGotenbergPdfGenerated();
}
}
Systeme de debug et journalisation
Chaque requete vers Gotenberg est tracee dans le Profiler Symfony et dans les logs. Vous pouvez voir exactement ce qui est envoye a Gotenberg et diagnostiquer les problemes rapidement.
Conversion de documents Office
En plus du HTML, GotenbergBundle sait convertir des fichiers Office grace a LibreOffice :
// Convertir un fichier Word en PDF
return $gotenberg->office()
->files('documents/contrat.docx')
->generate()
->stream();
Generation asynchrone via Webhooks
Pour les documents lourds, vous pouvez utiliser les webhooks pour generer le PDF en arriere-plan :
return $gotenberg->html()
->content('pdf/report.html.twig', $data)
->webhookConfiguration('my_webhook_config')
->generate();
Gotenberg enverra le PDF a l’URL de votre webhook une fois la generation terminee.
Conclusion
GotenbergBundle change completement la donne pour la generation de PDF en Symfony. Fini les galeres avec des librairies obsoletes ou des rendus CSS approximatifs. Avec quelques lignes de code, vous avez :
- Un rendu pixel-perfect grace a Chromium (Flexbox, Grid,
@media print… tout fonctionne) - Une integration native dans l’ecosysteme Symfony (Twig, Profiler, autowiring)
- La possibilite d’embarquer des fichiers dans vos PDF (XML Factur-X, pieces jointes…)
- La conversion de documents Office en PDF via LibreOffice
- Le tout dans un conteneur Docker scalable
Ce qu’on a construit dans ce tutoriel
- Une facture PDF generee depuis un template Twig, avec CSS moderne et un rendu professionnel
- Une facture Factur-X avec un XML structure genere dynamiquement et embarque dans le PDF
On a aussi vu les pieges a eviter (AssetMapper vs gotenberg_asset()) et les limites actuelles (conformite Factur-X stricte), en toute transparence.
Le bundle est en pleine evolution
GotenbergBundle est en developpement actif par SensioLabs. La methode embeds() que nous avons utilisee est toute recente (branche 1.x), et de nouvelles fonctionnalites arrivent regulierement. Le support de Factur-X va continuer a s’ameliorer, notamment avec un bundle Sylius dedie en cours de developpement.
Pour aller plus loin
- GitHub du GotenbergBundle
- Documentation officielle de Gotenberg
- Talk SymfonyLive Paris 2025 : “Du lego de composants pour un bundle Gotenberg !”
- Article Medium : How to generate a PDF in a few lines of code with Symfony
- Validateur Factur-X FNFE-MPE
Le bundle est open source et maintenu par le createur de Symfony. N’hesitez pas a le tester, a consulter la doc et a contribuer. Que ce soit du code, de la documentation ou simplement un retour d’experience : chaque contribution compte !
Transparence : cet article est réalisé dans le cadre d’un partenariat rémunéré avec SensioLabs, créateur de Symfony et mainteneur de GotenbergBundle. Les opinions et retours techniques exprimés restent les miens.
Loading comments...