Refactor: resolve validation contradiction, add transactional event bridge, remove duplicate TransactionManager block, fix section numbering 1-13
This commit is contained in:
+253
-166
@@ -5,10 +5,10 @@
|
|||||||
Das UseCase Pattern bildet das Herzstück der Business-Logik. Zusammen mit dem Read Pattern (Sektion 2) folgt die Architektur einem **Light-CQRS Ansatz** (Command Query Responsibility Segregation), bei dem Schreiboperationen (Commands/UseCases) strikt von Leseoperationen (Queries) getrennt werden, um Komplexität zu reduzieren und die Performance zu optimieren. 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. Zusammen mit dem Read Pattern (Sektion 2) folgt die Architektur einem **Light-CQRS Ansatz** (Command Query Responsibility Segregation), bei dem Schreiboperationen (Commands/UseCases) strikt von Leseoperationen (Queries) getrennt werden, um Komplexität zu reduzieren und die Performance zu optimieren. Jeder Anwendungsfall wird als eigenständige Klasse implementiert, um eine klare Trennung der Verantwortlichkeiten und eine hohe Testbarkeit zu gewährleisten.
|
||||||
|
|
||||||
### Struktur & Definition
|
### Struktur & Definition
|
||||||
- **Interface**: Alle UseCases müssen das `UseCaseInterface` implementieren.
|
- **Kein Interface**: UseCases benötigen kein separates `UseCaseInterface`. Der Vertrag wird ausschließlich durch die Native PHP Typisierung der Signatur und den Symfony Dependency Injection Container gestellt.
|
||||||
- **Methode**: Die primäre Logik liegt in der Methode `execute()`.
|
- **Methode**: Die primäre Logik liegt in der Methode `execute()`.
|
||||||
- **Eingabe/Ausgabe**: Daten werden strikt über DTOs (Data Transfer Objects) ausgetauscht.
|
- **Eingabe/Ausgabe**: Daten werden strikt über DTOs (Data Transfer Objects) ausgetauscht.
|
||||||
- **Rückgabewert**: Ein UseCase gibt entweder ein Response-DTO zurück oder `void` (bei reinen Schreiboperationen).
|
- **Rückgabewert**: Ein UseCase gibt entweder ein Response-DTO zurück oder `void` (bei reinen Schreiboperationen). Die PHP-Signatur ist native typisiert, sodass zur Laufzeit keine `instanceof`-Prüfungen nötig sind.
|
||||||
- **Fehlerbehandlung**: Business-Fehler werden über spezifische **Domain-Exceptions** signalisiert.
|
- **Fehlerbehandlung**: Business-Fehler werden über spezifische **Domain-Exceptions** signalisiert.
|
||||||
|
|
||||||
### DTOs (Data Transfer Objects)
|
### DTOs (Data Transfer Objects)
|
||||||
@@ -25,55 +25,142 @@ Das UseCase Pattern bildet das Herzstück der Business-Logik. Zusammen mit dem R
|
|||||||
- `src/Logic/Sales/Order/Dto/CreateOrderRequest.php`
|
- `src/Logic/Sales/Order/Dto/CreateOrderRequest.php`
|
||||||
- `src/Logic/Sales/Order/Dto/CreateOrderResponse.php`
|
- `src/Logic/Sales/Order/Dto/CreateOrderResponse.php`
|
||||||
|
|
||||||
### Code Beispiel
|
### Input-Validierungsstrategie (Zweistufig)
|
||||||
|
DTOs im Logic-Layer sind **reine Datenträger** und enthalten keinerlei Framework-Abhängigkeiten. Der Einbau von Symfony Validator Constraints würde das Logic-Layer an ein konkretes Webframework koppeln.
|
||||||
|
|
||||||
#### Interface
|
Daher gilt: **Syntaktische Validierung findet ausschließlich im UI-Layer statt**, bevor das DTO erstellt wird. **Semantische / Business-Validierung** erfolgt in der Logic Layer (UseCase und Models).
|
||||||
|
|
||||||
|
#### UI Layer — Syntaktische Vorabprüfung
|
||||||
|
- **Verantwortung**: Controller / Command parsen und prüfen die Rohdaten (meist aus `$_POST`, Request-Bodies oder Konsolenargumenten).
|
||||||
|
- **Prüfung**: Der Controller prüft Datentypen, Pflichtfelder, String-Längen, E-Mail-Format. Bei einer Abweichung wird eine `ValidationException` geworfen.
|
||||||
|
- **DTO-Konstruktion**: Erst wenn alle syntaktischen Checks bestanden sind, baut der UI-Layer das typisierte Request-DTO für den UseCase.
|
||||||
|
|
||||||
|
#### Logic Layer — Business-Validierung
|
||||||
|
- **Verantwortung**: Der UseCase und die Business Models prüfen fachliche Regeln (z. B. „Reicht der Bestand?", „Ist der Statusübergang erlaubt?").
|
||||||
|
- **Prüfung**: Erfolgt innerhalb von `execute()` und in Model-Konstruktoren bzw. -Methoden. Bei Verstoß werden spezifische **Domain-Exceptions** geworfen.
|
||||||
|
- **Ziel**: Die Business-Regeln bleiben framework-unabhängig und sind auch für nicht-HTTP-Eintrittspunkte (Message Handler, CLI, interne Service-Calls) konsistent gültig.
|
||||||
|
|
||||||
|
#### Beispiel: Controller mit manueller Validierung
|
||||||
```php
|
```php
|
||||||
namespace App\Logic\Common;
|
namespace App\UI\Controller;
|
||||||
|
|
||||||
/**
|
use App\Logic\Sales\Order\Dto\CreateOrderRequest;
|
||||||
* @template TRequest
|
use App\Logic\Sales\Order\UseCase\CreateOrderUseCase;
|
||||||
* @template TResponse
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
*/
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
interface UseCaseInterface
|
|
||||||
|
class OrdersController
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(private CreateOrderUseCase $createOrder) {}
|
||||||
* @param TRequest $request
|
|
||||||
* @return TResponse|void
|
public function create(Request $request): string
|
||||||
*/
|
{
|
||||||
public function execute(mixed $request): mixed;
|
$data = json_decode($request->getContent(), true);
|
||||||
|
|
||||||
|
// Validierung im UI-Layer, bevor das DTO zustande kommt.
|
||||||
|
if (!isset($data['customerId']) || !is_string($data['customerId'])) {
|
||||||
|
throw new BadRequestHttpException('customerId is required and must be a string.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($data['items']) || !is_array($data['items']) || empty($data['items'])) {
|
||||||
|
throw new BadRequestHttpException('At least one item is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$createOrderRequest = new CreateOrderRequest(
|
||||||
|
customerId: $data['customerId'],
|
||||||
|
items: $data['items'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->createOrder->execute($createOrderRequest);
|
||||||
|
|
||||||
|
return "Order created with ID: {$response->orderId}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Event-Publishing (UseCase Strategie)
|
||||||
|
Wenn ein UseCase einen signifikanten Statuswechsel bewirkt, ist dieser auch für das Publizieren entsprechender **Domänen-Events** zuständig.
|
||||||
|
|
||||||
|
- **Verantwortung**: Der UseCase dispatcht das Event am Ende von `execute()`, nach erfolgreicher Geschäftsfertigung.
|
||||||
|
- **Entkopplung**: Das Logic-Layer spricht ausschließlich gegen ein Interface (z.B. `EventDispatcherInterface`). Das konkrete Framework (Symfony, RabbitMQ, AWS SNS) bleibt im Data/UI-Layer.
|
||||||
|
- **Granularität**: Es werden spezifische Domänenereignisse auf UseCase-Ebene verwandt (z.B. `OrderCreatedEvent`), nicht generische Model-Ereignisse („Entity Changed").
|
||||||
|
|
||||||
#### Implementierung
|
#### Implementierung
|
||||||
```php
|
```php
|
||||||
namespace App\Logic\Sales\Order\UseCase;
|
namespace App\Logic\Sales\Order\UseCase;
|
||||||
|
|
||||||
use App\Logic\Common\UseCaseInterface;
|
use App\Logic\Common\EventDispatcherInterface;
|
||||||
use App\Logic\Sales\Order\Dto\CreateOrderRequest;
|
use App\Logic\Sales\Order\Dto\CreateOrderRequest;
|
||||||
use App\Logic\Sales\Order\Dto\CreateOrderResponse;
|
use App\Logic\Sales\Order\Dto\CreateOrderResponse;
|
||||||
|
use App\Logic\Sales\Order\Event\OrderCreatedEvent;
|
||||||
use App\Logic\Sales\Order\Exception\InsufficientStockException;
|
use App\Logic\Sales\Order\Exception\InsufficientStockException;
|
||||||
|
|
||||||
readonly class CreateOrderUseCase implements UseCaseInterface
|
readonly class CreateOrderUseCase
|
||||||
{
|
{
|
||||||
public function execute(mixed $request): CreateOrderResponse
|
public function __construct(
|
||||||
{
|
private EventDispatcherInterface $eventDispatcher,
|
||||||
if (!$request instanceof CreateOrderRequest) {
|
) {}
|
||||||
throw new \InvalidArgumentException('Invalid request type');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public function execute(CreateOrderRequest $request): CreateOrderResponse
|
||||||
|
{
|
||||||
// Business Logik hier...
|
// Business Logik hier...
|
||||||
if ($this->stockTooLow()) {
|
if ($this->stockTooLow()) {
|
||||||
throw new InsufficientStockException();
|
throw new InsufficientStockException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CreateOrderResponse(orderId: '123');
|
$orderId = '123';
|
||||||
|
|
||||||
|
// Publish domain event nach erfolgreichem Abschluss.
|
||||||
|
// Die EventDispatcherInterface-Implementierung garantiert automatisch
|
||||||
|
// ein Post-Commit-Dispatching, sodass bei einem DB-Rollback kein
|
||||||
|
// "Geister-Event" in den Message Bus gelangt.
|
||||||
|
$this->eventDispatcher->dispatch(new OrderCreatedEvent(orderId: $orderId));
|
||||||
|
|
||||||
|
return new CreateOrderResponse(orderId: $orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function stockTooLow(): bool { return false; }
|
private function stockTooLow(): bool { return false; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Transaktionale Event-Garantie (Bridge-Konvention)
|
||||||
|
Um „Geister-Events" bei Datenbank-Rollbacks zu vermeiden, darf kein Async- oder Broadcast-Event vor erfolgreichem Commit in den Message Bus gelangen. Die Logic Layer ruft einfach `dispatch()` auf und bleibt frei von Transaktions-Boilerplate. Die Infrastruktur-Bridge kapselt das Framework-Spezifische und stellt die transaktionale Konsistenz sicher:
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace App\Data\Infrastructure\Messenger;
|
||||||
|
|
||||||
|
use App\Logic\Common\EventDispatcherInterface;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentTransactionStamp;
|
||||||
|
|
||||||
|
readonly class SymfonyEventDispatcherBridge implements EventDispatcherInterface
|
||||||
|
{
|
||||||
|
public function __construct(private MessageBusInterface $bus) {}
|
||||||
|
|
||||||
|
public function dispatch(object $event): void
|
||||||
|
{
|
||||||
|
$this->bus->dispatch($event, [new DispatchAfterCurrentTransactionStamp()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Vorteile
|
||||||
|
- **Logic bleibt clean**: Der UseCase ist frei von Transaktions-Boilerplate und ORM-Wissen.
|
||||||
|
- **Konsistenz**: Nur com-mittete Daten erzeugen Events.
|
||||||
|
- **Testbarkeit**: Das Interface wird im Unit-Test gemockt; die Bridge-Implementierung ist separat in Integrationstests prüfbar.
|
||||||
|
|
||||||
|
#### Domain Event
|
||||||
|
```php
|
||||||
|
namespace App\Logic\Sales\Order\Event;
|
||||||
|
|
||||||
|
readonly class OrderCreatedEvent
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $orderId,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### DTOs
|
#### DTOs
|
||||||
```php
|
```php
|
||||||
namespace App\Logic\Sales\Order\Dto;
|
namespace App\Logic\Sales\Order\Dto;
|
||||||
@@ -109,7 +196,7 @@ Das Read Pattern beschreibt den Weg einer Datenabfrage von der UI bis zur Datenq
|
|||||||
|
|
||||||
#### Manager (Logic Layer)
|
#### Manager (Logic Layer)
|
||||||
- **Zweck**: Steuert die Bereitstellung der Daten. Hier erfolgt primär die Cache-Logik ("Cache-Aside") und die Koordination von Providern.
|
- **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.
|
- **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.
|
||||||
- **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.
|
||||||
- **Zustand**: Strikt zustandslos (stateless).
|
- **Zustand**: Strikt zustandslos (stateless).
|
||||||
|
|
||||||
@@ -129,21 +216,16 @@ Das Read Pattern beschreibt den Weg einer Datenabfrage von der UI bis zur Datenq
|
|||||||
```php
|
```php
|
||||||
namespace App\Logic\Sales\Order\Query;
|
namespace App\Logic\Sales\Order\Query;
|
||||||
|
|
||||||
use App\Logic\Common\UseCaseInterface;
|
|
||||||
use App\Logic\Sales\Order\Dto\GetOrdersRequest;
|
use App\Logic\Sales\Order\Dto\GetOrdersRequest;
|
||||||
use App\Logic\Sales\Order\Dto\GetOrdersResponse;
|
use App\Logic\Sales\Order\Dto\GetOrdersResponse;
|
||||||
use App\Logic\Sales\Order\Manager\OrderManagerInterface;
|
use App\Logic\Sales\Order\Manager\OrderManagerInterface;
|
||||||
|
|
||||||
readonly class GetOrdersQuery implements UseCaseInterface
|
readonly class GetOrdersQuery
|
||||||
{
|
{
|
||||||
public function __construct(private OrderManagerInterface $orderManager) {}
|
public function __construct(private OrderManagerInterface $orderManager) {}
|
||||||
|
|
||||||
public function execute(mixed $request): GetOrdersResponse
|
public function execute(GetOrdersRequest $request): GetOrdersResponse
|
||||||
{
|
{
|
||||||
if (!$request instanceof GetOrdersRequest) {
|
|
||||||
throw new \InvalidArgumentException('Invalid request type');
|
|
||||||
}
|
|
||||||
|
|
||||||
$orders = $this->orderManager->findOrders($request->filter, $request->page);
|
$orders = $this->orderManager->findOrders($request->filter, $request->page);
|
||||||
|
|
||||||
return new GetOrdersResponse(orders: $orders);
|
return new GetOrdersResponse(orders: $orders);
|
||||||
@@ -242,7 +324,51 @@ Das Write Pattern definiert den Weg von einer Zustandsänderung hin zur Persiste
|
|||||||
- **Input**: Ein Business-Model.
|
- **Input**: Ein Business-Model.
|
||||||
- **Rückgabewert**: Das aktualisierte/neu erstellte Business-Model (z.B. zur Rückgabe von generierten IDs).
|
- **Rückgabewert**: Das aktualisierte/neu erstellte Business-Model (z.B. zur Rückgabe von generierten IDs).
|
||||||
- **Fehlerbehandlung**: Wirft im Fehlerfall direkt **Domain-Exceptions**.
|
- **Fehlerbehandlung**: Wirft im Fehlerfall direkt **Domain-Exceptions**.
|
||||||
- **Strategie**: Verfolgt den "All or Nothing" Ansatz; die übergeordnete Transaktion wird vom UseCase gesteuert.
|
|
||||||
|
#### Transaktionssteuerung ("All or Nothing")
|
||||||
|
Um Schreibvorgänge atomar zu gestalten, wird ein `TransactionManager` eingesetzt, der im Data-Layer implementiert ist (z.B. mittels Doctrine) und gegen ein Interface in der Logic Layer spricht.
|
||||||
|
- **Strategie**: Der UseCase übergibt die gesamte Schreiblogik als Callback (`callable`) an den TransactionManager. Die Logik bleibt frei von Boilerplate wie manuellem `beginTransaction()`, `commit()` oder `rollback()`.
|
||||||
|
- **Mechanik**: Der Manager führt die DB-Transaktion aus. Gibt der Callback einen Wert zurück, wird gecommittet. Tritt eine Exception auf, erfolgt ein automatisches Rollback und die Exception wird hochgeworfen.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// --- Interface (Logic Layer) ---
|
||||||
|
namespace App\Logic\Common;
|
||||||
|
|
||||||
|
interface TransactionManagerInterface
|
||||||
|
{
|
||||||
|
public function execute(callable $work): mixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Implementierung (Data Layer) ---
|
||||||
|
namespace App\Data\Infrastructure\Doctrine;
|
||||||
|
|
||||||
|
use App\Logic\Common\TransactionManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
|
readonly class EntityManagerTransactionManager implements TransactionManagerInterface
|
||||||
|
{
|
||||||
|
public function __construct(private EntityManagerInterface $em) {}
|
||||||
|
|
||||||
|
public function execute(callable $work): mixed
|
||||||
|
{
|
||||||
|
$this->em->beginTransaction();
|
||||||
|
try {
|
||||||
|
$result = $work();
|
||||||
|
$this->em->commit();
|
||||||
|
return $result;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->em->rollback();
|
||||||
|
throw $e;
|
||||||
|
} finally {
|
||||||
|
if ($this->em->getConnection()->isTransactionActive()) {
|
||||||
|
$this->em->rollback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Beispiel (Vollständig integriert)
|
||||||
- **Granularität**: Fokus auf eine einzelne Entität pro Processor.
|
- **Granularität**: Fokus auf eine einzelne Entität pro Processor.
|
||||||
|
|
||||||
### Code Beispiel
|
### Code Beispiel
|
||||||
@@ -280,53 +406,65 @@ readonly class OrderProcessor implements OrderProcessorInterface
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### UseCase und Write-Manager Integration
|
#### UseCase mit Transaktionssteuerung
|
||||||
```php
|
```php
|
||||||
namespace App\Logic\Sales\Order\UseCase;
|
namespace App\Logic\Sales\Order\UseCase;
|
||||||
|
|
||||||
use App\Logic\Common\UseCaseInterface;
|
use App\Logic\Common\EventDispatcherInterface;
|
||||||
|
use App\Logic\Common\TransactionManagerInterface;
|
||||||
|
use App\Logic\Sales\Event\OrderCreatedEvent;
|
||||||
use App\Logic\Sales\Order\Dto\CreateOrderRequest;
|
use App\Logic\Sales\Order\Dto\CreateOrderRequest;
|
||||||
use App\Logic\Sales\Order\Dto\CreateOrderResponse;
|
use App\Logic\Sales\Order\Dto\CreateOrderResponse;
|
||||||
|
use App\Logic\Sales\Order\Exception\InsufficientStockException;
|
||||||
use App\Logic\Sales\Order\Manager\OrderManagerInterface;
|
use App\Logic\Sales\Order\Manager\OrderManagerInterface;
|
||||||
|
use App\Logic\Sales\Stock\Manager\StockManagerInterface;
|
||||||
|
|
||||||
readonly class CreateOrderUseCase implements UseCaseInterface
|
readonly class CreateOrderUseCase
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
private TransactionManagerInterface $transactionManager,
|
||||||
private OrderManagerInterface $orderManager,
|
private OrderManagerInterface $orderManager,
|
||||||
// TransactionManager ggf. für Multi-Processor-Szenarien
|
private StockManagerInterface $stockManager,
|
||||||
|
private EventDispatcherInterface $eventDispatcher,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function execute(mixed $request): CreateOrderResponse
|
public function execute(CreateOrderRequest $request): CreateOrderResponse
|
||||||
{
|
{
|
||||||
if (!$request instanceof CreateOrderRequest) {
|
return $this->transactionManager->execute(function () use ($request): CreateOrderResponse {
|
||||||
throw new \InvalidArgumentException('Invalid request type');
|
// 1. Vorbedingungen prüfen -> Fehlschlag löst automatisches Rollback aus.
|
||||||
}
|
if (!$this->stockManager->isSufficient($request->items)) {
|
||||||
|
throw new InsufficientStockException();
|
||||||
|
}
|
||||||
|
|
||||||
// Business Logik / Validierung ...
|
// 2. Business Model anlegen und eigene State-Logik ausführen.
|
||||||
|
$order = new Order(/* ... */);
|
||||||
|
$order->confirm();
|
||||||
|
|
||||||
// Delegiere an den Manager: Der persistiert UND invalidiert den Read-Cache
|
// 3. Persistieren (Manager -> Processor).
|
||||||
$order = $this->orderManager->createOrder($request);
|
$createdOrder = $this->orderManager->create($order);
|
||||||
|
|
||||||
return new CreateOrderResponse(orderId: $order->id);
|
// 4. Domänen-Event dispatchen. Die Bridge-Implementierung des
|
||||||
|
// EventDispatcherInterface garantiert ein Post-Commit-Dispatching,
|
||||||
|
// sodass das Event nur den Bus erreicht, wenn die Transaktion
|
||||||
|
// erfolgreich abgeschlossen wurde (siehe Sektion 1).
|
||||||
|
$this->eventDispatcher->dispatch(new OrderCreatedEvent(orderId: $createdOrder->id));
|
||||||
|
|
||||||
|
return new CreateOrderResponse(orderId: $createdOrder->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)
|
## 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.
|
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 PHP-basierte Validierung 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
|
### Verantwortung pro Layer
|
||||||
|
|
||||||
| Layer | Was wird geprüft? | Beispiel |
|
| Layer | Was wird geprüft? | Beispiel |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- |
|
||||||
| **Frontend** | Sofortiges Format-Feedback | E-Mail-Format, Positive Nummern |
|
| **Frontend** | Sofortiges Format-Feedback | E-Mail-Format, Positive Nummern |
|
||||||
| **UI (Controller + DTO)** | Syntaktische Prüfung via Symfony Validator | Pflichtfelder, Datentypen, Längen |
|
| **UI (Controller)** | Syntaktische Prüfung mit reinem PHP | Pflichtfelder, Datentypen, Längen |
|
||||||
| **Logic (UseCase / Model)** | Business-Regeln, Cross-Entity Constraints, Zustandsgültigkeit | Host auflösbar? Guthaben reicht? Statusübergang erlaubt? |
|
| **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 |
|
| **Data (Processor)** | Externe Constraints, DB-Integrität | Unique-Konflikt, API Rate Limits |
|
||||||
|
|
||||||
@@ -334,42 +472,39 @@ Validierung findet so früh wie möglich statt. Jede Layer prüft, was sie prüf
|
|||||||
|
|
||||||
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.
|
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
|
```php
|
||||||
namespace App\UI\Http\Sales\Order;
|
namespace App\UI\Http\Sales\Order;
|
||||||
|
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
|
||||||
|
|
||||||
readonly class OrderController extends AbstractController
|
readonly class OrderController extends AbstractController
|
||||||
{
|
{
|
||||||
public function create(
|
public function create(
|
||||||
Request $request,
|
Request $request,
|
||||||
ValidatorInterface $validator,
|
|
||||||
CreateOrderUseCase $useCase
|
CreateOrderUseCase $useCase
|
||||||
): JsonResponse {
|
): JsonResponse {
|
||||||
// DTO aus Request bauen ...
|
// DTO aus Request bauen ...
|
||||||
$dto = /* mapping */ ;
|
$email = $request->request->get('customerEmail');
|
||||||
|
$name = $request->request->get('customerName');
|
||||||
|
$items = $request->request->all('items') ?? [];
|
||||||
|
|
||||||
// Syntaktische Validierung (frühes Fail)
|
// Syntaktische Validierung mit reinem PHP (frühes Fail)
|
||||||
$violations = $validator->validate($dto);
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
if (count($violations) > 0) {
|
return new JsonResponse(['error' => 'Invalid email format'], 422);
|
||||||
return new JsonResponse(['errors' => (string)$violations], 422);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (empty($name) || strlen($name) < 2) {
|
||||||
|
return new JsonResponse(['error' => 'Customer name is required'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($items)) {
|
||||||
|
return new JsonResponse(['error' => 'Order must contain items'], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dto = new CreateOrderRequest(
|
||||||
|
customerEmail: $email,
|
||||||
|
customerName: $name,
|
||||||
|
itemIds: $items,
|
||||||
|
);
|
||||||
|
|
||||||
// UseCase übernimmt Business-Validierung
|
// UseCase übernimmt Business-Validierung
|
||||||
$responseDto = $useCase->execute($dto);
|
$responseDto = $useCase->execute($dto);
|
||||||
return new JsonResponse(['orderId' => $responseDto->orderId], 201);
|
return new JsonResponse(['orderId' => $responseDto->orderId], 201);
|
||||||
@@ -402,94 +537,14 @@ class Order extends BaseModel
|
|||||||
- **Frühes Erkennen**: Formatfehler werden im Controller erkannt, sodass keine UseCase- oder Data-Layer-Logik ausgelastet wird.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
## 5. Entity vs. Business Model (`src/Data` $\rightarrow$ `src/Logic`)
|
||||||
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;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Anwendung im UseCase (`src/Logic`)
|
|
||||||
Der UseCase nutzt den TransactionManager, um sicherzustellen, dass entweder alle oder keine Änderungen persistiert werden (All-or-Nothing). Die Orchestrierung zwischen Managern erfolgt innerhalb der Transaktions-Closure.
|
|
||||||
|
|
||||||
```php
|
|
||||||
readonly class CreateOrderUseCase implements UseCaseInterface
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private TransactionManagerInterface $transactionManager,
|
|
||||||
private OrderManager $orderManager,
|
|
||||||
private StockManager $stockManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function execute(mixed $request): CreateOrderResponse
|
|
||||||
{
|
|
||||||
return $this->transactionManager->transactional(function() use ($request) {
|
|
||||||
// 1. Produktbestand reduzieren (Manager orchestriert Processor + Cache)
|
|
||||||
$this->stockManager->reduceStockForOrder($request->items);
|
|
||||||
|
|
||||||
// 2. Bestellung anlegen (Manager orchestriert Processor + Cache)
|
|
||||||
$order = $this->orderManager->createOrder($request);
|
|
||||||
|
|
||||||
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 Manager können konsistent in einer Transaktion orchestriert werden.
|
|
||||||
- **Testbarkeit**: Der TransactionManager kann in Unit Tests einfach durch einen Mock ersetzt werden, der den Closure direkt ausführt.
|
|
||||||
|
|
||||||
Die Manager-Konzepte werden in Sektion 3 (Write Pattern) detailliert beschrieben. Die UseCases arbeiten auf Managern, die ihrerseits Processors aufrufen.
|
|
||||||
|
|
||||||
## 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.
|
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?
|
### 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.
|
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.
|
2. **Vermeidung von Side-Effects**: ORMs wie Doctrine nutzen Lazy-Loading Proxies. Wenn diese Objekte in die 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.
|
3. **Anämische vs. Reichhaltige Modelle**: Persistenz-Entities sind oft "anämisch" (nur Getter/Setter), um die DB-Struktur abzubilden. Business Models hingegen enthalten Rich Domain Logic.
|
||||||
|
|
||||||
### Definitionen
|
### Definitionen
|
||||||
|
|
||||||
@@ -500,10 +555,11 @@ Um die Geschäftslogik vollständig vom Framework und dem ORM zu entkoppeln, wir
|
|||||||
|
|
||||||
#### Business Model (`src/Logic`)
|
#### Business Model (`src/Logic`)
|
||||||
- **Zweck**: Repräsentation des fachlichen Objekts innerhalb der Geschäftslogik.
|
- **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.
|
- **Eigenschaften**: POPO (Plain Old PHP Object), readonly wo möglich, enthält Business-Validierung und Logik.
|
||||||
- **Logik-Verteilung**:
|
- **Logik-Verteilung**:
|
||||||
- **Intrinsic Logic** (im Model): Beantwortet Fragen über den eigenen Zustand, die keine externen Abhängigkeiten benötigen (z.B. `getFullName()`, `getAge()`, `isExpired()`).
|
- **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).
|
- **Extrinsic Logic** (im UseCase): Steuert Prozesse, Koordination und Logik mit externen Abhängigkeiten (z.B. Prüfung von Beständen via Processor).
|
||||||
|
- **Zustandsübergänge (State Transitions)**: Das Model kapselt die eigenen Statusänderungen. Anstatt `setStatus()` zu nutzen, bietet das Model explizite Methoden an (`confirm()`, `cancel()`), welche die Erlaubtheit des Übergangs prüfen und eine fachliche Exception werfen, falls er gegen die Geschäftsregeln verstößt.
|
||||||
- **Regel**: Kennt keine Details über die Persistenz.
|
- **Regel**: Kennt keine Details über die Persistenz.
|
||||||
|
|
||||||
### Übergabe & Mapping
|
### Übergabe & Mapping
|
||||||
@@ -513,7 +569,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.
|
||||||
|
|
||||||
## 7. 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.
|
||||||
|
|
||||||
@@ -527,7 +583,39 @@ Ein Mapper implementiert typischerweise zwei Methoden:
|
|||||||
1. `toModel(Entity $entity): Model`: Konvertierung von der DB zur Logik (genutzt in Providern).
|
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.
|
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
|
### DTO-zu-Model-Mapping (Logic Layer)
|
||||||
|
Um die UseCases schlank zu halten, findet die Transformation von Eingabe-DTOs (`Request`) in Business Models nicht durch Inline-Logik im UseCase statt. Stattdessen kommen dedizierte **ModelFactories** oder **Logic-Maps** zum Einsatz.
|
||||||
|
|
||||||
|
- **Ort**: `src/Logic/{Module}/{Feature}/Mapping/`
|
||||||
|
- **Verantwortung**: Abbildung der Struktur von einem DTO auf ein Business Model (einschließlich Child-Models).
|
||||||
|
- **Dependency Rule**: Diese Mappers liegen zwingend in der Logic Layer und kennen keine Entitys oder Provider.
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace App\Logic\Sales\Order\Mapping;
|
||||||
|
|
||||||
|
use App\Logic\Sales\Order\Dto\CreateOrderRequest;
|
||||||
|
use App\Logic\Sales\Order\Model\Order;
|
||||||
|
use App\Logic\Sales\Order\Model\OrderItem;
|
||||||
|
use App\Logic\Sales\Order\Model\OrderStatus;
|
||||||
|
|
||||||
|
readonly class OrderModelFactory
|
||||||
|
{
|
||||||
|
public function createFromRequest(CreateOrderRequest $request): Order
|
||||||
|
{
|
||||||
|
return new Order(
|
||||||
|
customerId: $request->customerId,
|
||||||
|
// Delegierung der Child-Objekte
|
||||||
|
items: array_map(
|
||||||
|
fn($data) => new OrderItem(productId: $data['productId'], quantity: $data['quantity']),
|
||||||
|
$request->items
|
||||||
|
),
|
||||||
|
status: OrderStatus::Created,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Beispiel (Data Layer Mapping)
|
||||||
|
|
||||||
#### Mapper Implementierung
|
#### Mapper Implementierung
|
||||||
```php
|
```php
|
||||||
@@ -565,7 +653,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.
|
||||||
|
|
||||||
## 8. 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.
|
||||||
|
|
||||||
@@ -617,7 +705,7 @@ readonly class ShippingProvider implements ShippingProviderInterface
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 9. UI Controller Pattern (`src/UI`)
|
## 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.
|
Die Controller fungieren als reine Adapter zwischen dem externen Eintrittspunkt (HTTP/CLI) und der Logic Layer. Sie enthalten keine Geschäftslogik.
|
||||||
|
|
||||||
@@ -676,7 +764,7 @@ readonly class OrderController extends AbstractController
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 10. Exception Hierarchie & Error Handling (`src/Logic` $\rightarrow$ `src/UI`)
|
## 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.
|
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.
|
||||||
|
|
||||||
@@ -742,8 +830,7 @@ Je nach Reichweite und Zeitkritikalität kommen verschiedene Mechanismen zum Ein
|
|||||||
- **Beispiel**: Benachrichtigung des Logistik-Systems über eine neue Bestellung.
|
- **Beispiel**: Benachrichtigung des Logistik-Systems über eine neue Bestellung.
|
||||||
|
|
||||||
### Transaktions-Sicherheit (Transactional Consistency)
|
### Transaktions-Sicherheit (Transactional Consistency)
|
||||||
Um "Geister-Events" durch Datenbank-Rollbacks zu vermeiden, gilt folgende Regel:
|
Um "Geister-Events" durch Datenbank-Rollbacks zu vermeiden, ist das Dispatching von Async- und Broadcast-Events zwingend an einen erfolgreichen DB-Commit gebunden. Die konkrete Umsetzung erfolgt über die **Bridge-Konvention** aus Sektion 1: Der UseCase ruft `dispatch()` innerhalb der Transaktions-Closure auf; die `SymfonyEventDispatcherBridge` im Data Layer setzt automatisch den `DispatchAfterCurrentTransactionStamp`, sodass nur com-mittete Daten Events erzeugen. Ein Fehlschlag in der DB verhindert das Erreichen des Message Bus.
|
||||||
**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
|
### Zusammenfassung Payload-Strategie
|
||||||
| Event Typ | Timing | Payload | Transport |
|
| Event Typ | Timing | Payload | Transport |
|
||||||
@@ -768,7 +855,7 @@ Die UI Layer fängt die `ConcurrencyException` im zentralen Exception-Subscriber
|
|||||||
- **HTTP Status**: `409 Conflict`.
|
- **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.
|
- **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`)
|
## 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.
|
Um die Logic Layer unabhängig von Symfony's Parameter-System zu halten und gleichzeitig pragmatisch zu bleiben, wird ein hybrider Ansatz verfolgt.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user