docs: consolidate module communication, handler paths, manager granularity, mapping factory pattern, and testing guidelines

This commit is contained in:
2026-07-17 12:27:08 +02:00
parent d3b2fad8a9
commit 0da6f5c10e
2 changed files with 55 additions and 52 deletions
+48 -47
View File
@@ -195,8 +195,9 @@ Das Read Pattern beschreibt den Weg einer Datenabfrage von der UI bis zur Datenq
- **Pfad**: `src/Logic/{Module}/{Feature}/Query/...` - **Pfad**: `src/Logic/{Module}/{Feature}/Query/...`
#### Manager (Logic Layer) #### Manager (Logic Layer)
- **Zweck**: Steuert die Bereitstellung der Daten. Hier erfolgt primär die Cache-Logik ("Cache-Aside") und die Koordination von Providern. - **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 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. - **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. - **Schnittstelle**: Besitzt ein eigenes Interface in der Logic Layer zur Gewährleistung von Testbarkeit und Austauschbarkeit.
- **Zustand**: Strikt zustandslos (stateless). - **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** `UI Layer` $\rightarrow$ **UseCase** $\rightarrow$ **Manager** $\rightarrow$ **Processor**
#### Manager (Logic Layer) #### Manager (Logic Layer)
- **Zweck**: Zentrale Steuereinheit pro Domain, die sowohl Lese- als auch Schreiboperationen koordiniert. - **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). Dies garantiert Konsistenz zwischen gecachten Daten und der primären Datenquelle. - **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) #### Processor (Data Layer)
- **Zweck**: Übernimmt die physische Persistierung eines Business-Models. - **Zweck**: Übernimmt die physische Persistierung eines Business-Models.
@@ -647,52 +649,32 @@ readonly class OrderModelFactory
} }
``` ```
#### Kopier-Pattern für Partial-Updates (Model.copyWith) #### Rebuild-Factor-Pattern für Updates (Partial & Full)
Wenn nur bestimmte Felder eines bestehenden Business-Models geändert werden sollen, empfiehlt es sich, eine **copyWith**-Methode im Model selbst zu implementieren: 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 ```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 function rebuildFromPartialUpdate(Order $current, UpdateOrderRequest $request): Order
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
{ {
return new self( return new self(
id: $this->id, id: $request->id,
customerEmail: $this->customerEmail, customerEmail: $request->customerEmail ?? $current->customerEmail,
totalAmount: $this->totalAmount, totalAmount: $request->totalAmount ?? $current->totalAmount,
version: $this->version + 1, // ... übrige Felder werden unverändert übernommen.
); );
} }
} }
// Verwendung im UseCase oder in der Factory // Verwendung im UseCase:
$updatedOrder = $existingOrder->with(customerEmail: 'new@example.com', totalAmount: 123.45); $dto = new UpdateOrderRequest(id: '123', customerEmail: 'neuer@test.de');
$updatedOrder = $this->orderModelFactory->rebuildFromPartialUpdate($existingOrder, $dto);
``` ```
### Code Beispiel (Data Layer Mapping) ### 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`) ### Die Hierarchie (`src/Logic/Common/Exception`)
Alle fachlichen Exceptions erben von einer abstrakten Basisklasse `DomainException`. Alle fachlichen Exceptions erben von einer abstrakten Basisklasse `DomainException`.
#### 1. Root: `DomainException` (abstract) #### Code-Skelett (Basisimplementierung)
Die Wurzel aller fachlichen Fehler der Logic Layer. ```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 #### 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 | | 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). - **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`. - **Mapper-Tests**: Explizite Prüfung: `Entity` $\rightarrow$ `toModel()` $\rightarrow$ `Business Model`.
#### 3. Functional Tests (`tests/Functional`) #### 3. Functional Tests - UI-Mirroring (`tests/Functional/UI`)
Diese nutzen den Symfony `WebTestCase`, um das System als "Black Box" zu testen. Diese nutzen den Symfony `WebTestCase` für isolierte Endpunkt-Tests.
- **UI Mirroring**: In `tests/Functional/UI` wird pro Controller ein entsprechender Test-Case angelegt, der die HTTP-Antworten (Status-Codes, JSON-Struktur) validiert. * **Vorteil:** Garantiert genaue HTTP-Antworten (Status-Codes, JSON-Struktur) und isoliert Fehler präzise auf einen Controller.
- **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. * **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 #### 4. Mocking Guidelines
Um "fragile Tests" zu vermeiden, gilt: Um "fragile Tests" zu vermeiden, gilt:
+7 -5
View File
@@ -16,7 +16,7 @@ Die Anwendung ist in drei strikt getrennte Layer unterteilt:
- **Komponenten**: - **Komponenten**:
- **UseCases / Workflows**: Repräsentieren eine spezifische Geschäftsaktion (z.B. `CreateUserUseCase`). Nutzen DTOs für den Datentransfer. - **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. - **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. - **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. - **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. - **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. 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. 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. 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 ## 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). - **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). - **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. - **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 ### Message Handler
- **Ort**: Message-Handler befinden sich in der **UI Layer**. - **Ort**: Message-Handler befinden sich in `src/UI/{Module}/Handlers/{Feature}/{Name}Handler.php`.
- **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. - **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 ## 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. 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.