
Contao 5 Console Commands: Automatisierung & Hintergrund-Jobs

In den vergangenen zehn Modulen haben wir unsere Strandkorb-Applikation auf Enterprise-Niveau gehoben. Doch ein professionelles System muss nicht nur auf Benutzereingaben reagieren, sondern auch selbstständig im Hintergrund agieren. Genau hier kommen Contao 5 Console Commands ins Spiel. Mit diesen mächtigen Kommandozeilen-Befehlen hebst du die Automatisierung deiner Website auf das nächste Level, ganz ohne die Ladezeiten für deine Frontend-Besucher zu beeinträchtigen.
Was passiert mit abgelaufenen Buchungen? Wie werden monatliche Reportings automatisch generiert? Wer räumt temporäre Dateien auf? In alten Contao-Versionen passierte das oft über einen sogenannten "Poor Man's Cron", der bei jedem Seitenaufruf unbemerkt im Hintergrund mitlief. Das war fatal für die Performance. Die moderne und hochperformante Lösung lautet: Echte Contao 5 Console Commands in Kombination mit dem nativen serverseitigen Cronjob Framework.
Der Paradigmenwechsel: DevOps und die Symfony Console
Mit dem Sprung auf Contao 5 hat sich die Architektur des Systems grundlegend in Richtung professioneller DevOps-Workflows verschoben. Die Kommandozeile (Command Line Interface, kurz CLI) ist nicht länger ein optionales Tool für Server-Admins. Eigene Contao 5 Console Commands zu schreiben, ist mittlerweile der primäre Weg für die Wartung, das Deployment und die Hintergrundverarbeitung.
Das Herzstück dieser Architektur ist die ausführbare Datei vendor/bin/contao-console (welche auf der mächtigen Symfony Console Component basiert). Über diesen zentralen Einstiegspunkt initialisieren wir den Symfony-Kernel abseits des Web-Browsers. Das bedeutet für dich als Entwickler:
Keine Timeouts: PHP-Skripte im Browser brechen oft nach 30 oder 60 Sekunden ab. Ein Konsolen-Befehl kann stundenlang laufen, um beispielsweise Tausende von Datensätzen zu importieren.
Keine Speicher-Limits: Memory-Limits können für CLI-Prozesse viel großzügiger eingestellt werden.
Isolierte Performance: Automatisierte Contao 5 Console Commands blockieren nicht die Web-Worker deines Servers. Deine Website bleibt für Besucher pfeilschnell.
Der Plan für dieses Modul
Wir werden unsere Strandkorb-Applikation vollautomatisch machen und die Magie der Konsole nutzen:
Wir lernen die wichtigsten Core-Commands von Contao kennen (wie
contao:migrate).Wir programmieren einen eigenen Symfony Console Command (
beachside:archive-bookings), um abgelaufene Buchungen aufzuräumen.Wir machen unseren Befehl interaktiv (mit bunten Ausgaben und Fortschrittsbalken).
Wir integrieren unser Skript in das Contao Cronjob Framework, damit es verlässlich jede Nacht ausgeführt wird.
Wir werfen einen Blick auf das asynchrone Messaging und das neue Jobs Framework (ab Contao 5.7).
Lass uns das Terminal öffnen und herausfinden, was Contao out-of-the-box auf der Kommandozeile bietet!

