Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
SidebarMenuBlock
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
4 / 4
17
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 fetchMenuTree
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 hydrateMenuItems
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
11
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Menu\Presentation\Block;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Api\ApiClientInterface;
12use App\Core\Layout\BlockInterface;
13use App\Modules\Menu\Domain\Model\MenuItem;
14use App\Modules\Menu\Domain\Repository\MenuRepositoryInterface;
15use App\Modules\Menu\Presentation\Api\MenuApiController;
16use Throwable;
17use Twig\Environment as TwigEnvironment;
18
19/**
20 * Sidebar Dynamic Menu Block.
21 *
22 * Implements BlockInterface to render dynamic menu tree hierarchy into sidebar-left position
23 * fetched via REST API endpoint /api/v1/menu/tree.
24 *
25 * @package App\Modules\Menu\Presentation\Block
26 */
27final readonly class SidebarMenuBlock implements BlockInterface
28{
29    /**
30     * SidebarMenuBlock constructor.
31     *
32     * @param MenuApiController|ApiClientInterface|MenuRepositoryInterface $menuSource Menu API, client, or repo.
33     * @param TwigEnvironment $twig Twig template environment.
34     */
35    public function __construct(
36        private MenuApiController|ApiClientInterface|MenuRepositoryInterface $menuSource,
37        private TwigEnvironment $twig
38    ) {
39    }
40
41    /**
42     * {@inheritdoc}
43     */
44    public function render(array $context = []): string
45    {
46        $menuTree = $this->fetchMenuTree();
47
48        return $this->twig->render('blocks/sidebar_menu.twig', [
49            'menuTree' => $menuTree,
50        ]);
51    }
52
53    /**
54     * Fetches active menu tree structure from REST API or repository.
55     *
56     * @return array<int, MenuItem> Structured list of root menu items.
57     */
58    private function fetchMenuTree(): array
59    {
60        if ($this->menuSource instanceof MenuRepositoryInterface) {
61            return $this->menuSource->getActiveMenuTree();
62        }
63
64        try {
65            if ($this->menuSource instanceof MenuApiController) {
66                $response = $this->menuSource->tree();
67                $body = (string)$response->getBody();
68                /** @var array<string, mixed> $payload */
69                $payload = (array)json_decode($body, true);
70                $rawItems = (array)($payload['tree'] ?? $payload['data'] ?? []);
71            } else {
72                $payload = $this->menuSource->get('/api/v1/menu/tree');
73                $rawItems = (array)($payload['tree'] ?? $payload['data'] ?? []);
74            }
75
76            return $this->hydrateMenuItems($rawItems);
77        } catch (Throwable) {
78            return [];
79        }
80    }
81
82    /**
83     * Hydrates nested MenuItem domain models from raw API payload arrays.
84     *
85     * @param array<int, array<string, mixed>> $rawItems API items list.
86     * @return array<int, MenuItem> Hydrated menu items.
87     */
88    private function hydrateMenuItems(array $rawItems): array
89    {
90        $items = [];
91        foreach ($rawItems as $raw) {
92            $children = isset($raw['children']) && is_array($raw['children'])
93                ? $this->hydrateMenuItems((array)$raw['children'])
94                : [];
95
96            $item = new MenuItem(
97                (int)($raw['id'] ?? 0),
98                isset($raw['parent_id']) && $raw['parent_id'] !== null ? (int)$raw['parent_id'] : null,
99                (string)($raw['type'] ?? 'link'),
100                (string)($raw['label'] ?? ''),
101                isset($raw['route_url']) && $raw['route_url'] !== null ? (string)$raw['route_url'] : null,
102                isset($raw['icon_class']) && $raw['icon_class'] !== null ? (string)$raw['icon_class'] : null,
103                (int)($raw['sort_order'] ?? 0),
104                (bool)($raw['is_active'] ?? true)
105            );
106
107            foreach ($children as $child) {
108                $item->addChild($child);
109            }
110
111            $items[] = $item;
112        }
113
114        return $items;
115    }
116}