Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
124 / 124
94.59% covered (success)
94.59%
70 / 74
51.85% covered (warning)
51.85%
28 / 54
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
StaticReflectionCache
100.00% covered (success)
100.00%
124 / 124
94.59% covered (success)
94.59%
70 / 74
51.85% covered (warning)
51.85%
28 / 54
100.00% covered (success)
100.00%
8 / 8
154.55
100.00% covered (success)
100.00%
1 / 1
 metadata
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 reset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 build
100.00% covered (success)
100.00%
21 / 21
94.12% covered (success)
94.12%
16 / 17
23.81% covered (danger)
23.81%
5 / 21
100.00% covered (success)
100.00%
1 / 1
28.67
 parameterMetadata
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 columnName
100.00% covered (success)
100.00%
18 / 18
92.31% covered (success)
92.31%
12 / 13
50.00% covered (danger)
50.00%
4 / 8
100.00% covered (success)
100.00%
1 / 1
8.12
 typeMetadata
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
12 / 12
57.14% covered (warning)
57.14%
4 / 7
100.00% covered (success)
100.00%
1 / 1
8.83
 namedTypeMetadata
100.00% covered (success)
100.00%
21 / 21
92.86% covered (success)
92.86%
13 / 14
75.00% covered (warning)
75.00%
6 / 8
100.00% covered (success)
100.00%
1 / 1
6.56
 isBuiltinTypeName
