Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.33% covered (success)
98.33%
59 / 60
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiMenuController
98.33% covered (success)
98.33%
59 / 60
83.33% covered (warning)
83.33%
5 / 6
20
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 list
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 save
96.77% covered (success)
96.77%
30 / 31
0.00% covered (danger)
0.00%
0 / 1
9
 delete
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 jsonResponse
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace App\Web\Controller\Api;
6
7use App\Domain\Menu\Entity\MenuRecord;
8use App\Domain\Menu\Repository\MenuRecordRepository;
9use App\Domain\Menu\Service\MenuService;
10use Psr\Http\Message\ResponseFactoryInterface;
11use Psr\Http\Message\ResponseInterface;
12use Psr\Http\Message\ServerRequestInterface;
13
14 class ApiMenuController
15{
16    public function __construct(
17        private MenuRecordRepository $menuRecordRepository,
18        private MenuService $menuService,
19        private ResponseFactoryInterface $responseFactory
20    ) {}
21
22    public function list(): ResponseInterface
23    {
24        $tree = $this->menuService->getMenuTree();
25        $records = $this->menuRecordRepository->findAll();
26
27        $data = [];
28        foreach ($records as $m) {
29            $data[] = [
30                'id' => $m->getId(),
31                'type' => $m->getType(),
32                'title' => $m->getTitle(),
33                'icon' => $m->getIcon(),
34                'url' => $m->getUrl(),
35                'badge_text' => $m->getBadgeText(),
36                'badge_color' => $m->getBadgeColor(),
37                'target' => $m->getTarget(),
38                'status' => $m->getStatus(),
39                'created_at' => $m->getCreatedAt(),
40                'updated_at' => $m->getUpdatedAt(),
41            ];
42        }
43
44        return $this->jsonResponse(['success' => true, 'tree' => $tree, 'records' => $data]);
45    }
46
47    public function save(ServerRequestInterface $request): ResponseInterface
48    {
49        $stream = $request->getBody();
50        if ($stream->isSeekable()) {
51            $stream->rewind();
52        }
53        $rawBody = (string) $stream;
54        $parsedBody = $request->getParsedBody();
55        $parsed = is_array($parsedBody) ? $parsedBody : json_decode($rawBody, true);
56        /** @var array<string, mixed> $body */
57        $body = is_array($parsed) ? $parsed : [];
58
59        $id = $this->getString($body, 'id', 'menu-' . bin2hex(random_bytes(4)));
60        $type = $this->getString($body, 'type', 'element');
61        $title = $this->getString($body, 'title', 'New Item');
62        $icon = $this->getString($body, 'icon', 'bi-circle');
63        $url = $this->getString($body, 'url', '#');
64        $badgeText = $this->getString($body, 'badge_text', '');
65        $badgeColor = $this->getString($body, 'badge_color', 'primary');
66        $target = $this->getString($body, 'target', '_self');
67        $status = $this->getString($body, 'status', 'active');
68
69        $record = new MenuRecord(
70            id: $id,
71            type: $type,
72            title: $title,
73            icon: $icon !== '' ? $icon : null,
74            url: $url !== '' ? $url : null,
75            badgeText: $badgeText !== '' ? $badgeText : null,
76            badgeColor: $badgeColor !== '' ? $badgeColor : null,
77            target: $target !== '' ? $target : null,
78            status: $status,
79            createdAt: time(),
80            updatedAt: time()
81        );
82        $this->menuRecordRepository->save($record);
83
84        return $this->jsonResponse(['success' => true, 'id' => $id]);
85    }
86
87    public function delete(ServerRequestInterface $request): ResponseInterface
88    {
89        /** @var array<string, mixed> $queryParams */
90        $queryParams = $request->getQueryParams();
91        $id = isset($queryParams['id']) && is_string($queryParams['id']) ? $queryParams['id'] : '';
92
93        if ($id !== '') {
94            $this->menuRecordRepository->delete($id);
95        }
96
97        return $this->jsonResponse(['success' => true]);
98    }
99
100    /**
101     * @param array<string, mixed> $data
102     */
103    private function jsonResponse(array $data): ResponseInterface
104    {
105        $response = $this->responseFactory->createResponse(200)
106            ->withHeader('Content-Type', 'application/json');
107        $response->getBody()->write((string) json_encode($data, JSON_THROW_ON_ERROR));
108        return $response;
109    }
110
111    /**
112     * @param array<string, mixed> $data
113     */
114    private function getString(array $data, string $key, string $default): string
115    {
116        return isset($data[$key]) && is_string($data[$key]) ? trim($data[$key]) : $default;
117    }
118}