Files
svc-essensplanung/Doku/architektur.md
T
jens 1589088379 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).
2026-09-19 08:55:54 +02:00

544 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 + SQLite (oder in-memory)
│ ├── 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).