Create a Custom Builder – Eine GotenbergBundle Story

· Steven Renaux · Expertise · 7 Minuten zum Lesen
A man sculpting a rock with PDF written on it

In einem vorherigen Artikel haben wir uns angeschaut, wie du mit nur wenigen Zeilen Code dein erstes PDF generierst. Dabei kamen Gotenberg und das GotenbergBundle zum Einsatz – ein Symfony Bundle, das die HTTP API von Gotenberg kapselt, um HTML- oder Office-Dateien in PDFs umzuwandeln oder Screenshots zu erstellen. Das war ein super Anfang. Aber was passiert, wenn deine Applikation viele verschiedene PDFs erzeugen muss, jedes mit eigenem Layout, Style und Daten?

Vorheriger Artikel >> Hier finden

Schnell landest du bei duplizierter Konfiguration, überladenen Services und einer schwer wartbaren Conditional Logic. Dieser Artikel setzt genau dort an. Wir bauen einen custom Builder – eine dedizierte Klasse, die die gesamte Konfiguration und Logik für einen speziellen PDF-Typ kapselt. Am Ende wird dein Controller auf einen einzigen, aussagekräftigen Aufruf reduziert, und jeder PDF-Typ lebt in seiner eigenen, sauberen und testbaren Klasse.

Was ist der Zweck eines Custom Builders?

Ganz einfach: Dir das Leben leichter zu machen, wenn du mehrere PDFs generierst.

Wenn du in deiner Anwendung verschiedene PDFs erstellst, kann die Konfiguration oft nicht geteilt werden – meistens wegen unterschiedlicher Styles.

Hier kommt das Builder Pattern ins Spiel. Es ist ein Design Pattern, das dir hilft, komplexe Objekte Schritt für Schritt aufzubauen und dabei die Konstruktion von der finalen Repräsentation trennt. In unserem Kontext erlaubt es dir, die gesamte Konfiguration und Logik für einen spezifischen PDF-Typ in eine eigene Builder-Klasse auszulagern.

Durch einen custom Builder vermeidest du unübersichtliche Logik in deinen Services oder Code-Duplizierung. Stattdessen kümmert sich jeder Builder sauber um seine eigene Konfiguration, was deine Codebase wartbarer und erweiterbar macht.

Schritt 1: Dependencies aktualisieren

TL;DR: Siehe diesen Commit

Zuerst musst du deine Dependencies aktualisieren, um die neueste Version des GotenbergBundles zu erhalten – endlich die v1.2, die kein Experimental-Status mehr hat. Yeah! 🍾

composer require sensiolabs/gotenberg-bundle:1.2.*

Schritt 2: Eine custom Builder Klasse erstellen

TL;DR: Siehe diesen Commit

Erstelle eine custom Builder Klasse, die AbstractBuilder erweitert und das BuilderAssetInterface implementiert.

<?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.
   }
}

Alle nativen Builder erweitern AbstractBuilder, welcher die generate-Methode definiert. Er speichert auch alle benötigten Konfigurationen wie Margins, Width etc. und bereitet das Payload vor, bevor es an die Gotenberg API gesendet wird.

Da wir Assets in das Twig-Template einfügen, müssen wir das BuilderAssetInterface implementieren.

Füge nun das Attribut #[WithBuilderConfiguration('pdf', 'invoice')] oben an der Klasse hinzu. 

<?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
}

Dieses Attribut hilft dir dabei, eine semantische Konfiguration für diesen Builder zu erhalten. Das erste Argument ordnet den neuen Builder-Typ der Sektion 'pdf' oder 'screenshot' zu. Das zweite Argument ist der Name deiner Wahl.

Lass uns die Methoden implementieren. Für die getEndpoint-Methode nutzen wir die bestehende Konstante HtmlPdfBuilder::ENDPOINT, da der API-Endpoint derselbe ist.

Wenn du PDFs generieren willst, stehen folgende Traits zur Verfügung:

  • AssetTrait Methoden zum Hinzufügen von Assets.

  • ContentTrait Methoden für verschiedene Content-Parts (Header, Footer, Content).

  • CookieTrait Setzen und Weiterleiten von Cookies.

  • CustomHttpHeadersTrait Eigene HTTP-Header hinzufügen.

  • EmulatedMediaTypeTrait Emulation von Screen oder Print.

  • FailOnTrait Verhalten bei ungültigen Status-Codes.

  • PdfPagePropertiesTrait PDF-Rendering anpassen (Layout).

  • PerformanceModeTrait Nicht darauf warten, dass das Chromium-Netzwerk idle ist.

  • WaitBeforeRenderingTrait Delay vor der Konvertierung hinzufügen.

  • DownloadFromTrait Externe Ressourcen einbinden.

  • MetadataTrait Metadaten hinzufügen.

  • PdfFormatTrait PDF-Formate.

  • SplitTrait PDFs aufteilen.

  • WebhookTrait Webhooks nutzen.

