From 0da6f5c10ee6c83bbfa710b1b6977f80b542e592 Mon Sep 17 00:00:00 2001 From: Jens Date: Fri, 17 Jul 2026 12:27:08 +0200 Subject: [PATCH] docs: consolidate module communication, handler paths, manager granularity, mapping factory pattern, and testing guidelines --- architektur-patterns.md | 95 +++++++++++++++++++++-------------------- architektur.md | 12 +++--- 2 files changed, 55 insertions(+), 52 deletions(-) diff --git a/architektur-patterns.md b/architektur-patterns.md index a480f92..f36a1d0 100644 --- a/architektur-patterns.md +++ b/architektur-patterns.md @@ -195,8 +195,9 @@ Das Read Pattern beschreibt den Weg einer Datenabfrage von der UI bis zur Datenq - **Pfad**: `src/Logic/{Module}/{Feature}/Query/...` #### Manager (Logic Layer) -- **Zweck**: Steuert die Bereitstellung der Daten. Hier erfolgt primär die Cache-Logik ("Cache-Aside") und die Koordination von Providern. -- **Verantwortung**: Ausschließlich Read- und Write-Operationen für das fachliche Business-Model sowie die Cache-Koordination. Es finden keine geschäftlichen Berechnungen oder Logikverzweigungen im Manager statt. Die eigentliche Geschäftslogik liegt in den UseCases oder spezialisierten Services. +- **Zweck**: Steuert die Bereitstellung der Daten eines einzelnen fachlichen Business-Models. Hier erfolgt primär die Cache-Logik ("Cache-Aside"). +- **Verantwortung**: Ausschließlich Read- und Write-Operationen für genau ein Model sowie die Cache-Koordination. Komplexe Aggregationen über verschiedene Domänen hinweg werden bewusst vermieden. Die eigentliche Geschäftslogik liegt in den UseCases oder spezialisierten Services. +- **Granularität**: **Ein Manager pro Business Model**, um Kopplung und zyklische Dependencies zu vermeiden. - **Schnittstelle**: Besitzt ein eigenes Interface in der Logic Layer zur Gewährleistung von Testbarkeit und Austauschbarkeit. - **Zustand**: Strikt zustandslos (stateless). @@ -315,8 +316,9 @@ Das Write Pattern definiert den Weg von einer Zustandsänderung hin zur Persiste `UI Layer` $\rightarrow$ **UseCase** $\rightarrow$ **Manager** $\rightarrow$ **Processor** #### Manager (Logic Layer) -- **Zweck**: Zentrale Steuereinheit pro Domain, die sowohl Lese- als auch Schreiboperationen koordiniert. -- **Verantwortung**: Führt im Write-Kontext die Persistierung via Processor durch und invalidiert daraufhin zwingend alle relevanten eigenen Read-Caches (Cache-Sidecar). Dies garantiert Konsistenz zwischen gecachten Daten und der primären Datenquelle. +- **Zweck**: Zentrale Steuereinheit pro Business Model, die sowohl Lese- als auch Schreiboperationen koordiniert. +- **Verantwortung**: Führt im Write-Kontext die Persistierung via Processor durch und invalidiert daraufhin zwingend alle relevanten eigenen Read-Caches (Cache-Sidecar). Komplexe Multi-Domain-Aggregationen werden bewusst vermieden; eine solche Orchestrierung ist Aufgabe der UseCases. +- **Granularität**: **Ein Manager pro Business Model**. #### Processor (Data Layer) - **Zweck**: Übernimmt die physische Persistierung eines Business-Models. @@ -647,52 +649,32 @@ readonly class OrderModelFactory } ``` -#### Kopier-Pattern für Partial-Updates (Model.copyWith) -Wenn nur bestimmte Felder eines bestehenden Business-Models geändert werden sollen, empfiehlt es sich, eine **copyWith**-Methode im Model selbst zu implementieren: +#### Rebuild-Factor-Pattern für Updates (Partial & Full) +Um das Business Model "Create-only" und schlank zu halten, sind Update-Operationen immer Aufgabe einer Factory. +Sowohl bei einem kompletten Neuberechnungsprozess als auch bei der Verwendung von Partial-DTOs (Updates), greifen wir ausschließlich auf eine zentralisierte Factory zurück: ```php -namespace App\Logic\Sales\Order\Model; +namespace App\Logic\Sales\Order\Mapping; -readonly class Order +use App\Logic\Sales\Order\Dto\UpdateOrderRequest; // Enthält id:, email?, amount? ... +use App\Logic\Sales\Order\Model\Order; + +readonly class OrderModelFactory { - public function __construct( - public string $id, - public string $customerEmail, - public float $totalAmount, - public int $version, - ) {} - - /** - * Erstellt eine Kopie und überschreibt nur die mitgegebenen Felder. - */ - public function with( - ?string $customerEmail = null, - ?float $totalAmount = null, - ): self { - return new self( - id: $this->id, - customerEmail: $customerEmail ?? $this->customerEmail, - totalAmount: $totalAmount ?? $this->totalAmount, - version: 0, // Wird beim Speichern durch den Data Layer aktualisiert. - ); - } - - /** - * Erstellt eine Version-Kopie für ein neues Update (Concurrency Handling). - */ - public function nextVersion(): self + public function rebuildFromPartialUpdate(Order $current, UpdateOrderRequest $request): Order { return new self( - id: $this->id, - customerEmail: $this->customerEmail, - totalAmount: $this->totalAmount, - version: $this->version + 1, + id: $request->id, + customerEmail: $request->customerEmail ?? $current->customerEmail, + totalAmount: $request->totalAmount ?? $current->totalAmount, + // ... übrige Felder werden unverändert übernommen. ); } } -// Verwendung im UseCase oder in der Factory -$updatedOrder = $existingOrder->with(customerEmail: 'new@example.com', totalAmount: 123.45); +// Verwendung im UseCase: +$dto = new UpdateOrderRequest(id: '123', customerEmail: 'neuer@test.de'); +$updatedOrder = $this->orderModelFactory->rebuildFromPartialUpdate($existingOrder, $dto); ``` ### Code Beispiel (Data Layer Mapping) @@ -903,11 +885,21 @@ Um eine konsistente Fehlerbehandlung über alle Layer hinweg zu gewährleisten, ### Die Hierarchie (`src/Logic/Common/Exception`) Alle fachlichen Exceptions erben von einer abstrakten Basisklasse `DomainException`. -#### 1. Root: `DomainException` (abstract) -Die Wurzel aller fachlichen Fehler der Logic Layer. +#### Code-Skelett (Basisimplementierung) +```php +// src/Logic/Common/Exception/ +namespace App\Logic\Common\Exception; + +abstract class DomainException extends \RuntimeException {} + +class ResourceNotFoundException extends DomainException {} +class BusinessRuleViolationException extends DomainException {} +class AccessDeniedException extends DomainException {} +class InfrastructureException extends DomainException {} +``` #### 2. Kategorien & HTTP-Mapping -Folgende Untergruppen definieren das Verhalten im UI-Layer: +Die Basis-Exceptions definieren das Verhalten im UI-Layer. Module implementieren darauf aufbauend spezifische Exceptions (z. B. `OrderNotFoundException` erbt von `ResourceNotFoundException`). | Exception Gruppe | HTTP Status | Zweck | Beispiel | | :--- | :--- | :--- | :--- | @@ -1064,10 +1056,19 @@ Hier wird die Brücke zur Infrastruktur geprüft. - **Datenbank**: Nutzung einer dedizierten Test-DB. Jeder Test sollte in einer Transaktion laufen, die am Ende gerolled wird (oder via Database-Reset). - **Mapper-Tests**: Explizite Prüfung: `Entity` $\rightarrow$ `toModel()` $\rightarrow$ `Business Model`. -#### 3. Functional Tests (`tests/Functional`) -Diese nutzen den Symfony `WebTestCase`, um das System als "Black Box" zu testen. -- **UI Mirroring**: In `tests/Functional/UI` wird pro Controller ein entsprechender Test-Case angelegt, der die HTTP-Antworten (Status-Codes, JSON-Struktur) validiert. -- **Scenario-Tests**: In `tests/Functional/Scenarios` werden reale Business Flows abgebildet (z.B. `OrderProcessTest`), die mehrere API-Calls hintereinander ausführen und den finalen Zustand in der Datenbank prüfen. +#### 3. Functional Tests - UI-Mirroring (`tests/Functional/UI`) +Diese nutzen den Symfony `WebTestCase` für isolierte Endpunkt-Tests. +* **Vorteil:** Garantiert genaue HTTP-Antworten (Status-Codes, JSON-Struktur) und isoliert Fehler präzise auf einen Controller. +* **Nachteil:** Hoher Wartungsaufwand bei jeder API-Änderung. Oft redundante Coverage, wenn UseCases bereits im Logic Layer getestet sind. + +#### 4. Functional Tests - Scenarios (`tests/Functional/Scenarios`) +Diese testen den vollständigen Blackbox-Durchlauf über mehrere Endpunkte. +* **Vorteil:** Stellt sicher, dass komplette Business Flows funktionieren (z.B. Order → Rechnung → Email). Geringere Wartung durch weniger Testfälle und Validierung der Layer-Integration. +* **Nachteil:** Schwierigeres Debugging. Wenn ein Scenario fehltschlägt, muss man herausfinden, ob der Fehler an Schritt 1 (Validierung), am UseCase oder einem späteren DB-Schritt lag. + +#### 5. Empfehlungen zur Auswahl: +- Nutze **UI-Mirroring**, wenn die API-Kontrakte kritisch sind und häufig von externen Systemen verwendet werden. +- Nutze **Scenarios**, um den Business-Wert sicherzustellen und komplexe Transaktionen zu prüfen, die über einen einzigen Request hinausgehen. #### 4. Mocking Guidelines Um "fragile Tests" zu vermeiden, gilt: diff --git a/architektur.md b/architektur.md index 35c2fb7..c35678a 100644 --- a/architektur.md +++ b/architektur.md @@ -16,7 +16,7 @@ Die Anwendung ist in drei strikt getrennte Layer unterteilt: - **Komponenten**: - **UseCases / Workflows**: Repräsentieren eine spezifische Geschäftsaktion (z.B. `CreateUserUseCase`). Nutzen DTOs für den Datentransfer. - **BusinessQueries**: Spezifische Abfragen für geschäftsrelevante Daten. - - **Manager**: Orchestratoren, die z.B. Caching-Strategien implementieren und zwischen verschiedenen Providern/Processoren vermitteln. + - **Manager**: Steuert Lese- und Schreibzugriffe auf ein einzelnes Business Model (z. B. Caching via "Cache-Aside"). Komplexe Aggregationen über mehrere Domänen hinweg werden bewusst vermieden, um Kopplung zu reduzieren. - **Models**: Kernlogik-Objekte zur internen Verarbeitung innerhalb der Logic Layer. - **Provider / Processor Interfaces**: Definieren den Datenaustausch mit der Data Layer und ermöglichen Dependency Inversion. - **Regel**: Ist unabhängig von der UI Layer und definiert die Anforderungen an die Data Layer über Interfaces. @@ -40,12 +40,13 @@ Die Anwendung ist in drei strikt getrennte Layer unterteilt: 1. **Eintrittspunkt**: Die UI Layer ruft immer einen UseCase, Workflow oder eine BusinessQuery in der Logic Layer auf. 2. **Orchestrierung**: Manager in der Logic Layer organisieren den Zugriff (z.B. Cache-Check) und delegieren die eigentliche Arbeit an Provider- oder Processor-Interfaces der Data Layer. 3. **Entkopplung**: Die Logic Layer sollte idealerweise gegen Interfaces der Data Layer programmieren, um die Austauschbarkeit der Datenquelle zu gewährleisten. +4. **Modulkapselung (Adapter)**: Module kommunizieren nicht direkt mit den internen Strukturen anderer Module. Wenn ein UseCase aus Modul A eine Fähigkeit von Modul B benötigt, definiert Modul A ein eigenes Adapter-Interface. Die Implementation von Modul B wird via Dependency Inversion im Container auf dieses Interface gemappt. Dies verhindert enge Kopplung und vereinfacht Tests entscheidend. ## 3. Validierungsstrategie -Das Prinzip lautet: Jeder Layer validiert das, was er in seinem Kontext validieren kann. +Das Prinzip lautet: Jeder Layer validiert das, was er in seinem Kontext validieren kann. Eine detaillierte Beschreibung mit Layer-Verantwortlichkeiten und Codebeispielen findet sich in [architektur-patterns.md Sektion 4](./architektur-patterns.md#4-validierungsstrategie-mehrschichtig). -- **UI Layer**: Syntaktische Validierung (z. B. Format, Pflichtfelder, Typen). Ziel ist es, ungültigen Input so früh wie möglich abzufangen. +- **UI Layer**: Syntaktische Validierung (z. B. Format, Pflichtfelder, Typen). - **Logic Layer**: Semantische/Business-Validierung (z. B. Logik-Checks gegen Geschäftsregeln, Berechtigungen). - **Data Layer**: Technische Integrität (z. B. Unique-Constraints der Datenbank, korrekte Datentypen in der Persistenzschicht). @@ -70,8 +71,9 @@ Zur Entkopplung zeitintensiver Prozesse wird ein Message-Bus eingesetzt. Dabei g - **Dispatcher Entkopplung**: Die Logic Layer nutzt ein eigenes Interface (z. B. `EventPublisherInterface`), um Nachrichten zu versenden. Die technische Implementierung dieses Interfaces erfolgt außerhalb der Logikschicht (Framework-Bridge), sodass die Business-Logik unabhängig vom Symfony Messenger bleibt. ### Message Handler -- **Ort**: Message-Handler befinden sich in der **UI Layer**. -- **Rolle**: Da ein asynchroner Nachrichteneingang ein externer Eintrittspunkt ist, fungiert der Handler als Adapter. Er nimmt die Message entgegen und delegiert die eigentliche Verarbeitung an einen entsprechenden UseCase oder Workflow in der Logic Layer. +- **Ort**: Message-Handler befinden sich in `src/UI/{Module}/Handlers/{Feature}/{Name}Handler.php`. +- **One-to-One Prinzip**: Es gilt strikt eine Nachricht pro Handler. Jeder Handler bleibt schlank und übernimmt nur die Transformation der Message und delegiert sie an den korrekten UseCase oder Workflow. +- **Rolle**: Da ein asynchroner Nachrichteneingang ein externer Eintrittspunkt ist, fungiert der Handler als Adapter. Er nimmt die Message entgegen und delegiert die eigentliche Verarbeitung an einen genau einem bestimmten UseCase oder Workflow in der Logic Layer. ## 6. Teststrategie & Symmetrie Um eine hohe Codequalität und Wartbarkeit zu gewährleisten, wird eine pyramidale Teststrategie verfolgt. Dabei gilt das Prinzip der **Symmetrie**: Die Verzeichnisstruktur unter `tests/` spiegelt exakt die Struktur von `src/` wider, um die Auffindbarkeit von Tests sicherzustellen.