docs: replace toEntity() with separate createEntity/updateEntity factory pattern
This commit is contained in:
+69
-11
@@ -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.
|
- **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).
|
- **Status**: Mapper sind strikt zustandslos (stateless).
|
||||||
|
|
||||||
### Mapping Richtungen
|
### Mapping Richtungen (Factory-Pattern)
|
||||||
Ein Mapper implementiert typischerweise zwei Methoden:
|
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).
|
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)
|
### 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.
|
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->setEmail($model->customerEmail);
|
||||||
$entity->setAmount($model->totalAmount);
|
$entity->setAmount($model->totalAmount);
|
||||||
// ... weitere Felder
|
// ... weitere Felder
|
||||||
|
|
||||||
return $entity;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Integration in Provider/Processor
|
#### Integration in Processor (Complete/Update-Unterscheidung)
|
||||||
- **Provider**: Ruft `toModel()` auf, bevor das Resultat an den Manager zurückgegeben wird.
|
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:
|
||||||
- **Processor**: Ruft `toEntity()` auf, um das Business Model persistierbar zu machen, und führt anschließend den Save-Vorgang des ORMs aus.
|
|
||||||
|
```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`)
|
## 7. Externe Systemintegration (`src/Data`)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user