docs: consolidate module communication, handler paths, manager granularity, mapping factory pattern, and testing guidelines
This commit is contained in:
+48
-47
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user