vendor/friendsofsymfony/elastica-bundle/src/Doctrine/Listener.php line 183

  1. <?php
  2. /*
  3. * This file is part of the FOSElasticaBundle package.
  4. *
  5. * (c) FriendsOfSymfony <https://friendsofsymfony.github.com/>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace FOS\ElasticaBundle\Doctrine;
  11. use Doctrine\Persistence\Event\LifecycleEventArgs;
  12. use FOS\ElasticaBundle\Persister\ObjectPersister;
  13. use FOS\ElasticaBundle\Persister\ObjectPersisterInterface;
  14. use FOS\ElasticaBundle\Provider\IndexableInterface;
  15. use Psr\Log\LoggerInterface;
  16. use Symfony\Component\PropertyAccess\PropertyAccess;
  17. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  18. /**
  19. * Automatically update ElasticSearch based on changes to the Doctrine source
  20. * data. One listener is generated for each Doctrine entity / ElasticSearch type.
  21. */
  22. class Listener
  23. {
  24. /**
  25. * Objects scheduled for insertion.
  26. */
  27. public array $scheduledForInsertion = [];
  28. /**
  29. * Objects scheduled to be updated or removed.
  30. */
  31. public array $scheduledForUpdate = [];
  32. /**
  33. * IDs of objects scheduled for removal.
  34. */
  35. public array $scheduledForDeletion = [];
  36. /**
  37. * Object persister.
  38. */
  39. protected ObjectPersisterInterface $objectPersister;
  40. /**
  41. * PropertyAccessor instance.
  42. */
  43. protected PropertyAccessorInterface $propertyAccessor;
  44. /**
  45. * Configuration for the listener.
  46. */
  47. private array $config;
  48. private IndexableInterface $indexable;
  49. public function __construct(
  50. ObjectPersisterInterface $objectPersister,
  51. IndexableInterface $indexable,
  52. array $config = [],
  53. ?LoggerInterface $logger = null
  54. ) {
  55. $this->config = \array_merge([
  56. 'identifier' => 'id',
  57. 'defer' => false,
  58. ], $config);
  59. $this->indexable = $indexable;
  60. $this->objectPersister = $objectPersister;
  61. $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
  62. if ($logger && $this->objectPersister instanceof ObjectPersister) {
  63. $this->objectPersister->setLogger($logger);
  64. }
  65. }
  66. /**
  67. * Handler for the "kernel.terminate" and "console.terminate" Symfony events.
  68. * These event are subscribed to if the listener is configured to persist asynchronously.
  69. */
  70. public function onTerminate()
  71. {
  72. if ($this->config['defer']) {
  73. $this->config['defer'] = false;
  74. $this->persistScheduled();
  75. $this->config['defer'] = true;
  76. }
  77. }
  78. /**
  79. * Looks for new objects that should be indexed.
  80. */
  81. public function postPersist(LifecycleEventArgs $eventArgs)
  82. {
  83. $entity = $eventArgs->getObject();
  84. if ($this->objectPersister->handlesObject($entity) && $this->isObjectIndexable($entity)) {
  85. $this->scheduledForInsertion[] = $entity;
  86. }
  87. }
  88. /**
  89. * Looks for objects being updated that should be indexed or removed from the index.
  90. */
  91. public function postUpdate(LifecycleEventArgs $eventArgs)
  92. {
  93. $entity = $eventArgs->getObject();
  94. if ($this->objectPersister->handlesObject($entity)) {
  95. if ($this->isObjectIndexable($entity)) {
  96. $this->scheduledForUpdate[] = $entity;
  97. } else {
  98. // Delete if no longer indexable
  99. $this->scheduleForDeletion($entity);
  100. }
  101. }
  102. }
  103. /**
  104. * Delete objects preRemove instead of postRemove so that we have access to the id. Because this is called
  105. * preRemove, first check that the entity is managed by Doctrine.
  106. */
  107. public function preRemove(LifecycleEventArgs $eventArgs)
  108. {
  109. $entity = $eventArgs->getObject();
  110. if ($this->objectPersister->handlesObject($entity)) {
  111. $this->scheduleForDeletion($entity);
  112. }
  113. }
  114. /**
  115. * Iterate through scheduled actions before flushing to emulate 2.x behavior.
  116. * Note that the ElasticSearch index will fall out of sync with the source
  117. * data in the event of a crash during flush.
  118. *
  119. * This method is only called in legacy configurations of the listener.
  120. *
  121. * @deprecated This method should only be called in applications that depend
  122. * on the behaviour that entities are indexed regardless of if a
  123. * flush is successful
  124. */
  125. public function preFlush()
  126. {
  127. $this->persistScheduled();
  128. }
  129. /**
  130. * Iterating through scheduled actions *after* flushing ensures that the
  131. * ElasticSearch index will be affected only if the query is successful.
  132. */
  133. public function postFlush()
  134. {
  135. $this->persistScheduled();
  136. }
  137. /**
  138. * Determines whether or not it is okay to persist now.
  139. *
  140. * @return bool
  141. */
  142. private function shouldPersist()
  143. {
  144. return !$this->config['defer'];
  145. }
  146. /**
  147. * Persist scheduled objects to ElasticSearch
  148. * After persisting, clear the scheduled queue to prevent multiple data updates when using multiple flush calls.
  149. */
  150. private function persistScheduled()
  151. {
  152. if ($this->shouldPersist()) {
  153. if (\count($this->scheduledForInsertion)) {
  154. $this->objectPersister->insertMany($this->scheduledForInsertion);
  155. $this->scheduledForInsertion = [];
  156. }
  157. if (\count($this->scheduledForUpdate)) {
  158. $this->objectPersister->replaceMany($this->scheduledForUpdate);
  159. $this->scheduledForUpdate = [];
  160. }
  161. if (\count($this->scheduledForDeletion)) {
  162. $this->objectPersister->deleteManyByIdentifiers($this->scheduledForDeletion);
  163. $this->scheduledForDeletion = [];
  164. }
  165. }
  166. }
  167. /**
  168. * Record the specified identifier to delete. Do not need to entire object.
  169. *
  170. * @param object $object
  171. */
  172. private function scheduleForDeletion($object)
  173. {
  174. if ($identifierValue = $this->propertyAccessor->getValue($object, $this->config['identifier'])) {
  175. $this->scheduledForDeletion[] = !\is_scalar($identifierValue) ? (string) $identifierValue : $identifierValue;
  176. }
  177. }
  178. /**
  179. * Checks if the object is indexable or not.
  180. *
  181. * @param object $object
  182. *
  183. * @return bool
  184. */
  185. private function isObjectIndexable($object)
  186. {
  187. return $this->indexable->isObjectIndexable(
  188. $this->config['indexName'],
  189. $object
  190. );
  191. }
  192. }