
Die fehlende Schicht: Was zwischen Controller und Geschäftslogik passiert
Jedes komplexe PHP-Projekt startet mit Wochen Foundation-Arbeit zwischen Framework und Geschäftslogik. Warum diese Lücke existiert und warum sie nicht muss.
Jeder, der schon einmal ein nicht-triviales PHP-Projekt aufgesetzt hat, kennt
die Wochen zwischen composer create-project und der ersten Zeile echter
Geschäftslogik. Der erste Tag fühlt sich produktiv an. Composer läuft, die
ersten Routen antworten, die ersten Migrations sitzen. Zwei Wochen später
schreibt das Team immer noch keine Features. Es baut Repositories, definiert
DTOs, diskutiert über die Verzeichnisstruktur unter src/Domain/, entscheidet
zum vierten Mal welche Library für Validierung verwendet wird, und schiebt
zwischendurch ein Aggregate-Skelett von Hand zusammen, das im nächsten Sprint
wieder umgeworfen wird.
Diese Phase ist keine Schwäche des Teams. Sie ist eine strukturelle Lücke im PHP-Stack, die in fast jedem Projekt sichtbar wird, sobald die Komplexität über eine reine CRUD-Anwendung hinausgeht. Die Frage ist nicht, warum diese Wochen anfallen. Die Frage ist, wo im Stack sie eigentlich hingehören.
Das Stack-Bild, das fast stimmt
Wer einem Außenstehenden erklären will, wie ein PHP-Projekt aufgebaut ist, zeichnet meist eine kurze Kette:
HTTP Request
→ Web-Framework (Laravel, Symfony, Slim)
→ Controller
→ Geschäftslogik
→ Datenbank
Das Bild ist nicht falsch. Es ist nur unvollständig. Zwischen dem Controller und der Geschäftslogik fehlt eine Schicht, die in jedem ernsthaften Projekt existiert, aber selten benannt wird: die Domain-Schicht. Aggregate (in Domain-Driven Design: konsistente Geschäftsobjekt-Cluster wie eine Bestellung mit ihren Positionen), Repositories, Command- und Query-Handler, Validierung, Domain Events, Bounded-Context-Fassaden. Alles das, was den Controller davon trennt, ein 600-Zeilen-Monster zu werden, und was die Geschäftslogik davon trennt, mit SQL durchsetzt zu sein.
Diese Schicht ist nicht optional. Sobald ein Projekt mehrere fachliche Bereiche hat, mehrere Teams parallel daran arbeiten, oder Geschäftsregeln, die mehr als einen Datensatz berühren, entsteht sie zwangsläufig. Die Frage ist nur, ob sie als bewusste Schicht entsteht oder als Sediment in den Controllern und Services.
Warum Frameworks diese Schicht nicht liefern
Es wäre einfach, das den Frameworks vorzuwerfen. Symfony hat Doctrine. Laravel hat Eloquent. Beide haben Service-Container, Validator-Komponenten, Event-Dispatcher. Warum ist das nicht genug?
Weil Frameworks eine andere Aufgabe haben. Ein Web-Framework löst alles, was zwischen HTTP-Request und einem ausgeführten Stück PHP-Code passiert: Routing, Request-Parsing, Authentifizierung, Dependency Injection, Response- Serialisierung, Middleware-Ketten, HTTP-Caching, Session-Handling. Das ist eine umfassende und schwierige Aufgabe, und die großen PHP-Frameworks lösen sie sehr gut.
Was sie bewusst nicht lösen, ist die Form der Domain-Schicht. Und das ist richtig so. Eine Domain-Schicht ist projektspezifisch in einer Weise, die sich nicht generisch vorgeben lässt. Welche Aggregate ein Projekt braucht, wie tief die Bounded Contexts geschnitten werden, ob Domain Events synchron oder asynchron verarbeitet werden, welche Invarianten ein Aggregate schützt: diese Entscheidungen sind die fachliche Arbeit. Ein Framework, das sie vorgibt, würde entweder zu rigide werden oder zu generisch, um nützlich zu sein.
Die Konsequenz ist die Lücke. Frameworks enden am Controller, Geschäftslogik beginnt nach der Domain-Schicht, und dazwischen liegt ein Stück, das jedes Team neu baut. Mit jeweils eigenen Konventionen, jeweils eigenen Verzeichnisstrukturen, jeweils eigener Interpretation des Repository-Patterns.
Wie die Lücke konkret aussieht
Was wird in diesen Wochen Foundation-Arbeit tatsächlich gebaut? Es lohnt sich, das einmal aufzuschreiben, weil die Liste in fast jedem PHP-Projekt identisch ist.
- Aggregate-Klassen mit typisierten Attributen, Konstruktoren, die Invarianten prüfen, und Methoden, die Zustandsänderungen kapseln
- Repositories mit klar getrennten Query- und Persist-Wegen, die ein Aggregate aus der Datenbank rekonstruieren oder dort ablegen
- Command-Handler mit den wiederkehrenden Stufen Initialisierung, Validierung, Hydration, Anwendung der Änderung, Persistierung
- Query-Handler für Einzelabfragen und paginierte Listen, oft mit separaten Read-Models
- DTOs für eingehende und ausgehende Daten, mit Mapping-Logik in beide Richtungen
- Validatoren, die nicht nur Form-Eingaben prüfen, sondern Geschäftsregeln durchsetzen, bevor ein Aggregate angefasst wird
- Domain Events, die nach erfolgreichen Operationen ausgelöst und an andere Bounded Contexts oder externe Systeme weitergereicht werden
- Bounded-Context-Fassaden, die das Innenleben eines Kontextes vor den Nachbarn schützen und nur klar definierte Operationen exponieren
- eine Domain API, abgeleitet aus den Use-Case-Definitionen: typisierte Operationen mit festen Ein- und Ausgaben, gegen die andere Kontexte integrieren
Pro Aggregate sind das gut 40 bis 60 Dateien, die im selben Muster gebaut werden. Bei drei bis fünf Aggregaten in einem mittelgroßen Projekt rechnet sich das zu hunderten Dateien, die alle dem gleichen strukturellen Pattern folgen müssen, sonst zerfällt die Architektur in viele leicht unterschiedliche Interpretationen desselben Konzepts.
Die Kosten der Lücke
Diese Foundation-Wochen sind nicht nur ein Zeitproblem. Sie sind ein Konsistenzproblem, das sich über die Lebensdauer des Projekts amortisiert. Drei Effekte fallen typischerweise an, jeder messbar in den Code-Bases, die ich in den letzten Jahren gesehen habe.
Erstens, der Drift. Was im ersten Aggregate sauber gemacht wurde, sieht im dritten leicht anders aus. Wer das vierte schreibt, übernimmt aus dem zweiten, das selbst schon ein Kompromiss war. Nach achtzehn Monaten existieren in derselben Codebase drei verschiedene Wege, ein Aggregate zu hydratisieren, zwei Konventionen für Repository-Methoden, und eine wachsende Zahl von Sonderfällen, die nirgends mehr passen.
Zweitens, das Onboarding. Ein neuer Entwickler braucht in einem klassisch aufgesetzten DDD-Projekt sechs bis acht Wochen, bis er einen vollständigen Bounded Context selbstständig erweitern kann. Nicht weil die Konzepte schwer sind, sondern weil jede Codebase ihre eigene Variante der Konzepte hat. Das generische DDD-Wissen reicht nicht, das projektspezifische Pattern muss abgelesen werden.
Drittens, die Wartung. Wer eine Schema-Änderung in einem klassisch aufgesetzten Aggregate durchziehen will, fasst typischerweise zwischen fünfzehn und dreißig Dateien an. Entity, Repository-Methoden, Command-DTOs, Validatoren, API-Schemata, Tests. Jede dieser Änderungen ist eine Gelegenheit, etwas zu vergessen. Eine fehlerhafte Schema-Migration ist nicht ein Bug, sondern eine Klasse von Bugs.
Aus meinen Beobachtungen in PHP-Projekten, als Schätzung und nicht als Messwert, geht ungefähr die Hälfte der Entwicklungskapazität in den ersten zwölf Monaten in diese strukturelle Arbeit. Nicht in das Bauen von Features, sondern in das Bauen der Infrastruktur, die Features tragen soll. Wer den Drift einmal in einer eigenen Codebase gesehen hat, kennt das Symptom als schleichende Architektur-Erosion: sauberer Start, leise Verschlechterung, irgendwann eine Refaktorisierung, die nicht stattfindet.
Greenfield-Projekte richtig starten. Mit DDD in PHP.
Neues Projekt, hundert offene Entscheidungen. Jardis gibt dir am Tag 1 eine produktionsreife DDD-Architektur, kein Prototyp der nie aufgeräumt wird.
Die Schicht hat einen Namen
Wenn die Lücke existiert und in jedem komplexen PHP-Projekt aufgemacht wird, ist die naheliegende Frage: gehört sie nicht ins Werkzeug? Nicht ins Web-Framework, das hat eine andere Aufgabe, sondern in eine separate Schicht, die genau diese Lücke schließt.
Genau dort sitzt Jardis im Stack:
HTTP Request
→ Web-Framework (Laravel, Symfony, Slim, frei wählbar)
→ Controller
::: Jardis-Schicht :::
→ Domain → Bounded Context → Command/Query → Response
→ Geschäftslogik (Phase 3, handgeschrieben)
Unter Jardis liegt das Web-Framework, was immer das Team gewählt hat: Symfony, Laravel, Slim, ein selbstgebautes Routing über PSR-15. Jardis ersetzt nichts davon. Über Jardis liegt die Geschäftslogik des konkreten Projekts, also das, was die Software fachlich besonders macht. Auch das bleibt klassische Entwicklerarbeit.
Was Jardis übernimmt, ist die Schicht dazwischen: Aggregate, Repositories, Command- und Query-Handler, Validierung, Domain Events, Bounded-Context-Fassaden. Aus einer Schema-Definition werden die Dateien erzeugt, die ein Team sonst von Hand schreibt: pro Aggregate die kompletten 40 bis 60 Dateien, im selben strukturellen Pattern, bei jedem Build identisch.
Das ist der Kern der Aussage "Start building the foundation of complex software in days, not months." Die Foundation eines komplexen Projekts entsteht in Tagen, nicht in Monaten, weil die strukturelle Arbeit aus der Schema-Definition kommt und nicht aus repetitiver Tipparbeit.
Ergänzung, nicht Ersatz
Es ist wichtig, an dieser Stelle eine Abgrenzung zu ziehen, die in Gesprächen mit PHP-Architekten regelmäßig aufkommt. Jardis tritt nicht gegen Symfony oder Laravel an. Diese Frameworks lösen das HTTP-Problem, das Routing, das Request-Handling, die Dependency Injection. Sie lösen es gut, und sie haben Jahre an gereifter Ökosystem-Arbeit hinter sich, die keine neue Plattform sinnvoll nachbauen würde.
Was Jardis löst, ist eine andere Aufgabe: die Schicht über dem Controller, die das Framework bewusst nicht liefert. Beide Schichten gehören in denselben Stack. Wer Jardis nutzt, behält seinen Symfony- oder Laravel-Controller, ruft aus ihm in die von Jardis erzeugte Domain-Schicht hinein, und bekommt eine Antwort zurück, die er wie gewohnt an den Client ausliefert.
Diese Trennung ist nicht nur Marketing-Höflichkeit. Sie ist eine technische Konsequenz aus der Idee der hexagonalen Architektur: der fachliche Kern einer Software soll nichts über die Außenwelt wissen, weder über die Datenbank noch über das Web-Framework, in dem er gerade läuft. Wenn die Domain-Schicht framework-agnostisch ist, kann darunter jedes Web-Framework liegen. Genau das ist die Voraussetzung dafür, dass Jardis als separate Schicht existieren kann, ohne sich mit einem Framework verheiraten zu müssen.
In der Praxis bedeutet das: Ein bestehendes Symfony-Projekt kann Aggregat für Aggregat auf eine von Jardis erzeugte Domain-Schicht umstellen, ohne das Framework zu wechseln. Ein Greenfield-Projekt wählt Slim, weil das HTTP-Layer minimal sein soll, und nutzt Jardis für alles darüber. Ein Team, das schon Laravel im Haus hat, behält die Controller dort und schiebt die Domain in die Jardis-Schicht. Das Framework bleibt die Wahl des Teams.
Was bleibt für das Team
Die naheliegende Sorge bei jedem Vorschlag, der eine Schicht aus dem Projektalltag herauszieht, ist eine Persona-Frage: was bleibt dann eigentlich für das Team. Die Antwort ist konkret: alles, was die Software fachlich ausmacht.
Wo verlaufen die Bounded-Context-Grenzen, gehört Versand zu Sales oder ist das ein eigener Kontext? Welche Invarianten schützt das Order-Aggregate, darf eine Bestellung ohne Lieferadresse existieren, bis die Zahlung durchgeht? Welche Domain Events lösen welche Folgeprozesse aus, wann läuft die Rechnungsstellung, wann die Lager-Reservierung, wann die Kundenbenachrichtigung? Diese Entscheidungen sind die eigentliche Architektur-Arbeit, und sie werden vor dem ersten Builder-Lauf getroffen.
Phase 3, die handgeschriebene Geschäftslogik, sitzt parallel zum von Jardis erzeugten Code in einem eigenen Ordner. Hier lebt das, was die Software fachlich ausmacht: wie eine Stornierung wirklich abläuft, welche Zahlungsverifikation für einen bestimmten Marktplatz nötig ist, welche Datentransformation für die Integration mit einem konkreten ERP-System gebraucht wird. Diese Arbeit verschwindet nicht. Sie wird sichtbarer, weil die strukturelle Routine sie nicht mehr zudeckt.
PHP-Architektur, die sich selbst durchsetzt
Du definierst die Architektur. Der Jardis Builder erzwingt sie auf Dateisystem-Ebene. Hexagonale Architektur, CQRS und Domain Events als erzeugte Struktur, nicht als Konvention die nach Sprint 10 niemand mehr einhält.
Wie Jardis das Thema angeht
Die fehlende Schicht zwischen Controller und Geschäftslogik ist kein neues Problem. Sie ist der Grund, warum ein einzelner Senior-Architekt in einem PHP-Projekt monatelang gebraucht wird, bevor das erste Feature live gehen kann. Sie ist der Grund, warum die Hälfte der Entwicklungskapazität in den ersten zwölf Monaten nicht in fachliche Arbeit fließt. Und sie ist der Grund, warum DDD in der PHP-Welt einen Ruf als zu aufwendig hat: nicht weil die Konzepte zu komplex wären, sondern weil ihre Umsetzung jedes Mal von vorne beginnt.
Jardis ist die Antwort auf diese Lücke. Eine eigene Schicht im Stack, die zwischen Controller und Geschäftslogik sitzt, aus Schema-Definitionen die vollständige Domain-Architektur erzeugt und die Konsistenz dieser Architektur strukturell sicherstellt, nicht durch Konvention, sondern durch die Eigenschaft, dass alle Aggregate aus demselben Builder kommen. Das Web-Framework bleibt frei, die Geschäftslogik bleibt beim Team, und die Foundation eines komplexen Projekts entsteht in Tagen, nicht in Monaten.
Wie der Builder das konkret tut, welche Dateien aus einer Schema-Definition entstehen, wie der Workflow von der Domain-Beschreibung bis zum lauffähigen Code aussieht: das ist das Thema des nächsten Artikels in dieser Reihe. Wer verstanden hat, dass die Schicht existiert und in den Stack gehört, ist bereit für die Frage, wie sie konkret erzeugt wird.