---
title: "GotenbergBundle : Generer des PDF comme un pro avec Symfony"
excerpt: "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."
publishDate: 2026-02-21T00:00:00.000Z
tags: ["symfony", "gotenberg", "pdf", "docker", "factur-x"]
canonical: "https://yoandev.co/gotenberg-bundle"
---

> **Transparence** : cet article est réalisé dans le cadre d'un partenariat rémunéré avec [SensioLabs](https://sensiolabs.com/fr/), 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 :

```bash
symfony new GotenbergDemo --webapp
cd GotenbergDemo
```

> L'option `--webapp` installe tous les packages necessaires (Twig, Doctrine, Profiler, etc.).

### 3. Installation du GotenbergBundle

```bash
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 :

```yaml
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` :

```env
###> sensiolabs/gotenberg-bundle ###
GOTENBERG_DSN=http://localhost:3000
###< sensiolabs/gotenberg-bundle ###
```

### 6. Demarrage des services

```bash
docker compose up -d
symfony serve -d
```

Vous pouvez verifier que Gotenberg est bien demarre :

```bash
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
<?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` :

```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` :

```yaml
sensiolabs_gotenberg:
    http_client: 'gotenberg.client'
    assets_directory:
        - '%kernel.project_dir%/gotenberg'
```

Puis creez le dossier et le fichier CSS :

```bash
mkdir -p gotenberg/css
```

Creez le fichier `gotenberg/css/invoice.css` :

```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 :

```php
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 :

```bash
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` :

```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` :

```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 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](https://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](https://github.com/gotenberg/gotenberg/issues/877).

> **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`](https://github.com/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 :

```php
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 :

```php
// 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 :

```php
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

- [GitHub du GotenbergBundle](https://github.com/sensiolabs/GotenbergBundle)
- [Documentation officielle de Gotenberg](https://gotenberg.dev/docs/getting-started/introduction)
- [Talk SymfonyLive Paris 2025 : "Du lego de composants pour un bundle Gotenberg !"](https://symfony.com/blog/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](https://medium.com/the-sensiolabs-tech-blog/how-to-generate-a-pdf-file-in-a-few-lines-of-code-with-symfony-39786a679d29)
- [Validateur Factur-X FNFE-MPE](https://services.fnfe-mpe.org/)

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](https://sensiolabs.com/fr/), créateur de Symfony et mainteneur de GotenbergBundle. Les opinions et retours techniques exprimés restent les miens.
