Créer un Custom Builder - L'histoire du GotenbergBundle

· Steven Renaux · Expertise · Temps de lecture: 7 minutes
A man sculpting a rock with PDF written on it

Nous avons déjà vu comment générer un fichier PDF en quelques lignes de code à l'aide de Gotenberg et de GotenbergBundle, un bundle Symfony. Mais que faire lorsque votre application doit générer plusieurs fichiers PDF différents, chacun avec sa propre mise en page, ses propres styles et ses propres données ?

Lire l'article précédent >> ici

On se retrouve rapidement avec de la configuration dupliquée, des services encombrés et une logique conditionnelle difficile à maintenir. Cet article reprend exactement là où nous nous sommes arrêtés. Nous allons construire un Builder personnalisé (Custom Builder), une classe dédiée qui encapsule toute la configuration et la logique pour un type de PDF spécifique. À la fin, votre contrôleur sera réduit à un seul appel, et chaque type de PDF résidera dans sa propre classe propre et sera testable.

Quel est l'intérêt d'un Custom Builder ?

L'objectif est de vous faciliter la vie lorsque vous générez de nombreux PDF.

Si vous générez plusieurs PDF à travers votre application, la configuration ne peut parfois pas être partagée entre eux, principalement à cause des différences concernant le style.

C’est ici que le Design Pattern Builder entre en jeu. C'est un patron de conception qui vous aide à construire des objets complexes étape par étape, en séparant la construction de l'objet de sa représentation finale. Dans ce contexte, cela vous permet d'encapsuler toute la configuration et la logique d'un type de PDF spécifique dans une classe builder dédiée.

En créant un Builder personnalisé, vous évitez de polluer votre service avec de la logique conditionnelle ou de dupliquer du code. Au lieu de cela, chaque builder gère sa propre configuration de manière propre et efficace, rendant votre codebase plus facile à maintenir et à faire évoluer.

Étape 1 : Mettre à jour les dépendances

TL;DR : voir ce commit

Tout d'abord, vous devez mettre à jour vos dépendances pour obtenir la dernière version du GotenbergBundle. Enfin la v1.2, qui n'est plus expérimentale ! Yeah! 🍾

composer require sensiolabs/gotenberg-bundle:1.2.*

Étape 2 : Créer la classe du Custom Builder

TL;DR : voir ce commit

Créez une classe qui étend AbstractBuilder et implémente BuilderAssetInterface.

<?php

namespace App\Pdf;

use Sensiolabs\GotenbergBundle\Builder\AbstractBuilder;
use Sensiolabs\GotenbergBundle\Builder\BuilderAssetInterface;

final class InvoicePdfBuilder extends AbstractBuilder
implements BuilderAssetInterface
{

   protected function getEndpoint(): string
   {
       // TODO: Implement getEndpoint() method.
   }

   public function addAsset(string $path): static
   {
       // TODO: Implement addAsset() method.
   }
}

Tous les builders natifs étendent AbstractBuilder, qui définit la méthode generate. Celle-ci stocke toutes les configurations nécessaires (margin, largeur, etc.) et prépare le payload avant de l'envoyer à l'API Gotenberg.

Parce que vous ajoutez des assets dans le template Twig, vous devez implémenter l'interface BuilderAssetInterface.

Ajoutez l'attribut #[WithBuilderConfiguration('pdf', 'invoice')] en haut de la classe.

<?php

namespace App\Pdf;

use Sensiolabs\GotenbergBundle\Builder\AbstractBuilder;
use Sensiolabs\GotenbergBundle\Builder\Attributes\WithBuilderConfiguration;
use Sensiolabs\GotenbergBundle\Builder\BuilderAssetInterface;
#[WithBuilderConfiguration('pdf', 'invoice')]
final class InvoicePdfBuilder extends AbstractBuilder implements BuilderAssetInterface
{
	// rest of the code
}

Cet attribut vous permettra d'obtenir une configuration sémantique pour ce builder. Le premier argument sert à injecter ce nouveau type de builder dans la section 'pdf' ou 'screenshot'. Le second correspond au nom que vous souhaitez lui donner.

Implémentons maintenant les méthodes. Concernant getEndpoint, utilisons la constante existante HtmlPdfBuilder::ENDPOINT, car l'endpoint de l'API Gotenberg est le même que celui utilisé par le HtmlPdfBuilder.