1. Der Einstieg: Das Terminal öffnen
Um die Contao 5 Console Commands nutzen zu können, benötigst du einen Kommandozeilen-Zugang (SSH) zu deinem Webserver oder ein lokales Terminal (z.B. in DDEV, Docker oder MAMP). Öffne dein Terminal und navigiere in das Stammverzeichnis (Root-Verzeichnis) deiner Contao-Installation.
Das Herzstück der Automatisierung ist diese ausführbare Datei:
php vendor/bin/contao-consoleWenn du diesen Befehl ohne weitere Parameter ausführst, begrüßt dich das System mit einer riesigen Liste aller verfügbaren Befehle (Symfony und Contao gemischt). Das kann anfangs erschlagend wirken. Um dir gezielt nur die Befehle anzeigen zu lassen, die direkt von Contao kommen, hängst du einfach den Namensraum an:
php vendor/bin/contao-console list contao2. Die wichtigsten Core-Commands im Alltag
Wenn du professionell mit Contao 5 arbeitest, wirst du bestimmte Contao 5 Console Commands fast täglich nutzen. Sie ersetzen viele Klicks, die du früher mühsam im Backend oder im alten Installtool machen musstest.
Hier sind die absoluten Lebensretter für jeden Entwickler:
contao:migrate(Das neue Installtool): In Contao 5 gibt es das klassische, passwortgeschützte Installtool (/contao/install) für Datenbank-Updates nicht mehr. Alles läuft jetzt über Migrationen. Wenn du per Composer eine neue Erweiterung installiert oder ein Update gemacht hast, führst du aus:Bashphp vendor/bin/contao-console contao:migratePro-Tipp: Nutze
contao:migrate --dry-run, um vorher gefahrlos zu sehen, welche Datenbanktabellen geändert werden würden, ohne dass der Befehl wirklich etwas an der Datenbank verändert. Füge--with-deleteshinzu, wenn Contao auch alte, nicht mehr benötigte Spalten löschen soll (DROP-Queries).contao:user:createundcontao:user:password: Stell dir vor, du übernimmst das Projekt eines Kunden, aber niemand kennt mehr das Admin-Passwort für das Contao-Backend. Anstatt mühsam Hashes in der Datenbank (tl_user) zu manipulieren, nutzt du einfach die CLI:Bashphp vendor/bin/contao-console contao:user:password dein_admin_nameDas System fragt dich interaktiv nach dem neuen Passwort und hasht es sofort sicher nach den neuesten Standards.
contao:maintenance-mode: Du musst kurz Wartungsarbeiten an der Datenbank durchführen? Mitphp vendor/bin/contao-console contao:maintenance-mode enableschaltest du das Frontend sofort in den Wartungsmodus. Deine Besucher sehen eine Info-Seite, während du ungestört arbeiten kannst.Symfony Standard-Befehle (
cache:clear&cache:warmup): Da Contao 5 auf Symfony basiert, erbst du auch deren CLI-Magie. Nach Änderungen an Twig-Templates oder Konfigurationsdateien (YAML) musst du den Cache leeren:Bashphp vendor/bin/contao-console cache:clear --env=prod --no-warmup php vendor/bin/contao-console cache:warmup --env=prodÜber die Konsole geht das meist zehnmal schneller als ein Klick im Contao Manager!
Warum ist das so wichtig?
Das Beherrschen dieser Standard-Befehle ist die Grundvoraussetzung für echtes "Continuous Integration / Continuous Deployment" (CI/CD). Wenn du später Skripte schreibst, die deine Webseite automatisch von einem Staging-Server auf den Live-Server deployen, kannst du dort nicht mehr manuell im Backend klicken. Dein Deployment-Skript ruft nach dem Hochladen der Dateien einfach automatisch contao:migrate und cache:warmup auf – und die Seite ist in Sekundenbruchteilen aktualisiert.
Im nächsten Schritt verlassen wir die vorgefertigten Befehle. Wir werden Entwickler und programmieren unseren komplett eigenen Symfony Console Command, um unsere Strandkorb-Buchungen zu verwalten!

