Symfony Bundle von Grund auf erstellen: Eigene wiederverwendbare Komponenten bauen
AI generated
SF
{ }
Symfony · Bundles · Wiederverwendbarkeit · PHP
Symfony Bundle von Grund auf erstellen
Vom Copy-Paste-Code zur eigenen wiederverwendbaren Komponente

Wer denselben Code in mehreren Symfony-Projekten wiederholt, sollte ein eigenes Symfony Bundle bauen. Dieser Artikel zeigt die vollstaendige Verzeichnisstruktur, die Bundle-Klasse, das Composer-Paket und die Registrierung im Kernel, damit aus wiederkehrender Logik eine gepflegte, testbare Komponente wird.

18 Min. Lesezeit Bundle-Klasse · Composer · Kernel · Struktur Symfony 7.x · PHP 8.3+

1. Wann sich ein eigenes Symfony Bundle lohnt

Ein Symfony Bundle ist die offizielle Erweiterungseinheit des Frameworks: ein in sich geschlossenes Paket aus Code, Konfiguration, Ressourcen und optional Assets, das sich in beliebig viele Symfony-Anwendungen einbinden laesst. Der Punkt, an dem sich ein eigenes Bundle lohnt, ist erreicht, sobald ein Team dieselbe Logik in einem zweiten oder dritten Projekt findet: eine Audit-Log-Funktion, ein Multi-Mandanten-Filter, eine Integration zu einem internen Zahlungsdienst. Copy-Paste zwischen Projekten funktioniert kurzfristig, erzeugt aber langfristig divergierenden Code, weil Bugfixes nur in einem der Projekte landen.

Ein Symfony Bundle loest dieses Problem, indem es die Logik an genau einer Stelle versioniert und ueber Composer in jedes Projekt eingebunden wird. Anders als ein reines PHP-Paket bringt ein Bundle die volle Integration in den Symfony-Kernel mit: eigene Services im Container, eigene Konfigurationsoptionen, eigene Routen und Kommandos. Wer nur ein paar Hilfsfunktionen teilen will, braucht kein Bundle, sondern reicht eine normale Composer-Library. Sobald aber Dependency Injection, Konfigurationsvalidierung oder Kernel-Hooks noetig sind, ist ein eigenes Bundle die richtige Antwort.

Wichtig ist die Abgrenzung zur Kernel-Konfiguration selbst: Ein Bundle ist kein Ersatz fuer Architekturentscheidungen im Hauptprojekt, sondern eine Auslagerung von Querschnittsfunktionalitaet. Wer ein Symfony Bundle richtig plant, denkt zuerst an die oeffentliche API der Komponente, dann erst an die Implementierung. Diese Reihenfolge verhindert, dass interne Details des Bundles nach aussen durchsickern und spaetere Versionswechsel unnoetig schwer machen.

2. Die Standard-Verzeichnisstruktur eines Bundles

Seit Symfony 4 ist die Verzeichnisstruktur eines Bundles deutlich schlanker als frueher, folgt aber weiterhin einer klaren Konvention. Im Wurzelverzeichnis liegen composer.json, die Bundle-Klasse und ein src-Verzeichnis fuer den eigentlichen PHP-Code. Innerhalb von src gibt es typischerweise DependencyInjection fuer die Extension-Klasse und den Configuration-Tree, Resources beziehungsweise config fuer Service-Definitionen in YAML oder PHP, sowie fachliche Unterordner wie Service, Repository oder EventListener je nach Inhalt des Bundles.

Ein zentraler Unterschied zu einer Anwendung: Ein Symfony Bundle hat keinen eigenen Kernel und keine eigene config/packages-Struktur zur Laufzeit, es liefert lediglich Definitionen, die der Host-Kernel einliest. Deshalb liegen Service-Definitionen im Bundle meist unter config/services.php oder Resources/config/services.yaml, je nachdem, ob das Team PHP-basierte oder YAML-basierte Konfiguration bevorzugt. Tests gehoeren in ein separates tests-Verzeichnis auf oberster Ebene, damit sie nicht versehentlich mit ins produktive Autoloading wandern.


# Typical directory layout of a standalone Symfony Bundle
acme-audit-bundle/
├── composer.json
├── README.md
├── src/
│   ├── AcmeAuditBundle.php
│   ├── DependencyInjection/
│   │   ├── AcmeAuditExtension.php
│   │   └── Configuration.php
│   ├── EventListener/
│   │   └── AuditLogListener.php
│   ├── Service/
│   │   └── AuditLogger.php
│   └── Repository/
│       └── AuditEntryRepository.php
├── config/
│   └── services.php
└── tests/
    └── Service/
        └── AuditLoggerTest.php

3. Die Bundle-Klasse als Einstiegspunkt

