Skip to content

Commit def0841

Browse files
committed
add clear method to EntityManager and tests for IdentityMap and ChangeSet
1 parent 2738f25 commit def0841

File tree

4 files changed

+601
-0
lines changed

4 files changed

+601
-0
lines changed

doc/17_entity_manager.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,66 @@ $this->entityManager->remove($video);
405405

406406
---
407407

408+
### clear() - Clear Entity Manager State
409+
410+
Clears all cached entities from the Identity Map and resets ChangeSet tracking.
411+
412+
#### Method Signature
413+
414+
```php
415+
public function clear(): void
416+
```
417+
418+
#### Example
419+
420+
```php
421+
// After processing many entities in a long-running process
422+
foreach ($largeDataSet as $item) {
423+
$video = $this->entityManager->find(Video::class, new ElasticId($item->id));
424+
// Process video...
425+
}
426+
427+
// Clear to free memory and reset state
428+
$this->entityManager->clear();
429+
430+
// Now all entities will be freshly loaded from ElasticSearch
431+
$video = $this->entityManager->find(Video::class, new ElasticId('abc123'));
432+
// This is a new instance, not from cache
433+
```
434+
435+
#### How It Works
436+
437+
1. **Clears Identity Map** - All cached entity instances are removed
438+
2. **Clears ChangeSet** - All entity tracking is reset
439+
440+
#### Use Cases
441+
442+
- **Long-running processes** - CLI commands, queue workers, batch imports where memory accumulates
443+
- **Force fresh loads** - When you need to reload entities from database without cached state
444+
- **Testing** - Reset state between test cases
445+
- **Batch operations** - Clear between batches to prevent memory issues
446+
447+
#### Important Notes
448+
449+
- Does **not** affect persisted data in ElasticSearch
450+
- Does **not** dispatch any events
451+
- After clear, the same entity ID will return a **new object instance**
452+
- Any in-memory changes to entities that weren't persisted will be lost
453+
454+
```php
455+
// Example: Same ID, different instances after clear
456+
$video1 = $this->entityManager->find(Video::class, $id);
457+
$video2 = $this->entityManager->find(Video::class, $id);
458+
$video1 === $video2; // true (same instance from Identity Map)
459+
460+
$this->entityManager->clear();
461+
462+
$video3 = $this->entityManager->find(Video::class, $id);
463+
$video1 === $video3; // false (new instance after clear)
464+
```
465+
466+
---
467+
408468
## Identity Map Integration
409469

410470
EntityManager automatically manages the Identity Map to ensure:
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SpameriTests\Elastic\EntityManager;
4+
5+
require_once __DIR__ . '/../../../bootstrap.php';
6+
7+
/**
8+
* Tests for EntityManager clear() method.
9+
*
10+
* @testCase
11+
*/
12+
class ClearTest extends \SpameriTests\Elastic\AbstractTestCase
13+
{
14+
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
// Delete any existing indexes
20+
/** @var \Spameri\Elastic\ClientProvider $clientProvider */
21+
$clientProvider = $this->container->getByType(\Spameri\Elastic\ClientProvider::class);
22+
try {
23+
$clientProvider->client()->indices()->delete(['index' => \SpameriTests\Elastic\Config::INDEX_TITLE . '*']);
24+
} catch (\Throwable $e) {
25+
// Ignore if index doesn't exist
26+
}
27+
try {
28+
$clientProvider->client()->indices()->delete(['index' => \SpameriTests\Elastic\Config::INDEX_IMAGE . '*']);
29+
} catch (\Throwable $e) {
30+
// Ignore if index doesn't exist
31+
}
32+
33+
\usleep(100000);
34+
35+
/** @var \Spameri\Elastic\Model\Indices\Create $create */
36+
$create = $this->container->getByType(\Spameri\Elastic\Model\Indices\Create::class);
37+
$create->execute(\SpameriTests\Elastic\Config::INDEX_TITLE, []);
38+
$create->execute(\SpameriTests\Elastic\Config::INDEX_IMAGE, []);
39+
40+
// Wait for index to be ready
41+
\usleep(100000);
42+
}
43+
44+
45+
public function testClearRemovesEntitiesFromIdentityMap(): void
46+
{
47+
/** @var \Spameri\Elastic\EntityManager $entityManager */
48+
$entityManager = $this->container->getByType(\Spameri\Elastic\EntityManager::class);
49+
50+
/** @var \Spameri\Elastic\Model\IdentityMap $identityMap */
51+
$identityMap = $this->container->getByType(\Spameri\Elastic\Model\IdentityMap::class);
52+
53+
$entity = new \SpameriTests\Elastic\Data\Entity\Title(
54+
new \Spameri\Elastic\Entity\Property\EmptyElasticId(),
55+
null,
56+
);
57+
58+
$id = $entityManager->persist($entity);
59+
60+
// Wait for ES to index
61+
\usleep(100000);
62+
63+
// Load entity to populate identity map
64+
$loadedEntity = $entityManager->find($id, \SpameriTests\Elastic\Data\Entity\Title::class);
65+
66+
// Verify entity is in identity map
67+
\Tester\Assert::notNull($identityMap->get(\SpameriTests\Elastic\Data\Entity\Title::class, $id));
68+
69+
// Clear entity manager
70+
$entityManager->clear();
71+
72+
// Entity should be removed from identity map
73+
\Tester\Assert::null($identityMap->get(\SpameriTests\Elastic\Data\Entity\Title::class, $id));
74+
}
75+
76+
77+
public function testClearRemovesChangeSetTracking(): void
78+
{
79+
/** @var \Spameri\Elastic\EntityManager $entityManager */
80+
$entityManager = $this->container->getByType(\Spameri\Elastic\EntityManager::class);
81+
82+
/** @var \Spameri\Elastic\Model\ChangeSet $changeSet */
83+
$changeSet = $this->container->getByType(\Spameri\Elastic\Model\ChangeSet::class);
84+
85+
$entity = new \SpameriTests\Elastic\Data\Entity\Title(
86+
new \Spameri\Elastic\Entity\Property\EmptyElasticId(),
87+
null,
88+
);
89+
90+
$id = $entityManager->persist($entity);
91+
92+
// Wait for ES to index
93+
\usleep(100000);
94+
95+
// Load entity - this should mark it as existing in ChangeSet
96+
$loadedEntity = $entityManager->find($id, \SpameriTests\Elastic\Data\Entity\Title::class);
97+
98+
// Verify entity is tracked as existing
99+
\Tester\Assert::true($changeSet->isExisting($loadedEntity));
100+
101+
// Clear entity manager
102+
$entityManager->clear();
103+
104+
// Entity should no longer be tracked as existing
105+
\Tester\Assert::false($changeSet->isExisting($loadedEntity));
106+
}
107+
108+
109+
public function testClearAllowsFreshEntityLoad(): void
110+
{
111+
/** @var \Spameri\Elastic\EntityManager $entityManager */
112+
$entityManager = $this->container->getByType(\Spameri\Elastic\EntityManager::class);
113+
114+
$entity = new \SpameriTests\Elastic\Data\Entity\Title(
115+
new \Spameri\Elastic\Entity\Property\EmptyElasticId(),
116+
null,
117+
);
118+
119+
$id = $entityManager->persist($entity);
120+
121+
// Wait for ES to index
122+
\usleep(100000);
123+
124+
// Load entity first time
125+
$firstLoad = $entityManager->find($id, \SpameriTests\Elastic\Data\Entity\Title::class);
126+
127+
// Load entity second time (should be same instance from identity map)
128+
$secondLoad = $entityManager->find($id, \SpameriTests\Elastic\Data\Entity\Title::class);
129+
\Tester\Assert::same($firstLoad, $secondLoad);
130+
131+
// Clear entity manager
132+
$entityManager->clear();
133+
134+
// Load entity third time (should be new instance after clear)
135+
$thirdLoad = $entityManager->find($id, \SpameriTests\Elastic\Data\Entity\Title::class);
136+
\Tester\Assert::notSame($firstLoad, $thirdLoad);
137+
\Tester\Assert::notSame($secondLoad, $thirdLoad);
138+
}
139+
140+
141+
public function testClearDoesNotAffectPersistedData(): void
142+
{
143+
/** @var \Spameri\Elastic\EntityManager $entityManager */
144+
$entityManager = $this->container->getByType(\Spameri\Elastic\EntityManager::class);
145+
146+
$entity = new \SpameriTests\Elastic\Data\Entity\Title(
147+
new \Spameri\Elastic\Entity\Property\EmptyElasticId(),
148+
null,
149+
);
150+
151+
$id = $entityManager->persist($entity);
152+
153+
// Wait for ES to index
154+
\usleep(100000);
155+
156+
// Clear entity manager
157+
$entityManager->clear();
158+
159+
// Data should still be retrievable from ElasticSearch
160+
$loadedEntity = $entityManager->find($id, \SpameriTests\Elastic\Data\Entity\Title::class);
161+
\Tester\Assert::notNull($loadedEntity);
162+
\Tester\Assert::same($id, $loadedEntity->id()->value());
163+
}
164+
165+
166+
public function testClearOnEmptyEntityManagerDoesNotThrow(): void
167+
{
168+
/** @var \Spameri\Elastic\EntityManager $entityManager */
169+
$entityManager = $this->container->getByType(\Spameri\Elastic\EntityManager::class);
170+
171+
// Should not throw any exceptions
172+
$entityManager->clear();
173+
174+
/** @var \Spameri\Elastic\Model\IdentityMap $identityMap */
175+
$identityMap = $this->container->getByType(\Spameri\Elastic\Model\IdentityMap::class);
176+
177+
/** @var \Spameri\Elastic\Model\ChangeSet $changeSet */
178+
$changeSet = $this->container->getByType(\Spameri\Elastic\Model\ChangeSet::class);
179+
180+
\Tester\Assert::true(empty($identityMap->identityMap));
181+
\Tester\Assert::true(empty($changeSet->created));
182+
}
183+
184+
185+
public function testClearWithMultipleEntities(): void
186+
{
187+
/** @var \Spameri\Elastic\EntityManager $entityManager */
188+
$entityManager = $this->container->getByType(\Spameri\Elastic\EntityManager::class);
189+
190+
/** @var \Spameri\Elastic\Model\IdentityMap $identityMap */
191+
$identityMap = $this->container->getByType(\Spameri\Elastic\Model\IdentityMap::class);
192+
193+
$title = new \SpameriTests\Elastic\Data\Entity\Title(
194+
new \Spameri\Elastic\Entity\Property\EmptyElasticId(),
195+
null,
196+
);
197+
$image = new \SpameriTests\Elastic\Data\Entity\Image(
198+
new \Spameri\Elastic\Entity\Property\EmptyElasticId(),
199+
null,
200+
);
201+
202+
$titleId = $entityManager->persist($title);
203+
$imageId = $entityManager->persist($image);
204+
205+
// Wait for ES to index
206+
\usleep(100000);
207+
208+
// Load entities
209+
$loadedTitle = $entityManager->find($titleId, \SpameriTests\Elastic\Data\Entity\Title::class);
210+
$loadedImage = $entityManager->find($imageId, \SpameriTests\Elastic\Data\Entity\Image::class);
211+
212+
// Verify entities are in identity map
213+
\Tester\Assert::notNull($identityMap->get(\SpameriTests\Elastic\Data\Entity\Title::class, $titleId));
214+
\Tester\Assert::notNull($identityMap->get(\SpameriTests\Elastic\Data\Entity\Image::class, $imageId));
215+
216+
// Clear
217+
$entityManager->clear();
218+
219+
// Both should be gone
220+
\Tester\Assert::null($identityMap->get(\SpameriTests\Elastic\Data\Entity\Title::class, $titleId));
221+
\Tester\Assert::null($identityMap->get(\SpameriTests\Elastic\Data\Entity\Image::class, $imageId));
222+
}
223+
224+
225+
protected function tearDown(): void
226+
{
227+
/** @var \Spameri\Elastic\Model\Indices\Delete $delete */
228+
$delete = $this->container->getByType(\Spameri\Elastic\Model\Indices\Delete::class);
229+
230+
try {
231+
$delete->execute(\SpameriTests\Elastic\Config::INDEX_TITLE);
232+
} catch (\Throwable $e) {
233+
// Ignore if index doesn't exist
234+
}
235+
236+
try {
237+
$delete->execute(\SpameriTests\Elastic\Config::INDEX_IMAGE);
238+
} catch (\Throwable $e) {
239+
// Ignore if index doesn't exist
240+
}
241+
}
242+
243+
}
244+
245+
(new ClearTest())->run();

tests/SpameriTests/Elastic/Model/ChangeSetTest.phpt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,69 @@ class ChangeSetTest extends \Tester\TestCase
130130
\Tester\Assert::true(isset($this->changeSet->created[\ArrayObject::class]));
131131
}
132132

133+
134+
public function testClearRemovesAllTrackedEntities(): void
135+
{
136+
$entity1 = new \stdClass();
137+
$entity2 = new \ArrayObject([]);
138+
$entity3 = new \DateTime();
139+
140+
$this->changeSet->markExisting($entity1);
141+
$this->changeSet->markExisting($entity2);
142+
$this->changeSet->markExisting($entity3);
143+
144+
// Verify all are tracked
145+
\Tester\Assert::true($this->changeSet->isExisting($entity1));
146+
\Tester\Assert::true($this->changeSet->isExisting($entity2));
147+
\Tester\Assert::true($this->changeSet->isExisting($entity3));
148+
149+
// Clear the change set
150+
$this->changeSet->clear();
151+
152+
// All should be gone
153+
\Tester\Assert::false($this->changeSet->isExisting($entity1));
154+
\Tester\Assert::false($this->changeSet->isExisting($entity2));
155+
\Tester\Assert::false($this->changeSet->isExisting($entity3));
156+
}
157+
158+
159+
public function testClearOnEmptyChangeSetDoesNotThrow(): void
160+
{
161+
// Should not throw any exceptions
162+
$this->changeSet->clear();
163+
164+
\Tester\Assert::true(empty($this->changeSet->created));
165+
}
166+
167+
168+
public function testClearResetsCreatedArray(): void
169+
{
170+
$entity = new \stdClass();
171+
$this->changeSet->markExisting($entity);
172+
173+
\Tester\Assert::false(empty($this->changeSet->created));
174+
175+
$this->changeSet->clear();
176+
177+
\Tester\Assert::true(empty($this->changeSet->created));
178+
}
179+
180+
181+
public function testEntitiesCanBeReMarkedAfterClear(): void
182+
{
183+
$entity = new \stdClass();
184+
185+
$this->changeSet->markExisting($entity);
186+
\Tester\Assert::true($this->changeSet->isExisting($entity));
187+
188+
$this->changeSet->clear();
189+
\Tester\Assert::false($this->changeSet->isExisting($entity));
190+
191+
// Can be marked again
192+
$this->changeSet->markExisting($entity);
193+
\Tester\Assert::true($this->changeSet->isExisting($entity));
194+
}
195+
133196
}
134197

135198
(new ChangeSetTest())->run();

0 commit comments

Comments
 (0)