Skip to content

Commit 82b4660

Browse files
authored
Merge pull request #1969 from magnetik/conditional-update
Implement ConditionalUpdate interface for selective entity updates
2 parents 7f4c410 + 2f56aa7 commit 82b4660

File tree

5 files changed

+351
-2
lines changed

5 files changed

+351
-2
lines changed

src/Doctrine/ConditionalUpdate.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSElasticaBundle package.
5+
*
6+
* (c) FriendsOfSymfony <https://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\ElasticaBundle\Doctrine;
13+
14+
interface ConditionalUpdate
15+
{
16+
/**
17+
* Determines if an entity should be updated in Elasticsearch.
18+
*/
19+
public function shouldBeUpdated(): bool;
20+
}

src/Doctrine/Listener.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ public function postPersist(LifecycleEventArgs $eventArgs)
9696
$entity = $eventArgs->getObject();
9797

9898
if ($this->objectPersister->handlesObject($entity) && $this->isObjectIndexable($entity)) {
99-
$this->scheduledForInsertion[] = $entity;
99+
if (!$entity instanceof ConditionalUpdate || $entity->shouldBeUpdated()) {
100+
$this->scheduledForInsertion[] = $entity;
101+
}
100102
}
101103
}
102104

@@ -109,7 +111,9 @@ public function postUpdate(LifecycleEventArgs $eventArgs)
109111

110112
if ($this->objectPersister->handlesObject($entity)) {
111113
if ($this->isObjectIndexable($entity)) {
112-
$this->scheduledForUpdate[] = $entity;
114+
if (!$entity instanceof ConditionalUpdate || $entity->shouldBeUpdated()) {
115+
$this->scheduledForUpdate[] = $entity;
116+
}
113117
} else {
114118
// Delete if no longer indexable
115119
$this->scheduleForDeletion($entity);

tests/Unit/Doctrine/AbstractListenerTestCase.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ public function getId()
3535
}
3636
}
3737

