Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
72 / 72
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
Types
100.00% covered (success)
100.00%
72 / 72
100.00% covered (success)
100.00%
9 / 9
21
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 list
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 type
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 summary
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
 field
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
8
 row
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 readable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 kind
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 user
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Api\Controller;
6
7use Celema\Core\Exception\HttpNotFound;
8use Celema\Core\Request;
9use Cosray\Api\Responses;
10use Cosray\Cms;
11use Cosray\Context;
12use Cosray\Field\Blocks;
13use Cosray\Field\Entries;
14use Cosray\Field\Field;
15use Cosray\Node\Factory;
16use Cosray\Node\Types as NodeTypes;
17use Cosray\Security\NodeAccess;
18use Cosray\User;
19use Psr\Http\Message\ResponseInterface as Response;
20
21/** The node types a client may read, with the field schema writes have to follow. */
22final class Types
23{
24    /** Descriptor keys a client needs; the rest only lay out the panel's editor. */
25    private const array PROPERTIES = [
26        'label',
27        'description',
28        'required',
29        'translate',
30        'translateMode',
31        'options',
32        'limit',
33        'syntaxes',
34        'immutable',
35        'columns',
36    ];
37
38    public function __construct(
39        private readonly Responses $responses,
40        private readonly NodeAccess $access,
41        private readonly NodeTypes $types,
42    ) {}
43
44    public function list(Request $request): Response
45    {
46        $user = self::user($request);
47        $items = [];
48
49        foreach ($this->readable($user) as $handle => $class) {
50            $items[] = $this->summary($user, $handle, $class);
51        }
52
53        return $this->responses->json(['items' => $items]);
54    }
55
56    public function type(Request $request, Context $context, Cms $cms, string $type): Response
57    {
58        $user = self::user($request);
59        $class = $this->readable($user)[$type]
60            ?? throw new HttpNotFound($request, payload: ['message' => "There is no type '{$type}'."]);
61        $blueprint = $cms->nodeFactory()->blueprint($class, $context, $cms);
62        $fields = [];
63
64        foreach (Factory::fieldNamesFor($blueprint) as $name) {
65            $fields[] = $this->field(Factory::fieldFor($blueprint, $name), withDefault: true);
66        }
67
68        return $this->responses->json([...$this->summary($user, $type, $class), 'fields' => $fields]);
69    }
70
71    /**
72     * @param class-string $class
73     * @return array<string, mixed>
74     */
75    private function summary(?User $user, string $handle, string $class): array
76    {
77        $children = $this->types->get($class, 'children', []);
78        $route = $this->types->get($class, 'route');
79
80        return [
81            'handle' => $handle,
82            'label' => __((string) $this->types->get($class, 'label')),
83            'routable' => (bool) $this->types->get($class, 'routable', false),
84            'renderable' => (bool) $this->types->get($class, 'renderable', false),
85            'route' => $route === '' ? null : $route,
86            'children' => array_values(array_map(
87                fn(string $child): string => (string) $this->types->get($child, 'handle'),
88                is_array($children) ? array_filter($children, class_exists(...)) : [],
89            )),
90            'actions' => [
91                'read' => $this->access->allows($user, NodeAccess::READ, $class),
92                'create' => $this->access->allows($user, NodeAccess::CREATE, $class),
93                'change' => $this->access->allows($user, NodeAccess::CHANGE, $class),
94                'delete' => $this->access->allows($user, NodeAccess::DELETE, $class),
95            ],
96        ];
97    }
98
99    /** @return array<string, mixed> */
100    private function field(Field $field, bool $withDefault): array
101    {
102        $properties = $field->properties();
103        $schema = ['name' => $field->name, 'type' => self::kind($field)];
104
105        foreach (self::PROPERTIES as $key) {
106            if (array_key_exists($key, $properties)) {
107                $schema[$key] = $properties[$key];
108            }
109        }
110
111        $rows = match (true) {
112            $field instanceof Blocks => array_map(fn(string $type): array => $this->row(
113                $type,
114                $field->rowHandle($type),
115                $field->blockFields($type),
116            ), $field->allowedBlockTypes()),
117            $field instanceof Entries => array_map(fn(string $type): array => $this->row(
118                $type,
119                $field->rowHandle($type),
120                $field->entryFields($type),
121            ), $field->allowedEntryTypes()),
122            default => null,
123        };
124
125        if ($rows !== null) {
126            $schema['rows'] = array_values($rows);
127        }
128
129        if ($withDefault) {
130            $default = $field->structure();
131            unset($default['type']);
132            $schema['default'] = $default;
133        }
134
135        return $schema;
136    }
137
138    /**
139     * @param class-string $type
140     * @param array<string, Field> $fields
141     * @return array<string, mixed>
142     */
143    private function row(string $type, string $handle, array $fields): array
144    {
145        return [
146            'type' => $handle,
147            'label' => __((string) $this->types->get($type, 'label')),
148            'fields' => array_values(array_map(fn(Field $field): array => $this->field($field, false), $fields)),
149        ];
150    }
151
152    /** @return array<string, class-string> */
153    private function readable(?User $user): array
154    {
155        $readable = $this->access->readable($user);
156        $handles = [...$readable['any'], ...$readable['live']];
157        sort($handles);
158        $registered = $this->access->registered();
159
160        return array_combine($handles, array_map(static fn(string $handle): string => $registered[$handle], $handles));
161    }
162
163    /** A field class as a type name: `RichText` reads `rich-text`. */
164    private static function kind(Field $field): string
165    {
166        $name = basename(str_replace('\\', '/', $field::class));
167
168        return strtolower(ltrim((string) preg_replace('/[A-Z]/', '-$0', $name), '-'));
169    }
170
171    private static function user(Request $request): ?User
172    {
173        $user = $request->get('user', null);
174
175        return $user instanceof User ? $user : null;
176    }
177}