api: Mahlzeiten GET/POST + checkedIn/checkedOut (X-Api-Token)

This commit is contained in:
2026-09-25 19:06:08 +02:00
parent ac4a2af7cb
commit 9c148a1242
4 changed files with 143 additions and 0 deletions
+10
View File
@@ -946,6 +946,15 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* twig?: TwigConfig,
* doctrine?: DoctrineConfig,
* doctrine_migrations?: DoctrineMigrationsConfig,
* "when@dev"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
* services?: ServicesConfig,
* framework?: FrameworkConfig,
* twig?: TwigConfig,
* doctrine?: DoctrineConfig,
* doctrine_migrations?: DoctrineMigrationsConfig,
* },
* "when@prod"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
@@ -1046,6 +1055,7 @@ namespace Symfony\Component\Routing\Loader\Configurator;
* deprecated?: array{package:string, version:string, message?:string},
* }
* @psalm-type RoutesConfig = array{
* "when@dev"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
* "when@prod"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
* "when@test"?: array<string, RouteConfig|ImportConfig|AliasConfig>,
* ...<string, RouteConfig|ImportConfig|AliasConfig>
+3
View File
@@ -1,5 +1,8 @@
# yaml-language-server: $schema=https://symfony.com/schema/config/routing/routing.yaml
import:
resource: routes/api.yaml
home:
path: /
controller: App\UI\Http\HomeController::index
+26
View File
@@ -0,0 +1,26 @@
# yaml-language-server: $schema=https://symfony.com/schema/config/routing/routing.yaml
# Stateless-JSON-API für HA & Co. (Auth: X-Api-Token, s. ApiTokenSubscriber)
api_mahlzeiten_liste:
path: /api/mahlzeiten
controller: App\UI\Http\Api\MahlzeitApiController::index
methods: [GET]
api_mahlzeiten_create:
path: /api/mahlzeiten
controller: App\UI\Http\Api\MahlzeitApiController::create
methods: [POST]
api_mahlzeiten_checkoff:
path: /api/mahlzeiten/{id}/checkoff
controller: App\UI\Http\Api\MahlzeitApiController::checkoff
methods: [POST]
requirements:
id: \d+
api_mahlzeiten_uncheck:
path: /api/mahlzeiten/{id}/uncheck
controller: App\UI\Http\Api\MahlzeitApiController::uncheck
methods: [POST]
requirements:
id: \d+
+104
View File
@@ -0,0 +1,104 @@
<?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),
];
}
}