38+
class ConditionalUpdateEntity extends Entity
39+
{
40+
private $shouldBeUpdated;
41+
42+
public function __construct($id, $shouldBeUpdated)
43+
{
44+
parent::__construct($id);
45+
$this->shouldBeUpdated = $shouldBeUpdated;
46+
}
47+
48+
public function shouldBeUpdated(): bool
49+
{
50+
return $this->shouldBeUpdated;
51+
}
52+
}
53+
3854
/**
3955
* See concrete MongoDB/ORM instances of this abstract test.
4056
*
@@ -254,6 +270,90 @@ public function testShouldPersistOnKernelTerminateIfDeferIsTrue()
254270
$listener->onTerminate();
255271
}
256272

273+
public function testConditionalUpdateObjectInsertedOnPersistWhenShouldBeUpdatedIsTrue()
274+
{
275+
$entity = new ConditionalUpdateEntity(1, true);
276+
$persister = $this->getMockPersister($entity, 'index');
277+
$eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager());
278+
$indexable = $this->getMockIndexable('index', $entity, true);
279+
280+
$listener = $this->createListener($persister, $indexable, ['indexName' => 'index']);
281+
$listener->postPersist($eventArgs);
282+
283+
$this->assertSame($entity, \current($listener->scheduledForInsertion));
284+
285+
$persister->expects($this->once())
286+
->method('insertMany')
287+
->with($listener->scheduledForInsertion)
288+
;
289+
290+
$listener->postFlush($eventArgs);
291+
}
292+
293+
public function testConditionalUpdateObjectNotInsertedOnPersistWhenShouldBeUpdatedIsFalse()
294+
{
295+
$entity = new ConditionalUpdateEntity(1, false);
296+
$persister = $this->getMockPersister($entity, 'index');
297+
$eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager());
298+
$indexable = $this->getMockIndexable('index', $entity, true);
299+
300+
$listener = $this->createListener($persister, $indexable, ['indexName' => 'index']);
301+
$listener->postPersist($eventArgs);
302+
303+
$this->assertEmpty($listener->scheduledForInsertion);
304+
305+
$persister->expects($this->never())
306+
->method('insertMany')
307+
;
308+
309+
$listener->postFlush($eventArgs);
310+
}
311+
312+
public function testConditionalUpdateObjectReplacedOnUpdateWhenShouldBeUpdatedIsTrue()
313+
{
314+
$entity = new ConditionalUpdateEntity(1, true);
315+
$persister = $this->getMockPersister($entity, 'index');
316+
$eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager());
317+
$indexable = $this->getMockIndexable('index', $entity, true);
318+
319+
$listener = $this->createListener($persister, $indexable, ['indexName' => 'index']);
320+
$listener->postUpdate($eventArgs);
321+
322+
$this->assertSame($entity, \current($listener->scheduledForUpdate));
323+
324+
$persister->expects($this->once())
325+
->method('replaceMany')
326+
->with([$entity])
327+
;
328+
$persister->expects($this->never())
329+
->method('deleteById')
330+
;
331+
332+
$listener->postFlush($eventArgs);
333+
}
334+
335+
public function testConditionalUpdateObjectNotReplacedOnUpdateWhenShouldBeUpdatedIsFalse()
336+
{
337+
$entity = new ConditionalUpdateEntity(1, false);
338+
$persister = $this->getMockPersister($entity, 'index');
339+
$eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager());
340+
$indexable = $this->getMockIndexable('index', $entity, true);
341+
342+
$listener = $this->createListener($persister, $indexable, ['indexName' => 'index']);
343+
$listener->postUpdate($eventArgs);
344+
345+
$this->assertEmpty($listener->scheduledForUpdate);
346+
347+
$persister->expects($this->never())
348+
->method('replaceMany')
349+
;
350+
$persister->expects($this->never())
351+
->method('deleteById')
352+
;
353+
354+
$listener->postFlush($eventArgs);
355+
}
356+
257357
abstract protected function getLifecycleEventArgsClass();
258358

259359
abstract protected function getListenerClass();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSElasticaBundle package.
5+
*
6+
* (c) FriendsOfSymfony <https://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\ElasticaBundle\Tests\Unit\Doctrine;
13+
14+
use FOS\ElasticaBundle\Doctrine\ConditionalUpdate;
15+
16+
class ConditionalUpdateEntity implements ConditionalUpdate
17+
{
18+
public $identifier;
19+
private $id;
20+
private $shouldBeUpdated = true;
21+
22+
public function __construct($id, $shouldBeUpdated = true)
23+
{
24+
$this->id = $id;
25+
$this->shouldBeUpdated = $shouldBeUpdated;
26+
}
27+
28+
public function getId()
29+
{
30+
return $this->id;
31+
}
32+
33+
public function shouldBeUpdated(): bool
34+
{
35+
return $this->shouldBeUpdated;
36+
}
37+
38+
public function setShouldBeUpdated(bool $shouldBeUpdated): void
39+
{
40+
$this->shouldBeUpdated = $shouldBeUpdated;
41+
}
42+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSElasticaBundle package.
5+
*
6+
* (c) FriendsOfSymfony <https://friendsofsymfony.github.com/>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace FOS\ElasticaBundle\Tests\Unit\Doctrine;
13+
14+
use Doctrine\Persistence\Event\LifecycleEventArgs;
15+
use FOS\ElasticaBundle\Doctrine\ConditionalUpdate;
16+
use FOS\ElasticaBundle\Doctrine\Listener;
17+
use FOS\ElasticaBundle\Persister\ObjectPersisterInterface;
18+
use FOS\ElasticaBundle\Provider\IndexableInterface;
19+
use PHPUnit\Framework\TestCase;
20+
21+
/**
22+
* @internal
23+
*/
24+
class ConditionalUpdateListenerTest extends TestCase
25+
{
26+
public function testEntityWithConditionalUpdateTrueIsIndexed()
27+
{
28+
$entity = $this->createMock(ConditionalUpdate::class);
29+
$entity->expects($this->once())
30+
->method('shouldBeUpdated')
31+
->willReturn(true)
32+
;
33+
34+
$persister = $this->createMock(ObjectPersisterInterface::class);
35+
$persister->expects($this->once())
36+
->method('handlesObject')
37+
->with($entity)
38+
->willReturn(true)
39+
;
40+
41+
$indexable = $this->createMock(IndexableInterface::class);
42+
$indexable->expects($this->once())
43+
->method('isObjectIndexable')
44+
->with('index_name', $entity)
45+
->willReturn(true)
46+
;
47+
48+
$eventArgs = $this->createMock(LifecycleEventArgs::class);
49+
$eventArgs->expects($this->once())
50+
->method('getObject')
51+
->willReturn($entity)
52+
;
53+
54+
$listener = new Listener($persister, $indexable, ['indexName' => 'index_name']);
55+
56+
$listener->postPersist($eventArgs);
57+
58+
$this->assertContains($entity, $listener->scheduledForInsertion);
59+
}
60+
61+
public function testEntityWithConditionalUpdateFalseIsNotIndexed()
62+
{
63+
// Create a mock entity implementing ConditionalUpdate that returns false
64+
$entity = $this->createMock(ConditionalUpdate::class);
65+
$entity->expects($this->once())
66+
->method('shouldBeUpdated')
67+
->willReturn(false)
68+
;
69+
70+
// Mock dependencies
71+
$persister = $this->createMock(ObjectPersisterInterface::class);
72+
$persister->expects($this->once())
73+
->method('handlesObject')
74+
->with($entity)
75+
->willReturn(true)
76+
;
77+
78+
$indexable = $this->createMock(IndexableInterface::class);
79+
$indexable->expects($this->once())
80+
->method('isObjectIndexable')
81+
->with('index_name', $entity)
82+
->willReturn(true)
83+
;
84+
85+
// Create the event args
86+
$eventArgs = $this->createMock(LifecycleEventArgs::class);
87+
$eventArgs->expects($this->once())
88+
->method('getObject')
89+
->willReturn($entity)
90+
;
91+
92+
// Create listener
93+
$listener = new Listener($persister, $indexable, ['indexName' => 'index_name']);
94+
95+
// Test postPersist
96+
$listener->postPersist($eventArgs);
97+
98+
// Check if entity is NOT in scheduledForInsertion
99+
$this->assertEmpty($listener->scheduledForInsertion);
100+
}
101+
102+
public function testEntityWithConditionalUpdateTrueIsUpdated()
103+
{
104+
// Create a mock entity implementing ConditionalUpdate that returns true
105+
$entity = $this->createMock(ConditionalUpdate::class);
106+
$entity->expects($this->once())
107+
->method('shouldBeUpdated')
108+
->willReturn(true)
109+
;
110+
111+
// Mock dependencies
112+
$persister = $this->createMock(ObjectPersisterInterface::class);
113+
$persister->expects($this->once())
114+
->method('handlesObject')
115+
->with($entity)
116+
->willReturn(true)
117+
;
118+
119+
$indexable = $this->createMock(IndexableInterface::class);
120+
$indexable->expects($this->once())
121+
->method('isObjectIndexable')
122+
->with('index_name', $entity)
123+
->willReturn(true)
124+
;
125+
126+
// Create the event args
127+
$eventArgs = $this->createMock(LifecycleEventArgs::class);
128+
$eventArgs->expects($this->once())
129+
->method('getObject')
130+
->willReturn($entity)
131+
;
132+
133+
// Create listener
134+
$listener = new Listener($persister, $indexable, ['indexName' => 'index_name']);
135+
136+
// Test postUpdate
137+
$listener->postUpdate($eventArgs);
138+
139+
// Check if entity is in scheduledForUpdate
140+
$this->assertContains($entity, $listener->scheduledForUpdate);
141+
}
142+
143+
public function testEntityWithConditionalUpdateFalseIsNotUpdated()
144+
{
145+
// Create a mock entity implementing ConditionalUpdate that returns false
146+
$entity = $this->createMock(ConditionalUpdate::class);
147+
$entity->expects($this->once())
148+
->method('shouldBeUpdated')
149+
->willReturn(false)
150+
;
151+
152+
// Mock dependencies
153+
$persister = $this->createMock(ObjectPersisterInterface::class);
154+
$persister->expects($this->once())
155+
->method('handlesObject')
156+
->with($entity)
157+
->willReturn(true)
158+
;
159+
160+
$indexable = $this->createMock(IndexableInterface::class);
161+
$indexable->expects($this->once())
162+
->method('isObjectIndexable')
163+
->with('index_name', $entity)
164+
->willReturn(true)
165+
;
166+
167+
// Create the event args
168+
$eventArgs = $this->createMock(LifecycleEventArgs::class);
169+
$eventArgs->expects($this->once())
170+
->method('getObject')
171+
->willReturn($entity)
172+
;
173+
174+
// Create listener
175+
$listener = new Listener($persister, $indexable, ['indexName' => 'index_name']);
176+
177+
// Test postUpdate
178+
$listener->postUpdate($eventArgs);
179+
180+
// Check if entity is NOT in scheduledForUpdate
181+
$this->assertEmpty($listener->scheduledForUpdate);
182+
}
183+
}

0 commit comments

Comments
 (0)