Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.83% covered (warning)
87.83%
296 / 337
47.83% covered (danger)
47.83%
11 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
CollectionPage
87.83% covered (warning)
87.83%
296 / 337
47.83% covered (danger)
47.83%
11 / 23
144.50
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 from
98.28% covered (success)
98.28%
57 / 58
0.00% covered (danger)
0.00%
0 / 1
12
 label
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 searchFields
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 viewLinks
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
3
 headers
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 rows
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
12
 treeNode
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 cells
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
7.03
 createLinks
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 childCreateLinks
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
 childLinks
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 statusList
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 status
55.56% covered (warning)
55.56%
10 / 18
0.00% covered (danger)
0.00%
0 / 1
13.62
 childBlueprints
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 sortKeys
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 columnSort
18.75% covered (danger)
18.75%
3 / 16
0.00% covered (danger)
0.00%
0 / 1
63.64
 blueprints
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 displayValue
60.00% covered (warning)
60.00%
15 / 25
0.00% covered (danger)
0.00%
0 / 1
26.54
 formatDate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 strings
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 items
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 arrayFrom
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Panel;
6
7use Cosray\CollectionListMeta;
8use Cosray\Column;
9use DateTimeImmutable;
10use DateTimeInterface;
11use DateTimeZone;
12use IntlDateFormatter;
13use Stringable;
14use Throwable;
15use Traversable;
16
17final class CollectionPage
18{
19    /**
20     * @param list<array{kind: string, label: string}> $parentStatus
21     * @param list<array{name: string, value: string}> $searchFields
22     * @param list<array{label: string, url: string, active: bool}> $viewLinks
23     * @param list<array{slug: string, name: string, url: string}> $createLinks
24     * @param list<array{label: string, url: ?string, class: string}> $headers
25     * @param list<array{
26     *     uid: string,
27     *     depth: int,
28     *     expanded: bool,
29     *     last: bool,
30     *     published: bool,
31     *     hasChildren: bool,
32     *     cells: list<array{label: string, value: string, class: string, editUrl: ?string}>,
33     *     status: list<array{kind: string, label: string}>,
34     *     childrenUrl: ?string,
35     *     focusedChildrenUrl: ?string,
36     *     childCreateLinks: list<array{slug: string, name: string, url: string}>,
37     *     childLinks: list<array{label: string, url: string}>,
38     * }> $rows
39     */
40    private function __construct(
41        public readonly string $name,
42        public readonly string $title,
43        public readonly ?string $parentTitle,
44        public readonly ?string $parentType,
45        public readonly ?string $parentEditUrl,
46        public readonly ?string $parentTreeUrl,
47        public readonly array $parentStatus,
48        public readonly CollectionUrls $urls,
49        public readonly CollectionQuery $query,
50        public readonly string $path,
51        public readonly ?string $clearSearchUrl,
52        public readonly ?string $rootUrl,
53        public readonly int $total,
54        public readonly int $pageCount,
55        public readonly int $currentPage,
56        public readonly int $rangeStart,
57        public readonly int $rangeEnd,
58        public readonly bool $showChildren,
59        public readonly bool $treeMode,
60        public readonly array $searchFields,
61        public readonly array $viewLinks,
62        public readonly array $createLinks,
63        public readonly array $headers,
64        public readonly array $rows,
65        public readonly ?string $previousUrl,
66        public readonly ?string $nextUrl,
67    ) {}
68
69    /**
70     * @param iterable<Column> $columns
71     * @param iterable<mixed> $sortKeys
72     * @param iterable<mixed> $blueprints
73     * @param iterable<mixed> $nodes
74     * @param iterable<mixed>|null $createBlueprints
75     */
76    public static function from(
77        string $name,
78        CollectionUrls $urls,
79        iterable $columns,
80        iterable $sortKeys,
81        iterable $blueprints,
82        iterable $nodes,
83        int $total,
84        CollectionListMeta $meta,
85        string $locale,
86        DateTimeZone $timezone,
87        ?string $parentTitle = null,
88        ?string $parentType = null,
89        ?iterable $parentStatus = null,
90        ?iterable $createBlueprints = null,
91    ): self {
92        $query = $urls->query;
93        $nodes = self::items($nodes);
94        $blueprints = self::blueprints($blueprints);
95        $createBlueprints = $createBlueprints === null
96            ? $blueprints
97            : self::blueprints($createBlueprints);
98        $parentTitle = self::label($parentTitle);
99        $parentType = self::label($parentType);
100        $parentStatus = self::statusList($parentStatus ?? []);
101        $headers = self::headers($columns, $sortKeys, $urls);
102        $pageCount = $query->limit > 0 ? max(1, (int) ceil($total / $query->limit)) : 1;
103        $currentPage = $query->limit > 0
104            ? min($pageCount, (int) floor($query->offset / $query->limit) + 1)
105            : 1;
106        $rowCount = count($nodes);
107        $rangeStart = $total === 0 ? 0 : min($query->offset + 1, $total);
108        $rangeEnd = min($query->offset + $rowCount, $total);
109
110        return new self(
111            name: $name,
112            title: $parentTitle ?? $name,
113            parentTitle: $parentTitle,
114            parentType: $parentType,
115            parentEditUrl: $query->parent === null ? null : $urls->edit($query->parent),
116            parentTreeUrl: $query->parent === null ? null : $urls->showInTree($query->parent),
117            parentStatus: $parentStatus,
118            urls: $urls,
119            query: $query,
120            path: $urls->path(),
121            clearSearchUrl: $query->q === ''
122                ? null
123                : $urls->collection(['q' => '', 'offset' => '']),
124            rootUrl: $query->parent === null
125                ? null
126                : $urls->collection([
127                    'parent' => '',
128                    'offset' => '',
129                    'view' => '',
130                    'open' => '',
131                ]),
132            total: $total,
133            pageCount: $pageCount,
134            currentPage: $currentPage,
135            rangeStart: $rangeStart,
136            rangeEnd: $rangeEnd,
137            showChildren: $meta->showChildren,
138            treeMode: $meta->showChildren && $query->view === 'tree',
139            searchFields: self::searchFields($query),
140            viewLinks: self::viewLinks($meta, $query, $urls),
141            createLinks: self::createLinks($createBlueprints, $urls),
142            headers: $headers,
143            rows: self::rows($nodes, $headers, $urls, $meta, $locale, $timezone),
144            previousUrl: $query->offset > 0
145                ? $urls->collection(['offset' => max(0, $query->offset - $query->limit)])
146                : null,
147            nextUrl: ($query->offset + $query->limit) < $total
148                ? $urls->collection(['offset' => $query->offset + $query->limit])
149                : null,
150        );
151    }
152
153    private static function label(?string $label): ?string
154    {
155        $label = trim((string) $label);
156
157        return $label === '' ? null : $label;
158    }
159
160    /** @return list<array{name: string, value: string}> */
161    private static function searchFields(CollectionQuery $query): array
162    {
163        $fields = [];
164
165        if ($query->sort !== '') {
166            $fields[] = ['name' => 'sort', 'value' => $query->sort];
167        }
168
169        if ($query->dir !== '') {
170            $fields[] = ['name' => 'dir', 'value' => $query->dir];
171        }
172
173        if ($query->limit !== 50) {
174            $fields[] = ['name' => 'limit', 'value' => (string) $query->limit];
175        }
176
177        if ($query->parent !== null) {
178            $fields[] = ['name' => 'parent', 'value' => $query->parent];
179        }
180
181        if ($query->view !== $query->defaultView) {
182            $fields[] = ['name' => 'view', 'value' => $query->view];
183        }
184
185        if ($query->open !== []) {
186            $fields[] = ['name' => 'open', 'value' => implode(',', $query->open)];
187        }
188
189        return $fields;
190    }
191
192    /** @return list<array{label: string, url: string, active: bool}> */
193    private static function viewLinks(
194        CollectionListMeta $meta,
195        CollectionQuery $query,
196        CollectionUrls $urls,
197    ): array {
198        if (!$meta->showChildren || $query->parent !== null) {
199            return [];
200        }
201
202        return [
203            [
204                'label' => 'Tree',
205                'url' => $urls->collection([
206                    'view' => 'tree',
207                    'open' => '',
208                    'offset' => '',
209                ]),
210                'active' => $query->view === 'tree',
211            ],
212            [
213                'label' => 'List',
214                'url' => $urls->collection([
215                    'view' => 'list',
216                    'open' => '',
217                    'offset' => '',
218                ]),
219                'active' => $query->view === 'list',
220            ],
221        ];
222    }
223
224    /**
225     * @param iterable<Column> $columns
226     * @param iterable<mixed> $sortKeys
227     * @return list<array{label: string, url: ?string, class: string}>
228     */
229    private static function headers(
230        iterable $columns,
231        iterable $sortKeys,
232        CollectionUrls $urls,
233    ): array {
234        $sortKeys = self::sortKeys($sortKeys);
235        $headers = [];
236
237        foreach ($columns as $column) {
238            $label = $column->title;
239            $sort = self::columnSort($column, $sortKeys);
240            $isSorted = $sort !== null && $sort === $urls->query->sort;
241            $nextDir = $isSorted && $urls->query->dir === 'asc' ? 'desc' : 'asc';
242            $class = $sort === null ? '' : 'is-sortable';
243
244            if ($isSorted) {
245                $class .= ' is-sorted is-' . $urls->query->dir;
246            }
247
248            $headers[] = [
249                'label' => $label,
250                'url' => $sort === null
251                    ? null
252                    : $urls->collection(['sort' => $sort, 'dir' => $nextDir, 'offset' => '']),
253                'class' => $class,
254            ];
255        }
256
257        return $headers;
258    }
259
260    /**
261     * @param list<array{label: string, url: ?string, class: string}> $headers
262     * @param list<mixed> $nodes
263     * @return list<array{
264     *     uid: string,
265     *     depth: int,
266     *     expanded: bool,
267     *     last: bool,
268     *     published: bool,
269     *     hasChildren: bool,
270     *     cells: list<array{label: string, value: string, class: string, editUrl: ?string}>,
271     *     status: list<array{kind: string, label: string}>,
272     *     childrenUrl: ?string,
273     *     focusedChildrenUrl: ?string,
274     *     childCreateLinks: list<array{slug: string, name: string, url: string}>,
275     *     childLinks: list<array{label: string, url: string}>,
276     * }>
277     */
278    private static function rows(
279        array $nodes,
280        array $headers,
281        CollectionUrls $urls,
282        CollectionListMeta $meta,
283        string $locale,
284        DateTimeZone $timezone,
285    ): array {
286        $rows = [];
287
288        foreach ($nodes as $node) {
289            $tree = self::treeNode($node);
290            $node = $tree['node'];
291            $uid = (string) ($node['uid'] ?? '');
292            $hasChildren = (bool) ($node['hasChildren'] ?? false);
293            $childBlueprints = self::childBlueprints($node);
294            $childrenUrl = null;
295
296            if ($meta->showChildren && $hasChildren && $uid !== '') {
297                $childrenUrl = $tree['expanded']
298                    ? $urls->collapse($uid, $tree['descendants'])
299                    : $urls->expand($uid);
300            }
301
302            $rows[] = [
303                'uid' => $uid,
304                'depth' => $tree['depth'],
305                'expanded' => $tree['expanded'],
306                'last' => $tree['last'],
307                'published' => (bool) ($node['published'] ?? false),
308                'hasChildren' => $hasChildren,
309                'cells' => self::cells($node, $headers, $urls, $locale, $timezone),
310                'status' => self::status($node, $meta),
311                'childrenUrl' => $childrenUrl,
312                'focusedChildrenUrl' =>
313                    $meta->showChildren && $uid !== '' && ($hasChildren || $childBlueprints !== [])
314                        ? $urls->children($uid)
315                        : null,
316                'childCreateLinks' => $meta->showChildren
317                    ? self::childCreateLinks($childBlueprints, $urls, $uid)
318                    : [],
319                'childLinks' => $meta->showChildren
320                    ? self::childLinks($node, $urls, $childBlueprints)
321                    : [],
322            ];
323        }
324
325        return $rows;
326    }
327
328    /**
329     * @return array{
330     *     node: array<string, mixed>,
331     *     depth: int,
332     *     expanded: bool,
333     *     last: bool,
334     *     descendants: list<string>,
335     * }
336     */
337    private static function treeNode(mixed $node): array
338    {
339        $tree = self::arrayFrom($node);
340
341        if (!array_key_exists('node', $tree)) {
342            return [
343                'node' => $tree,
344                'depth' => 0,
345                'expanded' => false,
346                'last' => false,
347                'descendants' => [],
348            ];
349        }
350
351        return [
352            'node' => self::arrayFrom($tree['node']),
353            'depth' => max(0, (int) ($tree['depth'] ?? 0)),
354            'expanded' => (bool) ($tree['expanded'] ?? false),
355            'last' => (bool) ($tree['last'] ?? false),
356            'descendants' => self::strings($tree['descendants'] ?? []),
357        ];
358    }
359
360    /**
361     * @param array<string, mixed> $node
362     * @param list<array{label: string, url: ?string, class: string}> $headers
363     * @return list<array{label: string, value: string, class: string, editUrl: ?string}>
364     */
365    private static function cells(
366        array $node,
367        array $headers,
368        CollectionUrls $urls,
369        string $locale,
370        DateTimeZone $timezone,
371    ): array {
372        $cells = [];
373        $uid = (string) ($node['uid'] ?? '');
374
375        foreach (self::items(self::arrayFrom($node['columns'] ?? [])) as $index => $column) {
376            $column = self::arrayFrom($column);
377            $label = $headers[$index]['label'] ?? 'Column ' . ((int) $index + 1);
378            $classes = ['collection-cell'];
379
380            if ((bool) ($column['bold'] ?? false)) {
381                $classes[] = 'is-bold';
382            }
383
384            if ((bool) ($column['italic'] ?? false)) {
385                $classes[] = 'is-italic';
386            }
387
388            if ((bool) ($column['badge'] ?? false)) {
389                $classes[] = 'is-badge';
390            }
391
392            $cells[] = [
393                'label' => $label,
394                'value' => self::displayValue(
395                    $column['value'] ?? '',
396                    (bool) ($column['date'] ?? false),
397                    $locale,
398                    $timezone,
399                ),
400                'class' => implode(' ', $classes),
401                'editUrl' => $index === 0 && $uid !== '' ? $urls->edit($uid) : null,
402            ];
403        }
404
405        return $cells;
406    }
407
408    /**
409     * @param list<array{slug: string, name: string}> $blueprints
410     * @return list<array{slug: string, name: string, url: string}>
411     */
412    private static function createLinks(array $blueprints, CollectionUrls $urls): array
413    {
414        $links = [];
415
416        foreach ($blueprints as $blueprint) {
417            $links[] = [
418                'slug' => $blueprint['slug'],
419                'name' => $blueprint['name'],
420                'url' => $urls->create($blueprint['slug']),
421            ];
422        }
423
424        return $links;
425    }
426
427    /**
428     * @param list<array{slug: string, name: string}> $blueprints
429     * @return list<array{slug: string, name: string, url: string}>
430     */
431    private static function childCreateLinks(
432        array $blueprints,
433        CollectionUrls $urls,
434        string $uid,
435    ): array {
436        $links = [];
437
438        if ($uid === '') {
439            return [];
440        }
441
442        foreach ($blueprints as $blueprint) {
443            $links[] = [
444                'slug' => $blueprint['slug'],
445                'name' => $blueprint['name'],
446                'url' => $urls->create($blueprint['slug'], $uid),
447            ];
448        }
449
450        return $links;
451    }
452
453    /**
454     * @param array<string, mixed> $node
455     * @param list<array{slug: string, name: string}> $blueprints
456     * @return list<array{label: string, url: string}>
457     */
458    private static function childLinks(
459        array $node,
460        CollectionUrls $urls,
461        array $blueprints,
462    ): array {
463        $links = [];
464        $uid = (string) ($node['uid'] ?? '');
465
466        if ($uid === '') {
467            return [];
468        }
469
470        if ((bool) ($node['hasChildren'] ?? false)) {
471            $links[] = [
472                'label' => 'Open children',
473                'url' => $urls->children($uid),
474            ];
475        }
476
477        foreach ($blueprints as $blueprint) {
478            $links[] = [
479                'label' => 'Add ' . $blueprint['name'],
480                'url' => $urls->create($blueprint['slug'], $uid),
481            ];
482        }
483
484        return $links;
485    }
486
487    /**
488     * @return list<array{kind: string, label: string}>
489     */
490    private static function statusList(iterable $status): array
491    {
492        $badges = [];
493
494        foreach ($status as $badge) {
495            $badge = self::arrayFrom($badge);
496            $kind = trim((string) ($badge['kind'] ?? ''));
497            $label = trim((string) ($badge['label'] ?? ''));
498
499            if ($kind === '' || $label === '') {
500                continue;
501            }
502
503            $badges[] = [
504                'kind' => $kind,
505                'label' => $label,
506            ];
507        }
508
509        return $badges;
510    }
511
512    /**
513     * @param array<string, mixed> $node
514     * @return list<array{kind: string, label: string}>
515     */
516    private static function status(array $node, CollectionListMeta $meta): array
517    {
518        $badges = [];
519
520        if ($meta->showPublished) {
521            $published = (bool) ($node['published'] ?? false);
522            $badges[] = [
523                'kind' => $published ? 'published' : 'draft',
524                'label' => $published ? 'Published' : 'Draft',
525            ];
526        }
527
528        if ($meta->showHidden && (bool) ($node['hidden'] ?? false)) {
529            $badges[] = [
530                'kind' => 'hidden',
531                'label' => 'Hidden',
532            ];
533        }
534
535        if ($meta->showLocked && (bool) ($node['locked'] ?? false)) {
536            $badges[] = [
537                'kind' => 'locked',
538                'label' => 'Locked',
539            ];
540        }
541
542        return $badges;
543    }
544
545    /**
546     * @param array<string, mixed> $node
547     * @return list<array{slug: string, name: string}>
548     */
549    private static function childBlueprints(array $node): array
550    {
551        $blueprints = [];
552
553        foreach (self::items(self::arrayFrom($node['childBlueprints'] ?? [])) as $blueprint) {
554            $blueprint = self::arrayFrom($blueprint);
555            $slug = trim((string) ($blueprint['slug'] ?? ''));
556            $name = trim((string) ($blueprint['name'] ?? ''));
557
558            if ($slug === '' || $name === '') {
559                continue;
560            }
561
562            $blueprints[] = [
563                'slug' => $slug,
564                'name' => $name,
565            ];
566        }
567
568        return $blueprints;
569    }
570
571    /** @return list<string> */
572    private static function sortKeys(iterable $sorts): array
573    {
574        return array_values(array_filter(
575            array_map(static fn(mixed $sort): string => trim((string) $sort), self::items($sorts)),
576            static fn(string $sort): bool => $sort !== '',
577        ));
578    }
579
580    /** @param list<string> $sorts */
581    private static function columnSort(Column $column, array $sorts): ?string
582    {
583        $explicit = $column->sortKey();
584
585        if ($explicit !== null) {
586            return in_array($explicit, $sorts, true) ? $explicit : null;
587        }
588
589        if (!is_string($column->field)) {
590            return null;
591        }
592
593        $candidates = [$column->field];
594
595        if (str_starts_with($column->field, 'meta.')) {
596            $candidates[] = substr($column->field, 5);
597        }
598
599        $candidates[] = match ($column->field) {
600            'meta.name', 'meta.class', 'meta.classname' => 'type',
601            default => '',
602        };
603
604        foreach (array_unique($candidates) as $candidate) {
605            if ($candidate !== '' && in_array($candidate, $sorts, true)) {
606                return $candidate;
607            }
608        }
609
610        return null;
611    }
612
613    /** @return list<array{slug: string, name: string}> */
614    private static function blueprints(iterable $blueprints): array
615    {
616        $result = [];
617
618        foreach ($blueprints as $blueprint) {
619            $blueprint = self::arrayFrom($blueprint);
620            $slug = trim((string) ($blueprint['slug'] ?? ''));
621            $name = trim((string) ($blueprint['name'] ?? ''));
622
623            if ($slug === '' || $name === '') {
624                continue;
625            }
626
627            $result[] = [
628                'slug' => $slug,
629                'name' => $name,
630            ];
631        }
632
633        return $result;
634    }
635
636    private static function displayValue(
637        mixed $value,
638        bool $date,
639        string $locale,
640        DateTimeZone $timezone,
641    ): string {
642        if ($date && $value instanceof DateTimeInterface) {
643            $formatted = self::formatDate($value, $locale, $timezone);
644
645            if ($formatted !== null) {
646                return $formatted;
647            }
648        }
649
650        if ($date && (is_scalar($value) || $value instanceof Stringable)) {
651            $original = $value;
652            $value = trim((string) $value);
653
654            if ($value !== '') {
655                try {
656                    $formatted = self::formatDate(
657                        new DateTimeImmutable($value, $timezone),
658                        $locale,
659                        $timezone,
660                    );
661
662                    if ($formatted !== null) {
663                        return $formatted;
664                    }
665                } catch (Throwable) {
666                    $value = $original;
667                }
668            }
669
670            $value = $original;
671        }
672
673        if (is_bool($value)) {
674            return $value ? 'Yes' : 'No';
675        }
676
677        if (is_scalar($value)) {
678            return (string) $value;
679        }
680
681        if ($value instanceof Stringable) {
682            return (string) $value;
683        }
684
685        return '';
686    }
687
688    private static function formatDate(
689        DateTimeInterface $value,
690        string $locale,
691        DateTimeZone $timezone,
692    ): ?string {
693        $formatter = new IntlDateFormatter(
694            $locale,
695            IntlDateFormatter::MEDIUM,
696            IntlDateFormatter::SHORT,
697            $timezone,
698        );
699        $formatted = $formatter->format($value->getTimestamp());
700
701        return $formatted === false ? null : $formatted;
702    }
703
704    /** @return list<string> */
705    private static function strings(mixed $items): array
706    {
707        $strings = [];
708
709        foreach (self::items(self::arrayFrom($items)) as $item) {
710            $item = trim((string) $item);
711
712            if ($item !== '' && !in_array($item, $strings, true)) {
713                $strings[] = $item;
714            }
715        }
716
717        return $strings;
718    }
719
720    /** @return list<mixed> */
721    private static function items(iterable $items): array
722    {
723        if ($items instanceof Traversable) {
724            return array_values(iterator_to_array($items, false));
725        }
726
727        return array_values($items);
728    }
729
730    /** @return array<array-key, mixed> */
731    private static function arrayFrom(mixed $value): array
732    {
733        if ($value instanceof Traversable) {
734            return iterator_to_array($value);
735        }
736
737        return is_array($value) ? $value : [];
738    }
739}