SF
{ }
Symfony · Kernel · Bundles · DI · PHP 8.4
Symfony Kernel & Custom Bundles:
in 2026 noch sinnvoll?

Custom Bundles galten lange als der Standard für wiederverwendbare Symfony-Module. Mit PHP-Config-Klassen, Symfony Flex und modernen DI-Patterns gibt es heute leichtgewichtigere Wege — aber für öffentliche Pakete, komplexe Container-Manipulation und Compiler Passes sind Bundles nach wie vor das richtige Werkzeug.

17 Min. Lesezeit Kernel · Bundles · Compiler Passes · Extensions · PHP-Config Symfony 7.x · PHP 8.4

1. Symfony Kernel-Architektur verstehen

Der Symfony Kernel ist der zentrale Bootstrapping-Mechanismus jeder Symfony-Anwendung. Er bootstrappt den Dependency Injection Container, registriert Bundles, kompiliert den Container und stellt ihn für die gesamte Anwendungslaufzeit bereit. Die Methode registerBundles() im Kernel gibt eine Liste aller aktiven Symfony Bundles zurück — in welcher Reihenfolge sie registriert sind, bestimmt, welche Konfiguration welche andere überschreiben darf. Der Container-Kompilierungsschritt ist der Moment, in dem alle Bundle-Extensions, Service-Definitionen und Compiler Passes ausgeführt werden.

Der Kernel durchläuft beim Start mehrere klar definierte Phasen: Zuerst werden alle Symfony Bundles über registerBundles() instanziiert. Dann ruft der Kernel für jedes Bundle build(ContainerBuilder) auf, wo Compiler Passes registriert werden. Anschließend lädt er Konfigurationsdateien und ruft die Bundle-Extensions auf, die Services in den Container-Builder eintragen. Schließlich werden alle Compiler Passes in der festgelegten Reihenfolge ausgeführt, und der Container wird kompiliert und gecacht. Dieser Ablauf ist in jedem Symfony-Projekt identisch — Custom Bundles integrieren sich in genau diese Phasen, was sie sowohl mächtig als auch komplex macht.

2. Aufbau eines Custom Bundles

Ein Custom Bundle in Symfony ist eine PHP-Klasse, die AbstractBundle (Symfony 6.1+) oder Bundle erbt. Die neue AbstractBundle-Basisklasse vereinfacht die frühere Aufteilung in Bundle-Klasse, Extension-Klasse und Configuration-Klasse erheblich: Alles findet nun in einer einzigen Klasse statt. Die Methode configure(DefinitionConfigurator) definiert das Konfigurations-Schema, loadExtension() lädt Service-Definitionen und injiziert Konfigurationswerte, und build(ContainerBuilder) registriert Compiler Passes. Diese Konsolidierung macht Custom Bundles in 2026 deutlich einfacher zu schreiben als noch in Symfony 4 oder 5.

Die Verzeichnisstruktur eines gut organisierten Custom Bundles folgt einer klaren Konvention: Die Bundle-Klasse liegt im Wurzelverzeichnis des Pakets, Services werden in Resources/config/ oder per PHP-Closures direkt in der Bundle-Klasse registriert. Für öffentliche Pakete auf Packagist gehört das Bundle in einen eigenen Composer-Namespace, der per composer.json autoloaded wird. Für Bundles, die nur innerhalb einer Anwendung genutzt werden, liegt der Code unter src/Bundle/ oder direkt unter src/ als gewöhnlicher Service — was die Frage aufwirft, ob ein Bundle hier überhaupt nötig ist.


<?php

declare(strict_types=1);

namespace Mironsoft\NotificationBundle;

use Mironsoft\NotificationBundle\DependencyInjection\Compiler\NotificationChannelPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

/**
 * Symfony Bundle for multi-channel notifications.
 * Uses the modern AbstractBundle API (Symfony 6.1+).
 */
final class MironsoftNotificationBundle extends AbstractBundle
{
    /**
     * Define the configuration schema accepted by this bundle.
     */
    public function configure(\Symfony\Component\Config\Definition\Builder\DefinitionConfigurator $definition): void
    {
        $definition->rootNode()
            ->children()
                ->scalarNode('default_channel')->defaultValue('email')->end()
                ->booleanNode('queue_notifications')->defaultTrue()->end()
                ->arrayNode('channels')
                    ->scalarPrototype()->end()
                ->end()
            ->end();
    }

