Skip to content

Commit 2eb9fda

Browse files
authored
Merge pull request #7 from bancer/develop
Improve mapping of datetime fields
2 parents 8cdf287 + bf908f2 commit 2eb9fda

6 files changed

Lines changed: 88 additions & 9 deletions

File tree

src/ORM/AutoHydratorRecursive.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ class AutoHydratorRecursive
4141
*/
4242
protected array $entitiesMap = [];
4343

44+
/**
45+
* The map of aliases and corresponding Table objects.
46+
*
47+
* @var array<string,\Cake\ORM\Table|null>
48+
*/
49+
protected array $aliasMap = [];
50+
4451
/**
4552
* @var \Cake\Datasource\EntityInterface[]
4653
*/
@@ -56,11 +63,13 @@ class AutoHydratorRecursive
5663
/**
5764
* @param \Cake\ORM\Table $rootTable
5865
* @param mixed[] $mappingStrategy Mapping strategy.
66+
* @param array<string,\Cake\ORM\Table> $aliasMap Aliases and corresponding Table objects.
5967
*/
60-
public function __construct(Table $rootTable, array $mappingStrategy)
68+
public function __construct(Table $rootTable, array $mappingStrategy, array $aliasMap)
6169
{
6270
$this->rootTable = $rootTable;
6371
$this->mappingStrategy = $mappingStrategy;
72+
$this->aliasMap = $aliasMap;
6473
}
6574

6675
/**
@@ -215,6 +224,17 @@ protected function constructEntity(
215224
}
216225
}
217226
}
227+
if (isset($this->aliasMap[$alias])) {
228+
/** @var \Cake\ORM\Table $Table */
229+
$Table = $this->aliasMap[$alias];
230+
$options = [
231+
'validate' => false,
232+
];
233+
$entity = $Table->marshaller()->one($fields, $options);
234+
$entity->clean();
235+
$entity->setNew(false);
236+
return $entity;
237+
}
218238
$options = [
219239
'markClean' => true,
220240
'markNew' => false,

src/ORM/MappingStrategy.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Cake\ORM\Locator\LocatorAwareTrait;
1414
use Cake\Utility\Hash;
1515
use Cake\Utility\Inflector;
16+
use RuntimeException;
1617

1718
class MappingStrategy
1819
{
@@ -35,6 +36,13 @@ class MappingStrategy
3536
*/
3637
protected array $aliasList;
3738

39+
/**
40+
* The map of aliases and corresponding Table objects.
41+
*
42+
* @var array<string,\Cake\ORM\Table|null>
43+
*/
44+
protected array $aliasMap = [];
45+
3846
/**
3947
* A list of aliases to be mapped.
4048
*
@@ -63,12 +71,14 @@ public function __construct(Table $rootTable, array $aliases)
6371
}
6472
$this->aliasList = $aliases;
6573
$this->unknownAliases = array_combine($aliases, $aliases);
74+
$this->aliasMap = array_fill_keys($aliases, null);
6675
$rootAlias = $rootTable->getAlias();
6776
if (!isset($this->unknownAliases[$rootAlias])) {
6877
$message = "The query must select at least one column from the root table.";
6978
$message .= " The column alias must use {$rootAlias}__{column_name} format";
7079
throw new UnknownAliasException($message);
7180
}
81+
$this->aliasMap[$rootAlias] = $rootTable;
7282
unset($this->unknownAliases[$rootAlias]);
7383
}
7484

@@ -120,6 +130,7 @@ private function scanRootLevel(Table $table): array
120130
if (!isset($this->unknownAliases[$alias])) {
121131
continue;
122132
}
133+
$this->aliasMap[$alias] = $target;
123134
unset($this->unknownAliases[$alias]);
124135
$firstLevelAssoc = [
125136
'className' => $target->getEntityClass(),
@@ -137,6 +148,7 @@ private function scanRootLevel(Table $table): array
137148
'primaryKey' => $assoc->junction()->getPrimaryKey(),
138149
'propertyName' => Inflector::underscore(Inflector::singularize($through)),
139150
];
151+
$this->aliasMap[$through] = $assoc->junction();
140152
unset($this->unknownAliases[$through]);
141153
}
142154
}
@@ -179,6 +191,7 @@ private function scanTableRecursive(string $alias): array
179191
if (!isset($this->unknownAliases[$childAlias])) {
180192
continue;
181193
}
194+
$this->aliasMap[$childAlias] = $target;
182195
unset($this->unknownAliases[$childAlias]);
183196
$result[$type][$childAlias]['className'] = $target->getEntityClass();
184197
$result[$type][$childAlias]['primaryKey'] = $target->getPrimaryKey();
@@ -194,6 +207,7 @@ private function scanTableRecursive(string $alias): array
194207
'propertyName' => Inflector::underscore(Inflector::singularize($through)),
195208
];
196209
if (isset($this->unknownAliases[$through])) {
210+
$this->aliasMap[$through] = $assoc->junction();
197211
unset($this->unknownAliases[$through]);
198212
}
199213
} else {
@@ -241,4 +255,18 @@ private function unknownAliasesToString(): string
241255
{
242256
return implode("', '", array_keys($this->unknownAliases));
243257
}
258+
259+
/**
260+
* Gets aliases map.
261+
*
262+
* @return array<string,\Cake\ORM\Table> Keys are alias names.
263+
*/
264+
public function getAliasMap(): array
265+
{
266+
$aliasWithoutTable = array_search(null, $this->aliasMap, true);
267+
if (in_array(null, $this->aliasMap, true) || $aliasWithoutTable !== false) {
268+
throw new RuntimeException("Failed to locate Table object for alias '$aliasWithoutTable'");
269+
}
270+
return $this->aliasMap;
271+
}
244272
}

src/ORM/StatementQuery.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,14 @@ public function all(): array
5252
if (!$rows) {
5353
return [];
5454
}
55+
$aliasMap = [];
5556
if ($this->mapStrategy === null) {
5657
$aliases = $this->extractAliases($rows);
5758
$strategy = new MappingStrategy($this->rootTable, $aliases);
5859
$this->mapStrategy = $strategy->build()->toArray();
60+
$aliasMap = $strategy->getAliasMap();
5961
}
60-
$hydrator = new AutoHydratorRecursive($this->rootTable, $this->mapStrategy);
62+
$hydrator = new AutoHydratorRecursive($this->rootTable, $this->mapStrategy, $aliasMap);
6163
return $hydrator->hydrateMany($rows);
6264
}
6365

