Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.45% covered (success)
91.45%
107 / 117
53.85% covered (warning)
53.85%
7 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Editor
91.45% covered (success)
91.45%
107 / 117
53.85% covered (warning)
53.85%
7 / 13
39.95
0.00% covered (danger)
0.00%
0 / 1
 edit
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 collection
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 canCreate
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 parentNode
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 blueprintHandles
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 queryState
92.59% covered (success)
92.59%
25 / 27
0.00% covered (danger)
0.00%
0 / 1
9.03
 editorContext
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 editorAvailable
100.00% covered (success)
100.00%
1 / 1
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\CollectionQuery;
17use Cosray\Panel\CollectionUrls;
18
19final class Editor extends Panel
20{
21    private const string LEGACY_PANEL_PATH = '/panel';
22    private const int LIMIT_DEFAULT = 50;
23    private const int LIMIT_MAX = 250;
24
25    public function edit(string $collection, string $node): array
26    {
27        [$name, $obj] = $this->collection($collection);
28        $query = $this->queryState($obj);
29
30        return $this->editorContext(
31            mode: 'edit',
32            name: $name,
33            collection: $collection,
34            node: $node,
35            type: null,
36            query: $query,
37        );
38    }
39
40    public function create(string $collection, string $type): array
41    {
42        [$name, $obj] = $this->collection($collection);
43        $query = $this->queryState($obj);
44
45        if (!$this->canCreate($obj, $type, $query->parent)) {
46            throw new HttpNotFound($this->request);
47        }
48
49        return $this->editorContext(
50            mode: 'create',
51            name: $name,
52            collection: $collection,
53            node: null,
54            type: $type,
55            query: $query,
56        );
57    }
58
59    /** @return array{string, CmsCollection} */
60    private function collection(string $collection): array
61    {
62        try {
63            $ref = $this->navigation()->ref($collection);
64        } catch (RuntimeException $e) {
65            throw new HttpNotFound($this->request, previous: $e);
66        }
67
68        $creator = new Creator($this->container);
69        $obj = $creator->create(
70            $ref::class,
71            predefinedTypes: [Request::class => $this->request],
72        );
73        assert($obj instanceof CmsCollection, 'The editor route must resolve a collection');
74
75        return [$ref->meta->label, $obj];
76    }
77
78    private function canCreate(CmsCollection $collection, string $type, ?string $parent): bool
79    {
80        if ($parent === null) {
81            return in_array($type, $this->blueprintHandles($collection), true);
82        }
83
84        if (!$collection->listMeta->showChildren) {
85            return false;
86        }
87
88        $childHandles = array_column(
89            $collection->childBlueprints($this->parentNode($collection, $parent)),
90            'slug',
91        );
92
93        return in_array($type, $childHandles, true);
94    }
95
96    private function parentNode(CmsCollection $collection, string $uid): Node
97    {
98        $node = $collection->cms?->node->byUid($uid, published: null);
99
100        if (!$node) {
101            throw new HttpNotFound($this->request);
102        }
103
104        return $node;
105    }
106
107    /** @return list<string> */
108    private function blueprintHandles(CmsCollection $collection): array
109    {
110        $types = $this->container->get(Types::class);
111        assert($types instanceof Types, 'The node type service must be available');
112        $handles = [];
113
114        foreach ($collection->blueprints() as $blueprint) {
115            $handles[] = (string) $types->get($blueprint, 'handle');
116        }
117
118        return $handles;
119    }
120
121    private function queryState(CmsCollection $collection): CollectionQuery
122    {
123        $offset = $this->intParam('offset', 0, min: 0);
124        $limit = $this->intParam('limit', self::LIMIT_DEFAULT, min: 1, max: self::LIMIT_MAX);
125        $dir = strtolower($this->stringParam('dir'));
126
127        if ($dir !== '' && !in_array($dir, ['asc', 'desc'], true)) {
128            throw new HttpBadRequest($this->request);
129        }
130
131        $parent = $this->stringParam('parent');
132        $parent = $parent === '' ? null : $parent;
133        $view = $this->stringParam('view');
134        $open = $this->openParam('open');
135        $defaultView = $collection->listMeta->showChildren && $parent === null ? 'tree' : 'list';
136
137        if ($view === '') {
138            $view = $defaultView;
139        }
140
141        if (!in_array($view, ['tree', 'list'], true)) {
142            throw new HttpBadRequest($this->request);
143        }
144
145        if (!$collection->listMeta->showChildren) {
146            $open = [];
147        }
148
149        return new CollectionQuery(
150            q: $this->stringParam('q'),
151            sort: $this->stringParam('sort'),
152            dir: $dir,
153            offset: $offset,
154            limit: $limit,
155            parent: $parent,
156            view: $view,
157            open: $open,
158            defaultView: $defaultView,
159        );
160    }
161
162    private function editorContext(
163        string $mode,
164        string $name,
165        string $collection,
166        ?string $node,
167        ?string $type,
168        CollectionQuery $query,
169    ): array {
170        return $this->context([
171            'mode' => $mode,
172            'name' => $name,
173            'slug' => $collection,
174            'nodeUid' => $node,
175            'type' => $type,
176            'parent' => $query->parent,
177            'queryState' => $query,
178            'links' => new CollectionUrls($this->panelPath(), $collection, $query),
179            'legacyLinks' => new CollectionUrls(self::LEGACY_PANEL_PATH, $collection, $query),
180            'legacyApiBase' => self::LEGACY_PANEL_PATH . '/api',
181            'legacyBootUrl' => self::LEGACY_PANEL_PATH . '/boot',
182            'editorAvailable' => $this->editorAvailable(),
183        ]);
184    }
185
186    private function editorAvailable(): bool
187    {
188        return $this->config->env() === 'development' || $this->hasPanelBuild();
189    }
190
191    private function intParam(
192        string $key,
193        int $default,
194        int $min,
195        ?int $max = null,
196    ): int {
197        $value = $this->request->param($key, (string) $default);
198
199        if (is_int($value)) {
200            $int = $value;
201        } elseif (is_string($value) && preg_match('/^-?[0-9]+$/', $value)) {
202            $int = (int) $value;
203        } else {
204            throw new HttpBadRequest($this->request);
205        }
206
207        if ($int < $min) {
208            throw new HttpBadRequest($this->request);
209        }
210
211        if ($max !== null && $int > $max) {
212            throw new HttpBadRequest($this->request);
213        }
214
215        return $int;
216    }
217
218    /** @return list<string> */
219    private function openParam(string $key): array
220    {
221        $value = $this->request->param($key, '');
222
223        if (!is_string($value)) {
224            throw new HttpBadRequest($this->request);
225        }
226
227        $open = [];
228
229        foreach (explode(',', $value) as $uid) {
230            $uid = trim($uid);
231
232            if ($uid !== '' && !in_array($uid, $open, true)) {
233                $open[] = $uid;
234            }
235        }
236
237        return $open;
238    }
239
240    private function stringParam(string $key): string
241    {
242        $value = $this->request->param($key, '');
243
244        if (!is_string($value)) {
245            throw new HttpBadRequest($this->request);
246        }
247
248        return trim($value);
249    }
250
251    private function navigation(): Navigation
252    {
253        $navigation = $this->container->get(Navigation::class);
254        assert($navigation instanceof Navigation, 'The navigation service must be available');
255
256        return $navigation;
257    }
258}