
Contao 5 Bundle Setup: Das Fundament für professionelle Erweiterungen

Ein sauberes Contao 5 Bundle Setup ist das Fundament jeder erfolgreichen Erweiterung. Ein Haus, das auf Sand gebaut ist, stürzt beim ersten Sturm ein. Bei Software ist es genauso. Viele Entwickler scheitern, weil sie nicht wissen, wo genau sie anfangen sollen und versuchen, alte Contao 4 Strukturen in die neue Welt zu pressen.
In diesem ersten Teil unserer Masterclass kümmern wir uns um das korrekte Contao 5 Bundle Setup. Wir gießen ein Fundament aus Stahlbeton für unser Strandkorb-Buchungssystem (BeachsideBundle).
Das ist der Plan:
Die Entwicklungsumgebung vorbereiten (Wo liegen die Dateien?).
Die PSR-4 Ordnerstruktur anlegen.
Die
composer.jsonkonfigurieren.Das Bundle via Manager Plugin registrieren.
Lass uns die IDE öffnen.
Voraussetzungen & der Speicherort
Bevor wir eine Zeile Code schreiben, brauchen wir eine laufende Contao 5 Installation. Ich gehe davon aus, dass du bereits eine frische Contao-Instanz lokal installiert hast (idealerweise mit DDEV, XAMPP oder Herd).
Falls nicht: Installiere dir jetzt eine leere Contao 5 Version. Wenn du fertig bist, sieht dein Projekt-Ordner (das Root-Verzeichnis) ungefähr so aus:
1/mein-contao-projekt
2├── /assets
3├── /config
4├── /public (oder /web)
5├── /system
6├── /var
7├── /vendor
8├── composer.json
9└── ...Wo legen wir jetzt unser Bundle ab? Wir arbeiten nicht im /vendor Ordner (dort liegen nur fertige Pakete von Dritten). Wir arbeiten auch nicht in /system/modules (das ist veraltet).
Für ein sauberes Contao 5 Bundle Setup erstellen wir im Hauptverzeichnis einen neuen Ordner namens src. Das ist der Standard-Ort für lokalen Code in Symfony-Anwendungen.
Dein Ziel-Verzeichnis ist also: /mein-contao-projekt/src/.
Exkurs: Lokal vs. Public (Packagist) Vielleicht fragst du dich: „Warum laden wir das nicht gleich hoch?“ Aktuell nutzen wir den src-Ordner als unsere lokale Werkstatt. Das ist der Standard-Weg für private Projekte oder während der Entwicklung.
Aber keine Sorge: Wenn unser Bundle fertig und poliert ist, können wir es veröffentlichen. In einem Bonus-Teil (Teil 11) dieser Serie zeige ich dir exakt, wie du deinen Code aus src extrahierst, auf GitHub pusht und via Packagist der ganzen Welt zur Verfügung stellst. Dann landet er bei anderen Usern ganz professionell im vendor-Ordner.
Fürs Erste bleiben wir aber hier lokal und halten die Wege kurz.
1. Die Architektur für das Contao 5 Bundle Setup
Um Konflikte mit dem standardmäßigen Autoloading von Contao zu vermeiden, entwickeln wir wiederverwendbare Pakete am besten außerhalb des globalen /src-Verzeichnisses der Hauptanwendung. Wir erstellen stattdessen auf der Root-Ebene deines Projekts einen Ordner namens /packages.
Wir nennen unseren Namensraum (Namespace) Acme (als Platzhalter für deine Firma) und das Bundle BeachsideBundle. Erstelle folgende Ordnerstruktur in deinem Projekt:
1/mein-contao-projekt
2├── /packages <-- Ordner auf Root-Ebene erstellen!
3│ └── /BeachsideBundle <-- Unser Bundle-Root
4│ ├── /config
5│ │ └── services.yaml
6│ ├── /src <-- Der innere PHP-Code-Ordner
7│ │ ├── /ContaoManager
8│ │ │ └── Plugin.php
9│ │ ├── /DependencyInjection
10│ │ │ ├── BeachsideExtension.php
11│ │ │ └── Configuration.php
12│ │ └── BeachsideBundle.php
13│ └── composer.json
14└── /src <-- Bleibt unangetastet für die HauptanwendungJetzt haben wir den physischen Ort für unseren Code geschaffen.
Wichtiger Hinweis zur Struktur: In einer früheren Version dieses Artikels lag das Bundle direkt unter /src/BeachsideBundle. Da Contao den /src-Ordner der App standardmäßig vollautomatisch scannt, führt die zusätzliche Einbindung als lokales Composer-Repository zu doppelten Klassen-Registrierungen (Cannot redeclare class). Die Auslagerung nach /packages löst dieses Problem sauber.
2. Die composer.json: Das Herzstück
Erstelle nun direkt im Hauptverzeichnis deines Bundles (also unter packages/BeachsideBundle/) die Datei composer.json. Diese Datei teilt Composer mit, wie das Paket heißt, welche Abhängigkeiten es hat und wo sich der PHP-Code für das Autoloading befindet.
Da unser Bundle nun isoliert im packages-Ordner liegt, bleibt die Struktur innerhalb des Pakets wunderbar sauber.
Füge folgenden Inhalt in die packages/BeachsideBundle/composer.json ein:
1{
2 "name": "acme/beachside-bundle",
3 "description": "Ein professionelles Strandkorb-Buchungssystem für Contao 5",
4 "type": "contao-bundle",
5 "license": "MIT",
6 "require": {
7 "php": "^8.1",
8 "contao/core-bundle": "^5.0",
9 "symfony/framework-bundle": "^6.0"
10 },
11 "autoload": {
12 "psr-4": {
13 "Acme\\BeachsideBundle\\": "src/"
14 }
15 },
16 "extra": {
17 "contao-manager-plugin": "Acme\\BeachsideBundle\\ContaoManager\\Plugin"
18 }
19}Der Profi-Tipp: Achte auf den Bereich extra. Hier sagen wir dem Contao Manager (und der Console), wo unsere Plugin-Klasse liegt. Wenn du das vergisst, wird dein Bundle nie geladen.
Erklärung zum Autoloading: Der Eintrag "Acme\\BeachsideBundle\\": "src/" bedeutet: Wenn PHP nach Klassen im Namespace Acme\BeachsideBundle sucht, schaut Composer in den Ordner src/ relativ zur Position dieser composer.json. In unserer neuen Struktur zeigt das also exakt auf packages/BeachsideBundle/src/, wo deine Bundle-Logik und der ContaoManager liegen.
Nachdem du die composer.json im Bundle erstellt hast, sagst du der Contao-Hauptanwendung in deren composer.json über ein path-Repository, wo das Paket liegt, und installierst es lokal:
Bashcomposer config repositories.beachside-bundle path "packages/BeachsideBundle"
composer require acme/beachside-bundle:@dev
3. Das Contao Manager Plugin: Der Türsteher
Damit Symfony die Service-Konfiguration des Bundles findet, lädt die Extension-Klasse die Konfigurationsdatei. Da sich unsere Datei im Ordnerstruktur-Zweig /packages/BeachsideBundle/src/DependencyInjection/BeachsideExtension.php befindet, navigieren wir mit __DIR__ . '/../../config' exakt in den /config-Ordner unseres Bundles.
Achte zudem darauf, dass die Datei auf .yaml (und nicht fälschlicherweise auf .yml) endet, um Ladeprobleme im Dependency Injection Container zu vermeiden:

