• 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

93.43
/src/View/Helper/Navigation/AbstractBreadcrumbs.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\I18n\View\Helper\Translate;
18
use Laminas\Stdlib\Exception\InvalidArgumentException;
19
use Laminas\View\Exception;
20
use Laminas\View\Helper\EscapeHtml;
21
use Laminas\View\Model\ModelInterface;
22
use Mimmi20\LaminasView\Helper\PartialRenderer\Helper\PartialRendererInterface;
23
use Mimmi20\Mezzio\Navigation\ContainerInterface;
24
use Mimmi20\Mezzio\Navigation\Page\PageInterface;
25
use Mimmi20\NavigationHelper\ContainerParser\ContainerParserInterface;
26
use Mimmi20\NavigationHelper\Htmlify\HtmlifyInterface;
27
use Override;
28

29
use function array_merge;
30
use function array_reverse;
31
use function array_unshift;
32
use function assert;
33
use function count;
34
use function get_debug_type;
35
use function implode;
36
use function is_array;
37
use function is_string;
38
use function sprintf;
39

40
/**
41
 * Helper for printing breadcrumbs.
42
 *
43
 * phpcs:disable SlevomatCodingStandard.Classes.TraitUseDeclaration.MultipleTraitsPerDeclaration
44
 */
45
abstract class AbstractBreadcrumbs extends AbstractHelper implements BreadcrumbsInterface
46
{
47
    /**
48
     * Whether last page in breadcrumb should be hyperlinked.
49
     */
50
    protected bool $linkLast = false;
51

52
    /**
53
     * Partial view script to use for rendering menu.
54
     *
55
     * @var array<int, string>|ModelInterface|string|null
56
     */
57
    protected array | ModelInterface | string | null $partial = null;
58

59
    /**
60
     * Breadcrumbs separator string.
61
     */
62
    protected string $separator = ' &gt; ';
63

64
    /** @throws void */
65
    public function __construct(
50✔
66
        HtmlifyInterface $htmlify,
67
        ContainerParserInterface $containerParser,
68
        private readonly EscapeHtml $escaper,
69
        private readonly PartialRendererInterface $renderer,
70
        private readonly Translate | null $translator = null,
71
    ) {
72
        parent::__construct($htmlify, $containerParser);
50✔
73
    }
74

75
    /**
76
     * Renders helper.
77
     *
78
     * Implements {@link ViewHelperInterface::render()}.
79
     *
80
     * @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()}.
81
     *
82
     * @throws Exception\InvalidArgumentException
83
     * @throws Exception\RuntimeException
84
     */
85
    #[Override]
16✔
86
    public function render(ContainerInterface | string | null $container = null): string
87
    {
88
        $partial = $this->getPartial();
16✔
89

90
        if ($partial) {
16✔
91
            return $this->renderPartial($container, $partial);
4✔
92
        }
93

94
        return $this->renderStraight($container);
12✔
95
    }
96

97
    /**
98
     * Renders the given $container by invoking the partial view helper.
99
     *
100
     * The container will simply be passed on as a model to the view script
101
     * as-is, and will be available in the partial script as 'container', e.g.
102
     * <code>echo 'Number of pages: ', count($this->container);</code>.
103
     *
104
     * @param ContainerInterface<PageInterface>|string|null $container [optional] container to pass to view script. Default is to use the container registered in the helper.
105
     * @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.
106
     *
107
     * @throws Exception\RuntimeException         if no partial provided
108
     * @throws Exception\InvalidArgumentException if partial is invalid array
109
     */
110
    #[Override]
4✔
111
    public function renderPartial(
112
        ContainerInterface | string | null $container = null,
113
        array | ModelInterface | string | null $partial = null,
114
    ): string {
115
        return $this->renderPartialModel([], $container, $partial);
4✔
116
    }
117

118
    /**
119
     * Renders the given $container by invoking the partial view helper with the given parameters as the model.
120
     *
121
     * The container will simply be passed on as a model to the view script
122
     * as-is, and will be available in the partial script as 'container', e.g.
123
     * <code>echo 'Number of pages: ', count($this->container);</code>.
124
     *
125
     * Any parameters provided will be passed to the partial via the view model.
126
     *
127
     * @param array<string, array<int|string, mixed>|string> $params
128
     * @param ContainerInterface<PageInterface>|string|null  $container [optional] container to pass to view script. Default is to use the container registered in the helper.
129
     * @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.
130
     *
131
     * @throws Exception\RuntimeException         if no partial provided
132
     * @throws Exception\InvalidArgumentException if partial is invalid array
133
     */
134
    #[Override]
1✔
135
    public function renderPartialWithParams(
136
        array $params = [],
137
        ContainerInterface | string | null $container = null,
138
        array | ModelInterface | string | null $partial = null,
139
    ): string {
140
        return $this->renderPartialModel($params, $container, $partial);
1✔
141
    }
142

143
    /**
144
     * Renders breadcrumbs by chaining 'a' elements with the separator
145
     * registered in the helper.
146
     *
147
     * @param ContainerInterface<PageInterface>|string|null $container [optional] container to render. Default is to render the container registered in the helper.
148
     *
149
     * @throws Exception\InvalidArgumentException
150
     * @throws Exception\RuntimeException
151
     */
152
    #[Override]
17✔
153
    public function renderStraight(ContainerInterface | string | null $container = null): string
154
    {
155
        try {
156
            $container = $this->containerParser->parseContainer($container);
17✔
157
        } catch (InvalidArgumentException $e) {
1✔
158
            throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
1✔
159
        }
160

161
        if (!$container instanceof ContainerInterface) {
16✔
162
            $container = $this->getContainer();
9✔
163
        }
164

165
        // find deepest active
166
        $active = $this->findActive($container);
16✔
167

168
        if (!$active) {
16✔
169
            return '';
3✔
170
        }
171

172
        $active = $active['page'];
13✔
173

174
        assert(
13✔
175
            $active instanceof PageInterface,
13✔
176
            sprintf(
13✔
177
                '$active should be an Instance of %s, but was %s',
13✔
178
                PageInterface::class,
13✔
179
                get_debug_type($active),
13✔
180
            ),
13✔
181
        );
13✔
182

183
        $html = [];
13✔
184

185
        // put the deepest active page last in breadcrumbs
186
        if ($this->getLinkLast()) {
13✔
187
            try {
188
                $entryHtml = $this->htmlify->toHtml(static::class, $active);
3✔
189
            } catch (RuntimeException $e) {
1✔
190
                throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
1✔
191
            }
192

193
            $html[] = $this->renderBreadcrumbItem(
2✔
194
                $entryHtml,
2✔
195
                $active->getLiClass() ?? '',
2✔
196
                $active->isActive(),
2✔
197
            );
2✔
198
        } else {
199
            $label = (string) $active->getLabel();
10✔
200

201
            if ($this->translator !== null) {
10✔
202
                try {
203
                    $label = ($this->translator)($label, $active->getTextDomain());
1✔
204
                } catch (RuntimeException $e) {
1✔
205
                    throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
1✔
206
                }
207

208
                assert(is_string($label));
×
209
            }
210

211
            $label = ($this->escaper)($label);
9✔
212
            assert(is_string($label));
9✔
213

214
            $html[] = $this->renderBreadcrumbItem(
9✔
215
                $label,
9✔
216
                $active->getLiClass() ?? '',
9✔
217
                $active->isActive(),
9✔
218
            );
9✔
219
        }
220

221
        // walk back to root
222
        while ($parent = $active->getParent()) {
11✔
223
            if ($parent instanceof PageInterface) {
11✔
224
                try {
225
                    $entryHtml = $this->htmlify->toHtml(static::class, $parent);
10✔
226
                } catch (RuntimeException $e) {
×
227
                    throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
×
228
                }
229

230
                // prepend crumb to html
231
                $entry = $this->renderBreadcrumbItem(
10✔
232
                    $entryHtml,
10✔
233
                    $parent->getLiClass() ?? '',
10✔
234
                    $parent->isActive(),
10✔
235
                );
10✔
236
                array_unshift($html, $entry);
10✔
237
            }
238

239
            if ($parent === $container) {
11✔
240
                // at the root of the given container
241
                break;
11✔
242
            }
243

244
            $active = $parent;
10✔
245
        }
246

247
        return $this->combineRendered($html);
11✔
248
    }
249

250
    /**
251
     * Sets whether last page in breadcrumbs should be hyperlinked.
252
     *
253
     * @param bool $linkLast whether last page should be hyperlinked
254
     *
255
     * @throws void
256
     */
257
    #[Override]
7✔
258
    public function setLinkLast(bool $linkLast): static
259
    {
260
        $this->linkLast = $linkLast;
7✔
261

262
        return $this;
7✔
263
    }
264

265
    /**
266
     * Returns whether last page in breadcrumbs should be hyperlinked.
267
     *
268
     * @throws void
269
     */
270
    #[Override]
14✔
271
    public function getLinkLast(): bool
272
    {
273
        return $this->linkLast;
14✔
274
    }
275

276
    /**
277
     * Sets which partial view script to use for rendering menu.
278
     *
279
     * @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.
280
     *
281
     * @throws void
282
     */
283
    #[Override]
7✔
284
    public function setPartial($partial): static
285
    {
286
        if (
287
            $partial === null
7✔
288
            || is_string($partial)
7✔
289
            || is_array($partial)
3✔
290
            || $partial instanceof ModelInterface
7✔
291
        ) {
292
            $this->partial = $partial;
7✔
293
        }
294

295
        return $this;
7✔
296
    }
297

298
    /**
299
     * Returns partial view script to use for rendering menu.
300
     *
301
     * @return array<int, string>|ModelInterface|string|null
302
     *
303
     * @throws void
304
     */
305
    #[Override]
18✔
306
    public function getPartial(): array | ModelInterface | string | null
307
    {
308
        return $this->partial;
18✔
309
    }
310

311
    /**
312
     * Sets breadcrumb separator.
313
     *
314
     * @param string $separator separator string
315
     *
316
     * @throws void
317
     */
318
    #[Override]
9✔
319
    public function setSeparator(string $separator): static
320
    {
321
        $this->separator = $separator;
9✔
322

323
        return $this;
9✔
324
    }
325

326
    /**
327
     * Returns breadcrumb separator.
328
     *
329
     * @return string breadcrumb separator
330
     *
331
     * @throws void
332
     */
333
    #[Override]
16✔
334
    public function getSeparator(): string
335
    {
336
        return $this->separator;
16✔
337
    }
338

339
    /**
340
     * Returns minimum depth a page must have to be included when rendering
341
     *
342
     * @throws void
343
     *
344
     * @api
345
     */
346
    #[Override]
21✔
347
    public function getMinDepth(): int
348
    {
349
        if ($this->minDepth === null || $this->minDepth < 0) {
21✔
350
            return 1;
15✔
351
        }
352

353
        return $this->minDepth;
7✔
354
    }
355

356
    /**
357
     * @throws void
358
     *
359
     * @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
360
     */
361
    protected function renderBreadcrumbItem(string $content, string $liClass = '', bool $active = false): string
11✔
362
    {
363
        return $content;
11✔
364
    }
365

366
    /**
367
     * @param array<int|string, string> $html
368
     *
369
     * @throws void
370
     */
371
    protected function combineRendered(array $html): string
11✔
372
    {
373
        return $html !== [] ? $this->getIndent() . implode($this->renderSeparator(), $html) : '';
11✔
374
    }
375

376
    /** @throws void */
377
    protected function renderSeparator(): string
11✔
378
    {
379
        return $this->getSeparator();
11✔
380
    }
381

382
    /**
383
     * Render a partial with the given "model".
384
     *
385
     * @param array<string, array<mixed>|string>            $params
386
     * @param ContainerInterface<PageInterface>|string|null $container
387
     * @param array<int, string>|ModelInterface|string|null $partial
388
     *
389
     * @throws Exception\RuntimeException         if no partial provided
390
     * @throws Exception\InvalidArgumentException if partial is invalid array
391
     */
392
    private function renderPartialModel(
5✔
393
        array $params,
394
        ContainerInterface | string | null $container,
395
        array | ModelInterface | string | null $partial,
396
    ): string {
397
        if ($partial === null) {
5✔
398
            $partial = $this->getPartial();
1✔
399
        }
400

401
        if ($partial === null || $partial === '' || $partial === []) {
5✔
402
            throw new Exception\RuntimeException(
×
403
                'Unable to render breadcrumbs: No partial view script provided',
×
404
            );
×
405
        }
406

407
        if (is_array($partial)) {
5✔
408
            if (count($partial) !== 2) {
2✔
409
                throw new Exception\InvalidArgumentException(
1✔
410
                    'Unable to render breadcrumbs: A view partial supplied as '
1✔
411
                    . 'an array must contain one value: the partial view script',
1✔
412
                );
1✔
413
            }
414

415
            $partial = $partial[0];
1✔
416
        }
417

418
        try {
419
            $container = $this->containerParser->parseContainer($container);
4✔
420
        } catch (InvalidArgumentException $e) {
×
421
            throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
×
422
        }
423

424
        if (!$container instanceof ContainerInterface) {
4✔
425
            $container = $this->getContainer();
4✔
426
        }
427

428
        /** @var array<string, array<mixed>> $model */
429
        $model  = array_merge($params, ['pages' => []], ['separator' => $this->getSeparator()]);
4✔
430
        $active = $this->findActive($container);
4✔
431

432
        if ($active !== []) {
4✔
433
            $active = $active['page'];
4✔
434

435
            assert(
4✔
436
                $active instanceof PageInterface,
4✔
437
                sprintf(
4✔
438
                    '$active should be an Instance of %s, but was %s',
4✔
439
                    PageInterface::class,
4✔
440
                    get_debug_type($active),
4✔
441
                ),
4✔
442
            );
4✔
443

444
            $model['pages'][] = $active;
4✔
445

446
            while ($parent = $active->getParent()) {
4✔
447
                if (!$parent instanceof PageInterface) {
4✔
448
                    break;
4✔
449
                }
450

451
                $model['pages'][] = $parent;
4✔
452

453
                if ($parent === $container) {
4✔
454
                    // break if at the root of the given container
455
                    break;
×
456
                }
457

458
                $active = $parent;
4✔
459
            }
460

461
            $model['pages'] = array_reverse($model['pages']);
4✔
462
        }
463

464
        return $this->renderer->render($partial, $model);
4✔
465
    }
466
}
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