    /**
     * Load services and inject resolved configuration values.
     *
     * @param array<string, mixed> $config
     */
    public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
    {
        $container->import('../config/services.php');

        // Inject resolved config values directly into service definitions
        $builder->getDefinition('mironsoft_notification.dispatcher')
            ->setArgument('$defaultChannel', $config['default_channel'])
            ->setArgument('$queueEnabled', $config['queue_notifications']);
    }

    /**
     * Register compiler passes that run at container compile time.
     */
    public function build(ContainerBuilder $container): void
    {
        parent::build($container);
        $container->addCompilerPass(new NotificationChannelPass());
    }
}

3. Bundle-Extension: Services registrieren

Mit dem modernen AbstractBundle-Ansatz werden Services direkt in loadExtension() registriert — entweder über eine separate PHP-Services-Konfigurationsdatei oder inline. Die injizierte Konfiguration ist bereits validiert und mit Default-Werten vervollständigt, bevor loadExtension() aufgerufen wird. Das ermöglicht bedingte Service-Registrierung: Wenn ein optionales Feature in der Konfiguration deaktiviert ist, wird der zugehörige Service gar nicht erst in den Container eingetragen. Das ist sauberer als Service-Definitionen mit aktivierbaren Tags oder Conditional-Dekoratoren.

Ein häufiges Pattern in Custom Bundles ist die Registrierung von Extension-Points über Tagged Services. Das Bundle definiert ein Interface und ein Tag, Anwendungscode implementiert das Interface und taggt Services damit, und ein Compiler Pass sammelt alle getaggten Services ein und injiziert sie in den zentralen Service. Das klassische Beispiel: Ein Notification-Bundle definiert NotificationChannelInterface und den Tag mironsoft.notification.channel. Anwendungs-Services, die dieses Interface implementieren und diesen Tag tragen, werden automatisch beim nächsten Container-Compile als Kanäle registriert — ohne dass eine manuelle Registrierung im Bundle-Code nötig wäre.

4. Compiler Passes: Container-Manipulation zur Compile-Time

Compiler Passes sind die mächtigste Funktion von Symfony Bundles — und der Hauptgrund, warum Bundles in komplexen Szenarien noch sinnvoll sind. Ein Compiler Pass implementiert CompilerPassInterface und erhält Zugriff auf den vollständig aufgebauten ContainerBuilder, bevor er kompiliert und gecacht wird. Das erlaubt strukturelle Änderungen am Container, die mit normaler Konfiguration nicht möglich sind: Service-Definitionen nachträglich modifizieren, Services mit bestimmten Tags einsammeln und in andere Services injizieren, dekorative Chains aufbauen und Definitions klonen oder löschen.

Das Tagged-Services-Pattern über Compiler Passes ist eines der elegantesten Muster in der Symfony Bundle-Architektur. Der Pass iteriert über alle Services mit einem bestimmten Tag, sammelt ihre Service-IDs und injiziert sie als Argument in den zentralen Dispatcher. Das Resultat: Entwickler können neue Kanäle, Handler oder Transformer hinzufügen, indem sie eine Klasse mit einem Interface implementieren und den Service korrekt taggen — ohne den Bundle-Code oder die Dispatcher-Klasse zu modifizieren. Symfony's eigene Bundles nutzen dieses Pattern intensiv: Event-Listener, Console-Commands, Security-Voter und Form-Types werden alle über getaggte Services und Compiler Passes im Kernel registriert.


<?php

declare(strict_types=1);

namespace Mironsoft\NotificationBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Collects all tagged notification channels and injects them into the dispatcher.
 * Runs at container compile time — not at runtime.
 */
final class NotificationChannelPass implements CompilerPassInterface
{
    public const TAG = 'mironsoft.notification.channel';

    /**
     * Gather all channel services by tag and inject them into the dispatcher.
     */
    public function process(ContainerBuilder $container): void
    {
        if (!$container->has('mironsoft_notification.dispatcher')) {
            return;
        }

        $dispatcher = $container->findDefinition('mironsoft_notification.dispatcher');

        // Find all services tagged with our channel tag, sorted by priority
        $taggedServices = $container->findTaggedServiceIds(self::TAG, throwOnAbstract: true);

        $channels = [];
        foreach ($taggedServices as $serviceId => $tags) {
            foreach ($tags as $attributes) {
                $priority = (int) ($attributes['priority'] ?? 0);
                $channels[$priority][] = new Reference($serviceId);
            }
        }

        // Sort by priority descending, flatten and inject
        krsort($channels);
        $flatChannels = array_merge(...$channels);

        $dispatcher->setArgument('$channels', $flatChannels);
    }
}

