Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.00% covered (success)
95.00%
57 / 60
62.50% covered (warning)
62.50%
5 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Section
95.00% covered (success)
95.00%
57 / 60
62.50% covered (warning)
62.50%
5 / 8
20
0.00% covered (danger)
0.00%
0 / 1
 __construct
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 children
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 section
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 icon
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 link
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 collection
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 sort
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray;
6
7use Closure;
8use Cosray\Collection\Ref;
9use Cosray\Collection\Schemas;
10use Cosray\Contract\Entries;
11use Cosray\Exception\RuntimeException;
12use Override;
13
14final class Section implements NavigationItem
15{
16    #[Override]
17    public readonly NavMeta $meta;
18
19    /** @var list<NavigationItem> */
20    private array $children = [];
21
22    private readonly ?Closure $onCollection;
23
24    private readonly Schemas $schemas;
25
26    public function __construct(
27        string $label,
28        ?Closure $onCollection = null,
29        ?Schemas $schemas = null,
30    ) {
31        $label = trim($label);
32
33        if ($label === '') {
34            throw new RuntimeException('Section labels must not be empty');
35        }
36
37        $this->meta = new NavMeta($label);
38        $this->onCollection = $onCollection;
39        $this->schemas = $schemas ?? new Schemas();
40    }
41
42    #[Override]
43    public function slug(): ?string
44    {
45        return null;
46    }
47
48    /** @return list<NavigationItem> */
49    #[Override]
50    public function children(): array
51    {
52        $visible = [];
53
54        foreach ($this->children as $item) {
55            if ($item->meta->hidden) {
56                continue;
57            }
58
59            if ($item instanceof self && $item->children() === []) {
60                continue;
61            }
62
63            $visible[] = $item;
64        }
65
66        return $this->sort($visible);
67    }
68
69    public function section(string $label): self
70    {
71        $section = new self($label, $this->onCollection, $this->schemas);
72        $this->children[] = $section;
73
74        return $section;
75    }
76
77    /** @param array<array-key, mixed> $args */
78    public function icon(string $id, array $args = []): static
79    {
80        $id = trim($id);
81
82        if ($id === '') {
83            throw new RuntimeException('Section icon ids must not be empty');
84        }
85
86        $this->meta->icon = [
87            'id' => $id,
88            'args' => $args,
89        ];
90
91        return $this;
92    }
93
94    public function link(string $label, string $url, ?string $activePrefix = null): NavLink
95    {
96        $link = new NavLink($label, $url, $activePrefix);
97        $this->children[] = $link;
98
99        return $link;
100    }
101
102    /**
103     * Registers a collection class. It must declare what it lists, through
104     * #[Types] or by implementing Entries, so mistakes fail at boot rather
105     * than on the first panel request.
106     *
107     * @param class-string $class
108     */
109    public function collection(string $class): Ref
110    {
111        if (!class_exists($class)) {
112            throw new RuntimeException("Unknown collection class '{$class}'");
113        }
114
115        if ($this->schemas->get($class, 'types') === [] && !is_a($class, Entries::class, true)) {
116            throw new RuntimeException(
117                "Collection '{$class}' must declare #[Types] or implement " . Entries::class,
118            );
119        }
120
121        $ref = new Ref(
122            $class,
123            $this->schemas->nav($class),
124            (string) $this->schemas->get($class, 'handle'),
125        );
126        $this->children[] = $ref;
127
128        if ($this->onCollection !== null) {
129            ($this->onCollection)($ref);
130        }
131
132        return $ref;
133    }
134
135    /**
136     * @param list<NavigationItem> $items
137     * @return list<NavigationItem>
138     */
139    private function sort(array $items): array
140    {
141        $indexed = [];
142
143        foreach ($items as $index => $item) {
144            $indexed[] = [
145                'index' => $index,
146                'item' => $item,
147            ];
148        }
149
150        usort($indexed, static function (array $left, array $right): int {
151            $cmp = $left['item']->meta->order <=> $right['item']->meta->order;
152
153            if ($cmp !== 0) {
154                return $cmp;
155            }
156
157            return $left['index'] <=> $right['index'];
158        });
159
160        return array_map(
161            static fn(array $item): NavigationItem => $item['item'],
162            $indexed,
163        );
164    }
165}