NavigationDataFactory::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 12
rs 9.9666
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Hyde\Framework\Factories;
6
7
use Hyde\Facades\Config;
8
use Illuminate\Support\Str;
9
use Hyde\Pages\MarkdownPost;
10
use Hyde\Pages\DocumentationPage;
11
use Hyde\Markdown\Models\FrontMatter;
12
use Hyde\Framework\Factories\Concerns\CoreDataObject;
13
use Hyde\Framework\Features\Navigation\NavigationMenu;
14
use Hyde\Markdown\Contracts\FrontMatter\SubSchemas\NavigationSchema;
15
use Hyde\Framework\Features\Navigation\NumericalPageOrderingHelper;
16
17
use function basename;
18
use function array_flip;
19
use function in_array;
20
use function is_a;
21
use function array_key_exists;
22
23
/**
24
 * Discover data used for navigation menus and the documentation sidebar.
25
 */
26
class NavigationDataFactory extends Concerns\PageDataFactory implements NavigationSchema
27
{
28
    /**
29
     * The front matter properties supported by this factory.
30
     *
31
     * Note that this represents a sub-schema, and is used as part of the page schema.
32
     */
33
    final public const SCHEMA = NavigationSchema::NAVIGATION_SCHEMA;
34
35
    protected readonly ?string $label;
36
    protected readonly ?string $group;
37
    protected readonly ?bool $hidden;
38
    protected readonly ?int $priority;
39
    private readonly string $title;
40
    private readonly string $routeKey;
41
    private readonly string $pageClass;
42
    private readonly string $identifier;
43
    private readonly FrontMatter $matter;
44
45
    public function __construct(CoreDataObject $pageData, string $title)
46
    {
47
        $this->matter = $pageData->matter;
0 ignored issues
show
Bug introduced by
The property matter is declared read-only in Hyde\Framework\Factories\NavigationDataFactory.
Loading history...
48
        $this->identifier = $pageData->identifier;
0 ignored issues
show
Bug introduced by
The property identifier is declared read-only in Hyde\Framework\Factories\NavigationDataFactory.
Loading history...
49
        $this->pageClass = $pageData->pageClass;
0 ignored issues
show
Bug introduced by
The property pageClass is declared read-only in Hyde\Framework\Factories\NavigationDataFactory.
Loading history...
50
        $this->routeKey = $pageData->routeKey;
0 ignored issues
show
Bug introduced by
The property routeKey is declared read-only in Hyde\Framework\Factories\NavigationDataFactory.
Loading history...
51
        $this->title = $title;
0 ignored issues
show
Bug introduced by
The property title is declared read-only in Hyde\Framework\Factories\NavigationDataFactory.
Loading history...
52
53
        $this->label = $this->makeLabel();
0 ignored issues
show
Bug introduced by
The property label is declared read-only in Hyde\Framework\Factories\NavigationDataFactory.
Loading history...
54
        $this->group = $this->makeGroup();
0 ignored issues
show
Bug introduced by
The property group is declared read-only in Hyde\Framework\Factories\NavigationDataFactory.
Loading history...
55
        $this->hidden = $this->makeHidden();
0 ignored issues
show
Bug introduced by
The property hidden is declared read-only in Hyde\Framework\Factories\NavigationDataFactory.
Loading history...
56
        $this->priority = $this->makePriority();
0 ignored issues
show
Bug introduced by
The property priority is declared read-only in Hyde\Framework\Factories\NavigationDataFactory.
Loading history...
57
    }
58
59
    /**
60
     * @return array{label: string|null, group: string|null, hidden: bool|null, priority: int|null}
61
     */
62
    public function toArray(): array
63
    {
64
        return [
0 ignored issues
show
introduced by
The expression return array('label' => ...ty' => $this->priority) returns an array which contains values of type boolean|integer|string which are incompatible with the return type Illuminate\Contracts\Support\TValue mandated by Illuminate\Contracts\Support\Arrayable::toArray().
Loading history...
65
            'label' => $this->label,
66
            'group' => $this->group,
67
            'hidden' => $this->hidden,
68
            'priority' => $this->priority,
69
        ];
70
    }
71
72
    protected function makeLabel(): ?string
73
    {
74
        return $this->searchForLabelInFrontMatter()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->searchForL...title') ?? $this->title could return the type boolean which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
75
            ?? $this->searchForLabelInConfigs()
76
            ?? $this->getMatter('title')
77
            ?? $this->title;
78
    }
79
80
    protected function makeGroup(): ?string
81
    {
82
        if ($this->pageIsInSubdirectory() && $this->canUseSubdirectoryForGroups()) {
83
            return NumericalPageOrderingHelper::hasNumericalPrefix($this->getSubdirectoryName())
84
                ? NumericalPageOrderingHelper::splitNumericPrefix($this->getSubdirectoryName())[1]
85
                : $this->getSubdirectoryName();
86
        }
87
88
        return $this->searchForGroupInFrontMatter();
89
    }
90
91
    protected function makeHidden(): bool
92
    {
93
        return $this->isInstanceOf(MarkdownPost::class)
94
            || $this->searchForHiddenInFrontMatter()
95
            || $this->searchForHiddenInConfigs()
96
            || $this->isNonDocumentationPageInHiddenSubdirectory();
97
    }
98
99
    protected function makePriority(): int
100
    {
101
        return $this->searchForPriorityInFrontMatter()
102
            ?? $this->searchForPriorityInConfigs()
103
            ?? $this->checkFilePrefixForOrder()
104
            ?? NavigationMenu::LAST;
105
    }
106
107
    private function searchForLabelInFrontMatter(): ?string
108
    {
109
        return $this->getMatter('navigation.label')
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getMatter(...ter('navigation.title') could return the type boolean which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
110
            ?? $this->getMatter('navigation.title');
111
    }
112
113
    private function searchForGroupInFrontMatter(): ?string
114
    {
115
        return $this->getMatter('navigation.group')
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getMatter(...('navigation.category') could return the type boolean which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
116
            ?? $this->getMatter('navigation.category');
117
    }
118
119
    private function searchForHiddenInFrontMatter(): ?bool
120
    {
121
        return $this->getMatter('navigation.hidden')
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getMatter(...('navigation.visible')) could return the type integer|string which is incompatible with the type-hinted return boolean|null. Consider adding an additional type-check to rule them out.
Loading history...
122
            ?? $this->invert($this->getMatter('navigation.visible'));
0 ignored issues
show
Bug introduced by
It seems like $this->getMatter('navigation.visible') can also be of type integer and string; however, parameter $value of Hyde\Framework\Factories...onDataFactory::invert() does only seem to accept boolean|null, 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

122
            ?? $this->invert(/** @scrutinizer ignore-type */ $this->getMatter('navigation.visible'));
Loading history...
123
    }
124
125
    private function searchForHiddenInConfigs(): ?bool
126
    {
127
        return $this->isInstanceOf(DocumentationPage::class)
128
            ? $this->isPageHiddenInSidebarConfiguration()
129
            : $this->isPageHiddenInNavigationConfiguration();
130
    }
131
132
    private function isPageHiddenInNavigationConfiguration(): bool
133
    {
134
        return in_array($this->routeKey, Config::getArray('hyde.navigation.exclude', ['404']));
135
    }
136
137
    private function isPageHiddenInSidebarConfiguration(): ?bool
138
    {
139
        $config = Config::getArray('docs.sidebar.exclude', ['404']);
140
141
        return
142
            // Check if the page is hidden from the sidebar by route key or identifier.
143
            (in_array($this->routeKey, $config) || in_array($this->identifier, $config))
144
            // Check if the page is hidden from the main navigation by its route key.
145
            || $this->isPageHiddenInNavigationConfiguration();
146
    }
147
148
    private function isNonDocumentationPageInHiddenSubdirectory(): bool
149
    {
150
        return ! $this->isInstanceOf(DocumentationPage::class)
151
            && $this->pageIsInSubdirectory()
152
            && $this->getSubdirectoryConfiguration() === 'hidden'
153
            && basename($this->identifier) !== 'index';
154
    }
155
156
    private function searchForPriorityInFrontMatter(): ?int
157
    {
158
        return $this->getMatter('navigation.priority')
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getMatter(...ter('navigation.order') could return the type boolean|string which is incompatible with the type-hinted return integer|null. Consider adding an additional type-check to rule them out.
Loading history...
159
            ?? $this->getMatter('navigation.order');
160
    }
161
162
    private function searchForLabelInConfigs(): ?string
163
    {
164
        return $this->isInstanceOf(DocumentationPage::class)
165
            ? $this->searchForLabelInSidebarConfig()
166
            : $this->searchForLabelInNavigationConfig();
167
    }
168
169
    private function searchForLabelInNavigationConfig(): ?string
170
    {
171
        /** @var array<string, string> $config */
172
        $config = Config::getArray('hyde.navigation.labels', [
173
            'index' => 'Home',
174
            DocumentationPage::homeRouteName() => 'Docs',
175
        ]);
176
177
        return $config[$this->routeKey] ?? null;
178
    }
179
180
    private function searchForLabelInSidebarConfig(): ?string
181
    {
182
        /** @var array<string>|array<string, string> $config */
183
        $config = Config::getArray('docs.sidebar.labels', [
184
            DocumentationPage::homeRouteName() => 'Docs',
185
        ]);
186
187
        return $config[$this->routeKey] ?? $config[$this->identifier] ?? null;
188
    }
189
190
    private function searchForPriorityInConfigs(): ?int
191
    {
192
        return $this->isInstanceOf(DocumentationPage::class)
193
            ? $this->searchForPriorityInSidebarConfig()
194
            : $this->searchForPriorityInNavigationConfig();
195
    }
196
197
    private function searchForPriorityInSidebarConfig(): ?int
198
    {
199
        /** @var array<string>|array<string, int> $config */
200
        $config = Config::getArray('docs.sidebar.order', []);
201
202
        return
203
            // For consistency with the navigation config.
204
            $this->parseNavigationPriorityConfig($config, 'routeKey')
205
            // For backwards compatibility, and ease of use, as the route key prefix
206
            // is redundant due to it being the same for all documentation pages
207
            ?? $this->parseNavigationPriorityConfig($config, 'identifier');
208
    }
209
210
    private function searchForPriorityInNavigationConfig(): ?int
211
    {
212
        /** @var array<string, int>|array<string> $config */
213
        $config = Config::getArray('hyde.navigation.order', [
214
            'index' => 0,
215
            'posts' => 10,
216
            'docs/index' => 100,
217
        ]);
218
219
        return $this->parseNavigationPriorityConfig($config, 'routeKey');
220
    }
221
222
    /**
223
     * @param  array<string, int>|array<string>  $config
224
     * @param  'routeKey'|'identifier'  $pageKeyName
0 ignored issues
show
Documentation Bug introduced by
The doc comment 'routeKey'|'identifier' at position 0 could not be parsed: Unknown type name ''routeKey'' at position 0 in 'routeKey'|'identifier'.
Loading history...
225
     */
226
    private function parseNavigationPriorityConfig(array $config, string $pageKeyName): ?int
227
    {
228
        /** @var string $pageKey */
229
        $pageKey = $this->{$pageKeyName};
230
231
        // Check if the config entry is a flat array or a keyed array.
232
        if (! array_key_exists($pageKey, $config)) {
233
            // Adding an offset makes so that pages with a front matter priority, or
234
            // explicit keyed priority selection that is lower can be shown first.
235
            // This is all to make it easier to mix ways of adding priorities.
236
237
            return $this->offset(
238
                array_flip($config)[$pageKey] ?? null,
239
                NavigationMenu::DEFAULT
240
            );
241
        }
242
243
        return $config[$pageKey] ?? null;
244
    }
245
246
    private function checkFilePrefixForOrder(): ?int
247
    {
248
        if (! $this->isInstanceOf(DocumentationPage::class)) {
249
            return null;
250
        }
251
252
        if (! NumericalPageOrderingHelper::hasNumericalPrefix($this->identifier)) {
253
            return null;
254
        }
255
256
        return NumericalPageOrderingHelper::splitNumericPrefix($this->identifier)[0];
257
    }
258
259
    private function canUseSubdirectoryForGroups(): bool
260
    {
261
        return $this->getSubdirectoryConfiguration() === 'dropdown'
262
            || $this->isInstanceOf(DocumentationPage::class);
263
    }
264
265
    private function pageIsInSubdirectory(): bool
266
    {
267
        return Str::contains($this->identifier, '/');
268
    }
269
270
    private function getSubdirectoryName(): string
271
    {
272
        return Str::before($this->identifier, '/');
273
    }
274
275
    protected function getSubdirectoryConfiguration(): string
276
    {
277
        return Config::getString('hyde.navigation.subdirectory_display', 'hidden');
278
    }
279
280
    /** @param class-string<\Hyde\Pages\Concerns\HydePage> $class */
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<\Hyde\Pages\Concerns\HydePage> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<\Hyde\Pages\Concerns\HydePage>.
Loading history...
281
    protected function isInstanceOf(string $class): bool
282
    {
283
        return is_a($this->pageClass, $class, true);
284
    }
285
286
    protected function invert(?bool $value): ?bool
287
    {
288
        return $value === null ? null : ! $value;
289
    }
290
291
    protected function offset(?int $value, int $offset): ?int
292
    {
293
        return $value === null ? null : $value + $offset;
294
    }
295
296
    protected function getMatter(string $key): string|null|int|bool
297
    {
298
        /** @var string|null|int|bool $value */
299
        $value = $this->matter->get($key);
300
301
        return $value;
302
    }
303
}
304