diff --git a/architektur-patterns.md b/architektur-patterns.md index 73cd575..39cc5c5 100644 --- a/architektur-patterns.md +++ b/architektur-patterns.md @@ -578,10 +578,16 @@ Um Redundanz zu vermeiden und eine konsistente Transformation zwischen Persisten - **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: +### Mapping Richtungen (Factory-Pattern) +Ein Mapper implementiert drei klar getrennte Methoden, um Create- und Update-Pfade zu trennen: 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. `createEntity(Model $model): Entity`: Erstellt eine brandneue Entity aus dem Model (für INSERT). Gibt das frische Entity-Objekt zurück, das vom Processor persistiert wird. +3. `updateEntity(Model $model, Entity $entity): void`: Schreibt die Felder des Models in eine bestehende, bereits vom EntityManager verwaltete Entity (für UPDATE). Die Entity wird über den PRIMARY KEY im Processor geladen (`$em->find()`), dann an den Mapper übergaben. Nach dem `flush()` erkennt Doctrine dank Dirty-Checking die Änderungen. Der Mapper selbst kennt Doctrine nicht — er ist reines Feld-Mapping. + +Die Trennung von Create und Update vermeidet folgende Probleme: +- **Klare Semantik**: Keine optionale Parameter mit implizitem Verhalten (`null` = erstelle neu, vorhanden = aktualisiere). +- **Einfachere Tests**: Jeder Pfad wird isoliert testbar, ohne Mock-Fälle für beides abzudecken. +- **Doctrine-Lifecycle-Korrekte Trennung**: Eine Entity muss beim UPDATE bereits vom EntityManager verwaltet sein (`$em->find()`). Der Mapper sollte sich nicht darum kümmern — das ist Prozessoraufgabe. ### 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. @@ -636,22 +642,74 @@ readonly class OrderMapper ); } - public function toEntity(Order $model, ?OrderEntity $entity = null): OrderEntity + public function createEntity(Order $model): OrderEntity + { + return (new OrderEntity()) + ->setEmail($model->customerEmail) + ->setAmount($model->totalAmount); + // ... weitere Felder + } + + public function updateEntity(Order $model, OrderEntity $entity): void { - $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. +#### Integration in Processor (Complete/Update-Unterscheidung) +Der Processor entscheidet basierend auf der Model-ID, ob er `createEntity()` oder `updateEntity()` aufruft. Bei einem Update lädt er zunächst die bestehende Entity vom EntityManager, sodass Doctrine den PRIMARY KEY und den Lifecycle korrekt verwaltet: + +```php +namespace App\Data\Sales\Order\Processor; + +use App\Data\Sales\Order\Entity\OrderEntity; +use App\Data\Sales\Order\Mapper\OrderMapper; +use App\Logic\Sales\Order\Model\Order; +use App\Logic\Sales\Order\OrderProcessorInterface; +use Doctrine\ORM\EntityManagerInterface; + +readonly class OrderProcessor implements OrderProcessorInterface +{ + public function __construct( + private EntityManagerInterface $em, + private OrderMapper $mapper, + ) {} + + public function save(Order $model): Order + { + if ($model->id === null) { + // CREATE-Pfad: Neue Entity erzeugen und persistieren + $entity = $this->mapper->createEntity($model); + $this->em->persist($entity); + $this->em->flush(); + + // ID vom Entity zurück in das Model übernehmen + return new Order( + id: $entity->getId(), + customerEmail: $model->customerEmail, + totalAmount: $model->totalAmount, + orderItems: $model->orderItems, + status: $model->status, + ); + } + + // UPDATE-Pfad: Bestehende Entity laden und aktualisieren + $entity = $this->em->getRepository(OrderEntity::class)->find($model->id); + + if ($entity === null) { + throw new OrderNotFoundException($model->id); + } + + $this->mapper->updateEntity($model, $entity); + $this->em->flush(); // Doctrine Dirty-Checking übernimmt den Rest + + return $model; + } +} +``` ## 7. Externe Systemintegration (`src/Data`)