Wenn Ihr PHP-Monolith anfängt, Risse zu bekommen
\nWaren Sie schon einmal in der Situation, dass der Wechsel von MySQL zu PostgreSQL zu einer wochenlangen Herausforderung wurde? Oder wenn Sie einen neuen API-Client hinzufügen mussten und der Code so stark mit dem Framework verwoben war, dass es einfacher gewesen wäre, alles von Grund auf neu zu schreiben? Kommt Ihnen das bekannt vor?
\nDie meisten PHP-Entwickler stoßen früher oder später auf „Spaghetti-Code“ in Controllern und Modellen. Frameworks wie Laravel oder Symfony bieten praktische Werkzeuge, erzwingen aber gleichzeitig eine starke Bindung an sich selbst. Es gibt einen Ausweg – die hexagonale Architektur (auch bekannt als „Ports und Adapter“).
\nIn diesem Artikel werden wir untersuchen, wie man eine PHP-Anwendung erstellt, die unabhängig von Framework, Datenbank oder externen Diensten ist. Sie werden Codebeispiele sehen, die Sie sofort in Ihrem eigenen Projekt anwenden können. Los geht's!
\n\nWas ist hexagonale Architektur und warum brauchen Sie sie?
\nStellen Sie sich ein Sechseck vor. In der Mitte befindet sich Ihre Geschäftslogik (die Domäne). Darum herum befinden sich die „Ports“ (Schnittstellen), über die die Außenwelt mit dem Kern kommuniziert. Und außen befinden sich die „Adapter“, die diese Ports implementieren: Controller, Repositories, API-Clients.
\nDie Hauptidee: Der Kern der Anwendung weiß nichts über die Außenwelt. Er importiert Laravel nicht, kennt PDO nicht und ruft curl nicht auf. Die gesamte Abhängigkeit ist nach innen gerichtet – die Adapter hängen von den Ports ab, nicht umgekehrt.
\nDie Vorteile dieses Ansatzes liegen auf der Hand:
\n- \n
- Testbarkeit: Die Geschäftslogik kann ohne Datenbank und HTTP-Anfragen getestet werden. \n
- Flexibilität: Der Austausch von Datenbank, Warteschlange oder Framework bedeutet lediglich das Schreiben eines neuen Adapters. \n
- Sauberer Code: Sie vermischen nicht „was zu tun ist“ mit „wie es zu tun ist“. \n
Klingt das nach Magie? Werfen wir einen Blick auf den Code.
\n\nProjektstruktur: Das grundlegende Skelett
\nAls Beispiel nehmen wir ein einfaches Szenario: Benutzerregistrierung. Wir müssen den Benutzer in der Datenbank speichern und eine Begrüßungs-E-Mail senden.
\nSo wird eine typische Verzeichnisstruktur aussehen:
\nsrc/\n├── Domain/\n│ ├── Entity/\n│ │ └── User.php\n│ ├── Repository/\n│ │ └── UserRepositoryInterface.php\n│ ├── Service/\n│ │ └── UserRegistrationService.php\n│ └── Event/\n│ └── UserRegisteredEvent.php\n├── Infrastructure/\n│ ├── Persistence/\n│ │ └── DoctrineUserRepository.php\n│ ├── Mail/\n│ │ └── SymfonyMailerAdapter.php\n│ └── Controller/\n│ └── RegisterUserController.php\n└── Application/\n └── UseCase/\n └── RegisterUserUseCase.php\nBeachten Sie: Domain ist die reine Geschäftslogik. Infrastructure sind die Adapter. Application sind die Anwendungsfälle (Use Cases).
Schritt 1: Der Kern – Domäne und Ports
\nEntität (Entity)
\nBeginnen wir mit dem Einfachsten – dem Benutzer. Dies ist nur eine Klasse mit Daten, ohne Abhängigkeiten.
\n<?php\nnamespace App\\Domain\\Entity;\n\nclass User\n{\n public function __construct(\n private readonly string $id,\n private string $name,\n private string $email\n ) {}\n\n public function getId(): string\n {\n return $this->id;\n }\n\n public function getName(): string\n {\n return $this->name;\n }\n\n public function getEmail(): string\n {\n return $this->email;\n }\n}\n\nPort – Repository-Schnittstelle
\nJetzt definieren wir, wie wir den Benutzer speichern. Dies ist ein Port – eine Schnittstelle, die von einem Adapter implementiert wird.
\n<?php\nnamespace App\\Domain\\Repository;\n\nuse App\\Domain\\Entity\\User;\n\ninterface UserRepositoryInterface\n{\n public function save(User $user): void;\n public function findByEmail(string $email): ?User;\n}\n\nPort – E-Mail-Dienst-Schnittstelle
\nAnalog dazu für den E-Mail-Versand:
\n<?php\nnamespace App\\Domain\\Service;\n\nuse App\\