3. Warum einen eigenen Command schreiben?
Erinnern wir uns an unser Strandkorb-Projekt: Wenn eine Buchung abgelaufen ist (das Abreisedatum liegt in der Vergangenheit), wollen wir sie idealerweise in der Datenbank als "archiviert" markieren, damit sie in unserem Backend-Dashboard (aus Teil 10) nicht mehr als aktive Buchung gezählt wird.
Wir könnten das bei jedem Seitenaufruf prüfen, aber das kostet Performance. Stattdessen bauen wir ein Kommandozeilen-Skript, das diese Aufgabe übernimmt. Da Contao 5 auf der Symfony Console Component basiert, ist das Erstellen eines solchen Befehls elegant und strikt objektorientiert.
4. Die Command-Klasse anlegen
In modernen Contao 5 (und Symfony 6+) Anwendungen registrieren wir Commands über das PHP 8 Attribut #[AsCommand]. Dadurch sparen wir uns lästige Einträge in der services.yaml – Symfony findet und registriert unseren Befehl vollautomatisch.
Erstelle in deinem Bundle die Datei /src/BeachsideBundle/src/Command/ArchiveBookingsCommand.php:
1<?php
2
3namespace Acme\BeachsideBundle\Command;
4
5use Acme\BeachsideBundle\Repository\BookingRepository;
6use Symfony\Component\Console\Attribute\AsCommand;
7use Symfony\Component\Console\Command\Command;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Output\OutputInterface;
10
11// 1. Das Attribut definiert den Befehlsnamen für das Terminal
12#[AsCommand(
13 name: 'beachside:archive-bookings',
14 description: 'Archiviert abgelaufene Strandkorb-Buchungen.',
15 hidden: false
16)]
17class ArchiveBookingsCommand extends Command
18{
19 // 2. Dependency Injection: Wir holen uns unser Doctrine Repository
20 public function __construct(
21 private readonly BookingRepository $bookingRepository
22 ) {
23 // WICHTIG: parent::__construct() muss bei Commands zwingend aufgerufen werden!
24 parent::__construct();
25 }
26
27 // 3. Die execute-Methode: Hier passiert die Magie
28 protected function execute(InputInterface $input, OutputInterface $output): int
29 {
30 // Einfache Textausgabe in der Konsole
31 $output->writeln('Starte Archivierung abgelaufener Buchungen...');
32
33 try {
34 // Wir rufen eine (fiktive) Methode in unserem Repository auf
35 // Diese macht z.B.: UPDATE tl_booking SET archived=1 WHERE dateEnd < NOW()
36 $archivedCount = $this->bookingRepository->archiveExpiredBookings();
37
38 $output->writeln(sprintf('Erfolgreich! Es wurden %d Buchungen archiviert.', $archivedCount));
39
40 // Wenn alles glatt lief, geben wir den SUCCESS-Status zurück
41 return Command::SUCCESS;
42
43 } catch (\Exception $e) {
44
45 // Bei einem Fehler geben wir FAILURE zurück und loggen das Problem
46 $output->writeln('<error>Fehler bei der Archivierung: ' . $e->getMessage() . '</error>');
47 return Command::FAILURE;
48 }
49 }
50}5. Den Code verstehen
Lass uns die wichtigsten Bausteine dieses Contao 5 Console Commands analysieren:
Der Namensraum (
name: 'beachside:archive-bookings'): Es ist Best Practice, Commands mit einem Präfix (deinem App- oder Bundle-Namen) zu versehen und durch einen Doppelpunkt von der eigentlichen Aktion zu trennen.InputInterfaceundOutputInterface: Diese beiden Parameter in derexecute()-Methode sind deine Kommunikationsschnittstelle zur Konsole. Über$inputlesen wir später Argumente aus, über$outputschreiben wir Text auf den Bildschirm.Der Return-Wert (
Command::SUCCESS): Ein Konsolen-Befehl muss dem Betriebssystem (Linux/Windows) mitteilen, ob er erfolgreich war. Das geschieht über Exit-Codes (Integer). Symfony liefert dafür Konstanten mit:Command::SUCCESS(0) oderCommand::FAILURE(1). Das ist extrem wichtig, wenn du diesen Befehl später in automatisierten Pipelines oder echten Server-Cronjobs nutzt, da diese bei einemFAILUREz. B. Alarm schlagen können.
6. Den Befehl testen
Leere nach dem Erstellen der Datei kurz den Cache deiner Applikation (php vendor/bin/contao-console cache:clear).
Wenn du nun in deinem Terminal in das Stammverzeichnis deiner Contao-Installation gehst, kannst du deinen brandneuen Befehl direkt aufrufen:
php vendor/bin/contao-console beachside:archive-bookingsDu wirst sehen, wie das System dir in Millisekunden antwortet: Starte Archivierung abgelaufener Buchungen... Erfolgreich! Es wurden 14 Buchungen archiviert.
Das ist schon großartig! Aber was ist, wenn der Vermieter nicht alle abgelaufenen Buchungen archivieren will, sondern nur die eines bestimmten Jahres? Oder was passiert, wenn 10.000 Buchungen archiviert werden und das Skript 5 Minuten läuft – der Nutzer vor dem Terminal starrt auf einen schwarzen Bildschirm.
Wir müssen unseren Command interaktiv machen (mit Argumenten und Fortschrittsbalken). Das schauen im weiteren Verlauf an!