1<?php
2
3namespace Acme\BeachsideBundle\DependencyInjection;
4
5use Symfony\Component\Config\FileLocator;
6use Symfony\Component\DependencyInjection\ContainerBuilder;
7use Symfony\Component\DependencyInjection\Extension\Extension;
8use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
9
10class BeachsideExtension extends Extension
11{
12 public function load(array $configs, ContainerBuilder $container): void
13 {
14 $loader = new YamlFileLoader(
15 $container,
16 new FileLocator(__DIR__ . '/../../config')
17 );
18
19 $loader->load('services.yaml'); // Nutzt die korrekte Dateiendung .yaml
20 }
21}Was passiert hier? Wir sagen Contao: "Lade mein BeachsideBundle, aber bitte erst nachdem der ContaoCoreBundle geladen wurde." Das ist wichtig, damit wir Core-Funktionen überschreiben oder nutzen können.
4. Die Bundle-Klasse und Extension: Symfony Magic
Jetzt wird es kurz etwas abstrakt, aber das ist der Standard-Weg in Symfony. Wir brauchen zwei Klassen, um den Container zu konfigurieren.
A. Die Bundle-Klasse Datei: /src/BeachsideBundle/src/BeachsideBundle.php
1<?php
2
3namespace Acme\BeachsideBundle;
4
5use Symfony\Component\HttpKernel\Bundle\Bundle;
6
7class BeachsideBundle extends Bundle
8{
9 public function getPath(): string
10 {
11 return \dirname(__DIR__);
12 }
13}Hinweis: getPath() ist wichtig, damit Symfony weiß, wo die Root des Bundles ist (wegen Assets, Templates etc.).
B. Die Extension-Klasse Diese Klasse lädt unsere Konfigurationen. Symfony sucht automatisch nach einer Klasse, die [BundleName]Extension heißt.
Datei: /src/BeachsideBundle/src/DependencyInjection/BeachsideExtension.php
1<?php
2
3namespace Acme\BeachsideBundle\DependencyInjection;
4
5use Symfony\Component\Config\FileLocator;
6use Symfony\Component\DependencyInjection\ContainerBuilder;
7use Symfony\Component\DependencyInjection\Extension\Extension;
8use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
9
10class BeachsideExtension extends Extension
11{
12 public function load(array $configs, ContainerBuilder $container): void
13 {
14 $loader = new YamlFileLoader(
15 $container,
16 new FileLocator(__DIR__ . '/../../config')
17 );
18
19 $loader->load('services.yaml');
20 }
21}5. Services.yaml: Die Schaltzentrale (Dependency Injection)
In deiner packages/BeachsideBundle/config/services.yaml definieren wir, welche Klassen per Autowiring geladen werden sollen. Um spätere Probleme bei der automatischen Erkennung von Doctrine-Entities zu verhindern, optimieren wir den Ressourcen-Pfad wie folgt:
Erstelle /src/BeachsideBundle/config/services.yaml:

