Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
127 / 127
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
NavBuilder
100.00% covered (success)
100.00%
127 / 127
100.00% covered (success)
100.00%
2 / 2
84
100.00% covered (success)
100.00%
1 / 1
 build
100.00% covered (success)
100.00%
93 / 93
100.00% covered (success)
100.00%
1 / 1
60
 prepare
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
24
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\Dom\Child;
18
19/**
20 * Nav builder class
21 *
22 * @category   Pop
23 * @package    Pop\Nav
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    5.0.0
28 */
29class NavBuilder
30{
31
32    /**
33     * Build the navigation from the nav object
34     *
35     * @param  Nav     $navObject
36     * @param  array   $tree
37     * @param  int     $depth
38     * @param  ?string $parentHref
39     * @throws Exception|\Pop\Acl\Exception
40     * @return Child
41     */
42    public static function build(Nav $navObject, array $tree, int $depth = 1, ?string $parentHref = null): Child
43    {
44        $config        = $navObject->getConfig();
45        $globalPolicy  = $config['policy'] ?? null;
46        [$nav, $child] = self::prepare($navObject, $config, $depth);
47
48        $navObject->incrementParentLevel();
49        $depth++;
50
51        // Recursively loop through the nodes
52        foreach ($tree as $node) {
53            $allowed = true;
54            if (isset($node['acl'])) {
55                if ($navObject->getAcl() === null) {
56                    throw new Exception('The access control object is not set.');
57                }
58                if (empty($navObject->getRoles())) {
59                    $allowed = false;
60                } else {
61                    $resource   = (isset($node['acl']['resource'])) ? $node['acl']['resource'] : null;
62                    $permission = (isset($node['acl']['permission'])) ? $node['acl']['permission'] : null;
63                    $policy     = (isset($node['acl']['policy'])) ? $node['acl']['policy'] : $globalPolicy;
64                    $allowed    = ($navObject->isAclStrict()) ?
65                        $navObject->getAcl()->isAllowedMultiStrict($navObject->getRoles(), $resource, $permission) :
66                        $navObject->getAcl()->isAllowedMulti($navObject->getRoles(), $resource, $permission);
67
68                    if (!empty($policy)) {
69                        // The 'policy' config/node value is a callable that must resolve to the
70                        // ACL role to evaluate the policy against - a role name string, or an
71                        // object using Pop\Acl\Policy\PolicyTrait. It is *not* itself a permission
72                        // decision; Acl::evaluatePolicy() calls $policyRole->can($permission, $resource).
73                        if ($policy instanceof \Pop\Utils\CallableObject) {
74                            $policyRole = $policy->call();
75                        } else if (is_callable($policy)) {
76                            $policyRole = call_user_func($policy);
77                        } else if (is_array($policy) && isset($policy[0]) && is_callable($policy[0])) {
78                            $callable = $policy[0];
79                            unset($policy[0]);
80                            $policyRole = call_user_func_array($callable, array_values($policy));
81                        } else {
82                            $policyRole = $policy;
83                        }
84
85                        $policyResult = $navObject->getAcl()->evaluatePolicy($permission, $policyRole, $resource);
86                        if ($policyResult !== null) {
87                            $allowed = $policyResult;
88                        }
89                    }
90                }
91            }
92            if (($allowed) && isset($node['name']) && isset($node['href'])) {
93                // Create child node and child link node
94                $a = new Child('a', htmlspecialchars($node['name'], ENT_QUOTES));
95
96                if ((str_starts_with($node['href'], '#')) || (str_ends_with($node['href'], '#')) ||
97                    (str_starts_with($node['href'], 'http')) || (str_starts_with($node['href'], 'mailto:'))) {
98                    $href = $node['href'];
99                } else if (str_starts_with($node['href'], '/')) {
100                    $href = $navObject->getBaseUrl() . $node['href'];
101                } else {
102                    if (str_ends_with($parentHref, '/')) {
103                        $href = $parentHref . $node['href'];
104                    } else {
105                        $href = $parentHref . '/' . $node['href'];
106                    }
107                }
108
109                $a->setAttribute('href', $href);
110
111                if (($navObject->isReturnFalse()) && (($href == '#') || (str_ends_with($href, '#')))) {
112                    $a->setAttribute('onclick', 'return false;');
113                }
114                $url = $navObject->getCurrentUrl() ?? ($_SERVER['REQUEST_URI'] ?? null);
115                if (($url !== null) && str_contains($url, '?')) {
116                    $url = substr($url, 0, strpos($url, '?'));
117                }
118
119                $linkClass = null;
120                if ($href == $url) {
121                    if (isset($config['on'])) {
122                        $linkClass = $config['on'];
123                    }
124                } else {
125                    if (isset($config['off'])) {
126                        $linkClass = $config['off'];
127                    }
128                }
129
130                // If the node has any attributes
131                if (isset($node['attributes'])) {
132                    foreach ($node['attributes'] as $attrib => $value) {
133                        $value = (($attrib == 'class') && ($linkClass !== null)) ? $value . ' ' . $linkClass : $value;
134                        $a->setAttribute($attrib, $value);
135                    }
136                } else if ($linkClass !== null) {
137                    $a->setAttribute('class', $linkClass);
138                }
139
140                if ($child !== null) {
141                    $navChild = new Child($child);
142
143                    // Set child attributes if they exist
144                    if (isset($config['child']) && isset($config['child']['id'])) {
145                        $navChild->setAttribute('id', $config['child']['id'] . '-' . $navObject->getChildLevel());
146                    }
147                    if (isset($config['child']) && isset($config['child']['class'])) {
148                        $navChild->setAttribute('class', $config['child']['class'] . '-' . ($depth - 1));
149                    }
150                    if (isset($config['child']['attributes'])) {
151                        foreach ($config['child']['attributes'] as $attrib => $value) {
152                            $navChild->setAttribute($attrib, $value);
153                        }
154                    }
155
156                    // Add link node
157                    $navChild->addChild($a);
158                    $navObject->incrementChildLevel();
159
160                    // If there are children, loop through and add them
161                    if (isset($node['children']) && is_array($node['children']) && (count($node['children']) > 0)) {
162                        $childrenAllowed = true;
163                        // Check if the children are allowed
164
165                        $i = 0;
166                        foreach ($node['children'] as $nodeChild) {
167                            if (isset($nodeChild['acl'])) {
168                                if ($navObject->getAcl() === null) {
169                                    throw new Exception('The access control object is not set.');
170                                }
171                                if (empty($navObject->getRoles())) {
172                                    $childrenAllowed = false;
173                                } else {
174                                    $resource   = (isset($nodeChild['acl']['resource'])) ? $nodeChild['acl']['resource'] : null;
175                                    $permission = (isset($nodeChild['acl']['permission'])) ? $nodeChild['acl']['permission'] : null;
176                                    $method     = ($navObject->isAclStrict()) ? 'isAllowedMultiStrict' : 'isAllowedMulti';
177                                    if (!($navObject->getAcl()->{$method}($navObject->getRoles(), $resource, $permission))) {
178                                        $i++;
179                                    }
180                                }
181                            }
182                        }
183                        if ($i == count($node['children'])) {
184                            $childrenAllowed = false;
185                        }
186                        if ($childrenAllowed) {
187                            $nextChild = self::build($navObject, $node['children'], $depth, $href);
188                            if (($nextChild->hasChildren()) || ($nextChild->getNodeValue() !== null)) {
189                                $navChild->addChild($nextChild);
190                            }
191                        }
192                    }
193                    // Add child node
194                    $nav->addChild($navChild);
195                } else {
196                    $nav->addChild($a);
197                }
198            }
199        }
200
201        return $nav;
202    }
203
204    /**
205     * Prepare nav node
206     *
207     * @param  Nav   $navObject
208     * @param  array $config
209     * @param  int   $depth
210     * @return array
211     */
212    public static function prepare(Nav $navObject, array $config, int $depth = 1): array
213    {
214        // Create overriding top level parent, if set
215        if (($depth == 1) && isset($config['top'])) {
216            $parent = (isset($config['top']['node'])) ? $config['top']['node'] : 'nav';
217            $child  = null;
218            if (isset($config['child']) && isset($config['child']['node'])) {
219                $child = $config['child']['node'];
220            } else if ($parent == 'nav') {
221                $child = 'nav';
222            }
223
224            // Create parent node
225            $nav = new Child($parent);
226            if ($navObject->getIndent() !== null) {
227                $nav->setIndent(str_repeat($navObject->getIndent(), $depth));
228            }
229
230            // Set top attributes if they exist
231            if (isset($config['top']['id'])) {
232                $nav->setAttribute('id', $config['top']['id']);
233            }
234            if (isset($config['top']['class'])) {
235                $nav->setAttribute('class', $config['top']['class']);
236            }
237            if (isset($config['top']['attributes'])) {
238                foreach ($config['top']['attributes'] as $attrib => $value) {
239                    $nav->setAttribute($attrib, $value);
240                }
241            }
242        } else {
243            // Set up parent/child node names
244            $parent = (isset($config['parent']) && isset($config['parent']['node'])) ? $config['parent']['node'] : 'nav';
245            $child  = null;
246            if (isset($config['child']) && isset($config['child']['node'])) {
247                $child = $config['child']['node'];
248            } else if ($parent == 'nav') {
249                $child = 'nav';
250            }
251
252            // Create parent node
253            $nav = new Child($parent);
254            if ($navObject->getIndent() !== null) {
255                $nav->setIndent(str_repeat($navObject->getIndent(), $depth));
256            }
257
258            // Set parent attributes if they exist
259            if (isset($config['parent']) && isset($config['parent']['id'])) {
260                $nav->setAttribute('id', $config['parent']['id'] . '-' . $navObject->getParentLevel());
261            }
262            if (isset($config['parent']) && isset($config['parent']['class'])) {
263                $nav->setAttribute('class', $config['parent']['class'] . '-' . $depth);
264            }
265            if (isset($config['parent']['attributes'])) {
266                foreach ($config['parent']['attributes'] as $attrib => $value) {
267                    $nav->setAttribute($attrib, $value);
268                }
269            }
270        }
271
272        return [$nav, $child];
273    }
274
275}