├── .gitignore
├── EnqueueElasticaBundle.php
├── Queue
├── Commands.php
└── PopulateProcessor.php
├── Doctrine
├── Queue
│ ├── Commands.php
│ └── SyncIndexWithObjectChangeProcessor.php
└── SyncIndexWithObjectChangeListener.php
├── composer.json
├── LICENSE
├── README.md
├── Persister
├── Listener
│ └── PurgePopulateQueueListener.php
└── QueuePagerPersister.php
└── DependencyInjection
├── Configuration.php
└── EnqueueElasticaExtension.php
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | /composer.lock
3 | /composer.phar
4 | /phpunit.xml
5 | /vendor/
6 | /.idea/
7 |
--------------------------------------------------------------------------------
/EnqueueElasticaBundle.php:
--------------------------------------------------------------------------------
1 | context = $context;
15 | }
16 |
17 | public function purgePopulateQueue(PrePersistEvent $event)
18 | {
19 | $options = $event->getOptions();
20 | if (empty($options['purge_populate_queue'])) {
21 | return;
22 | }
23 | if (empty($options['populate_queue'])) {
24 | return;
25 | }
26 |
27 | if (method_exists($this->context, 'purge')) {
28 | $queue = $this->context->createQueue($options['populate_queue']);
29 |
30 | $this->context->purge($queue);
31 | }
32 |
33 | if (method_exists($this->context, 'purgeQueue')) {
34 | $queue = $this->context->createQueue($options['populate_queue']);
35 |
36 | $this->context->purgeQueue($queue);
37 | }
38 | }
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | public static function getSubscribedEvents()
44 | {
45 | return [
46 | PrePersistEvent::class => 'purgePopulateQueue',
47 | ];
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 | getRootNode();
19 | } else {
20 | // BC layer for symfony/config 4.1 and older
21 | $rootNode = $tb->root('enqueue_elastica');
22 | }
23 | $rootNode
24 | ->children()
25 | ->booleanNode('enabled')->defaultValue(true)->end()
26 | ->scalarNode('transport')->defaultValue('%enqueue.default_transport%')->cannotBeEmpty()->isRequired()->end()
27 | ->arrayNode('doctrine')
28 | ->children()
29 | ->scalarNode('driver')->defaultValue('orm')->cannotBeEmpty()
30 | ->validate()->ifNotInArray(['orm', 'mongodb'])->thenInvalid('Invalid driver')
31 | ->end()->end()
32 | ->arrayNode('queue_listeners')
33 | ->prototype('array')
34 | ->addDefaultsIfNotSet()
35 | ->children()
36 | ->booleanNode('insert')->defaultTrue()->end()
37 | ->booleanNode('update')->defaultTrue()->end()
38 | ->booleanNode('remove')->defaultTrue()->end()
39 | ->scalarNode('connection')->defaultValue('default')->cannotBeEmpty()->end()
40 | ->scalarNode('index_name')->isRequired()->cannotBeEmpty()->end()
41 | ->scalarNode('model_class')->isRequired()->cannotBeEmpty()->end()
42 | ->scalarNode('model_id')->defaultValue('id')->cannotBeEmpty()->end()
43 | ->scalarNode('repository_method')->defaultValue('find')->cannotBeEmpty()->end()
44 | ->end()
45 | ;
46 |
47 | return $tb;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Doctrine/SyncIndexWithObjectChangeListener.php:
--------------------------------------------------------------------------------
1 | context = $context;
34 | $this->modelClass = $modelClass;
35 | $this->config = $config;
36 | }
37 |
38 | public function postUpdate(LifecycleEventArgs $args)
39 | {
40 | if ($args->getObject() instanceof $this->modelClass) {
41 | $this->scheduledForUpdateIndex[] = [
42 | 'action' => SyncProcessor::UPDATE_ACTION,
43 | 'id' => $this->extractId($args->getObject())
44 | ];
45 | }
46 | }
47 |
48 | public function postPersist(LifecycleEventArgs $args)
49 | {
50 | if ($args->getObject() instanceof $this->modelClass) {
51 | $this->scheduledForUpdateIndex[] = [
52 | 'action' => SyncProcessor::INSERT_ACTION,
53 | 'id' => $this->extractId($args->getObject())
54 | ];
55 | }
56 | }
57 |
58 | public function preRemove(LifecycleEventArgs $args)
59 | {
60 | if ($args->getObject() instanceof $this->modelClass) {
61 | $this->scheduledForUpdateIndex[] = [
62 | 'action' => SyncProcessor::REMOVE_ACTION,
63 | 'id' => $this->extractId($args->getObject())
64 | ];
65 | }
66 | }
67 |
68 | public function postFlush(PostFlushEventArgs $event)
69 | {
70 | if (count($this->scheduledForUpdateIndex)) {
71 | foreach ($this->scheduledForUpdateIndex as $updateIndex) {
72 | $this->sendUpdateIndexMessage($updateIndex['action'], $updateIndex['id']);
73 | }
74 |
75 | $this->scheduledForUpdateIndex = [];
76 | }
77 | }
78 |
79 | public function getSubscribedEvents()
80 | {
81 | return [
82 | 'postPersist',
83 | 'postUpdate',
84 | 'preRemove',
85 | 'postFlush'
86 | ];
87 | }
88 |
89 | /**
90 | * @param string $action
91 | * @param $id
92 | */
93 | private function sendUpdateIndexMessage($action, $id)
94 | {
95 | $queue = $this->context->createQueue(Commands::SYNC_INDEX_WITH_OBJECT_CHANGE);
96 |
97 | $message = $this->context->createMessage(JSON::encode([
98 | 'action' => $action,
99 | 'model_class' => $this->modelClass,
100 | 'model_id' => $this->config['model_id'],
101 | 'id' => $id,
102 | 'index_name' => $this->config['index_name'],
103 | 'repository_method' => $this->config['repository_method'],
104 | ]));
105 |
106 | $this->context->createProducer()->send($queue, $message);
107 | }
108 |
109 | /**
110 | * @param $object
111 | * @return mixed
112 | * @throws \ReflectionException
113 | */
114 | private function extractId($object)
115 | {
116 | $rp = (new \ReflectionClass($this->modelClass))->getProperty($this->config['model_id']);
117 | $rp->setAccessible(true);
118 | $id = $rp->getValue($object);
119 | $rp->setAccessible(false);
120 |
121 | return $id;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Doctrine/Queue/SyncIndexWithObjectChangeProcessor.php:
--------------------------------------------------------------------------------
1 | persisterRegistry = $persisterRegistry;
32 | $this->indexable = $indexable;
33 | $this->doctrine = $doctrine;
34 | }
35 |
36 | public function process(Message $message, Context $context): Result
37 | {
38 | $data = JSON::decode($message->getBody());
39 |
40 | if (false == isset($data['action'])) {
41 | return Result::reject('The message data misses action');
42 | }
43 | if (false == isset($data['model_class'])) {
44 | return Result::reject('The message data misses model_class');
45 | }
46 | if (false == isset($data['id'])) {
47 | return Result::reject('The message data misses id');
48 | }
49 | if (false == isset($data['index_name'])) {
50 | return Result::reject('The message data misses index_name');
51 | }
52 | if (false == isset($data['repository_method'])) {
53 | return Result::reject('The message data misses repository_method');
54 | }
55 |
56 | $action = $data['action'];
57 | $modelClass = $data['model_class'];
58 | $id = $data['id'];
59 | $index = $data['index_name'];
60 | $repositoryMethod = $data['repository_method'];
61 |
62 | $repository = $this->doctrine->getManagerForClass($modelClass)->getRepository($modelClass);
63 | $persister = $this->persisterRegistry->getPersister($index);
64 |
65 | switch ($action) {
66 | case self::UPDATE_ACTION:
67 | if (false == $object = $repository->{$repositoryMethod}($id)) {
68 | $persister->deleteById($id);
69 |
70 | return Result::ack(sprintf('The object "%s" with id "%s" could not be found.', $modelClass, $id));
71 | }
72 |
73 | if ($persister->handlesObject($object)) {
74 | if ($this->indexable->isObjectIndexable($index, $object)) {
75 | $persister->replaceOne($object);
76 | } else {
77 | $persister->deleteOne($object);
78 | }
79 | }
80 |
81 | return Result::ack();
82 | case self::INSERT_ACTION:
83 | if (false == $object = $repository->{$repositoryMethod}($id)) {
84 | $persister->deleteById($id);
85 |
86 | return Result::ack(sprintf('The object "%s" with id "%s" could not be found.', $modelClass, $id));
87 | }
88 |
89 | if ($persister->handlesObject($object) && $this->indexable->isObjectIndexable($index, $object)) {
90 | $persister->insertOne($object);
91 | }
92 |
93 | return Result::ack();
94 | case self::REMOVE_ACTION:
95 | $persister->deleteById($id);
96 |
97 | return Result::ack();
98 | default:
99 | return Result::reject(sprintf('The action "%s" is not supported', $action));
100 | }
101 | }
102 |
103 | public static function getSubscribedCommand(): array
104 | {
105 | return [
106 | 'command' => Commands::SYNC_INDEX_WITH_OBJECT_CHANGE,
107 | 'queue' => Commands::SYNC_INDEX_WITH_OBJECT_CHANGE,
108 | 'prefix_queue' => false,
109 | 'exclusive' => true,
110 | ];
111 | }
112 |
113 | public static function getSubscribedQueues(): array
114 | {
115 | return [Commands::SYNC_INDEX_WITH_OBJECT_CHANGE];
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/DependencyInjection/EnqueueElasticaExtension.php:
--------------------------------------------------------------------------------
1 | processConfiguration(new Configuration(), $configs);
24 |
25 | if (!$config['enabled']) {
26 | return;
27 | }
28 |
29 | $transport = $container->getParameterBag()->resolveValue($config['transport']);
30 |
31 | $diUtils = new DiUtils(TransportFactory::MODULE, $transport);
32 | $container->setAlias('enqueue_elastica.context', $diUtils->format('context'));
33 |
34 | $container->register('enqueue_elastica.populate_processor', PopulateProcessor::class)
35 | ->addArgument(new Reference('fos_elastica.pager_provider_registry'))
36 | ->addArgument(new Reference('fos_elastica.pager_persister_registry'))
37 | ->addArgument(new Reference('fos_elastica.index_manager'))
38 |
39 | ->addTag('enqueue.command_subscriber', ['client' => $transport])
40 | ->addTag('enqueue.transport.processor', ['transport' => $transport])
41 | ;
42 |
43 | $container->register('enqueue_elastica.purge_populate_queue_listener', PurgePopulateQueueListener::class)
44 | ->addArgument(new Reference('enqueue_elastica.context'))
45 |
46 | ->addTag('kernel.event_subscriber')
47 | ;
48 |
49 | $container->register('enqueue_elastica.queue_pager_persister', QueuePagerPersister::class)
50 | ->addArgument(new Reference('enqueue_elastica.context'))
51 | ->addArgument(new Reference('fos_elastica.persister_registry'))
52 | ->addArgument(new Reference('event_dispatcher'))
53 | ->addArgument(new Reference('fos_elastica.index_manager'))
54 |
55 | ->addTag('fos_elastica.pager_persister', ['persisterName' => 'queue'])
56 | ->setPublic(true)
57 | ;
58 |
59 | if (false == empty($config['doctrine']['queue_listeners'])) {
60 | $doctrineDriver = $config['doctrine']['driver'];
61 |
62 | $container->register('enqueue_elastica.doctrine.sync_index_with_object_change_processor', SyncIndexWithObjectChangeProcessor::class)
63 | ->addArgument(new Reference($this->getManagerRegistry($doctrineDriver)))
64 | ->addArgument(new Reference('fos_elastica.persister_registry'))
65 | ->addArgument(new Reference('fos_elastica.indexable'))
66 | ->addTag('enqueue.command_subscriber', ['client' => $transport])
67 | ->addTag('enqueue.transport.processor', ['transport' => $transport])
68 | ;
69 |
70 | foreach ($config['doctrine']['queue_listeners'] as $listenerConfig) {
71 | $listenerId = sprintf(
72 | 'enqueue_elastica.doctrine_queue_listener.%s',
73 | $listenerConfig['index_name']
74 | );
75 |
76 | $container->register($listenerId, SyncIndexWithObjectChangeListener::class)
77 | ->setPublic(true)
78 | ->addArgument(new Reference('enqueue_elastica.context'))
79 | ->addArgument($listenerConfig['model_class'])
80 | ->addArgument($listenerConfig)
81 | ->addTag($this->getEventSubscriber($doctrineDriver), ['connection' => $listenerConfig['connection']])
82 | ;
83 | }
84 | }
85 | }
86 |
87 | private function getManagerRegistry(string $driver): string
88 | {
89 | switch ($driver) {
90 | case 'mongodb':
91 | return 'doctrine_mongodb';
92 | break;
93 | case 'orm':
94 | default:
95 | return 'doctrine';
96 | }
97 | }
98 |
99 | private function getEventSubscriber(string $driver): string
100 | {
101 | switch ($driver) {
102 | case 'mongodb':
103 | return 'doctrine_mongodb.odm.event_subscriber';
104 | break;
105 | case 'orm':
106 | default:
107 | return 'doctrine.event_subscriber';
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Queue/PopulateProcessor.php:
--------------------------------------------------------------------------------
1 | pagerPersisterRegistry = $pagerPersisterRegistry;
30 | $this->pagerProviderRegistry = $pagerProviderRegistry;
31 | $this->indexManager = $indexManager;
32 | }
33 |
34 | public function process(Message $message, Context $context): Result
35 | {
36 | if ($message->isRedelivered()) {
37 | $replyMessage = $this->createReplyMessage($context, $message, 0,'The message was redelivered. Chances are that something has gone wrong.');
38 |
39 | return Result::reply($replyMessage, Result::REJECT);
40 | }
41 |
42 | $objectsCount = 0;
43 |
44 | try {
45 | $data = JSON::decode($message->getBody());
46 |
47 | if (!isset($data['options'])) {
48 | return Result::reply($this->createReplyMessage($context, $message, 0,'The message is invalid. Missing options.'));
49 | }
50 | if (!isset($data['page'])) {
51 | return Result::reply($this->createReplyMessage($context, $message, 0,'The message is invalid. Missing page.'));
52 | }
53 | if (!isset($data['options']['indexName'])) {
54 | return Result::reply($this->createReplyMessage($context, $message, 0,'The message is invalid. Missing indexName option.'));
55 | }
56 |
57 | $options = $data['options'];
58 | $options['first_page'] = $data['page'];
59 | $options['last_page'] = $data['page'];
60 |
61 | if (isset($options['realIndexName'])) {
62 | $this->indexManager->getIndex($options['indexName'])->overrideName($options['realIndexName']);
63 | }
64 |
65 | $provider = $this->pagerProviderRegistry->getProvider($options['indexName']);
66 | $pager = $provider->provide($options);
67 | $pager->setMaxPerPage($options['max_per_page']);
68 | $pager->setCurrentPage($options['first_page']);
69 |
70 | $objectsCount = count($pager->getCurrentPageResults());
71 |
72 | $pagerPersister = $this->pagerPersisterRegistry->getPagerPersister(InPlacePagerPersister::NAME);
73 | $pagerPersister->insert($pager, $options);
74 |
75 | return Result::reply($this->createReplyMessage($context, $message, $objectsCount));
76 | } catch (\Throwable $e) {
77 | return Result::reply($this->createExceptionReplyMessage($context, $message, $objectsCount, $e), Result::REJECT);
78 | }
79 | }
80 |
81 | private function createExceptionReplyMessage(Context $context, Message $message, int $objectsCount, \Throwable $e): Message
82 | {
83 | $errorMessage = sprintf(
84 | 'The queue processor has failed to process the message with exception: %s: %s in file %s at line %s.',
85 | get_class($e),
86 | $e->getMessage(),
87 | $e->getFile(),
88 | $e->getLine()
89 | );
90 |
91 | return $this->createReplyMessage($context, $message, $objectsCount, $errorMessage);
92 | }
93 |
94 | private function createReplyMessage(Context $context, Message $message, int $objectsCount, string $error = null): Message
95 | {
96 | $replyMessage = $context->createMessage($message->getBody(), $message->getProperties(), $message->getHeaders());
97 | $replyMessage->setProperty('fos-populate-objects-count', $objectsCount);
98 |
99 | if ($error) {
100 | $replyMessage->setProperty('fos-populate-error', $error);
101 | }
102 |
103 | return $replyMessage;
104 | }
105 |
106 | public static function getSubscribedCommand(): array
107 | {
108 | return [
109 | 'command' => Commands::POPULATE,
110 | 'queue' => Commands::POPULATE,
111 | 'prefix_queue' => false,
112 | 'exclusive' => true,
113 | ];
114 | }
115 |
116 | public static function getSubscribedQueues(): array
117 | {
118 | return [Commands::POPULATE];
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/Persister/QueuePagerPersister.php:
--------------------------------------------------------------------------------
1 | context = $context;
44 | $this->dispatcher = $dispatcher;
45 | $this->registry = $registry;
46 | $this->indexManager = $indexManager;
47 | }
48 |
49 | /**
50 | * {@inheritdoc}
51 | */
52 | public function insert(PagerInterface $pager, array $options = array())
53 | {
54 | $pager->setMaxPerPage(empty($options['max_per_page']) ? 100 : $options['max_per_page']);
55 |
56 | $defaultOptions = [
57 | 'max_per_page' => $pager->getMaxPerPage(),
58 | 'first_page' => $pager->getCurrentPage(),
59 | 'last_page' => $pager->getNbPages(),
60 | 'populate_queue' => Commands::POPULATE,
61 | 'populate_reply_queue' => null,
62 | 'reply_receive_timeout' => 5000, // ms
63 | 'limit_overall_reply_time' => 180 // sec
64 | ];
65 | $index = $this->indexManager->getIndex($options['indexName']);
66 | if ($index->getName() !== $index->getOriginalName()) {
67 | $defaultOptions['realIndexName'] = $index->getName();
68 | }
69 |
70 | $options = array_replace($defaultOptions, $options);
71 |
72 | $pager->setCurrentPage($options['first_page']);
73 |
74 | $objectPersister = $this->registry->getPersister($options['indexName']);
75 |
76 | $event = new PrePersistEvent($pager, $objectPersister, $options);
77 | $this->dispatcher->dispatch($event);
78 | $pager = $event->getPager();
79 | $options = $event->getOptions();
80 |
81 | $queue = $this->context->createQueue($options['populate_queue']);
82 | $replyQueue = $options['populate_reply_queue'] ?
83 | $this->context->createQueue($options['populate_reply_queue']) :
84 | $this->context->createTemporaryQueue()
85 | ;
86 | $options['populate_reply_queue'] = $replyQueue->getQueueName();
87 |
88 | $producer = $this->context->createProducer();
89 |
90 | $lastPage = min($options['last_page'], $pager->getNbPages());
91 | $page = $pager->getCurrentPage();
92 | $sentCount = 0;
93 | do {
94 | $pager->setCurrentPage($page);
95 |
96 | $filteredOptions = $options;
97 | unset(
98 | $filteredOptions['first_page'],
99 | $filteredOptions['last_page'],
100 | $filteredOptions['populate_queue'],
101 | $filteredOptions['populate_reply_queue'],
102 | $filteredOptions['reply_receive_timeout'],
103 | $filteredOptions['limit_overall_reply_time']
104 | );
105 |
106 | $message = $this->context->createMessage(JSON::encode([
107 | 'options' => $filteredOptions,
108 | 'page' => $page,
109 | ]));
110 | $message->setReplyTo($replyQueue->getQueueName());
111 |
112 | // Because of https://github.com/php-enqueue/enqueue-dev/issues/907
113 | \usleep(10);
114 |
115 | $producer->send($queue, $message);
116 |
117 | $page++;
118 | $sentCount++;
119 | } while ($page <= $lastPage);
120 |
121 | $consumer = $this->context->createConsumer($replyQueue);
122 | $limitTime = microtime(true) + $options['limit_overall_reply_time'];
123 | while ($sentCount) {
124 | if ($message = $consumer->receive($options['reply_receive_timeout'])) {
125 | $sentCount--;
126 |
127 | $data = JSON::decode($message->getBody());
128 |
129 | $errorMessage = $message->getProperty('fos-populate-error', false);
130 | $objectsCount = (int) $message->getProperty('fos-populate-objects-count', false);
131 |
132 | $pager->setCurrentPage($data['page']);
133 |
134 | $event = new PostAsyncInsertObjectsEvent(
135 | $pager,
136 | $objectPersister,
137 | $objectsCount,
138 | $errorMessage,
139 | $data['options']
140 | );
141 | $this->dispatcher->dispatch($event);
142 | }
143 |
144 | if (microtime(true) > $limitTime) {
145 | throw new \LogicException(sprintf('Overall reply time (%s seconds) has been exceeded.', $options['limit_overall_reply_time']));
146 | }
147 | }
148 |
149 | $event = new PostPersistEvent($pager, $objectPersister, $options);
150 | $this->dispatcher->dispatch($event);
151 | }
152 | }
153 |
--------------------------------------------------------------------------------