1services:
2 _defaults:
3 autowire: true # Automatisch Abhängigkeiten injizieren
4 autoconfigure: true # Automatisch Tags setzen (z.B. für EventListener)
5 public: false # Services sind standardmäßig privat
6
7 # Symfony sagen: Alles im inneren /src-Ordner ist ein Service
8 Acme\BeachsideBundle\:
9 resource: '../src/'
10 exclude:
11 - '../src/DependencyInjection/'
12 - '../src/ContaoManager/'
13 - '../src/BeachsideBundle.php'
14 - '../src/Entity/' # Entities werden exklusiv von Doctrine verwaltetWarum ist das genial? Früher musstest du jede Klasse hier eintragen. Jetzt erstellst du einfach eine PHP-Klasse, und sie ist sofort als Service verfügbar. Das spart Stunden an Arbeit.
6. Installation & Test
Jetzt kommt der Moment der Wahrheit. Wir müssen Composer sagen, dass er dieses lokale Verzeichnis als Repository nutzen soll.
Öffne die Haupt-composer.json deiner Contao-Installation (im Root-Verzeichnis deines Projekts, nicht im Bundle!) und füge das path-Repository hinzu. Falls du bereits ein repositories-Array hast, fügst du das Objekt einfach hinzu:
1"repositories": [
2 {
3 "type": "path",
4 "url": "packages/BeachsideBundle"
5 }
6]Um das Bundle nun testweise in deine Contao-Installation zu installieren, führst du folgenden Befehl im Terminal deines Projekt-Roots aus.
Wichtig: Wir hängen ein :@dev an, da lokale Bundles im Entwicklungsstadium noch keine stabilen Versions-Tags besitzen und Composer sie sonst blockieren würde:
composer require acme/beachside-bundle:@devWenn alles klappt, siehst du grüne Schrift und Composer erstellt einen sauberen Symlink von deinem /packages-Ordner direkt in das /vendor-Verzeichnis deiner Contao-Installation.
Danach kannst du das Contao-Setup aufrufen (z. B. über vendor/bin/contao-setup oder den Contao Manager), um die Datenbank-Tabellen zu aktualisieren und das Bundle final zu aktivieren!
Prüfung: Führe folgenden Befehl aus:
vendor/bin/contao-console debug:container "Acme\BeachsideBundle"Wenn hier keine Fehlermeldung kommt, ist dein Bundle offiziell Teil des Systems. Herzlichen Glückwunsch!
Teil der Serie
Contao 5 Masterclass: The Beachside Project
Contao 5 Bundle Entwicklung: Die Masterclass für echte Entwickler Pillar
Contao 5 Bundle Setup: Das Fundament für professionelle Erweiterungen
Contao 5 Doctrine Entities: Moderne Datenmodellierung statt SQL-Chaos
Contao 5 DCA: Perfekte Backend-Masken für Entities erstellen
Contao 5 Doctrine Relations: Wir bauen die Buchungs-Logik
Contao 5 Service Layer: Business-Logik sauber kapseln
Contao 5 Unit Testing: Code-Absicherung mit PHPUnit & Test-Case
Contao 5 Twig Templates: Frontend-Ausgabe mit Fragment Controllern
Contao 5 Symfony Forms: Professionelle Formularverarbeitung
Contao 5 Notification Center: Zentrale Kommunikation & E-Mail-Workflows
Contao 5 Backend Dashboards & Custom Routing
Contao 5 Console Commands: Automatisierung & Hintergrund-Jobs
Contao 5 Headless CMS API: REST-Schnittstellen bauen
Contao 5 Performance, Caching & Zero-Downtime Deployment
Contao 5 Erweiterung veröffentlichen: Packagist & Open Source
Häufig gestellte Fragen (FAQ)
Zusammenfassung & Ausblick
Du hast jetzt das leere Gerüst eines professionellen Contao 5 Bundles.
Ordnerstruktur steht.
Autoloading läuft.
Dependency Injection ist bereit.
Noch tut das Bundle nichts. Aber das Fundament ist solide. Im nächsten Teil verlassen wir die Konfiguration und gehen an die Daten. Wir modellieren unsere Strandkörbe.
Nächster Artikel: Datenmodellierung mit Doctrine & Entities

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