tests/TestApp/Model/Table/CommentsTable.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ public function initialize(array $config): void
2121
parent::initialize($config);
2222
$this->belongsTo('Articles', ['className' => ArticlesTable::class]);
2323
$this->belongsTo('Users', ['className' => UsersTable::class]);
24+
$this->addBehavior('Timestamp');
2425
}
2526
}

tests/TestCase/ORM/NativeQueryMapperTest.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Bancer\NativeQueryMapperTest\TestCase;
66

7-
use PHPUnit\Framework\TestCase;
87
use Bancer\NativeQueryMapper\ORM\MissingColumnException;
98
use Bancer\NativeQueryMapper\ORM\UnknownAliasException;
109
use Bancer\NativeQueryMapperTest\TestApp\Model\Entity\Article;
@@ -18,6 +17,8 @@
1817
use Bancer\NativeQueryMapperTest\TestApp\Model\Table\CountriesTable;
1918
use Bancer\NativeQueryMapperTest\TestApp\Model\Table\UsersTable;
2019
use Cake\ORM\Locator\LocatorAwareTrait;
20+
use DateTimeImmutable;
21+
use PHPUnit\Framework\TestCase;
2122

2223
class NativeQueryMapperTest extends TestCase
2324
{
@@ -1224,4 +1225,30 @@ public function testDeepAssociationsWithBelongsToManyMinimalSQL(): void
12241225
$this->assertEqualsEntities($cakeEntities, $actual);
12251226
static::assertEquals($cakeEntities, $actual);*/
12261227
}
1228+
1229+
public function testDatetimeFields(): void
1230+
{
1231+
/** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CommentsTable $CommentsTable */
1232+
$CommentsTable = $this->fetchTable(CommentsTable::class);
1233+
$stmt = $CommentsTable->prepareSQL("
1234+
SELECT
1235+
id AS Comments__id,
1236+
content AS Comments__content,
1237+
created AS Comments__created
1238+
FROM comments
1239+
");
1240+
$actual = $CommentsTable->fromNativeQuery($stmt)->all();
1241+
static::assertCount(5, $actual);
1242+
static::assertInstanceOf(Comment::class, $actual[0]);
1243+
$expected = [
1244+
'id' => 1,
1245+
'content' => 'Comment 1',
1246+
'created' => new DateTimeImmutable('2025-10-23 14:00:00'),
1247+
];
1248+
$cakeEntities = $CommentsTable->find()
1249+
->select(['Comments.id', 'Comments.content', 'Comments.created'])
1250+
->toArray();
1251+
static::assertEquals($expected, $actual[0]->toArray());
1252+
static::assertEquals($cakeEntities, $actual);
1253+
}
12271254
}

tests/bootstrap.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
article_id INTEGER NOT NULL,
6161
user_id INTEGER NOT NULL,
6262
content TEXT,
63+
created DATETIME NULL,
6364
FOREIGN KEY (article_id) REFERENCES articles(id),
6465
FOREIGN KEY (user_id) REFERENCES users(id)
6566
);
@@ -123,13 +124,13 @@
123124
");
124125

125126
$connection->execute("
126-
INSERT INTO comments (id, article_id, user_id, content)
127+
INSERT INTO comments (id, article_id, user_id, content, created)
127128
VALUES
128-
(1,1,2,'Comment 1'),
129-
(2,1,3,'Comment 2'),
130-
(3,2,1,'Comment 3'),
131-
(4,3,4,'Comment 4'),
132-
(5,5,5,'Comment 5');
129+
(1,1,2,'Comment 1','2025-10-23 14:00:00'),
130+
(2,1,3,'Comment 2','2025-10-24 15:00:00'),
131+
(3,2,1,'Comment 3','2025-10-25 16:00:00'),
132+
(4,3,4,'Comment 4','2025-10-26 17:00:00'),
133+
(5,5,5,'Comment 5','2025-10-27 18:00:00');
133134
");
134135

135136
$connection->execute("

0 commit comments

Comments
 (0)