diff --git a/src/Doctrine/ConditionalUpdate.php b/src/Doctrine/ConditionalUpdate.php new file mode 100644 index 000000000..a6c3b5aab --- /dev/null +++ b/src/Doctrine/ConditionalUpdate.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Doctrine; + +interface ConditionalUpdate +{ + /** + * Determines if an entity should be updated in Elasticsearch. + */ + public function shouldBeUpdated(): bool; +} diff --git a/src/Doctrine/Listener.php b/src/Doctrine/Listener.php index 5e1819629..07fc11f8c 100644 --- a/src/Doctrine/Listener.php +++ b/src/Doctrine/Listener.php @@ -96,7 +96,9 @@ public function postPersist(LifecycleEventArgs $eventArgs) $entity = $eventArgs->getObject(); if ($this->objectPersister->handlesObject($entity) && $this->isObjectIndexable($entity)) { - $this->scheduledForInsertion[] = $entity; + if (!$entity instanceof ConditionalUpdate || $entity->shouldBeUpdated()) { + $this->scheduledForInsertion[] = $entity; + } } } @@ -109,7 +111,9 @@ public function postUpdate(LifecycleEventArgs $eventArgs) if ($this->objectPersister->handlesObject($entity)) { if ($this->isObjectIndexable($entity)) { - $this->scheduledForUpdate[] = $entity; + if (!$entity instanceof ConditionalUpdate || $entity->shouldBeUpdated()) { + $this->scheduledForUpdate[] = $entity; + } } else { // Delete if no longer indexable $this->scheduleForDeletion($entity); diff --git a/tests/Unit/Doctrine/AbstractListenerTestCase.php b/tests/Unit/Doctrine/AbstractListenerTestCase.php index 0cd9f31c6..8fe70eb25 100644 --- a/tests/Unit/Doctrine/AbstractListenerTestCase.php +++ b/tests/Unit/Doctrine/AbstractListenerTestCase.php @@ -35,6 +35,22 @@ public function getId() } } +class ConditionalUpdateEntity extends Entity +{ + private $shouldBeUpdated; + + public function __construct($id, $shouldBeUpdated) + { + parent::__construct($id); + $this->shouldBeUpdated = $shouldBeUpdated; + } + + public function shouldBeUpdated(): bool + { + return $this->shouldBeUpdated; + } +} + /** * See concrete MongoDB/ORM instances of this abstract test. * @@ -254,6 +270,90 @@ public function testShouldPersistOnKernelTerminateIfDeferIsTrue() $listener->onTerminate(); } + public function testConditionalUpdateObjectInsertedOnPersistWhenShouldBeUpdatedIsTrue() + { + $entity = new ConditionalUpdateEntity(1, true); + $persister = $this->getMockPersister($entity, 'index'); + $eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager()); + $indexable = $this->getMockIndexable('index', $entity, true); + + $listener = $this->createListener($persister, $indexable, ['indexName' => 'index']); + $listener->postPersist($eventArgs); + + $this->assertSame($entity, \current($listener->scheduledForInsertion)); + + $persister->expects($this->once()) + ->method('insertMany') + ->with($listener->scheduledForInsertion) + ; + + $listener->postFlush($eventArgs); + } + + public function testConditionalUpdateObjectNotInsertedOnPersistWhenShouldBeUpdatedIsFalse() + { + $entity = new ConditionalUpdateEntity(1, false); + $persister = $this->getMockPersister($entity, 'index'); + $eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager()); + $indexable = $this->getMockIndexable('index', $entity, true); + + $listener = $this->createListener($persister, $indexable, ['indexName' => 'index']); + $listener->postPersist($eventArgs); + + $this->assertEmpty($listener->scheduledForInsertion); + + $persister->expects($this->never()) + ->method('insertMany') + ; + + $listener->postFlush($eventArgs); + } + + public function testConditionalUpdateObjectReplacedOnUpdateWhenShouldBeUpdatedIsTrue() + { + $entity = new ConditionalUpdateEntity(1, true); + $persister = $this->getMockPersister($entity, 'index'); + $eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager()); + $indexable = $this->getMockIndexable('index', $entity, true); + + $listener = $this->createListener($persister, $indexable, ['indexName' => 'index']); + $listener->postUpdate($eventArgs); + + $this->assertSame($entity, \current($listener->scheduledForUpdate)); + + $persister->expects($this->once()) + ->method('replaceMany') + ->with([$entity]) + ; + $persister->expects($this->never()) + ->method('deleteById') + ; + + $listener->postFlush($eventArgs); + } + + public function testConditionalUpdateObjectNotReplacedOnUpdateWhenShouldBeUpdatedIsFalse() + { + $entity = new ConditionalUpdateEntity(1, false); + $persister = $this->getMockPersister($entity, 'index'); + $eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager()); + $indexable = $this->getMockIndexable('index', $entity, true); + + $listener = $this->createListener($persister, $indexable, ['indexName' => 'index']); + $listener->postUpdate($eventArgs); + + $this->assertEmpty($listener->scheduledForUpdate); + + $persister->expects($this->never()) + ->method('replaceMany') + ; + $persister->expects($this->never()) + ->method('deleteById') + ; + + $listener->postFlush($eventArgs); + } + abstract protected function getLifecycleEventArgsClass(); abstract protected function getListenerClass(); diff --git a/tests/Unit/Doctrine/ConditionalUpdateEntity.php b/tests/Unit/Doctrine/ConditionalUpdateEntity.php new file mode 100644 index 000000000..d8e1f8d21 --- /dev/null +++ b/tests/Unit/Doctrine/ConditionalUpdateEntity.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Tests\Unit\Doctrine; + +use FOS\ElasticaBundle\Doctrine\ConditionalUpdate; + +class ConditionalUpdateEntity implements ConditionalUpdate +{ + public $identifier; + private $id; + private $shouldBeUpdated = true; + + public function __construct($id, $shouldBeUpdated = true) + { + $this->id = $id; + $this->shouldBeUpdated = $shouldBeUpdated; + } + + public function getId() + { + return $this->id; + } + + public function shouldBeUpdated(): bool + { + return $this->shouldBeUpdated; + } + + public function setShouldBeUpdated(bool $shouldBeUpdated): void + { + $this->shouldBeUpdated = $shouldBeUpdated; + } +} diff --git a/tests/Unit/Doctrine/ConditionalUpdateListenerTest.php b/tests/Unit/Doctrine/ConditionalUpdateListenerTest.php new file mode 100644 index 000000000..22f351b26 --- /dev/null +++ b/tests/Unit/Doctrine/ConditionalUpdateListenerTest.php @@ -0,0 +1,183 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Tests\Unit\Doctrine; + +use Doctrine\Persistence\Event\LifecycleEventArgs; +use FOS\ElasticaBundle\Doctrine\ConditionalUpdate; +use FOS\ElasticaBundle\Doctrine\Listener; +use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; +use FOS\ElasticaBundle\Provider\IndexableInterface; +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +class ConditionalUpdateListenerTest extends TestCase +{ + public function testEntityWithConditionalUpdateTrueIsIndexed() + { + $entity = $this->createMock(ConditionalUpdate::class); + $entity->expects($this->once()) + ->method('shouldBeUpdated') + ->willReturn(true) + ; + + $persister = $this->createMock(ObjectPersisterInterface::class); + $persister->expects($this->once()) + ->method('handlesObject') + ->with($entity) + ->willReturn(true) + ; + + $indexable = $this->createMock(IndexableInterface::class); + $indexable->expects($this->once()) + ->method('isObjectIndexable') + ->with('index_name', $entity) + ->willReturn(true) + ; + + $eventArgs = $this->createMock(LifecycleEventArgs::class); + $eventArgs->expects($this->once()) + ->method('getObject') + ->willReturn($entity) + ; + + $listener = new Listener($persister, $indexable, ['indexName' => 'index_name']); + + $listener->postPersist($eventArgs); + + $this->assertContains($entity, $listener->scheduledForInsertion); + } + + public function testEntityWithConditionalUpdateFalseIsNotIndexed() + { + // Create a mock entity implementing ConditionalUpdate that returns false + $entity = $this->createMock(ConditionalUpdate::class); + $entity->expects($this->once()) + ->method('shouldBeUpdated') + ->willReturn(false) + ; + + // Mock dependencies + $persister = $this->createMock(ObjectPersisterInterface::class); + $persister->expects($this->once()) + ->method('handlesObject') + ->with($entity) + ->willReturn(true) + ; + + $indexable = $this->createMock(IndexableInterface::class); + $indexable->expects($this->once()) + ->method('isObjectIndexable') + ->with('index_name', $entity) + ->willReturn(true) + ; + + // Create the event args + $eventArgs = $this->createMock(LifecycleEventArgs::class); + $eventArgs->expects($this->once()) + ->method('getObject') + ->willReturn($entity) + ; + + // Create listener + $listener = new Listener($persister, $indexable, ['indexName' => 'index_name']); + + // Test postPersist + $listener->postPersist($eventArgs); + + // Check if entity is NOT in scheduledForInsertion + $this->assertEmpty($listener->scheduledForInsertion); + } + + public function testEntityWithConditionalUpdateTrueIsUpdated() + { + // Create a mock entity implementing ConditionalUpdate that returns true + $entity = $this->createMock(ConditionalUpdate::class); + $entity->expects($this->once()) + ->method('shouldBeUpdated') + ->willReturn(true) + ; + + // Mock dependencies + $persister = $this->createMock(ObjectPersisterInterface::class); + $persister->expects($this->once()) + ->method('handlesObject') + ->with($entity) + ->willReturn(true) + ; + + $indexable = $this->createMock(IndexableInterface::class); + $indexable->expects($this->once()) + ->method('isObjectIndexable') + ->with('index_name', $entity) + ->willReturn(true) + ; + + // Create the event args + $eventArgs = $this->createMock(LifecycleEventArgs::class); + $eventArgs->expects($this->once()) + ->method('getObject') + ->willReturn($entity) + ; + + // Create listener + $listener = new Listener($persister, $indexable, ['indexName' => 'index_name']); + + // Test postUpdate + $listener->postUpdate($eventArgs); + + // Check if entity is in scheduledForUpdate + $this->assertContains($entity, $listener->scheduledForUpdate); + } + + public function testEntityWithConditionalUpdateFalseIsNotUpdated() + { + // Create a mock entity implementing ConditionalUpdate that returns false + $entity = $this->createMock(ConditionalUpdate::class); + $entity->expects($this->once()) + ->method('shouldBeUpdated') + ->willReturn(false) + ; + + // Mock dependencies + $persister = $this->createMock(ObjectPersisterInterface::class); + $persister->expects($this->once()) + ->method('handlesObject') + ->with($entity) + ->willReturn(true) + ; + + $indexable = $this->createMock(IndexableInterface::class); + $indexable->expects($this->once()) + ->method('isObjectIndexable') + ->with('index_name', $entity) + ->willReturn(true) + ; + + // Create the event args + $eventArgs = $this->createMock(LifecycleEventArgs::class); + $eventArgs->expects($this->once()) + ->method('getObject') + ->willReturn($entity) + ; + + // Create listener + $listener = new Listener($persister, $indexable, ['indexName' => 'index_name']); + + // Test postUpdate + $listener->postUpdate($eventArgs); + + // Check if entity is NOT in scheduledForUpdate + $this->assertEmpty($listener->scheduledForUpdate); + } +}