Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
124 / 124 |
|
94.59% |
70 / 74 |
|
51.85% |
28 / 54 |
|
50.00% |
4 / 8 |
CRAP | |
0.00% |
0 / 1 |
| StaticReflectionCache | |
100.00% |
124 / 124 |
|
94.59% |
70 / 74 |
|
51.85% |
28 / 54 |
|
100.00% |
8 / 8 |
154.55 | |
100.00% |
1 / 1 |
| metadata | |
100.00% |
3 / 3 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| reset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| build | |
100.00% |
21 / 21 |
|
94.12% |
16 / 17 |
|
23.81% |
5 / 21 |
|
100.00% |
1 / 1 |
28.67 | |||
| parameterMetadata | |
100.00% |
19 / 19 |
|
100.00% |
10 / 10 |
|
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
5 | |||
| columnName | |
100.00% |
18 / 18 |
|
92.31% |
12 / 13 |
|
50.00% |
4 / 8 |
|
100.00% |
1 / 1 |
8.12 | |||
| typeMetadata | |
100.00% |
21 / 21 |
|
100.00% |
12 / 12 |
|
57.14% |
4 / 7 |
|
100.00% |
1 / 1 |
8.83 | |||
| namedTypeMetadata | |
100.00% |
21 / 21 |
|
92.86% |
13 / 14 |
|
75.00% |
6 / 8 |
|
100.00% |
1 / 1 |
6.56 | |||
| isBuiltinTypeName | |
100.00% |
20 / 20 |
|
75.00% |
3 / 4 |
|
50.00% |
1 / 2 |
|
100.00% |
1 / 1 |
1.12 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Quma\Hydration; |
| 6 | |
| 7 | use BackedEnum; |
| 8 | use Celemas\Quma\Column; |
| 9 | use Celemas\Quma\Exception\InvalidHydrationTarget; |
| 10 | use Celemas\Quma\Hydratable; |
| 11 | use DateTime; |
| 12 | use DateTimeImmutable; |
| 13 | use ReflectionClass; |
| 14 | use ReflectionNamedType; |
| 15 | use ReflectionParameter; |
| 16 | use ReflectionType; |
| 17 | use ReflectionUnionType; |
| 18 | use Throwable; |
| 19 | |
| 20 | /** @internal */ |
| 21 | final 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 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 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)) { |
| 46 | if ($this->isBuiltinTypeName($class) || !class_exists($class)) { |
| 47 | throw InvalidHydrationTarget::forTarget( |
| 48 | $class, |
| 49 | reason: 'target is not an existing class', |
| 53 | if (is_a($class, Hydratable::class, true)) { |
| 54 | return new ClassMetadata($class, true, true, null); |
| 57 | $reflection = new ReflectionClass($class); |
| 58 | |
| 59 | if (!$reflection->isInstantiable()) { |
| 60 | throw InvalidHydrationTarget::forTarget($class, reason: 'target is not instantiable'); |
| 63 | $constructor = $reflection->getConstructor(); |
| 64 | |
| 65 | if ($constructor === null) { |
| 66 | return new ClassMetadata($class, false, true, []); |
| 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) { |
| 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 | } |
| 124 | string $class, |
| 125 | ReflectionParameter $parameter, |
| 126 | string $parameterName, |
| 127 | ): string { |
| 128 | $attributes = $parameter->getAttributes(Column::class); |
| 129 | |
| 130 | if ($attributes === []) { |
| 131 | return $parameterName; |
| 134 | try { |
| 135 | $column = $attributes[0]->newInstance(); |
| 136 | } catch (Throwable) { |
| 137 | throw InvalidHydrationTarget::forParameter( |
| 138 | $class, |
| 139 | $parameterName, |
| 140 | 'has an invalid #[Column] attribute', |
| 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) === '') { |
| 146 | if ($columnName === '' || trim($columnName) === '') { |
| 147 | throw InvalidHydrationTarget::forParameter( |
| 148 | $class, |
| 149 | $parameterName, |
| 150 | 'has an empty #[Column] name', |
| 154 | return $columnName; |
| 155 | } |
| 250 | private function isBuiltinTypeName(string $target): bool |
| 251 | { |
| 252 | return in_array( |
| 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 | } |
| 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 | } |
| 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)) { |
| 220 | if (in_array($lower, ['int', 'float', 'bool', 'string'], true)) { |
| 221 | return new NamedTypeMetadata($lower, true, null, $lower, null, null); |
| 224 | throw InvalidHydrationTarget::forParameter( |
| 225 | $class, |
| 226 | $parameterName, |
| 227 | "uses unsupported type {$name}", |
| 231 | if ($name === DateTimeImmutable::class) { |
| 232 | return new NamedTypeMetadata($name, false, DateTimeImmutable::class, null, 'immutable', null); |
| 235 | if ($name === DateTime::class) { |
| 236 | return new NamedTypeMetadata($name, false, DateTime::class, null, 'mutable', null); |
| 239 | if (is_subclass_of($name, BackedEnum::class)) { |
| 240 | return new NamedTypeMetadata($name, false, $name, null, null, $name); |
| 243 | throw InvalidHydrationTarget::forParameter( |
| 244 | $class, |
| 245 | $parameterName, |
| 246 | "uses unsupported type {$name}", |
| 247 | ); |
| 248 | } |
| 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'); |
| 94 | if ($parameter->isPassedByReference()) { |
| 95 | throw InvalidHydrationTarget::forParameter($class, $name, 'is by-reference'); |
| 98 | $type = $parameter->getType(); |
| 99 | |
| 100 | if ($type === null) { |
| 101 | throw InvalidHydrationTarget::forParameter($class, $name, 'has no declared type'); |
| 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, |
| 113 | $hasDefault ? $parameter->getDefaultValue() : null, |
| 114 | $parameter->getPosition(), |
| 115 | ); |
| 116 | } |
| 40 | self::$entries = []; |
| 41 | } |
| 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]); |
| 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', |
| 184 | if (strtolower($inner->getName()) === 'null') { |
| 185 | continue; |
| 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) { |
| 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); |
| 200 | throw InvalidHydrationTarget::forParameter( |
| 201 | $class, |
| 202 | $parameterName, |
| 203 | 'uses an unsupported intersection type', |
| 204 | ); |
| 205 | } |
| 77 | static fn(ParameterMetadata $a, ParameterMetadata $b): int => $a->position <=> $b->position, |