Refactor validation strategy: remove Symfony Validator from Logic Layer in favor of pure PHP checks in UI
This commit is contained in:
+160
-18
@@ -151,7 +151,7 @@ readonly class GetOrdersQuery implements UseCaseInterface
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Manager & Provider
|
#### Manager (Read + Cache)
|
||||||
```php
|
```php
|
||||||
namespace App\Logic\Sales\Order\Manager;
|
namespace App\Logic\Sales\Order\Manager;
|
||||||
|
|
||||||
@@ -159,19 +159,41 @@ use App\Data\Sales\Order\Provider\OrderProvider;
|
|||||||
|
|
||||||
readonly class OrderManager
|
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
|
public function findOrders(array $filters, int $page): array
|
||||||
{
|
{
|
||||||
// Cache-Logik hier...
|
// cacheKey aus filters + page generieren ...
|
||||||
return $this->orderProvider->fetchOrders($filters, $page);
|
// if ($cache->has($key)) return $cache->get($key);
|
||||||
}
|
|
||||||
|
$orders = $this->orderProvider->fetchOrders($filters, $page);
|
||||||
|
// $cache->set($key, $orders);
|
||||||
|
return $orders;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Data Layer ---
|
/**
|
||||||
|
* Invalidiert den Cache für die Order-Domain.
|
||||||
|
*/
|
||||||
|
public function invalidateCache(): void
|
||||||
|
{
|
||||||
|
// Tags/Keys löschen, die durch den Write betroffen sind ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Provider (Data Layer)
|
||||||
|
```php
|
||||||
namespace App\Data\Sales\Order\Provider;
|
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
|
public function fetchOrders(array $filters, int $page): array
|
||||||
{
|
{
|
||||||
@@ -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.
|
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
|
### Der Datenfluss
|
||||||
`UI Layer` $\rightarrow$ **UseCase** $\rightarrow$ **Manager** (optional) $\rightarrow$ **Processor**
|
`UI Layer` $\rightarrow$ **UseCase** $\rightarrow$ **Manager** $\rightarrow$ **Processor**
|
||||||
|
|
||||||
#### Manager (Logic Layer - optional)
|
#### Manager (Logic Layer)
|
||||||
- **Zweck**: Koordinationsschicht für Schreiboperationen.
|
- **Zweck**: Zentrale Steuereinheit pro Domain, die sowohl Lese- als auch Schreiboperationen koordiniert.
|
||||||
- **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).
|
- **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.
|
||||||
- **Schnittstelle**: Besitzt wie im Read Pattern ein eigenes Interface zur Gewährleistung von Testbarkeit.
|
|
||||||
|
|
||||||
#### Processor (Data Layer)
|
#### Processor (Data Layer)
|
||||||
- **Zweck**: Übernimmt die physische Persistierung eines Business-Models.
|
- **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.
|
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.
|
- **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`)
|
## 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.
|
||||||
|
|
||||||
@@ -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.
|
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.
|
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.
|
- **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.
|
||||||
|
|
||||||
## 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.
|
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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -18,6 +18,7 @@ Die Anwendung ist in drei strikt getrennte Layer unterteilt:
|
|||||||
- **BusinessQueries**: Spezifische Abfragen für geschäftsrelevante Daten.
|
- **BusinessQueries**: Spezifische Abfragen für geschäftsrelevante Daten.
|
||||||
- **Manager**: Orchestratoren, die z.B. Caching-Strategien implementieren und zwischen verschiedenen Providern/Processoren vermitteln.
|
- **Manager**: Orchestratoren, die z.B. Caching-Strategien implementieren und zwischen verschiedenen Providern/Processoren vermitteln.
|
||||||
- **Models**: Kernlogik-Objekte zur internen Verarbeitung innerhalb der Logic Layer.
|
- **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.
|
- **Regel**: Ist unabhängig von der UI Layer und definiert die Anforderungen an die Data Layer über Interfaces.
|
||||||
|
|
||||||
### Data Layer (`src/Data`)
|
### Data Layer (`src/Data`)
|
||||||
@@ -26,7 +27,7 @@ Die Anwendung ist in drei strikt getrennte Layer unterteilt:
|
|||||||
- **Komponenten**:
|
- **Komponenten**:
|
||||||
- **Repositories**: Zugriff auf Datenbankentitäten.
|
- **Repositories**: Zugriff auf Datenbankentitäten.
|
||||||
- **Entities**: Domain-Modelle für die Persistenz (spiegeln DB-Schema).
|
- **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.
|
- **Mapper**: Transformation zwischen Entity und Business Model.
|
||||||
- **Regel**: Kennt keine Geschäftslogik und ist nur für die Bereitstellung/Speicherung von Daten zuständig.
|
- **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. |
|
| UseCase / Workflow | `src/Logic` | Einzelschrittliche Geschäftsoperation. |
|
||||||
| BusinessQuery | `src/Logic` | Funktionale Abfrage von Geschäftsdaten. |
|
| BusinessQuery | `src/Logic` | Funktionale Abfrage von Geschäftsdaten. |
|
||||||
| Manager | `src/Logic` | Cache-Steuerung und Koordination von Providern. |
|
| Manager | `src/Logic` | Cache-Steuerung und Koordination von Providern/Processoren. |
|
||||||
| Provider | `src/Data` | Bereitstellung von Daten (Read). |
|
| Provider Interface | `src/Logic` | Vertragsdefinition für Datenbereitstellung (Read). |
|
||||||
| Processor | `src/Data` | Verarbeitung/Persistierung von Daten (Write). |
|
| 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. |
|
||||||
|
|||||||
Reference in New Issue
Block a user