• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mimmi20 / mezzio-navigation-laminasviewrenderer / 17879055887

20 Sep 2025 11:03AM UTC coverage: 91.323% (+0.006%) from 91.317%
17879055887

push

github

web-flow
Merge pull request #540 from mimmi20/updates

remove workflows

1263 of 1383 relevant lines covered (91.32%)

13.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.07
/src/View/Helper/Navigation/AbstractMenu.php
1
<?php
2

3
/**
4
 * This file is part of the mimmi20/mezzio-navigation-laminasviewrenderer package.
5
 *
6
 * Copyright (c) 2020-2025, Thomas Mueller <mimmi20@live.de>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types = 1);
13

14
namespace Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation;
15

16
use Laminas\I18n\Exception\RuntimeException;
17
use Laminas\Stdlib\Exception\InvalidArgumentException;
18
use Laminas\View\Exception;
19
use Laminas\View\Helper\EscapeHtmlAttr;
20
use Laminas\View\Model\ModelInterface;
21
use Mimmi20\LaminasView\Helper\PartialRenderer\Helper\PartialRendererInterface;
22
use Mimmi20\Mezzio\Navigation\ContainerInterface;
23
use Mimmi20\Mezzio\Navigation\Page\PageInterface;
24
use Mimmi20\NavigationHelper\ContainerParser\ContainerParserInterface;
25
use Mimmi20\NavigationHelper\Htmlify\HtmlifyInterface;
26
use Override;
27
use RecursiveIteratorIterator;
28

29
use function array_key_exists;
30
use function array_merge;
31
use function assert;
32
use function count;
33
use function get_debug_type;
34
use function implode;
35
use function is_array;
36
use function is_bool;
37
use function is_int;
38
use function is_string;
39
use function rtrim;
40
use function sprintf;
41
use function str_repeat;
42

43
use const PHP_EOL;
44

45
/**
46
 * Helper for rendering menus from navigation containers.
47
 *
48
 * phpcs:disable SlevomatCodingStandard.Classes.TraitUseDeclaration.MultipleTraitsPerDeclaration
49
 */
