Navigation   F
last analyzed

Complexity

Total Complexity 101

Size/Duplication

Total Lines 641
Duplicated Lines 0 %

Test Coverage

Coverage 38.08%

Importance

Changes 0
Metric Value
eloc 200
c 0
b 0
f 0
dl 0
loc 641
ccs 123
cts 323
cp 0.3808
rs 2
wmc 101

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getBackendUrlForBlock() 0 25 3
A getFirstChildId() 0 23 4
A __construct() 0 9 1
A getFooterLinks() 0 34 6
A hasMetaNavigation() 0 3 2
A getNavigation() 0 3 1
A getKeys() 0 3 1
D getNavigationHTML() 0 161 35
A getPageInfo() 0 27 5
A getUrl() 0 21 5
A getPageId() 0 18 2
B getUrlForExtraId() 0 31 7
D getUrlForBlock() 0 80 23
A setExcludedPageIds() 0 7 2
A setSelectedPageIds() 0 24 4

How to fix   Complexity   

Complex Class

Complex classes like Navigation often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Navigation, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Frontend\Core\Engine;
4
5
use ForkCMS\App\KernelLoader;
6
use Frontend\Core\Language\Language;
7
use Symfony\Component\HttpKernel\KernelInterface;
8
use Backend\Modules\Pages\Engine\Model as BackendPagesModel;
9
use Frontend\Core\Engine\Model as FrontendModel;
10
use Frontend\Modules\Profiles\Engine\Authentication as FrontendAuthentication;
11
12
/**
13
 * This class will be used to build the navigation
14
 */