Alle diese sind im ChromiumPdfTrait kombiniert. Falls du Builder für Office oder Screenshots bauen willst, findest du die passenden Traits im Quellcode des Bundles auf GitHub.

Wir fügen das AssetTrait hinzu, das die addAsset-Methode für uns implementiert. Diese Methode speichert Assets aus Twig-Templates oder solche, die "on the fly" hinzugefügt werden.

Das ContentTrait ermöglicht die Nutzung von Methoden wie header und content im Controller oder Footer, wie sie in der sensiolabs_gotenberg.yaml konfiguriert sind.

Das PdfPagePropertiesTrait ist für Margins, Landscape-Modus, Papierformat etc. zuständig

<?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;
   }
}

Schritt 3: Konfigurationsdatei aktualisieren

TL;DR: Siehe diesen Commit

Du musst nur den Namen des Builders anpassen. Nutze den Namen, den du im WithBuilderConfiguration-Attribut vergeben hast.

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

Schritt 4: Controller aktualisieren

TL;DR: Siehe diesen Commit

Genau wie bei der Konfiguration müssen nur 2 Zeilen angepasst werden.

#[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()
   ;
}

Schritt 5: Bringen wir es zum Laufen 💪

TL;DR: Siehe diesen Commit

Wenn du eine Version kleiner als v1.2 verwendest, musst du den Configurator manuell hinzufügen:

# services.yaml

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

Seit Version v1.0.1 musst du das nicht mehr selbst machen. Registriere ihn einfach in der build-Methode deiner Kernel-Klasse (siehe bei Bedarf die Dokumentation zum Symfony-Kernel).

Wir holen uns die sensiolabs_gotenberg Extension und rufen registerBuilder mit dem FQCN des custom Builders auf. Et voilà!

class Kernel extends BaseKernel
{
   use MicroKernelTrait;


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

Und alles funktioniert wie am Schnürchen.

Schritt 6: Warte... wir haben den eigentlichen Punkt vergessen! 🤦

TL;DR: Siehe diesen Commit

Huch, wir waren so auf den neuen Builder fokussiert, dass wir den eigentlichen Zweck vergessen haben: die Logik zu kapseln!

Aktuell weiß der Controller noch zu viel – er holt die Rechnungsdaten, übergibt sie dem Header, dem Content... Genau diese Verantwortung sollte im InvoicePdfBuilder liegen.

Verschieben wir das alles.

#[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
        ];
    }
}

Jetzt wird der Controller extrem simpel, genau wie es Symfony Best Practices vorsehen.

#[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()
+   ;
}

Der Controller weiß nichts mehr über Templates oder Datenstrukturen. Er sagt nur noch: "Gib mir das Rechnungs-PDF" und der Builder erledigt den Rest. Das ist das Ziel! 🎯

Fazit

Wenn du nun php bin/console debug:config sensiolabs_gotenberg ausführst, siehst du die Konfiguration deines custom Builders sauber integriert.

Aber was noch wichtiger ist:

  • Der Controller ist clean. Keine Ahnung mehr von Templates oder Rendering-Logik.

  • Die Logik ist gekapselt. Der InvoicePdfBuilder besitzt alles, was mit Rechnungs-PDFs zu tun hat. Ändert sich das Layout morgen, weißt du genau, wo du ansetzen musst.

  • Die Konfiguration ist semantisch. Dank #[WithBuilderConfiguration('pdf', 'invoice')] list sich deine Datei sensiolabs_gotenberg.yaml ganz natürlich und du knnest Ränder, Fusszeile oder Papierformat für jeden Builder ohn Umwge konfigurieren.

  • Es ist skalierbar. Benötigst du einen ReportPdfBuilder? Befolge dazu dieselben Schritte. Jeder PDF-Typ verfügt über einen eigenen Builder und Konfigurationsblock und beeinträchtigt die anderein in keiner Weise.

Das ist das Builder Pattern in Bestform im Symfony-Kontext. Probier es in deinem nächsten Projekt aus! 🚀

Bist du bereit, deinen eigenen Custom Builder zu erstellen?

Unser Team aus Symfony- und PHP-Experten, das maßgeblich an der Entwicklung des GotenbergBundle beteiligt ist, unterstützt dich gerne dabei, deinen Custom Builder optimal zu erstellen und zu implementieren.

Das könnte dich auch interessieren

Nicolas Grekas standing on stage at SymfonyLive Paris 2026
Jules Daunay

SymfonyLive Paris 2026: KI-Revolution und Gipfeltreffen für das SensioLabs-Team

