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/...`
#### 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: