1010
1111use function array_flip ;
1212use function array_intersect_key ;
13+ use function count ;
14+ use function is_int ;
15+ use function is_scalar ;
16+ use function is_string ;
1317
1418abstract 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