M1: CRUD-Slice 'Mahlzeit' Schritt 1 – Logik + Daten + Tests

Rich-Model Mahlzeit (MahlzeitStatus, Wochentag) mit Invarianten und
Zustandsübergängen; DTOs + MahlzeitFactory (reine Mapping-Funktionen);
Manager (create hängt an Ende an: maxSortOrder+1, find → 404) und
Create/Update/Delete-UseCases mit Transaktion + Events nach Commit.

Data-Layer: MahlzeitEntity (Tabelle essens_mahlzeiten, wochentage als
CSV), Mapper (CSV/Enum/null), Provider (find/list/maxSortOrder),
Processor (create/save/delete). DI-Bindings in services.yaml.
ClockInterface (Logic) + SystemClock (Data).

Tests: Model/Factory/UseCase-Unit + Provider/Processor/Mapper-Integration
(In-Mem-SQLite). composer test grün (53 Tests, 161 Assertions).
This commit is contained in:
2026-09-19 08:55:54 +02:00
parent cb3cbfb394
commit 1589088379
42 changed files with 2493 additions and 138 deletions
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Logic\Essen\Mapping;
use App\Logic\Common\Exception\BusinessRuleViolationException;
use App\Logic\Essen\Dto\MahlzeitCreateRequest;
use App\Logic\Essen\Dto\MahlzeitUpdateRequest;
use App\Logic\Essen\Model\Mahlzeit;
/**
* Reine Funktionen: Request-DTO → Modell. Keine Framework-/DB-Abhängigkeit.
*
* Fachregeln (Pflichtfelder) werden hier als Business-Validierung geprüft.
* Zeitstempel (`erstelltAm`/`aktualisiertAm`) liefert der UseCase über `ClockInterface`.
*/
final class MahlzeitFactory
{
/**
* Erstellt ein neues, geplantes Modell aus einem Anlegen-DTO.
*
* `sortOrder` wird bewusst auf 0 gesetzt – der Manager hängt den Eintrag
* anschließend am Ende an (max+1).
*/
public function createFromCreateRequest(MahlzeitCreateRequest $req, \DateTimeImmutable $jetzt): Mahlzeit
{
$this->assertRequired($req->name, 'name');
return Mahlzeit::anlegen(
name: $req->name,
notiz: $req->notiz,
wochentage: $req->wochentage,
sortOrder: 0,
jetzt: $jetzt,
);
}
/**
* Wendet ein Teil-Update auf ein bestehendes Modell an. `null`-Felder bleiben unverändert.
*/
public function updateFromUpdateRequest(Mahlzeit $mahlzeit, MahlzeitUpdateRequest $req, \DateTimeImmutable $jetzt): Mahlzeit
{
if ($req->name !== null) {
$this->assertRequired($req->name, 'name');
}
$name = $req->name ?? $mahlzeit->name;
$notiz = $req->notiz ?? $mahlzeit->notiz;
$wochentage = $req->wochentage ?? $mahlzeit->wochentage;
return $mahlzeit->aktualisieren(
name: $name,
notiz: $notiz,
wochentage: $wochentage,
sortOrder: $mahlzeit->sortOrder,
jetzt: $jetzt,
);
}
private function assertRequired(?string $value, string $feld): void
{
if ($value === null || \trim($value) === '') {
throw new BusinessRuleViolationException(\sprintf('%s ist ein Pflichtfeld.', $feld));
}
}
}