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:
+150
-132
@@ -3,6 +3,30 @@
|
||||
> Konkretisierung der ADR-007 für diese App. Referenz:
|
||||
> [jens/symfony-architektur](https://git.ljh31.de/jens/symfony-architektur).
|
||||
|
||||
## 0.1 Namenskonzept (2026-09-13 final)
|
||||
|
||||
**Regel: Domäne deutsch, technische Begriffe / Suffixe englisch.**
|
||||
|
||||
| Rolle | Name | Beispiel |
|
||||
|---|---|---|
|
||||
| Rich-Modell | Substantiv | `Mahlzeit` |
|
||||
| Domain-Enum (Status) | `<Model>Status` | `MahlzeitStatus` (`Geplant`, `Gegessen`) |
|
||||
| Domain-Enum (Wert) | Substantiv, Werte kurz | `Wochentag` (`Mo`…`So`) |
|
||||
| DTOs | `<Model><Aktion>{Request,Response}` | `MahlzeitCreateRequest`, `MahlzeitListResponse` |
|
||||
| Fabrik (DTO→Model) | `<Model>Factory` | `MahlzeitFactory` |
|
||||
| UseCase | `<Model><Aktion>UseCase` | `MahlzeitCreateUseCase` |
|
||||
| Read-Query | `<Model><Aktion>Query` | `MahlzeitListQuery` |
|
||||
| Manager | `<Model>Manager` | `MahlzeitManager` |
|
||||
| Provider (Read) | `<Model>Provider(Interface)` | `MahlzeitProvider` |
|
||||
| Processor (Write) | `<Model>Processor(Interface)` | `MahlzeitProcessor` |
|
||||
| Mapper | `<Model>Mapper` | `MahlzeitMapper` |
|
||||
| Entity | `<Model>Entity` | `MahlzeitEntity` |
|
||||
| Exception | `<Model>…Exception` | `MahlzeitNotFoundException` |
|
||||
| Events | `<Model><Aktion>Event` | `MahlzeitCreatedEvent` |
|
||||
|
||||
Methode-Suffixe in Interfaces bleiben kurz/englisch (`create`, `save`, `delete`, `find`, `list`),
|
||||
da sie technische Konvention sind — nur die **Klassen-/Modellnamen** sind domänendeutsch.
|
||||
|
||||
## 0. Die drei Layer (Kurzreferenz)
|
||||
|
||||
```
|
||||
@@ -26,43 +50,36 @@ UI → Logic; Logic → Interface (Definition im Logic); Data → Interface
|
||||
(Definition im Data). Die Data-Layer implementiert Interfaces aus der
|
||||
Logic-Layer. **Keine** Umkehr.
|
||||
|
||||
**Namenskonvention (Kurz):**
|
||||
- Interface: Suffixe `Interface` (z. B. `DishProviderInterface`).
|
||||
- UseCase: Suffix `UseCase` (z. B. `CreateDishUseCase`).
|
||||
- Query: Suffix `Query` (z. B. `ListDishesQuery`).
|
||||
- Manager: Suffix `Manager` (z. B. `DishManager`).
|
||||
- Provider/Processor: Suffix `Provider`/`Processor` (z. B. `DishProvider`).
|
||||
- Mapper: Suffix `Mapper` (z. B. `DishMapper`).
|
||||
- Model-Factory: Suffix `Factory` (z. B. `DishModelFactory`).
|
||||
- DTO: Suffix `Request` bzw. `Response`.
|
||||
- Exception: Suffix `Exception`.
|
||||
- Entity (Data): Suffix `Entity` (z. B. `DishEntity`).
|
||||
**Namenskonvention (Kurz)** — siehe §0.1 Namenskonzept:
|
||||
`MahlzeitProviderInterface`, `MahlzeitCreateUseCase`, `MahlzeitListQuery`,
|
||||
`Manager`, `Provider`/`Processor`, `Mapper`, `MahlzeitFactory` (statt `*ModelFactory`),
|
||||
`Create/Update/Delete/…Request`, `…Response`, Exception, `Entity`.
|
||||
|
||||
## 1. Domain-Modell (Logik)
|
||||
|
||||
**Eine** Modul-Domäne: `Essen`. Darin ein Business-Model: `Dish`
|
||||
**Eine** Modul-Domäne: `Essen`. Darin ein Business-Model: `Mahlzeit`
|
||||
(Gericht/Eintrag). Wichtiger Hinweis: Die "Archivsicht" ist **kein**
|
||||
abgesonderter Business-Model, sondern eine **Volltext-Sicht** (Read-Query) mit
|
||||
dem Filter `status = gegessen`. Das reduziert die Komplexität.
|
||||
|
||||
### 1.1. Das Business-Model `Dish`
|
||||
### 1.1. Das Business-Model `Mahlzeit`
|
||||
|
||||
`src/Logic/Essen/Model/Dish.php` – **Rich Domain Model** (POPO, readonly,
|
||||
`src/Logic/Essen/Model/Mahlzeit.php` – **Rich Domain Model** (POPO, readonly,
|
||||
kein Framework):
|
||||
|
||||
```php
|
||||
namespace App\Logic\Essen\Model;
|
||||
|
||||
class Dish
|
||||
class Mahlzeit
|
||||
{
|
||||
public function __construct(
|
||||
public readonly ?int $id,
|
||||
public string $name,
|
||||
public ?string $notiz,
|
||||
/** @var DishDay[] */
|
||||
/** @var Wochentag[] */
|
||||
public readonly array $wochentage,
|
||||
public readonly int $sort_order,
|
||||
public DishStatus $status,
|
||||
public MahlzeitStatus $status,
|
||||
public readonly ?\DateTimeImmutable $gegessen_am,
|
||||
public readonly \DateTimeImmutable $erstellt_am,
|
||||
public readonly \DateTimeImmutable $aktualisiert_am,
|
||||
@@ -70,11 +87,11 @@ class Dish
|
||||
|
||||
// State transitions: kapseln eigene Statusänderungen
|
||||
public function markAsGegessen(\DateTimeImmutable $now): self {
|
||||
if ($this->status === DishStatus::GEGESSEN) {
|
||||
if ($this->status === MahlzeitStatus::GEGESSEN) {
|
||||
throw new BusinessRuleViolationException('Bereits gegessen.');
|
||||
}
|
||||
return new self($this->id, $this->name, $this->notiz, $this->wochentage,
|
||||
$this->sort_order, DishStatus::GEGESSEN, $now, $this->erstellt_am, $now);
|
||||
$this->sort_order, MahlzeitStatus::GEGESSEN, $now, $this->erstellt_am, $now);
|
||||
}
|
||||
|
||||
public function markAsGeplant(\DateTimeImmutable $now): self { /* symmetrisch */ }
|
||||
@@ -82,23 +99,24 @@ class Dish
|
||||
```
|
||||
|
||||
**Enums** in `src/Logic/Essen/Model/`:
|
||||
- `DishStatus` (`GEPLANT`, `GEGESSEN`)
|
||||
- `DishDay` (`MO`, `DI`, `MI`, `DO`, `FR`, `SA`, `SO`)
|
||||
- `MahlzeitStatus` (`GEPLANT`, `GEGESSEN`)
|
||||
- `Wochentag` (`MO`, `DI`, `MI`, `DO`, `FR`, `SA`, `SO`)
|
||||
|
||||
### 1.2. DTOs
|
||||
|
||||
```
|
||||
src/Logic/Essen/Dto/
|
||||
├── CreateDishRequest.php // name, notiz?, wochentag[]?
|
||||
├── UpdateDishRequest.php // id, name, notiz?, wochentag[]?, sort_order?
|
||||
├── DeleteDishRequest.php // id
|
||||
├── MoveDishRequest.php // id, position (int)
|
||||
├── ToggleDishStatusRequest.php // id
|
||||
├── ListDishesRequest.php // status? (optional), sort='sort_order'|'gegessen_am'
|
||||
├── ListDishesResponse.php // items: Dish[]
|
||||
├── MahlzeitCreateRequest.php // name, notiz?, wochentage?: Wochentag[]
|
||||
├── MahlzeitUpdateRequest.php // name, notiz?, wochentage? (sort_order via M3-Reorder)
|
||||
├── MahlzeitMoveRequest.php // id, position (int)
|
||||
├── MahlzeitToggleStatusRequest.php // id
|
||||
├── MahlzeitListRequest.php // status? (optional), sort='sort_order'|'gegessen_am'
|
||||
├── MahlzeitListResponse.php // items: Mahlzeit[]
|
||||
├── MahlzeitCreateResponse.php // id, mahlzeit
|
||||
├── MahlzeitUpdateResponse.php // id, mahlzeit
|
||||
├── InspirationRequest.php // limit=5, offset=0, excludeIds: int[]
|
||||
├── InspirationResponse.php // suggestions: Dish[]
|
||||
└── TodayResponse.php // dishes: Dish[] // optional HA-Consumer
|
||||
├── InspirationResponse.php // suggestions: Mahlzeit[]
|
||||
└── TodayResponse.php // mahlzeiten: Mahlzeit[] // optional HA-Consumer
|
||||
```
|
||||
|
||||
Alle `readonly`, native Typisierung, **keine** Attribute/Constraints.
|
||||
@@ -107,43 +125,43 @@ Alle `readonly`, native Typisierung, **keine** Attribute/Constraints.
|
||||
|
||||
```
|
||||
src/Logic/Essen/UseCase/
|
||||
├── CreateDishUseCase.php
|
||||
├── UpdateDishUseCase.php
|
||||
├── DeleteDishUseCase.php
|
||||
├── MoveDishUseCase.php // ändert sort_order (bzw. sort_order-Reihenfolge)
|
||||
└── ToggleDishStatusUseCase.php // markAsGegessen / markAsGeplant
|
||||
├── MahlzeitCreateUseCase.php // execute(MahlzeitCreateRequest): MahlzeitCreateResponse
|
||||
├── MahlzeitUpdateUseCase.php // execute(int $id, MahlzeitUpdateRequest): MahlzeitUpdateResponse
|
||||
├── MahlzeitDeleteUseCase.php // execute(int $id): void
|
||||
├── MahlzeitMoveUseCase.php // ändert sort_order
|
||||
└── MahlzeitToggleStatusUseCase.php // markAsGegessen / markAsGeplant
|
||||
```
|
||||
|
||||
**Verfahren (schlau aus Referenz Sektion 1 & 3):**
|
||||
1. Syntaktische Validierung im UI-Layer (Controller/Command) als **rein
|
||||
PHP** (keine Symfony-Validator-Constraints auf DTOs).
|
||||
2. Business-Validierung im UseCase + im Model-Konstruktor (Invarianzen).
|
||||
3. Persistence im UseCase via `DishManager` → `DishProcessor`
|
||||
3. Persistence im UseCase via `MahlzeitManager` → `MahlzeitProcessor`
|
||||
(Write); Cache-Sidecar-Invalidierung im Manager.
|
||||
4. Event-Dispatch nach erfolgreichem Commit (Bridge in Data-Layer).
|
||||
5. Response-DTO (z. B. `UpdateDishResponse`) zurück an Controller.
|
||||
5. Response-DTO (z. B. `MahlzeitUpdateResponse`) zurück an Controller.
|
||||
|
||||
**Beispiel** (UseCase `CreateDishUseCase`):
|
||||
**Beispiel** (UseCase `MahlzeitCreateUseCase`):
|
||||
|
||||
```php
|
||||
namespace App\Logic\Essen\UseCase;
|
||||
|
||||
readonly class CreateDishUseCase
|
||||
readonly class MahlzeitCreateUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private TransactionManagerInterface $tx,
|
||||
private DishManagerInterface $dishes,
|
||||
private DishModelFactory $factory,
|
||||
private MahlzeitManagerInterface $mahlzeiten,
|
||||
private MahlzeitFactory $factory,
|
||||
private EventDispatcherInterface $events,
|
||||
) {}
|
||||
|
||||
public function execute(CreateDishRequest $req): CreateDishResponse
|
||||
public function execute(MahlzeitCreateRequest $req): MahlzeitCreateResponse
|
||||
{
|
||||
return $this->tx->execute(function() use ($req): CreateDishResponse {
|
||||
$dish = $this->factory->createFromRequest($req); // DTO -> Model
|
||||
$saved = $this->dishes->create($dish); // Model -> Processor
|
||||
$this->events->dispatch(new DishCreatedEvent(id: $saved->id));
|
||||
return new CreateDishResponse(id: $saved->id, dish: $saved);
|
||||
return $this->tx->execute(function() use ($req): MahlzeitCreateResponse {
|
||||
$mahlzeit = $this->factory->createFromCreateRequest($req); // DTO -> Model
|
||||
$gespeichert = $this->mahlzeiten->create($mahlzeit); // Model -> Processor
|
||||
$this->events->dispatch(new MahlzeitCreatedEvent(id: $gespeichert->id));
|
||||
return new MahlzeitCreateResponse(id: $gespeichert->id, mahlzeit: $gespeichert);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -153,8 +171,8 @@ readonly class CreateDishUseCase
|
||||
|
||||
```
|
||||
src/Logic/Essen/Query/
|
||||
├── ListDishesQuery.php // -> ListDishesResponse
|
||||
├── GetDishQuery.php // -> Dish (optional)
|
||||
├── MahlzeitListQuery.php // -> MahlzeitListResponse
|
||||
├── MahlzeitGetQuery.php // -> Mahlzeit (optional)
|
||||
├── TodayQuery.php // -> TodayResponse (HA-Consumer / optional)
|
||||
└── InspirationQuery.php // -> InspirationResponse (5er-Batch + Offset)
|
||||
```
|
||||
@@ -166,28 +184,28 @@ readonly class InspirationQuery
|
||||
{
|
||||
public function __construct(
|
||||
private InspirationWeightSettings $settings, // w_slow/w_rare/w_hot
|
||||
private DishManagerInterface $dishes,
|
||||
private DishHistoryProviderInterface $history, // aus Data-Layer
|
||||
private MahlzeitManagerInterface $mahlzeiten,
|
||||
private MahlzeitHistoryProviderInterface $history, // aus Data-Layer
|
||||
) {}
|
||||
|
||||
public function execute(InspirationRequest $req): InspirationResponse
|
||||
{
|
||||
$all = $this->dishes->listGegessen();
|
||||
$history = $this->history->historyByDishName();
|
||||
$all = $this->mahlzeiten->listGegessen();
|
||||
$history = $this->history->historyByMahlzeitName();
|
||||
|
||||
$scored = $all
|
||||
->filter(fn(Dish $d) => !in_array($d->id, $req->excludeIds, true))
|
||||
->map(fn(Dish $d) => new Scored(
|
||||
dish: $d,
|
||||
score: $this->score($d, $history),
|
||||
->filter(fn(Mahlzeit $m) => !in_array($m->id, $req->excludeIds, true))
|
||||
->map(fn(Mahlzeit $m) => new Scored(
|
||||
mahlzeit: $m,
|
||||
score: $this->score($m, $history),
|
||||
))
|
||||
->sortDescByScore()
|
||||
->slice($req->offset, $req->limit);
|
||||
|
||||
return new InspirationResponse(suggestions: $scored->map(fn($s)=>$s->dish));
|
||||
return new InspirationResponse(suggestions: $scored->map(fn($s)=>$s->mahlzeit));
|
||||
}
|
||||
|
||||
private function score(Dish $d, array $h): float {
|
||||
private function score(Mahlzeit $m, array $h): float {
|
||||
// w_slow * staleness + w_rare * rarity + w_hot * popularity
|
||||
// (Details: ADR-006)
|
||||
}
|
||||
@@ -198,24 +216,24 @@ readonly class InspirationQuery
|
||||
|
||||
```
|
||||
src/Logic/Essen/Manager/
|
||||
├── DishManagerInterface.php
|
||||
└── DishManager.php // Read + Write-Koordination, Cache-Aside (vorerst
|
||||
├── MahlzeitManagerInterface.php
|
||||
└── MahlzeitManager.php // Read + Write-Koordination, Cache-Aside (vorerst
|
||||
// ohne Cache, Platzhalter für Phase 2)
|
||||
```
|
||||
|
||||
Interface (in Logic):
|
||||
|
||||
```php
|
||||
interface DishManagerInterface
|
||||
interface MahlzeitManagerInterface
|
||||
{
|
||||
public function create(Dish $dish): Dish;
|
||||
public function update(Dish $dish): Dish;
|
||||
public function create(Mahlzeit $mahlzeit): Mahlzeit;
|
||||
public function update(Mahlzeit $mahlzeit): Mahlzeit;
|
||||
public function delete(int $id): void;
|
||||
public function move(Dish $dish, int $newSortOrder): void;
|
||||
public function find(int $id): Dish;
|
||||
/** @return Dish[] */
|
||||
public function move(Mahlzeit $mahlzeit, int $newSortOrder): void;
|
||||
public function find(int $id): Mahlzeit; // -> MahlzeitNotFoundException (404)
|
||||
/** @return list<Mahlzeit> */
|
||||
public function listGegessen(): array;
|
||||
/** @return Dish[] */
|
||||
/** @return list<Mahlzeit> */
|
||||
public function listGeplant(): array;
|
||||
public function invalidateCache(): void;
|
||||
}
|
||||
@@ -225,8 +243,8 @@ interface DishManagerInterface
|
||||
|
||||
```
|
||||
src/Logic/Essen/
|
||||
├── DishProviderInterface.php // Read: find, listGegessen, listGeplant, countByName...
|
||||
└── DishProcessorInterface.php // Write: create, save, delete; move (sort_order)
|
||||
├── MahlzeitProviderInterface.php // Read: find, listGegessen, listGeplant, countByName...
|
||||
└── MahlzeitProcessorInterface.php // Write: create, save, delete; move (sort_order)
|
||||
```
|
||||
|
||||
Implementierungen liegen in `src/Data/Essen/Provider/` bzw. `Processor/`.
|
||||
@@ -234,10 +252,10 @@ Implementierungen liegen in `src/Data/Essen/Provider/` bzw. `Processor/`.
|
||||
### 1.7. Model-Factory (DTO -> Model)
|
||||
|
||||
```
|
||||
src/Logic/Essen/Mapping/DishModelFactory.php
|
||||
├── createFromRequest(CreateDishRequest): Dish
|
||||
├── updateFromRequest(UpdateDishRequest): Dish
|
||||
└── rebuildFromPartialUpdate(Dish $current, UpdateDishRequest $req): Dish
|
||||
src/Logic/Essen/Mapping/MahlzeitFactory.php
|
||||
├── createFromCreateRequest(MahlzeitCreateRequest): Mahlzeit
|
||||
├── updateFromUpdateRequest(Mahlzeit, MahlzeitUpdateRequest): Mahlzeit
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 1.8. Domänen-Exceptions
|
||||
@@ -252,7 +270,7 @@ src/Logic/Common/Exception/
|
||||
└── InfrastructureException.php // -> 503
|
||||
|
||||
src/Logic/Essen/Exception/
|
||||
└── DishNotFoundException.php // erbt ResourceNotFoundException
|
||||
└── MahlzeitNotFoundException.php // erbt ResourceNotFoundException
|
||||
```
|
||||
|
||||
**HTTP-Mapping** erfolgt zentral in `src/UI/Http/ExceptionSubscriber.php`
|
||||
@@ -275,59 +293,59 @@ src/Logic/Common/
|
||||
|
||||
```
|
||||
src/Data/Essen/
|
||||
├── Entity/DishEntity.php // #[ORM\Entity] – Doctrine, Attribute erlaubt!
|
||||
├── Provider/DishProvider.php // implementiert DishProviderInterface
|
||||
├── Processor/DishProcessor.php // implementiert DishProcessorInterface
|
||||
├── Mapper/DishMapper.php // toModel / createEntity / updateEntity
|
||||
└── HistoryProvider/DishHistoryProvider.php // (optional, für Inspiration)
|
||||
├── Entity/MahlzeitEntity.php // #[ORM\Entity] – Doctrine, Attribute erlaubt!
|
||||
├── Provider/MahlzeitProvider.php // implementiert MahlzeitProviderInterface
|
||||
├── Processor/MahlzeitProcessor.php // implementiert MahlzeitProcessorInterface
|
||||
├── Mapper/MahlzeitMapper.php // toModel / createEntity / updateEntity
|
||||
└── HistoryProvider/MahlzeitHistoryProvider.php // (optional, für Inspiration)
|
||||
|
||||
src/Data/Infrastructure/
|
||||
├── Doctrine/EntityManagerTransactionManager.php
|
||||
└── SymfonyEventDispatcherBridge.php
|
||||
```
|
||||
|
||||
**Doctrine Entity** (`DishEntity`):
|
||||
**Doctrine Entity** (`MahlzeitEntity`):
|
||||
|
||||
```php
|
||||
#[ORM\Entity, ORM\Table(name: 'essens_dishes')]
|
||||
class DishEntity
|
||||
#[ORM\Entity, ORM\Table(name: 'essens_mahlzeiten')]
|
||||
class MahlzeitEntity
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private string $name = '';
|
||||
// (notiz, wochentage (set), sort_order, status, gegessen_am, timestamps)
|
||||
// (notiz, wochentage (CSV-string „mo,di,fr", nullable), sort_order, status (enum), gegessen_am, timestamps)
|
||||
|
||||
// nur Getter/Setter, keine Logik
|
||||
}
|
||||
```
|
||||
|
||||
**Mapper** (stateless):
|
||||
- `toModel(DishEntity): Dish`
|
||||
- `createEntity(Dish): DishEntity`
|
||||
- `updateEntity(Dish, DishEntity): void`
|
||||
- `toModel(MahlzeitEntity): Mahlzeit`
|
||||
- `createEntity(Mahlzeit): MahlzeitEntity`
|
||||
- `updateEntity(Mahlzeit, MahlzeitEntity): void`
|
||||
|
||||
**Processor** (Write):
|
||||
|
||||
```php
|
||||
readonly class DishProcessor implements DishProcessorInterface
|
||||
readonly class MahlzeitProcessor implements MahlzeitProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private DishMapper $mapper,
|
||||
private MahlzeitMapper $mapper,
|
||||
) {}
|
||||
|
||||
public function create(Dish $model): Dish {
|
||||
public function create(Mahlzeit $model): Mahlzeit {
|
||||
$entity = $this->mapper->createEntity($model);
|
||||
$this->em->persist($entity);
|
||||
$this->em->flush();
|
||||
return $this->mapper->toModel($entity); // mit id aus DB
|
||||
}
|
||||
|
||||
public function save(Dish $model): Dish {
|
||||
$entity = $this->em->getRepository(DishEntity::class)->find($model->id);
|
||||
if ($entity === null) throw new DishNotFoundException($model->id);
|
||||
public function save(Mahlzeit $model): Mahlzeit {
|
||||
$entity = $this->em->getRepository(MahlzeitEntity::class)->find($model->id);
|
||||
if ($entity === null) throw new MahlzeitNotFoundException($model->id);
|
||||
$this->mapper->updateEntity($model, $entity);
|
||||
$this->em->flush();
|
||||
return $model;
|
||||
@@ -340,13 +358,13 @@ readonly class DishProcessor implements DishProcessorInterface
|
||||
|
||||
```
|
||||
src/UI/Http/Essen/
|
||||
├── DishController.php // CRUD, toggle, move (sort_order)
|
||||
├── MahlzeitController.php // CRUD, toggle, move (sort_order)
|
||||
├── InspirationController.php // 5er-Batch
|
||||
├── TodayController.php // optional: HA-Consumer (GET /api/today)
|
||||
└── ExceptionSubscriber.php // zentrales Domain-Exception → HTTP-Mapping
|
||||
|
||||
src/UI/Twig/ // Twig-Templates (htmx-Fragmente)
|
||||
├── dish/
|
||||
templates/ // Twig-Templates (htmx-Fragmente)
|
||||
├── mahlzeiten/
|
||||
│ ├── index.html.twig // Planungsliste + Archiv-Tab
|
||||
│ ├── form.html.twig // Erstellen/Bearbeiten
|
||||
│ └── item.html.twig // Eintrag (li) – SortableJS
|
||||
@@ -369,19 +387,19 @@ eingeblendet. Kein Build-Step.
|
||||
```
|
||||
tests/
|
||||
├── Unit/Logic/ // Pure PHP, ohne Kernel (schnell)
|
||||
│ ├── Essen/Model/DishTest.php
|
||||
│ ├── Essen/UseCase/CreateDishUseCaseTest.php
|
||||
│ ├── Essen/Model/MahlzeitTest.php
|
||||
│ ├── Essen/UseCase/MahlzeitCreateUseCaseTest.php
|
||||
│ ├── Essen/Query/InspirationQueryTest.php // mit gemocktem Provider
|
||||
│ └── Essen/Mapping/DishModelFactoryTest.php
|
||||
│ └── Essen/Mapping/MahlzeitFactoryTest.php
|
||||
├── Integration/Data/ // Test-Env + SQLite (oder in-memory)
|
||||
│ ├── Essen/DishProviderTest.php
|
||||
│ ├── Essen/DishProcessorTest.php
|
||||
│ └── Essen/DishMapperTest.php
|
||||
│ ├── Essen/MahlzeitProviderTest.php
|
||||
│ ├── Essen/MahlzeitProcessorTest.php
|
||||
│ └── Essen/MahlzeitMapperTest.php
|
||||
├── Functional/UI/ // Symfony WebTestCase, isolierte Endpunkte
|
||||
│ ├── Essen/DishControllerTest.php
|
||||
│ ├── Essen/MahlzeitControllerTest.php
|
||||
│ └── Essen/ExceptionSubscriberTest.php
|
||||
└── Functional/Scenarios/ // User-Flows
|
||||
└── EssenDishLifecycleTest.php // create → toggle → archive → restore
|
||||
└── MahlzeitLifecycleTest.php // create → toggle → archive → restore
|
||||
```
|
||||
|
||||
**Mocking-Richtlinien** (aus Referenz Sektion 13):
|
||||
@@ -420,25 +438,25 @@ svc-essensplanung/
|
||||
│ │ │ ├── TransactionManagerInterface.php
|
||||
│ │ │ └── EventDispatcherInterface.php
|
||||
│ │ └── Essen/
|
||||
│ │ ├── Model/ (Dish, DishDay, DishStatus)
|
||||
│ │ ├── Model/ (Mahlzeit, Wochentag, MahlzeitStatus)
|
||||
│ │ ├── Dto/ (10 Dateien)
|
||||
│ │ ├── UseCase/ (5)
|
||||
│ │ ├── Query/ (4)
|
||||
│ │ ├── Manager/ (interface + impl)
|
||||
│ │ ├── DishProviderInterface.php
|
||||
│ │ ├── DishProcessorInterface.php
|
||||
│ │ ├── Mapping/DishModelFactory.php
|
||||
│ │ └── Event/DishCreatedEvent.php, DishArchivedEvent.php, ...
|
||||
│ │ ├── MahlzeitProviderInterface.php
|
||||
│ │ ├── MahlzeitProcessorInterface.php
|
||||
│ │ ├── Mapping/MahlzeitFactory.php
|
||||
│ │ └── Event/MahlzeitCreatedEvent.php, MahlzeitArchivedEvent.php, ...
|
||||
│ ├── Data/
|
||||
│ │ ├── Infrastructure/
|
||||
│ │ │ ├── Doctrine/EntityManagerTransactionManager.php
|
||||
│ │ │ └── SymfonyEventDispatcherBridge.php
|
||||
│ │ └── Essen/
|
||||
│ │ ├── Entity/DishEntity.php
|
||||
│ │ ├── Provider/DishProvider.php
|
||||
│ │ ├── Processor/DishProcessor.php
|
||||
│ │ ├── Mapper/DishMapper.php
|
||||
│ │ └── HistoryProvider/DishHistoryProvider.php
|
||||
│ │ ├── Entity/MahlzeitEntity.php
|
||||
│ │ ├── Provider/MahlzeitProvider.php
|
||||
│ │ ├── Processor/MahlzeitProcessor.php
|
||||
│ │ ├── Mapper/MahlzeitMapper.php
|
||||
│ │ └── HistoryProvider/MahlzeitHistoryProvider.php
|
||||
│ └── UI/
|
||||
│ ├── Http/Essen/ (3 Controllers + ExceptionSubscriber)
|
||||
│ └── Twig/ (Layout + Fragmente)
|
||||
@@ -459,28 +477,28 @@ svc-essensplanung/
|
||||
## 3. Dependency Graph (konkret)
|
||||
|
||||
```
|
||||
UI: DishController
|
||||
-> Logic: CreateDishUseCase
|
||||
-> Logic: MoveDishUseCase
|
||||
-> Logic: ToggleDishStatusUseCase
|
||||
-> Logic: ListDishesQuery
|
||||
UI: MahlzeitController
|
||||
-> Logic: MahlzeitCreateUseCase
|
||||
-> Logic: MahlzeitMoveUseCase
|
||||
-> Logic: MahlzeitToggleStatusUseCase
|
||||
-> Logic: MahlzeitListQuery
|
||||
-> Logic: InspirationQuery
|
||||
-> Logic DTOs (CreateDishRequest, MoveDishRequest, ...)
|
||||
-> Logic DTOs (MahlzeitCreateRequest, MahlzeitMoveRequest, ...)
|
||||
|
||||
Logic: CreateDishUseCase
|
||||
Logic: MahlzeitCreateUseCase
|
||||
-> Logic: Common TransactionManagerInterface
|
||||
-> Logic: Essen DishManagerInterface
|
||||
-> Logic: Essen Mapping/DishModelFactory
|
||||
-> Logic: Essen MahlzeitManagerInterface
|
||||
-> Logic: Essen Mapping/MahlzeitFactory
|
||||
-> Logic: Common EventDispatcherInterface
|
||||
-> Logic: Essen/Event DishCreatedEvent
|
||||
-> Logic: Essen/Event MahlzeitCreatedEvent
|
||||
|
||||
Logic: DishManager
|
||||
-> Logic: Essen DishProviderInterface (Read)
|
||||
-> Logic: Essen DishProcessorInterface (Write)
|
||||
Logic: MahlzeitManager
|
||||
-> Logic: Essen MahlzeitProviderInterface (Read)
|
||||
-> Logic: Essen MahlzeitProcessorInterface (Write)
|
||||
|
||||
Data: DishProvider implements Logic:DishProviderInterface
|
||||
Data: DishProcessor implements Logic:DishProcessorInterface
|
||||
Data: DishMapper (Entity <-> Model, stateless)
|
||||
Data: MahlzeitProvider implements Logic:MahlzeitProviderInterface
|
||||
Data: MahlzeitProcessor implements Logic:MahlzeitProcessorInterface
|
||||
Data: MahlzeitMapper (Entity <-> Model, stateless)
|
||||
Data: EntityManagerTransactionManager implements Logic:Common TransactionManagerInterface
|
||||
Data: SymfonyEventDispatcherBridge implements Logic:Common EventDispatcherInterface
|
||||
```
|
||||
@@ -519,7 +537,7 @@ Data: SymfonyEventDispatcherBridge implements Logic:Common EventDispatcherIn
|
||||
## 6. Offene / Phase-2-Punkte
|
||||
|
||||
- [ ] `settings.yaml` für Inspiration-Gewichte (ADR-006). Default 0.5/0.3/0.2.
|
||||
- [ ] `DishHistoryProvider` konkret (für Inspiration, zählt Auftritte pro Name).
|
||||
- [ ] `MahlzeitHistoryProvider` konkret (für Inspiration, zählt Auftritte pro Name).
|
||||
- [ ] `GET /api/today` für HA-Consumer (Phase 2, ADR-004).
|
||||
- [ ] Optional: `Push`-Benachrichtigung per HA-Notify (Phase 2).
|
||||
- [ ] Optional: Cache-Aside im `DishManager` (vorerst ohne, Phase 2).
|
||||
- [ ] Optional: Cache-Aside im `MahlzeitManager` (vorerst ohne, Phase 2).
|
||||
|
||||
Reference in New Issue
Block a user