5. PHP-Config-Klassen als Bundle-Alternative

Seit Symfony 5.3 gibt es eine leichtgewichtigere Alternative zum vollständigen Custom Bundle: PHP-Config-Klassen. Statt einer Bundle-Klasse mit Extension und Compiler Pass definiert man eine PHP-Klasse, die ContainerConfigurator verwendet und per Import in config/services.php eingebunden wird. Für anwendungsinterne Code-Organisation — also Features, die nicht als Paket verteilt werden — ist das in den meisten Fällen ausreichend und erheblich einfacher zu verstehen und zu warten.

Die PHP-Config-Klassen unterstützen Tags, Service-Definitionen, Parameter und alle normalen DI-Features. Was sie nicht können: Das Konfigurationsschema des Bundles definieren (mit Validierung und Default-Werten), Compiler Passes registrieren und in fremde Bundle-Container eingreifen. Für Projekte, bei denen ein Merkmal nur innerhalb der Anwendung existiert und kein öffentliches Konfigurations-Interface braucht, ist der PHP-Config-Ansatz einfacher, schneller zu entwickeln und leichter zu debuggen. Die Grenze zwischen Bundle und PHP-Config ist damit heute klarer: Bundles für öffentliche, konfigurierbare Pakete — PHP-Config für anwendungsinternen Querschnitts-Code.

6. Symfony Flex und Recipes statt Bundle-Boilerplate

Symfony Flex hat die Art verändert, wie Symfony Bundles installiert werden. Eine Recipe ist ein JSON-Manifest, das beim composer require automatisch Konfigurationsdateien anlegt, Umgebungsvariablen in .env einträgt und das Bundle in config/bundles.php registriert. Das macht das Onboarding-Erlebnis eines öffentlichen Bundles radikal einfacher — der Nutzer führt einen einzigen Composer-Befehl aus und hat eine funktionierende Basiskonfiguration, ohne manuell Dateien anzulegen oder Bundle-Klassen zu registrieren.

Für den Entscheid, ob ein Projekt ein vollständiges Custom Bundle oder eine einfachere Lösung braucht, ist die Flex-Perspektive hilfreicher als die technische: Wenn das Ergebnis mit einem composer require installiert und über YAML oder PHP-Config konfiguriert werden soll, braucht man ein Bundle. Wenn der Code nur intern im Projekt strukturiert werden soll, reichen Services, PHP-Config-Klassen und Symfony-Conventions. Das ist die pragmatische Antwort auf die Frage, ob Custom Bundles in 2026 noch sinnvoll sind: Ja — aber nur für Pakete, die öffentlich verteilt, konfigurierbar und unabhängig von einer konkreten Anwendung sein müssen.

7. Wann ein Custom Bundle noch die richtige Wahl ist

Ein Custom Bundle ist die richtige Wahl in vier konkreten Szenarien. Erstens: Das Modul wird als Composer-Paket veröffentlicht und von anderen Projekten installiert. Nur Bundles können über Flex-Recipes automatisch konfiguriert werden. Zweitens: Das Modul stellt ein Konfigurations-Interface bereit, das andere Entwickler in ihrer config/packages/-Datei konfigurieren sollen — mit Validierung, Default-Werten und IDE-Unterstützung. Drittens: Der Code muss Compiler Passes registrieren, um in andere Bundle-Definitionen einzugreifen oder Tagged-Services zu verarbeiten. Viertens: Das Modul muss in verschiedenen Symfony-Versionen oder Kombinationen mit anderen Bundles getestet werden — Bundles haben einen klaren Integrations-Test-Ansatz mit KernelTestCase.

Wenn keines dieser vier Szenarien zutrifft, ist ein Custom Bundle Overhead. Ein Projekt-Feature, das nur intern genutzt wird, braucht keine Bundle-Klasse. Symfony Autowiring, Autoconfiguration und PHP-Config-Klassen lösen 95 % der Code-Organisations-Anforderungen ohne das Bundle-Konzept. Der häufigste Fehler: Entwickler bauen Bundles für Features, die nur in einem Projekt existieren und nie als Paket verteilt werden. Das führt zu unnötiger Komplexität ohne Mehrwert — Bundle-Verzeichnis, Extension-Klasse, Configuration-Klasse, alles für Code, der genauso gut direkt unter src/ liegen könnte.


