Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.12% covered (success)
95.12%
39 / 41
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Schema
95.12% covered (success)
95.12%
39 / 41
75.00% covered (warning)
75.00%
6 / 8
19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 __get
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 properties
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveAttributes
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
7
 isFieldProperty
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Node;
6
7use Cosray\Exception\NoSuchProperty;
8use Cosray\Exception\RuntimeException;
9use Cosray\Field\Field;
10use Cosray\Node\Schema\Registry;
11use Cosray\Schema\Title;
12use ReflectionClass;
13use ReflectionProperty;
14use ReflectionUnionType;
15
16class Schema
17{
18    /** @var array<string, mixed> */
19    private array $properties;
20
21    private readonly Registry $registry;
22
23    /**
24     * @param class-string $nodeClass
25     */
26    public function __construct(
27        private readonly string $nodeClass,
28        ?Registry $registry = null,
29    ) {
30        $this->registry = $registry ?? new Registry();
31        $resolved = $this->resolveAttributes();
32        $this->properties = $this->registry->resolveDefaults($this->nodeClass, $resolved);
33    }
34
35    public function __get(string $key): mixed
36    {
37        if (!$this->has($key)) {
38            throw new NoSuchProperty(
39                "The node schema '{$this->nodeClass}' doesn't have the property '{$key}'",
40            );
41        }
42
43        return $this->get($key);
44    }
45
46    public function __isset(string $key): bool
47    {
48        return $this->has($key) && $this->properties[$key] !== null;
49    }
50
51    /**
52     * Get a schema property by key.
53     */
54    public function get(string $key, mixed $default = null): mixed
55    {
56        if (array_key_exists($key, $this->properties)) {
57            return $this->properties[$key];
58        }
59
60        return $default;
61    }
62
63    public function has(string $key): bool
64    {
65        return array_key_exists($key, $this->properties);
66    }
67
68    /**
69     * Return all schema properties as an array.
70     *
71     * @return array<string, mixed>
72     */
73    public function properties(): array
74    {
75        return $this->properties;
76    }
77
78    /**
79     * @return array<string, mixed>
80     */
81    private function resolveAttributes(): array
82    {
83        $reflection = new ReflectionClass($this->nodeClass);
84        $resolved = [];
85
86        foreach ($reflection->getAttributes() as $attribute) {
87            $instance = $attribute->newInstance();
88            $handler = $this->registry->getHandler($instance);
89
90            if ($handler !== null) {
91                $resolved = array_merge($resolved, $handler->resolve($instance, $this->nodeClass));
92            }
93        }
94
95        foreach ($reflection->getProperties() as $property) {
96            foreach ($property->getAttributes(Title::class) as $attribute) {
97                $instance = $attribute->newInstance();
98                $handler = $this->registry->getHandler($instance);
99
100                if ($handler === null) {
101                    continue;
102                }
103
104                if (!$this->isFieldProperty($property)) {
105                    throw new RuntimeException(
106                        "The #[Title] attribute on property '{$this->nodeClass}::{$property->getName()}"
107                        . 'requires a field-typed property.',
108                    );
109                }
110
111                $resolved = array_merge($resolved, $handler->resolve(
112                    new Title($property->getName()),
113                    $this->nodeClass,
114                ));
115            }
116        }
117
118        return $resolved;
119    }
120
121    private function isFieldProperty(ReflectionProperty $property): bool
122    {
123        $type = $property->getType();
124
125        if ($type === null || $type::class === ReflectionUnionType::class) {
126            return false;
127        }
128
129        return is_subclass_of($type->getName(), Field::class);
130    }
131}