544 lines
22 KiB
Markdown
544 lines
22 KiB
Markdown
# Architektur – svc-essensplanung
|
||
|
||
> 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)
|
||
|
||
```
|
||
+-------------+ +--------------------------+ +--------------------------------------+
|
||
| UI | | Logic | | Data |
|
||
| (src/UI) | --> | (src/Logic) | --> | (src/Data) |
|
||
├-------------┤ ├──────────────────────────┤ ├────────────────────────────────────────┤
|
||
│ Controllers │ │ UseCases (Writes) | │ Entities (Doctrine) │
|
||
│ Commands │ │ Queries (Reads) | │ Mappers (Entity <-> Model) │
|
||
│ Templates | │ Managers (per Model) | │ Providers (Read) │
|
||
│ (Twig) | │ Models (POPO, Rich) | │ Processors (Write) │
|
||
│ Exception | │ DTOs (readonly) | │ Transaction-Manager-Bridge │
|
||
│ Subscriber | │ Exceptions (Domain) | │ EventDispatcher-Bridge │
|
||
└-------------┘ │ Mapping (Model-Factory) | │ │
|
||
| Provider/Processor Ifcs | │ │
|
||
└──────────────────────────┘ └────────────────────────────────────────┘
|
||
```
|
||
|
||
**Abhängigkeitsregel (Dependency Inversion):**
|
||
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)** — 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: `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 `Mahlzeit`
|
||
|
||
`src/Logic/Essen/Model/Mahlzeit.php` – **Rich Domain Model** (POPO, readonly,
|
||
kein Framework):
|
||
|
||
```php
|
||
namespace App\Logic\Essen\Model;
|
||
|
||
class Mahlzeit
|
||
{
|
||
public function __construct(
|
||
public readonly ?int $id,
|
||
public string $name,
|
||
public ?string $notiz,
|
||
/** @var Wochentag[] */
|
||
public readonly array $wochentage,
|
||
public readonly int $sort_order,
|
||
public MahlzeitStatus $status,
|
||
public readonly ?\DateTimeImmutable $gegessen_am,
|
||
public readonly \DateTimeImmutable $erstellt_am,
|
||
public readonly \DateTimeImmutable $aktualisiert_am,
|
||
) {}
|
||
|
||
// State transitions: kapseln eigene Statusänderungen
|
||
public function markAsGegessen(\DateTimeImmutable $now): self {
|
||
if ($this->status === MahlzeitStatus::GEGESSEN) {
|
||
throw new BusinessRuleViolationException('Bereits gegessen.');
|
||
}
|
||
return new self($this->id, $this->name, $this->notiz, $this->wochentage,
|
||
$this->sort_order, MahlzeitStatus::GEGESSEN, $now, $this->erstellt_am, $now);
|
||
}
|
||
|
||
public function markAsGeplant(\DateTimeImmutable $now): self { /* symmetrisch */ }
|
||
}
|
||
```
|
||
|
||
**Enums** in `src/Logic/Essen/Model/`:
|
||
- `MahlzeitStatus` (`GEPLANT`, `GEGESSEN`)
|
||
- `Wochentag` (`MO`, `DI`, `MI`, `DO`, `FR`, `SA`, `SO`)
|
||
|
||
### 1.2. DTOs
|
||
|
||
```
|
||
src/Logic/Essen/Dto/
|
||
├── 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: Mahlzeit[]
|
||
└── TodayResponse.php // mahlzeiten: Mahlzeit[] // optional HA-Consumer
|
||
```
|
||
|
||
Alle `readonly`, native Typisierung, **keine** Attribute/Constraints.
|
||
|
||
### 1.3. UseCases (Writes)
|
||
|
||
```
|
||
src/Logic/Essen/UseCase/
|
||
├── 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 `MahlzeitManager` → `MahlzeitProcessor`
|
||
(Write); Cache-Sidecar-Invalidierung im Manager.
|
||
4. Event-Dispatch nach erfolgreichem Commit (Bridge in Data-Layer).
|
||
5. Response-DTO (z. B. `MahlzeitUpdateResponse`) zurück an Controller.
|
||
|
||
**Beispiel** (UseCase `MahlzeitCreateUseCase`):
|
||
|
||
```php
|
||
namespace App\Logic\Essen\UseCase;
|
||
|
||
readonly class MahlzeitCreateUseCase
|
||
{
|
||
public function __construct(
|
||
private TransactionManagerInterface $tx,
|
||
private MahlzeitManagerInterface $mahlzeiten,
|
||
private MahlzeitFactory $factory,
|
||
private EventDispatcherInterface $events,
|
||
) {}
|
||
|
||
public function execute(MahlzeitCreateRequest $req): MahlzeitCreateResponse
|
||
{
|
||
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);
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 1.4. Queries (Reads)
|
||
|
||
```
|
||
src/Logic/Essen/Query/
|
||
├── MahlzeitListQuery.php // -> MahlzeitListResponse
|
||
├── MahlzeitGetQuery.php // -> Mahlzeit (optional)
|
||
├── TodayQuery.php // -> TodayResponse (HA-Consumer / optional)
|
||
└── InspirationQuery.php // -> InspirationResponse (5er-Batch + Offset)
|
||
```
|
||
|
||
**InspirationQuery** (Algorithmus, ADR-006):
|
||
|
||
```php
|
||
readonly class InspirationQuery
|
||
{
|
||
public function __construct(
|
||
private InspirationWeightSettings $settings, // w_slow/w_rare/w_hot
|
||
private MahlzeitManagerInterface $mahlzeiten,
|
||
private MahlzeitHistoryProviderInterface $history, // aus Data-Layer
|
||
) {}
|
||
|
||
public function execute(InspirationRequest $req): InspirationResponse
|
||
{
|
||
$all = $this->mahlzeiten->listGegessen();
|
||
$history = $this->history->historyByMahlzeitName();
|
||
|
||
$scored = $all
|
||
->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->mahlzeit));
|
||
}
|
||
|
||
private function score(Mahlzeit $m, array $h): float {
|
||
// w_slow * staleness + w_rare * rarity + w_hot * popularity
|
||
// (Details: ADR-006)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 1.5. Manager (pro Business-Model)
|
||
|
||
```
|
||
src/Logic/Essen/Manager/
|
||
├── MahlzeitManagerInterface.php
|
||
└── MahlzeitManager.php // Read + Write-Koordination, Cache-Aside (vorerst
|
||
// ohne Cache, Platzhalter für Phase 2)
|
||
```
|
||
|
||
Interface (in Logic):
|
||
|
||
```php
|
||
interface MahlzeitManagerInterface
|
||
{
|
||
public function create(Mahlzeit $mahlzeit): Mahlzeit;
|
||
public function update(Mahlzeit $mahlzeit): Mahlzeit;
|
||
public function delete(int $id): void;
|
||
public function move(Mahlzeit $mahlzeit, int $newSortOrder): void;
|
||
public function find(int $id): Mahlzeit; // -> MahlzeitNotFoundException (404)
|
||
/** @return list<Mahlzeit> */
|
||
public function listGegessen(): array;
|
||
/** @return list<Mahlzeit> */
|
||
public function listGeplant(): array;
|
||
public function invalidateCache(): void;
|
||
}
|
||
```
|
||
|
||
### 1.6. Provider-/Processor-Interfaces (in Logic)
|
||
|
||
```
|
||
src/Logic/Essen/
|
||
├── 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/`.
|
||
|
||
### 1.7. Model-Factory (DTO -> Model)
|
||
|
||
```
|
||
src/Logic/Essen/Mapping/MahlzeitFactory.php
|
||
├── createFromCreateRequest(MahlzeitCreateRequest): Mahlzeit
|
||
├── updateFromUpdateRequest(Mahlzeit, MahlzeitUpdateRequest): Mahlzeit
|
||
└── ...
|
||
```
|
||
|
||
### 1.8. Domänen-Exceptions
|
||
|
||
```
|
||
src/Logic/Common/Exception/
|
||
├── DomainException.php // abstract base (RuntimeException)
|
||
├── ResourceNotFoundException.php // -> 404
|
||
├── BusinessRuleViolationException.php // -> 422
|
||
├── ConcurrencyException.php // -> 409 (falls Locking)
|
||
├── AccessDeniedException.php // -> 403
|
||
└── InfrastructureException.php // -> 503
|
||
|
||
src/Logic/Essen/Exception/
|
||
└── MahlzeitNotFoundException.php // erbt ResourceNotFoundException
|
||
```
|
||
|
||
**HTTP-Mapping** erfolgt zentral in `src/UI/Http/ExceptionSubscriber.php`
|
||
(keine try-catch in jedem Controller).
|
||
|
||
### 1.9. Gemeinsame Schnittstellen in `src/Logic/Common/`
|
||
|
||
```
|
||
src/Logic/Common/
|
||
├── TransactionManagerInterface.php // execute(callable): mixed
|
||
├── EventDispatcherInterface.php // dispatch(object $event): void
|
||
└── Exception/ (wie oben)
|
||
```
|
||
|
||
**Bridges in Data** (siehe Sektion 1.10):
|
||
- `src/Data/Infrastructure/Doctrine/EntityManagerTransactionManager.php`
|
||
- `src/Data/Infrastructure/SymfonyEventDispatcherBridge.php`
|
||
|
||
### 1.10. Data-Layer (Konkrete Implementierung)
|
||
|
||
```
|
||
src/Data/Essen/
|
||
├── 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** (`MahlzeitEntity`):
|
||
|
||
```php
|
||
#[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 (CSV-string „mo,di,fr", nullable), sort_order, status (enum), gegessen_am, timestamps)
|
||
|
||
// nur Getter/Setter, keine Logik
|
||
}
|
||
```
|
||
|
||
**Mapper** (stateless):
|
||
- `toModel(MahlzeitEntity): Mahlzeit`
|
||
- `createEntity(Mahlzeit): MahlzeitEntity`
|
||
- `updateEntity(Mahlzeit, MahlzeitEntity): void`
|
||
|
||
**Processor** (Write):
|
||
|
||
```php
|
||
readonly class MahlzeitProcessor implements MahlzeitProcessorInterface
|
||
{
|
||
public function __construct(
|
||
private EntityManagerInterface $em,
|
||
private MahlzeitMapper $mapper,
|
||
) {}
|
||
|
||
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(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;
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 1.11. UI-Layer
|
||
|
||
```
|
||
src/UI/Http/Essen/
|
||
├── 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
|
||
|
||
templates/ // Twig-Templates (htmx-Fragmente)
|
||
├── mahlzeiten/
|
||
│ ├── index.html.twig // Planungsliste + Archiv-Tab
|
||
│ ├── form.html.twig // Erstellen/Bearbeiten
|
||
│ └── item.html.twig // Eintrag (li) – SortableJS
|
||
├── inspiration/
|
||
│ └── panel.html.twig
|
||
└── layout/base.html.twig
|
||
```
|
||
|
||
**htmx** wird als statische Datei ausgeliefert
|
||
(`public/js/htmx.min.js`, `public/js/sortable.min.js`) und in `layout`
|
||
eingeblendet. Kein Build-Step.
|
||
|
||
**Antwort-Vertrag (htmx):**
|
||
- Alle Writes geben ein HTML-Fragment zurück (aktualisierte Liste/Tab), oder
|
||
`Hix-Response-Header: `HX-Reswap` etc. für Fragment-Austausch.
|
||
- Für API-Consumer (HA) parallel ein JSON-Endpunkt `GET /api/today`.
|
||
|
||
### 1.12. Testing
|
||
|
||
```
|
||
tests/
|
||
├── Unit/Logic/ // Pure PHP, ohne Kernel (schnell)
|
||
│ ├── Essen/Model/MahlzeitTest.php
|
||
│ ├── Essen/UseCase/MahlzeitCreateUseCaseTest.php
|
||
│ ├── Essen/Query/InspirationQueryTest.php // mit gemocktem Provider
|
||
│ └── Essen/Mapping/MahlzeitFactoryTest.php
|
||
├── Integration/Data/ // Test-Env + file-basierte SQLite (var/test.db)
|
||
│ ├── Essen/MahlzeitProviderTest.php
|
||
│ ├── Essen/MahlzeitProcessorTest.php
|
||
│ └── Essen/MahlzeitMapperTest.php
|
||
├── Functional/UI/ // Symfony WebTestCase, isolierte Endpunkte
|
||
│ ├── Essen/MahlzeitControllerTest.php
|
||
│ └── Essen/ExceptionSubscriberTest.php
|
||
└── Functional/Scenarios/ // User-Flows
|
||
└── MahlzeitLifecycleTest.php // create → toggle → archive → restore
|
||
```
|
||
|
||
**Mocking-Richtlinien** (aus Referenz Sektion 13):
|
||
- Mocke **Interface**, nie die konkrete Implementierung.
|
||
- Mocke **kein** Business-Model/DTO (eigentlich instanziert).
|
||
- Mocke **keine** 3rd-Party-Libs; brüche in Interfaces.
|
||
|
||
## 2. Directory Structure (End-to-End)
|
||
|
||
```
|
||
svc-essensplanung/
|
||
├── bin/
|
||
│ └── console
|
||
├── config/
|
||
│ ├── bundles.php
|
||
│ ├── packages/
|
||
│ │ ├── doctrine.yaml
|
||
│ │ ├── framework.yaml
|
||
│ │ └── routing.yaml
|
||
│ ├── routes/
|
||
│ │ ├── essen.yaml
|
||
│ │ └── api.yaml # /api/today (HA-Consumer)
|
||
│ ├── services.yaml # (fast nur auto-wire, wenige explizite)
|
||
│ └── inspiration_settings.yaml // w_slow/w_rare/w_hot (ADR-006)
|
||
├── docker/
|
||
│ └── prod_entrypoint.sh
|
||
├── public/
|
||
│ ├── index.php
|
||
│ ├── js/htmx.min.js
|
||
│ ├── js/sortable.min.js
|
||
│ └── images/icon.svg // PWA
|
||
├── src/
|
||
│ ├── Logic/
|
||
│ │ ├── Common/
|
||
│ │ │ ├── Exception/ (6 Klassen, s. 1.8)
|
||
│ │ │ ├── TransactionManagerInterface.php
|
||
│ │ │ └── EventDispatcherInterface.php
|
||
│ │ └── Essen/
|
||
│ │ ├── Model/ (Mahlzeit, Wochentag, MahlzeitStatus)
|
||
│ │ ├── Dto/ (10 Dateien)
|
||
│ │ ├── UseCase/ (5)
|
||
│ │ ├── Query/ (4)
|
||
│ │ ├── Manager/ (interface + impl)
|
||
│ │ ├── MahlzeitProviderInterface.php
|
||
│ │ ├── MahlzeitProcessorInterface.php
|
||
│ │ ├── Mapping/MahlzeitFactory.php
|
||
│ │ └── Event/MahlzeitCreatedEvent.php, MahlzeitArchivedEvent.php, ...
|
||
│ ├── Data/
|
||
│ │ ├── Infrastructure/
|
||
│ │ │ ├── Doctrine/EntityManagerTransactionManager.php
|
||
│ │ │ └── SymfonyEventDispatcherBridge.php
|
||
│ │ └── Essen/
|
||
│ │ ├── Entity/MahlzeitEntity.php
|
||
│ │ ├── Provider/MahlzeitProvider.php
|
||
│ │ ├── Processor/MahlzeitProcessor.php
|
||
│ │ ├── Mapper/MahlzeitMapper.php
|
||
│ │ └── HistoryProvider/MahlzeitHistoryProvider.php
|
||
│ └── UI/
|
||
│ ├── Http/Essen/ (3 Controllers + ExceptionSubscriber)
|
||
│ └── Twig/ (Layout + Fragmente)
|
||
├── templates/ // (oder src/UI/Twig – siehe oben)
|
||
├── migrations/ // (Doctrine Migrations, Auto-Gen)
|
||
├── tests/
|
||
├── public/js/htmx.min.js
|
||
├── public/js/sortable.min.js
|
||
├── docker-compose.yaml // FrankenPHP + Volume /db
|
||
├── Dockerfile
|
||
├── .env.dist
|
||
├── composer.json
|
||
├── symfony.lock
|
||
├── phpunit.xml
|
||
└── README.md
|
||
```
|
||
|
||
## 3. Dependency Graph (konkret)
|
||
|
||
```
|
||
UI: MahlzeitController
|
||
-> Logic: MahlzeitCreateUseCase
|
||
-> Logic: MahlzeitMoveUseCase
|
||
-> Logic: MahlzeitToggleStatusUseCase
|
||
-> Logic: MahlzeitListQuery
|
||
-> Logic: InspirationQuery
|
||
-> Logic DTOs (MahlzeitCreateRequest, MahlzeitMoveRequest, ...)
|
||
|
||
Logic: MahlzeitCreateUseCase
|
||
-> Logic: Common TransactionManagerInterface
|
||
-> Logic: Essen MahlzeitManagerInterface
|
||
-> Logic: Essen Mapping/MahlzeitFactory
|
||
-> Logic: Common EventDispatcherInterface
|
||
-> Logic: Essen/Event MahlzeitCreatedEvent
|
||
|
||
Logic: MahlzeitManager
|
||
-> Logic: Essen MahlzeitProviderInterface (Read)
|
||
-> Logic: Essen MahlzeitProcessorInterface (Write)
|
||
|
||
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
|
||
```
|
||
|
||
## 4. Konventionen für neue Module/Features
|
||
|
||
1. **Neue Domäne**: Neues Ordner-Paar `src/Logic/{Modul}` +
|
||
`src/Data/{Modul}` (gleicher Name, z. B. `Einkaufsliste`).
|
||
2. **Neues Business-Model**: Model-Ordner, eigene Manager-/Provider-/-Processor,
|
||
eigene Exceptions, eigenes `*ModelFactory`.
|
||
3. **Neuer UseCase**: Immer unter `src/Logic/{Modul}/{Feature}/UseCase/`,
|
||
DTOs im `Dto/`, Read in `Query/`.
|
||
4. **Neue Schnittstelle (Provider/Processor)**: Interface in Logic,
|
||
Implementation in Data; Autowiring via Symfony (nichts in `services.yaml`,
|
||
es sei denn mehrere Implementierungen).
|
||
5. **Neue Business-Regel**: Im Model-Konstruktor oder in einer State-Transition-
|
||
Methode – nie im UseCase, wenn sie das Model selbst betrifft.
|
||
6. **Neue Konsole/CLI**: `src/UI/Console/`, UseCase-Delegation wie HTTP.
|
||
7. **Neues Event**: In `src/Logic/{Modul}/{Feature}/Event/`, DTO-Struktur,
|
||
State-Carry (nur was der Subscriber braucht), kein ORM-Wissen.
|
||
8. **Testing**: Jede Unit in `tests/Unit/{Modul}/`, je Layer ein Testfall.
|
||
**Kein** Mock auf konkrete Klassen.
|
||
|
||
## 5. Anti-Pattern (bewusst vermieden)
|
||
|
||
- ❌ Symfony-Validator-Constraints auf Logik-DTOs.
|
||
- ❌ `EntityManagerInterface` in der Logik-Layer.
|
||
- ❌ Doctrine-Entities als Rückgabewert aus der Logic-Layer.
|
||
- ❌ HTTP-Header/Routing-Knowledge in der Logik-Layer.
|
||
- ❌ Try-catch in jedem Controller (zentraler Exception-Subscriber).
|
||
- ❌ Anemic-Entities in der Data-Layer, *die* die Geschäftsregeln tragen.
|
||
- ❌ Mehrere Implementierungen eines Interfaces ohne explizite Bindung
|
||
in `services.yaml` (Ambiguität).
|
||
- ❌ „Service" als catch-all-Name (immer UseCase/Query/Manager).
|
||
|
||
## 6. Offene / Phase-2-Punkte
|
||
|
||
- [ ] `settings.yaml` für Inspiration-Gewichte (ADR-006). Default 0.5/0.3/0.2.
|
||
- [ ] `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 `MahlzeitManager` (vorerst ohne, Phase 2).
|