Jedes Symfony Bundle braucht genau eine Klasse, die von Symfony\Component\HttpKernel\Bundle\Bundle erbt. Diese Klasse ist der Einstiegspunkt, den der Host-Kernel beim Booten anspricht. In den meisten Faellen reicht die leere Standardimplementierung, weil Symfony ueber Konventionen automatisch die passende Extension-Klasse im DependencyInjection-Namespace findet, sofern Name und Namespace zusammenpassen. Wer diesen Automatismus nicht nutzen will oder eigene Compiler Passes registrieren muss, ueberschreibt build() in der Bundle-Klasse.

Seit Symfony 5.3 gibt es zusaetzlich die Moeglichkeit, eine Extension direkt in der Bundle-Klasse per Attribut oder Methode zu definieren, ohne eine separate Extension-Klasse anzulegen, was fuer kleine Bundles den Boilerplate-Code reduziert. Fuer groessere Bundles mit vielen Konfigurationsoptionen bleibt die klassische Trennung zwischen Bundle-Klasse und Extension-Klasse jedoch sinnvoll, weil sie die Verantwortlichkeiten sauber trennt: Die Bundle-Klasse kuemmert sich um Kernel-Integration, die Extension-Klasse um das Laden der Konfiguration.


// src/AcmeAuditBundle.php
declare(strict_types=1);

namespace Acme\AuditBundle;

use Acme\AuditBundle\DependencyInjection\Compiler\AuditSubscriberPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
 * Entry point of the reusable audit logging bundle.
 */
final class AcmeAuditBundle extends Bundle
{
    public function build(ContainerBuilder $container): void
    {
        parent::build($container);

        // Register a custom compiler pass that collects tagged audit subscribers
        $container->addCompilerPass(new AuditSubscriberPass());
    }
}

4. Composer-Paket: composer.json richtig aufsetzen

Damit ein Symfony Bundle als eigenstaendiges Composer-Paket verteilt werden kann, braucht es einen eindeutigen Namen im Vendor/Paket-Format, eine PSR-4-Autoload-Deklaration und die richtigen require-Eintraege fuer symfony/framework-bundle und symfony/dependency-injection. Der type-Wert symfony-bundle ist optional, hilft aber Tools wie Composer Plugins, das Paket korrekt einzuordnen. Wichtig ist eine realistische Versionsbeschraenkung der Symfony-Kernkomponenten, damit das Bundle weder zu eng noch zu offen an eine Symfony-Version gebunden ist.

Ein haeufiger Fehler beim ersten eigenen Bundle: Zu viele konkrete Abhaengigkeiten werden in require statt in require-dev aufgenommen, etwa PHPUnit oder ein Test-Kernel. Das blaeht die Abhaengigkeitskette jedes Projekts auf, das dieses Symfony Bundle einbindet. Alles, was nur zum Testen des Bundles selbst gebraucht wird, gehoert konsequent nach require-dev, waehrend require nur die tatsaechlichen Laufzeitabhaengigkeiten des Bundles enthaelt.


{
  "name": "acme/audit-bundle",
  "type": "symfony-bundle",
  "description": "Reusable audit logging bundle for Symfony applications",
  "license": "MIT",
  "require": {
    "php": ">=8.2",
    "symfony/framework-bundle": "^6.4 || ^7.0",
    "symfony/dependency-injection": "^6.4 || ^7.0",
    "symfony/config": "^6.4 || ^7.0"
  },
  "require-dev": {
    "phpunit/phpunit": "^11.0",
    "symfony/phpunit-bridge": "^7.0"
  },
  "autoload": {
    "psr-4": { "Acme\\AuditBundle\\": "src/" }
  },
  "autoload-dev": {
    "psr-4": { "Acme\\AuditBundle\\Tests\\": "tests/" }
  }
}

5. Registrierung im Ziel-Projekt

Ein fertiges Symfony Bundle wird im Ziel-Projekt ueber die Datei config/bundles.php registriert, die Symfony Flex normalerweise automatisch pflegt, sobald ein offizieller Recipe-Eintrag existiert. Fuer interne, nicht auf Packagist.org veroeffentlichte Bundles gibt es kein automatisches Recipe, deshalb muss der Eintrag manuell ergaenzt werden. Der Eintrag ordnet der Bundle-Klasse die Umgebungen zu, in denen sie aktiv sein soll, meist all fuer alle Umgebungen oder eine Einschraenkung auf dev und test bei reinen Debugging-Bundles.

Nach der Registrierung muss der Cache geleert werden, damit der Container das neue Bundle und dessen Services beruecksichtigt. Ein haeufiger Stolperstein: Wird das Bundle nach dem require per Composer nicht in bundles.php eingetragen, laedt Symfony weder die Extension noch irgendwelche Services, ohne eine deutliche Fehlermeldung auszugeben, was die Fehlersuche unnoetig verlaengert.