Si vous voulez générer des PDF, les traits disponibles sont :

  • AssetTrait : Inclut les méthodes pour ajouter des assets.

  • ContentTrait : Inclut les méthodes pour ajouter différentes parties de contenu à votre PDF.

  • CookieTrait : Méthodes pour définir et transférer des cookies à l'API.

  • CustomHttpHeadersTrait : Méthodes pour ajouter des headers HTTP.

  • EmulatedMediaTypeTrait : Méthode pour émuler le mode screen ou print.

  • FailOnTrait : Permet de personnaliser le comportement en cas de code d'état (status code) invalide.

  • PdfPagePropertiesTrait : Méthodes pour personnaliser le rendu du PDF (tailles, marges...).

  • PerformanceModeTrait : Méthode pour ne pas attendre que le réseau de Chromium soit inactif.

  • WaitBeforeRenderingTrait : Ajoute un délai avant la conversion.

  • DownloadFromTrait : Permet d'ajouter des ressources externes.

  • MetadataTrait : Permet d'ajouter des métadonnées.

  • PdfFormatTrait : Méthodes relatives aux formats PDF.

  • SplitTrait : Méthodes pour découper le PDF.

  • WebhookTrait : Méthodes pour utiliser des webhooks.

Tous ces traits sont combinés dans le ChromiumPdfTrait.

Si vous souhaitez créer un builder personnalisé pour Office ou des captures d'écran, vous trouverez tous les traits disponibles dans le code source du GotenbergBundle sur GitHub.

Ajoutons donc l'AssetTrait qui implémentera la méthode addAsset pour nous. Sous le capot, cette méthode stocke les assets provenant des templates Twig ou ceux ajoutés à la volée.

Le ContentTrait nous donne la possibilité d'utiliser les méthodes header et content comme dans le contrôleur, ou le footer configuré dans le fichier sensiolabs_gotenberg.yaml.

Enfin, le PdfPagePropertiesTrait gère tout ce qui concerne les marges, l'orientation paysage, la largeur du papier et la personnalisation du rendu.

<?php

namespace App\Pdf;

use Sensiolabs\GotenbergBundle\Builder\AbstractBuilder;
use Sensiolabs\GotenbergBundle\Builder\Attributes\WithBuilderConfiguration;
use Sensiolabs\GotenbergBundle\Builder\Behaviors\Chromium\AssetTrait;
use Sensiolabs\GotenbergBundle\Builder\Behaviors\Chromium\ContentTrait;
use Sensiolabs\GotenbergBundle\Builder\Behaviors\Chromium\PdfPagePropertiesTrait;
use Sensiolabs\GotenbergBundle\Builder\BuilderAssetInterface;
use Sensiolabs\GotenbergBundle\Builder\Pdf\HtmlPdfBuilder;

#[WithBuilderConfiguration('pdf', 'invoice')]
final class InvoicePdfBuilder extends AbstractBuilder implements BuilderAssetInterface
{
   use AssetTrait;
   use ContentTrait;
   use PdfPagePropertiesTrait;

   protected function getEndpoint(): string
   {
       return HtmlPdfBuilder::ENDPOINT;
   }
}

Étape 3 : Mettre à jour le fichier de configuration

TL;DR : voir ce commit

Il vous suffit de mettre à jour le nom du builder. Le nom est celui que vous avez configuré dans l'attribut WithBuilderConfiguration.

sensiolabs_gotenberg:
   http_client: 'gotenberg.client'
   default_options:
       pdf:
-          html:
+          invoice:
               footer:
                   template: 'footer.html.twig'
               paper_width: '21cm'
               paper_height: '29.7cm'
               margin_top: '6cm'
               margin_bottom: '2cm'
               landscape: true

Étape 4 : Mettre à jour le contrôleur

TL;DR : voir ce commit

Comme pour la configuration, seules deux lignes doivent être modifiées.

#[Route('/pdf', 'pdf')]
public function pdf(GotenbergPdfInterface $gotenbergPdf): Response
{
   $invoiceData = $this->invoiceData();


-   return $gotenbergPdf
-      ->html()
+   return $gotenbergPdf->get(InvoicePdfBuilder::class)
       ->header('header.html.twig', [
           'invoice' => $invoiceData['invoice'],
           'client' => $invoiceData['client'],
       ])
       ->content('content.html.twig', [
           'purchases' => $invoiceData['purchases'],
           'invoice' => $invoiceData['invoice'],
       ])
       ->generate()
       ->stream()
   ;
}

Étape 5 : Faire fonctionner le tout 💪

TL;DR : voir ce commit

Si vous utilisez une version du GotenbergBundle inférieure à la v1.2, vous devez ajouter le configurateur manuellement :

# services.yaml

services:
	App\Pdf\InvoicePdfBuilder:
   configurator: '@sensiolabs_gotenberg.builder_configurator'

Cependant, depuis la version v1.0.1, vous pouvez l’automatiser. Enregistrez simplement le builder dans la méthode build de votre classe Kernel.

Il suffit de récupérer l'extension sensiolabs_gotenberg, puis d'appeler la méthode registerBuilder avec le FQCN de votre builder personnalisé. Et le tour est joué !