7. Die Grenzen von einfachem Textausgaben
In vorherigen Abschnitten haben wir $output->writeln() genutzt, um simplen Text in die Konsole zu schreiben. Für kleine Skripte ist das in Ordnung. Wenn dein Contao 5 Console Command jedoch Teil eines professionellen Workflows wird, möchtest du Warnungen farblich hervorheben (Rot), Erfolgsmeldungen deutlich kennzeichnen (Grün) und bei langwierigen Prozessen einen Fortschrittsbalken (Progress Bar) anzeigen.
Symfony liefert dafür die fantastische Helper-Klasse SymfonyStyle. Sie standardisiert das Aussehen deiner Konsolen-Ausgaben.
8. Argumente und Optionen hinzufügen
Zudem möchten wir unserem Befehl mehr Intelligenz verleihen. Der Vermieter soll entscheiden können:
Option (
--dry-run): Ein Testlauf. Das Skript soll prüfen, wie viele Buchungen abgelaufen sind, aber noch nichts in der Datenbank ändern.Argument (
year): Das Skript soll optional nur Buchungen aus einem bestimmten Jahr archivieren (z. B.beachside:archive-bookings 2023).
Wir öffnen unsere Datei /src/BeachsideBundle/src/Command/ArchiveBookingsCommand.php und erweitern sie signifikant:
1<?php
2
3namespace Acme\BeachsideBundle\Command;
4
5use Acme\BeachsideBundle\Repository\BookingRepository;
6use Symfony\Component\Console\Attribute\AsCommand;
7use Symfony\Component\Console\Command\Command;
8use Symfony\Component\Console\Input\InputArgument;
9use Symfony\Component\Console\Input\InputInterface;
10use Symfony\Component\Console\Input\InputOption;
11use Symfony\Component\Console\Output\OutputInterface;
12use Symfony\Component\Console\Style\SymfonyStyle; // <-- WICHTIG!
13
14#[AsCommand(name: 'beachside:archive-bookings', description: 'Archiviert abgelaufene Buchungen.')]
15class ArchiveBookingsCommand extends Command
16{
17 public function __construct(private readonly BookingRepository $bookingRepository)
18 {
19 parent::__construct();
20 }
21
22 // NEU: Hier definieren wir, welche Eingaben der Befehl erlaubt
23 protected function configure(): void
24 {
25 $this
26 // Ein Argument ist ein nackter Wert (z.B. '2023')
27 ->addArgument('year', InputArgument::OPTIONAL, 'Nur Buchungen aus diesem Jahr archivieren')
28 // Eine Option ist ein Flag mit -- (z.B. '--dry-run')
29 ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Führt einen Testlauf ohne DB-Änderungen durch');
30 }
31
32 protected function execute(InputInterface $input, OutputInterface $output): int
33 {
34 // 1. SymfonyStyle instanziieren für hübsche Ausgaben
35 $io = new SymfonyStyle($input, $output);
36 $io->title('Strandkorb-Buchungen Archivierung');
37
38 // 2. Eingaben auslesen
39 $year = $input->getArgument('year');
40 $isDryRun = $input->getOption('dry-run');
41
42 if ($year) {
43 $io->note('Filter aktiv: Archiviere nur Buchungen aus dem Jahr ' . $year);
44 }
45
46 if ($isDryRun) {
47 $io->warning('DRY-RUN AKTIV: Es werden keine echten Änderungen an der Datenbank vorgenommen!');
48 }
49
50 // 3. (Fiktive) Daten aus der Datenbank holen
51 // In der Praxis: $bookings = $this->bookingRepository->findExpiredBookings($year);
52 $bookingsToArchive = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // Dummy-Daten
53
54 if (empty($bookingsToArchive)) {
55 $io->info('Es wurden keine abgelaufenen Buchungen gefunden.');
56 return Command::SUCCESS;
57 }
58
59 // 4. Den Fortschrittsbalken starten!
60 $io->progressStart(count($bookingsToArchive));
61
62 $archivedCount = 0;
63 foreach ($bookingsToArchive as $bookingId) {
64
65 // Wenn KEIN Dry-Run, dann wirklich in der Datenbank speichern
66 if (!$isDryRun) {
67 // $this->bookingRepository->markAsArchived($bookingId);
68 }
69
70 // Simuliere Arbeit (z.B. PDF löschen, E-Mail senden)
71 usleep(250000); // Wartet 0,25 Sekunden
72
73 $archivedCount++;
74 // Den Balken um 1 Schritt voranschieben
75 $io->progressAdvance();
76 }
77
78 // 5. Balken abschließen und Erfolgsmeldung ausgeben
79 $io->progressFinish();
80
81 $io->success(sprintf('Erfolgreich %d Buchungen verarbeitet!', $archivedCount));
82
83 return Command::SUCCESS;
84 }
85}9. Die Usability auf der Kommandozeile
Wenn du diesen Code testest, siehst du den massiven Unterschied, den SymfonyStyle für Contao 5 Console Commands macht.
Führe einmal folgenden Befehl aus:
php vendor/bin/contao-console beachside:archive-bookings 2023 --dry-runDas Terminal rendert nun einen wunderschönen Titel, eine gelbe Warn-Box für den aktivierten "DRY-RUN" und anschließend einen sich live füllenden Fortschrittsbalken ([=========>--] 8/10). Am Ende erscheint eine grüne Erfolgsbox. Das ist nicht nur optisch ein Highlight, es gibt Systemadministratoren und Entwicklern auch visuell sofort Feedback, ob Skripte hängen bleiben oder reibungslos durchlaufen.
Unser Befehl ist nun perfekt. Er ist sicher (durch --dry-run testbar), flexibel (durch das year-Argument) und benutzerfreundlich. Aber: Bisher müssen wir diesen Befehl immer noch händisch in das Terminal eintippen. Eine echte Automatisierung ist das noch nicht.
Wie wir Contao dazu bringen, diesen Befehl automatisch jede Nacht um 03:00 Uhr auszuführen, schauen wir uns im nächsten Schritt an: dem Contao Cronjob Framework.

