Skip to content

Commit 325dea6

Browse files
committed
Uses identityMap from Data
1 parent 25e47ed commit 325dea6

2 files changed

Lines changed: 137 additions & 9 deletions

File tree

src/Mapper.php

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ public function __construct(PDO|Db $db, EntityFactory $entityFactory = new Entit
4444

4545
public function fetch(Collection $collection, mixed $extra = null): mixed
4646
{
47+
if ($extra === null) {
48+
$cached = $this->findInIdentityMap($collection);
49+
if ($cached !== null) {
50+
return $cached;
51+
}
52+
}
53+
4754
$hydrated = $this->fetchHydrated($collection, $this->createStatement($collection, $extra));
4855

4956
return $hydrated ? $this->parseHydrated($hydrated) : false;
@@ -97,7 +104,7 @@ public function flush(): void
97104
$conn->beginTransaction();
98105

99106
try {
100-
foreach ($this->changed as $entity) {
107+
foreach ($this->pending as $entity) {
101108
$this->flushSingle($entity);
102109
}
103110
} catch (Throwable $e) {
@@ -133,15 +140,20 @@ private function getRelatedEntity(object $object, string $remoteField): object|n
133140

134141
private function flushSingle(object $entity): void
135142
{
136-
$coll = $this->tracked[$entity];
137-
$cols = $this->extractColumns($entity, $coll);
138-
139-
if ($this->removed->offsetExists($entity)) {
140-
$this->rawDelete($cols, $coll, $entity);
141-
} elseif ($this->new->offsetExists($entity)) {
142-
$this->rawInsert($cols, $coll, $entity);
143+
$coll = $this->tracked[$entity];
144+
$cols = $this->extractColumns($entity, $coll);
145+
$op = $this->pending[$entity];
146+
147+
match ($op) {
148+
'delete' => $this->rawDelete($cols, $coll, $entity),
149+
'insert' => $this->rawInsert($cols, $coll, $entity),
150+
default => $this->rawUpdate($cols, $coll),
151+
};
152+
153+
if ($op === 'delete') {
154+
$this->evictFromIdentityMap($entity, $coll);
143155
} else {
144-
$this->rawUpdate($cols, $coll);
156+
$this->registerInIdentityMap($entity, $coll);
145157
}
146158
}
147159

@@ -489,6 +501,12 @@ private function hasComposition(string $entity, string|null $next, string|null $
489501
private function parseHydrated(SplObjectStorage $hydrated): object
490502
{
491503
$this->tracked->addAll($hydrated);
504+
505+
// Register all hydrated entities in the PK-indexed identity map
506+
foreach ($hydrated as $entity) {
507+
$this->registerInIdentityMap($entity, $hydrated[$entity]);
508+
}
509+
492510
$hydrated->rewind();
493511

494512
return $hydrated->current();

tests/MapperTest.php

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,6 +1242,116 @@ public function testFilteredPersistInsertsOnlyFilteredColumns(): void
12421242
$this->assertNull($row['text'], 'Non-filtered columns should not be inserted');
12431243
}
12441244

1245+
/** Identity Map: fetch() short-circuit */
1246+
public function testFetchReturnsSameInstanceOnRepeatedPkLookup(): void
1247+
{
1248+
$first = $this->mapper->post(5)->fetch();
1249+
$second = $this->mapper->post(5)->fetch();
1250+
1251+
$this->assertSame($first, $second);
1252+
}
1253+
1254+
public function testFetchWithExtraBypassesIdentityMap(): void
1255+
{
1256+
$first = $this->mapper->post(5)->fetch();
1257+
$extra = new Sql();
1258+
$extra->orderBy('post.id');
1259+
$second = $this->mapper->post(5)->fetch($extra);
1260+
1261+
$this->assertNotSame($first, $second);
1262+
$this->assertEquals($first->id, $second->id);
1263+
}
1264+
1265+
public function testIdentityMapCountIncreasesOnFetch(): void
1266+
{
1267+
$this->assertSame(0, $this->mapper->identityMapCount());
1268+
1269+
$this->mapper->author(1)->fetch();
1270+
1271+
$this->assertGreaterThan(0, $this->mapper->identityMapCount());
1272+
}
1273+
1274+
public function testClearIdentityMapForcesFreshFetch(): void
1275+
{
1276+
$first = $this->mapper->post(5)->fetch();
1277+
$this->mapper->clearIdentityMap();
1278+
$second = $this->mapper->post(5)->fetch();
1279+
1280+
$this->assertNotSame($first, $second);
1281+
$this->assertEquals($first->id, $second->id);
1282+
}
1283+
1284+
/** Identity Map: flushSingle() insert/update/delete */
1285+
public function testInsertedEntityIsRetrievableFromIdentityMap(): void
1286+
{
1287+
$entity = new stdClass();
1288+
$entity->id = null;
1289+
$entity->title = 'New Post';
1290+
$entity->text = 'New Text';
1291+
$entity->author_id = 1;
1292+
1293+
$this->mapper->post->persist($entity);
1294+
$this->mapper->flush();
1295+
1296+
// The entity should now have an auto-assigned id and be cached
1297+
$this->assertNotNull($entity->id);
1298+
1299+
$fetched = $this->mapper->post($entity->id)->fetch();
1300+
$this->assertSame($entity, $fetched);
1301+
}
1302+
1303+
public function testUpdatedEntityKeepsReturningUpdatedInstance(): void
1304+
{
1305+
$entity = $this->mapper->post(5)->fetch();
1306+
$entity->title = 'Updated Title';
1307+
1308+
$this->mapper->post->persist($entity);
1309+
$this->mapper->flush();
1310+
1311+
$fetched = $this->mapper->post(5)->fetch();
1312+
$this->assertSame($entity, $fetched);
1313+
$this->assertSame('Updated Title', $fetched->title);
1314+
}
1315+
1316+
public function testDeletedEntityIsEvictedFromIdentityMap(): void
1317+
{
1318+
$entity = $this->mapper->post(5)->fetch();
1319+
$this->assertSame($entity, $this->mapper->post(5)->fetch());
1320+
1321+
$this->mapper->post->remove($entity);
1322+
$this->mapper->flush();
1323+
1324+
// After delete, fetch should hit DB (and return false since the row is gone)
1325+
$result = $this->mapper->post(5)->fetch();
1326+
$this->assertFalse($result);
1327+
}
1328+
1329+
/** Identity Map: parseHydrated() registers related entities */
1330+
public function testRelatedEntityFromJoinReturnsSameInstanceOnDirectFetch(): void
1331+
{
1332+
// Fetch a comment with its related post via join
1333+
$comment = $this->mapper->comment(7)->post->fetch();
1334+
1335+
// The related post entity should have been registered in the identity map
1336+
$post = $this->mapper->post(5)->fetch();
1337+
$this->assertSame($comment->post_id, $post->id);
1338+
1339+
// They should be the same object instance since parseHydrated()
1340+
// registers all entities (including nested ones) in the identity map
1341+
$this->assertSame($post, $this->mapper->post($post->id)->fetch());
1342+
}
1343+
1344+
public function testNestedRelatedEntitiesAllRegisteredInIdentityMap(): void
1345+
{
1346+
$this->mapper->comment(7)->post->author->fetch();
1347+
1348+
$postFromMap = $this->mapper->post(5)->fetch();
1349+
$authorFromMap = $this->mapper->author(1)->fetch();
1350+
1351+
$this->assertSame($postFromMap, $this->mapper->post(5)->fetch());
1352+
$this->assertSame($authorFromMap, $this->mapper->author(1)->fetch());
1353+
}
1354+
12451355
private function query(string $sql): PDOStatement
12461356
{
12471357
$stmt = $this->conn->query($sql);

0 commit comments

Comments
 (0)