100.00% covered (success)
100.00%
20 / 20
75.00% covered (warning)
75.00%
3 / 4
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
1.12
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Quma\Hydration;
6
7use BackedEnum;
8use Celemas\Quma\Column;
9use Celemas\Quma\Exception\InvalidHydrationTarget;
10use Celemas\Quma\Hydratable;
11use DateTime;
12use DateTimeImmutable;
13use ReflectionClass;
14use ReflectionNamedType;
15use ReflectionParameter;
16use ReflectionType;
17use ReflectionUnionType;
18use Throwable;
19
20/** @internal */
21final class StaticReflectionCache implements MetadataCache
22{
23    /** @var array<class-string, ClassMetadata> */
24    private static array $entries = [];
25
26    /** @param class-string $class */
27    #[\Override]
28    public function metadata(string $class): ClassMetadata
29    {
30        if (!array_key_exists($class, self::$entries)) {
31            self::$entries[$class] = $this->build($class);
32        }
33
34        return self::$entries[$class];
35    }
36
37    /** @psalm-suppress PossiblyUnusedMethod */
38    public static function reset(): void
39    {
40        self::$entries = [];
41    }
42
43    /** @param class-string $class */
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
47            throw InvalidHydrationTarget::forTarget(
48                $class,
49                reason: 'target is not an existing class',
50            );
51        }
52
53        if (is_a($class, Hydratable::class, true)) {
54            return new ClassMetadata($class, true, true, null);
55        }
56
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
60            throw InvalidHydrationTarget::forTarget($class, reason: 'target is not instantiable');
61        }
62
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
66            return new ClassMetadata($class, false, true, []);
67        }
68
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
82
83    /** @param class-string $class */
84    private function parameterMetadata(
85        string $class,
86        ReflectionParameter $parameter,
87    ): ParameterMetadata {
88        $name = $parameter->getName();
89
90        if ($parameter->isVariadic()) {
91            throw InvalidHydrationTarget::forParameter($class, $name, 'is variadic');
92        }
93
94        if ($parameter->isPassedByReference()) {
95            throw InvalidHydrationTarget::forParameter($class, $name, 'is by-reference');
96        }
97
98        $type = $parameter->getType();
99
100        if ($type === null) {
101            throw InvalidHydrationTarget::forParameter($class, $name, 'has no declared type');
102        }
103
104        $column = $this->columnName($class, $parameter, $name);
105        $hasDefault = $parameter->isDefaultValueAvailable();
106
107        return new ParameterMetadata(
108            $name,
109            $column,
110            $this->typeMetadata($class, $name, $type),
111            $type->allowsNull(),
112            $hasDefault,
113            $hasDefault ? $parameter->getDefaultValue() : null,
114            $parameter->getPosition(),
115        );
116    }
117
118    /**
119     * @param class-string $class
120     * @param non-empty-string $parameterName
121     * @return non-empty-string
122     */
123    private function columnName(
124        string $class,
125        ReflectionParameter $parameter,
126        string $parameterName,
127    ): string {
128        $attributes = $parameter->getAttributes(Column::class);
129
130        if ($attributes === []) {
131            return $parameterName;
132        }
133
134        try {
135            $column = $attributes[0]->newInstance();
136        } catch (Throwable) {
137            throw InvalidHydrationTarget::forParameter(
138                $class,
139                $parameterName,
140                'has an invalid #[Column] attribute',
141            );
142        }
143
144        $columnName = $column->name;
145
146        if ($columnName === '' || trim($columnName) === '') {
147            throw InvalidHydrationTarget::forParameter(
148                $class,
149                $parameterName,
150                'has an empty #[Column] name',
151            );
152        }
153
154        return $columnName;
155    }
156
157    /**
158     * @param class-string $class
159     * @param non-empty-string $parameterName
160     */
161    private function typeMetadata(
162        string $class,
163        string $parameterName,
164        ReflectionType $type,
165    ): TypeMetadata {
166        if ($type instanceof ReflectionNamedType) {
167            $name = $this->namedTypeMetadata($class, $parameterName, $type);
168
169            return new TypeMetadata('named', $type->allowsNull(), [$name]);
170        }
171
172        if ($type instanceof ReflectionUnionType) {
173            $names = [];
174
175            foreach ($type->getTypes() as $inner) {
176                if (!$inner instanceof ReflectionNamedType) {
177                    throw InvalidHydrationTarget::forParameter(
178                        $class,
179                        $parameterName,
180                        'uses an unsupported intersection or DNF type',
181                    );
182                }
183
184                if (strtolower($inner->getName()) === 'null') {
185                    continue;
186                }
187
188                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
189            }
190
191            /**
192             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
193             * Psalm cannot infer that after the runtime null-arm filter above.
194             *
195             * @var non-empty-list<NamedTypeMetadata> $names
196             */
197            return new TypeMetadata('union', $type->allowsNull(), $names);
198        }
199
200        throw InvalidHydrationTarget::forParameter(
201            $class,
202            $parameterName,
203            'uses an unsupported intersection type',
204        );
205    }
206
207    /**
208     * @param class-string $class
209     * @param non-empty-string $parameterName
210     */
211    private function namedTypeMetadata(
212        string $class,
213        string $parameterName,
214        ReflectionNamedType $type,
215    ): NamedTypeMetadata {
216        $name = $type->getName();
217        $lower = strtolower($name);
218
219        if ($type->isBuiltin()) {
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
221                return new NamedTypeMetadata($lower, true, null, $lower, null, null);
222            }
223
224            throw InvalidHydrationTarget::forParameter(
225                $class,
226                $parameterName,
227                "uses unsupported type {$name}",
228            );
229        }
230
231        if ($name === DateTimeImmutable::class) {
232            return new NamedTypeMetadata($name, false, DateTimeImmutable::class, null, 'immutable', null);
233        }
234
235        if ($name === DateTime::class) {
236            return new NamedTypeMetadata($name, false, DateTime::class, null, 'mutable', null);
237        }
238
239        if (is_subclass_of($name, BackedEnum::class)) {
240            return new NamedTypeMetadata($name, false, $name, null, null, $name);
241        }
242
243        throw InvalidHydrationTarget::forParameter(
244            $class,
245            $parameterName,
246            "uses unsupported type {$name}",
247        );
248    }
249
250    private function isBuiltinTypeName(string $target): bool
251    {
252        return in_array(
253            strtolower($target),
254            [
255                'array',
256                'bool',
257                'callable',
258                'false',
259                'float',
260                'int',
261                'iterable',
262                'mixed',
263                'never',
264                'null',
265                'object',
266                'string',
267                'true',
268                'void',
269            ],
270            true,
271        );
272    }
273}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