10. Die Illusion der Automatisierung (Poor Man's Cron)
In frühen Web-Zeiten – und auch in älteren Contao-Versionen – gab es oft keinen Zugriff auf echte Server-Werkzeuge. Das System behalf sich mit einem Trick, dem sogenannten "Poor Man's Cron". Das funktionierte so: Bei jedem Seitenaufruf im Frontend prüfte Contao, ob seit der letzten Ausführung einer geplanten Aufgabe genug Zeit vergangen war. Wenn ja, wurde die Aufgabe (z.B. ein Newsletter-Versand) an den Seitenaufruf dieses ahnungslosen Besuchers angehängt. Die Folge: Die Webseite lud für diesen einen Besucher plötzlich extrem langsam. Das ist in professionellen Umgebungen ein absolutes No-Go.
Mit den Contao 5 Console Commands drehen wir den Spieß um. Die Webseite kümmert sich nur um die Auslieferung von HTML. Die Hintergrundaufgaben werden von einem echten, serverseitigen Cronjob abgewickelt, der losgelöst vom Web-Traffic agiert.
11. Das Konzept: Ein Cronjob, der sie alle knechtet
Du musst auf deinem Linux-Server (z. B. in Plesk, cPanel oder per SSH) nicht für jede einzelne Aufgabe (Archivierung, Cache-Bereinigung, Suchindex) einen eigenen Cronjob anlegen. Contao liefert einen zentralen Dispatcher-Befehl: contao:cron.
Du richtest auf deinem Server exakt einen einzigen Cronjob ein, der jede Minute läuft:
* * * * * /usr/bin/php /var/www/dein-projekt/vendor/bin/contao-console contao:cronDieser Befehl ist wie ein intelligenter Postverteiler. Er prüft intern, ob irgendwelche Module oder Erweiterungen Aufgaben für die Intervalle minutely, hourly, daily, weekly oder monthly angemeldet haben. Wenn ja, führt er sie nacheinander aus.
12. Deinen eigenen Cronjob in Contao 5 registrieren
Um unsere Strandkorb-Archivierung nun jede Nacht automatisch laufen zu lassen, müssen wir dem Contao Cronjob Framework mitteilen, dass wir eine Aufgabe haben.
Dank PHP 8 Attributen ist das lächerlich einfach. Wir erstellen eine neue Service-Klasse und setzen das Attribut #[AsCronJob].
(Best Practice: Damit wir Code nicht doppelt schreiben, lagern wir die eigentliche Archivierungs-Logik am besten in das BookingRepository oder einen eigenen Service aus. Sowohl unser manueller CLI-Befehl aus vorherigen Abschnitt als auch unser neuer Cronjob greifen dann auf denselben Code zu).
Erstelle die Datei /src/BeachsideBundle/src/Cron/ArchiveBookingsCron.php:
1<?php
2
3namespace Acme\BeachsideBundle\Cron;
4
5use Acme\BeachsideBundle\Repository\BookingRepository;
6use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob;
7use Psr\Log\LoggerInterface;
8
9// 1. Wir registrieren diese Klasse als täglichen Cronjob
10#[AsCronJob('daily')]
11class ArchiveBookingsCron
12{
13 public function __construct(
14 private readonly BookingRepository $bookingRepository,
15 private readonly LoggerInterface $logger // Für das System-Log
16 ) {
17 }
18
19 // 2. Die magische __invoke Methode wird vom contao:cron Befehl aufgerufen
20 public function __invoke(): void
21 {
22 try {
23 // Wir führen unsere Archivierung aus
24 $archivedCount = $this->bookingRepository->archiveExpiredBookings();
25
26 // Wir loggen den Erfolg in die var/logs/prod.log von Symfony
27 if ($archivedCount > 0) {
28 $this->logger->info(sprintf(
29 'Cronjob: %d abgelaufene Strandkorb-Buchungen wurden archiviert.',
30 $archivedCount
31 ));
32 }
33
34 } catch (\Exception $e) {
35
36 // Bei einem Fehler protokollieren wir diesen als Error
37 $this->logger->error('Cronjob Fehler bei der Archivierung: ' . $e->getMessage());
38 }
39 }
40}13. Was passiert jetzt im Hintergrund?
Sobald du den Cache geleert hast (cache:clear), ist die Klasse im System registriert.
Wenn nun dein Server um 24:00 Uhr den Befehl contao:cron aufruft, erkennt Contao: "Aha, der Intervall daily ist fällig!". Es durchsucht alle registrierten Dienste nach dem Attribut #[AsCronJob('daily')], findet unsere ArchiveBookingsCron-Klasse und führt die __invoke() Methode aus.
Alles passiert vollautomatisch, geräuschlos und pfeilschnell im Hintergrund, ohne dass ein einziger Frontend-Besucher davon etwas mitbekommt. Und falls etwas schiefgeht, findest du den Fehler detailliert in der Symfony Log-Datei unter var/logs.
Im nächsten Schritt werfen wir einen Blick auf die Zukunft der Automatisierung: das neue Contao Jobs Framework und asynchrone Queues (Warteschlangen), die noch feingranularer arbeiten als statische Cronjobs.

14. Die Grenzen von Cronjobs
Cronjobs sind fantastisch für wiederkehrende, unsichtbare Aufgaben (wie unsere nächtliche Archivierung). Aber was passiert, wenn der Vermieter im Backend auf einen Button klickt: "Alle 10.000 Buchungen des Jahres als PDF-Report exportieren"?
Wenn wir das synchron im Browser machen, rennt PHP nach 30 Sekunden in einen Timeout und die Seite stürzt ab. Wenn wir es in einen nächtlichen Cronjob auslagern, bekommt der Vermieter im Backend kein direktes Feedback, wann sein Report fertig ist.
Für genau dieses Problem hat Contao in der Version 5.7 LTS ein revolutionäres, neues (aktuell noch experimentelles) Feature eingeführt: Das Jobs Framework.
15. Warteschlangen und Live-Feedback im Backend
Das Jobs Framework erlaubt es dir, aus einem Controller heraus einen "Job" zu erstellen. Contao übernimmt dann die komplette visuelle Darstellung im Backend: Der Vermieter sieht in einer neuen Ansicht, dass sein Export "in der Warteschlange" (Pending) ist, sieht einen echten Live-Fortschrittsbalken und kann am Ende die fertige PDF-Datei direkt herunterladen.
Verarbeitet wird dieser Job im Hintergrund – und hier schließt sich der Kreis – durch Contao 5 Console Commands (genauer gesagt über den Symfony Messenger und Web-Worker).
16. So funktioniert das Jobs Framework im Code
Um das Jobs Framework zu nutzen, interagierst du mit dem zentralen Service Contao\CoreBundle\Job\Jobs. Ein Job durchläuft dabei verschiedene Status-Phasen (new, pending, completed), die du als Entwickler aktiv setzt.
Hier ist ein stark vereinfachtes Beispiel, wie der Hintergrund-Worker (Message Handler) aussieht, der den Job abarbeitet:
1<?php
2
3namespace Acme\BeachsideBundle\MessageHandler;
4
5use Contao\CoreBundle\Job\Jobs;
6use Symfony\Component\Messenger\Attribute\AsMessageHandler;
7
8#[AsMessageHandler]
9class ExportBookingsHandler
10{
11 public function __construct(
12 private readonly Jobs $jobs,
13 // ... (Repositories etc.)
14 ) {}
15
16 public function __invoke(ExportBookingsMessage $message): void
17 {
18 // 1. Den Job anhand seiner UUID laden
19 $job = $this->jobs->getByUuid($message->getJobId());
20
21 if (!$job || $job->isCompleted()) {
22 return;
23 }
24
25 // 2. Dem Backend mitteilen: Wir fangen jetzt an!
26 $job = $job->markPending();
27 $this->jobs->persist($job);
28
29 $totalBookings = 1000; // Fiktive Menge
30
31 for ($i = 0; $i < $totalBookings; $i++) {
32
33 // ... (Hier würde die echte PDF-Generierung passieren) ...
34
35 // 3. Den Fortschrittsbalken im Backend live aktualisieren!
36 // Contao berechnet die Prozente automatisch.
37 $job = $job->withProgressFromAmounts($i + 1, $totalBookings);
38 $this->jobs->persist($job);
39 }
40
41 // 4. Die fertige Datei anhängen und den Job abschließen
42 $this->jobs->addAttachment($job, 'report_2026.pdf', '... PDF INHALT ...');
43
44 $job = $job->markCompleted();
45 $this->jobs->persist($job);
46 }
47}17. Der Arbeiter im Hintergrund: messenger:consume
Damit dieser Code ausgeführt wird, reicht der Webbrowser allein nicht aus. Die Nachrichten liegen in einer Warteschlange (Queue) in der Datenbank.
Um diese Warteschlange abzuarbeiten, nutzt du wieder die Kraft der Contao 5 Console Commands. Auf deinem Server muss dauerhaft ein sogenannter "Worker" laufen. Diesen startest du über das Terminal:
php vendor/bin/contao-console messenger:consume contao_prio_normal -vvDieser Befehl bleibt im Terminal offen, wartet auf neue Jobs in der Datenbank, arbeitet sie Stück für Stück ab und aktualisiert über unseren Code ($this->jobs->persist($job)) laufend den Status in der Datenbank, welchen das Contao-Backend dann visuell für den Redakteur darstellt.
Das Jobs Framework ist ein massiver Meilenstein für Contao 5.7+ und hebt die Administration von extrem datenintensiven Projekten auf ein völlig neues Level.
Im nächsten Schritt fassen wir unsere Erkenntnisse zur Automatisierung zusammen und klären, wie du diese Befehle auf einem echten Live-Server dauerhaft am Leben hältst.

18. Best Practices für Contao 5 Console Commands
Wenn du Skripte schreibst, die im Hintergrund tausende von Datensätzen verarbeiten, gelten andere Regeln als bei normalen Web-Requests. Um Server-Abstürze zu vermeiden, solltest du diese drei goldenen Regeln beachten:
Dauerläufer mit Supervisor absichern: Der Befehl
php vendor/bin/contao-console messenger:consume(für Queues und das Jobs Framework) läuft endlos. Wenn du aber dein SSH-Terminal schließt oder der Server neu startet, bricht der Befehl ab. In einer professionellen Umgebung nutzt du Tools wie Supervisor (Linux) oder systemd. Diese Tools überwachen den Befehl im Hintergrund und starten ihn automatisch neu, falls er abstürzt.Memory Leaks bei Massendaten verhindern: Doctrine ORM (das System, mit dem wir Datenbankabfragen machen) merkt sich standardmäßig jedes geladene Objekt im Arbeitsspeicher. Wenn dein Command 50.000 alte Buchungen archiviert, läuft dein Server irgendwann out-of-memory. Die Lösung: Nutze in deinen Schleifen regelmäßig
$this->entityManager->clear();, um den Arbeitsspeicher wieder freizugeben.URLs im CLI-Kontext generieren: Ein Contao 5 Console Command hat keinen Browser. Er weiß also nicht, ob deine Webseite
https://mein-strandkorb.deoderhttp://localhostheißt. Wenn dein Cronjob nachts Bestätigungs-E-Mails mit Links verschickt, schlagen diese Links oft fehl. Du musst Symfony (und Contao) den sogenannten "Request Context" in der Konfiguration (config/packages/routing.yaml) mitteilen:YAMLframework: router: default_uri: 'https://mein-strandkorb.de'

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
Häufig gestellte Fragen (FAQ)
Zusammenfassung
Wir haben unsere Strandkorb-Applikation von einem rein reaktiven System in eine proaktive, automatisierte Plattform verwandelt.
Mit dem Wissen aus diesem Modul beherrschst du die professionelle Server-Architektur:
Core Commands: Du nutzt
contao:migrateundcache:clearfür schnelle Deployments.Custom Commands: Mit dem
#[AsCommand]Attribut schreibst du eigene, interaktive CLI-Tools mit Fortschrittsbalken (SymfonyStyle).Cronjob Framework: Du lagerst wiederkehrende Aufgaben per
#[AsCronJob]in nächtliche Server-Prozesse aus.Jobs Framework (5.7+): Du verstehst, wie man asynchrone Queues nutzt, um Redakteuren im Backend Live-Feedback bei schweren Berechnungen zu geben.
Unsere Buchungsplattform ist nun extrem robust, hochgradig automatisiert und absolut Enterprise-ready.
Doch ein Thema fehlt noch: Was ist, wenn der Vermieter die Strandkörbe nicht nur über seine Website vermieten will, sondern auch über eine eigene Smartphone-App oder ein Terminal direkt am Strand? In Teil 12 brechen wir das System auf. Wir verwandeln Contao in ein Headless CMS und bauen eine REST-API, um unsere Daten für Drittsysteme bereitzustellen.
Möchtest du direkt mit Teil 12 Headless CMS & API-Entwicklung mit API Platform starten?

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.


