Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
95 / 95
95.59% covered (success)
95.59%
65 / 68
55.77% covered (warning)
55.77%
29 / 52
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
StaticReflectionCache
100.00% covered (success)
100.00%
95 / 95
95.59% covered (success)
95.59%
65 / 68
55.77% covered (warning)
55.77%
29 / 52
100.00% covered (success)
100.00%
7 / 7
114.16
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%
20 / 20
93.33% covered (success)
93.33%
14 / 15
42.86% covered (danger)
42.86%
6 / 14
100.00% covered (success)
100.00%
1 / 1
12.72
 parameterMetadata
100.00% covered (success)
100.00%
18 / 18
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
62.50% covered (warning)
62.50%
5 / 8
100.00% covered (success)
100.00%
1 / 1
6.32
 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%
14 / 14
92.86% covered (success)
92.86%
13 / 14
40.00% covered (danger)
40.00%
6 / 15
100.00% covered (success)
100.00%
1 / 1
13.78
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Quma\Hydration;
6
7use BackedEnum;
8use Celema\Quma\Column;
9use Celema\Quma\Exception\InvalidHydrationTarget;
10use Celema\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 The tests clear the cache with it. */
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        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
48            throw InvalidHydrationTarget::forTarget(
49                $class,
50                reason: 'target is not an existing class',
51            );
52        }
53
54        if (is_a($class, Hydratable::class, true)) {
55            return new ClassMetadata($class, true, true, null);
56        }
57
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
61            throw InvalidHydrationTarget::forTarget($class, reason: 'target is not instantiable');
62        }
63
64        $constructor = $reflection->getConstructor();
65
66        if ($constructor === null) {
67            throw InvalidHydrationTarget::forTarget(
68                $class,
69                reason: 'target has no constructor; declare a public constructor or implement Hydratable',
70            );
71        }
72
73        $parameters = [];
74
75        // Reflection lists the parameters in declaration order.
76        foreach ($constructor->getParameters() as $parameter) {
77            $parameters[] = $this->parameterMetadata($class, $parameter);
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        );
115    }
116
117    /**
118     * @param class-string $class
119     * @param non-empty-string $parameterName
120     * @return non-empty-string
121     */
122    private function columnName(
123        string $class,
124        ReflectionParameter $parameter,
125        string $parameterName,
126    ): string {
127        $attributes = $parameter->getAttributes(Column::class);
128
129        if ($attributes === []) {
130            return $parameterName;
131        }
132
133        try {
134            $column = $attributes[0]->newInstance();
135        } catch (Throwable) {
136            throw InvalidHydrationTarget::forParameter(
137                $class,
138                $parameterName,
139                'has an invalid #[Column] attribute',
140            );
141        }
142
143        $columnName = $column->name;
144
145        if ($columnName === '' || trim($columnName) === '') {
146            throw InvalidHydrationTarget::forParameter(
147                $class,
148                $parameterName,
149                'has an empty #[Column] name',
150            );
151        }
152
153        return $columnName;
154    }
155
156    /**
157     * @param class-string $class
158     * @param non-empty-string $parameterName
159     */
160    private function typeMetadata(
161        string $class,
162        string $parameterName,
163        ReflectionType $type,
164    ): TypeMetadata {
165        if ($type instanceof ReflectionNamedType) {
166            $name = $this->namedTypeMetadata($class, $parameterName, $type);
167
168            return new TypeMetadata('named', $type->allowsNull(), [$name]);
169        }
170
171        if ($type instanceof ReflectionUnionType) {
172            $names = [];
173
174            foreach ($type->getTypes() as $inner) {
175                if (!$inner instanceof ReflectionNamedType) {
176                    throw InvalidHydrationTarget::forParameter(
177                        $class,
178                        $parameterName,
179                        'uses an unsupported intersection or DNF type',
180                    );
181                }
182
183                if ($inner->getName() === 'null') {
184                    continue;
185                }
186
187                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
188            }
189
190            /**
191             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
192             * Psalm cannot infer that after the runtime null-arm filter above.
193             *
194             * @var non-empty-list<NamedTypeMetadata> $names
195             */
196            return new TypeMetadata('union', $type->allowsNull(), $names);
197        }
198
199        throw InvalidHydrationTarget::forParameter(
200            $class,
201            $parameterName,
202            'uses an unsupported intersection type',
203        );
204    }
205
206    /**
207     * @param class-string $class
208     * @param non-empty-string $parameterName
209     */
210    private function namedTypeMetadata(
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
219            return new NamedTypeMetadata($name, $name, null, null);
220        }
221
222        if ($name === DateTimeImmutable::class) {
223            return new NamedTypeMetadata($name, null, 'immutable', null);
224        }
225
226        if ($name === DateTime::class) {
227            return new NamedTypeMetadata($name, null, 'mutable', null);
228        }
229
230        if (is_subclass_of($name, BackedEnum::class)) {
231            return new NamedTypeMetadata($name, null, null, $name);
232        }
233
234        throw InvalidHydrationTarget::forParameter(
235            $class,
236            $parameterName,
237            "uses unsupported type {$name}",
238        );
239    }
240}

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        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
48            throw InvalidHydrationTarget::forTarget(
49                $class,
50                reason: 'target is not an existing class',
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
55            return new ClassMetadata($class, true, true, null);
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
61            throw InvalidHydrationTarget::forTarget($class, reason: 'target is not instantiable');
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
64        $constructor = $reflection->getConstructor();
65
66        if ($constructor === null) {
 
67            throw InvalidHydrationTarget::forTarget(
68                $class,
69                reason: 'target has no constructor; declare a public constructor or implement Hydratable',
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
64        $constructor = $reflection->getConstructor();
65
66        if ($constructor === null) {
 
73        $parameters = [];
74
75        // Reflection lists the parameters in declaration order.
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
77            $parameters[] = $this->parameterMetadata($class, $parameter);
 
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
77            $parameters[] = $this->parameterMetadata($class, $parameter);
78        }
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
64        $constructor = $reflection->getConstructor();
65
66        if ($constructor === null) {
 
73        $parameters = [];
74
75        // Reflection lists the parameters in declaration order.
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
77            $parameters[] = $this->parameterMetadata($class, $parameter);
78        }
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
64        $constructor = $reflection->getConstructor();
65
66        if ($constructor === null) {
 
73        $parameters = [];
74
75        // Reflection lists the parameters in declaration order.
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
77            $parameters[] = $this->parameterMetadata($class, $parameter);
78        }
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
48            throw InvalidHydrationTarget::forTarget(
49                $class,
50                reason: 'target is not an existing class',
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
55            return new ClassMetadata($class, true, true, null);
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
61            throw InvalidHydrationTarget::forTarget($class, reason: 'target is not instantiable');
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
64        $constructor = $reflection->getConstructor();
65
66        if ($constructor === null) {
 
67            throw InvalidHydrationTarget::forTarget(
68                $class,
69                reason: 'target has no constructor; declare a public constructor or implement Hydratable',
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
64        $constructor = $reflection->getConstructor();
65
66        if ($constructor === null) {
 
73        $parameters = [];
74
75        // Reflection lists the parameters in declaration order.
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
77            $parameters[] = $this->parameterMetadata($class, $parameter);
 
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
77            $parameters[] = $this->parameterMetadata($class, $parameter);
78        }
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
64        $constructor = $reflection->getConstructor();
65
66        if ($constructor === null) {
 
73        $parameters = [];
74
75        // Reflection lists the parameters in declaration order.
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
77            $parameters[] = $this->parameterMetadata($class, $parameter);
78        }
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
44    private function build(string $class): ClassMetadata
45    {
46        // Builtin type names like `int` are never class names.
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
47        if (!class_exists($class)) {
 
54        if (is_a($class, Hydratable::class, true)) {
 
58        $reflection = new ReflectionClass($class);
59
60        if (!$reflection->isInstantiable()) {
 
64        $constructor = $reflection->getConstructor();
65
66        if ($constructor === null) {
 
73        $parameters = [];
74
75        // Reflection lists the parameters in declaration order.
76        foreach ($constructor->getParameters() as $parameter) {
 
76        foreach ($constructor->getParameters() as $parameter) {
77            $parameters[] = $this->parameterMetadata($class, $parameter);
78        }
79
80        return new ClassMetadata($class, false, true, $parameters);
81    }
StaticReflectionCache->columnName
123        string $class,
124        ReflectionParameter $parameter,
125        string $parameterName,
126    ): string {
127        $attributes = $parameter->getAttributes(Column::class);
128
129        if ($attributes === []) {
 
130            return $parameterName;
123        string $class,
124        ReflectionParameter $parameter,
125        string $parameterName,
126    ): string {
127        $attributes = $parameter->getAttributes(Column::class);
128
129        if ($attributes === []) {
 
133        try {
134            $column = $attributes[0]->newInstance();
 
143        $columnName = $column->name;
144
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
146            throw InvalidHydrationTarget::forParameter(
147                $class,
148                $parameterName,
149                'has an empty #[Column] name',
123        string $class,
124        ReflectionParameter $parameter,
125        string $parameterName,
126    ): string {
127        $attributes = $parameter->getAttributes(Column::class);
128
129        if ($attributes === []) {
 
133        try {
134            $column = $attributes[0]->newInstance();
 
143        $columnName = $column->name;
144
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
153        return $columnName;
154    }
123        string $class,
124        ReflectionParameter $parameter,
125        string $parameterName,
126    ): string {
127        $attributes = $parameter->getAttributes(Column::class);
128
129        if ($attributes === []) {
 
133        try {
134            $column = $attributes[0]->newInstance();
 
143        $columnName = $column->name;
144
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
146            throw InvalidHydrationTarget::forParameter(
147                $class,
148                $parameterName,
149                'has an empty #[Column] name',
123        string $class,
124        ReflectionParameter $parameter,
125        string $parameterName,
126    ): string {
127        $attributes = $parameter->getAttributes(Column::class);
128
129        if ($attributes === []) {
 
133        try {
134            $column = $attributes[0]->newInstance();
 
143        $columnName = $column->name;
144
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
153        return $columnName;
154    }
123        string $class,
124        ReflectionParameter $parameter,
125        string $parameterName,
126    ): string {
127        $attributes = $parameter->getAttributes(Column::class);
128
129        if ($attributes === []) {
 
133        try {
134            $column = $attributes[0]->newInstance();
 
143        $columnName = $column->name;
144
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
146            throw InvalidHydrationTarget::forParameter(
147                $class,
148                $parameterName,
149                'has an empty #[Column] name',
123        string $class,
124        ReflectionParameter $parameter,
125        string $parameterName,
126    ): string {
127        $attributes = $parameter->getAttributes(Column::class);
128
129        if ($attributes === []) {
 
133        try {
134            $column = $attributes[0]->newInstance();
 
143        $columnName = $column->name;
144
145        if ($columnName === '' || trim($columnName) === '') {
 
145        if ($columnName === '' || trim($columnName) === '') {
 
153        return $columnName;
154    }
135        } catch (Throwable) {
 
136            throw InvalidHydrationTarget::forParameter(
137                $class,
138                $parameterName,
139                'has an invalid #[Column] attribute',
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
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
219            return new NamedTypeMetadata($name, $name, null, null);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
223            return new NamedTypeMetadata($name, null, 'immutable', null);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
226        if ($name === DateTime::class) {
 
227            return new NamedTypeMetadata($name, null, 'mutable', null);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
226        if ($name === DateTime::class) {
 
230        if (is_subclass_of($name, BackedEnum::class)) {
 
231            return new NamedTypeMetadata($name, null, null, $name);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
226        if ($name === DateTime::class) {
 
230        if (is_subclass_of($name, BackedEnum::class)) {
 
234        throw InvalidHydrationTarget::forParameter(
235            $class,
236            $parameterName,
237            "uses unsupported type {$name}",
238        );
239    }
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
219            return new NamedTypeMetadata($name, $name, null, null);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
223            return new NamedTypeMetadata($name, null, 'immutable', null);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
226        if ($name === DateTime::class) {
 
227            return new NamedTypeMetadata($name, null, 'mutable', null);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
226        if ($name === DateTime::class) {
 
230        if (is_subclass_of($name, BackedEnum::class)) {
 
231            return new NamedTypeMetadata($name, null, null, $name);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
226        if ($name === DateTime::class) {
 
230        if (is_subclass_of($name, BackedEnum::class)) {
 
234        throw InvalidHydrationTarget::forParameter(
235            $class,
236            $parameterName,
237            "uses unsupported type {$name}",
238        );
239    }
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
219            return new NamedTypeMetadata($name, $name, null, null);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
223            return new NamedTypeMetadata($name, null, 'immutable', null);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
226        if ($name === DateTime::class) {
 
227            return new NamedTypeMetadata($name, null, 'mutable', null);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
226        if ($name === DateTime::class) {
 
230        if (is_subclass_of($name, BackedEnum::class)) {
 
231            return new NamedTypeMetadata($name, null, null, $name);
211        string $class,
212        string $parameterName,
213        ReflectionNamedType $type,
214    ): NamedTypeMetadata {
215        // Reflection reports builtin type names in lowercase.
216        $name = $type->getName();
217
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
218        if ($type->isBuiltin() && in_array($name, ['int', 'float', 'bool', 'string'], true)) {
 
222        if ($name === DateTimeImmutable::class) {
 
226        if ($name === DateTime::class) {
 
230        if (is_subclass_of($name, BackedEnum::class)) {
 
234        throw InvalidHydrationTarget::forParameter(
235            $class,
236            $parameterName,
237            "uses unsupported type {$name}",
238        );
239    }
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        );
115    }
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        );
115    }
StaticReflectionCache->reset
40        self::$entries = [];
41    }
StaticReflectionCache->typeMetadata
161        string $class,
162        string $parameterName,
163        ReflectionType $type,
164    ): TypeMetadata {
165        if ($type instanceof ReflectionNamedType) {
 
166            $name = $this->namedTypeMetadata($class, $parameterName, $type);
167
168            return new TypeMetadata('named', $type->allowsNull(), [$name]);
161        string $class,
162        string $parameterName,
163        ReflectionType $type,
164    ): TypeMetadata {
165        if ($type instanceof ReflectionNamedType) {
 
171        if ($type instanceof ReflectionUnionType) {
 
172            $names = [];
173
174            foreach ($type->getTypes() as $inner) {
 
174            foreach ($type->getTypes() as $inner) {
 
175                if (!$inner instanceof ReflectionNamedType) {
 
176                    throw InvalidHydrationTarget::forParameter(
177                        $class,
178                        $parameterName,
179                        'uses an unsupported intersection or DNF type',
161        string $class,
162        string $parameterName,
163        ReflectionType $type,
164    ): TypeMetadata {
165        if ($type instanceof ReflectionNamedType) {
 
171        if ($type instanceof ReflectionUnionType) {
 
172            $names = [];
173
174            foreach ($type->getTypes() as $inner) {
 
174            foreach ($type->getTypes() as $inner) {
 
175                if (!$inner instanceof ReflectionNamedType) {
 
183                if ($inner->getName() === 'null') {
 
184                    continue;
 
174            foreach ($type->getTypes() as $inner) {
 
174            foreach ($type->getTypes() as $inner) {
175                if (!$inner instanceof ReflectionNamedType) {
176                    throw InvalidHydrationTarget::forParameter(
177                        $class,
178                        $parameterName,
179                        'uses an unsupported intersection or DNF type',
180                    );
181                }
182
183                if ($inner->getName() === 'null') {
184                    continue;
185                }
186
187                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
188            }
189
190            /**
191             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
192             * Psalm cannot infer that after the runtime null-arm filter above.
193             *
194             * @var non-empty-list<NamedTypeMetadata> $names
195             */
196            return new TypeMetadata('union', $type->allowsNull(), $names);
161        string $class,
162        string $parameterName,
163        ReflectionType $type,
164    ): TypeMetadata {
165        if ($type instanceof ReflectionNamedType) {
 
171        if ($type instanceof ReflectionUnionType) {
 
172            $names = [];
173
174            foreach ($type->getTypes() as $inner) {
 
174            foreach ($type->getTypes() as $inner) {
 
175                if (!$inner instanceof ReflectionNamedType) {
 
183                if ($inner->getName() === 'null') {
 
174            foreach ($type->getTypes() as $inner) {
175                if (!$inner instanceof ReflectionNamedType) {
176                    throw InvalidHydrationTarget::forParameter(
177                        $class,
178                        $parameterName,
179                        'uses an unsupported intersection or DNF type',
180                    );
181                }
182
183                if ($inner->getName() === 'null') {
184                    continue;
185                }
186
187                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
 
174            foreach ($type->getTypes() as $inner) {
 
174            foreach ($type->getTypes() as $inner) {
175                if (!$inner instanceof ReflectionNamedType) {
176                    throw InvalidHydrationTarget::forParameter(
177                        $class,
178                        $parameterName,
179                        'uses an unsupported intersection or DNF type',
180                    );
181                }
182
183                if ($inner->getName() === 'null') {
184                    continue;
185                }
186
187                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
188            }
189
190            /**
191             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
192             * Psalm cannot infer that after the runtime null-arm filter above.
193             *
194             * @var non-empty-list<NamedTypeMetadata> $names
195             */
196            return new TypeMetadata('union', $type->allowsNull(), $names);
161        string $class,
162        string $parameterName,
163        ReflectionType $type,
164    ): TypeMetadata {
165        if ($type instanceof ReflectionNamedType) {
 
171        if ($type instanceof ReflectionUnionType) {
 
172            $names = [];
173
174            foreach ($type->getTypes() as $inner) {
 
174            foreach ($type->getTypes() as $inner) {
 
174            foreach ($type->getTypes() as $inner) {
175                if (!$inner instanceof ReflectionNamedType) {
176                    throw InvalidHydrationTarget::forParameter(
177                        $class,
178                        $parameterName,
179                        'uses an unsupported intersection or DNF type',
180                    );
181                }
182
183                if ($inner->getName() === 'null') {
184                    continue;
185                }
186
187                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
188            }
189
190            /**
191             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
192             * Psalm cannot infer that after the runtime null-arm filter above.
193             *
194             * @var non-empty-list<NamedTypeMetadata> $names
195             */
196            return new TypeMetadata('union', $type->allowsNull(), $names);
161        string $class,
162        string $parameterName,
163        ReflectionType $type,
164    ): TypeMetadata {
165        if ($type instanceof ReflectionNamedType) {
 
171        if ($type instanceof ReflectionUnionType) {
 
172            $names = [];
173
174            foreach ($type->getTypes() as $inner) {
 
174            foreach ($type->getTypes() as $inner) {
175                if (!$inner instanceof ReflectionNamedType) {
176                    throw InvalidHydrationTarget::forParameter(
177                        $class,
178                        $parameterName,
179                        'uses an unsupported intersection or DNF type',
180                    );
181                }
182
183                if ($inner->getName() === 'null') {
184                    continue;
185                }
186
187                $names[] = $this->namedTypeMetadata($class, $parameterName, $inner);
188            }
189
190            /**
191             * ReflectionUnionType always contains at least one non-null arm in valid PHP;
192             * Psalm cannot infer that after the runtime null-arm filter above.
193             *
194             * @var non-empty-list<NamedTypeMetadata> $names
195             */
196            return new TypeMetadata('union', $type->allowsNull(), $names);
161        string $class,
162        string $parameterName,
163        ReflectionType $type,
164    ): TypeMetadata {
165        if ($type instanceof ReflectionNamedType) {
 
171        if ($type instanceof ReflectionUnionType) {
 
199        throw InvalidHydrationTarget::forParameter(
200            $class,
201            $parameterName,
202            'uses an unsupported intersection type',
203        );
204    }