Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.31% covered (success)
92.31%
36 / 39
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MenuService
92.31% covered (success)
92.31%
36 / 39
66.67% covered (warning)
66.67%
4 / 6
11.06
0.00% covered (danger)
0.00%
0 / 1
 all
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 forLocation
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 forHandle
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 locationHasMenu
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildTree
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 markActive
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace App\Services\Builder;
4
5use App\Models\Menu;
6use App\Models\MenuItem;
7use App\Service\CacheService;
8use Illuminate\Support\Collection;
9
10/**
11 * Resolves builder menus for the storefront. A "location" (see
12 * config/builder.php → menu_locations) maps to one Menu; its items are
13 * turned into a plain, render-ready tree with live URLs + active state.
14 */
15class MenuService
16{
17    /** All menus keyed by location, each as a nested array tree. Cached. */
18    public function all(): array
19    {
20        return CacheService::remember(CacheService::KEY_MENUS_BUILDER, function () {
21            return Menu::query()
22                ->with(['items' => fn ($q) => $q->orderBy('sort_order')])
23                ->get()
24                ->mapWithKeys(fn (Menu $menu) => [
25                    $menu->handle => [
26                        'name' => $menu->name,
27                        'location' => $menu->location,
28                        'items' => $this->buildTree($menu->items),
29                    ],
30                ])
31                ->all();
32        });
33    }
34
35    /** Render-ready tree for a location, or [] if nothing is assigned. */
36    public function forLocation(string $location): array
37    {
38        foreach ($this->all() as $menu) {
39            if (($menu['location'] ?? null) === $location) {
40                return $this->markActive($menu['items']);
41            }
42        }
43
44        return [];
45    }
46
47    /** Render-ready tree for a specific menu handle. */
48    public function forHandle(string $handle): array
49    {
50        $menu = $this->all()[$handle] ?? null;
51
52        return $menu ? $this->markActive($menu['items']) : [];
53    }
54
55    public function locationHasMenu(string $location): bool
56    {
57        return collect($this->all())->contains(fn ($m) => ($m['location'] ?? null) === $location);
58    }
59
60    /* ------------------------------------------------------------------ */
61
62    /** @param  Collection<int,MenuItem>  $items */
63    private function buildTree(Collection $items, ?int $parentId = null): array
64    {
65        return $items
66            ->where('parent_id', $parentId)
67            ->map(fn (MenuItem $item) => [
68                'label' => $item->label,
69                'url' => $item->url(),
70                'icon' => $item->icon,
71                'new_tab' => $item->new_tab,
72                'children' => $this->buildTree($items, $item->id),
73            ])
74            ->values()
75            ->all();
76    }
77
78    private function markActive(array $items): array
79    {
80        $current = trim(request()->path(), '/');
81
82        return array_map(function ($item) use ($current) {
83            $target = trim(parse_url($item['url'], PHP_URL_PATH) ?? '', '/');
84            $item['children'] = $this->markActive($item['children']);
85            $item['active'] = ($target !== '' && $target === $current)
86                || collect($item['children'])->contains('active', true);
87
88            return $item;
89        }, $items);
90    }
91}