Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.62% covered (warning)
84.62%
44 / 52
66.67% covered (warning)
66.67%
16 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
MenuItem
84.62% covered (warning)
84.62%
44 / 52
66.67% covered (warning)
66.67%
16 / 24
51.05
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 rewind
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 current
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 key
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 next
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 valid
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hidden
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 node
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 type
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 title
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 path
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 href
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 target
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 image
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 class
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 level
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 children
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setChildren
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasChildren
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 translated
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 localized
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 assetPath
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 joined
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Finder;
6
7use Cosray\Context;
8use Cosray\Field\Field;
9use Generator;
10use Iterator;
11
12class MenuItem implements Iterator
13{
14    protected readonly array $data;
15    protected array $children;
16
17    public function __construct(
18        protected readonly Context $context,
19        protected readonly array $item,
20    ) {
21        $this->data = json_decode($item['data'], true);
22        $this->children = $item['children'];
23    }
24
25    public function rewind(): void
26    {
27        reset($this->children);
28    }
29
30    public function current(): MenuItem
31    {
32        return new MenuItem($this->context, current($this->children));
33    }
34
35    public function key(): string
36    {
37        return key($this->children);
38    }
39
40    public function next(): void
41    {
42        next($this->children);
43    }
44
45    public function valid(): bool
46    {
47        return key($this->children) !== null;
48    }
49
50    public function id(): string
51    {
52        return (string) $this->item['item'];
53    }
54
55    /** Whether the item is kept out of the site; only the panel sees these. */
56    public function hidden(): bool
57    {
58        return (bool) ($this->item['hidden'] ?? false);
59    }
60
61    /** The linked node uid of `node` and `children` items, null otherwise. */
62    public function node(): ?string
63    {
64        $uid = $this->data['node'] ?? null;
65
66        return is_string($uid) && $uid !== '' ? $uid : null;
67    }
68
69    public function type(): string
70    {
71        return $this->data['type'];
72    }
73
74    /**
75     * Node items linked by uid inherit their node's current title when
76     * no title is stored; a stored title always overrides.
77     */
78    public function title(): string
79    {
80        $title = $this->translated('title');
81
82        return $title !== '' ? $title : $this->localized($this->joined('node_title'));
83    }
84
85    /**
86     * Node items linked by uid follow their node's current path; the
87     * snapshot in `data` covers legacy rows and vanished nodes.
88     */
89    public function path(): string
90    {
91        $resolved = $this->localized($this->joined('node_paths'));
92
93        return $resolved !== '' ? $resolved : $this->translated('path');
94    }
95
96    /**
97     * The link target for anchored item types, null for plain labels:
98     * `node` and `url` items link their path, `asset` items their file.
99     */
100    public function href(): ?string
101    {
102        return match ($this->type()) {
103            'node', 'url' => $this->path() ?: null,
104            'asset' => $this->assetPath(),
105            default => null,
106        };
107    }
108
109    public function target(): ?string
110    {
111        $target = $this->data['target'] ?? null;
112
113        return is_string($target) && $target !== '' ? $target : null;
114    }
115
116    public function image(): ?string
117    {
118        $uid = $this->data['image'] ?? null;
119
120        if (!$uid || !is_string($uid)) {
121            return null;
122        }
123
124        return $this->context->assets()->get($uid)?->path();
125    }
126
127    public function class(): ?string
128    {
129        return $this->data['class'] ?? null;
130    }
131
132    public function level(): int
133    {
134        return $this->item['level'];
135    }
136
137    public function children(): Generator
138    {
139        foreach ($this->children as $child) {
140            yield new MenuItem($this->context, $child);
141        }
142    }
143
144    public function setChildren(array $children): void
145    {
146        $this->children = $children;
147    }
148
149    public function hasChildren(): bool
150    {
151        return count($this->children) > 0;
152    }
153
154    protected function translated(string $key): string
155    {
156        return $this->localized($this->data[$key] ?? null);
157    }
158
159    protected function localized(mixed $map): string
160    {
161        if (!is_array($map)) {
162            return '';
163        }
164
165        $locale = $this->context->locale();
166
167        while ($locale) {
168            $value = $map[$locale->id] ?? null;
169
170            if ($value) {
171                return $value;
172            }
173
174            $locale = $locale->fallback();
175        }
176
177        // A language-neutral value applies when no locale in the chain
178        // matches, mirroring the finder's field compilation.
179        return $map[Field::NEUTRAL_LOCALE] ?? '';
180    }
181
182    private function assetPath(): ?string
183    {
184        $uid = $this->data['asset'] ?? null;
185
186        if (!$uid || !is_string($uid)) {
187            return null;
188        }
189
190        return $this->context->assets()->get($uid)?->path();
191    }
192
193    /**
194     * A locale map the read query joined onto the row as jsonb, decoded.
195     *
196     * @return ?array<string, string>
197     */
198    private function joined(string $key): ?array
199    {
200        $value = $this->item[$key] ?? null;
201
202        if (!is_string($value)) {
203            return null;
204        }
205
206        $decoded = json_decode($value, true);
207
208        return is_array($decoded) ? $decoded : null;
209    }
210}