Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
123 / 123 |
|
96.36% |
53 / 55 |
|
72.73% |
24 / 33 |
|
77.78% |
7 / 9 |
CRAP | |
0.00% |
0 / 1 |
| Hydrator | |
100.00% |
123 / 123 |
|
96.36% |
53 / 55 |
|
72.73% |
24 / 33 |
|
100.00% |
9 / 9 |
41.79 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| default | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| hydrate | |
100.00% |
16 / 16 |
|
100.00% |
7 / 7 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| stringKeyRow | |
100.00% |
6 / 6 |
|
100.00% |
6 / 6 |
|
50.00% |
2 / 4 |
|
100.00% |
1 / 1 |
4.12 | |||
| resolveClass | |
100.00% |
6 / 6 |
|
100.00% |
5 / 5 |
|
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| validateClass | |
100.00% |
15 / 15 |
|
87.50% |
7 / 8 |
|
60.00% |
3 / 5 |
|
100.00% |
1 / 1 |
3.58 | |||
| hydrateViaFactory | |
100.00% |
13 / 13 |
|
100.00% |
7 / 7 |
|
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
| hydrateViaConstructor | |
100.00% |
44 / 44 |
|
100.00% |
16 / 16 |
|
60.00% |
6 / 10 |
|
100.00% |
1 / 1 |
12.10 | |||
| 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 Celema\Quma\Hydration; |
| 6 | |
| 7 | use Celema\Quma\Exception\HydrationFailure; |
| 8 | use Celema\Quma\Exception\InvalidHydrationTarget; |
| 9 | use Celema\Quma\Exception\InvalidTypeCoercion; |
| 10 | use Celema\Quma\Exception\MissingColumn; |
| 11 | use Celema\Quma\Hydratable; |
| 12 | use Closure; |
| 13 | use Throwable; |
| 14 | use TypeError; |
| 15 | |
| 16 | /** @internal */ |
| 17 | final class Hydrator |
| 18 | { |
| 19 | private MetadataCache $cache; |
| 20 | private TypeCoercer $coercer; |
| 21 | |
| 22 | public function __construct(?MetadataCache $cache = null) |
| 23 | { |
| 24 | $this->cache = $cache ?? new StaticReflectionCache(); |
| 25 | $this->coercer = new TypeCoercer(); |
| 26 | } |
| 27 | |
| 28 | public static function default(): self |
| 29 | { |
| 30 | return new self(); |
| 31 | } |
| 32 | |
| 33 | /** |
| 34 | * @template T of object |
| 35 | * |
| 36 | * @param array<array-key, mixed> $record |
| 37 | * @param class-string<T>|Closure(array<string, mixed>):class-string<T> $map |
| 38 | * @return T |
| 39 | */ |
| 40 | public function hydrate(array $record, string|Closure $map, ?string $sourcePath): object |
| 41 | { |
| 42 | $row = $this->stringKeyRow($record); |
| 43 | $rowKeys = array_keys($row); |
| 44 | $class = $this->resolveClass($map, $row, $sourcePath, $rowKeys); |
| 45 | |
| 46 | try { |
| 47 | $metadata = $this->cache->metadata($class); |
| 48 | } catch (InvalidHydrationTarget $e) { |
| 49 | throw InvalidHydrationTarget::forTarget( |
| 50 | $class, |
| 51 | $sourcePath, |
| 52 | $rowKeys, |
| 53 | $e->reason(), |
| 54 | $e, |
| 55 | ); |
| 56 | } |
| 57 | |
| 58 | /** |
| 59 | * @mago-expect lint:inline-variable-return Psalm makes this necessary |
| 60 | * @var T $object The object's class is the one $map names or returns. |
| 61 | */ |
| 62 | $object = $metadata->hydratable |
| 63 | ? $this->hydrateViaFactory($class, $row, $sourcePath, $rowKeys) |
| 64 | : $this->hydrateViaConstructor($metadata, $row, $sourcePath, $rowKeys); |
| 65 | |
| 66 | return $object; |
| 67 | } |
| 68 | |
| 69 | /** |
| 70 | * @param array<array-key, mixed> $record |
| 71 | * @return array<string, mixed> |
| 72 | * @psalm-suppress MixedAssignment Row values can be anything. |
| 73 | */ |
| 74 | private function stringKeyRow(array $record): array |
| 75 | { |
| 76 | $row = []; |
| 77 | |
| 78 | foreach ($record as $key => $value) { |
| 79 | if (!is_string($key)) { |
| 80 | continue; |
| 81 | } |
| 82 | |
| 83 | $row[$key] = $value; |
| 84 | } |
| 85 | |
| 86 | return $row; |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * @param array<string, mixed> $row |
| 91 | * @param list<string> $rowKeys |
| 92 | * @param string|Closure $map |
| 93 | * @return class-string |
| 94 | */ |
| 95 | private function resolveClass( |
| 96 | string|Closure $map, |
| 97 | array $row, |
| 98 | ?string $sourcePath, |
| 99 | array $rowKeys, |
| 100 | ): string { |
| 101 | if (is_string($map)) { |
| 102 | return $this->validateClass($map, $sourcePath, $rowKeys); |
| 103 | } |
| 104 | |
| 105 | $result = $map($row); |
| 106 | |
| 107 | if (!is_string($result)) { |
| 108 | throw InvalidHydrationTarget::forClosureResult($result, $sourcePath, $rowKeys); |
| 109 | } |
| 110 | |
| 111 | return $this->validateClass($result, $sourcePath, $rowKeys); |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * @param list<string> $rowKeys |
| 116 | * @return class-string |
| 117 | */ |
| 118 | private function validateClass(string $target, ?string $sourcePath, array $rowKeys): string |
| 119 | { |
| 120 | if ($this->isBuiltinTypeName($target)) { |
| 121 | throw InvalidHydrationTarget::forTarget( |
| 122 | $target, |
| 123 | $sourcePath, |
| 124 | $rowKeys, |
| 125 | 'built-in type names cannot be hydrated', |
| 126 | ); |
| 127 | } |
| 128 | |
| 129 | if (!class_exists($target)) { |
| 130 | throw InvalidHydrationTarget::forTarget( |
| 131 | $target, |
| 132 | $sourcePath, |
| 133 | $rowKeys, |
| 134 | 'target is not an existing class', |
| 135 | ); |
| 136 | } |
| 137 | |
| 138 | return $target; |
| 139 | } |
| 140 | |
| 141 | /** |
| 142 | * @param array<string, mixed> $row |
| 143 | * @param list<string> $rowKeys |
| 144 | * @param class-string $class |
| 145 | */ |
| 146 | private function hydrateViaFactory( |
| 147 | string $class, |
| 148 | array $row, |
| 149 | ?string $sourcePath, |
| 150 | array $rowKeys, |
| 151 | ): object { |
| 152 | if (!is_a($class, Hydratable::class, true)) { |
| 153 | throw InvalidHydrationTarget::forTarget( |
| 154 | $class, |
| 155 | $sourcePath, |
| 156 | $rowKeys, |
| 157 | 'target is not hydratable', |
| 158 | ); |
| 159 | } |
| 160 | |
| 161 | $hydratable = $class; |
| 162 | |
| 163 | try { |
| 164 | return $hydratable::fromRow($row); |
| 165 | } catch (HydrationFailure $e) { |
| 166 | throw $e; |
| 167 | } catch (Throwable $e) { |
| 168 | throw HydrationFailure::fromHydratableFailure($class, $sourcePath, $rowKeys, $e); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * @param array<string, mixed> $row |
| 174 | * @param list<string> $rowKeys |
| 175 | */ |
| 176 | private function hydrateViaConstructor( |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 183 | throw InvalidHydrationTarget::forTarget( |
| 184 | $metadata->class, |
| 185 | $sourcePath, |
| 186 | $rowKeys, |
| 187 | 'target is not instantiable', |
| 188 | ); |
| 189 | } |
| 190 | |
| 191 | $args = []; |
| 192 | |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 199 | } |
| 200 | |
| 201 | /** @psalm-suppress MixedAssignment The parameter's declared type can be anything. */ |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | if ($parameter->hasDefault) { |
| 218 | /** @psalm-suppress MixedAssignment Default values can be anything. */ |
| 219 | $args[$parameter->name] = $parameter->defaultValue; |
| 220 | |
| 221 | continue; |
| 222 | } |
| 223 | |
| 224 | throw MissingColumn::forColumn( |
| 225 | $metadata->class, |
| 226 | $parameter->name, |
| 227 | $parameter->column, |
| 228 | $sourcePath, |
| 229 | $rowKeys, |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | try { |
| 234 | $class = $metadata->class; |
| 235 | |
| 236 | /** @psalm-suppress MixedMethodCall Psalm cannot see the constructor of a bare class-string. */ |
| 237 | return new $class(...$args); |
| 238 | } catch (TypeError $e) { |
| 239 | throw InvalidTypeCoercion::constructorFailure( |
| 240 | $metadata->class, |
| 241 | $sourcePath, |
| 242 | $rowKeys, |
| 243 | $e, |
| 244 | ); |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | private function isBuiltinTypeName(string $target): bool |
| 249 | { |
| 250 | return in_array( |
| 251 | strtolower($target), |
| 252 | [ |
| 253 | 'array', |
| 254 | 'bool', |
| 255 | 'callable', |
| 256 | 'false', |
| 257 | 'float', |
| 258 | 'int', |
| 259 | 'iterable', |
| 260 | 'mixed', |
| 261 | 'never', |
| 262 | 'null', |
| 263 | 'object', |
| 264 | 'string', |
| 265 | 'true', |
| 266 | 'void', |
| 267 | ], |
| 268 | true, |
| 269 | ); |
| 270 | } |
| 271 | } |
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.
| 22 | public function __construct(?MetadataCache $cache = null) |
| 23 | { |
| 24 | $this->cache = $cache ?? new StaticReflectionCache(); |
| 25 | $this->coercer = new TypeCoercer(); |
| 26 | } |
| 30 | return new self(); |
| 31 | } |
| 40 | public function hydrate(array $record, string|Closure $map, ?string $sourcePath): object |
| 41 | { |
| 42 | $row = $this->stringKeyRow($record); |
| 43 | $rowKeys = array_keys($row); |
| 44 | $class = $this->resolveClass($map, $row, $sourcePath, $rowKeys); |
| 45 | |
| 46 | try { |
| 47 | $metadata = $this->cache->metadata($class); |
| 62 | $object = $metadata->hydratable |
| 63 | ? $this->hydrateViaFactory($class, $row, $sourcePath, $rowKeys) |
| 62 | $object = $metadata->hydratable |
| 63 | ? $this->hydrateViaFactory($class, $row, $sourcePath, $rowKeys) |
| 64 | : $this->hydrateViaConstructor($metadata, $row, $sourcePath, $rowKeys); |
| 65 | |
| 66 | return $object; |
| 67 | } |
| 40 | public function hydrate(array $record, string|Closure $map, ?string $sourcePath): object |
| 41 | { |
| 42 | $row = $this->stringKeyRow($record); |
| 43 | $rowKeys = array_keys($row); |
| 44 | $class = $this->resolveClass($map, $row, $sourcePath, $rowKeys); |
| 45 | |
| 46 | try { |
| 47 | $metadata = $this->cache->metadata($class); |
| 62 | $object = $metadata->hydratable |
| 62 | $object = $metadata->hydratable |
| 63 | ? $this->hydrateViaFactory($class, $row, $sourcePath, $rowKeys) |
| 64 | : $this->hydrateViaConstructor($metadata, $row, $sourcePath, $rowKeys); |
| 62 | $object = $metadata->hydratable |
| 63 | ? $this->hydrateViaFactory($class, $row, $sourcePath, $rowKeys) |
| 64 | : $this->hydrateViaConstructor($metadata, $row, $sourcePath, $rowKeys); |
| 65 | |
| 66 | return $object; |
| 67 | } |
| 48 | } catch (InvalidHydrationTarget $e) { |
| 49 | throw InvalidHydrationTarget::forTarget( |
| 50 | $class, |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 183 | throw InvalidHydrationTarget::forTarget( |
| 184 | $metadata->class, |
| 185 | $sourcePath, |
| 186 | $rowKeys, |
| 187 | 'target is not instantiable', |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 191 | $args = []; |
| 192 | |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 199 | } |
| 200 | |
| 201 | /** @psalm-suppress MixedAssignment The parameter's declared type can be anything. */ |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | if ($parameter->hasDefault) { |
| 218 | /** @psalm-suppress MixedAssignment Default values can be anything. */ |
| 219 | $args[$parameter->name] = $parameter->defaultValue; |
| 220 | |
| 221 | continue; |
| 222 | } |
| 223 | |
| 224 | throw MissingColumn::forColumn( |
| 225 | $metadata->class, |
| 226 | $parameter->name, |
| 227 | $parameter->column, |
| 228 | $sourcePath, |
| 229 | $rowKeys, |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | try { |
| 234 | $class = $metadata->class; |
| 235 | |
| 236 | /** @psalm-suppress MixedMethodCall Psalm cannot see the constructor of a bare class-string. */ |
| 237 | return new $class(...$args); |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 191 | $args = []; |
| 192 | |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 199 | } |
| 200 | |
| 201 | /** @psalm-suppress MixedAssignment The parameter's declared type can be anything. */ |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | if ($parameter->hasDefault) { |
| 218 | /** @psalm-suppress MixedAssignment Default values can be anything. */ |
| 219 | $args[$parameter->name] = $parameter->defaultValue; |
| 220 | |
| 221 | continue; |
| 222 | } |
| 223 | |
| 224 | throw MissingColumn::forColumn( |
| 225 | $metadata->class, |
| 226 | $parameter->name, |
| 227 | $parameter->column, |
| 228 | $sourcePath, |
| 229 | $rowKeys, |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | try { |
| 234 | $class = $metadata->class; |
| 235 | |
| 236 | /** @psalm-suppress MixedMethodCall Psalm cannot see the constructor of a bare class-string. */ |
| 237 | return new $class(...$args); |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 191 | $args = []; |
| 192 | |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 199 | } |
| 200 | |
| 201 | /** @psalm-suppress MixedAssignment The parameter's declared type can be anything. */ |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | if ($parameter->hasDefault) { |
| 218 | /** @psalm-suppress MixedAssignment Default values can be anything. */ |
| 219 | $args[$parameter->name] = $parameter->defaultValue; |
| 220 | |
| 221 | continue; |
| 222 | } |
| 223 | |
| 224 | throw MissingColumn::forColumn( |
| 225 | $metadata->class, |
| 226 | $parameter->name, |
| 227 | $parameter->column, |
| 228 | $sourcePath, |
| 229 | $rowKeys, |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | try { |
| 234 | $class = $metadata->class; |
| 235 | |
| 236 | /** @psalm-suppress MixedMethodCall Psalm cannot see the constructor of a bare class-string. */ |
| 237 | return new $class(...$args); |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 191 | $args = []; |
| 192 | |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 199 | } |
| 200 | |
| 201 | /** @psalm-suppress MixedAssignment The parameter's declared type can be anything. */ |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | if ($parameter->hasDefault) { |
| 218 | /** @psalm-suppress MixedAssignment Default values can be anything. */ |
| 219 | $args[$parameter->name] = $parameter->defaultValue; |
| 220 | |
| 221 | continue; |
| 222 | } |
| 223 | |
| 224 | throw MissingColumn::forColumn( |
| 225 | $metadata->class, |
| 226 | $parameter->name, |
| 227 | $parameter->column, |
| 228 | $sourcePath, |
| 229 | $rowKeys, |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | try { |
| 234 | $class = $metadata->class; |
| 235 | |
| 236 | /** @psalm-suppress MixedMethodCall Psalm cannot see the constructor of a bare class-string. */ |
| 237 | return new $class(...$args); |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 191 | $args = []; |
| 192 | |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 217 | if ($parameter->hasDefault) { |
| 219 | $args[$parameter->name] = $parameter->defaultValue; |
| 220 | |
| 221 | continue; |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 199 | } |
| 200 | |
| 201 | /** @psalm-suppress MixedAssignment The parameter's declared type can be anything. */ |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | if ($parameter->hasDefault) { |
| 218 | /** @psalm-suppress MixedAssignment Default values can be anything. */ |
| 219 | $args[$parameter->name] = $parameter->defaultValue; |
| 220 | |
| 221 | continue; |
| 222 | } |
| 223 | |
| 224 | throw MissingColumn::forColumn( |
| 225 | $metadata->class, |
| 226 | $parameter->name, |
| 227 | $parameter->column, |
| 228 | $sourcePath, |
| 229 | $rowKeys, |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | try { |
| 234 | $class = $metadata->class; |
| 235 | |
| 236 | /** @psalm-suppress MixedMethodCall Psalm cannot see the constructor of a bare class-string. */ |
| 237 | return new $class(...$args); |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 191 | $args = []; |
| 192 | |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 217 | if ($parameter->hasDefault) { |
| 224 | throw MissingColumn::forColumn( |
| 225 | $metadata->class, |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 191 | $args = []; |
| 192 | |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 199 | } |
| 200 | |
| 201 | /** @psalm-suppress MixedAssignment The parameter's declared type can be anything. */ |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | if ($parameter->hasDefault) { |
| 218 | /** @psalm-suppress MixedAssignment Default values can be anything. */ |
| 219 | $args[$parameter->name] = $parameter->defaultValue; |
| 220 | |
| 221 | continue; |
| 222 | } |
| 223 | |
| 224 | throw MissingColumn::forColumn( |
| 225 | $metadata->class, |
| 226 | $parameter->name, |
| 227 | $parameter->column, |
| 228 | $sourcePath, |
| 229 | $rowKeys, |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | try { |
| 234 | $class = $metadata->class; |
| 235 | |
| 236 | /** @psalm-suppress MixedMethodCall Psalm cannot see the constructor of a bare class-string. */ |
| 237 | return new $class(...$args); |
| 177 | ClassMetadata $metadata, |
| 178 | array $row, |
| 179 | ?string $sourcePath, |
| 180 | array $rowKeys, |
| 181 | ): object { |
| 182 | if (!$metadata->instantiable) { |
| 191 | $args = []; |
| 192 | |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 193 | foreach ($metadata->parameters ?? [] as $parameter) { |
| 194 | if (array_key_exists($parameter->column, $row)) { |
| 195 | if ($parameter->nullable && $row[$parameter->column] === null) { |
| 196 | $args[$parameter->name] = null; |
| 197 | |
| 198 | continue; |
| 199 | } |
| 200 | |
| 201 | /** @psalm-suppress MixedAssignment The parameter's declared type can be anything. */ |
| 202 | $args[$parameter->name] = $this->coercer->coerce( |
| 203 | $row[$parameter->column], |
| 204 | $parameter->type, |
| 205 | new HydrationContext( |
| 206 | $metadata->class, |
| 207 | $parameter->name, |
| 208 | $parameter->column, |
| 209 | $sourcePath, |
| 210 | $rowKeys, |
| 211 | ), |
| 212 | ); |
| 213 | |
| 214 | continue; |
| 215 | } |
| 216 | |
| 217 | if ($parameter->hasDefault) { |
| 218 | /** @psalm-suppress MixedAssignment Default values can be anything. */ |
| 219 | $args[$parameter->name] = $parameter->defaultValue; |
| 220 | |
| 221 | continue; |
| 222 | } |
| 223 | |
| 224 | throw MissingColumn::forColumn( |
| 225 | $metadata->class, |
| 226 | $parameter->name, |
| 227 | $parameter->column, |
| 228 | $sourcePath, |
| 229 | $rowKeys, |
| 230 | ); |
| 231 | } |
| 232 | |
| 233 | try { |
| 234 | $class = $metadata->class; |
| 235 | |
| 236 | /** @psalm-suppress MixedMethodCall Psalm cannot see the constructor of a bare class-string. */ |
| 237 | return new $class(...$args); |
| 238 | } catch (TypeError $e) { |
| 239 | throw InvalidTypeCoercion::constructorFailure( |
| 240 | $metadata->class, |
| 241 | $sourcePath, |
| 242 | $rowKeys, |
| 243 | $e, |
| 244 | ); |
| 245 | } |
| 246 | } |
| 147 | string $class, |
| 148 | array $row, |
| 149 | ?string $sourcePath, |
| 150 | array $rowKeys, |
| 151 | ): object { |
| 152 | if (!is_a($class, Hydratable::class, true)) { |
| 153 | throw InvalidHydrationTarget::forTarget( |
| 154 | $class, |
| 155 | $sourcePath, |
| 156 | $rowKeys, |
| 157 | 'target is not hydratable', |
| 147 | string $class, |
| 148 | array $row, |
| 149 | ?string $sourcePath, |
| 150 | array $rowKeys, |
| 151 | ): object { |
| 152 | if (!is_a($class, Hydratable::class, true)) { |
| 161 | $hydratable = $class; |
| 162 | |
| 163 | try { |
| 164 | return $hydratable::fromRow($row); |
| 165 | } catch (HydrationFailure $e) { |
| 166 | throw $e; |
| 165 | } catch (HydrationFailure $e) { |
| 167 | } catch (Throwable $e) { |
| 168 | throw HydrationFailure::fromHydratableFailure($class, $sourcePath, $rowKeys, $e); |
| 169 | } |
| 170 | } |
| 248 | private function isBuiltinTypeName(string $target): bool |
| 249 | { |
| 250 | return in_array( |
| 250 | return in_array( |
| 268 | true, |
| 269 | ); |
| 270 | } |
| 248 | private function isBuiltinTypeName(string $target): bool |
| 249 | { |
| 250 | return in_array( |
| 251 | strtolower($target), |
| 252 | [ |
| 253 | 'array', |
| 254 | 'bool', |
| 255 | 'callable', |
| 256 | 'false', |
| 257 | 'float', |
| 258 | 'int', |
| 259 | 'iterable', |
| 260 | 'mixed', |
| 261 | 'never', |
| 262 | 'null', |
| 263 | 'object', |
| 264 | 'string', |
| 265 | 'true', |
| 266 | 'void', |
| 267 | ], |
| 268 | true, |
| 268 | true, |
| 269 | ); |
| 270 | } |
| 96 | string|Closure $map, |
| 97 | array $row, |
| 98 | ?string $sourcePath, |
| 99 | array $rowKeys, |
| 100 | ): string { |
| 101 | if (is_string($map)) { |
| 102 | return $this->validateClass($map, $sourcePath, $rowKeys); |
| 96 | string|Closure $map, |
| 97 | array $row, |
| 98 | ?string $sourcePath, |
| 99 | array $rowKeys, |
| 100 | ): string { |
| 101 | if (is_string($map)) { |
| 105 | $result = $map($row); |
| 106 | |
| 107 | if (!is_string($result)) { |
| 108 | throw InvalidHydrationTarget::forClosureResult($result, $sourcePath, $rowKeys); |
| 96 | string|Closure $map, |
| 97 | array $row, |
| 98 | ?string $sourcePath, |
| 99 | array $rowKeys, |
| 100 | ): string { |
| 101 | if (is_string($map)) { |
| 105 | $result = $map($row); |
| 106 | |
| 107 | if (!is_string($result)) { |
| 111 | return $this->validateClass($result, $sourcePath, $rowKeys); |
| 112 | } |
| 74 | private function stringKeyRow(array $record): array |
| 75 | { |
| 76 | $row = []; |
| 77 | |
| 78 | foreach ($record as $key => $value) { |
| 78 | foreach ($record as $key => $value) { |
| 78 | foreach ($record as $key => $value) { |
| 79 | if (!is_string($key)) { |
| 80 | continue; |
| 78 | foreach ($record as $key => $value) { |
| 78 | foreach ($record as $key => $value) { |
| 79 | if (!is_string($key)) { |
| 80 | continue; |
| 81 | } |
| 82 | |
| 83 | $row[$key] = $value; |
| 84 | } |
| 85 | |
| 86 | return $row; |
| 87 | } |
| 74 | private function stringKeyRow(array $record): array |
| 75 | { |
| 76 | $row = []; |
| 77 | |
| 78 | foreach ($record as $key => $value) { |
| 78 | foreach ($record as $key => $value) { |
| 78 | foreach ($record as $key => $value) { |
| 79 | if (!is_string($key)) { |
| 78 | foreach ($record as $key => $value) { |
| 79 | if (!is_string($key)) { |
| 80 | continue; |
| 81 | } |
| 82 | |
| 83 | $row[$key] = $value; |
| 78 | foreach ($record as $key => $value) { |
| 78 | foreach ($record as $key => $value) { |
| 79 | if (!is_string($key)) { |
| 80 | continue; |
| 81 | } |
| 82 | |
| 83 | $row[$key] = $value; |
| 84 | } |
| 85 | |
| 86 | return $row; |
| 87 | } |
| 74 | private function stringKeyRow(array $record): array |
| 75 | { |
| 76 | $row = []; |
| 77 | |
| 78 | foreach ($record as $key => $value) { |
| 78 | foreach ($record as $key => $value) { |
| 78 | foreach ($record as $key => $value) { |
| 79 | if (!is_string($key)) { |
| 80 | continue; |
| 81 | } |
| 82 | |
| 83 | $row[$key] = $value; |
| 84 | } |
| 85 | |
| 86 | return $row; |
| 87 | } |
| 74 | private function stringKeyRow(array $record): array |
| 75 | { |
| 76 | $row = []; |
| 77 | |
| 78 | foreach ($record as $key => $value) { |
| 78 | foreach ($record as $key => $value) { |
| 79 | if (!is_string($key)) { |
| 80 | continue; |
| 81 | } |
| 82 | |
| 83 | $row[$key] = $value; |
| 84 | } |
| 85 | |
| 86 | return $row; |
| 87 | } |
| 118 | private function validateClass(string $target, ?string $sourcePath, array $rowKeys): string |
| 119 | { |
| 120 | if ($this->isBuiltinTypeName($target)) { |
| 121 | throw InvalidHydrationTarget::forTarget( |
| 122 | $target, |
| 123 | $sourcePath, |
| 124 | $rowKeys, |
| 125 | 'built-in type names cannot be hydrated', |
| 118 | private function validateClass(string $target, ?string $sourcePath, array $rowKeys): string |
| 119 | { |
| 120 | if ($this->isBuiltinTypeName($target)) { |
| 129 | if (!class_exists($target)) { |
| 129 | if (!class_exists($target)) { |
| 129 | if (!class_exists($target)) { |
| 130 | throw InvalidHydrationTarget::forTarget( |
| 131 | $target, |
| 132 | $sourcePath, |
| 133 | $rowKeys, |
| 134 | 'target is not an existing class', |
| 118 | private function validateClass(string $target, ?string $sourcePath, array $rowKeys): string |
| 119 | { |
| 120 | if ($this->isBuiltinTypeName($target)) { |
| 129 | if (!class_exists($target)) { |
| 129 | if (!class_exists($target)) { |
| 129 | if (!class_exists($target)) { |
| 138 | return $target; |
| 139 | } |
| 118 | private function validateClass(string $target, ?string $sourcePath, array $rowKeys): string |
| 119 | { |
| 120 | if ($this->isBuiltinTypeName($target)) { |
| 129 | if (!class_exists($target)) { |
| 129 | if (!class_exists($target)) { |
| 129 | if (!class_exists($target)) { |
| 130 | throw InvalidHydrationTarget::forTarget( |
| 131 | $target, |
| 132 | $sourcePath, |
| 133 | $rowKeys, |
| 134 | 'target is not an existing class', |
| 118 | private function validateClass(string $target, ?string $sourcePath, array $rowKeys): string |
| 119 | { |
| 120 | if ($this->isBuiltinTypeName($target)) { |
| 129 | if (!class_exists($target)) { |
| 129 | if (!class_exists($target)) { |
| 129 | if (!class_exists($target)) { |
| 138 | return $target; |
| 139 | } |