Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
106 / 106
100.00% covered (success)
100.00%
42 / 42
CRAP
100.00% covered (success)
100.00%
1 / 1
Nav
100.00% covered (success)
100.00%
106 / 106
100.00% covered (success)
100.00%
42 / 42
61
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 returnFalse
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setTree
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addBranch
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addLeaf
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 setConfig
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
5
 setAcl
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setRole
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addRoles
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setAclStrict
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setIndent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setBaseUrl
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setCurrentUrl
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setParentLevel
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 incrementParentLevel
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 decrementParentLevel
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setChildLevel
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 incrementChildLevel
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 decrementChildLevel
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isReturnFalse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAclStrict
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTree
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAcl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAcl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRoles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRole
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIndent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBaseUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCurrentUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParentLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getChildLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNav
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 nav
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 build
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 rebuild
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 traverseTree
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Nav;
16
17use Pop\Acl\Acl;
18use Pop\Acl\AclRole;
19use Pop\Dom\Child;
20
21/**
22 * Nav class
23 *
24 * @category   Pop
25 * @package    Pop\Nav
26 * @author     Nick Sagona, III <nick@popphp.org>
27 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    5.0.0
30 */
31class Nav
32{
33
34    /**
35     * Nav tree
36     * @var array
37     */
38    protected array $tree = [];
39
40    /**
41     * Nav config
42     * @var array
43     */
44    protected array $config = [];
45
46    /**
47     * Acl object
48     * @var ?Acl
49     */
50    protected ?Acl $acl = null;
51
52    /**
53     * AclRole role objects
54     * @var array
55     */
56    protected array $roles = [];
57
58    /**
59     * Acl strict flag
60     * @var bool
61     */
62    protected bool $aclStrict = false;
63
64    /**
65     * Indentation
66     * @var ?string
67     */
68    protected ?string $indent = null;
69
70    /**
71     * Base URL
72     * @var ?string
73     */
74    protected ?string $baseUrl = null;
75
76    /**
77     * Current URL, used to determine the "on"/"off" link class.
78     * Falls back to $_SERVER['REQUEST_URI'] when not explicitly set.
79     * @var ?string
80     */
81    protected ?string $currentUrl = null;
82
83    /**
84     * Nav parent level
85     * @var int
86     */
87    protected int $parentLevel = 1;
88
89    /**
90     * Nav child level
91     * @var int
92     */
93    protected int $childLevel = 1;
94
95    /**
96     * Return false flag
97     * @var bool
98     */
99    protected bool $returnFalse = false;
100
101    /**
102     * Parent nav element
103     * @var ?Child
104     */
105    protected ?Child $nav = null;
106
107    /**
108     * Constructor
109     *
110     * Instantiate the nav object
111     *
112     * @param  ?array $tree
113     * @param  ?array $config
114     */
115    public function __construct(?array $tree = null, ?array $config = null)
116    {
117        $this->setTree($tree);
118        $this->setConfig($config);
119    }
120
121    /**
122     * Set the return false flag
123     *
124     * @param  bool $return
125     * @return Nav
126     */
127    public function returnFalse(bool $return): Nav
128    {
129        $this->returnFalse = $return;
130        return $this;
131    }
132
133    /**
134     * Set the nav tree
135     *
136     * @param  ?array $tree
137     * @return Nav
138     */
139    public function setTree(?array $tree = null): Nav
140    {
141        $this->tree = ($tree !== null) ? $tree : [];
142        $this->nav  = null;
143        return $this;
144    }
145
146    /**
147     * Add to a nav tree branch
148     *
149     * @param  array   $branch
150     * @param  bool $prepend
151     * @return Nav
152     */
153    public function addBranch(array $branch, bool $prepend = false): Nav
154    {
155        if (isset($branch['name'])) {
156            $branch = [$branch];
157        }
158        $this->tree = ($prepend) ? array_merge($branch, $this->tree) : array_merge($this->tree, $branch);
159        $this->nav  = null;
160        return $this;
161    }
162
163    /**
164     * Add to a leaf to nav tree branch
165     *
166     * @param  string $branch
167     * @param  array  $leaf
168     * @param  ?int   $pos
169     * @param  bool   $prepend
170     * @return Nav
171     */
172    public function addLeaf(string $branch, array $leaf, ?int $pos = null, bool $prepend = false): Nav
173    {
174        $this->tree        = $this->traverseTree($this->tree, $branch, $leaf, $pos, $prepend);
175        $this->parentLevel = 1;
176        $this->childLevel  = 1;
177        $this->nav         = null;
178        return $this;
179    }
180
181    /**
182     * Set the nav tree
183     *
184     * @param  ?array $config
185     * @return Nav
186     */
187    public function setConfig(?array $config = null): Nav
188    {
189        if ($config === null) {
190            $this->config = [
191                'top'    => [
192                    'node'  => 'nav'
193                ],
194                'parent' => [
195                    'node'  => 'nav'
196                ],
197                'child' => [
198                    'node'  => 'nav'
199                ]
200            ];
201        } else {
202            $this->config = $config;
203        }
204
205        if (isset($config['indent'])) {
206            $this->setIndent($config['indent']);
207        }
208
209        if (isset($config['baseUrl'])) {
210            $this->setBaseUrl($config['baseUrl']);
211        }
212
213        if (isset($config['currentUrl'])) {
214            $this->setCurrentUrl($config['currentUrl']);
215        }
216
217        return $this;
218    }
219
220    /**
221     * Set the Acl object
222     *
223     * @param  ?Acl $acl
224     * @return Nav
225     */
226    public function setAcl(?Acl $acl = null): Nav
227    {
228        $this->acl = $acl;
229        return $this;
230    }
231
232    /**
233     * Set a AclRole object (alias method)
234     *
235     * @param  ?AclRole $role
236     * @return Nav
237     */
238    public function setRole(?AclRole $role = null): Nav
239    {
240        $this->roles[$role->getName()] = $role;
241        return $this;
242    }
243
244    /**
245     * Add a AclRole object
246     *
247     * @param  ?AclRole $role
248     * @return Nav
249     */
250    public function addRole(?AclRole $role = null): Nav
251    {
252        return $this->setRole($role);
253    }
254
255    /**
256     * Add AclRole objects
257     *
258     * @param  array $roles
259     * @return Nav
260     */
261    public function addRoles(array $roles): Nav
262    {
263        foreach ($roles as $role) {
264            $this->setRole($role);
265        }
266
267        return $this;
268    }
269
270    /**
271     * Set the Acl object as strict evaluation
272     *
273     * @param  bool $strict
274     * @return Nav
275     */
276    public function setAclStrict(bool $strict): Nav
277    {
278        $this->aclStrict = $strict;
279        return $this;
280    }
281
282    /**
283     * Set the indent
284     *
285     * @param  string $indent
286     * @return Nav
287     */
288    public function setIndent(string $indent): Nav
289    {
290        $this->indent = $indent;
291        return $this;
292    }
293
294    /**
295     * Set the base URL
296     *
297     * @param  string $baseUrl
298     * @return Nav
299     */
300    public function setBaseUrl(string $baseUrl): Nav
301    {
302        $this->baseUrl = $baseUrl;
303        return $this;
304    }
305
306    /**
307     * Set the current URL, used to determine the "on"/"off" link class.
308     * If not set, falls back to $_SERVER['REQUEST_URI'].
309     *
310     * @param  string $currentUrl
311     * @return Nav
312     */
313    public function setCurrentUrl(string $currentUrl): Nav
314    {
315        $this->currentUrl = $currentUrl;
316        return $this;
317    }
318
319    /**
320     * Set parent level
321     *
322     * @param  int $level
323     * @return Nav
324     */
325    public function setParentLevel(int $level): Nav
326    {
327        $this->parentLevel = $level;
328        return $this;
329    }
330
331    /**
332     * Increment parent level
333     *
334     * @return Nav
335     */
336    public function incrementParentLevel(): Nav
337    {
338        $this->parentLevel++;
339        return $this;
340    }
341
342    /**
343     * Decrement parent level
344     *
345     * @return Nav
346     */
347    public function decrementParentLevel(): Nav
348    {
349        $this->parentLevel--;
350        return $this;
351    }
352
353    /**
354     * Set child level
355     *
356     * @param  int $level
357     * @return Nav
358     */
359    public function setChildLevel(int $level): Nav
360    {
361        $this->childLevel = $level;
362        return $this;
363    }
364
365    /**
366     * Increment child level
367     *
368     * @return Nav
369     */
370    public function incrementChildLevel(): Nav
371    {
372        $this->childLevel++;
373        return $this;
374    }
375
376    /**
377     * Decrement child level
378     *
379     * @return Nav
380     */
381    public function decrementChildLevel(): Nav
382    {
383        $this->childLevel--;
384        return $this;
385    }
386
387    /**
388     * Set the return false flag
389     *
390     * @return bool
391     */
392    public function isReturnFalse(): bool
393    {
394        return $this->returnFalse;
395    }
396
397    /**
398     * Determine if the Acl object is set as strict evaluation
399     *
400     * @return bool
401     */
402    public function isAclStrict(): bool
403    {
404        return $this->aclStrict;
405    }
406
407    /**
408     * Get the nav tree
409     *
410     * @return array
411     */
412    public function getTree(): array
413    {
414        return $this->tree;
415    }
416
417    /**
418     * Get the config
419     *
420     * @return array
421     */
422    public function getConfig(): array
423    {
424        return $this->config;
425    }
426
427    /**
428     * Get the Acl object
429     *
430     * @return Acl|null
431     */
432    public function getAcl(): Acl|null
433    {
434        return $this->acl;
435    }
436
437    /**
438     * Determine if there is an ACL object
439     *
440     * @return bool
441     */
442    public function hasAcl(): bool
443    {
444        return ($this->acl !== null);
445    }
446
447    /**
448     * Determine if there are roles
449     *
450     * @return bool
451     */
452    public function hasRoles(): bool
453    {
454        return (count($this->roles) > 0);
455    }
456
457    /**
458     * Determine if there is a certain role
459     *
460     * @param  string $name
461     * @return bool
462     */
463    public function hasRole(string $name): bool
464    {
465        return (isset($this->roles[$name]));
466    }
467
468    /**
469     * Get the AclRole objects
470     *
471     * @return array
472     */
473    public function getRoles(): array
474    {
475        return $this->roles;
476    }
477
478    /**
479     * Get a AclRole object
480     *
481     * @param  string $name
482     * @return AclRole|null
483     */
484    public function getRole(string $name): AclRole|null
485    {
486        return $this->roles[$name] ?? null;
487    }
488
489    /**
490     * Get the indent
491     *
492     * @return string|null
493     */
494    public function getIndent(): string|null
495    {
496        return $this->indent;
497    }
498
499    /**
500     * Get the base URL
501     *
502     * @return string|null
503     */
504    public function getBaseUrl(): string|null
505    {
506        return $this->baseUrl;
507    }
508
509    /**
510     * Get the current URL
511     *
512     * @return string|null
513     */
514    public function getCurrentUrl(): string|null
515    {
516        return $this->currentUrl;
517    }
518
519    /**
520     * Get parent level
521     *
522     * @return int
523     */
524    public function getParentLevel(): int
525    {
526        return $this->parentLevel;
527    }
528
529    /**
530     * Get child level
531     *
532     * @return int
533     */
534    public function getChildLevel(): int
535    {
536        return $this->childLevel;
537    }
538
539    /**
540     * Get the nav object
541     *
542     * @return Child
543     */
544    public function getNav(): Child
545    {
546        if ($this->nav === null) {
547            $this->nav = NavBuilder::build($this, $this->tree);
548        }
549        return $this->nav;
550    }
551
552    /**
553     * Get the nav object (alias)
554     *
555     * @return Child
556     */
557    public function nav(): Child
558    {
559        return $this->getNav();
560    }
561
562    /**
563     * Build the nav object
564     *
565     * @return Nav
566     */
567    public function build(): Nav
568    {
569        if ($this->nav === null) {
570            $this->nav = NavBuilder::build($this, $this->tree);
571        }
572        return $this;
573    }
574
575    /**
576     * Re-build the nav object
577     *
578     * @return Nav
579     */
580    public function rebuild(): Nav
581    {
582        $this->parentLevel = 1;
583        $this->childLevel  = 1;
584        $this->nav         = NavBuilder::build($this, $this->tree);
585        return $this;
586    }
587
588    /**
589     * Render the nav object
590     *
591     * @return string
592     */
593    public function render(): string
594    {
595        if ($this->nav === null) {
596            $this->nav = NavBuilder::build($this, $this->tree);
597        }
598
599        return ($this->nav->hasChildren()) ? $this->nav->render() : '';
600    }
601
602    /**
603     * Render Nav object to string
604     *
605     * @return string
606     */
607    public function __toString()
608    {
609        return $this->render();
610    }
611
612    /**
613     * Traverse tree to insert new leaf
614     *
615     * @param  array  $tree
616     * @param  string $branch
617     * @param  array  $newLeaf
618     * @param  ?int   $pos
619     * @param  bool   $prepend
620     * @param  int    $depth
621     * @return array
622     */
623    protected function traverseTree(
624        array $tree, string $branch, array $newLeaf, ?int $pos = null, bool $prepend = false, int $depth = 0
625    ): array
626    {
627        $t = [];
628        foreach ($tree as $leaf) {
629            if ((($pos === null) || ($pos == $depth)) && ($leaf['name'] == $branch)) {
630                if (isset($leaf['children'])) {
631                    $leaf['children'] = ($prepend) ?
632                        array_merge([$newLeaf], $leaf['children']) : array_merge($leaf['children'], [$newLeaf]);
633                } else {
634                    $leaf['children'] = [$newLeaf];
635                }
636            }
637            if (isset($leaf['children'])) {
638                $leaf['children'] = $this->traverseTree($leaf['children'], $branch, $newLeaf, $pos, $prepend, ($depth + 1));
639            }
640            $t[] = $leaf;
641        }
642
643        return $t;
644    }
645
646}