Wenn CRUD nicht mehr reicht: Einführung in CQRS und Event Sourcing
\n\nKennen Sie das? Ihre PHP-Anwendung wächst und wächst... und plötzlich wird aus einem harmlosen UserController ein Monster mit über 2000 Zeilen Code? Kommt Ihnen bekannt vor? Bugs vermehren sich, die Performance leidet, und jedes neue Feature löst bei Ihrem Team nervöse Zuckungen aus. Herzlichen Glückwunsch, Sie sind bereit für ernsthafte Architekturmuster.
Heute tauchen wir in zwei mächtige Werkzeuge ein: CQRS (Command Query Responsibility Segregation) und Event Sourcing. Keine Angst vor den Fachbegriffen. Im Kern sind es elegante Methoden, um zu sagen: "Hören wir auf, Lesen und Schreiben in einen Topf zu werfen" und "Speichern wir nicht den aktuellen Zustand, sondern die gesamte Änderungshistorie".
\n\nDieser Artikel richtet sich an PHP-Entwickler, die die Grundlagen der OOP beherrschen und ihre Projekte auf das nächste Level bringen wollen. Sie lernen, wie Sie CQRS und Event Sourcing praktisch einsetzen, sehen echten Code und verstehen, wann diese Muster wirklich sinnvoll sind – und wann nicht.
\n\nWas ist CQRS? Trennung von Befehlen und Abfragen
\n\nStellen Sie sich Ihren Code wie ein Restaurant vor. Normalerweise nimmt der Kellner (Controller) eine Bestellung auf (Schreibzugriff), rennt in die Küche (Datenbank), kocht das Gericht (verarbeitet Daten) und serviert es sofort (gibt Antwort zurück). Das funktioniert prima bei 10 Gästen. Aber bei 1000 Gästen bricht die Küche zusammen.
\n\nCQRS (Command Query Responsibility Segregation) schlägt vor, die Verantwortung zu teilen: Einige Köche (Befehle) kochen nur, andere Kellner (Abfragen) servieren nur. Technisch bedeutet das, dass Sie unterschiedliche Modelle für Lese- und Schreibzugriffe verwenden.
\n\nBasisimplementierung von CQRS in PHP
\n\nSehen wir uns den Code an. Wir beginnen mit einem einfachen Beispiel ohne Frameworks, um das Prinzip zu verstehen.
\n\n// Ein Befehl ist ein DTO mit Daten zur Zustandsänderung\nclass CreateUserCommand {\n public function __construct(\n public readonly string $email,\n public readonly string $name\n ) {}\n}\n\n// Der Befehlshandler – hier findet die Geschäftslogik statt\nclass CreateUserHandler {\n public function __construct(\n private UserRepository $repository,\n private EventDispatcher $dispatcher\n ) {}\n\n public function handle(CreateUserCommand $command): void {\n $user = new User($command->email, $command->name);\n $this->repository->save($user);\n $this->dispatcher->dispatch(new UserCreatedEvent($user->getId()));\n }\n}\n\n// Eine Abfrage ist ein DTO zum Abrufen von Daten\nclass GetUserQuery {\n public function __construct(\n public readonly int $userId\n ) {}\n}\n\n// Der Abfragehandler – nur lesen, keine Änderungen\nclass GetUserHandler {\n public function __construct(\n private UserReadRepository $readRepository\n ) {}\n\n public function handle(GetUserQuery $query): ?UserDTO {\n return $this->readRepository->findById($query->userId);\n }\n}\n\nBeachten Sie: Wir haben zwei verschiedene Repositories. UserRepository ist für Schreibzugriffe zuständig (mit komplexer Geschäftslogik und Validierung), während UserReadRepository nur für Lesezugriffe da ist (einfache SQL-Abfragen oder sogar denormalisierte Tabellen).
Die Command Bus
\n\nIn echten Projekten werden Befehle und Abfragen über einen Bus gesendet – einen zentralen Dispatcher, der automatisch den passenden Handler findet.
\n\n// Einfachste Implementierung eines Command Bus\nclass SimpleCommandBus {\n public function __construct(\n private array $handlers = []\n ) {}\n\n public function registerHandler(string $commandClass, object $handler): void {\n $this->handlers[$commandClass] = $handler;\n }\n\n public function dispatch(object $command): void {\n $handler = $this->handlers[$co