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