// config/bundles.php in the host application
return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
    // Register the internal audit bundle for all environments
    Acme\AuditBundle\AcmeAuditBundle::class => ['all' => true],
];

6. Services, Konfiguration und Autoconfigure im Bundle

Ein gut geschnittenes Symfony Bundle registriert seine Services ueber eine Extension-Klasse, die vom Host-Container geladen wird, statt sich auf globale Autowiring-Regeln des Host-Projekts zu verlassen. Das gibt dem Bundle-Autor volle Kontrolle darueber, welche Services oeffentlich, welche privat und welche mit einem Alias fuer die Anwendung sichtbar sind. Autoconfigure innerhalb des Bundles funktioniert genauso wie in einer normalen Anwendung, muss aber explizit auf den Bundle-eigenen Namespace beschraenkt werden, um keine fremden Klassen des Host-Projekts versehentlich zu erfassen.

Konfigurationsoptionen, die das Ziel-Projekt anpassen darf, etwa der Name der Audit-Log-Tabelle oder die Aufbewahrungsdauer von Eintraegen, werden ueber den Configuration-Tree definiert und in der Extension-Klasse ausgewertet, um daraus Container-Parameter zu erzeugen. Diese Parameter stehen dann allen Services des Bundles zur Verfuegung und lassen sich im Ziel-Projekt unter einem eigenen Konfigurationsschluessel, etwa acme_audit, ueberschreiben.

7. Lokal entwickeln mit Composer Path-Repositories

Waehrend der Entwicklung eines neuen Symfony Bundles ist es unpraktisch, nach jeder Aenderung einen neuen Git-Tag zu pushen und im Ziel-Projekt composer update auszufuehren. Composer bietet dafuer Path-Repositories: Das Ziel-Projekt referenziert das Bundle ueber einen lokalen Dateisystempfad, Composer erstellt dabei standardmaessig einen Symlink, sodass Aenderungen im Bundle-Repository sofort im Ziel-Projekt sichtbar sind, ohne dass ein erneutes require noetig ist.

Dieser Workflow eignet sich besonders fuer Monorepo-nahe Setups, bei denen mehrere interne Symfony-Anwendungen und ein oder mehrere gemeinsame Bundles im selben Arbeitsbereich liegen. Sobald das Bundle stabil genug fuer echte Releases ist, wird die Path-Referenz durch eine echte Versionsangabe ersetzt, die auf ein privates Composer-Repository wie Private Packagist oder ein Satis-Setup zeigt.


{
  "repositories": [
    {
      "type": "path",
      "url": "../acme-audit-bundle",
      "options": { "symlink": true }
    }
  ],
  "require": {
    "acme/audit-bundle": "*"
  }
}

8. Typische Fallstricke beim ersten eigenen Bundle

Der haeufigste Fehler beim ersten eigenen Symfony Bundle ist eine zu enge Kopplung an das urspruengliche Projekt, aus dem der Code extrahiert wurde. Feste Werte, projektspezifische Entity-Klassen oder hartkodierte Routen-Namen verhindern, dass das Bundle in einem zweiten Projekt ueberhaupt funktioniert. Die Loesung ist konsequente Konfigurierbarkeit ueber den Configuration-Tree und Interfaces statt konkreter Klassen fuer alles, was projektspezifisch variieren kann.

Ein zweiter Fallstrick ist fehlende Versionsdisziplin: Wird ein Bundle ohne Semantic Versioning und ohne Changelog gepflegt, wissen abhaengige Projekte nicht, ob ein Update sicher ist. Ein dritter, oft unterschaetzter Fehler ist das Fehlen eigener Tests im Bundle-Repository. Ohne einen minimalen Test-Kernel, der das Bundle isoliert bootet, faellt oft erst im Host-Projekt auf, dass eine Aenderung die Konfiguration bricht, was die Fehlersuche erheblich erschwert.

9. Bundle vs. Library vs. Copy-Paste im Vergleich

Nicht jede Wiederverwendung von Code rechtfertigt ein vollwertiges Symfony Bundle. Die folgende Tabelle stellt die drei gaengigen Ansaetze gegenueber und zeigt, wann welcher sinnvoll ist.

Kriterium Copy-Paste Reine Composer-Library Symfony Bundle
Container-Integration Keine Manuell im Host noetig Automatisch ueber Extension
Konfigurierbarkeit Keine Nur per Konstruktor Configuration-Tree mit Validierung
Wartungsaufwand Divergiert pro Projekt Zentral, ohne Kernel-Hooks Zentral, mit voller Integration
Geeignet fuer Nichts, nur Uebergangsloesung Reine Utility-Funktionen Services, Routen, Kommandos, Events
Testaufwand initial Keiner Gering Hoeher, Test-Kernel noetig

