Einstieg in CQRS mit Vanilla-PHP: Befehle und Lesezugriffe ohne Framework trennen
Du hörst überall von CQRS, fragst dich aber, wie du es anwenden kannst, ohne sofort Symfony oder Laravel auszupacken? Gute Nachricht: Wir können Command Query Responsibility Segregation in purem PHP erkunden – mit einer klaren Organisation und ein paar gut durchdachten Klassen. Schnapp dir deinen Editor, wir legen gemeinsam die Basis.
Voraussetzungen: PHP 8.4 und ein minimales Setup
- PHP 8.4 installiert (CLI oder Webserver). Prüfe deine Version mit
php -v, umreadonly-Properties, strikte Typen und native First-Class Callables zu nutzen. - Composer (optional, aber praktisch fürs Autoloading) sowie ein leichter Webserver (
php -S 127.0.0.1:8000 -t publicreicht zum Experimentieren).
Wenn du bei einer älteren Version bleiben musst, stelle sicher, dass die verwendeten Features (Konstruktor-Promotion, readonly-Properties usw.) verfügbar sind oder passe die Snippets an.
1. CQRS in zwei Minuten
CQRS bedeutet, Lesen (Query) und Schreiben (Command) strikt zu trennen. Daraus entstehen zwei optimierte Pfade:
- Commands verändern den Zustand (Account anlegen, Artikel veröffentlichen) und liefern keine Anwendungsdaten zurück.
- Queries lesen den Zustand (Artikel auflisten, Account-Details abrufen) ohne Seiteneffekte.
Diese Trennung entspringt zwar der DDD-Philosophie, aber du kannst sie auch ohne viel Formalismus übernehmen. Sofortige Vorteile:
- Einfachere Tests: Handler lassen sich isoliert unit-testen.
- Besser lesbarer Code: Jeder Handler hat nur eine Aufgabe.
- Schrittweise Skalierung: Lese- und Schreibseite können unabhängig voneinander wachsen.
Denke daran, dass CQRS etwas strukturelle Komplexität mitbringt. Für ein simples CRUD kann das zu viel sein. Nutze es, wenn Geschäftslogik oder Skalierungsanforderungen den Aufwand rechtfertigen.
2. Minimale Ordnerstruktur in nativem PHP
Wir bleiben schlank mit einem einfachen Verzeichnisbaum:
src/
Domain/
Article.php
ArticleRepository.php
Application/
Command/
CreateArticle/
CreateArticleCommand.php
CreateArticleHandler.php
Query/
ListArticles/
ListArticlesQuery.php
ListArticlesHandler.php
Bus/
CommandBus.php
QueryBus.php
Infrastructure/
InMemoryArticleRepository.php
public/
index.php
So trennen wir Domain (Geschäftsregeln), Application (Use Cases) und Infrastructure (konkrete Implementierungen).
3. Ein minimalistisches Domänenmodell
Wir erstellen ein Article-Aggregat und sein Repository:
php
php
4. Command-Seite: Einen Artikel erstellen
Der Command transportiert die Absicht. Der Handler orchestriert die Geschäftslogik.
php
php
Wichtiger Hinweis: Der Handler gibt nichts zurück. Wenn du den Artikel erneut lesen möchtest, gehst du über eine Query.
5. Query-Seite: Artikel auflisten
php
php
Wir liefern hier ein Array, das direkt serialisiert werden kann, ohne die Präsentationsschicht an das Domänenmodell zu koppeln.
6. Ein In-Memory-Repository zum Starten
php
Später kannst du diese Implementierung durch Doctrine, PDO oder eine externe API ersetzen – ohne die Handler anzufassen.
7. Ultraleichte Busse verdrahten
Der CQRS-Ansatz glänzt, sobald du die Handler-Auflösung zentralisierst:
php
php
Diese Busse sind bewusst simpel, reichen aber, um das Prinzip zu verstehen. In echten Projekten kannst du sie an einen Dependency-Injection-Container (PHP-DI, Symfony DI usw.) anbinden.
8. Bootstrap-Beispiel in public/index.php
php
Der Operator (...) (First-Class Callable), seit PHP 8.1 verfügbar, wird in PHP 8.4 vollständig unterstützt. Musst du eine ältere Version (8.0 oder niedriger) bedienen, ersetze ihn durch fn ($command) => $createHandler($command).
9. Schrittweise weitergehen
Steht dieses Fundament, kannst du deine Lösung ausbauen:
- Validierung: Ergänze Value Objects (Title, Content) oder setze vor dem Aggregat Symfony Validator ein.
- Echte Persistenz: Implementiere ein MySQL- (PDO) oder PostgreSQL-Repository, ohne die Handler zu ändern.
- Domänenereignisse: Veröffentliche ein
ArticlePublished, wenn der Command erfolgreich war, und konsumiere es in einer anderen Schicht. - Dedizierte Projektion: Lege für anspruchsvolle Queries (Pagination, Volltextsuche) eine lesefreundliche Tabelle an.
- Tests: Schreibe Unit-Tests für jeden Handler und funktionale Tests für die Busse.
10. Checkliste für den ersten Produktionseinsatz
- Jeder Command hat einen eigenen Handler, der nichts zurückgibt.
- Jede Query ist isoliert und formatiert Daten für das Frontend.
- Technische Abhängigkeiten werden von außen injiziert.
- Fachliche Fehler werden in der Domain behandelt (spezialisierte Exceptions, Value Objects usw.).
- Tests decken Commands, Queries und Busse ab.
Mit diesem Gerüst kannst du CQRS schrittweise in ein bestehendes Projekt einführen oder einen neuen Service ohne Framework starten. Entscheidend ist, die Grenze zwischen Lesen und Schreiben klar zu halten und die Architektur iterativ weiterzuentwickeln. Viel Erfolg beim Experimentieren!