diff --git a/architektur-patterns.md b/architektur-patterns.md index fb92fc4..08ba6df 100644 --- a/architektur-patterns.md +++ b/architektur-patterns.md @@ -151,7 +151,7 @@ readonly class GetOrdersQuery implements UseCaseInterface } ``` -#### Manager & Provider +#### Manager (Read + Cache) ```php namespace App\Logic\Sales\Order\Manager; @@ -159,24 +159,46 @@ use App\Data\Sales\Order\Provider\OrderProvider; readonly class OrderManager { - public function __construct(private OrderProvider $orderProvider) {} + public function __construct( + private OrderProvider $orderProvider, + // private CacheInterface $cache, <-- Symfony/PSR-6 Cache (injected via DI) + ) {} + /** + * Liefert Orders mit Cache-Aside Pattern. + */ public function findOrders(array $filters, int $page): array { - // Cache-Logik hier... - return $this->orderProvider->fetchOrders($filters, $page); + // cacheKey aus filters + page generieren ... + // if ($cache->has($key)) return $cache->get($key); + + $orders = $this->orderProvider->fetchOrders($filters, $page); + // $cache->set($key, $orders); + return $orders; + } + + /** + * Invalidiert den Cache für die Order-Domain. + */ + public function invalidateCache(): void + { + // Tags/Keys löschen, die durch den Write betroffen sind ... } } +``` -// --- Data Layer --- +#### Provider (Data Layer) +```php namespace App\Data\Sales\Order\Provider; -readonly class OrderProvider +use App\Logic\Sales\Order\Manager\OrderManagerInterface; + +readonly class OrderProvider implements OrderManagerInterface { public function fetchOrders(array $filters, int $page): array { // DB Abfrage und Mapping zu Business-Models - return []; + return []; } } ``` @@ -186,12 +208,11 @@ readonly class OrderProvider Das Write Pattern definiert den Weg von einer Zustandsänderung hin zur Persistenz. Es stellt sicher, dass Schreiboperationen atomar erfolgen und die Integrität der Business-Models gewahrt bleibt. ### Der Datenfluss -`UI Layer` $\rightarrow$ **UseCase** $\rightarrow$ **Manager** (optional) $\rightarrow$ **Processor** +`UI Layer` $\rightarrow$ **UseCase** $\rightarrow$ **Manager** $\rightarrow$ **Processor** -#### Manager (Logic Layer - optional) -- **Zweck**: Koordinationsschicht für Schreiboperationen. -- **Verantwortung**: Primär zuständig für die Orchestrierung von Nebenwirkungen, die außerhalb der Kern-Transaktion liegen oder die Datenkonsistenz über verschiedene Ebenen hinweg sicherstellen müssen (z. B. Triggerung der **Cache-Invalidierung** in der Read-Layer nach erfolgreichem Write). -- **Schnittstelle**: Besitzt wie im Read Pattern ein eigenes Interface zur Gewährleistung von Testbarkeit. +#### 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. #### Processor (Data Layer) - **Zweck**: Übernimmt die physische Persistierung eines Business-Models. @@ -237,7 +258,128 @@ readonly class OrderProcessor implements OrderProcessorInterface } ``` -## 4. Transaction Management (`src/Logic` $\rightarrow$ `src/Data`) +#### UseCase und Write-Manager Integration +```php +namespace App\Logic\Sales\Order\UseCase; + +use App\Logic\Common\UseCaseInterface; +use App\Logic\Sales\Order\Dto\CreateOrderRequest; +use App\Logic\Sales\Order\Dto\CreateOrderResponse; +use App\Logic\Sales\Order\Manager\OrderManager; + +readonly class CreateOrderUseCase implements UseCaseInterface +{ + public function __construct( + private OrderManager $orderManager, + // TransactionManager ggf. für Multi-Processor-Szenarien + ) {} + + public function execute(mixed $request): CreateOrderResponse + { + if (!$request instanceof CreateOrderRequest) { + throw new \InvalidArgumentException('Invalid request type'); + } + + // Business Logik / Validierung ... + + // Delegiere an den Manager: Der persistiert UND invalidiert den Read-Cache + $order = $this->orderManager->createOrder($request); + + return new CreateOrderResponse(orderId: $order->id); + } +} +``` + +Die Manager-Methode `createOrder` orchestriert intern: +1. Mapping Request-DTO $\rightarrow$ Business Model +2. Aufruf des Processors innerhalb einer Transaktion +3. Cache-Invalidierung via `invalidateCache()`, sodass der nächste Read frische Daten liefert. + +## 4. Validierungsstrategie (Mehrschichtig) + +Validierung findet so früh wie möglich statt. Jede Layer prüft, was sie prüfen kann — von syntaktischen Format-Checks im Frontend über DTO-Attribute im UI-Layer bis hin zu komplexen Business-Regeln in der Logic Layer. Ein schlecht formatierter Input schlägt bereits im Controller fehl, wohingegen eine komplexe fachliche Regel erst in der UseCase- oder Model-Schicht geprüft wird. + +### Verantwortung pro Layer + +| Layer | Was wird geprüft? | Beispiel | +| :--- | :--- | :--- | +| **Frontend** | Sofortiges Format-Feedback | E-Mail-Format, Positive Nummern | +| **UI (Controller + DTO)** | Syntaktische Prüfung via Symfony Validator | Pflichtfelder, Datentypen, Längen | +| **Logic (UseCase / Model)** | Business-Regeln, Cross-Entity Constraints, Zustandsgültigkeit | Host auflösbar? Guthaben reicht? Statusübergang erlaubt? | +| **Data (Processor)** | Externe Constraints, DB-Integrität | Unique-Konflikt, API Rate Limits | + +### Syntaktische Validierung im Controller + +Die UI Layer führt syntaktische Checks mit reinem PHP durch – ohne Framework-Validator oder Annotations auf den DTOs. Request-DTOs in der Logic Layer bleiben unverändert und frei von jeglichen Framework-Abhängigkeiten. + +```php +namespace App\Logic\Sales\Order\Dto; + +readonly class CreateOrderRequest +{ + public function __construct( + public string $customerEmail, + public string $customerName, + public array $itemIds, + ) {} +} +``` + +### Controller-Integration + +```php +namespace App\UI\Http\Sales\Order; + +use Symfony\Component\Validator\Validator\ValidatorInterface; + +readonly class OrderController extends AbstractController +{ + public function create( + Request $request, + ValidatorInterface $validator, + CreateOrderUseCase $useCase + ): JsonResponse { + // DTO aus Request bauen ... + $dto = /* mapping */ ; + + // Syntaktische Validierung (frühes Fail) + $violations = $validator->validate($dto); + if (count($violations) > 0) { + return new JsonResponse(['errors' => (string)$violations], 422); + } + + // UseCase übernimmt Business-Validierung + $responseDto = $useCase->execute($dto); + return new JsonResponse(['orderId' => $responseDto->orderId], 201); + } +} +``` + +### Business Models ohne Symfony-Attributes + +Business Models in `src/Logic` sind frei von Framework-Abhängigkeiten. Ihre Invarianten werden im Konstruktor mit reinem PHP durchgesetzt: + +```php +class Order extends BaseModel +{ + public function __construct(public readonly string $email, public readonly float $totalAmount) + { + if ($this->totalAmount < 0) { + throw new BusinessRuleViolationException('Order amount must be positive'); + } + } + + public function canShip(): bool + { + return $this->status === OrderStatus::PAID && $this->isAddressComplete(); + } +} +``` + +### Vorteile +- **Frühes Erkennen**: Formatfehler werden im Controller erkannt, sodass keine UseCase- oder Data-Layer-Logik ausgelastet wird. +- **Framework-Unabhängigkeit**: Die Logic Layer bleibt frei von Symfony-Attributen und ist unabhängig testbar. +- **Schichtenweise Abdeckung**: Jede Schicht validiert ihren Bereich — das Frontend schützt den Controller, der Controller schützt den UseCase, der UseCase schützt die Persistenz. Da die Logic Layer framework-unabhängig bleibt, darf sie keinen direkten Zugriff auf den Doctrine `EntityManager` haben. Um dennoch atomare Schreiboperationen über mehrere Processor hinweg zu gewährleisten, wird das **Transaction Manager Pattern** eingesetzt. @@ -316,7 +458,7 @@ readonly class DoctrineTransactionManager implements TransactionManagerInterface - **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. -## 5. Entity vs. Business Model (`src/Data` $\rightarrow$ `src/Logic`) +## 6. 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. @@ -347,7 +489,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. -## 6. Mapping Pattern (`src/Data`) +## 7. 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. @@ -399,7 +541,7 @@ readonly class OrderMapper - **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. -## 7. Externe Systemintegration (`src/Data`) +## 8. 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. @@ -451,7 +593,7 @@ readonly class ShippingProvider implements ShippingProviderInterface } ``` -## 8. UI Controller Pattern (`src/UI`) +## 9. 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. @@ -510,7 +652,7 @@ readonly class OrderController extends AbstractController } ``` -## 9. Exception Hierarchie & Error Handling (`src/Logic` $\rightarrow$ `src/UI`) +## 10. 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. diff --git a/architektur.md b/architektur.md index ad59ad3..35c2fb7 100644 --- a/architektur.md +++ b/architektur.md @@ -18,6 +18,7 @@ Die Anwendung ist in drei strikt getrennte Layer unterteilt: - **BusinessQueries**: Spezifische Abfragen für geschäftsrelevante Daten. - **Manager**: Orchestratoren, die z.B. Caching-Strategien implementieren und zwischen verschiedenen Providern/Processoren vermitteln. - **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. ### Data Layer (`src/Data`) @@ -26,7 +27,7 @@ Die Anwendung ist in drei strikt getrennte Layer unterteilt: - **Komponenten**: - **Repositories**: Zugriff auf Datenbankentitäten. - **Entities**: Domain-Modelle für die Persistenz (spiegeln DB-Schema). - - **Provider / Processor Interfaces**: Definieren den Datenaustausch mit der Logic Layer. + - **Provider / Processor-Implementierungen**: Stellt Daten bereit bzw. persistiert sie (DB, API). Implementieren dabei die aus der Logic Layer importierten Interfaces (Dependency Inversion). - **Mapper**: Transformation zwischen Entity und Business Model. - **Regel**: Kennt keine Geschäftslogik und ist nur für die Bereitstellung/Speicherung von Daten zuständig. @@ -119,6 +120,8 @@ Weitere Details zur technischen Umsetzung finden sich in der [architektur-patter | :--- | :--- | :--- | | UseCase / Workflow | `src/Logic` | Einzelschrittliche Geschäftsoperation. | | BusinessQuery | `src/Logic` | Funktionale Abfrage von Geschäftsdaten. | -| Manager | `src/Logic` | Cache-Steuerung und Koordination von Providern. | -| Provider | `src/Data` | Bereitstellung von Daten (Read). | -| Processor | `src/Data` | Verarbeitung/Persistierung von Daten (Write). | +| Manager | `src/Logic` | Cache-Steuerung und Koordination von Providern/Processoren. | +| Provider Interface | `src/Logic` | Vertragsdefinition für Datenbereitstellung (Read). | +| Provider Implementation | `src/Data` | konkrete Bereitstellung von Daten (DB, API). | +| Processor Interface | `src/Logic` | Vertragsdefinition für Datenpersistierung (Write). | +| Processor Implementation | `src/Data` | Konkrete Verarbeitung/Persistierung von Daten. |