Die Tabelle zeigt: Ein Symfony Bundle hat einen hoeheren initialen Aufwand als eine einfache Library, zahlt sich aber ab dem dritten Verwendungsprojekt und bei jedem Bedarf an Kernel-Integration klar aus. Wer unsicher ist, sollte mit einer Library starten und erst dann zum Bundle wechseln, wenn Konfigurierbarkeit oder Service-Registrierung tatsaechlich gebraucht werden.

Mironsoft

Symfony Architektur, Bundle-Entwicklung und interne Komponenten-Bibliotheken

Wiederkehrender Code in mehreren Symfony-Projekten?

Wir extrahieren gemeinsame Logik aus bestehenden Symfony-Anwendungen in eigene, versionierte Bundles, mit sauberer Konfiguration, Tests und Composer-Registrierung fuer euer gesamtes Team.

Bundle-Extraktion

Analyse bestehender Projekte und Herauslösen wiederverwendbarer Komponenten

Composer-Setup

Private Packagist oder Satis-Repository fuer interne Bundles einrichten

Langfristige Pflege

Versionierung, Changelogs und Tests fuer stabile Bundle-Releases

10. Zusammenfassung

Ein eigenes Symfony Bundle lohnt sich, sobald derselbe Code in mehr als einem Projekt gebraucht wird und Kernel-Integration wie eigene Services, Konfiguration oder Kommandos noetig ist. Die Standard-Verzeichnisstruktur mit src, DependencyInjection und config sorgt fuer Konsistenz mit dem Symfony-Oekosystem. Die Bundle-Klasse ist der Einstiegspunkt, die Extension-Klasse laedt Services und wertet Konfiguration aus. Composer mit sauber getrennten require- und require-dev-Abhaengigkeiten macht das Bundle als eigenstaendiges Paket verteilbar.

Fuer die lokale Entwicklung sind Composer Path-Repositories der schnellste Weg, ohne staendige Releases. Beim Uebergang von Copy-Paste zu einem echten Bundle lohnt sich der Blick auf Konfigurierbarkeit ueber Interfaces statt konkreter Klassen, damit das Bundle wirklich in mehreren Projekten funktioniert. Wer diese Grundlagen beachtet, baut ein Symfony Bundle, das langfristig gepflegt und ohne Reibung in neue Projekte eingebunden werden kann.

Symfony Bundle von Grund auf erstellen — Das Wichtigste auf einen Blick

Wann sich ein Bundle lohnt

Ab dem zweiten oder dritten Projekt mit derselben Logik, sobald Kernel-Integration noetig ist.

Kernstruktur

Bundle-Klasse als Einstiegspunkt, Extension-Klasse fuer Services und Konfiguration.

Composer-Paket

PSR-4-Autoload, saubere Trennung von require und require-dev, realistische Versionsbeschraenkung.

Lokale Entwicklung

Composer Path-Repositories mit Symlink statt staendiger Git-Tags waehrend der Entwicklung.

11. FAQ: Symfony Bundle von Grund auf erstellen

1Wann eigenes Bundle statt Code kopieren?
Sobald dieselbe Logik in einem zweiten oder dritten Projekt gebraucht wird. Copy-Paste fuehrt langfristig zu divergierendem Code.
2Braucht jedes Bundle eine Extension-Klasse?
Nein, bei einfachen Konventionen findet Symfony sie automatisch. Bei komplexer Konfiguration lohnt sich eine eigene Klasse.
3Wie teste ich ein Bundle isoliert?
Mit einem minimalen Test-Kernel im tests-Verzeichnis, der nur das Bundle selbst laedt.
4require vs. require-dev in composer.json?
require nur echte Laufzeitabhaengigkeiten, Testwerkzeuge gehoeren nach require-dev.
5Internes Bundle ohne Packagist einbinden?
Via Composer Path-Repositories lokal, oder ueber Private Packagist beziehungsweise Satis fuer echte Releases.
6Muss bundles.php manuell gepflegt werden?
Nur bei internen Bundles ohne Flex-Recipe. Oeffentliche Bundles werden automatisch eingetragen.
7Zu enge Kopplung vermeiden?
Configuration-Tree fuer projektspezifische Werte nutzen, Interfaces statt konkreter Klassen verwenden.
8Welche Symfony-Version in composer.json?
Eine realistische Spanne wie ^6.4 || ^7.0 statt einer einzelnen fixen Version.
9Eigene Routen und Kommandos im Bundle?
Ja, ueber eine eigene Routing-Datei und normale Services mit dem console.command-Tag.
10Haeufigster Anfaengerfehler?
Fehlende Konfigurierbarkeit: hartkodierte Werte statt Configuration-Tree, wodurch das Bundle nur in einem Projekt funktioniert.