StaticReflectionCache->build
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
47            throw InvalidHydrationTarget::forTarget(
48                $class,
49                reason: 'target is not an existing class',
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
54            return new ClassMetadata($class, true, true, null);
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
60            throw InvalidHydrationTarget::forTarget($class, reason: 'target is not instantiable');
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
66            return new ClassMetadata($class, false, true, []);
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
 
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
47            throw InvalidHydrationTarget::forTarget(
48                $class,
49                reason: 'target is not an existing class',
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
54            return new ClassMetadata($class, true, true, null);
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
60            throw InvalidHydrationTarget::forTarget($class, reason: 'target is not instantiable');
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
66            return new ClassMetadata($class, false, true, []);
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
 
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
47            throw InvalidHydrationTarget::forTarget(
48                $class,
49                reason: 'target is not an existing class',
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
54            return new ClassMetadata($class, true, true, null);
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
60            throw InvalidHydrationTarget::forTarget($class, reason: 'target is not instantiable');
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
66            return new ClassMetadata($class, false, true, []);
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
 
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
46        if ($this->isBuiltinTypeName($class) || !class_exists($class)) {
 
53        if (is_a($class, Hydratable::class, true)) {
 
57        $reflection = new ReflectionClass($class);
58
59        if (!$reflection->isInstantiable()) {
 
63        $constructor = $reflection->getConstructor();
64
65        if ($constructor === null) {
 
69        $parameters = [];
70
71        foreach ($constructor->getParameters() as $parameter) {
 
71        foreach ($constructor->getParameters() as $parameter) {
72            $parameters[] = $this->parameterMetadata($class, $parameter);
73        }
74
75        usort(
76            $parameters,
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,
78        );
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
StaticReflectionCache->columnName
124        string $class,
125        ReflectionParameter $parameter,
126        string $parameterName,
127    ): string {
128        $attributes = $parameter->getAttributes(Column::class);
129
130        if ($attributes === []) {
 
131            return $parameterName;
124        string $class,
125        ReflectionParameter $parameter,
126        string $parameterName,
127    ): string {
128        $attributes = $parameter->getAttributes(Column::class);
129
130        if ($attributes === []) {
 
134        try {
135            $column = $attributes[0]->newInstance();
 
144        $columnName = $column->name;
145
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
147            throw InvalidHydrationTarget::forParameter(
148                $class,
149                $parameterName,
150                'has an empty #[Column] name',
124        string $class,
125        ReflectionParameter $parameter,
126        string $parameterName,
127    ): string {
128        $attributes = $parameter->getAttributes(Column::class);
129
130        if ($attributes === []) {
 
134        try {
135            $column = $attributes[0]->newInstance();
 
144        $columnName = $column->name;
145
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
154        return $columnName;
155    }
124        string $class,
125        ReflectionParameter $parameter,
126        string $parameterName,
127    ): string {
128        $attributes = $parameter->getAttributes(Column::class);
129
130        if ($attributes === []) {
 
134        try {
135            $column = $attributes[0]->newInstance();
 
144        $columnName = $column->name;
145
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
147            throw InvalidHydrationTarget::forParameter(
148                $class,
149                $parameterName,
150                'has an empty #[Column] name',
124        string $class,
125        ReflectionParameter $parameter,
126        string $parameterName,
127    ): string {
128        $attributes = $parameter->getAttributes(Column::class);
129
130        if ($attributes === []) {
 
134        try {
135            $column = $attributes[0]->newInstance();
 
144        $columnName = $column->name;
145
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
154        return $columnName;
155    }
124        string $class,
125        ReflectionParameter $parameter,
126        string $parameterName,
127    ): string {
128        $attributes = $parameter->getAttributes(Column::class);
129
130        if ($attributes === []) {
 
134        try {
135            $column = $attributes[0]->newInstance();
 
144        $columnName = $column->name;
145
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
147            throw InvalidHydrationTarget::forParameter(
148                $class,
149                $parameterName,
150                'has an empty #[Column] name',
124        string $class,
125        ReflectionParameter $parameter,
126        string $parameterName,
127    ): string {
128        $attributes = $parameter->getAttributes(Column::class);
129
130        if ($attributes === []) {
 
134        try {
135            $column = $attributes[0]->newInstance();
 
144        $columnName = $column->name;
145
146        if ($columnName === '' || trim($columnName) === '') {
 
146        if ($columnName === '' || trim($columnName) === '') {
 
154        return $columnName;
155    }
136        } catch (Throwable) {
 
137            throw InvalidHydrationTarget::forParameter(
138                $class,
139                $parameterName,
140                'has an invalid #[Column] attribute',
StaticReflectionCache->isBuiltinTypeName
250    private function isBuiltinTypeName(string $target): bool
251    {
252        return in_array(
 
252        return in_array(
 
270            true,
271        );
272    }
250    private function isBuiltinTypeName(string $target): bool
251    {
252        return in_array(
 
253            strtolower($target),
254            [
255                'array',
256                'bool',
257                'callable',
258                'false',
259                'float',
260                'int',
261                'iterable',
262                'mixed',
263                'never',
264                'null',
265                'object',
266                'string',
267                'true',
268                'void',
269            ],
270            true,
 
270            true,
271        );
272    }
StaticReflectionCache->metadata
28    public function metadata(string $class): ClassMetadata
29    {
30        if (!array_key_exists($class, self::$entries)) {
 
31            self::$entries[$class] = $this->build($class);
32        }
33
34        return self::$entries[$class];
 
34        return self::$entries[$class];
35    }
28    public function metadata(string $class): ClassMetadata
29    {
30        if (!array_key_exists($class, self::$entries)) {
 
34        return self::$entries[$class];
35    }
StaticReflectionCache->namedTypeMetadata
212        string $class,
213        string $parameterName,
214        ReflectionNamedType $type,
215    ): NamedTypeMetadata {
216        $name = $type->getName();
217        $lower = strtolower($name);
218
219        if ($type->isBuiltin()) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
221                return new NamedTypeMetadata($lower, true, null, $lower, null, null);
212        string $class,
213        string $parameterName,
214        ReflectionNamedType $type,
215    ): NamedTypeMetadata {
216        $name = $type->getName();
217        $lower = strtolower($name);
218
219        if ($type->isBuiltin()) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
224            throw InvalidHydrationTarget::forParameter(
225                $class,
226                $parameterName,
227                "uses unsupported type {$name}",
212        string $class,
213        string $parameterName,
214        ReflectionNamedType $type,
215    ): NamedTypeMetadata {
216        $name = $type->getName();
217        $lower = strtolower($name);
218
219        if ($type->isBuiltin()) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
221                return new NamedTypeMetadata($lower, true, null, $lower, null, null);
212        string $class,
213        string $parameterName,
214        ReflectionNamedType $type,
215    ): NamedTypeMetadata {
216        $name = $type->getName();
217        $lower = strtolower($name);
218
219        if ($type->isBuiltin()) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
220            if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) {
 
224            throw InvalidHydrationTarget::forParameter(
225                $class,
226                $parameterName,
227                "uses unsupported type {$name}",
212        string $class,
213        string $parameterName,
214        ReflectionNamedType $type,
215    ): NamedTypeMetadata {
216        $name = $type->getName();
217        $lower = strtolower($name);
218
219        if ($type->isBuiltin()) {
 
231        if ($name === DateTimeImmutable::class) {
 
232            return new NamedTypeMetadata($name, false, DateTimeImmutable::class, null, 'immutable', null);
212        string $class,
213        string $parameterName,
214        ReflectionNamedType $type,
215    ): NamedTypeMetadata {
216        $name = $type->getName();
217        $lower = strtolower($name);
218
219        if ($type->isBuiltin()) {
 
231        if ($name === DateTimeImmutable::class) {
 
235        if ($name === DateTime::class) {
 
236            return new NamedTypeMetadata($name, false, DateTime::class, null, 'mutable', null);
212        string $class,
213        string $parameterName,
214        ReflectionNamedType $type,
215    ): NamedTypeMetadata {
216        $name = $type->getName();
217        $lower = strtolower($name);
218
219        if ($type->isBuiltin()) {
 
231        if ($name === DateTimeImmutable::class) {
 
235        if ($name === DateTime::class) {
 
239        if (is_subclass_of($name, BackedEnum::class)) {
 
240            return new NamedTypeMetadata($name, false, $name, null, null, $name);
212        string $class,
213        string $parameterName,
214        ReflectionNamedType $type,
215    ): NamedTypeMetadata {
216        $name = $type->getName();
217        $lower = strtolower($name);
218
219        if ($type->isBuiltin()) {
 
231        if ($name === DateTimeImmutable::class) {
 
235        if ($name === DateTime::class) {
 
239        if (is_subclass_of($name, BackedEnum::class)) {
 
243        throw InvalidHydrationTarget::forParameter(
244            $class,
245            $parameterName,
246            "uses unsupported type {$name}",
247        );
248    }
StaticReflectionCache->parameterMetadata
85        string $class,
86        ReflectionParameter $parameter,
87    ): ParameterMetadata {
88        $name = $parameter->getName();
89
90        if ($parameter->isVariadic()) {
 
91            throw InvalidHydrationTarget::forParameter($class, $name, 'is variadic');
85        string $class,
86        ReflectionParameter $parameter,
87    ): ParameterMetadata {
88        $name = $parameter->getName();
89
90        if ($parameter->isVariadic()) {
 
94        if ($parameter->isPassedByReference()) {
 
95            throw InvalidHydrationTarget::forParameter($class, $name, 'is by-reference');
85        string $class,
86        ReflectionParameter $parameter,
87    ): ParameterMetadata {
88        $name = $parameter->getName();
89
90        if ($parameter->isVariadic()) {
 
94        if ($parameter->isPassedByReference()) {
 
98        $type = $parameter->getType();
99
100        if ($type === null) {
 
101            throw InvalidHydrationTarget::forParameter($class, $name, 'has no declared type');
85        string $class,
86        ReflectionParameter $parameter,
87    ): ParameterMetadata {
88        $name = $parameter->getName();
89
90        if ($parameter->isVariadic()) {
 
94        if ($parameter->isPassedByReference()) {
 
98        $type = $parameter->getType();
99
100        if ($type === null) {
 
104        $column = $this->columnName($class, $parameter, $name);
105        $hasDefault = $parameter->isDefaultValueAvailable();
106
107        return new ParameterMetadata(
108            $name,
109            $column,
110            $this->typeMetadata($class, $name, $type),
111            $type->allowsNull(),
112            $hasDefault,
113            $hasDefault ? $parameter->getDefaultValue() : null,
 
113            $hasDefault ? $parameter->getDefaultValue() : null,
 
113            $hasDefault ? $parameter->getDefaultValue() : null,
114            $parameter->getPosition(),
115        );
116    }
85        string $class,
86        ReflectionParameter $parameter,
87    ): ParameterMetadata {
88        $name = $parameter->getName();
89
90        if ($parameter->isVariadic()) {
 
94        if ($parameter->isPassedByReference()) {
 
98        $type = $parameter->getType();
99
100        if ($type === null) {
 
104        $column = $this->columnName($class, $parameter, $name);
105        $hasDefault = $parameter->isDefaultValueAvailable();
106
107        return new ParameterMetadata(
108            $name,
109            $column,
110            $this->typeMetadata($class, $name, $type),
111            $type->allowsNull(),
112            $hasDefault,
113            $hasDefault ? $parameter->getDefaultValue() : null,
 
113            $hasDefault ? $parameter->getDefaultValue() : null,
 
113            $hasDefault ? $parameter->getDefaultValue() : null,
114            $parameter->getPosition(),
115        );
116    }
StaticReflectionCache->reset
40        self::$entries = [];
41    }
StaticReflectionCache->typeMetadata
162        string $class,
163        string $parameterName,
164        ReflectionType $type,
165    ): TypeMetadata {
166        if ($type instanceof ReflectionNamedType) {
 
167            $name = $this->namedTypeMetadata($class, $parameterName, $type);
168
169            return new TypeMetadata('named', $type->allowsNull(), [$name]);
162        string $class,
163        string $parameterName,
164        ReflectionType $type,
165    ): TypeMetadata {
166        if ($type instanceof ReflectionNamedType) {
 
172        if ($type instanceof ReflectionUnionType) {
 
173            $names = [];
174
175            foreach ($type->getTypes() as $inner) {
 
175            foreach ($type->getTypes() as $inner) {
 
176                if (!$inner instanceof ReflectionNamedType) {
 
177                    throw InvalidHydrationTarget::forParameter(
178                        $class,
179                        $parameterName,
180                        'uses an unsupported intersection or DNF type',
162        string $class,
163        string $parameterName,
164        ReflectionType $type,
165    ): TypeMetadata {
166        if ($type instanceof ReflectionNamedType) {
 
172        if ($type instanceof ReflectionUnionType) {
 
173            $names = [];
174
175            foreach ($type->getTypes() as $inner) {
 
175            foreach ($type->getTypes() as $inner) {
 
176                if (!$inner instanceof ReflectionNamedType) {
 
184                if (strtolower($inner->getName()) === 'null') {
 
185                    continue;
 
175            foreach ($type->getTypes() as $inner) {
 
175            foreach ($type->getTypes() as $inner) {
176                if (!$inner instanceof ReflectionNamedType) {
177                    throw InvalidHydrationTarget::forParameter(
178                        $class,
179                        $parameterName,
180                        'uses an unsupported intersection or DNF type',
181                    );
182                }
183
184                if (strtolower($inner->getName()) === 'null') {
185                    continue;
186                }
187
188                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
189            }
190
191            /**
192             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
193             * Psalm cannot infer that after the runtime null-arm filter above.
194             *
195             * @var non-empty-list<NamedTypeMetadata> $names
196             */
197            return new TypeMetadata('union', $type->allowsNull(), $names);
162        string $class,
163        string $parameterName,
164        ReflectionType $type,
165    ): TypeMetadata {
166        if ($type instanceof ReflectionNamedType) {
 
172        if ($type instanceof ReflectionUnionType) {
 
173            $names = [];
174
175            foreach ($type->getTypes() as $inner) {
 
175            foreach ($type->getTypes() as $inner) {
 
176                if (!$inner instanceof ReflectionNamedType) {
 
184                if (strtolower($inner->getName()) === 'null') {
 
175            foreach ($type->getTypes() as $inner) {
176                if (!$inner instanceof ReflectionNamedType) {
177                    throw InvalidHydrationTarget::forParameter(
178                        $class,
179                        $parameterName,
180                        'uses an unsupported intersection or DNF type',
181                    );
182                }
183
184                if (strtolower($inner->getName()) === 'null') {
185                    continue;
186                }
187
188                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
 
175            foreach ($type->getTypes() as $inner) {
 
175            foreach ($type->getTypes() as $inner) {
176                if (!$inner instanceof ReflectionNamedType) {
177                    throw InvalidHydrationTarget::forParameter(
178                        $class,
179                        $parameterName,
180                        'uses an unsupported intersection or DNF type',
181                    );
182                }
183
184                if (strtolower($inner->getName()) === 'null') {
185                    continue;
186                }
187
188                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
189            }
190
191            /**
192             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
193             * Psalm cannot infer that after the runtime null-arm filter above.
194             *
195             * @var non-empty-list<NamedTypeMetadata> $names
196             */
197            return new TypeMetadata('union', $type->allowsNull(), $names);
162        string $class,
163        string $parameterName,
164        ReflectionType $type,
165    ): TypeMetadata {
166        if ($type instanceof ReflectionNamedType) {
 
172        if ($type instanceof ReflectionUnionType) {
 
173            $names = [];
174
175            foreach ($type->getTypes() as $inner) {
 
175            foreach ($type->getTypes() as $inner) {
 
175            foreach ($type->getTypes() as $inner) {
176                if (!$inner instanceof ReflectionNamedType) {
177                    throw InvalidHydrationTarget::forParameter(
178                        $class,
179                        $parameterName,
180                        'uses an unsupported intersection or DNF type',
181                    );
182                }
183
184                if (strtolower($inner->getName()) === 'null') {
185                    continue;
186                }
187
188                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
189            }
190
191            /**
192             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
193             * Psalm cannot infer that after the runtime null-arm filter above.
194             *
195             * @var non-empty-list<NamedTypeMetadata> $names
196             */
197            return new TypeMetadata('union', $type->allowsNull(), $names);
162        string $class,
163        string $parameterName,
164        ReflectionType $type,
165    ): TypeMetadata {
166        if ($type instanceof ReflectionNamedType) {
 
172        if ($type instanceof ReflectionUnionType) {
 
173            $names = [];
174
175            foreach ($type->getTypes() as $inner) {
 
175            foreach ($type->getTypes() as $inner) {
176                if (!$inner instanceof ReflectionNamedType) {
177                    throw InvalidHydrationTarget::forParameter(
178                        $class,
179                        $parameterName,
180                        'uses an unsupported intersection or DNF type',
181                    );
182                }
183
184                if (strtolower($inner->getName()) === 'null') {
185                    continue;
186                }
187
188                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
189            }
190
191            /**
192             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
193             * Psalm cannot infer that after the runtime null-arm filter above.
194             *
195             * @var non-empty-list<NamedTypeMetadata> $names
196             */
197            return new TypeMetadata('union', $type->allowsNull(), $names);
162        string $class,
163        string $parameterName,
164        ReflectionType $type,
165    ): TypeMetadata {
166        if ($type instanceof ReflectionNamedType) {
 
172        if ($type instanceof ReflectionUnionType) {
 
200        throw InvalidHydrationTarget::forParameter(
201            $class,
202            $parameterName,
203            'uses an unsupported intersection type',
204        );
205    }
{closure:/workspace/celemas/quma/src/Hydration/StaticReflectionCache.php:77-77}
77            static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position,