NavigationMenuGenerator   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 188
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 71
dl 0
loc 188
rs 9.84
c 0
b 0
f 0
wmc 32

13 Methods

Rating   Name   Duplication   Size   Complexity  
A searchForGroupLabelInConfig() 0 4 2
A searchForGroupPriorityInConfig() 0 3 2
A canAddRoute() 0 14 4
A handle() 0 7 1
A canGroupRoute() 0 11 3
A getConfigArray() 0 6 1
A generate() 0 25 6
A getOrCreateGroupItem() 0 20 3
A createGroupItem() 0 7 1
A addRouteToGroup() 0 13 3
A normalizeGroupLabel() 0 8 2
A usesGroups() 0 9 2
A __construct() 0 15 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Hyde\Framework\Features\Navigation;
6
7
use Hyde\Hyde;
8
use Hyde\Facades\Config;
9
use Hyde\Support\Models\Route;
10
use Hyde\Pages\DocumentationPage;
11
use Illuminate\Support\Collection;
12
use Hyde\Foundation\Facades\Routes;
13
use Hyde\Foundation\Kernel\RouteCollection;
14
use Hyde\Framework\Exceptions\InvalidConfigurationException;
15
16
use function filled;
17
use function assert;
18
use function collect;
19
use function in_array;
20
use function strtolower;
21
22
/**
23
 * @experimental This class may change significantly before its release.
24
 */