15
class Navigation extends KernelLoader
16
{
17
    /**
18
     * The excluded page ids. These will not be shown in the menu.
19
     *
20
     * @var array
21
     */
22
    private static $excludedPageIds = [];
23
24
    /**
25
     * The selected pageIds
26
     *
27
     * @var array
28
     */
29
    private static $selectedPageIds = [];
30
31
    /**
32
     * TwigTemplate instance
33
     *
34
     * @var TwigTemplate
35
     */
36
    protected $template;
37
38
    /**
39
     * URL instance
40
     *
41
     * @var Url
42
     */
43
    protected $url;
44
45 26
    public function __construct(KernelInterface $kernel)
46
    {
47 26
        parent::__construct($kernel);
48
49 26
        $this->template = $this->getContainer()->get('templating');
50 26
        $this->url = $this->getContainer()->get('url');
51
52
        // set selected ids
53 26
        $this->setSelectedPageIds();
54 26
    }
55
56
    /**
57
     * Creates a Backend URL for a given action and module
58
     * If you don't specify a language the current language will be used.
59
     *
60
     * @param string $action The action to build the URL for.
61
     * @param string $module The module to build the URL for.
62
     * @param string $language The language to use, if not provided we will use the working language.
63
     * @param array $parameters GET-parameters to use.
64
     * @param bool $urlencode Should the parameters be urlencoded?
65
     *
66
     * @return string
67
     */
68
    public static function getBackendUrlForBlock(
69
        string $action,
70
        string $module,
71
        string $language = null,
72
        array $parameters = null,
73
        bool $urlencode = true
74
    ): string {
75
        $language = $language ?? LANGUAGE;
76
77
        // add at least one parameter
78
        if (empty($parameters)) {
79
            $parameters['token'] = 'true';
80
        }
81
82
        if ($urlencode) {
83
            $parameters = array_map('rawurlencode', $parameters);
0 ignored issues
show
Bug introduced by
It seems like $parameters can also be of type null; however, parameter $array of array_map() does only seem to accept array, 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

83
            $parameters = array_map('rawurlencode', /** @scrutinizer ignore-type */ $parameters);
Loading history...
84
        }
85
86
        $queryString = '?' . http_build_query($parameters);
0 ignored issues
show
Bug introduced by
It seems like $parameters can also be of type null; however, parameter $data of http_build_query() does only seem to accept array|object, 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

86
        $queryString = '?' . http_build_query(/** @scrutinizer ignore-type */ $parameters);
Loading history...
87
88
        // build the URL and return it
89
        return FrontendModel::get('router')->generate(
90
            'backend',
91
            ['_locale' => $language, 'module' => $module, 'action' => $action]
92
        ) . $queryString;
93
    }
94
95
    /**
96
     * Get the first child for a given parent
97
     *
98
     * @param int $pageId The pageID wherefore we should retrieve the first child.
99
     *
100
     * @return int
101
     */
102
    public static function getFirstChildId(int $pageId): int
103
    {
104
        // init var
105
        $navigation = self::getNavigation();
106
107
        // loop depths
108
        foreach ($navigation as $parent) {
109
            // no available, skip this element
110
            if (!isset($parent[$pageId])) {
111
                continue;
112
            }
113
114
            // get keys
115
            $keys = array_keys($parent[$pageId]);
116
117
            // get first item
118
            if (isset($keys[0])) {
119
                return $keys[0];
120
            }
121
        }
122
123
        // fallback
124
        return false;
125
    }
126
127
    /**
128
     * Get all footer links
129
     *
130
     * @return array
131
     */
132 26
    public static function getFooterLinks(): array
133
    {
134
        // get the navigation
135 26
        $navigation = self::getNavigation();
136 26
        $footerLinks = [];
137
138
        // validate
139 26
        if (!isset($navigation['footer'][0])) {
140
            return $footerLinks;
141
        }
142
143 26
        foreach ($navigation['footer'][0] as $id => $data) {
144
            // skip hidden pages
145 26
            if ($data['hidden']) {
146
                continue;
147
            }
148
149
            // if data
150 26
            if (isset($data['data'])) {
151
                $data['data'] = unserialize($data['data']);
152
            }
153
154
            // add
155 26
            $footerLinks[] = [
156 26
                'id' => $id,
157 26
                'url' => self::getUrl($id),
158 26
                'title' => $data['title'],
159 26
                'navigation_title' => $data['navigation_title'],
160 26
                'selected' => in_array($id, self::$selectedPageIds, true),
161 26
                'link_class' => (isset($data['data']['link_class'])) ? $data['data']['link_class'] : null,
162
            ];
163
        }
164
165 26
        return $footerLinks;
166
    }
167
168
    /**
169
     * Get the page-keys
170
     *
171
     * @param string $language The language wherefore the navigation should be loaded,
172
     *                         if not provided we will load the language that was provided in the URL.
173
     *
174
     * @return array
175
     */
176 27
    public static function getKeys(string $language = null): array
177
    {
178 27
        return BackendPagesModel::getCacheBuilder()->getKeys($language ?? LANGUAGE);
179
    }
180
181
    /**
182
     * Get the navigation-items
183
     *
184
     * @param string $language The language wherefore the keys should be loaded,
185
     *                         if not provided we will load the language that was provided in the URL.
186
     *
187
     * @return array
188
     */
189 27
    public static function getNavigation(string $language = null): array
190
    {
191 27
        return BackendPagesModel::getCacheBuilder()->getNavigation($language ?? LANGUAGE);
192
    }
193
194
    /**
195
     * Check if we have meta navigation and that it is enabled
196
     *
197
     * @param array $navigation
198
     *
199
     * @return bool
200
     */
201 9
    private static function hasMetaNavigation(array $navigation): bool
202
    {
203 9
        return isset($navigation['meta']) && Model::get('fork.settings')->get('Pages', 'meta_navigation', true);
204
    }
205
206
    /**
207
     * Get navigation HTML
208
     *
209
     * @param string $type The type of navigation the HTML should be build for.
210
     * @param int $parentId The parentID to start of.
211
     * @param int $depth The maximum depth to parse.
212
     * @param array $excludeIds PageIDs to be excluded.
213
     * @param string $template The template that will be used.
214
     * @param int $depthCounter A counter that will hold the current depth.
215
     *
216
     * @throws Exception
217
     *
218
     * @return string
219
     */
220 26
    public static function getNavigationHTML(
221
        string $type = 'page',
222
        int $parentId = 0,
223
        int $depth = null,
224
        array $excludeIds = [],
225
        string $template = 'Core/Layout/Templates/Navigation.html.twig',
226
        int $depthCounter = 1
227
    ): string {
228
        // get navigation
229 26
        $navigation = self::getNavigation();
230
231
        // merge the exclude ids with the previously set exclude ids
232 26
        $excludeIds = array_merge($excludeIds, self::$excludedPageIds);
233
234
        // meta-navigation is requested but meta isn't enabled
235 26
        if ($type === 'meta' && !self::hasMetaNavigation($navigation)) {
236 9
            return '';
237
        }
238
239
        // validate
240 26
        if (!isset($navigation[$type])) {
241
            throw new Exception(
242
                'This type (' . $type . ') isn\'t a valid navigation type. Possible values are: page, footer, meta.'
243
            );
244
        }
245
246 26
        if (!isset($navigation[$type][$parentId])) {
247
            throw new Exception('The parent (' . $parentId . ') doesn\'t exists.');
248
        }
249
250
        // special construction to merge home with its immediate children
251 26
        $mergedHome = false;
252 26
        while (true) {
253
            // loop elements
254 26
            foreach ($navigation[$type][$parentId] as $id => $page) {
255
                // home is a special item, it should live on the same depth
256 26
                if (!$mergedHome && (int) $page['page_id'] === FrontendModel::HOME_PAGE_ID) {
257
                    // extra checks otherwise exceptions will wbe triggered.
258 26
                    if (!isset($navigation[$type][$parentId])
259 26
                        || !is_array($navigation[$type][$parentId])) {
260
                        $navigation[$type][$parentId] = [];
261
                    }
262 26
                    if (!isset($navigation[$type][$page['page_id']])
263 26
                        || !is_array($navigation[$type][$page['page_id']])
264
                    ) {
265
                        $navigation[$type][$page['page_id']] = [];
266
                    }
267
268
                    // add children
269 26
                    $navigation[$type][$parentId] = array_merge(
270 26
                        $navigation[$type][$parentId],
271 26
                        $navigation[$type][$page['page_id']]
272
                    );
273
274
                    // mark as merged
275 26
                    $mergedHome = true;
276
277
                    // restart loop
278 26
                    continue 2;
279
                }
280
281
                // not hidden and not an action
282 26
                if ($page['hidden'] || $page['tree_type'] === 'direct_action') {
283
                    unset($navigation[$type][$parentId][$id]);
284
                    continue;
285
                }
286
287
                // authentication
288 26
                if (isset($page['data'])) {
289
                    // unserialize data
290
291
                    $page['data'] = unserialize($page['data'], ['allowed_classes' => false]);
292
                    // add link class if needed
293
                    if (isset($page['data']['link_class'])) {
294
                        $navigation[$type][$parentId][$id]['link_class'] = $page['data']['link_class'];
295
                    }
296
297
                    // if auth_required isset and is true
298
                    if (isset($page['data']['auth_required']) && $page['data']['auth_required']) {
299
                        // is profile logged? unset
300
                        if (!FrontendAuthentication::isLoggedIn()) {
301
                            unset($navigation[$type][$parentId][$id]);
302
                            continue;
303
                        }
304
                        // check if group auth is set
305
                        if (!empty($page['data']['auth_groups'])) {
306
                            $inGroup = false;
307
                            // loop group and set value true if one is found
308
                            foreach ($page['data']['auth_groups'] as $group) {
309
                                if (FrontendAuthentication::getProfile()->isInGroup($group)) {
310
                                    $inGroup = true;
311
                                }
312
                            }
313
                            // unset page if not in any of the groups
314
                            if (!$inGroup) {
315
                                unset($navigation[$type][$parentId][$id]);
316
                            }
317
                        }
318
                    }
319
                }
320
321
                // some ids should be excluded
322 26
                if (in_array($page['page_id'], $excludeIds)) {
323
                    unset($navigation[$type][$parentId][$id]);
324
                    continue;
325
                }
326
327
                // if the item is in the selected page it should get an selected class
328 26
                $navigation[$type][$parentId][$id]['selected'] = in_array(
329 26
                    $page['page_id'],
330 26
                    self::$selectedPageIds
331
                );
332
333
                // add nofollow attribute if needed
334 26
                $navigation[$type][$parentId][$id]['nofollow'] = $page['no_follow'];
335
336
                // meta and footer subpages have the "page" type
337 26
                $subType = ($type === 'meta' || $type === 'footer') ? 'page' : $type;
338
339
                // fetch children if needed
340 26
                if (($depthCounter + 1 <= $depth || $depth === null)
341 26
                    && (int) $page['page_id'] !== FrontendModel::HOME_PAGE_ID
342 26
                    && isset($navigation[$subType][$page['page_id']])
343
                ) {
344
                    $navigation[$type][$parentId][$id]['children'] = self::getNavigationHTML(
345
                        $subType,
346
                        $page['page_id'],
347
                        $depth,
348
                        (array) $excludeIds,
349
                        $template,
350
                        $depthCounter + 1
351
                    );
352
                } else {
353 26
                    $navigation[$type][$parentId][$id]['children'] = false;
354
                }
355
356 26
                $navigation[$type][$parentId][$id]['parent_id'] = $parentId;
357 26
                $navigation[$type][$parentId][$id]['depth'] = $depthCounter;
358 26
                $navigation[$type][$parentId][$id]['link'] = static::getUrl($page['page_id']);
359
360
                // is this an internal redirect?
361 26
                if (isset($page['redirect_page_id']) && $page['redirect_page_id'] !== '') {
362
                    $navigation[$type][$parentId][$id]['link'] = static::getUrl(
363
                        (int) $page['redirect_page_id']
364
                    );
365
                }
366
367
                // is this an external redirect?
368 26
                if (isset($page['redirect_url']) && $page['redirect_url'] !== '') {
369 26
                    $navigation[$type][$parentId][$id]['link'] = $page['redirect_url'];
370
                }
371
            }
372
373
            // break the loop (it is only used for the special construction with home)
374 26
            break;
375
        }
376
377
        // return parsed content
378 26
        return Model::get('templating')->render(
379 26
            $template,
380 26
            ['navigation' => $navigation[$type][$parentId]]
381
        );
382
    }
383
384
    /**
385
     * Get a menuId for an specified URL
386
     *
387
     * @param string $url The URL wherefore you want a pageID.
388
     * @param string $language The language wherefore the pageID should be retrieved,
389
     *                          if not provided we will load the language that was provided in the URL.
390
     *
391
     * @return int
392
     */
393 26
    public static function getPageId(string $url, string $language = null): int
394
    {
395
        // redefine
396 26
        $url = trim($url, '/');
397
398
        // get menu items array
399 26
        $keys = self::getKeys($language ?? LANGUAGE);
400
401
        // get key
402 26
        $key = array_search($url, $keys, true);
403
404
        // return 404 if we don't known a valid Id
405 26
        if ($key === false) {
406
            return FrontendModel::ERROR_PAGE_ID;
407
        }
408
409
        // return the real Id
410 26
        return (int) $key;
411
    }
412
413
    /**
414
     * Get more info about a page
415
     *
416
     * @param int $requestedPageId The pageID wherefore you want more information.
417
     *
418
     * @return array|bool
419
     */
420 26
    public static function getPageInfo(int $requestedPageId)
421
    {
422
        // get navigation
423 26
        $navigation = self::getNavigation();
424
425
        // loop levels
426 26
        foreach ($navigation as $level) {
427
            // loop parents
428 26
            foreach ($level as $parentId => $children) {
429
                // loop children
430 26
                foreach ($children as $pageId => $page) {
431
                    // return if this is the requested page
432 26
                    if ($requestedPageId === (int) $pageId) {
433
                        // set return
434 26
                        $pageInfo = $page;
435 26
                        $pageInfo['page_id'] = $pageId;
436 26
                        $pageInfo['parent_id'] = $parentId;
437
438
                        // return
439 26
                        return $pageInfo;
440
                    }
441
                }
442
            }
443
        }
444
445
        // fallback
446
        return false;
447
    }
448
449
    /**
450
     * Get URL for a given pageId
451
     *
452
     * @param int $pageId The pageID wherefore you want the URL.
453
     * @param string $language The language wherein the URL should be retrieved,
454
     *                         if not provided we will load the language that was provided in the URL.
455
     *
456
     * @return string
457
     */
458 27
    public static function getUrl(int $pageId, string $language = null): string
459
    {
460 27
        $language = $language ?? LANGUAGE;
461
462
        // init URL
463 27
        $url = FrontendModel::getContainer()->getParameter('site.multilanguage') ? '/' . $language . '/' : '/';
464
465
        // get the menuItems
466 27
        $keys = self::getKeys($language);
467
468
        // get the URL, if it doesn't exist return 404
469 27
        if ($pageId !== FrontendModel::ERROR_PAGE_ID && !isset($keys[$pageId])) {
470
            return self::getUrl(FrontendModel::ERROR_PAGE_ID, $language);
471
        }
472
473 27
        if (empty($keys)) {
474
            return urldecode($url . FrontendModel::ERROR_PAGE_ID);
475
        }
476
477
        // return the URL
478 27
        return urldecode($url . $keys[$pageId]);
479
    }
480
481
    /**
482
     * Get the URL for a give module & action combination
483
     *
484
     * @param string $module The module wherefore the URL should be build.
485
     * @param string $action The specific action wherefore the URL should be build.
486
     * @param string $language The language wherein the URL should be retrieved,
487
     *                         if not provided we will load the language that was provided in the URL.
488
     * @param array $data An array with keys and values that partially or fully match the data of the block.
489
     *                    If it matches multiple versions of that block it will just return the first match.
490
     *
491
     * @return string
492
     */
493 27
    public static function getUrlForBlock(
494
        string $module,
495
        string $action = null,
496
        string $language = null,
497
        array $data = null
498
    ): string {
499 27
        $language = $language ?? LANGUAGE;
500
        // init var
501 27
        $pageIdForUrl = null;
502
503
        // get the menuItems
504 27
        $navigation = self::getNavigation($language);
505
506 27
        $dataMatch = false;
507
        // loop types
508 27
        foreach ($navigation as $level) {
509
            // loop level
510 27
            foreach ($level as $pages) {
511
                // loop pages
512 27
                foreach ($pages as $pageId => $properties) {
513
                    // only process pages with extra_blocks that are visible
514 27
                    if (!isset($properties['extra_blocks']) || $properties['hidden']) {
515
                        continue;
516
                    }
517
518
                    // loop extras
519 27
                    foreach ($properties['extra_blocks'] as $extra) {
520
                        // direct link?
521 27
                        if ($extra['module'] === $module && $extra['action'] === $action && $extra['action'] !== null) {
522
                            // if there is data check if all the requested data matches the extra data
523
                            if ($data !== null && isset($extra['data'])
524
                                && array_intersect_assoc($data, (array) $extra['data']) !== $data) {
525
                                // It is the correct action but has the wrong data
526
                                continue;
527
                            }
528
                            // exact page was found, so return
529
                            return self::getUrl($properties['page_id'], $language);
530
                        }
531
532 27
                        if ($extra['module'] === $module && $extra['action'] === null) {
533
                            // if there is data check if all the requested data matches the extra data
534 27
                            if ($data !== null && isset($extra['data'])) {
535
                                if (array_intersect_assoc($data, (array) $extra['data']) !== $data) {
536
                                    // It is the correct module but has the wrong data
537
                                    continue;
538
                                }
539
540
                                $pageIdForUrl = (int) $pageId;
541
                                $dataMatch = true;
542
                            }
543
544 27
                            if ($data === null && $extra['data'] === null) {
545 27
                                $pageIdForUrl = (int) $pageId;
546 27
                                $dataMatch = true;
547
                            }
548
549 27
                            if (!$dataMatch) {
550 27
                                $pageIdForUrl = (int) $pageId;
551
                            }
552
                        }
553
                    }
554
                }
555
            }
556
        }
557
558
        // pageId still null?
559 27
        if ($pageIdForUrl === null) {
560
            return self::getUrl(FrontendModel::ERROR_PAGE_ID, $language);
561
        }
562
563
        // build URL
564 27
        $url = self::getUrl($pageIdForUrl, $language);
565
566
        // append action
567 27
        if ($action !== null) {
568 16
            $url .= '/' . Language::act(\SpoonFilter::toCamelCase($action));
569
        }
570
571
        // return the URL
572 27
        return $url;
573
    }
574
575
    /**
576
     * Fetch the first direct link to an extra id
577
     *
578
     * @param int $id The id of the extra.
579
     * @param string $language The language wherein the URL should be retrieved,
580
     *                         if not provided we will load the language that was provided in the URL.
581
     *
582
     * @return string
583
     */
584
    public static function getUrlForExtraId(int $id, string $language = null): string
585
    {
586
        $language = $language ?? LANGUAGE;
587
        // get the menuItems
588
        $navigation = self::getNavigation($language);
589
590
        // loop types
591
        foreach ($navigation as $level) {
592
            // loop level
593
            foreach ($level as $pages) {
594
                // loop pages
595
                foreach ($pages as $properties) {
596
                    // no extra_blocks available, so skip this item
597
                    if (!isset($properties['extra_blocks'])) {
598
                        continue;
599
                    }
600
601
                    // loop extras
602
                    foreach ($properties['extra_blocks'] as $extra) {
603
                        // direct link?
604
                        if ((int) $extra['id'] === $id) {
605
                            // exact page was found, so return
606
                            return self::getUrl($properties['page_id'], $language);
607
                        }
608
                    }
609
                }
610
            }
611
        }
612
613
        // fallback
614
        return self::getUrl(FrontendModel::ERROR_PAGE_ID, $language);
615
    }
616
617
    /**
618
     * This function lets you add ignored pages
619
     *
620
     * @param mixed $pageIds This can be a single page id or this can be an array with page ids.
621
     */
622
    public static function setExcludedPageIds($pageIds): void
623
    {
624
        $pageIds = (array) $pageIds;
625
626
        // go trough the page ids to add them to the excluded page ids for later usage
627
        foreach ($pageIds as $pageId) {
628
            self::$excludedPageIds[] = $pageId;
629
        }
630
    }
631
632 26
    public function setSelectedPageIds(): void
633
    {
634
        // get pages
635 26
        $pages = (array) $this->url->getPages();
636
637
        // no pages, means we're at the homepage
638 26
        if (empty($pages)) {
639
            self::$selectedPageIds[] = FrontendModel::HOME_PAGE_ID;
640
641
            return;
642
        }
643
644
        // loop pages
645 26
        while (!empty($pages)) {
646
            // get page id
647 26
            $pageId = self::getPageId((string) implode('/', $pages));
648
649
            // add pageId into selected items
650 26
            if ($pageId !== false) {
651 26
                self::$selectedPageIds[] = $pageId;
652
            }
653
654
            // remove last element
655 26
            array_pop($pages);
656
        }
657 26
    }
658
}
659