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.
Inhaltsverzeichnis
- 1. Wann sich ein eigenes Symfony Bundle lohnt
- 2. Die Standard-Verzeichnisstruktur eines Bundles
- 3. Die Bundle-Klasse als Einstiegspunkt
- 4. Composer-Paket: composer.json richtig aufsetzen
- 5. Registrierung im Ziel-Projekt
- 6. Services, Konfiguration und Autoconfigure im Bundle
- 7. Lokal entwickeln mit Composer Path-Repositories
- 8. Typische Fallstricke beim ersten eigenen Bundle
- 9. Bundle vs. Library vs. Copy-Paste im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.