Refactor validation strategy: remove Symfony Validator from Logic Layer in favor of pure PHP checks in UI

This commit is contained in:
2026-07-16 18:15:54 +02:00
parent 075da0b5f6
commit ceb3bfe084
2 changed files with 167 additions and 22 deletions
+160 -18
View File
@@ -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,24 +159,46 @@ 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;
}
/**
* 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; 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
{ {
// DB Abfrage und Mapping zu Business-Models // 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. 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
View File
@@ -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. |