Skip to content

Commit 169cf0b

Browse files
committed
Add PK identity map to AbstractMapper, consolidate $pending
Replace three SplObjectStorage mutation registries ($new, $changed, $removed) with a single $pending mapping entity/operation ('insert'|'update'|'delete'). Add identity map infrastructure to AbstractMapper so any backend can leverage PK-indexed entity caching: - registerInIdentityMap() / evictFromIdentityMap() for write operations - findInIdentityMap() for read shortcut on scalar PK lookups - clearIdentityMap() / identityMapCount() / trackedCount() for observability InMemoryMapper updated to use $pending and identity map throughout. Tests added covering fetch caching, insert/delete eviction, reset behavior, and pending operation types.
1 parent 731ed33 commit 169cf0b

3 files changed

Lines changed: 417 additions & 91 deletions

File tree

src/AbstractMapper.php

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,21 @@
1010

1111
use function array_flip;
1212
use function array_intersect_key;
13+
use function count;
14+
use function is_int;
15+
use function is_scalar;
16+
use function is_string;
1317

1418
abstract class AbstractMapper
1519
{
16-
/** @var SplObjectStorage<object, true> */
17-
protected SplObjectStorage $new;
18-
19-
/** @var SplObjectStorage<object, Collection> */
20+
/** @var SplObjectStorage<object, Collection> Maps entity → source Collection */
2021
protected SplObjectStorage $tracked;
2122

22-
/** @var SplObjectStorage<object, true> */
23-
protected SplObjectStorage $changed;
23+
/** @var SplObjectStorage<object, string> Maps entity → 'insert'|'update'|'delete' */
24+
protected SplObjectStorage $pending;
2425

25-
/** @var SplObjectStorage<object, true> */
26-
protected SplObjectStorage $removed;
26+
/** @var array<string, array<int|string, object>> PK-indexed identity map: [collectionName][pkValue] → entity */
27+
protected array $identityMap = [];
2728

2829
/** @var array<string, Collection> */
2930
private array $collections = [];
@@ -33,10 +34,8 @@ abstract class AbstractMapper
3334
public function __construct(
3435
public readonly EntityFactory $entityFactory = new EntityFactory(),
3536
) {
36-
$this->tracked = new SplObjectStorage();
37-
$this->changed = new SplObjectStorage();
38-
$this->removed = new SplObjectStorage();
39-
$this->new = new SplObjectStorage();
37+
$this->tracked = new SplObjectStorage();
38+
$this->pending = new SplObjectStorage();
4039
}
4140

4241
abstract public function flush(): void;
@@ -48,9 +47,27 @@ abstract public function fetchAll(Collection $collection, mixed $extra = null):
4847

4948
public function reset(): void
5049
{
51-
$this->changed = new SplObjectStorage();
52-
$this->removed = new SplObjectStorage();
53-
$this->new = new SplObjectStorage();
50+
$this->pending = new SplObjectStorage();
51+
}
52+
53+
public function clearIdentityMap(): void
54+
{
55+
$this->identityMap = [];
56+
}
57+
58+
public function trackedCount(): int
59+
{
60+
return count($this->tracked);
61+
}
62+
63+
public function identityMapCount(): int
64+
{
65+
$total = 0;
66+
foreach ($this->identityMap as $entries) {
67+
$total += count($entries);
68+
}
69+
70+
return $total;
5471
}
5572

5673
public function markTracked(object $entity, Collection $collection): bool
@@ -69,29 +86,29 @@ public function persist(object $object, Collection $onCollection): bool
6986
return true;
7087
}
7188

72-
$this->changed[$object] = true;
73-
7489
if ($this->isTracked($object)) {
90+
$currentOp = $this->pending[$object] ?? null;
91+
if ($currentOp !== 'insert') {
92+
$this->pending[$object] = 'update';
93+
}
94+
7595
return true;
7696
}
7797

78-
$this->new[$object] = true;
98+
$this->pending[$object] = 'insert';
7999
$this->markTracked($object, $onCollection);
80100

81101
return true;
82102
}
83103

84104
public function remove(object $object, Collection $fromCollection): bool
85105
{
86-
$this->changed[$object] = true;
87-
$this->removed[$object] = true;
106+
$this->pending[$object] = 'delete';
88107

89-
if ($this->isTracked($object)) {
90-
return true;
108+
if (!$this->isTracked($object)) {
109+
$this->markTracked($object, $fromCollection);
91110
}
92111

93-
$this->markTracked($object, $fromCollection);
94-
95112
return true;
96113
}
97114

@@ -134,6 +151,55 @@ protected function resolveHydrator(Collection $collection): Hydrator
134151
return $collection->hydrator ?? $this->defaultHydrator($collection);
135152
}
136153

154+
protected function registerInIdentityMap(object $entity, Collection $coll): void
155+
{
156+
if ($coll->name === null) {
157+
return;
158+
}
159+
160+
$pkValue = $this->entityPkValue($entity, $coll->name);
161+
if ($pkValue === null) {
162+
return;
163+
}
164+
165+
$this->identityMap[$coll->name][$pkValue] = $entity;
166+
}
167+
168+
protected function evictFromIdentityMap(object $entity, Collection $coll): void
169+
{
170+
if ($coll->name === null) {
171+
return;
172+
}
173+
174+
$pkValue = $this->entityPkValue($entity, $coll->name);
175+
if ($pkValue === null) {
176+
return;
177+
}
178+
179+
unset($this->identityMap[$coll->name][$pkValue]);
180+
}
181+
182+
protected function findInIdentityMap(Collection $collection): object|null
183+
{
184+
if ($collection->name === null || !is_scalar($collection->condition) || $collection->more) {
185+
return null;
186+
}
187+
188+
$condition = $collection->condition;
189+
if (!is_int($condition) && !is_string($condition)) {
190+
return null;
191+
}
192+
193+
return $this->identityMap[$collection->name][$condition] ?? null;
194+
}
195+
196+
private function entityPkValue(object $entity, string $collName): int|string|null
197+
{
198+
$pkValue = $this->entityFactory->get($entity, $this->style->identifier($collName));
199+
200+
return is_int($pkValue) || is_string($pkValue) ? $pkValue : null;
201+
}
202+
137203
public function __get(string $name): Collection
138204
{
139205
if (isset($this->collections[$name])) {

0 commit comments

Comments
 (0)