<?php

declare(strict_types=1);

// ALTERNATIVE TO A BUNDLE: PHP Config class for internal application features
// src/Config/NotificationConfig.php — no Bundle class needed

namespace App\Config;

use App\Notification\EmailChannel;
use App\Notification\SmsChannel;
use App\Notification\NotificationDispatcher;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

/**
 * Registers all notification services without a Symfony Bundle.
 * Suitable for application-internal features not distributed as a package.
 */
function configureNotifications(ContainerConfigurator $container): void
{
    $services = $container->services();

    // Register channels with autoconfiguration tags
    $services
        ->set(EmailChannel::class)
        ->tag('app.notification.channel', ['priority' => 100]);

    $services
        ->set(SmsChannel::class)
        ->tag('app.notification.channel', ['priority' => 50]);

    // Dispatcher receives channels via tagged_iterator — no Compiler Pass needed
    $services
        ->set(NotificationDispatcher::class)
        ->arg('$channels', tagged_iterator('app.notification.channel'));
}

// In config/services.php:
// (static function(ContainerConfigurator $container): void {
//     \App\Config\configureNotifications($container);
// })($container);

8. Bundle-Konfiguration in Tests validieren

Eines der häufig vernachlässigten Aspekte von Custom Bundles ist die Testbarkeit der Konfiguration selbst. Symfony bietet mit der Extension\ExtensionInterface-Testinfrastruktur und dem ContainerBuilder eine Möglichkeit, Bundle-Extensions zu testen, ohne den vollen Kernel zu booten. Man instanziiert den ContainerBuilder direkt, ruft die Extension-Methode auf und prüft, ob die erwarteten Service-Definitionen vorhanden sind. Das ist schneller als KernelTestCase und isoliert Konfigurationsfehler von Anwendungsfehlern.

Für Compiler-Pass-Tests gilt dasselbe Prinzip: ContainerBuilder aufbauen, Services mit den erwarteten Tags eintragen, den Compiler Pass ausführen und prüfen, ob die Dispatcher-Definition das richtige Argument enthält. Diese Tests laufen in Millisekunden und brauchen keinen echten Symfony-Kernel. Mit dieser Test-Coverage ist ein Custom Bundle gegen Konfigurationsänderungen abgesichert, bevor es als Composer-Paket veröffentlicht wird. Fehlende Tags, falsche Service-IDs und inkompatible Konfigurationswerte werden im CI sichtbar, nicht erst im Produktionsbetrieb.

9. Bundle vs. PHP-Config vs. Service-Provider im Vergleich

Die drei häufigsten Ansätze zur Code-Organisation in Symfony-Projekten unterscheiden sich in Komplexität, Flexibilität und dem richtigen Anwendungsfall. Der direkte Vergleich hilft bei der Entscheidung.

Merkmal Custom Bundle PHP-Config-Klasse Autowiring + Tags
Öffentlich verteilbar Ja, mit Flex Recipe Nein Nein
Konfigurationsschema Ja, mit Validierung Nur per Parameter Nein
Compiler Passes Ja Nein Nur via tagged_iterator
Komplexität Hoch Gering Sehr gering
Geeignet für Pakete, Plugins Interne Module Einzelne Services

Die Empfehlung für neue Projekte in 2026: Mit Autowiring und PHP-Config beginnen. Wenn das Modul als Paket verteilt werden soll oder Compiler Passes braucht, zum vollständigen Custom Bundle mit AbstractBundle wechseln. Bestehende Bundles, die nur intern in einer Anwendung genutzt werden, können schrittweise zu PHP-Config-Klassen migriert werden — der Gewinn liegt in reduzierter Komplexität und einfacherem Debugging.

Mironsoft

Symfony Architektur, Bundle-Entwicklung und DI-Container-Expertise

Symfony-Architektur für euer Projekt aufbauen?

Wir entwerfen die richtige Modul-Architektur für euer Symfony-Projekt — von Compiler Passes und Custom Bundles bis zu PHP-Config-Klassen und Flex-Recipes für öffentliche Pakete.

Bundle-Entwicklung

Custom Bundles mit AbstractBundle, Extension, Compiler Passes und Flex-Recipe

DI-Architektur

Tagged Services, PHP-Config-Klassen und optimierter Container-Aufbau

Migration

Legacy-Bundles modernisieren und zu AbstractBundle oder PHP-Config migrieren

