
Contao 5 Service Layer: Business-Logik sauber kapseln

Ein robuster Contao 5 Service Layer unterscheidet den Profi vom Bastler. Wir haben Entities (Daten) und wir haben Controller/DCA (Ausgabe). Aber wo gehört die Regel hin, dass ein Strandkorb nicht doppelt gebucht werden darf?
Viele Anfänger packen das direkt in den Controller oder – noch schlimmer – in Hooks. Das führt zu massivem, unwartbarem Code. In einer modernen Architektur gehört diese Logik in den Contao 5 Service Layer. Das sind PHP-Klassen, die genau eine Aufgabe haben (Single Responsibility Principle) und überall wiederverwendet werden können (im Frontend, im Backend, in der API, in der Konsole).
Das ist der Plan für unseren Contao 5 Service Layer:
Verstehen, warum "Fat Controllers" böse sind.
Das Repository erweitern (Datenbank-Abfrage für Überlappungen).
Den
AvailabilityCheckerService schreiben.Dependency Injection nutzen, um den Service verfügbar zu machen.
1. Das Repository erweitern: Die Query
Bevor wir den Service schreiben, muss unser Repository (BookingRepository) in der Lage sein, Konflikte zu finden. Der Contao 5 Service Layer verlässt sich darauf, dass das Repository die "Drecksarbeit" mit der Datenbank erledigt.
Wir brauchen eine Methode: hasOverlap(). Logik: Eine Überschneidung liegt vor, wenn eine existierende Buchung beginnt, bevor die neue endet, UND endet, nachdem die neue beginnt.

Öffne /src/BeachsideBundle/src/Repository/BookingRepository.php und füge hinzu:
1// ... imports
2 use Acme\BeachsideBundle\Entity\BeachChair;
3
4 // ... innerhalb der Klasse
5
6 public function hasOverlap(BeachChair $chair, int $newStart, int $newEnd): bool
7 {
8 $qb = $this->createQueryBuilder('b');
9
10 $count = $qb->select('count(b.id)')
11 ->where('b.beachChair = :chair')
12 // Die goldene Regel der Überlappung:
13 ->andWhere('b.dateStart < :newEnd')
14 ->andWhere('b.dateEnd > :newStart')
15 ->setParameter('chair', $chair)
16 ->setParameter('newStart', $newStart)
17 ->setParameter('newEnd', $newEnd)
18 ->getQuery()
19 ->getSingleScalarResult();
20
21 return $count > 0;
22 }2. Das Herzstück: Der AvailabilityChecker
Jetzt erstellen wir den eigentlichen Service. Das ist eine normale PHP-Klasse. Ort: /src/BeachsideBundle/src/Service/AvailabilityChecker.php (Ordner Service ggf. erstellen).

1<?php
2
3namespace Acme\BeachsideBundle\Service;
4
5use Acme\BeachsideBundle\Entity\BeachChair;
6use Acme\BeachsideBundle\Repository\BookingRepository;
7
8class AvailabilityChecker
9{
10 public function __construct(
11 private readonly BookingRepository $bookingRepository
12 ) {
13 }
14
15 /**
16 * Prüft, ob ein Strandkorb im gewünschten Zeitraum frei ist.
17 */
18 public function isAvailable(BeachChair $chair, \DateTimeImmutable $start, \DateTimeImmutable $end): bool
19 {
20 // Validierung: Start darf nicht nach Ende sein
21 if ($start > $end) {
22 return false;
23 }
24
25 // Wir arbeiten mit Integern (Timestamps) wie in der Entity definiert
26 $startTimestamp = $start->getTimestamp();
27 $endTimestamp = $end->getTimestamp();
28
29 // Repository fragen
30 $hasOverlap = $this->bookingRepository->hasOverlap($chair, $startTimestamp, $endTimestamp);
31
32 // Wenn es eine Überlappung gibt (hasOverlap = true), ist er NICHT verfügbar.
33 return !$hasOverlap;
34 }
35}Analyse:
Dependency Injection: Wir injizieren das Repository über den Konstruktor. Wir nutzen
new BookingRepository()niemals manuell!Typsicherheit: Wir nutzen
DateTimeImmutablefür die API des Services, wandeln es aber intern um. Das macht den Contao 5 Service Layer robust gegen falsche Eingaben.
3. Service Registrierung (Services.yaml)
Damit Symfony und Contao diesen Service kennen, müssen wir in die config/services.yaml schauen. Erinnerst du dich an Teil 1? Wir haben dort folgendes definiert:
Acme\BeachsideBundle\:
resource: '../src/*'
exclude:
# ...
- '../src/Entity/'Da der Ordner /src/Service nicht ausgeschlossen ist und wir autowire: true aktiviert haben, ist unser Service automatisch registriert!
Das ist die Magie des modernen Contao 5 Service Layer. Du erstellst die Klasse, und sie ist sofort einsatzbereit.
4. Wie nutze ich den Service jetzt?
Noch rufen wir den Service nirgendwo auf. Aber theoretisch könntest du ihn jetzt in jedem Controller oder DCA-Callback nutzen.
Ein fiktives Beispiel für einen Controller:
1class BookingController extends AbstractController
2{
3 public function __construct(
4 private readonly AvailabilityChecker $checker
5 ) {}
6
7 public function book(BeachChair $chair)
8 {
9 $start = new \DateTimeImmutable('2023-08-01');
10 $end = new \DateTimeImmutable('2023-08-05');
11
12 if ($this->checker->isAvailable($chair, $start, $end)) {
13 // Juhu, buchen!
14 } else {
15 // Fehler: Schon belegt!
16 }
17 }
18}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 & Ausblick
Du hast jetzt das Gehirn deiner Anwendung gebaut.
Das Repository findet Konflikte in der Datenbank.
Der Contao 5 Service Layer kapselt die Prüfung sauber ab.
Der Code ist entkoppelt und überall nutzbar.
Aber Moment mal... Funktioniert das wirklich? Haben wir uns beim >= oder < vertan? Bevor wir das im Frontend einbauen und riskieren, dass Kunden falsche Daten sehen, müssen wir diesen kritischen Code prüfen.
Wir schreiben Unit Tests. Das ist der Ritterschlag für jeden Entwickler.
Nächster Artikel: Unit Testing: Code absichern

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.