25
class NavigationMenuGenerator
26
{
27
    /** @var \Illuminate\Support\Collection<string, \Hyde\Framework\Features\Navigation\NavigationItem|\Hyde\Framework\Features\Navigation\NavigationGroup> */
28
    protected Collection $items;
29
30
    /** @var \Hyde\Foundation\Kernel\RouteCollection<string, \Hyde\Support\Models\Route> */
31
    protected RouteCollection $routes;
32
33
    /** @var class-string<\Hyde\Framework\Features\Navigation\NavigationMenu> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<\Hyde\Frame...igation\NavigationMenu> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<\Hyde\Framework\Features\Navigation\NavigationMenu>.
Loading history...
34
    protected string $menuType;
35
36
    protected bool $generatesSidebar;
37
    protected bool $usesGroups;
38
39
    /** @param class-string<\Hyde\Framework\Features\Navigation\NavigationMenu> $menuType */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<\Hyde\Frame...igation\NavigationMenu> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<\Hyde\Framework\Features\Navigation\NavigationMenu>.
Loading history...
40
    protected function __construct(string $menuType)
41
    {
42
        assert(in_array($menuType, [MainNavigationMenu::class, DocumentationSidebar::class]));
43
44
        $this->menuType = $menuType;
45
46
        $this->items = new Collection();
47
48
        $this->generatesSidebar = $menuType === DocumentationSidebar::class;
49
50
        $this->routes = $this->generatesSidebar
51
            ? Routes::getRoutes(DocumentationPage::class)
0 ignored issues
show
Bug introduced by
The method getRoutes() does not exist on Hyde\Foundation\Facades\Routes. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

51
            ? Routes::/** @scrutinizer ignore-call */ getRoutes(DocumentationPage::class)
Loading history...
52
            : Routes::all();
53
54
        $this->usesGroups = $this->usesGroups();
55
    }
56
57
    /** @param class-string<\Hyde\Framework\Features\Navigation\NavigationMenu> $menuType */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<\Hyde\Frame...igation\NavigationMenu> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<\Hyde\Framework\Features\Navigation\NavigationMenu>.
Loading history...
58
    public static function handle(string $menuType): MainNavigationMenu|DocumentationSidebar
59
    {
60
        $menu = new static($menuType);
61
62
        $menu->generate();
63
64
        return new $menuType($menu->items);
65
    }
66
67
    protected function generate(): void
68
    {
69
        $this->routes->each(function (Route $route): void {
70
            if ($this->canAddRoute($route)) {
71
                if ($this->canGroupRoute($route)) {
72
                    $this->addRouteToGroup($route);
73
                } else {
74
                    $this->items->put($route->getRouteKey(), NavigationItem::create($route));
0 ignored issues
show
Bug introduced by
$route->getRouteKey() of type string is incompatible with the type Illuminate\Support\TKey expected by parameter $key of Illuminate\Support\Collection::put(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

74
                    $this->items->put(/** @scrutinizer ignore-type */ $route->getRouteKey(), NavigationItem::create($route));
Loading history...
75
                }
76
            }
77
        });
78
79
        if ($this->generatesSidebar) {
80
            // If there are no pages other than the index page, we add it to the sidebar so that it's not empty
81
            if ($this->items->count() === 0 && DocumentationPage::home() !== null) {
82
                $this->items->push(NavigationItem::create(DocumentationPage::home()));
0 ignored issues
show
Bug introduced by
It seems like Hyde\Pages\DocumentationPage::home() can also be of type null; however, parameter $destination of Hyde\Framework\Features\...avigationItem::create() does only seem to accept Hyde\Support\Models\Route|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

82
                $this->items->push(NavigationItem::create(/** @scrutinizer ignore-type */ DocumentationPage::home()));
Loading history...
83
            }
84
        } else {
85
            collect(Config::getArray('hyde.navigation.custom', []))->each(function (array $data): void {
0 ignored issues
show
Bug introduced by
Hyde\Facades\Config::get...ation.custom', array()) of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $value of collect(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

85
            collect(/** @scrutinizer ignore-type */ Config::getArray('hyde.navigation.custom', []))->each(function (array $data): void {
Loading history...
86
                /** @var array{destination: string, label: ?string, priority: ?int, attributes: array<string, scalar>} $data */
87
                $message = 'Invalid navigation item configuration detected the configuration file. Please double check the syntax.';
88
                $item = InvalidConfigurationException::try(fn () => NavigationItem::create(...$data), $message);
0 ignored issues
show
Bug introduced by
$data is expanded, but the parameter $destination of Hyde\Framework\Features\...avigationItem::create() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
                $item = InvalidConfigurationException::try(fn () => NavigationItem::create(/** @scrutinizer ignore-type */ ...$data), $message);
Loading history...
89
90
                // Since these were added explicitly by the user, we can assume they should always be shown
91
                $this->items->push($item);
92
            });
93
        }
94
    }
95
96
    protected function usesGroups(): bool
97
    {
98
        if ($this->generatesSidebar) {
99
            // In order to know if we should use groups in the sidebar, we need to loop through the pages and see if they have a group set.
100
            // This automatically enables the sidebar grouping for all pages if at least one group is set.
101
102
            return $this->routes->first(fn (Route $route): bool => filled($route->getPage()->navigationMenuGroup())) !== null;
103
        } else {
104
            return Config::getString('hyde.navigation.subdirectory_display', 'hidden') === 'dropdown';
105
        }
106
    }
107
108
    protected function canAddRoute(Route $route): bool
109
    {
110
        if (! $route->getPage()->showInNavigation()) {
111
            return false;
112
        }
113
114
        if ($this->generatesSidebar) {
115
            // Since the index page is linked in the header, we don't want it in the sidebar
116
            return ! $route->is(DocumentationPage::homeRouteName());
117
        } else {
118
            // While we for the most part can rely on the navigation visibility state provided by the navigation data factory,
119
            // we need to make an exception for documentation pages, which generally have a visible state, as the data is
120
            // also used in the sidebar. But we only want the documentation index page to be in the main navigation.
121
            return ! $route->getPage() instanceof DocumentationPage || $route->is(DocumentationPage::homeRouteName());
122
        }
123
    }
124
125
    protected function canGroupRoute(Route $route): bool
126
    {
127
        if (! $this->generatesSidebar) {
128
            return $route->getPage()->navigationMenuGroup() !== null;
129
        }
130
131
        if (! $this->usesGroups) {
132
            return false;
133
        }
134
135
        return true;
136
    }
137
138
    protected function addRouteToGroup(Route $route): void
139
    {
140
        $item = NavigationItem::create($route);
141
142
        $groupKey = $item->getPage()->navigationMenuGroup();
143
        $groupName = $this->generatesSidebar ? ($groupKey ?? 'Other') : $groupKey;
144
145
        $groupItem = $this->getOrCreateGroupItem($groupName);
0 ignored issues
show
Bug introduced by
It seems like $groupName can also be of type null; however, parameter $groupName of Hyde\Framework\Features\...:getOrCreateGroupItem() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

145
        $groupItem = $this->getOrCreateGroupItem(/** @scrutinizer ignore-type */ $groupName);
Loading history...
146
147
        $groupItem->add($item);
148
149
        if (! $this->items->has($groupItem->getGroupKey())) {
150
            $this->items->put($groupItem->getGroupKey(), $groupItem);
0 ignored issues
show
Bug introduced by
$groupItem->getGroupKey() of type string is incompatible with the type Illuminate\Support\TKey expected by parameter $key of Illuminate\Support\Collection::put(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

150
            $this->items->put(/** @scrutinizer ignore-type */ $groupItem->getGroupKey(), $groupItem);
Loading history...
151
        }
152
    }
153
154
    protected function getOrCreateGroupItem(string $groupName): NavigationGroup
155
    {
156
        $groupKey = NavigationGroup::normalizeGroupKey($groupName);
157
        $group = $this->items->get($groupKey);
158
159
        if ($group instanceof NavigationGroup) {
160
            return $group;
161
        } elseif ($group instanceof NavigationItem) {
162
            // We are trying to add children to an existing navigation menu item,
163
            // so here we create a new instance to replace the base one, this
164
            // does mean we lose the destination as we can't link to them.
165
166
            $item = new NavigationGroup($group->getLabel(), [], $group->getPriority());
167
168
            $this->items->put($groupKey, $item);
0 ignored issues
show
Bug introduced by
$groupKey of type string is incompatible with the type Illuminate\Support\TKey expected by parameter $key of Illuminate\Support\Collection::put(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

168
            $this->items->put(/** @scrutinizer ignore-type */ $groupKey, $item);
Loading history...
169
170
            return $item;
171
        }
172
173
        return $this->createGroupItem($groupKey, $groupName);
174
    }
175
176
    protected function createGroupItem(string $groupKey, string $groupName): NavigationGroup
177
    {
178
        $label = $this->searchForGroupLabelInConfig($groupKey) ?? $groupName;
179
180
        $priority = $this->searchForGroupPriorityInConfig($groupKey);
181
182
        return NavigationGroup::create($this->normalizeGroupLabel($label), [], $priority ?? NavigationMenu::LAST);
183
    }
184
185
    protected function normalizeGroupLabel(string $label): string
186
    {
187
        // If there is no label, and the group is a slug, we can make a title from it
188
        if ($label === strtolower($label)) {
189
            return Hyde::makeTitle($label);
0 ignored issues
show
Bug introduced by
The method makeTitle() does not exist on Hyde\Hyde. Since you implemented __callStatic, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

189
            return Hyde::/** @scrutinizer ignore-call */ makeTitle($label);
Loading history...
190
        }
191
192
        return $label;
193
    }
194
195
    protected function searchForGroupLabelInConfig(string $groupKey): ?string
196
    {
197
        // TODO: Normalize this: sidebar_group_labels -> docs.sidebar.labels
198
        return $this->getConfigArray($this->generatesSidebar ? 'docs.sidebar_group_labels' : 'hyde.navigation.labels')[$groupKey] ?? null;
199
    }
200
201
    protected function searchForGroupPriorityInConfig(string $groupKey): ?int
202
    {
203
        return $this->getConfigArray($this->generatesSidebar ? 'docs.sidebar.order' : 'hyde.navigation.order')[$groupKey] ?? null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getConfigA...er')[$groupKey] ?? null could return the type string which is incompatible with the type-hinted return integer|null. Consider adding an additional type-check to rule them out.
Loading history...
204
    }
205
206
    /** @return array<string|int, string|int> */
207
    protected function getConfigArray(string $key): array
208
    {
209
        /** @var array<string|int, string|int> $array */
210
        $array = Config::getArray($key, []);
211
212
        return $array;
213
    }
214
}
215