Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.89% covered (warning)
88.89%
128 / 144
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Collection
88.89% covered (warning)
88.89%
128 / 144
25.00% covered (danger)
25.00%
2 / 8
51.16
0.00% covered (danger)
0.00%
0 / 1
 collection
98.85% covered (success)
98.85%
86 / 87
0.00% covered (danger)
0.00%
0 / 1
21
 parentNode
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 nodeStatus
57.89% covered (warning)
57.89%
11 / 19
0.00% covered (danger)
0.00%
0 / 1
12.78
 blueprints
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 intParam
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
9.36
 openParam
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 stringParam
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 navigation
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Controller\Panel;
6
7use Celemas\Core\Exception\HttpBadRequest;
8use Celemas\Core\Exception\HttpNotFound;
9use Celemas\Core\Request;
10use Celemas\Wire\Creator;
11use Cosray\Collection as CmsCollection;
12use Cosray\Exception\RuntimeException;
13use Cosray\Navigation;
14use Cosray\Node\Node;
15use Cosray\Node\Types;
16use Cosray\Panel\CollectionPage;
17use Cosray\Panel\CollectionQuery;
18use Cosray\Panel\CollectionTree;
19use Cosray\Panel\CollectionUrls;
20
21final class Collection extends Panel
22{
23    private const int LIMIT_DEFAULT = 50;
24    private const int LIMIT_MAX = 250;
25
26    public function collection(string $collection): array
27    {
28        $creator = new Creator($this->container);
29        $navigation = $this->navigation();
30
31        try {
32            $ref = $navigation->ref($collection);
33        } catch (RuntimeException $e) {
34            throw new HttpNotFound($this->request, previous: $e);
35        }
36
37        $obj = $creator->create(
38            $ref::class,
39            predefinedTypes: [Request::class => $this->request],
40        );
41        assert($obj instanceof CmsCollection, 'The collection route must resolve a collection');
42
43        $offset = $this->intParam('offset', 0, min: 0);
44        $limit = $this->intParam('limit', self::LIMIT_DEFAULT, min: 1, max: self::LIMIT_MAX);
45        $q = $this->stringParam('q');
46        $sort = $this->stringParam('sort');
47        $dir = strtolower($this->stringParam('dir'));
48        $parent = $this->stringParam('parent');
49        $view = $this->stringParam('view');
50        $open = $this->openParam('open');
51
52        if ($dir !== '' && !in_array($dir, ['asc', 'desc'], true)) {
53            throw new HttpBadRequest($this->request);
54        }
55
56        $sorts = $obj->sorts();
57
58        if ($sort !== '' && !array_key_exists($sort, $sorts)) {
59            throw new HttpBadRequest($this->request);
60        }
61
62        $parentUid = $obj->listMeta->showChildren && $parent !== '' ? $parent : null;
63        $defaultView = $obj->listMeta->showChildren && $parentUid === null ? 'tree' : 'list';
64
65        if ($view === '') {
66            $view = $defaultView;
67        }
68
69        if (!in_array($view, ['tree', 'list'], true)) {
70            throw new HttpBadRequest($this->request);
71        }
72
73        if (!$obj->listMeta->showChildren) {
74            $open = [];
75        }
76
77        $parentNode = $parentUid === null ? null : $this->parentNode($obj, $parentUid);
78        $parentTitle = $parentNode?->title();
79
80        if ($parentTitle !== null && trim($parentTitle) === '') {
81            $parentTitle = $parentUid;
82        }
83
84        $listing = $obj->list(
85            offset: $offset,
86            limit: $limit,
87            q: $q,
88            sort: $sort,
89            dir: $dir,
90            parent: $parentUid,
91        );
92
93        $query = new CollectionQuery(
94            q: $listing['q'],
95            sort: $listing['sort'],
96            dir: $listing['dir'],
97            offset: $listing['offset'],
98            limit: $listing['limit'],
99            parent: $parentUid,
100            view: $view,
101            open: $open,
102            defaultView: $defaultView,
103        );
104        $nodes = $listing['nodes'];
105
106        if ($obj->listMeta->showChildren && $view === 'tree') {
107            $nodes = CollectionTree::build(
108                nodes: $nodes,
109                open: $open,
110                children: static fn(string $uid): array => $obj->list(
111                    offset: 0,
112                    limit: self::LIMIT_MAX,
113                    sort: $listing['sort'],
114                    dir: $listing['dir'],
115                    parent: $uid,
116                )['nodes'],
117            );
118        }
119
120        $urls = new CollectionUrls($this->panelPath(), $collection, $query);
121
122        return $this->context([
123            'page' => CollectionPage::from(
124                name: $ref->meta->label,
125                urls: $urls,
126                columns: $obj->columns(),
127                sortKeys: array_keys($sorts),
128                blueprints: $this->blueprints($obj),
129                nodes: $nodes,
130                total: $listing['total'],
131                meta: $obj->listMeta,
132                locale: $this->localeId(),
133                timezone: $this->config->app->timezone,
134                parentTitle: $parentTitle,
135                parentType: $parentNode === null
136                    ? null
137                    : (string) $parentNode->meta->type->get('label', ''),
138                parentStatus: $parentNode === null ? null : $this->nodeStatus($obj, $parentNode),
139                createBlueprints: $parentNode === null ? null : $obj->childBlueprints($parentNode),
140            ),
141        ]);
142    }
143
144    private function parentNode(CmsCollection $collection, string $uid): Node
145    {
146        $node = $collection->cms?->node->byUid($uid, published: null);
147
148        if (!$node) {
149            throw new HttpNotFound($this->request);
150        }
151
152        return $node;
153    }
154
155    /** @return list<array{kind: string, label: string}> */
156    private function nodeStatus(CmsCollection $collection, Node $node): array
157    {
158        $status = [];
159        $meta = $collection->listMeta;
160
161        if ($meta->showPublished) {
162            $published = (bool) $node->meta->get('published');
163            $status[] = [
164                'kind' => $published ? 'published' : 'draft',
165                'label' => $published ? 'Published' : 'Draft',
166            ];
167        }
168
169        if ($meta->showHidden && (bool) $node->meta->get('hidden')) {
170            $status[] = [
171                'kind' => 'hidden',
172                'label' => 'Hidden',
173            ];
174        }
175
176        if ($meta->showLocked && (bool) $node->meta->get('locked')) {
177            $status[] = [
178                'kind' => 'locked',
179                'label' => 'Locked',
180            ];
181        }
182
183        return $status;
184    }
185
186    /** @return list<array{slug: string, name: string}> */
187    private function blueprints(CmsCollection $collection): array
188    {
189        $types = $this->container->get(Types::class);
190        assert($types instanceof Types, 'The node type service must be available');
191        $result = [];
192
193        foreach ($collection->blueprints() as $blueprint) {
194            $result[] = [
195                'slug' => (string) $types->get($blueprint, 'handle'),
196                'name' => (string) $types->get($blueprint, 'label'),
197            ];
198        }
199
200        return $result;
201    }
202
203    private function intParam(
204        string $key,
205        int $default,
206        int $min,
207        ?int $max = null,
208    ): int {
209        $value = $this->request->param($key, (string) $default);
210
211        if (is_int($value)) {
212            $int = $value;
213        } elseif (is_string($value) && preg_match('/^-?[0-9]+$/', $value)) {
214            $int = (int) $value;
215        } else {
216            throw new HttpBadRequest($this->request);
217        }
218
219        if ($int < $min) {
220            throw new HttpBadRequest($this->request);
221        }
222
223        if ($max !== null && $int > $max) {
224            throw new HttpBadRequest($this->request);
225        }
226
227        return $int;
228    }
229
230    /** @return list<string> */
231    private function openParam(string $key): array
232    {
233        $value = $this->request->param($key, '');
234
235        if (!is_string($value)) {
236            throw new HttpBadRequest($this->request);
237        }
238
239        $open = [];
240
241        foreach (explode(',', $value) as $uid) {
242            $uid = trim($uid);
243
244            if ($uid !== '' && !in_array($uid, $open, true)) {
245                $open[] = $uid;
246            }
247        }
248
249        return $open;
250    }
251
252    private function stringParam(string $key): string
253    {
254        $value = $this->request->param($key, '');
255
256        if (!is_string($value)) {
257            throw new HttpBadRequest($this->request);
258        }
259
260        return trim($value);
261    }
262
263    private function navigation(): Navigation
264    {
265        $navigation = $this->container->get(Navigation::class);
266        assert($navigation instanceof Navigation, 'The navigation service must be available');
267
268        return $navigation;
269    }
270}