
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:
/mein-contao-projekt
├── /assets
├── /config
├── /public (oder /web)
├── /system
├── /var
├── /vendor
├── composer.json
└── ...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
Jetzt erstellen wir in diesem neuen src-Ordner unser Bundle. 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└── /src <-- Neu erstellt!
3 └── /BeachsideBundle <-- Unser Bundle-Root
4 ├── /src
5 │ ├── /ContaoManager
6 │ │ └── Plugin.php
7 │ ├── /DependencyInjection
8 │ │ ├── BeachsideExtension.php
9 │ │ └── Configuration.php
10 │ ├── BeachsideBundle.php
11 │ └── BeachsideExtension.php
12 ├── /config
13 │ └── services.yaml
14 └── composer.jsonJetzt haben wir den physischen Ort für unseren Code geschaffen.
2. Die composer.json: Das Herzstück
Ohne composer.json funktioniert kein Contao 5 Bundle Setup. Hier definieren wir das Autoloading.
Erstelle die Datei /src/BeachsideBundle/composer.json:
JSON
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.
3. Das Contao Manager Plugin: Der Türsteher
Damit Contao weiß, dass wir existieren, brauchen wir ein Plugin. Es implementiert das BundlePluginInterface.
Erstelle /src/BeachsideBundle/src/ContaoManager/Plugin.php:

PHP
1<?php
2
3namespace Acme\BeachsideBundle\ContaoManager;
4
5use Acme\BeachsideBundle\BeachsideBundle;
6use Contao\CoreBundle\ContaoCoreBundle;
7use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
8use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
9use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
10
11class Plugin implements BundlePluginInterface
12{
13 public function getBundles(ParserInterface $parser): array
14 {
15 return [
16 BundleConfig::create(BeachsideBundle::class)
17 ->setLoadAfter([ContaoCoreBundle::class]),
18 ];
19 }
20}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
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
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)
Hier definieren wir, wie unsere Klassen instanziiert werden. In modernen Symfony-Apps nutzen wir Autowiring. Das heißt: Wir müssen nicht jeden Service einzeln definieren, Symfony errät anhand der Type-Hints, was wir brauchen.
Erstelle /src/BeachsideBundle/config/services.yaml:

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 (Performance!)
6
7 # Hier sagen wir Symfony: "Alles in /src 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 sind keine Services!Warum 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, nicht im Bundle!) und füge das hinzu:
JSON
"repositories": [
{
"type": "path",
"url": "src/BeachsideBundle"
}
],Und dann im Terminal:
Bash
composer require acme/beachside-bundleWenn alles klappt, siehst du grüne Schrift.
Prüfung: Führe folgenden Befehl aus:
Bash
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
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: [Teil 2 – 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.


