Skip to content

Commit a6b9458

Browse files
authored
Merge pull request #10 from bancer/develop
Refactor native hydration to convert database types using table schema
2 parents 71b9072 + 9906cb3 commit a6b9458

File tree

1 file changed

+108
-17
lines changed

1 file changed

+108
-17
lines changed

src/ORM/RecursiveEntityHydrator.php

Lines changed: 108 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
namespace Bancer\NativeQueryMapper\ORM;
66

77
use Cake\ORM\Table;
8+
use Cake\Database\Connection;
9+
use Cake\Database\FieldTypeConverter;
10+
use Cake\Database\TypeFactory;
11+
use Cake\Database\TypeInterface;
12+
use Cake\Database\TypeMap;
813
use Cake\Datasource\EntityInterface;
914
use Cake\Utility\Hash;
1015
use RuntimeException;
@@ -74,6 +79,22 @@ class RecursiveEntityHydrator
7479
*/
7580
protected array $entities = [];
7681

82+
/**
83+
* Resolved column type objects indexed by alias and column name.
84+
*
85+
* Structure:
86+
* [
87+
* '{alias}' => [
88+
* '{column}' => \Cake\Database\TypeInterface|null
89+
* ]
90+
* ]
91+
*
92+
* A null value indicates that the column exists but has no resolvable type.
93+
*
94+
* @var array<string, array<string, \Cake\Database\TypeInterface|null>>
95+
*/
96+
protected array $columnTypes = [];
97+
7798
/**
7899
* Whether the presence of primary keys is mandatory for all entities,
79100
* inferred automatically based on the mapping strategy.
@@ -213,17 +234,23 @@ protected function map(
213234
}
214235

215236
/**
216-
* Create an entity from raw field data using either:
217-
* - Table marshaller (preferred), or
218-
* - direct entity instantiation (fallback).
237+
* Constructs an entity instance from raw database fields.
219238
*
220-
* Returns null when the row for the alias is "empty" (all NULL fields).
239+
* This method:
240+
* - Skips hydration when all fields are NULL (LEFT JOIN safety)
241+
* - Enforces presence of primary keys when required by mapping strategy
242+
* - Converts database values to PHP values using table schema types
243+
* - Instantiates the entity in a "persisted & clean" state
221244
*
222245
* @param class-string<\Cake\Datasource\EntityInterface> $className Entity class.
223-
* @param mixed[] $fields Raw database fields.
246+
* @param mixed[] $fields Raw database fields (alias stripped).
224247
* @param string $alias Alias of the entity.
225-
* @param string[]|string|null $primaryKey Primary key name(s).
226-
* @return \Cake\Datasource\EntityInterface|null
248+
* @param string[]|string|null $primaryKey Primary key column name(s), if required.
249+
* @throws \RuntimeException When primary keys are required but not configured.
250+
* @throws \Bancer\NativeQueryMapper\ORM\MissingColumnException When required primary key columns are missing
251+
* from the result set.
252+
* @return \Cake\Datasource\EntityInterface|null Fully hydrated entity,
253+
* or null when the row contains only NULL values.
227254
*/
228255
protected function constructEntity(
229256
string $className,
@@ -258,24 +285,88 @@ protected function constructEntity(
258285
}
259286
}
260287
}
288+
$options = [
289+
'markClean' => true,
290+
'markNew' => false,
291+
];
261292
if (isset($this->aliasMap[$alias])) {
262293
/** @var \Cake\ORM\Table $Table */
263294
$Table = $this->aliasMap[$alias];
264-
$options = [
265-
'validate' => false,
295+
$converted = $this->convertDatabaseTypesToPHP($alias, $fields);
296+
$options += [
297+
'source' => $Table->getRegistryAlias(),
266298
];
267-
$entity = $Table->marshaller()->one($fields, $options);
268-
$entity->clean();
269-
$entity->setNew(false);
270-
return $entity;
299+
return new $className($converted, $options);
271300
}
272-
$options = [
273-
'markClean' => true,
274-
'markNew' => false,
275-
];
276301
return new $className($fields, $options);
277302
}
278303

304+
/**
305+
* Converts raw database values to PHP values using the table schema.
306+
*
307+
* Each column is converted using its corresponding database type.
308+
*
309+
* Column types are resolved lazily and cached per alias to avoid repeated
310+
* schema lookups and type instantiation.
311+
*
312+
* @param string $alias Query alias identifying the table schema.
313+
* @param mixed[] $fields Raw database field values indexed by column name.
314+
* @return mixed[] Converted field values suitable for entity construction.
315+
*/
316+
protected function convertDatabaseTypesToPHP(string $alias, array $fields): array
317+
{
318+
/** @var \Cake\ORM\Table $Table */
319+
$Table = $this->aliasMap[$alias];
320+
$driver = $Table->getConnection()->getDriver();
321+
$converted = [];
322+
foreach ($fields as $field => $value) {
323+
if ($value === null) {
324+
$converted[$field] = $value;
325+
continue;
326+
}
327+
$type = $this->getColumnType($alias, $field);
328+
if ($type !== null) {
329+
$converted[$field] = $type->toPHP($value, $driver);
330+
} else {
331+
$converted[$field] = $value;
332+
}
333+
}
334+
return $converted;
335+
}
336+
337+
/**
338+
* Resolves the database type for a given column.
339+
*
340+
* The column type is derived from the table schema associated with
341+
* the provided alias. The resolved type instance is cached to a class field to avoid
342+
* repeated schema access and object construction.
343+
*
344+
* @param string $alias Query alias used to resolve the table.
345+
* @param string $columnName Column name within the table.
346+
* @return \Cake\Database\TypeInterface|null
347+
* Type instance when resolvable, or null if the column does not exist
348+
* or has no associated type.
349+
*/
350+
protected function getColumnType(string $alias, string $columnName): ?TypeInterface
351+
{
352+
if (
353+
!array_key_exists($alias, $this->columnTypes) ||
354+
!array_key_exists($columnName, $this->columnTypes[$alias])
355+
) {
356+
$this->columnTypes[$alias][$columnName] = null;
357+
/** @var \Cake\ORM\Table $Table */
358+
$Table = $this->aliasMap[$alias];
359+
$schema = $Table->getSchema();
360+
if ($schema->hasColumn($columnName)) {
361+
$typeName = $schema->getColumnType($columnName);
362+
if ($typeName !== null) {
363+
$this->columnTypes[$alias][$columnName] = TypeFactory::build($typeName);
364+
}
365+
}
366+
}
367+
return $this->columnTypes[$alias][$columnName];
368+
}
369+
279370
/**
280371
* Compute a stable hash for an entity's field set,
281372
* optionally including the parent entity's hash for hasMany relations.

0 commit comments

Comments
 (0)