10. Zusammenfassung

Symfony Custom Bundles sind in 2026 kein veraltetes Konzept — aber ihr Einsatzbereich ist klarer abgegrenzt als in früheren Symfony-Versionen. Mit AbstractBundle sind Bundles deutlich einfacher zu schreiben, und die Trennung in Extension und Configuration-Klasse entfällt. Compiler Passes bleiben das mächtigste Tool für strukturelle Container-Manipulation und Tagged-Service-Aggregation — und dafür bleibt das Bundle das richtige Werkzeug. Für anwendungsinterne Code-Organisation sind PHP-Config-Klassen, Autowiring und Tags in fast allen Fällen die bessere, einfachere Alternative.

Die pragmatische Entscheidungsregel für 2026: Wenn das Modul auf Packagist landet oder von anderen Entwicklern über composer require installiert werden soll, braucht man ein Bundle — komplett mit Flex-Recipe und Konfigurations-Schema. Wenn der Code intern bleibt, reichen Services und PHP-Config. Das eliminiert einen großen Teil des Bundle-Boilerplates aus Projekten, in denen er nie gebraucht wurde — und macht die Codebasis einfacher zu verstehen, zu testen und zu warten.

Symfony Custom Bundles in 2026 — Das Wichtigste auf einen Blick

Wann Bundle sinnvoll

Öffentliche Composer-Pakete, konfigurierbare Module mit Validierung, Compiler Passes und Flex-Recipe — nur dann lohnt sich der Bundle-Overhead.

AbstractBundle (Symfony 6.1+)

Eine Klasse statt drei: configure(), loadExtension() und build() in der Bundle-Klasse — kein separates Extension- oder Configuration-Objekt mehr nötig.

Compiler Passes

Tagged Services einsammeln und in Dispatchers injizieren — das mächtigste Feature von Bundles, das PHP-Config-Klassen nicht replizieren können.

Interne Alternative

PHP-Config-Klassen und tagged_iterator() ersetzen Bundles für anwendungsinternen Code — einfacher, schneller zu entwickeln, leichter zu debuggen.

11. FAQ: Symfony Custom Bundles und Kernel-Architektur

1Was ist ein Symfony Bundle?
PHP-Klasse, die Services, Konfiguration und Compiler Passes in den Symfony-Container einbringt. Integriert sich in den Kernel-Bootstrapping-Prozess über registerBundles().
2Was ist AbstractBundle?
Seit Symfony 6.1 die empfohlene Basisklasse — konsolidiert Bundle, Extension und Configuration in einer Klasse. configure(), loadExtension() und build() ersetzen die frühere Dreiteilung.
3Was ist ein Compiler Pass?
Implementiert CompilerPassInterface und modifiziert den ContainerBuilder zur Compile-Time — getaggte Services einsammeln, Definitionen ändern, Decorator-Chains aufbauen.
4Wann kein Bundle nötig?
Wenn der Code nur intern in einer Anwendung genutzt wird — dann reichen Autowiring, PHP-Config-Klassen und tagged_iterator() vollständig aus.
5Extension vs. PHP-Config?
Extension: validiertes Konfigurations-Schema, Compiler Passes, öffentlich konfigurierbar. PHP-Config: einfache Service-Registrierung ohne Schema und ohne Compiler-Pass-Support.
6Was ist Symfony Flex?
Composer-Plugin, das Recipes ausführt: Konfigurationsdateien automatisch anlegen und Bundles registrieren beim composer require — ein Befehl, sofort einsatzbereit.
7Bundle im Kernel registrieren?
config/bundles.php: ['Vendor\\Bundle\\MyBundle' => ['all' => true]]. Symfony Flex macht das beim composer require automatisch.
8Tagged Services ohne Bundle?
Ja. tagged_iterator() und tagged_locator() in PHP-Config oder YAML funktionieren ohne Bundle — für einfache Aggregations-Patterns kein Compiler Pass nötig.
9Bundle-Extension testen?
ContainerBuilder direkt instanziieren, loadExtension() mit Test-Konfiguration aufrufen und Service-Definitionen prüfen — schneller als voller Kernel-Test.
10Bundles in Symfony 7 noch unterstützt?
Ja, vollständig. AbstractBundle wird aktiv weiterentwickelt. Keine Deprecation-Pläne für das Bundle-System — es bleibt das zentrale Extensibility-Konzept für öffentliche Pakete.