class Kernel extends BaseKernel
{
   use MicroKernelTrait;


   protected function build(ContainerBuilder $container): void
   {
       /** @var SensiolabsGotenbergExtension $extension */
       $extension = $container->getExtension('sensiolabs_gotenberg');
       $extension->registerBuilder(InvoicePdfBuilder::class);
   }
}

Étape 6 : Attendez... on a oublié l'essentiel ! 🤦

TL;DR : voir ce commit

Nous étions tellement concentrés sur la création du builder que nous avons oublié son but premier : encapsuler la logique. Actuellement, le contrôleur en sait encore trop : il récupère les données de facturation, les passe au header, les passe au contenu... C’est précisément ce genre de responsabilité qui devrait se trouver dans InvoicePdfBuilder. Déplaçons tout cela.

#[WithBuilderConfiguration('pdf', 'invoice')]
final class InvoicePdfBuilder extends AbstractBuilder implements BuilderAssetInterface
{
    use AssetTrait;
    use ContentTrait;
    use PdfPagePropertiesTrait;


    public function invoice(): self
    {
        $invoiceData = $this->invoiceData();
        
        $this->header('header.html.twig', [
            'invoice' => $invoiceData['invoice'],
            'client' => $invoiceData['client'],
        ]);
        
        $this->content('content.html.twig', [
            'purchases' => $invoiceData['purchases'],
            'invoice' => $invoiceData['invoice'],
        ]);
        
        return $this;
    }

    protected function getEndpoint(): string
    {
        return HtmlPdfBuilder::ENDPOINT;
    }


    private function invoiceData(): array
    {
        $factory = Factory::create();
        
        $allPurchases = [];
        for ($i = 0; $i < 20; $i++) {
            $allPurchases[] = [
                'orderId' => $factory->unixTime(),
                'period' => $factory->dateTimeBetween('- 1 week')->format('Y-m-d') . ' - ' . $factory->dateTime('now')->format('Y-m-d'),
                'description' => $factory->sentence(),
                'price' => $factory->randomFloat(2, 1),
                'quantity' => $factory->randomDigitNotZero(),
                'total' => $factory->randomFloat(2, 1),
            ];
        }
        
        return [
            'invoice' => [
                'id' => $factory->unixTime(),
                'date' => $factory->dateTime()->format('Y-m-d'),
                'due_date' => $factory->dateTime('+1 week')->format('Y-m-d'),
                'sub_total' => $factory->randomFloat(2, 1),
                'total' => $factory->randomFloat(2, 1),
            ],
            'client' => [
                'phone_number' => $factory->e164PhoneNumber(),
                'name' => $factory->company(),
                'address' => $factory->address(),
                'city' => $factory->city(),
            ],
            'purchases' => $allPurchases
        ];
    }
}

Désormais, le contrôleur devient extrêmement simple, respectant les best practices de Symfony :

#[Route('/pdf', 'pdf')]
public function pdf(GotenbergPdfInterface $gotenbergPdf): Response
{
-   $invoiceData = $this->invoiceData();
-
-   return $gotenbergPdf->get(InvoicePdfBuilder::class)
-       ->header('header.html.twig', [
-           'invoice' => $invoiceData['invoice'],
-           'client' => $invoiceData['client'],
-       ])
-       ->content('content.html.twig', [
-           'purchases' => $invoiceData['purchases'],
-           'invoice' => $invoiceData['invoice'],
-       ])
-       ->generate()
-       ->stream()
-   ;


+   return $gotenbergPdf->get(InvoicePdfBuilder::class)
+       ->invoice()
+       ->generate()
+       ->stream()
+   ;
}

Le contrôleur ne connaît plus les templates ni la structure des données. Il dit simplement "donne-moi le PDF de la facture" et le builder s'occupe du reste. C'est tout l'intérêt de la démarche 🎯

Conclusion

Désormais, lorsque vous lancez php bin/console debug:config sensiolabs_gotenberg, vous verrez la configuration de votre builder personnalisé parfaitement intégrée aux côtés des builders natifs — le signe que tout est bien câblé.

Voici ce que nous avons accompli :

  1. Le contrôleur est propre. Il ignore tout des templates et de la logique de rendu.

  2. La logique est encapsulée. InvoicePdfBuilder centralise tout ce qui concerne les factures : l'endpoint, les traits, les templates et les données.

  3. La configuration est sémantique. Grâce à l'attribut, votre fichier YAML est naturel et permet de configurer les marges ou le format de papier par builder sans bidouillage.

  4. C’est scalable. Besoin d’un ReportPdfBuilder ? Suivez les mêmes étapes. Chaque type de PDF a son propre bloc de configuration sans interférer avec les autres.

C'est le pattern Builder à son meilleur dans un contexte Symfony : prévisible, maintenable et facile à étendre. Essayez-le dans votre prochain projet 🚀

