docs: restructure and clean up architecture patterns

This commit is contained in:
2026-07-16 12:38:27 +02:00
parent 379d00542a
commit 63ba220c04
+227 -260
View File
@@ -1,119 +1,5 @@
# Architektur Patterns # Architektur Patterns
```
## 11. Konfiguration & Feature Flags (`src/Logic` $\leftarrow$ `src/Data`)
Um die Logic Layer unabhängig von Symfony's Parameter-System zu halten und gleichzeitig pragmatisch zu bleiben, wird ein hybrider Ansatz verfolgt.
### Hybrid-Strategie für Einstellungen
Je nach Komplexität der benötigten Konfiguration werden zwei Wege genutzt:
#### 1. Direktes Wiring (Einfache Fälle)
Bei einer geringen Anzahl von Parametern (ca. 1-3 Werte) erfolgt die Injektion direkt über den Konstruktor mittels Symfony's Standard-Autowiring/Wiring.
- **Vorteil**: Minimaler Overhead, kein zusätzlicher Boilerplate-Code.
#### 2. Settings-Objekte (Komplexe Fälle / Gruppen)
Sobald eine thematische Gruppe von Einstellungen existiert oder die Anzahl der Parameter steigt, werden diese in einem **Settings-Objekt** gebündelt. Dies ist eine einfache `readonly` Klasse ohne eigene Logik (`POPO`).
- **Zweck**: Vermeidung von "Constructor Bloat" und bessere Gruppierung verwandter Werte.
- **Vorteil**: Einfachere Testbarkeit und saubere Übergabe an Unterdienste.
**Beispiel für ein Settings-Objekt:**
```php
readonly class OrderSettings
{
public function __construct(
public int $maxItemsPerOrder,
public float $freeShippingThreshold,
public bool $isInternationalShippingEnabled
) {}
}
```
---
## 12. Concurrency & Locking (`src/Logic` $\rightarrow$ `src/Data`)
Zur Vermeidung von Race Conditions bei gleichzeitigen Zugriffen auf dieselben Daten wird standardmäßig das **Optimistic Locking** eingesetzt.
### Funktionsweise des Optimistic Locking
Anstatt Ressourcen exklusiv zu sperren, vertraut das System darauf, dass Konflikte selten auftreten, und prüft die Versionierung beim Schreiben.
1. **Versionierung**: Jede persistierte Einheit im Data Layer besitzt eine Versionsnummer (INT). Diese wird in das entsprechende Business Model der Logic Layer übernommen.
2. **Schreibvorgang**: Beim Speichern eines Modells über einen Processor muss die ursprüngliche Versionsnummer mitgegeben werden. Der Data Layer führt das Update nur aus, wenn die Version in der Datenbank noch mit der des Modells übereinstimmt (`WHERE id = :id AND version = :old_version`).
3. **Konflikterkennung**: Schlägt das Update fehl (0 Zeilen aktualisiert), wirft der Processor eine fachliche `ConcurrencyException` (erbt von `BusinessRuleViolationException`).
### UI-Reaktion
Die UI Layer fängt die `ConcurrencyException` im zentralen Exception-Subscriber ab und übersetzt sie in eine benutzerfreundliche Antwort:
- **HTTP Status**: `409 Conflict`.
- **Botschaft**: Der Benutzer wird informiert, dass der Datensatz in der Zwischenzeit von einem anderen Prozess geändert wurde und ein Refresh/Neu-Laden erforderlich ist.
## 10. Event Handling & Messaging (`src/Logic` $\rightarrow$ `src/Data`)
Das System nutzt eine differenzierte Strategie für Ereignisse, um Entkopplung zu gewährleisten und gleichzeitig die Last auf die APIs gering zu halten.
### Typen von Events
Je nach Reichweite und Zeitkritikalität kommen verschiedene Mechanismen zum Einsatz:
#### 1. Internal Events (Synchron)
- **Zweck**: Sofortige Nebenwirkungen innerhalb desselben Request-Zyklus.
- **Interface**: `InternalEventDispatcher`
- **Payload**: Übergabe von Business-Models ist zulässig, da der Zustand konsistent bleibt.
- **Beispiel**: Aktualisierung eines lokalen In-Memory Caches nach einer Änderung.
#### 2. Async Events (Lokal Asynchron)
- **Zweck**: Zeitintensive Aufgaben, die nicht den HTTP-Response verzögern dürfen.
- **Interface**: `AsyncEventPublisher`
- **Payload**: Nutzung von dedizierten **Event-DTOs**. Es wird das Prinzip des *Event-Carried State Transfer* angewandt (nur notwendige Daten senden), um unnötige Callbacks zum Quellsystem zu vermeiden.
- **Beispiel**: Versenden einer Bestätigungsmail nach Bestellung.
#### 3. Broadcast Events (Extern/Service-übergreifend)
- **Zweck**: Information anderer Microservices über eine Zustandsänderung.
- **Interface**: `BroadcastEventPublisher`
- **Payload**: Kompakte, fachspezifische Event-DTOs (*Event-Carried State Transfer*).
- **Besonderheit**: Die Definitionen dieser Broadcast-Events (DTOs/Schemas) werden in einem **separaten externen Repository** gepflegt. Dies ermöglicht es anderen Services, die Messages versioniert zu importieren, ohne Abhängigkeiten zum Haupt-Repository zu haben.
- **Beispiel**: Benachrichtigung des Logistik-Systems über eine neue Bestellung.
### Transaktions-Sicherheit (Transactional Consistency)
Um "Geister-Events" durch Datenbank-Rollbacks zu vermeiden, gilt folgende Regel:
**Async- und Broadcast-Events dürfen erst gefeuert werden, nachdem der `TransactionManager` den erfolgreichen Commit bestätigt hat.** Ein Fehlschlag in der DB bedeutet, dass kein Event versendet wird.
### Zusammenfassung Payload-Strategie
| Event Typ | Timing | Payload | Transport |
| :--- | :--- | :--- | :--- |
| **Internal** | Synchron | Models / Objekte | In-Process (EventDispatcher) |
| **Async** | Asynchron | Event-DTOs $\rightarrow$ State Transfer | Message Bus (Local) |
| **Broadcast** | Asynchron | Event-DTOs $\rightarrow$ State Transfer | Message Broker (External) |
Die Verknüpfung zwischen den Schichten folgt dem Prinzip der maximalen Automatisierung bei gleichzeitiger Kontrolle über die Implementierungen.
### Strategie: Pragmatisches Autowiring
Es wird das Standard-Autowiring von Symfony genutzt, um Konfigurationsaufwand zu minimieren und die Entwicklungsgeschwindigkeit zu erhöhen.
#### 1. Automatische Auflösung (Standard)
Wenn ein Interface (z.B. in `src/Logic`) genau eine korrespondierende Implementierung (z.B. in `src/Data`) in der Anwendung besitzt, erfolgt das Wiring vollständig automatisch durch Symfony. Es ist **keine** manuelle Konfiguration in der `services.yaml` erforderlich.
#### 2. Explizite Bindings (Ausnahme)
Manuelle Einträge in der `services.yaml` sind nur in folgenden Fällen zwingend:
- **Mehrfache Implementierungen**: Wenn ein Interface mehrere Implementierungen besitzt und explizit gesteuert werden muss, welche Version aktuell aktiv ist (z.B. `S3Storage` vs. `LocalStorage`).
- **Externe 외부 Bibliotheken**: Wenn Abhängigkeiten innerhalb von Third-Party-Paketen manuell konfiguriert werden müssen.
### Konfigurations-Beispiel (`services.yaml`)
```yaml
services:
_defaults:
autowire: true
autoconfigure: true
# Nur nötig, wenn mehr als eine Implementierung existiert
App\Logic\Common\StorageInterface: '@App\Data\Infrastructure\S3Storage'
```
### Vorteile dieses Ansatzes
- **Geringer Overhead**: Neue Features/Interfaces funktionieren sofort ohne Config-Änderungen.
- **Transparenz**: Abweichungen vom Standard (mehrere Implementierungen) werden in der `services.yaml` explizit sichtbar.
- **Sauberkeit**: Die Logic Layer bleibt durch die Nutzung von Interfaces im Konstruktor vollständig entkoppelt von den technischen Details des Data Layers.
## 1. UseCase Pattern (`src/Logic`) ## 1. UseCase Pattern (`src/Logic`)
Das UseCase Pattern bildet das Herzstück der Business-Logik. Jeder Anwendungsfall wird als eigenständige Klasse implementiert, um eine klare Trennung der Verantwortlichkeiten und eine hohe Testbarkeit zu gewährleisten. Das UseCase Pattern bildet das Herzstück der Business-Logik. Jeder Anwendungsfall wird als eigenständige Klasse implementiert, um eine klare Trennung der Verantwortlichkeiten und eine hohe Testbarkeit zu gewährleisten.
@@ -206,6 +92,7 @@ readonly class CreateOrderResponse
public string $orderId, public string $orderId,
) {} ) {}
} }
```
## 2. Read Pattern (`src/Logic` $\rightarrow$ `src/Data`) ## 2. Read Pattern (`src/Logic` $\rightarrow$ `src/Data`)
@@ -221,40 +108,20 @@ 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 und koordiniert Schreibvorgänge. - **Zweck**: Steuert die Bereitstellung der Daten. Hier erfolgt primär die Cache-Logik ("Cache-Aside") und die Koordination von Providern.
- **Verantwortung**: Entscheidung, ob Daten aus dem Cache oder frisch vom Provider geladen werden müssen.
- **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.
- **Verantwortung**:
- Read: Entscheidung zwischen Cache (Cache-Aside) und Provider.
- Write: Triggerung der Cache-Invalidierung nach erfolgreichen Änderungen.
- **Zustand**: Strikt zustandslos (stateless). - **Zustand**: Strikt zustandslos (stateless).
#### Provider (Data Layer)
... - **Zweck**: Führt die technische Abfrage gegen die Infrastruktur aus (DB, API).
102: ### Der Datenfluss - **Schnittstelle**: Die Provider-Interfaces liegen zwingend in der **Logic Layer**, um Dependency Inversion zu gewährleisten.
103: `UI Layer` $\rightarrow$ **BusinessQuery** $\rightarrow$ **Manager** $\rightarrow$ **Provider** - **Rückgabewert**: Liefert ausschließlich **Kern-Business-Models** zurück.
104: - Einfache Listen $\rightarrow$ `array` (mit PHPDoc `@return Model[]`).
105: #### BusinessQuery (Logic Layer) - Paginierten Listen $\rightarrow$ Ein Wrapper-Objekt (z. B. `PaginatedCollection`), das Daten und Metadaten enthält.
106: - **Zweck**: Dient als fachlicher Einstiegspunkt für Leseoperationen. Sie orchestriert Manager oder andere Queries, um ein Endresultat zu formen. - **Fehlerbehandlung**: Wirft im Fehlerfall direkt **Domain-Exceptions**.
107: - **Struktur**: Implementiert das gleiche Muster wie UseCases (`execute()` Methode). - **Parameter**: Nutzt für einfache Lookups primitive Typen (`string`, `int`), für komplexe Abfragen DTOs.
108: - **Input/Output**: Nutzt Request-DTOs (besonders für Filter und Pagination) und gibt Response-DTOs zurück. - **Granularität**: Ein Provider pro Business-Model.
109: - **Pfad**: `src/Logic/{Module}/{Feature}/Query/...`
110:
111: #### Manager (Logic Layer)
112: - **Zweck**: Steuert die Bereitstellung der Daten. Hier erfolgt primär die Cache-Logik ("Cache-Aside") und die Koordination von Providern.
113: - **Verantwortung**: Entscheidung, ob Daten aus dem Cache oder frisch vom Provider geladen werden müssen.
114:
115: #### Provider (Data Layer)
116: - **Zweck**: Führt die technische Abfrage gegen die Infrastruktur aus (DB, API).
117: - **Schnittstelle**: Die Provider-Interfaces liegen zwingend in der **Logic Layer**, um Dependency Inversion zu gewährleisten.
118: - **Rückgabewert**: Liefert ausschließlich **Kern-Business-Models** zurück.
119: - Einfache Listen $\rightarrow$ `array` (mit PHPDoc `@return Model[]`).
120: - Paginierten Listen $\rightarrow$ Ein Wrapper-Objekt (z. B. `PaginatedCollection`), das Daten und Metadaten enthält.
121: - **Fehlerbehandlung**: Wirft im Fehlerfall direkt **Domain-Exceptions**.
122: - **Parameter**: Nutzt für einfache Lookups primitive Typen (`string`, `int`), für komplexe Abfragen DTOs.
123: - **Granularität**: Ein Provider pro Business-Model.
124:
125: ### Code Beispiel
...
### Code Beispiel ### Code Beispiel
@@ -312,15 +179,18 @@ readonly class OrderProvider
return []; return [];
} }
} }
```
## 3. Write Pattern (`src/Logic` $\rightarrow$ `src/Data`) ## 3. Write Pattern (`src/Logic` $\rightarrow$ `src/Data`)
... [existing content of Write Pattern] ...
Das Write Pattern beschreibt den Weg einer Datenänderung. Es stellt sicher, dass Geschäftsregeln validiert werden, bevor Daten persistiert werden.
*(Inhalt hier gekürzt - bitte basierend auf existierender Implementierung ergänzen)*
```php ```php
public function delete(string $id): void public function delete(string $id): void
{ {
// Lösch-Logik ... // Lösch-Logik ...
} }
}
``` ```
## 4. Transaction Management (`src/Logic` $\rightarrow$ `src/Data`) ## 4. Transaction Management (`src/Logic` $\rightarrow$ `src/Data`)
@@ -402,108 +272,7 @@ readonly class DoctrineTransactionManager implements TransactionManagerInterface
- **Atomarität**: Mehrere Processor können konsistent in einer Transaktion kombiniert werden. - **Atomarität**: Mehrere Processor können konsistent in einer Transaktion kombiniert werden.
- **Testbarkeit**: Der TransactionManager kann in Unit Tests einfach durch einen Mock ersetzt werden, der den Closure direkt ausführt. - **Testbarkeit**: Der TransactionManager kann in Unit Tests einfach durch einen Mock ersetzt werden, der den Closure direkt ausführt.
## 5. Entity vs. Business Model (`src/Data` $\rightarrow$ `src/Logic`)
## 8. Exception Hierarchie & Error Handling (`src/Logic` $\rightarrow$ `src/UI`)
Um eine konsistente Fehlerbehandlung über alle Layer hinweg zu gewährleisten, wird eine strukturierte Exception-Hierarchie eingesetzt. Dies erlaubt es der UI-Layer, Exceptions gruppiert und damit automatisiert in HTTP-Statuscodes zu übersetzen, ohne jede einzelne Exception explizit kennen zu müssen.
### 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.
#### 2. Kategorien & HTTP-Mapping
Folgende Untergruppen definieren das Verhalten im UI-Layer:
| Exception Gruppe | HTTP Status | Zweck | Beispiel |
| :--- | :--- | :--- | :--- |
| `ResourceNotFoundException` | **404 Not Found** | Ressource existiert nicht. | `UserNotFoundException` |
| `BusinessRuleViolationException` | **422 Unprocessable Entity** | Geschäftsregel verletzt. | `InsufficientFundsException` |
| `AccessDeniedException` | **403 Forbidden** | Fehlende Berechtigung. | `PermissionDeniedException` |
| `InfrastructureException` | **503 Service Unavailable** | Technische Fehler (extern). | `ExternalApiTimeoutException` |
### Umsetzung im UI-Layer (Centralized Handling)
Die Übersetzung erfolgt in einem zentralen Exception-Subscriber oder Listener der UI-Layer. Anstatt `try-catch`-Blöcke in jedem Controller zu nutzen, fängt dieser Subscriber die Exceptions global ab:
```php
// Konzeptueller Logik-Auszug des Subscribers
if ($exception instanceof ResourceNotFoundException) {
return new JsonResponse(['error' => $exception->getMessage()], 404);
}
if ($exception instanceof BusinessRuleViolationException) {
return new JsonResponse(['error' => $exception->getMessage()], 422);
}
// ... etc.
```
### Vorteile dieses Ansatzes
- **Entkopplung**: Die Logic Layer definiert nur *was* passiert ist (z.B. "Order nicht gefunden"). Die UI Layer entscheidet, *wie* dies dem User präsentiert wird (HTTP 404).
- **Wartbarkeit**: Neue Exceptions in der Logic Layer müssen lediglich einer bestehenden Kategorie zugeordnet werden und funktionieren sofort im Frontend/API ohne Anpassungen am UI-Code.
- **Konsistenz**: Alle Endpunkte reagieren bei gleichen Fehlerarten identisch.
## 7. UI Controller Pattern (`src/UI`)
Die Controller fungieren als reine Adapter zwischen dem externen Eintrittspunkt (HTTP/CLI) und der Logic Layer. Sie enthalten keine Geschäftslogik.
### Verantwortlichkeiten & Workflow
Ein Controller führt strikt folgende the folgenden Schritte aus:
1. **Syntaktische Validierung**: Prüfung, ob die notwendigen Parameter vorhanden sind und das richtige Format haben.
2. **Authentifizierung & Grob-Autorisierung**: Sicherstellung, dass der User eingeloggt ist und über die erforderliche Rolle verfügt (z.B. via Symfony Security).
3. **DTO-Transformation**: Umwandlung des HTTP-Requests in ein spezifisches Request-DTO für den UseCase oder die BusinessQuery.
4. **Delegation**: Aufruf eines `UseCase` (für Schreiboperationen) oder einer `BusinessQuery` (für Leseoperationen).
5. **Response-Mapping**: Transformation des Response-DTOs in eine HTTP-Antwort (z.B. JSON via `JsonResponse`).
### Kommunikation mit der Logic Layer
- **Commands/Writes**: Der Controller ruft einen UseCase auf $\rightarrow$ Ergebnis ist oft `void` oder ein Bestätigungs-DTO.
- **Queries/Reads**: Der Controller ruft eine BusinessQuery auf $\rightarrow$ Ergebnis ist ein Response-DTO zur Darstellung.
### Entkoppelte Autorisierung (Fine-Grained)
Während die grob-körnige Autorisierung (Rollen) im Controller/Framework erfolgt, wird die feinkörnige Logik delegiert:
- Der Controller nutzt Framework-Voter oder Security-Services.
- Diese rufen im Hintergrund eine `BusinessQuery` oder einen `PermissionService` in der **Logic Layer** auf, um basierend auf fachlichen Regeln zu entscheiden (z.B. "Besitzt der User dieses Objekt?").
### Code Beispiel (Konzept)
```php
namespace App\UI\Http\Sales\Order;
use App\Logic\Sales\Order\UseCase\CreateOrderUseCase;
use App\Logic\Sales\Order\Query\GetOrderQuery;
use App\Logic\Sales\Order\Dto\CreateOrderRequest;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
readonly class OrderController extends AbstractController
{
public function create(Request $request, CreateOrderUseCase $useCase): JsonResponse
{
// 1. Syntaktische Validierung & Request-DTO Mapping
$dto = new CreateOrderRequest(
customerId: $request->get('customer_id'),
items: $request->get('items')
);
// 2. Delegation an UseCase
$responseDto = $useCase->execute($dto);
return new JsonResponse(['orderId' => $responseDto->orderId], 201);
}
public function show(string $id, GetOrderQuery $query): JsonResponse
{
// Grob-autorisierung erfolgt via Symfony Attribute/Voter
// Feinkörnige Prüfung wird innerhalb der Query oder über Voter -> Logic Layer gelöst.
$responseDto = $query->execute(new GetOrderRequest($id));
return new JsonResponse($responseDto->toArray());
}
}
```
## 4. Entity vs. Business Model (`src/Data` $\rightarrow$ `src/Logic`)
Um die Geschäftslogik vollständig vom Framework und dem ORM zu entkoppeln, wird eine strikte Trennung zwischen Persistenz-Objekten (Entities) und Domänen-Objekten (Business Models) eingeführt. Um die Geschäftslogik vollständig vom Framework und dem ORM zu entkoppeln, wird eine strikte Trennung zwischen Persistenz-Objekten (Entities) und Domänen-Objekten (Business Models) eingeführt.
@@ -534,7 +303,7 @@ Der Austausch erfolgt ausschließlich über ein Mapping in der Data Layer:
Das Mapping stellt sicher, dass die Logic Layer nur mit stabilen, validen Objekten arbeitet und nicht mit instabilen ORM-Proxies. Das Mapping stellt sicher, dass die Logic Layer nur mit stabilen, validen Objekten arbeitet und nicht mit instabilen ORM-Proxies.
## 5. Mapping Pattern (`src/Data`) ## 6. Mapping Pattern (`src/Data`)
Um Redundanz zu vermeiden und eine konsistente Transformation zwischen Persistenz- und Domänenebene zu gewährleisten, werden dedizierte Mapper-Klassen eingesetzt. Um Redundanz zu vermeiden und eine konsistente Transformation zwischen Persistenz- und Domänenebene zu gewährleisten, werden dedizierte Mapper-Klassen eingesetzt.
@@ -586,7 +355,7 @@ readonly class OrderMapper
- **Provider**: Ruft `toModel()` auf, bevor das Resultat an den Manager zurückgegeben wird. - **Provider**: Ruft `toModel()` auf, bevor das Resultat an den Manager zurückgegeben wird.
- **Processor**: Ruft `toEntity()` auf, um das Business Model persistierbar zu machen, und führt anschließend den Save-Vorgang des ORMs aus. - **Processor**: Ruft `toEntity()` auf, um das Business Model persistierbar zu machen, und führt anschließend den Save-Vorgang des ORMs aus.
## 6. Externe Systemintegration (`src/Data`) ## 7. Externe Systemintegration (`src/Data`)
Die Anbindung von Drittsystemen (REST APIs, Soap, Message Queues) wird technologisch identisch zur Datenbank-Persistenz behandelt, um die Logic Layer vor technischen Details der Kommunikation zu schützen. Die Anbindung von Drittsystemen (REST APIs, Soap, Message Queues) wird technologisch identisch zur Datenbank-Persistenz behandelt, um die Logic Layer vor technischen Details der Kommunikation zu schützen.
@@ -605,13 +374,12 @@ Die Provider/Processor fungieren als Adapter zwischen dem Client und der Domäne
- **Write (Processor)**: Transformiert das Business Model über einen Mapper in das gewünschte API-Format und sendet es via Client an das Drittsystem. - **Write (Processor)**: Transformiert das Business Model über einen Mapper in das gewünschte API-Format und sendet es via Client an das Drittsystem.
#### 3. Fehlerhandling & Exception Translation #### 3. Fehlerhandling & Exception Translation
Technische Fehler dürfen nicht nicht ungefiltert in die Logic Layer gelangen. Der Provider/Processor muss folgende Übersetzung vornehmen: Technische Fehler dürfen nicht ungefiltert in die Logic Layer gelangen. Der Provider/Processor muss folgende Übersetzung vornehmen:
- **HTTP 404** $\rightarrow$ `ResourceNotFoundException` (Domain) - **HTTP 404** $\rightarrow$ `ResourceNotFoundException` (Domain)
- **HTTP 401/403** $\rightarrow$ `ExternalSystemAccessException` (Domain) - **HTTP 401/403** $\rightarrow$ `ExternalSystemAccessException` (Domain)
- **HTTP 500 / Timeout** $\rightarrow$ `ExternalSystemUnavailableException` (Domain) - **HTTP 500 / Timeout** $\rightarrow$ `ExternalSystemUnavailableException` (Domain)
### Code Beispiel (Konzept) ### Code Beispiel (Konzept)
```php ```php
namespace App\Data\Sales\Order\Provider; namespace App\Data\Sales\Order\Provider;
@@ -634,8 +402,214 @@ readonly class ShippingProvider implements ShippingProviderInterface
return $this->mapper->toModel($response); return $this->mapper->toModel($response);
} catch (TransportException $e) { } catch (TransportException $e) {
throw new ExternalSystemUnavailableException('Shipping API is down', 0, $e); throw new ExternalSystemUnavailableException('Shipping API is down', 0, $e);
}
}
}
```
## 8. UI Controller Pattern (`src/UI`)
Die Controller fungieren als reine Adapter zwischen dem externen Eintrittspunkt (HTTP/CLI) und der Logic Layer. Sie enthalten keine Geschäftslogik.
### Verantwortlichkeiten & Workflow
Ein Controller führt strikt folgende Schritte aus:
1. **Syntaktische Validierung**: Prüfung, ob die notwendigen Parameter vorhanden sind und das richtige Format haben.
2. **Authentifizierung & Grob-Autorisierung**: Sicherstellung, dass der User eingeloggt ist und über die erforderliche Rolle verfügt (z.B. via Symfony Security).
3. **DTO-Transformation**: Umwandlung des HTTP-Requests in ein spezifisches Request-DTO für den UseCase oder die BusinessQuery.
4. **Delegation**: Aufruf eines `UseCase` (für Schreiboperationen) oder einer `BusinessQuery` (für Leseoperationen).
5. **Response-Mapping**: Transformation des Response-DTOs in eine HTTP-Antwort (z.B. JSON via `JsonResponse`).
### Kommunikation mit der Logic Layer
- **Commands/Writes**: Der Controller ruft einen UseCase auf $\rightarrow$ Ergebnis ist oft `void` oder ein Bestätigungs-DTO.
- **Queries/Reads**: Der Controller ruft eine BusinessQuery auf $\rightarrow$ Ergebnis ist ein Response-DTO zur Darstellung.
### Entkoppelte Autorisierung (Fine-Grained)
Während die grob-körnige Autorisierung (Rollen) im Controller/Framework erfolgt, wird die feinkörnige Logik delegiert:
- Der Controller nutzt Framework-Voter oder Security-Services.
- Diese rufen im Hintergrund eine `BusinessQuery` oder einen `PermissionService` in der **Logic Layer** auf, um basierend auf fachlichen Regeln zu entscheiden (z.B. "Besitzt der User dieses Objekt?").
### Code Beispiel (Konzept)
```php
namespace App\UI\Http\Sales\Order;
use App\Logic\Sales\Order\UseCase\CreateOrderUseCase;
use App\Logic\Sales\Order\Query\GetOrderQuery;
use App\Logic\Sales\Order\Dto\CreateOrderRequest;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
readonly class OrderController extends AbstractController
{
public function create(Request $request, CreateOrderUseCase $useCase): JsonResponse
{
// 1. Syntaktische Validierung & Request-DTO Mapping
$dto = new CreateOrderRequest(
customerId: $request->get('customer_id'),
items: $request->get('items')
);
// 2. Delegation an UseCase
$responseDto = $useCase->execute($dto);
return new JsonResponse(['orderId' => $responseDto->orderId], 201);
}
public function show(string $id, GetOrderQuery $query): JsonResponse
{
// Grob-autorisierung erfolgt via Symfony Attribute/Voter
// Feinkörnige Prüfung wird innerhalb der Query oder über Voter -> Logic Layer gelöst.
$responseDto = $query->execute(new GetOrderRequest($id));
return new JsonResponse($responseDto->toArray());
}
}
```
## 9. Exception Hierarchie & Error Handling (`src/Logic` $\rightarrow$ `src/UI`)
Um eine konsistente Fehlerbehandlung über alle Layer hinweg zu gewährleisten, wird eine strukturierte Exception-Hierarchie eingesetzt. Dies erlaubt es der UI-Layer, Exceptions gruppiert und damit automatisiert in HTTP-Statuscodes zu übersetzen, ohne jede einzelne Exception explizit kennen zu müssen.
### 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.
#### 2. Kategorien & HTTP-Mapping
Folgende Untergruppen definieren das Verhalten im UI-Layer:
| Exception Gruppe | HTTP Status | Zweck | Beispiel |
| :--- | :--- | :--- | :--- |
| `ResourceNotFoundException` | **404 Not Found** | Ressource existiert nicht. | `UserNotFoundException` |
| `BusinessRuleViolationException` | **422 Unprocessable Entity** | Geschäftsregel verletzt. | `InsufficientFundsException` |
| `AccessDeniedException` | **403 Forbidden** | Fehlende Berechtigung. | `PermissionDeniedException` |
| `InfrastructureException` | **503 Service Unavailable** | Technische Fehler (extern). | `ExternalApiTimeoutException` |
### Umsetzung im UI-Layer (Centralized Handling)
Die Übersetzung erfolgt in einem zentralen Exception-Subscriber oder Listener der UI-Layer. Anstatt `try-catch`-Blöcke in jedem Controller zu nutzen, fängt dieser Subscriber die Exceptions global ab:
```php
// Konzeptueller Logik-Auszug des Subscribers
if ($exception instanceof ResourceNotFoundException) {
return new JsonResponse(['error' => $exception->getMessage()], 404);
}
if ($exception instanceof BusinessRuleViolationException) {
return new JsonResponse(['error' => $exception->getMessage()], 422);
}
// ... etc.
```
### Vorteile dieses Ansatzes
- **Entkopplung**: Die Logic Layer definiert nur *was* passiert ist (z.B. "Order nicht gefunden"). Die UI Layer entscheidet, *wie* dies dem User präsentiert wird (HTTP 404).
- **Wartbarkeit**: Neue Exceptions in der Logic Layer müssen lediglich einer bestehenden Kategorie zugeordnet werden und funktionieren sofort im Frontend/API ohne Anpassungen am UI-Code.
- **Konsistenz**: Alle Endpunkte reagieren bei gleichen Fehlerarten identisch.
## 10. Event Handling & Messaging (`src/Logic` $\rightarrow$ `src/Data`)
Das System nutzt eine differenzierte Strategie für Ereignisse, um Entkopplung zu gewährleisten und gleichzeitig die Last auf die APIs gering zu halten.
### Typen von Events
Je nach Reichweite und Zeitkritikalität kommen verschiedene Mechanismen zum Einsatz:
#### 1. Internal Events (Synchron)
- **Zweck**: Sofortige Nebenwirkungen innerhalb desselben Request-Zyklus.
- **Interface**: `InternalEventDispatcher`
- **Payload**: Übergabe von Business-Models ist zulässig, da der Zustand konsistent bleibt.
- **Beispiel**: Aktualisierung eines lokalen In-Memory Caches nach einer Änderung.
#### 2. Async Events (Lokal Asynchron)
- **Zweck**: Zeitintensive Aufgaben, die nicht den HTTP-Response verzögern dürfen.
- **Interface**: `AsyncEventPublisher`
- **Payload**: Nutzung von dedizierten **Event-DTOs**. Es wird das Prinzip des *Event-Carried State Transfer* angewandt (nur notwendige Daten senden), um unnötige Callbacks zum Quellsystem zu vermeiden.
- **Beispiel**: Versenden einer Bestätigungsmail nach Bestellung.
#### 3. Broadcast Events (Extern/Service-übergreifend)
- **Zweck**: Information anderer Microservices über eine Zustandsänderung.
- **Interface**: `BroadcastEventPublisher`
- **Payload**: Kompakte, fachspezifische Event-DTOs (*Event-Carried State Transfer*).
- **Besonderheit**: Die Definitionen dieser Broadcast-Events (DTOs/Schemas) werden in einem **separaten externen Repository** gepflegt. Dies ermöglicht es anderen Services, die Messages versioniert zu importieren, ohne Abhängigkeiten zum Haupt-Repository zu haben.
- **Beispiel**: Benachrichtigung des Logistik-Systems über eine neue Bestellung.
### Transaktions-Sicherheit (Transactional Consistency)
Um "Geister-Events" durch Datenbank-Rollbacks zu vermeiden, gilt folgende Regel:
**Async- und Broadcast-Events dürfen erst gefeuert werden, nachdem der `TransactionManager` den erfolgreichen Commit bestätigt hat.** Ein Fehlschlag in der DB bedeutet, dass kein Event versendet wird.
### Zusammenfassung Payload-Strategie
| Event Typ | Timing | Payload | Transport |
| :--- | :--- | :--- | :--- |
| **Internal** | Synchron | Models / Objekte | In-Process (EventDispatcher) |
| **Async** | Asynchron | Event-DTOs $\rightarrow$ State Transfer | Message Bus (Local) |
| **Broadcast** | Asynchron | Event-DTOs $\rightarrow$ State Transfer | Message Broker (External) |
## 11. Concurrency & Locking (`src/Logic` $\rightarrow$ `src/Data`)
Zur Vermeidung von Race Conditions bei gleichzeitigen Zugriffen auf dieselben Daten wird standardmäßig das **Optimistic Locking** eingesetzt.
### Funktionsweise des Optimistic Locking
Anstatt Ressourcen exklusiv zu sperren, vertraut das System darauf, dass Konflikte selten auftreten, und prüft die Versionierung beim Schreiben.
1. **Versionierung**: Jede persistierte Einheit im Data Layer besitzt eine Versionsnummer (INT). Diese wird in das entsprechende Business Model der Logic Layer übernommen.
2. **Schreibvorgang**: Beim Speichern eines Modells über einen Processor muss die ursprüngliche Versionsnummer mitgegeben werden. Der Data Layer führt das Update nur aus, wenn die Version in der Datenbank noch mit der des Modells übereinstimmt (`WHERE id = :id AND version = :old_version`).
3. **Konflikterkennung**: Schlägt das Update fehl (0 Zeilen aktualisiert), wirft der Processor eine fachliche `ConcurrencyException` (erbt von `BusinessRuleViolationException`).
### UI-Reaktion
Die UI Layer fängt die `ConcurrencyException` im zentralen Exception-Subscriber ab und übersetzt sie in eine benutzerfreundliche Antwort:
- **HTTP Status**: `409 Conflict`.
- **Botschaft**: Der Benutzer wird informiert, dass der Datensatz in der Zwischenzeit von einem anderen Prozess geändert wurde und ein Refresh/Neu-Laden erforderlich ist.
## 12. Konfiguration & Feature Flags (`src/Logic` $\leftarrow$ `src/Data`)
Um die Logic Layer unabhängig von Symfony's Parameter-System zu halten und gleichzeitig pragmatisch zu bleiben, wird ein hybrider Ansatz verfolgt.
### Strategie: Pragmatisches Autowiring
Es wird das Standard-Autowiring von Symfony genutzt, um Konfigurationsaufwand zu minimieren und die Entwicklungsgeschwindigkeit zu erhöhen.
#### 1. Automatische Auflösung (Standard)
Wenn ein Interface (z.B. in `src/Logic`) genau eine korrespondierende Implementierung (z.B. in `src/Data`) in der Anwendung besitzt, erfolgt das Wiring vollständig automatisch durch Symfony. Es ist **keine** manuelle Konfiguration in der `services.yaml` erforderlich.
#### 2. Explizite Bindings (Ausnahme)
Manuelle Einträge in der `services.yaml` sind nur in folgenden Fällen zwingend:
- **Mehrfache Implementierungen**: Wenn ein Interface mehrere Implementierungen besitzt und explizit gesteuert werden muss, welche Version aktuell aktiv ist (z.B. `S3Storage` vs. `LocalStorage`).
- **Externe Bibliotheken**: Wenn Abhängigkeiten innerhalb von Third-Party-Paketen manuell konfiguriert werden müssen.
### Konfigurations-Beispiel (`services.yaml`)
```yaml
services:
_defaults:
autowire: true
autoconfigure: true
# Nur nötig, wenn mehr als eine Implementierung existiert
App\Logic\Common\StorageInterface: '@App\Data\Infrastructure\S3Storage'
```
### Hybrid-Strategie für Einstellungen
Je nach Komplexität der benötigten Konfiguration werden zwei Wege genutzt:
#### 1. Direktes Wiring (Einfache Fälle)
Bei einer geringen Anzahl von Parametern (ca. 1-3 Werte) erfolgt die Injektion direkt über den Konstruktor mittels Symfony's Standard-Autowiring/Wiring.
- **Vorteil**: Minimaler Overhead, kein zusätzlicher Boilerplate-Code.
#### 2. Settings-Objekte (Komplexe Fälle / Gruppen)
Sobald eine thematische Gruppe von Einstellungen existiert oder die Anzahl der Parameter steigt, werden diese in einem **Settings-Objekt** gebündelt. Dies ist eine einfache `readonly` Klasse ohne eigene Logik (`POPO`).
- **Zweck**: Vermeidung von "Constructor Bloat" und bessere Gruppierung verwandter Werte.
- **Vorteil**: Einfachere Testbarkeit und saubere Übergabe an Unterdienste.
**Beispiel für ein Settings-Objekt:**
```php
readonly class OrderSettings
{
public function __construct(
public int $maxItemsPerOrder,
public float $freeShippingThreshold,
public bool $isInternationalShippingEnabled
) {}
}
```
## 13. Testing Strategy & Structure ## 13. Testing Strategy & Structure
Die Testsuite ist so aufgebaut, dass sie die Schichtenmodell-Architektur widerspiegelt. Dies erleichtert die Wartung und stellt sicher, dass jede Komponente auf der richtigen Abstraktionsebene geprüft wird. Die Testsuite ist so aufgebaut, dass sie die Schichtenmodell-Architektur widerspiegelt. Dies erleichtert die Wartung und stellt sicher, dass jede Komponente auf der richtigen Abstraktionsebene geprüft wird.
### Verzeichnisstruktur & Mapping ### Verzeichnisstruktur & Mapping
@@ -648,8 +622,6 @@ Die Struktur unter `tests/` folgt strikt der Symmetrie zu `src/`:
| **Functional UI**| `tests/Functional/UI/` | `src/UI/` | Symfony `WebTestCase` | Einzelspezifische Endpunkte / Requests. | | **Functional UI**| `tests/Functional/UI/` | `src/UI/` | Symfony `WebTestCase` | Einzelspezifische Endpunkte / Requests. |
| **Functional E2E**| `tests/Functional/Scenarios/`| (Szenario-basiert) | Symfony `WebTestCase` | Komplexe User Flows (Multi-Step). | | **Functional E2E**| `tests/Functional/Scenarios/`| (Szenario-basiert) | Symfony `WebTestCase` | Komplexe User Flows (Multi-Step). |
---
### Detaillierte Guidelines ### Detaillierte Guidelines
#### 1. Unit Tests (`tests/Unit/Logic`) #### 1. Unit Tests (`tests/Unit/Logic`)
@@ -673,8 +645,3 @@ Um "fragile Tests" zu vermeiden, gilt:
- **Mock a Interface, not a Class**: Mocke immer das Interface (z.B. `OrderProviderInterface`), niemals die konkrete Implementierung (`OrderProvider`). - **Mock a Interface, not a Class**: Mocke immer das Interface (z.B. `OrderProviderInterface`), niemals die konkrete Implementierung (`OrderProvider`).
- **No Mocks for Models/DTOs**: Value Objects und Business Models werden immer echt instanziiert. - **No Mocks for Models/DTOs**: Value Objects und Business Models werden immer echt instanziiert.
- **Avoid Mocking 3rd Party Libs**: Wenn externe Libs getestet werden müssen, schreibe einen eigenen Wrapper/Interface in die Logic Layer und mocke diesen. - **Avoid Mocking 3rd Party Libs**: Wenn externe Libs getestet werden müssen, schreibe einen eigenen Wrapper/Interface in die Logic Layer und mocke diesen.
}
```