105 lines
3.7 KiB
PHP
105 lines
3.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\UI\Http\Api;
|
||
|
||
use App\Logic\Essen\Dto\MahlzeitCreateRequest;
|
||
use App\Logic\Essen\Dto\MahlzeitListRequest;
|
||
use App\Logic\Essen\Model\MahlzeitStatus;
|
||
use App\Logic\Essen\Model\Wochentag;
|
||
use App\Logic\Essen\Query\MahlzeitListQuery;
|
||
use App\Logic\Essen\UseCase\MahlzeitCheckOffUseCase;
|
||
use App\Logic\Essen\UseCase\MahlzeitCreateUseCase;
|
||
use App\Logic\Essen\UseCase\MahlzeitUncheckUseCase;
|
||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||
use Symfony\Component\HttpFoundation\Request;
|
||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||
|
||
/**
|
||
* Stateless JSON-API für Consumer wie Home Assistant (Auth via `X-Api-Token`,
|
||
* s. `ApiTokenSubscriber`).
|
||
*
|
||
* - `GET /api/mahlzeiten` – Liste, optional `?status=geplant|gegessen`.
|
||
* - `POST /api/mahlzeiten/{id}/checkoff` – abhaken (geplant → gegessen).
|
||
* - `POST /api/mahlzeiten/{id}/uncheck` – aufheben (gegessen → geplant).
|
||
* - `POST /api/mahlzeiten` – anlegen (JSON `name` Pflicht, `notiz`/`wochentage` optional).
|
||
*
|
||
* Fachregeln laufen über dieselben UseCases/Queries wie die Web-UI;
|
||
* Fehler werden vom `ExceptionSubscriber` als JSON gerendert.
|
||
*/
|
||
final readonly class MahlzeitApiController
|
||
{
|
||
public function __construct(
|
||
private MahlzeitListQuery $list,
|
||
private MahlzeitCheckOffUseCase $checkOff,
|
||
private MahlzeitUncheckUseCase $uncheck,
|
||
private MahlzeitCreateUseCase $create,
|
||
) {}
|
||
|
||
/** @param non-empty-string $name – für die Route-Signatur, hier nur Doku */
|
||
public function index(Request $request): JsonResponse
|
||
{
|
||
$status = null;
|
||
$filter = (string) $request->query->get('status', '');
|
||
if ($filter !== '') {
|
||
$status = MahlzeitStatus::tryFrom($filter);
|
||
if ($status === null) {
|
||
throw new BadRequestHttpException('status muss "geplant" oder "gegessen" sein.');
|
||
}
|
||
}
|
||
|
||
$items = $this->list->execute(new MahlzeitListRequest(status: $status))->items;
|
||
|
||
return new JsonResponse([
|
||
'mahlzeiten' => \array_map(self::serialize(...), $items),
|
||
]);
|
||
}
|
||
|
||
public function checkoff(int $id): JsonResponse
|
||
{
|
||
return new JsonResponse(self::serialize($this->checkOff->execute($id)));
|
||
}
|
||
|
||
public function uncheck(int $id): JsonResponse
|
||
{
|
||
return new JsonResponse(self::serialize($this->uncheck->execute($id)));
|
||
}
|
||
|
||
public function create(Request $request): JsonResponse
|
||
{
|
||
$payload = json_decode((string) $request->getContent(), true);
|
||
if (!\is_array($payload)) {
|
||
throw new BadRequestHttpException('JSON-Body erwartet, z. B. {"name": "Bockbrot"}');
|
||
}
|
||
|
||
$name = \trim((string) ($payload['name'] ?? ''));
|
||
if ($name === '') {
|
||
throw new BadRequestHttpException('name ist ein Pflichtfeld.');
|
||
}
|
||
|
||
$notiz = \trim((string) ($payload['notiz'] ?? ''));
|
||
$wochentage = Wochentag::ausWerten((array) ($payload['wochentage'] ?? []));
|
||
|
||
return new JsonResponse(self::serialize($this->create->execute(new MahlzeitCreateRequest(
|
||
name: $name,
|
||
notiz: $notiz,
|
||
wochentage: $wochentage,
|
||
))->mahlzeit), 201);
|
||
}
|
||
|
||
/** Serialisiert ein Read-Modell in das API-Format. */
|
||
public static function serialize(object $m): array
|
||
{
|
||
return [
|
||
'id' => $m->id,
|
||
'name' => $m->name,
|
||
'notiz' => $m->notiz,
|
||
'wochentage' => \array_map(static fn (Wochentag $t): string => $t->value, $m->wochentage),
|
||
'status' => $m->status->value,
|
||
'sort_order' => $m->sortOrder,
|
||
'gegessen_am' => $m->gegessenAm?->format(\DateTimeInterface::RFC3339),
|
||
];
|
||
}
|
||
}
|