docs: replace toEntity() with separate createEntity/updateEntity factory pattern

This commit is contained in:
2026-07-17 11:34:44 +02:00
parent bf34553ac8
commit 8858e9d762
+69 -11
View File
@@ -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`)