Prêts à créer votre propre Custom Builder

Notre équipe d'experts de Symfony et PHP est très impliquée dans GotenbergBundle et vous aide à créer et déployer votre Custom Builder de la meilleur façon possible.

Cela pourrait aussi vous intéresser

Nicolas Grekas standing on stage at SymfonyLive Paris 2026
Jules Daunay

SymfonyLive Paris 2026 : IA et retrouvailles au sommet pour la Team SensioLabs

Le rideau vient de tomber sur le SymfonyLive Paris 2026, et on a encore des étoiles ✨ (et des lignes de code) plein les yeux. En tant que créateur de Symfony et sponsor historique, SensioLabs ne pouvait rêver d'un meilleur moment pour célébrer l'open source, l'innovation et, surtout, l'incroyable communauté qui nous entoure.

En savoir plus
Why PHP?
Silas Joisten

Pourquoi PHP est le meilleur choix pour le backend des logiciels d'entreprise ?

Découvrez pourquoi PHP est une technologie de premier plan pour les applications d'entreprise. Il offre des performances élevées, dispose d'un vaste écosystème de développeurs, permet de réduire les coûts opérationnels et fait preuve d'une fiabilité éprouvée.

En savoir plus
Formation Symfony UX FR
Elise Hamimi

Des interfaces plus modernes : maîtrisez Symfony UX avec la nouvelle formation officielle de SensioLabs

En quelques années à peine, Symfony UX a été plébiscité par les utilisateurs de Symfony. Parfaitement adapté aux priorités actuelles des développeurs, il permet de créer simplement des interfaces interactives et performantes, sans quitter le confort du framework. Il était temps de l'ajouter à notre catalogue de formations. C’est pourquoi nous lançons officiellement notre nouvelle formation Symfony UX.

En savoir plus
Fabien Potencier
Elise Hamimi

SymfonyCon Amsterdam 2025 : Notre bilan et les moments forts

Après une première édition emblématique en 2019, SymfonyCon a fait son grand retour à Amsterdam. Dès les premières minutes, on sentait l’énergie d’un rendez-vous très attendu : plus de 1 200 participants, 39 nationalités, les retrouvailles avec la communauté, de belles découvertes… et une ambiance de folie. Cette année, l’événement avait une saveur toute particulière puisqu’il s’agissait de l’édition spéciale anniversaire des 20 ans de Symfony. SensioLabs y était : on vous raconte tout !

En savoir plus
The SensioLabs team celebrating the 20th anniversary of Symfony with balloons
Jules Daunay

L'histoire continue : SensioLabs célèbre les 20 ans de Symfony

Le temps passe vite, surtout quand on écrit le futur du développement ! L’équipe de SensioLabs vient de souffler les 20 bougies du framework Symfony. Nous avons marqué le coup au bureau, mais la fête n'est pas terminée. Le rendez-vous est déjà pris pour une célébration XXL à SymfonyCon Amsterdam 2025 les 27 au 28 novembre.

En savoir plus
PHP 8.5 URI extension
Oskar Stark

La nouvelle extension URI de PHP 8.5 : Une révolution pour l'analyse des URL

PHP 8.5 introduit une nouvelle extension URI puissante qui modernise la gestion des URL. Grâce au support des standards RFC 3986 et WHATWG, la nouvelle classe Uri fournit des objets immuables, des interfaces fluides et une validation appropriée, résolvant ainsi toutes les limites de la fonction historique parse_url(). Cet articl présente des exemples pratiques avant/après et explique quand utiliser chaque standard.

En savoir plus
3 dog heads
Mathieu Santostefano

Venez avec votre propre client HTTP

Libérez-vous des dépendances rigides de vos SDK PHP. Dans cet article, apprenez à utiliser les normes PSR-7, PSR-17 et PSR-18, ainsi que la bibliothèque php-http/discovery, pour permettre à vos utilisateurs d'utiliser le client HTTP de leur choix, qu'il s'agisse de Guzzle, de Symfony HttpClient ou d'un autre. Un incontournable pour les développeurs PHP et Symfony.

En savoir plus
Blue sign on a building with several Now What? letters
Thibaut Chieux

Comment prioriser les messages lors du développement d'applications asynchrones avec Symfony Messenger

Le traitement asynchrone offre des avantages tels que la découplage des processus et des temps de réponse plus rapides, mais la gestion des priorités des messages peut s'avérer complexe. Pour traiter des tâches allant de la réinitialisation de mot de passe à des exports complexes, il est essentiel de garantir la livraison rapide des messages critiques. Cet article examine les problèmes fréquents liés au traitement asynchrone et propose des solutions avec Symfony Messenger pour optimiser votre application sans refonte majeure.

En savoir plus
Image