diff --git a/architektur-patterns.md b/architektur-patterns.md index b39d174..9b13bbc 100644 --- a/architektur-patterns.md +++ b/architektur-patterns.md @@ -1,6 +1,118 @@ # Architektur Patterns -Diese Datei definiert die konkreten Implementierungsmuster für die verschiedenen Layer der Anwendung. +``` + +## 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`) @@ -202,54 +314,329 @@ readonly class OrderProvider } ## 3. Write Pattern (`src/Logic` $\rightarrow$ `src/Data`) - -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** - -#### Processor (Data Layer) -- **Zweck**: Übernimmt die physische Persistierung eines Business-Models. -- **Schnittstelle**: Das `ProcessorInterface` liegt zwingend in der **Logic Layer** (Dependency Inversion). -- **Input**: Ein Business-Model. -- **Rückgabewert**: Das aktualisierte/neu erstellte Business-Model (z.B. zur Rückgabe von generierten IDs). -- **Fehlerbehandlung**: Wirft im Fehlerfall direkt **Domain-Exceptions**. -- **Strategie**: Verfolgt den "All or Nothing" Ansatz; die übergeordnete Transaktion wird vom UseCase gesteuert. -- **Granularität**: Fokus auf eine einzelne Entität pro Processor. - -### Code Beispiel - -#### Processor Interface & Implementierung +... [existing content of Write Pattern] ... ```php -namespace App\Logic\Sales\Order; - -use App\Logic\Sales\Order\Model\Order; - -interface OrderProcessorInterface -{ - public function save(Order $order): Order; - public function delete(string $id): void; -} - -// --- Data Layer --- -namespace App\Data\Sales\Order\Processor; - -use App\Logic\Sales\Order\OrderProcessorInterface; -use App\Logic\Sales\Order\Model\Order; - -readonly class OrderProcessor implements OrderProcessorInterface -{ - public function save(Order $order): Order - { - // Persistenz-Logik (DB/API) ... - return $order; // Hier ggf. mit generierter ID zurückgeben - } - public function delete(string $id): void { // Lösch-Logik ... } } ``` + +## 4. Transaction Management (`src/Logic` $\rightarrow$ `src/Data`) + +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. + +### Konzept +Die Steuerung der Transaktion erfolgt über ein Interface in der Logic Layer, während die technische Umsetzung im Data Layer (Infrastructure) liegt. + +#### 1. TransactionManagerInterface (`src/Logic/Common`) +Dieses Interface definiert eine Methode, die einen Closures-Block innerhalb einer Transaktion ausführt. + +```php +namespace App\Logic\Common; + +interface TransactionManagerInterface +{ + /** + * Führt die übergebene Operation atomar aus. + * + * @template T + * @param callable(mixed...): T $operation + * @return T + * @throws \Throwable + */ + public function transactional(callable $operation): mixed; +} ``` + +#### 2. Anwendung im UseCase (`src/Logic`) +Der UseCase nutzt den TransactionManager, um sicherzustellen, dass entweder alle oder keine Änderungen persistiert werden (All-or-Nothing). + +```php +readonly class CreateOrderUseCase implements UseCaseInterface +{ + public function __construct( + private TransactionManagerInterface $transactionManager, + private OrderProcessorInterface $orderProcessor, + private StockProcessorInterface $stockProcessor + ) {} + + public function execute(mixed $request): CreateOrderResponse + { + return $this->transactionManager->transactional(function() use ($request) { + // 1. Produktbestand reduzieren + $this->stockProcessor->reduceStock($request->items); + + // 2. Bestellung anlegen + $order = $this->orderProcessor->save($request->toModel()); + + return new CreateOrderResponse(orderId: $order->id); + }); + } +} ``` + +#### 3. Implementierung im Data Layer (`src/Data`) +Die implementierende Klasse nutzt die technischen Mittel des Frameworks (z.B. Doctrine), um die Transaktion zu steuern. + +```php +namespace App\Data\Common; + +use App\Logic\Common\TransactionManagerInterface; +use Doctrine\ORM\EntityManagerInterface; + +readonly class DoctrineTransactionManager implements TransactionManagerInterface +{ + public function __construct(private EntityManagerInterface $entityManager) {} + + public function transactional(callable $operation): mixed + { + return $this->entityManager->wrapInTransaction($operation); + } +} +``` + +### Vorteile +- **Kein Leaking**: Die Logic Layer weiß nicht, dass Doctrine verwendet wird. +- **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. + + +## 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. + +### Warum diese Trennung? +1. **Framework-Unabhängigkeit**: Die Logic Layer bleibt ein "Pure PHP" Bereich. Änderungen am ORM oder ein Wechsel der Datenbanktechnologie haben keinen Einfluss auf die Business Rules. +2. **Vermeidung von Side-Effects**: ORMs wie Doctrine nutzen Lazy-Loading Proxies. Wenn diese Objekte in la Logic Layer gelangen, können unerwartete DB-Abfragen (N+1 Problem) an Stellen auftreten, an denen keine Infrastruktur-Logik sein sollte. +3. **Anämische vs. Reichhaltige Modelle**: Persistenz-Entities sind oft "anämisch" (nur Getter/Setter), um die DB-Struktur abzubilden. Business Models hingegen anwenden Rich Domain Logic. + +### Definitionen + +#### Persistence Entity (`src/Data`) +- **Zweck**: Spiegelung des Datenbank-Schemas. +- **Eigenschaften**: Enthält Framework-Annotationen/Attribute (z.B. `#[ORM\Entity]`). +- **Regel**: Darf niemals die Grenze zur Logic Layer überschreiten. + +#### Business Model (`src/Logic`) +- **Zweck**: Repräsentation des fachlichen Objekts innerhalb der Geschäftslogik. +- **Eigenschaften**: POPO (Plain Old PHP Object), readonly wo möglich, enthält Business-Validierung und Logik. +- **Logik-Verteilung**: + - **Intrinsic Logic** (im Model): Beantwortet Fragen über den eigenen Zustand, die keine externen Abhängigkeiten benötigen (z.B. `getFullName()`, `getAge()`, `isExpired()`). + - **Extrinsic Logic** (im UseCase): Steuert Prozesse, Koordination und Logik mit externen Abhängigkeiten (z.B. Prüfung von Beständen via Processor). +- **Regel**: Kennt keine Details über die Persistenz. + +### Übergabe & Mapping +Der Austausch erfolgt ausschließlich über ein Mapping in der Data Layer: +- **Provider (Read)**: `Persistence Entity` $\rightarrow$ `Business Model` +- **Processor (Write)**: `Business Model` $\rightarrow$ `Persistence Entity` + +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`) + +Um Redundanz zu vermeiden und eine konsistente Transformation zwischen Persistenz- und Domänenebene zu gewährleisten, werden dedizierte Mapper-Klassen eingesetzt. + +### Verantwortung & Ort +- **Ort**: Mappers befinden sich in `src/Data/{Module}/{Feature}/Mapper`. +- **Zweck**: Sie kapseln die Logik der Konvertierung. Dadurch bleiben Provider und Processor schlank und konzentrieren sich nur auf den Datenzugriff bzw. die Persistenz. +- **Status**: Mapper sind strikt zustandslos (stateless). + +### Mapping Richtungen +Ein Mapper implementiert typischerweise zwei Methoden: +1. `toModel(Entity $entity): Model`: Konvertierung von der DB zur Logik (genutzt in Providern). +2. `toEntity(Model $model, ?Entity $entity = null): Entity`: Konvertierung von der Logik zur DB. Das optionale `$entity` Objekt ermöglicht Updates bestehender Datensätze ohne Neuerstellung. + +### Code Beispiel + +#### Mapper Implementierung +```php +namespace App\Data\Sales\Order\Mapper; + +use App\Data\Sales\Order\Entity\OrderEntity; +use App\Logic\Sales\Order\Model\Order; + +readonly class OrderMapper +{ + public function toModel(OrderEntity $entity): Order + { + return new Order( + id: $entity->getId(), + customerEmail: $entity->getEmail(), + totalAmount: $entity->getAmount(), + // ... weitere Felder + ); + } + + public function toEntity(Order $model, ?OrderEntity $entity = null): OrderEntity + { + $entity ??= new OrderEntity(); + + $entity->setEmail($model->customerEmail); + $entity->setAmount($model->totalAmount); + // ... weitere Felder + + return $entity; + } +} +``` + +#### Integration in Provider/Processor +- **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. + +## 6. 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. + +### Architektur & Datenfluss +Der Fluss folgt dem Read- und Write-Pattern: +`Logic Layer` $\rightarrow$ `Provider / Processor` $\rightarrow$ `External Client` $\rightarrow$ `API/Drittsystem` + +#### 1. External Clients (Infrastruktur) +- **Zweck**: Technische Umsetzung der Kommunikation (z.B. via Symfony HttpClient). +- **Verantwortung**: Header, Authentication, Request-Body-Formatierung und reine HTTP-Antworten. +- **Ort**: `src/Data/{Module}/{Feature}/Client`. + +#### 2. Integration in Provider/Processor +Die Provider/Processor fungieren als Adapter zwischen dem Client und der Domäne: +- **Read (Provider)**: Ruft den Client auf, empfängt die Rohantwort (z.B. JSON) und nutzt einen Mapper, um diese in ein **Business Model** zu transformieren. +- **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 +Technische Fehler dürfen nicht nicht ungefiltert in die Logic Layer gelangen. Der Provider/Processor muss folgende Übersetzung vornehmen: +- **HTTP 404** $\rightarrow$ `ResourceNotFoundException` (Domain) +- **HTTP 401/403** $\rightarrow$ `ExternalSystemAccessException` (Domain) +- **HTTP 500 / Timeout** $\rightarrow$ `ExternalSystemUnavailableException` (Domain) + +### Code Beispiel (Konzept) + +```php +namespace App\Data\Sales\Order\Provider; + +use App\Data\Sales\Order\Client\ShippingApiClient; +use App\Data\Sales\Order\Mapper\ShippingApiMapper; +use App\Logic\Sales\Order\Model\ShippingStatus; +use App\Logic\Common\Exception\ExternalSystemUnavailableException; + +readonly class ShippingProvider implements ShippingProviderInterface +{ + public function __construct( + private ShippingApiClient $client, + private ShippingApiMapper $mapper + ) {} + + public function getStatus(string $trackingId): ShippingStatus + { + try { + $response = $this->client->fetchStatus($trackingId); + return $this->mapper->toModel($response); + } catch (TransportException $e) { + throw new ExternalSystemUnavailableException('Shipping API is down', 0, $e); + } + } +} +``` + + diff --git a/architektur.md b/architektur.md index e125653..7861d5e 100644 --- a/architektur.md +++ b/architektur.md @@ -8,6 +8,7 @@ Die Anwendung ist in drei strikt getrennte Layer unterteilt: ### UI layer (`src/UI`) - **Verantwortung**: Präsentation, Handling von User-Input, API-Endpunkte, CLI-Commands. +- **Struktur**: Folgt einer domänenorientierten Struktur `src/UI/{Module}/{Interface}/{Feature}/...` (z.B. `src/UI/Sales/Http/OrderController.php`), um maximale Symmetrie zu den anderen Layern zu gewährleisten. - **Regel**: Darf ausschließlich die Logic-Layer aufrufen. Ein direkter Zugriff auf die Data-Layer ist untersagt. ### Logic Layer (`src/Logic`) @@ -21,10 +22,12 @@ Die Anwendung ist in drei strikt getrennte Layer unterteilt: ### Data Layer (`src/Data`) - **Verantwortung**: Persistenz, Datenabruf und Kommunikation mit externen Systemen. +- **Struktur**: Spiegelt exakt die Verzeichnisstruktur der Logic Layer wider (`src/Data/{Module}/{Feature}/...`), um eine konsistente Auffindbarkeit zwischen Domänenlogik und technischer Implementierung zu gewährleisten. - **Komponenten**: - **Repositories**: Zugriff auf Datenbankentitäten. - - **Entities**: Domain-Modelle für die Persistenz. + - **Entities**: Domain-Modelle für die Persistenz (spiegeln DB-Schema). - **Provider / Processor Interfaces**: Definieren den Datenaustausch mit der Logic-Layer. + - **Mapper**: Transformation zwischen Entity und Business Model. - **Regel**: Kennt keine Geschäftslogik und ist nur für die Bereitstellung/Speicherung von Daten zuständig. ## 2. Datenfluss & Kommunikation