Der Vorhang für die SymfonyLive Paris 2026 ist gefallen, und wir haben immer noch Sterne ✨ (und Codezeilen) in den Augen. Als Entwickler von Symfony und langjähriger Hauptsponsor hätte sich SensioLabs keinen besseren Moment wünschen können, um Open Source, Innovation und vor allem die unglaubliche Community um uns herum zu feiern.

Mehr erfahren
Why PHP?
Silas Joisten

Warum PHP das Unternehmensweb antreibt und warum diese strategische Entscheidung zählt

PHP gehört weiterhin zu den zuverlässigsten, skalierbarsten und kosteneffizientesten Backend Technologien für Unternehmenssysteme.

Mehr erfahren
Symfony UX training
Elise Hamimi

Lerne Symfony UX mit dem neuen offiziellen Training von SensioLabs

In nur wenigen Jahren ist Symfony UX bei Symfony-Entwickler richtig beliebt geworden. Perfekt passend zu den heutigen Prioritäten, hilft es dir, interaktive und performante Interfaces zu bauen – ohne den Komfort des Frameworks zu verlassen. Zeit also, das Thema in unseren Schulungskatalog aufzunehmen. Genau deshalb starten wir offiziell unser neues Symfony-UX-Training.

Mehr erfahren
Fabien Potencier
Elise Hamimi

SymfonyCon Amsterdam 2025: Unser Rückblick und die Highlights

Nach einer legendären ersten Ausgabe im Jahr 2019 feierte die SymfonyCon ihr großes Comeback in Amsterdam. Von Anfang an war die Energie einer mit Spannung erwarteten Konferenz zu spüren: mehr als 1.200 Teilnehmer, 39 Nationalitäten, das größte Treffen der Symfony-Community des Jahres, großartige Entdeckungen ... und eine ausgelassene Atmosphäre. Dieses Jahr war etwas ganz Besonderes, denn es war das 20-jährige Jubiläum von Symfony. SensioLabs war dabei: Wir berichten Ihnen ausführlich über unsere Erfahrungen dort!

Mehr erfahren
The SensioLabs team celebrating the 20th anniversary of Symfony with balloons
Jules Daunay

Die Geschichte geht weiter: SensioLabs feiert 20 Jahre Symfony

Die Zeit vergeht wie im Flug – besonders, wenn man an der Zukunft der Entwicklung schreibt! Das SensioLabs-Team hat gerade die 20 Kerzen des Symfony-Frameworks ausgeblasen. Wir haben den Anlass im Büro gefeiert, doch die Party ist noch nicht vorbei. Das Datum für eine XXL-Feier steht bereits fest: die SymfonyCon Amsterdam vom 27. bis 28. November 2025.

Mehr erfahren
PHP 8.5 URI extension
Oskar Stark

PHP 8.5's neue URI-Erweiterung: Ein Game-Changer für URL-Parsing

PHP 8.5 führt eine leistungsstarke neue URI-Erweiterung ein, die die URL-Verarbeitung modernisiert. Mit Unterstützung für RFC 3986 und WHATWG-Standards bietet die neue Uri-Klasse unveränderliche Objekte, fluent Interfaces und korrekte Validierung - und behebt alle Einschränkungen der veralteten parse_url()-Funktion. Dieser Leitfaden zeigt praktische Vorher/Nachher-Beispiele und erklärt, wann welcher Standard zu verwenden ist.

Mehr erfahren
3 dog heads
Mathieu Santostefano

Lass die Nutzer des SDK ihren eigenen HTTP-Client nutzen

Befreie dich von starren Abhängigkeiten in deinen PHP-SDKs. Erfahre, wie du die Standards PSR-7, PSR-17 und PSR-18 zusammen mit PHP-HTTP/Discovery nutzt, um deinen Benutzern die Verwendung ihres bevorzugten HTTP-Clients zu ermöglichen – sei es Guzzle, Symfony HttpClient oder ein anderes Tool. Ein Muss für PHP- und Symfony-Entwickler.

Mehr erfahren
Blue sign on a building with several Now What? letters
Thibaut Chieux

Wie man Nachrichten beim Aufbau asynchroner Anwendungen mit dem Symfony-Messenger priorisiert

Die asynchrone Verarbeitung bietet Vorteile wie entkoppelte Prozesse und schnellere Reaktionszeiten. Die Verwaltung von Nachrichtenprioritäten kann jedoch zu einer Herausforderung werden. Bei Aufgaben, die vom Zurücksetzen von Passwörtern bis hin zu komplexen Exporten reichen, ist die rechtzeitige Zustellung kritischer Nachrichten unerlässlich. Dieser Artikel befasst sich mit häufigen Problemen bei der asynchronen Verarbeitung und zeigt Lösungen mit Symfony Messenger auf, mit denen Sie Ihre Anwendung ohne umfangreiches Refactoring optimieren können.

Mehr erfahren
Image