50
abstract class AbstractMenu extends AbstractHelper implements MenuInterface
51
{
52
    /**
53
     * Whether page class should be applied to <li> element.
54
     */
55
    protected bool $addClassToListItem = false;
56

57
    /**
58
     * Whether labels should be escaped.
59
     */
60
    protected bool $escapeLabels = true;
61

62
    /**
63
     * Whether only active branch should be rendered.
64
     */
65
    protected bool $onlyActiveBranch = false;
66

67
    /**
68
     * Partial view script to use for rendering menu.
69
     *
70
     * @var array<int, string>|ModelInterface|string|null
71
     */
72
    protected array | ModelInterface | string | null $partial = null;
73

74
    /**
75
     * Whether parents should be rendered when only rendering active branch.
76
     */
77
    protected bool $renderParents = true;
78

79
    /**
80
     * CSS class to use for the ul element.
81
     */
82
    protected string $ulClass = 'navigation';
83

84
    /**
85
     * CSS class to use for the li elements.
86
     */
87
    protected string $liClass = '';
88

89
    /**
90
     * CSS class to use for the active li element.
91
     */
92
    protected string $liActiveClass = 'active';
93

94
    /** @throws void */
95
    public function __construct(
80✔
96
        HtmlifyInterface $htmlify,
97
        ContainerParserInterface $containerParser,
98
        protected EscapeHtmlAttr $escaper,
99
        private readonly PartialRendererInterface $renderer,
100
    ) {
101
        parent::__construct($htmlify, $containerParser);
80✔
102
    }
103

104
    /**
105
     * Renders menu.
106
     *
107
     * Implements {@link ViewHelperInterface::render()}.
108
     *
109
     * If a partial view is registered in the helper, the menu will be rendered
110
     * using the given partial script. If no partial is registered, the menu
111
     * will be rendered as an 'ul' element by the helper's internal method.
112
     *
113
     * @see renderPartial()
114
     * @see renderMenu()
115
     *
116
     * @param ContainerInterface<PageInterface>|string|null $container [optional] container to render. Default is null, which indicates that the helper should render the container returned by {@link getContainer()}.
117
     *
118
     * @throws Exception\InvalidArgumentException
119
     * @throws Exception\RuntimeException
120
     */
121
    #[Override]
17✔
122
    public function render(ContainerInterface | string | null $container = null): string
123
    {
124
        $partial = $this->getPartial();
17✔
125

126
        if ($partial) {
17✔
127
            return $this->renderPartial($container, $partial);
3✔
128
        }
129

130
        return $this->renderMenu($container);
14✔
131
    }
132

133
    /**
134
     * Renders the given $container by invoking the partial view helper.
135
     *
136
     * The container will simply be passed on as a model to the view script
137
     * as-is, and will be available in the partial script as 'container', e.g.
138
     * <code>echo 'Number of pages: ', count($this->container);</code>.
139
     *
140
     * @param ContainerInterface<PageInterface>|string|null $container [optional] container to pass to view script. Default is to use the container registered in the helper.
141
     * @param array<int, string>|ModelInterface|string|null $partial   [optional] partial view script to use. Default is to use the partial registered in the helper. If an array is given, the first value is used for the partial view script.
142
     *
143
     * @throws Exception\RuntimeException         if no partial provided
144
     * @throws Exception\InvalidArgumentException if partial is invalid array
145
     */
146
    #[Override]
4✔
147
    public function renderPartial(
148
        ContainerInterface | string | null $container = null,
149
        array | ModelInterface | string | null $partial = null,
150
    ): string {
151
        return $this->renderPartialModel([], $container, $partial);
4✔
152
    }
153

154
    /**
155
     * Renders the given $container by invoking the partial view helper with the given parameters as the model.
156
     *
157
     * The container will simply be passed on as a model to the view script
158
     * as-is, and will be available in the partial script as 'container', e.g.
159
     * <code>echo 'Number of pages: ', count($this->container);</code>.
160
     *
161
     * Any parameters provided will be passed to the partial via the view model.
162
     *
163
     * @param array<int|string, mixed>                      $params
164
     * @param ContainerInterface<PageInterface>|string|null $container [optional] container to pass to view script. Default is to use the container registered in the helper.
165
     * @param array<int, string>|string|null                $partial   [optional] partial view script to use. Default is to use the partial registered in the helper. If an array is given, the first value is used for the partial view script.
166
     *
167
     * @throws Exception\RuntimeException         if no partial provided
168
     * @throws Exception\InvalidArgumentException if partial is invalid array
169
     */
170
    #[Override]
1✔
171
    public function renderPartialWithParams(
172
        array $params = [],
173
        ContainerInterface | string | null $container = null,
174
        array | string | null $partial = null,
175
    ): string {
176
        return $this->renderPartialModel($params, $container, $partial);
1✔
177
    }
178

179
    /**
180
     * Returns an HTML string containing an 'a' element for the given page if
181
     * the page's href is not empty, and a 'span' element if it is empty.
182
     *
183
     * Overrides {@link AbstractHelper::htmlify()}.
184
     *
185
     * @param PageInterface $page               page to generate HTML for
186
     * @param bool          $escapeLabel        Whether to escape the label
187
     * @param bool          $addClassToListItem Whether to add the page class to the list item
188
     *
189
     * @throws Exception\RuntimeException
190
     * @throws Exception\InvalidArgumentException
191
     */
192
    #[Override]
2✔
193
    public function htmlify(PageInterface $page, bool $escapeLabel = true, bool $addClassToListItem = false): string
194
    {
195
        try {
196
            return $this->htmlify->toHtml(static::class, $page, $escapeLabel, $addClassToListItem);
2✔
197
        } catch (RuntimeException $e) {
1✔
198
            throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
1✔
199
        }
200
    }
201

202
    /**
203
     * Sets a flag indicating whether labels should be escaped.
204
     *
205
     * @param bool $flag [optional] escape labels
206
     *
207
     * @throws void
208
     */
209
    #[Override]
1✔
210
    public function escapeLabels(bool $flag = true): static
211
    {
212
        $this->escapeLabels = $flag;
1✔
213

214
        return $this;
1✔
215
    }
216

217
    /** @throws void */
218
    public function getEscapeLabels(): bool
40✔
219
    {
220
        return $this->escapeLabels;
40✔
221
    }
222

223
    /**
224
     * Enables/disables page class applied to <li> element.
225
     *
226
     * @param bool $flag [optional] page class applied to <li> element Default
227
     *                   is true
228
     *
229
     * @return static fluent interface
230
     *
231
     * @throws void
232
     */
233
    #[Override]
2✔
234
    public function setAddClassToListItem(bool $flag = true): static
235
    {
236
        $this->addClassToListItem = $flag;
2✔
237

238
        return $this;
2✔
239
    }
240

241
    /**
242
     * Returns flag indicating whether page class should be applied to <li> element.
243
     *
244
     * By default, this value is false.
245
     *
246
     * @return bool whether parents should be rendered
247
     *
248
     * @throws void
249
     */
250
    #[Override]
41✔
251
    public function getAddClassToListItem(): bool
252
    {
253
        return $this->addClassToListItem;
41✔
254
    }
255

256
    /**
257
     * Sets a flag indicating whether only active branch should be rendered.
258
     *
259
     * @param bool $flag [optional] render only active branch
260
     *
261
     * @throws void
262
     */
263
    #[Override]
9✔
264
    public function setOnlyActiveBranch(bool $flag = true): static
265
    {
266
        $this->onlyActiveBranch = $flag;
9✔
267

268
        return $this;
9✔
269
    }
270

271
    /**
272
     * Returns a flag indicating whether only active branch should be rendered.
273
     *
274
     * By default, this value is false, meaning the entire menu will be rendered.
275
     *
276
     * @throws void
277
     */
278
    #[Override]
34✔
279
    public function getOnlyActiveBranch(): bool
280
    {
281
        return $this->onlyActiveBranch;
34✔
282
    }
283

284
    /**
285
     * Sets which partial view script to use for rendering menu.
286
     *
287
     * @param array<int, string>|ModelInterface|string|null $partial partial view script or null. If an array is given, the first value is used for the partial view script.
288
     *
289
     * @throws void
290
     */
291
    #[Override]
6✔
292
    public function setPartial($partial): static
293
    {
294
        if (
295
            $partial === null
6✔
296
            || is_string($partial)
6✔
297
            || is_array($partial)
4✔
298
            || $partial instanceof ModelInterface
6✔
299
        ) {
300
            $this->partial = $partial;
6✔
301
        }
302

303
        return $this;
6✔
304
    }
305

306
    /**
307
     * Returns partial view script to use for rendering menu.
308
     *
309
     * @return array<int, string>|ModelInterface|string|null
310
     *
311
     * @throws void
312
     */
313
    #[Override]
20✔
314
    public function getPartial(): array | ModelInterface | string | null
315
    {
316
        return $this->partial;
20✔
317
    }
318

319
    /**
320
     * Enables/disables rendering of parents when only rendering active branch.
321
     *
322
     * See {@link setOnlyActiveBranch()} for more information.
323
     *
324
     * @param bool $flag [optional] render parents when rendering active branch
325
     *
326
     * @throws void
327
     */
328
    #[Override]
5✔
329
    public function setRenderParents(bool $flag = true): static
330
    {
331
        $this->renderParents = $flag;
5✔
332

333
        return $this;
5✔
334
    }
335

336
    /**
337
     * Returns flag indicating whether parents should be rendered when rendering only the active branch.
338
     *
339
     * By default, this value is true.
340
     *
341
     * @throws void
342
     */
343
    #[Override]
39✔
344
    public function getRenderParents(): bool
345
    {
346
        return $this->renderParents;
39✔
347
    }
348

349
    /**
350
     * Sets CSS class to use for the first 'ul' element when rendering.
351
     *
352
     * @param string $ulClass CSS class to set
353
     *
354
     * @throws void
355
     */
356
    #[Override]
2✔
357
    public function setUlClass(string $ulClass): static
358
    {
359
        $this->ulClass = $ulClass;
2✔
360

361
        return $this;
2✔
362
    }
363

364
    /**
365
     * Returns CSS class to use for the first 'ul' element when rendering.
366
     *
367
     * @throws void
368
     */
369
    #[Override]
42✔
370
    public function getUlClass(): string
371
    {
372
        return $this->ulClass;
42✔
373
    }
374

375
    /**
376
     * Sets CSS class to use for the 'li' elements when rendering.
377
     *
378
     * @param string $liClass CSS class to set
379
     *
380
     * @throws void
381
     */
382
    #[Override]
1✔
383
    public function setLiClass(string $liClass): static
384
    {
385
        $this->liClass = $liClass;
1✔
386

387
        return $this;
1✔
388
    }
389

390
    /**
391
     * Returns CSS class to use for the 'li' elements when rendering.
392
     *
393
     * @throws void
394
     */
395
    #[Override]
42✔
396
    public function getLiClass(): string
397
    {
398
        return $this->liClass;
42✔
399
    }
400

401
    /**
402
     * Sets CSS class to use for the active 'li' element when rendering.
403
     *
404
     * @param string $liActiveClass CSS class to set
405
     *
406
     * @throws void
407
     */
408
    #[Override]
2✔
409
    public function setLiActiveClass(string $liActiveClass): static
410
    {
411
        $this->liActiveClass = $liActiveClass;
2✔
412

413
        return $this;
2✔
414
    }
415

416
    /**
417
     * Returns CSS class to use for the active 'li' element when rendering.
418
     *
419
     * @throws void
420
     */
421
    #[Override]
42✔
422
    public function getLiActiveClass(): string
423
    {
424
        return $this->liActiveClass;
42✔
425
    }
426

427
    /**
428
     * Renders helper.
429
     *
430
     * Renders a HTML 'ul' for the given $container. If $container is not given,
431
     * the container registered in the helper will be used.
432
     *
433
     * Available $options:
434
     *
435
     * @param ContainerInterface<PageInterface>|string|null $container [optional] container to create menu from. Default is to use the container retrieved from {@link getContainer()}.
436
     * @param array<string, bool|int|string|null>           $options   [optional] options for controlling rendering
437
     * @phpstan-param array{indent?: int|string|null, ulClass?: string|null, liClass?: string|null, minDepth?: int|null, maxDepth?: int|null, onlyActiveBranch?: bool, renderParents?: bool, escapeLabels?: bool, addClassToListItem?: bool, liActiveClass?: string|null} $options
438
     *
439
     * @throws Exception\RuntimeException
440
     * @throws Exception\InvalidArgumentException
441
     */
442
    #[Override]
43✔
443
    public function renderMenu(ContainerInterface | string | null $container = null, array $options = []): string
444
    {
445
        try {
446
            $container = $this->containerParser->parseContainer($container);
43✔
447
        } catch (InvalidArgumentException $e) {
1✔
448
            throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
1✔
449
        }
450

451
        if ($container === null) {
42✔
452
            $container = $this->getContainer();
30✔
453
        }
454

455
        $options = $this->normalizeOptions($options);
42✔
456

457
        assert(is_string($options['ulClass']));
42✔
458
        assert(is_string($options['liClass']));
42✔
459
        assert(is_string($options['indent']));
42✔
460
        assert(is_int($options['minDepth']));
42✔
461
        assert(is_bool($options['onlyActiveBranch']));
42✔
462
        assert(is_bool($options['renderParents']));
42✔
463
        assert(is_bool($options['escapeLabels']));
42✔
464
        assert(is_bool($options['addClassToListItem']));
42✔
465
        assert(is_string($options['liActiveClass']));
42✔
466

467
        assert($container instanceof ContainerInterface);
42✔
468

469
        if ($options['onlyActiveBranch'] && !$options['renderParents']) {
42✔
470
            return $this->renderDeepestMenu(
7✔
471
                container: $container,
7✔
472
                ulClass: $options['ulClass'],
7✔
473
                liCssClass: $options['liClass'],
7✔
474
                indent: $options['indent'],
7✔
475
                minDepth: $options['minDepth'],
7✔
476
                maxDepth: $options['maxDepth'],
7✔
477
                escapeLabels: $options['escapeLabels'],
7✔
478
                addClassToListItem: $options['addClassToListItem'],
7✔
479
                liActiveClass: $options['liActiveClass'],
7✔
480
            );
7✔
481
        }
482

483
        return $this->renderNormalMenu(
35✔
484
            container: $container,
35✔
485
            ulClass: $options['ulClass'],
35✔
486
            liCssClass: $options['liClass'],
35✔
487
            indent: $options['indent'],
35✔
488
            minDepth: $options['minDepth'],
35✔
489
            maxDepth: $options['maxDepth'],
35✔
490
            onlyActive: $options['onlyActiveBranch'],
35✔
491
            escapeLabels: $options['escapeLabels'],
35✔
492
            addClassToListItem: $options['addClassToListItem'],
35✔
493
            liActiveClass: $options['liActiveClass'],
35✔
494
        );
35✔
495
    }
496

497
    /**
498
     * Renders the inner-most sub menu for the active page in the $container.
499
     *
500
     * This is a convenience method which is equivalent to the following call:
501
     * <code>
502
     * renderMenu($container, array(
503
     *     'indent'           => $indent,
504
     *     'ulClass'          => $ulClass,
505
     *     'liClass'          => $liClass,
506
     *     'minDepth'         => null,
507
     *     'maxDepth'         => null,
508
     *     'onlyActiveBranch' => true,
509
     *     'renderParents'    => false,
510
     *     'liActiveClass'    => $liActiveClass
511
     * ));
512
     * </code>
513
     *
514
     * @param ContainerInterface<PageInterface>|null $container     [optional] container to render. Default is to render the container registered in the helper.
515
     * @param string|null                            $ulClass       [optional] CSS class to use for UL element. Default is to use the value from {@link getUlClass()}.
516
     * @param string|null                            $liClass       [optional] CSS class to use for LI elements. Default is to use the value from {@link getLiClass()}.
517
     * @param int|string|null                        $indent        [optional] indentation as a string or number of spaces. Default is to use the value retrieved from {@link getIndent()}.
518
     * @param string|null                            $liActiveClass [optional] CSS class to use for UL
519
     *
520
     * @throws Exception\RuntimeException
521
     * @throws Exception\InvalidArgumentException
522
     */
523
    #[Override]
1✔
524
    public function renderSubMenu(
525
        ContainerInterface | null $container = null,
526
        string | null $ulClass = null,
527
        string | null $liClass = null,
528
        int | string | null $indent = null,
529
        string | null $liActiveClass = null,
530
    ): string {
531
        $this->setMaxDepth(null);
1✔
532
        $this->setMinDepth(null);
1✔
533
        $this->setRenderParents(false);
1✔
534
        $this->setAddClassToListItem(false);
1✔
535

536
        return $this->renderMenu(
1✔
537
            $container,
1✔
538
            [
1✔
539
                'indent' => $indent,
1✔
540
                'ulClass' => $ulClass,
1✔
541
                'liClass' => $liClass,
1✔
542
                'onlyActiveBranch' => true,
1✔
543
                'escapeLabels' => true,
1✔
544
                'liActiveClass' => $liActiveClass,
1✔
545
            ],
1✔
546
        );
1✔
547
    }
548

549
    /**
550
     * Normalizes given render options.
551
     *
552
     * @param array<string, bool|int|string|null> $options [optional] options for controlling rendering
553
     * @phpstan-param array{indent?: int|string|null, ulClass?: string|null, liClass?: string|null, minDepth?: int|null, maxDepth?: int|null, onlyActiveBranch?: bool, renderParents?: bool, escapeLabels?: bool, addClassToListItem?: bool, liActiveClass?: string|null} $options
554
     *
555
     * @return array<string, bool|int|string|null>
556
     * @phpstan-return array{indent: string, ulClass: string, liClass: string, minDepth: int|null, maxDepth: int|null, onlyActiveBranch: bool, renderParents: bool, escapeLabels: bool, addClassToListItem: bool, liActiveClass: string}
557
     *
558
     * @throws void
559
     */
560
    protected function normalizeOptions(array $options = []): array
42✔
561
    {
562
        if (isset($options['indent'])) {
42✔
563
            assert(is_int($options['indent']) || is_string($options['indent']));
1✔
564
            $options['indent'] = $this->getWhitespace($options['indent']);
1✔
565
        } else {
566
            $options['indent'] = $this->getIndent();
42✔
567
        }
568

569
        if (!array_key_exists('ulClass', $options) || $options['ulClass'] === null) {
42✔
570
            $options['ulClass'] = $this->getUlClass();
41✔
571
        }
572

573
        if (!array_key_exists('liClass', $options) || $options['liClass'] === null) {
42✔
574
            $options['liClass'] = $this->getLiClass();
41✔
575
        }
576

577
        if (!array_key_exists('minDepth', $options) || $options['minDepth'] === null) {
42✔
578
            $options['minDepth'] = $this->getMinDepth();
36✔
579
        }
580

581
        if ($options['minDepth'] < 0 || $options['minDepth'] === null) {
42✔
582
            $options['minDepth'] = 0;
×
583
        }
584

585
        if (!array_key_exists('maxDepth', $options) || $options['maxDepth'] === null) {
42✔
586
            $options['maxDepth'] = $this->getMaxDepth();
37✔
587
        }
588

589
        if (!array_key_exists('onlyActiveBranch', $options)) {
42✔
590
            $options['onlyActiveBranch'] = $this->getOnlyActiveBranch();
33✔
591
        }
592

593
        if (!array_key_exists('escapeLabels', $options)) {
42✔
594
            $options['escapeLabels'] = $this->getEscapeLabels();
39✔
595
        }
596

597
        if (!array_key_exists('renderParents', $options)) {
42✔
598
            $options['renderParents'] = $this->getRenderParents();
38✔
599
        }
600

601
        if (!array_key_exists('addClassToListItem', $options)) {
42✔
602
            $options['addClassToListItem'] = $this->getAddClassToListItem();
40✔
603
        }
604

605
        if (!array_key_exists('liActiveClass', $options) || $options['liActiveClass'] === null) {
42✔
606
            $options['liActiveClass'] = $this->getLiActiveClass();
41✔
607
        }
608

609
        return $options;
42✔
610
    }
611

612
    /**
613
     * @param array<string, int|PageInterface|null> $found
614
     * @phpstan-param array{page?: PageInterface|null, depth?: int|null} $found
615
     *
616
     * @throws void
617
     */
618
    protected function isActiveBranch(array $found, PageInterface $page, int | null $maxDepth): bool
8✔
619
    {
620
        if (!array_key_exists('page', $found) || !($found['page'] instanceof PageInterface)) {
8✔
621
            return false;
×
622
        }
623

624
        $foundPage  = $found['page'];
8✔
625
        $foundDepth = $found['depth'] ?? 0;
8✔
626

627
        if ($foundPage->hasPage($page)) {
8✔
628
            // accept if page is a direct child of the active page
629
            return true;
×
630
        }
631

632
        if (
633
            $foundPage->getParent() instanceof ContainerInterface
8✔
634
            && $foundPage->getParent()->hasPage($page)
8✔
635
        ) {
636
            // page is a sibling of the active page...
637
            if (
638
                !$foundPage->hasPages(!$this->renderInvisible)
4✔
639
                || is_int($maxDepth) && $foundDepth + 1 > $maxDepth
4✔
640
            ) {
641
                // accept if active page has no children, or the
642
                // children are too deep to be rendered
643
                return true;
4✔
644
            }
645
        }
646

647
        return false;
8✔
648
    }
649

650
    /**
651
     * Render a partial with the given "model".
652
     *
653
     * @param array<int|string, mixed>                      $params
654
     * @param ContainerInterface<PageInterface>|string|null $container
655
     * @param array<int, string>|ModelInterface|string|null $partial
656
     *
657
     * @throws Exception\RuntimeException         if no partial provided
658
     * @throws Exception\InvalidArgumentException if partial is invalid array
659
     */
660
    private function renderPartialModel(
5✔
661
        array $params,
662
        ContainerInterface | string | null $container,
663
        array | ModelInterface | string | null $partial,
664
    ): string {
665
        if ($partial === null) {
5✔
666
            $partial = $this->getPartial();
2✔
667
        }
668

669
        if ($partial === null || $partial === '' || $partial === []) {
5✔
670
            throw new Exception\RuntimeException(
×
671
                'Unable to render menu: No partial view script provided',
×
672
            );
×
673
        }
674

675
        if (is_array($partial)) {
5✔
676
            if (count($partial) !== 2) {
3✔
677
                throw new Exception\InvalidArgumentException(
1✔
678
                    'Unable to render menu: A view partial supplied as '
1✔
679
                    . 'an array must contain one value: the partial view script',
1✔
680
                );
1✔
681
            }
682

683
            $partial = $partial[0];
2✔
684
        }
685

686
        try {
687
            $container = $this->containerParser->parseContainer($container);
4✔
688
        } catch (InvalidArgumentException $e) {
×
689
            throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
×
690
        }
691

692
        if ($container === null) {
4✔
693
            $container = $this->getContainer();
3✔
694
        }
695

696
        return $this->renderer->render(
4✔
697
            $partial,
4✔
698
            array_merge($params, ['container' => $container]),
4✔
699
        );
4✔
700
    }
701

702
    /**
703
     * Renders the deepest active menu within [$minDepth, $maxDepth], (called from {@link renderMenu()}).
704
     *
705
     * @param ContainerInterface<PageInterface> $container          container to render
706
     * @param string                            $ulClass            CSS class for first UL
707
     * @param string                            $liCssClass         CSS class for all LI
708
     * @param string                            $indent             initial indentation
709
     * @param int                               $minDepth           minimum depth
710
     * @param int|null                          $maxDepth           maximum depth
711
     * @param bool                              $escapeLabels       Whether or not to escape the labels
712
     * @param bool                              $addClassToListItem Whether or not page class applied to <li> element
713
     * @param string                            $liActiveClass      CSS class for active LI
714
     *
715
     * @throws Exception\RuntimeException
716
     * @throws Exception\InvalidArgumentException
717
     */
718
    private function renderDeepestMenu(
7✔
719
        ContainerInterface $container,
720
        string $ulClass,
721
        string $liCssClass,
722
        string $indent,
723
        int $minDepth,
724
        int | null $maxDepth,
725
        bool $escapeLabels,
726
        bool $addClassToListItem,
727
        string $liActiveClass,
728
    ): string {
729
        $active = $this->findActive($container, $minDepth - 1, $maxDepth);
7✔
730

731
        if (!array_key_exists('page', $active) || !($active['page'] instanceof PageInterface)) {
7✔
732
            return '';
×
733
        }
734

735
        $activePage = $active['page'];
7✔
736

737
        // special case if active page is one below minDepth
738
        if (!array_key_exists('depth', $active) || $active['depth'] < $minDepth) {
7✔
739
            if (!$activePage->hasPages(!$this->renderInvisible)) {
1✔
740
                return '';
×
741
            }
742
        } elseif (!$active['page']->hasPages(!$this->renderInvisible)) {
6✔
743
            // found pages has no children; render siblings
744
            $activePage = $active['page']->getParent();
4✔
745
        } elseif (is_int($maxDepth) && $active['depth'] + 1 > $maxDepth) {
2✔
746
            // children are below max depth; render siblings
747
            $activePage = $active['page']->getParent();
2✔
748
        }
749

750
        if ($ulClass !== '') {
7✔
751
            $ulClass = ($this->escaper)($ulClass);
7✔
752
            assert(is_string($ulClass));
7✔
753

754
            $ulClass = ' class="' . $ulClass . '"';
7✔
755
        }
756

757
        $html = $indent . '<ul' . $ulClass . '>' . PHP_EOL;
7✔
758

759
        assert(
7✔
760
            $activePage instanceof ContainerInterface,
7✔
761
            sprintf(
7✔
762
                '$activePage should be an Instance of %s, but was %s',
7✔
763
                ContainerInterface::class,
7✔
764
                get_debug_type($activePage),
7✔
765
            ),
7✔
766
        );
7✔
767

768
        foreach ($activePage as $subPage) {
7✔
769
            if (!$this->accept($subPage)) {
7✔
770
                continue;
2✔
771
            }
772

773
            // render li tag and page
774
            $liClasses = [];
7✔
775

776
            // Is page active?
777
            if ($subPage->isActive(true)) {
7✔
778
                $liClasses[] = $liActiveClass;
6✔
779
            }
780

781
            if ($liCssClass) {
7✔
782
                $liClasses[] = $liCssClass;
×
783
            }
784

785
            if ($subPage->getLiClass()) {
7✔
786
                $liClasses[] = $subPage->getLiClass();
×
787
            }
788

789
            // Add CSS class from page to <li>
790
            if ($addClassToListItem && $subPage->getClass()) {
7✔
791
                $liClasses[] = $subPage->getClass();
1✔
792
            }
793

794
            $liClass = '';
7✔
795

796
            if ($liClasses !== []) {
7✔
797
                $liClass = ($this->escaper)(implode(' ', $liClasses));
6✔
798
                assert(is_string($liClass));
6✔
799

800
                $liClass = ' class="' . $liClass . '"';
6✔
801
            }
802

803
            $html .= $indent . '    <li' . $liClass . '>' . PHP_EOL;
7✔
804

805
            try {
806
                $subPageHtml = $this->htmlify->toHtml(
7✔
807
                    static::class,
7✔
808
                    $subPage,
7✔
809
                    $escapeLabels,
7✔
810
                    $addClassToListItem,
7✔
811
                );
7✔
812
            } catch (RuntimeException $e) {
×
813
                throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
×
814
            }
815

816
            $html .= $indent . '        ' . $subPageHtml . PHP_EOL;
7✔
817
            $html .= $indent . '    </li>' . PHP_EOL;
7✔
818
        }
819

820
        return $html . $indent . '</ul>';
7✔
821
    }
822

823
    /**
824
     * Renders a normal menu (called from {@link renderMenu()}).
825
     *
826
     * @param ContainerInterface<PageInterface> $container          container to render
827
     * @param string                            $ulClass            CSS class for first UL
828
     * @param string                            $liCssClass         CSS class for all LI
829
     * @param string                            $indent             initial indentation
830
     * @param int|null                          $minDepth           minimum depth
831
     * @param int|null                          $maxDepth           maximum depth
832
     * @param bool                              $onlyActive         render only active branch?
833
     * @param bool                              $escapeLabels       Whether or not to escape the labels
834
     * @param bool                              $addClassToListItem Whether or not page class applied to <li> element
835
     * @param string                            $liActiveClass      CSS class for active LI
836
     *
837
     * @throws Exception\RuntimeException
838
     * @throws Exception\InvalidArgumentException
839
     */
840
    private function renderNormalMenu(
35✔
841
        ContainerInterface $container,
842
        string $ulClass,
843
        string $liCssClass,
844
        string $indent,
845
        int | null $minDepth,
846
        int | null $maxDepth,
847
        bool $onlyActive,
848
        bool $escapeLabels,
849
        bool $addClassToListItem,
850
        string $liActiveClass,
851
    ): string {
852
        $html = '';
35✔
853

854
        // find deepest active
855
        $found = $this->findActive($container, $minDepth, $maxDepth);
35✔
856

857
        // create iterator
858
        $iterator = new RecursiveIteratorIterator($container, RecursiveIteratorIterator::SELF_FIRST);
35✔
859

860
        if (is_int($maxDepth)) {
35✔
861
            $iterator->setMaxDepth($maxDepth);
8✔
862
        }
863

864
        // iterate container
865
        $prevDepth = -1;
35✔
866

867
        foreach ($iterator as $page) {
35✔
868
            assert(
34✔
869
                $page instanceof PageInterface,
34✔
870
                sprintf(
34✔
871
                    '$page should be an Instance of %s, but was %s',
34✔
872
                    PageInterface::class,
34✔
873
                    get_debug_type($page),
34✔
874
                ),
34✔
875
            );
34✔
876

877
            $depth = $iterator->getDepth();
34✔
878

879
            if ($depth < $minDepth || !$this->accept($page)) {
34✔
880
                // page is below minDepth or not accepted by Authorization/Visibility
881
                continue;
24✔
882
            }
883

884
            $isActive = $page->isActive(true);
34✔
885

886
            if ($onlyActive && !$isActive) {
34✔
887
                // page is not active itself, but might be in the active branch
888
                $accept = $this->isActiveBranch($found, $page, $maxDepth);
8✔
889

890
                if (!$accept) {
8✔
891
                    continue;
8✔
892
                }
893
            }
894

895
            // make sure indentation is correct
896
            $depth   -= $minDepth;
34✔
897
            $myIndent = $indent . str_repeat('        ', $depth);
34✔
898

899
            if ($depth > $prevDepth) {
34✔
900
                // start new ul tag
901
                if ($ulClass && $depth === 0) {
34✔
902
                    $ulClass = ($this->escaper)($ulClass);
34✔
903
                    assert(is_string($ulClass));
34✔
904

905
                    $ulClass = ' class="' . $ulClass . '"';
34✔
906
                } else {
907
                    $ulClass = '';
24✔
908
                }
909

910
                $html .= $myIndent . '<ul' . $ulClass . '>' . PHP_EOL;
34✔
911
            } elseif ($prevDepth > $depth) {
33✔
912
                // close li/ul tags until we're at current depth
913
                for ($i = $prevDepth; $i > $depth; --$i) {
24✔
914
                    $ind   = $indent . str_repeat('        ', $i);
24✔
915
                    $html .= $ind . '    </li>' . PHP_EOL;
24✔
916
                    $html .= $ind . '</ul>' . PHP_EOL;
24✔
917
                }
918

919
                // close previous li tag
920
                $html .= $myIndent . '    </li>' . PHP_EOL;
24✔
921
            } else {
922
                // close previous li tag
923
                $html .= $myIndent . '    </li>' . PHP_EOL;
33✔
924
            }
925

926
            // render li tag and page
927
            $liClasses = [];
34✔
928

929
            // Is page active?
930
            if ($isActive) {
34✔
931
                $liClasses[] = $liActiveClass;
32✔
932
            }
933

934
            if ($liCssClass) {
34✔
935
                $liClasses[] = $liCssClass;
1✔
936
            }
937

938
            if ($page->getLiClass()) {
34✔
939
                $liClasses[] = $page->getLiClass();
×
940
            }
941

942
            // Add CSS class from page to <li>
943
            if ($addClassToListItem && $page->getClass()) {
34✔
944
                $liClasses[] = $page->getClass();
1✔
945
            }
946

947
            $liClass = '';
34✔
948

949
            if ($liClasses !== []) {
34✔
950
                $liClass = ($this->escaper)(implode(' ', $liClasses));
32✔
951
                assert(is_string($liClass));
32✔
952

953
                $liClass = ' class="' . $liClass . '"';
32✔
954
            }
955

956
            try {
957
                $pageHtml = $this->htmlify->toHtml(
34✔
958
                    static::class,
34✔
959
                    $page,
34✔
960
                    $escapeLabels,
34✔
961
                    $addClassToListItem,
34✔
962
                );
34✔
963
            } catch (RuntimeException $e) {
1✔
964
                throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
1✔
965
            }
966

967
            $html .= $myIndent . '    <li' . $liClass . '>' . PHP_EOL
33✔
968
                . $myIndent . '        ' . $pageHtml . PHP_EOL;
33✔
969

970
            // store as previous depth for next iteration
971
            $prevDepth = $depth;
33✔
972
        }
973

974
        if ($html) {
34✔
975
            // done iterating container; close open ul/li tags
976
            for ($i = $prevDepth + 1; 0 < $i; --$i) {
33✔
977
                $myIndent = $indent . str_repeat('        ', $i - 1);
33✔
978
                $html    .= $myIndent . '    </li>' . PHP_EOL
33✔
979
                    . $myIndent . '</ul>' . PHP_EOL;
33✔
980
            }
981

982
            $html = rtrim($html, PHP_EOL);
33✔
983
        }
984

985
        return $html;
34✔
986
    }
987
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc