17 min de lecture

GotenbergBundle : Generer des PDF comme un pro avec Symfony

Decouvrez comment generer des PDF professionnels avec Symfony et GotenbergBundle. De l'installation a la creation de factures Factur-X, ce tutoriel vous guide pas a pas dans l'ecosysteme Docker-first de Gotenberg.

Decouvrez comment generer des PDF professionnels avec Symfony et GotenbergBundle. De l'installation a la creation de factures Factur-X, ce tutoriel vous guide pas a pas dans l'ecosysteme Docker-first de Gotenberg.
Mode de lecture :

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 ?

CritereAnciennes solutionsGotenbergBundle
Rendu CSSPartiel (CSS 2.1 max)Complet (Chromium)
InstallationExtensions PHP, binaires systemeDocker uniquement
MaintenanceSouvent abandonnesSensioLabs (createur de Symfony)
Integration SymfonyInexistante ou bricoleeNative (Twig, Profiler, AssetMapper)
Formats d’entreeHTML basiqueHTML, Twig, Markdown, URL, Office
ScalabiliteLimiteeDocker = 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 --webapp installe 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.yaml pour ajouter le service Gotenberg
  • Ajoute la variable GOTENBERG_DSN dans .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 une StreamedResponse qui 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&#xB0; :</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 dans assets_directory (ici gotenberg/).
  • 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.png et 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 AAAAMMJJ
  • schemeID="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 :

  1. On prepare les donnees de la facture (en vrai, elles viendraient de Doctrine)
  2. On calcule les totaux
  3. On genere le XML dynamiquement avec Twig
  4. On ecrit le XML dans un fichier temporaire
  5. On passe ce fichier a embeds() avec un chemin absolu
  6. 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 :

EtapeCe qui se passePourquoi
1. DonneesOn prepare un tableau avec toutes les infos de la factureEn vrai, ces donnees viendraient d’une entite Doctrine ($invoice->getCustomer(), etc.)
2. TotauxOn calcule HT, TVA et TTCLes montants du XML doivent correspondre exactement a ceux du PDF
3. Rendu Twig$twig->render() genere le XML en stringOn reutilise Twig, un outil que vous connaissez deja, pour generer du XML
4. Fichier tempOn ecrit le XML dans /tmp/.../factur-x.xmlembeds() attend un chemin de fichier, pas une string. Le fichier doit s’appeler factur-x.xml (exigence du standard)
5. GenerationGotenbergBundle envoie le HTML + le XML a GotenbergGotenberg genere le PDF et y embarque le XML comme piece jointe
6. Nettoyagefinally garantit que le fichier temp est supprimeMeme 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 ecrit factur-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 dossier gotenberg/.
  • 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 @ devant unlink/rmdir supprime 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 :

VerificationStatut
Profil detecte (Minimum)OK
XML valide contre le XSDOK
XML valide contre le SchematronOK
Fichier nomme factur-x.xmlOK
Conformite PDF/A-3KO
Metadonnees XMP Factur-XKO

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-x sera 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

  1. Une facture PDF generee depuis un template Twig, avec CSS moderne et un rendu professionnel
  2. 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

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.

Back to Blog

Comments (0)

Loading comments...

Leave a Comment