diff --git a/src/ORM/AutoHydratorRecursive.php b/src/ORM/AutoHydratorRecursive.php index 35afebf..ad5bec7 100644 --- a/src/ORM/AutoHydratorRecursive.php +++ b/src/ORM/AutoHydratorRecursive.php @@ -41,6 +41,13 @@ class AutoHydratorRecursive */ protected array $entitiesMap = []; + /** + * The map of aliases and corresponding Table objects. + * + * @var array + */ + protected array $aliasMap = []; + /** * @var \Cake\Datasource\EntityInterface[] */ @@ -56,11 +63,13 @@ class AutoHydratorRecursive /** * @param \Cake\ORM\Table $rootTable * @param mixed[] $mappingStrategy Mapping strategy. + * @param array $aliasMap Aliases and corresponding Table objects. */ - public function __construct(Table $rootTable, array $mappingStrategy) + public function __construct(Table $rootTable, array $mappingStrategy, array $aliasMap) { $this->rootTable = $rootTable; $this->mappingStrategy = $mappingStrategy; + $this->aliasMap = $aliasMap; } /** @@ -215,6 +224,17 @@ protected function constructEntity( } } } + if (isset($this->aliasMap[$alias])) { + /** @var \Cake\ORM\Table $Table */ + $Table = $this->aliasMap[$alias]; + $options = [ + 'validate' => false, + ]; + $entity = $Table->marshaller()->one($fields, $options); + $entity->clean(); + $entity->setNew(false); + return $entity; + } $options = [ 'markClean' => true, 'markNew' => false, diff --git a/src/ORM/MappingStrategy.php b/src/ORM/MappingStrategy.php index 0e70c68..d40c93f 100644 --- a/src/ORM/MappingStrategy.php +++ b/src/ORM/MappingStrategy.php @@ -13,6 +13,7 @@ use Cake\ORM\Locator\LocatorAwareTrait; use Cake\Utility\Hash; use Cake\Utility\Inflector; +use RuntimeException; class MappingStrategy { @@ -35,6 +36,13 @@ class MappingStrategy */ protected array $aliasList; + /** + * The map of aliases and corresponding Table objects. + * + * @var array + */ + protected array $aliasMap = []; + /** * A list of aliases to be mapped. * @@ -63,12 +71,14 @@ public function __construct(Table $rootTable, array $aliases) } $this->aliasList = $aliases; $this->unknownAliases = array_combine($aliases, $aliases); + $this->aliasMap = array_fill_keys($aliases, null); $rootAlias = $rootTable->getAlias(); if (!isset($this->unknownAliases[$rootAlias])) { $message = "The query must select at least one column from the root table."; $message .= " The column alias must use {$rootAlias}__{column_name} format"; throw new UnknownAliasException($message); } + $this->aliasMap[$rootAlias] = $rootTable; unset($this->unknownAliases[$rootAlias]); } @@ -120,6 +130,7 @@ private function scanRootLevel(Table $table): array if (!isset($this->unknownAliases[$alias])) { continue; } + $this->aliasMap[$alias] = $target; unset($this->unknownAliases[$alias]); $firstLevelAssoc = [ 'className' => $target->getEntityClass(), @@ -137,6 +148,7 @@ private function scanRootLevel(Table $table): array 'primaryKey' => $assoc->junction()->getPrimaryKey(), 'propertyName' => Inflector::underscore(Inflector::singularize($through)), ]; + $this->aliasMap[$through] = $assoc->junction(); unset($this->unknownAliases[$through]); } } @@ -179,6 +191,7 @@ private function scanTableRecursive(string $alias): array if (!isset($this->unknownAliases[$childAlias])) { continue; } + $this->aliasMap[$childAlias] = $target; unset($this->unknownAliases[$childAlias]); $result[$type][$childAlias]['className'] = $target->getEntityClass(); $result[$type][$childAlias]['primaryKey'] = $target->getPrimaryKey(); @@ -194,6 +207,7 @@ private function scanTableRecursive(string $alias): array 'propertyName' => Inflector::underscore(Inflector::singularize($through)), ]; if (isset($this->unknownAliases[$through])) { + $this->aliasMap[$through] = $assoc->junction(); unset($this->unknownAliases[$through]); } } else { @@ -241,4 +255,18 @@ private function unknownAliasesToString(): string { return implode("', '", array_keys($this->unknownAliases)); } + + /** + * Gets aliases map. + * + * @return array Keys are alias names. + */ + public function getAliasMap(): array + { + $aliasWithoutTable = array_search(null, $this->aliasMap, true); + if (in_array(null, $this->aliasMap, true) || $aliasWithoutTable !== false) { + throw new RuntimeException("Failed to locate Table object for alias '$aliasWithoutTable'"); + } + return $this->aliasMap; + } } diff --git a/src/ORM/StatementQuery.php b/src/ORM/StatementQuery.php index 374800a..83d3aa1 100644 --- a/src/ORM/StatementQuery.php +++ b/src/ORM/StatementQuery.php @@ -52,12 +52,14 @@ public function all(): array if (!$rows) { return []; } + $aliasMap = []; if ($this->mapStrategy === null) { $aliases = $this->extractAliases($rows); $strategy = new MappingStrategy($this->rootTable, $aliases); $this->mapStrategy = $strategy->build()->toArray(); + $aliasMap = $strategy->getAliasMap(); } - $hydrator = new AutoHydratorRecursive($this->rootTable, $this->mapStrategy); + $hydrator = new AutoHydratorRecursive($this->rootTable, $this->mapStrategy, $aliasMap); return $hydrator->hydrateMany($rows); } diff --git a/tests/TestApp/Model/Table/CommentsTable.php b/tests/TestApp/Model/Table/CommentsTable.php index 9b61b4d..f5fe330 100644 --- a/tests/TestApp/Model/Table/CommentsTable.php +++ b/tests/TestApp/Model/Table/CommentsTable.php @@ -21,5 +21,6 @@ public function initialize(array $config): void parent::initialize($config); $this->belongsTo('Articles', ['className' => ArticlesTable::class]); $this->belongsTo('Users', ['className' => UsersTable::class]); + $this->addBehavior('Timestamp'); } } diff --git a/tests/TestCase/ORM/NativeQueryMapperTest.php b/tests/TestCase/ORM/NativeQueryMapperTest.php index bffc304..7c59921 100644 --- a/tests/TestCase/ORM/NativeQueryMapperTest.php +++ b/tests/TestCase/ORM/NativeQueryMapperTest.php @@ -4,7 +4,6 @@ namespace Bancer\NativeQueryMapperTest\TestCase; -use PHPUnit\Framework\TestCase; use Bancer\NativeQueryMapper\ORM\MissingColumnException; use Bancer\NativeQueryMapper\ORM\UnknownAliasException; use Bancer\NativeQueryMapperTest\TestApp\Model\Entity\Article; @@ -18,6 +17,8 @@ use Bancer\NativeQueryMapperTest\TestApp\Model\Table\CountriesTable; use Bancer\NativeQueryMapperTest\TestApp\Model\Table\UsersTable; use Cake\ORM\Locator\LocatorAwareTrait; +use DateTimeImmutable; +use PHPUnit\Framework\TestCase; class NativeQueryMapperTest extends TestCase { @@ -1224,4 +1225,30 @@ public function testDeepAssociationsWithBelongsToManyMinimalSQL(): void $this->assertEqualsEntities($cakeEntities, $actual); static::assertEquals($cakeEntities, $actual);*/ } + + public function testDatetimeFields(): void + { + /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CommentsTable $CommentsTable */ + $CommentsTable = $this->fetchTable(CommentsTable::class); + $stmt = $CommentsTable->prepareSQL(" + SELECT + id AS Comments__id, + content AS Comments__content, + created AS Comments__created + FROM comments + "); + $actual = $CommentsTable->fromNativeQuery($stmt)->all(); + static::assertCount(5, $actual); + static::assertInstanceOf(Comment::class, $actual[0]); + $expected = [ + 'id' => 1, + 'content' => 'Comment 1', + 'created' => new DateTimeImmutable('2025-10-23 14:00:00'), + ]; + $cakeEntities = $CommentsTable->find() + ->select(['Comments.id', 'Comments.content', 'Comments.created']) + ->toArray(); + static::assertEquals($expected, $actual[0]->toArray()); + static::assertEquals($cakeEntities, $actual); + } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b2d6503..67f4a55 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -60,6 +60,7 @@ article_id INTEGER NOT NULL, user_id INTEGER NOT NULL, content TEXT, + created DATETIME NULL, FOREIGN KEY (article_id) REFERENCES articles(id), FOREIGN KEY (user_id) REFERENCES users(id) ); @@ -123,13 +124,13 @@ "); $connection->execute(" - INSERT INTO comments (id, article_id, user_id, content) + INSERT INTO comments (id, article_id, user_id, content, created) VALUES - (1,1,2,'Comment 1'), - (2,1,3,'Comment 2'), - (3,2,1,'Comment 3'), - (4,3,4,'Comment 4'), - (5,5,5,'Comment 5'); + (1,1,2,'Comment 1','2025-10-23 14:00:00'), + (2,1,3,'Comment 2','2025-10-24 15:00:00'), + (3,2,1,'Comment 3','2025-10-25 16:00:00'), + (4,3,4,'Comment 4','2025-10-26 17:00:00'), + (5,5,5,'Comment 5','2025-10-27 18:00:00'); "); $connection->execute("