Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.89% covered (danger)
31.89%
59 / 185
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MenuController
31.89% covered (danger)
31.89%
59 / 185
14.29% covered (danger)
14.29%
1 / 7
545.49
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
 index
86.57% covered (warning)
86.57%
58 / 67
0.00% covered (danger)
0.00%
0 / 1
12.35
 create
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
110
 update
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
132
 delete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 saveSort
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 redirectWithFlash
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace App\Web\Controller;
6
7use App\Domain\Menu\Entity\MenuRecord;
8use App\Domain\Menu\Entity\MenuSort;
9use App\Domain\Menu\Repository\MenuRecordRepository;
10use App\Domain\Menu\Repository\MenuSortRepository;
11use App\Domain\Menu\Service\MenuService;
12use App\Domain\Shared\Query\GridQuerySpecification;
13use App\Infrastructure\Shared\Query\GridQueryBuilder;
14use App\Shared\Host\ActiveHostProvider;
15use App\Shared\Host\TablePrefixResolver;
16use App\Shared\UI\ListView\ListViewData;
17use Psr\Http\Message\ResponseFactoryInterface;
18use Psr\Http\Message\ResponseInterface;
19use Psr\Http\Message\ServerRequestInterface;
20use Yiisoft\Yii\View\Renderer\WebViewRenderer;
21
22 class MenuController
23{
24    private const ROUTE_MENU = '/menu';
25
26    public function __construct(
27        private WebViewRenderer $viewRenderer,
28        private ActiveHostProvider $activeHostProvider,
29        private TablePrefixResolver $prefixResolver,
30        private MenuRecordRepository $recordRepository,
31        private MenuSortRepository $sortRepository,
32        private MenuService $menuService,
33        private ResponseFactoryInterface $responseFactory
34    ) {
35        $this->viewRenderer = $viewRenderer->withControllerName('menu');
36    }
37
38    public function index(
39        ServerRequestInterface $request,
40        GridQueryBuilder $queryBuilder
41    ): ResponseInterface {
42        /** @var array<string, mixed> $queryParams */
43        $queryParams = $request->getQueryParams();
44        $currentView = is_string($queryParams['view'] ?? null) ? (string) $queryParams['view'] : 'list';
45
46        $targetRecordsTable = $this->prefixResolver->resolve('menu_records');
47        $targetSortTable = $this->prefixResolver->resolve('menu_sort');
48
49        if ($currentView === 'summary') {
50            $tree = $this->menuService->getMenuTree($targetRecordsTable, $targetSortTable);
51            $allRecords = $this->recordRepository->findAll($targetRecordsTable);
52
53            return $this->viewRenderer->render('//menu/index.twig', [
54                'currentView' => 'summary',
55                'menuTree' => $tree,
56                'allRecords' => $allRecords,
57                'activeTable' => $targetRecordsTable,
58                'activeHost' => $this->activeHostProvider->getActiveHost(),
59            ]);
60        }
61
62        $allowedColumns = [
63            'type' => 'type',
64            'title' => 'title',
65            'icon' => 'icon',
66            'url' => 'url',
67            'status' => 'status',
68            'created_at' => 'created_at',
69        ];
70
71        $spec = new GridQuerySpecification($queryParams, $allowedColumns, 'created_at', 'DESC');
72        $queryResult = $queryBuilder->fetchData($targetRecordsTable, $spec, 'created_at', 'DESC');
73
74        $rawRows = $queryResult['items'];
75        $totalItems = $queryResult['totalCount'];
76
77        $items = array_map(function ($row) {
78            /** @var array<string, mixed> $row */
79            $createdAt = isset($row['created_at']) && is_numeric($row['created_at']) ? (int) $row['created_at'] : time();
80
81            return [
82                'id' => is_string($row['id'] ?? null) ? (string) $row['id'] : '',
83                'type' => is_string($row['type'] ?? null) ? (string) $row['type'] : 'element',
84                'title' => is_string($row['title'] ?? null) ? (string) $row['title'] : (is_string($row['label'] ?? null) ? (string) $row['label'] : ''),
85                'icon' => is_string($row['icon'] ?? null) ? (string) $row['icon'] : 'bi-circle',
86                'url' => is_string($row['url'] ?? null) ? (string) $row['url'] : '#',
87                'status' => is_string($row['status'] ?? null) ? (string) $row['status'] : 'active',
88                'created_at' => date('Y-m-d H:i:s', $createdAt),
89            ];
90        }, $rawRows);
91
92        $listView = new ListViewData(
93            items: $items,
94            columns: [
95                'type' => 'Typ',
96                'title' => 'Tytuł / Nazwa',
97                'icon' => 'Ikona',
98                'url' => 'Adres URL',
99                'status' => 'Status',
100                'created_at' => 'Utworzono',
101            ],
102            currentPage: $spec->getPage(),
103            pageSize: $spec->getPageSize(),
104            totalItems: $totalItems,
105            sortField: $spec->getSortField(),
106            sortDirection: $spec->getSortDirection(),
107            filterParams: $spec->getFilters(),
108            createUrl: '/menu/create',
109            viewRoute: 'menu/view',
110            updateRoute: 'menu/update',
111            deleteRoute: 'menu/delete',
112            tableExists: (bool) ($queryResult['tableExists'] ?? true),
113            tableName: (string) ($queryResult['tableName'] ?? 'menu_records')
114        );
115
116        return $this->viewRenderer->render('//menu/index.twig', [
117            'currentView' => 'list',
118            'listView' => $listView,
119            'activeTable' => $targetRecordsTable,
120            'activeHost' => $this->activeHostProvider->getActiveHost(),
121        ]);
122    }
123
124    public function create(ServerRequestInterface $request): ResponseInterface
125    {
126        $targetRecordsTable = $this->prefixResolver->resolve('menu_records');
127        $targetSortTable = $this->prefixResolver->resolve('menu_sort');
128        $errors = [];
129        $data = [];
130
131        if ($request->getMethod() === 'POST') {
132            /** @var array<string, string> $data */
133            $data = (array) $request->getParsedBody();
134            $type = trim($data['type'] ?? 'element');
135            $title = trim($data['title'] ?? '');
136            $icon = trim($data['icon'] ?? 'bi-circle');
137            $url = trim($data['url'] ?? '#');
138            $badgeText = trim($data['badge_text'] ?? '');
139            $badgeColor = trim($data['badge_color'] ?? 'primary');
140            $target = trim($data['target'] ?? '_self');
141            $status = trim($data['status'] ?? 'active');
142            $parentId = trim($data['parent_id'] ?? '');
143
144            if ($title === '') {
145                $errors['title'] = 'Tytuł jest wymagany.';
146            }
147
148            if (empty($errors)) {
149                $recordId = 'menu-' . bin2hex(random_bytes(4));
150                $record = new MenuRecord(
151                    id: $recordId,
152                    type: $type,
153                    title: $title,
154                    icon: $icon !== '' ? $icon : 'bi-circle',
155                    url: $url !== '' ? $url : '#',
156                    badgeText: $badgeText !== '' ? $badgeText : null,
157                    badgeColor: $badgeColor !== '' ? $badgeColor : 'primary',
158                    target: $target !== '' ? $target : '_self',
159                    status: $status,
160                    createdAt: time(),
161                    updatedAt: time()
162                );
163
164                $this->recordRepository->save($record, $targetRecordsTable);
165
166                // Add to menu_sort table
167                $sort = new MenuSort(
168                    id: 'sort-' . bin2hex(random_bytes(4)),
169                    menuRecordId: $recordId,
170                    parentId: $parentId !== '' ? $parentId : null,
171                    sortOrder: 99,
172                    createdAt: time(),
173                    updatedAt: time()
174                );
175                $this->sortRepository->save($sort, $targetSortTable);
176
177                return $this->redirectWithFlash(self::ROUTE_MENU);
178            }
179        }
180
181        $allRecords = $this->recordRepository->findAll($targetRecordsTable);
182
183        return $this->viewRenderer->render('//menu/create.twig', [
184            'errors' => $errors,
185            'data' => $data,
186            'allRecords' => $allRecords,
187            'activeTable' => $targetRecordsTable,
188        ]);
189    }
190
191    public function update(ServerRequestInterface $request): ResponseInterface
192    {
193        /** @var array<string, mixed> $queryParams */
194        $queryParams = $request->getQueryParams();
195        $id = is_string($queryParams['id'] ?? null) ? (string) $queryParams['id'] : '';
196        $targetRecordsTable = $this->prefixResolver->resolve('menu_records');
197        $item = $this->recordRepository->findById($id, $targetRecordsTable);
198
199        if ($item === null) {
200            return $this->redirectWithFlash(self::ROUTE_MENU);
201        }
202
203        $errors = [];
204        $data = [
205            'type' => $item->getType(),
206            'title' => $item->getTitle(),
207            'icon' => $item->getIcon(),
208            'url' => $item->getUrl(),
209            'badge_text' => $item->getBadgeText(),
210            'badge_color' => $item->getBadgeColor(),
211            'target' => $item->getTarget(),
212            'status' => $item->getStatus(),
213        ];
214
215        if ($request->getMethod() === 'POST') {
216            /** @var array<string, string> $data */
217            $data = (array) $request->getParsedBody();
218            $type = trim($data['type'] ?? 'element');
219            $title = trim($data['title'] ?? '');
220            $icon = trim($data['icon'] ?? 'bi-circle');
221            $url = trim($data['url'] ?? '#');
222            $badgeText = trim($data['badge_text'] ?? '');
223            $badgeColor = trim($data['badge_color'] ?? 'primary');
224            $target = trim($data['target'] ?? '_self');
225            $status = trim($data['status'] ?? 'active');
226
227            if ($title === '') {
228                $errors['title'] = 'Tytuł jest wymagany.';
229            }
230
231            if (empty($errors)) {
232                $updatedRecord = new MenuRecord(
233                    id: $item->getId(),
234                    type: $type,
235                    title: $title,
236                    icon: $icon !== '' ? $icon : 'bi-circle',
237                    url: $url !== '' ? $url : '#',
238                    badgeText: $badgeText !== '' ? $badgeText : null,
239                    badgeColor: $badgeColor !== '' ? $badgeColor : 'primary',
240                    target: $target !== '' ? $target : '_self',
241                    status: $status,
242                    createdAt: $item->getCreatedAt(),
243                    updatedAt: time()
244                );
245
246                $this->recordRepository->save($updatedRecord, $targetRecordsTable);
247                return $this->redirectWithFlash(self::ROUTE_MENU);
248            }
249        }
250
251        return $this->viewRenderer->render('//menu/update.twig', [
252            'item' => $item,
253            'errors' => $errors,
254            'data' => $data,
255            'activeTable' => $targetRecordsTable,
256        ]);
257    }
258
259    public function delete(ServerRequestInterface $request): ResponseInterface
260    {
261        /** @var array<string, mixed> $queryParams */
262        $queryParams = $request->getQueryParams();
263        $id = is_string($queryParams['id'] ?? null) ? (string) $queryParams['id'] : '';
264        $targetRecordsTable = $this->prefixResolver->resolve('menu_records');
265        if ($id !== '') {
266            $this->recordRepository->delete($id, $targetRecordsTable);
267        }
268        return $this->redirectWithFlash(self::ROUTE_MENU);
269    }
270
271    public function saveSort(ServerRequestInterface $request): ResponseInterface
272    {
273        $targetSortTable = $this->prefixResolver->resolve('menu_sort');
274        $rawBody = (string) $request->getBody();
275        /** @var array<int, array{id: string, menu_record_id: string, parent_id: ?string, sort_order: int}>|null $treeData */
276        $treeData = json_decode($rawBody, true);
277
278        if (is_array($treeData)) {
279            $this->sortRepository->replaceTree($treeData, $targetSortTable);
280        }
281
282        $response = $this->responseFactory->createResponse(200);
283        $response->getBody()->write((string) json_encode(['success' => true]));
284        return $response->withHeader('Content-Type', 'application/json');
285    }
286
287    private function redirectWithFlash(string $url): ResponseInterface
288    {
289        $response = $this->responseFactory->createResponse(302);
290        return $response->withHeader('Location', $url);
291    }
292}