├── Service ├── HeaderAwareInterface.php ├── MessageSerializerDecorator.php └── ConsumerWrapper.php ├── HappyrMq2phpBundle.php ├── Changelog.md ├── Event ├── PreHandleMessage.php └── PrePublishMessage.php ├── composer.json ├── DependencyInjection ├── Configurator │ └── MessageHeaderConfigurator.php ├── Compiler │ └── RegisterConsumers.php ├── Configuration.php └── HappyrMq2phpExtension.php ├── Resources ├── config │ └── services.yml └── bin │ └── dispatch-message.php ├── Consumer └── ExtendableEnvelopeConsumer.php ├── Command └── MessageDispatchCommand.php └── Readme.md /Service/HeaderAwareInterface.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new RegisterConsumers()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. 4 | 5 | ## 0.2.3 6 | 7 | ### Added 8 | 9 | - If no consumer is found we throw an exception to make sure we requeue the message. 10 | 11 | ## 0.2.2 12 | 13 | ### Added 14 | 15 | - A type property was added to `PrePublishMessage`. 16 | - Lots of code style changes 17 | 18 | ### Fixed 19 | 20 | - Bug when configure the Serializer 21 | 22 | ## 0.2.1 23 | 24 | ### Added 25 | 26 | * `PrePublishMessage` to allow you modify the message before we publish it. 27 | * Added lots of tests and meta files 28 | 29 | ## 0.2.0 30 | 31 | No change log before this version 32 | -------------------------------------------------------------------------------- /Event/PreHandleMessage.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PreHandleMessage extends Event 14 | { 15 | const NAME = 'happyr.mq2php.pre_handle_message'; 16 | 17 | /** 18 | * @var Envelope 19 | */ 20 | private $envelope; 21 | 22 | /** 23 | * @param Envelope $envelope 24 | */ 25 | public function __construct(Envelope $envelope) 26 | { 27 | $this->envelope = $envelope; 28 | } 29 | 30 | /** 31 | * @return Envelope 32 | */ 33 | public function getEnvelope() 34 | { 35 | return $this->envelope; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happyr/mq2php-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Automatically consume messages from a message queue without using a cron. Works great with SimpleBus", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Tobias Nyholm", 9 | "email": "tobias.nyholm@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.0", 14 | "psr/log": "~1.0", 15 | "simple-bus/asynchronous-bundle":"^3.0", 16 | "symfony/dependency-injection": "^3.4 || ^4.0", 17 | "symfony/http-kernel": "^3.4 || ^4.0", 18 | "symfony/framework-bundle": "^3.4 || ^4.0", 19 | "symfony/console": "^3.4 || ^4.0", 20 | "symfony/event-dispatcher": "^3.4 || ^4.0" 21 | }, 22 | "require-dev": { 23 | "matthiasnoback/symfony-dependency-injection-test": "^2.3", 24 | "simple-bus/jms-serializer-bundle-bridge": "^3.0.1", 25 | "symfony/phpunit-bridge": "^3.4 || ^4.0", 26 | "symfony/stopwatch": "^3.4 || ^4.0", 27 | "symfony/translation": "^3.4 || ^4.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { "Happyr\\Mq2phpBundle\\": "" } 31 | }, 32 | "scripts": { 33 | "test": "vendor/bin/simple-phpunit", 34 | "test-ci": "vendor/bin/simple-phpunit --coverage-text --coverage-clover=build/coverage.xml" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Event/PrePublishMessage.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class PrePublishMessage extends Event 13 | { 14 | const NAME = 'happyr.mq2php.pre_publish_message'; 15 | 16 | /** 17 | * This is the json message before we run json_encode. 18 | * 19 | * @var array 20 | */ 21 | private $message; 22 | 23 | /** 24 | * The class/type from the original message (command/event). 25 | * 26 | * @var string 27 | */ 28 | private $type; 29 | 30 | /** 31 | * @param array $message 32 | * @param string $type 33 | */ 34 | public function __construct(array $message, $type) 35 | { 36 | $this->message = $message; 37 | $this->type = $type; 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function getMessage() 44 | { 45 | return $this->message; 46 | } 47 | 48 | /** 49 | * @param array $message 50 | * 51 | * @return PrePublishMessage 52 | */ 53 | public function setMessage(array $message) 54 | { 55 | $this->message = $message; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getType() 64 | { 65 | return $this->type; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DependencyInjection/Configurator/MessageHeaderConfigurator.php: -------------------------------------------------------------------------------- 1 | kernel = $kernel; 26 | } 27 | 28 | /** 29 | * @param HeaderAwareInterface $service 30 | */ 31 | public function configure(HeaderAwareInterface $service) 32 | { 33 | //try to set default php bin 34 | if ($service->getHeader('php_bin') === null) { 35 | if (defined(PHP_BINARY)) { 36 | //since php 5.4 37 | $service->setHeader('php_bin', PHP_BINARY); 38 | } else { 39 | $service->setHeader('php_bin', PHP_BINDIR.'/php'); 40 | } 41 | } 42 | 43 | //try to set default dispatch_path 44 | if ($service->getHeader('dispatch_path') === null) { 45 | $service->setHeader( 46 | 'dispatch_path', 47 | $this->kernel->locateResource('@HappyrMq2phpBundle/Resources/bin/dispatch-message.php') 48 | ); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/RegisterConsumers.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class RegisterConsumers implements CompilerPassInterface 15 | { 16 | /** 17 | * @param ContainerBuilder $container 18 | */ 19 | public function process(ContainerBuilder $container) 20 | { 21 | $this->replaceArgumentWithReference($container, 'happyr.mq2php.extendable_command_envelope_consumer', 'simple_bus.asynchronous.command_bus'); 22 | $this->replaceArgumentWithReference($container, 'happyr.mq2php.extendable_event_envelope_consumer', 'simple_bus.asynchronous.event_bus'); 23 | } 24 | 25 | /** 26 | * @param ContainerBuilder $container 27 | * @param string $serviceId 28 | * @param string $referenceId 29 | */ 30 | private function replaceArgumentWithReference(ContainerBuilder $container, $serviceId, $referenceId) 31 | { 32 | if (!$container->hasDefinition($serviceId)) { 33 | return; 34 | } 35 | 36 | // If there is not $referenceId the $service has no use 37 | if (!$container->hasDefinition($referenceId)) { 38 | $container->removeDefinition($serviceId); 39 | 40 | return; 41 | } 42 | 43 | $container->getDefinition($serviceId)->replaceArgument(1, new Reference($referenceId)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('happyr_mq2php'); 20 | 21 | $root 22 | ->children() 23 | ->booleanNode('enabled')->defaultTrue()->end() 24 | ->scalarNode('command_queue')->defaultValue('asynchronous_commands')->end() 25 | ->scalarNode('event_queue')->defaultValue('asynchronous_events')->end() 26 | ->scalarNode('secret_key')->defaultNull()->info('The secret key is used to verify that the message is valid.')->end() 27 | ->arrayNode('message_headers')->addDefaultsIfNotSet() 28 | ->children() 29 | ->scalarNode('http_url')->defaultNull()->end() 30 | ->scalarNode('php_bin')->defaultNull()->end() 31 | ->scalarNode('dispatch_path')->defaultNull()->end() 32 | ->scalarNode('fastcgi_host')->cannotBeEmpty()->defaultValue('localhost')->end() 33 | ->scalarNode('fastcgi_port')->defaultValue(9000)->end() 34 | ->end() 35 | ->end() 36 | ->end(); 37 | 38 | return $treeBuilder; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | happyr.mq2php.message_serializer: 4 | class: Happyr\Mq2phpBundle\Service\MessageSerializerDecorator 5 | configurator: [ "@happyr.mq2php.di.message_header_configurator" , configure] 6 | arguments: ["@happyr.mq2php.message_serializer.inner", "@event_dispatcher", ~, "%happyr.mq2php.secret_key%"] 7 | decorates: 'simple_bus.asynchronous.message_serializer' 8 | 9 | happyr.mq2php.di.message_header_configurator: 10 | class: Happyr\Mq2phpBundle\DependencyInjection\Configurator\MessageHeaderConfigurator 11 | public: false 12 | arguments: ["@kernel"] 13 | 14 | happyr.mq2php.consumer_wrapper: 15 | class: Happyr\Mq2phpBundle\Service\ConsumerWrapper 16 | calls: 17 | - [setLogger, ["@?logger"]] 18 | arguments: 19 | - ~ 20 | - ~ 21 | - "@?happyr.mq2php.extendable_command_envelope_consumer" 22 | - "@?happyr.mq2php.extendable_event_envelope_consumer" 23 | 24 | happyr.mq2php.extendable_command_envelope_consumer: 25 | class: Happyr\Mq2phpBundle\Consumer\ExtendableEnvelopeConsumer 26 | public: false 27 | arguments: 28 | - '@simple_bus.asynchronous.message_serializer' 29 | - ~ 30 | - '@event_dispatcher' 31 | 32 | happyr.mq2php.extendable_event_envelope_consumer: 33 | class: Happyr\Mq2phpBundle\Consumer\ExtendableEnvelopeConsumer 34 | public: false 35 | arguments: 36 | - '@simple_bus.asynchronous.message_serializer' 37 | - ~ 38 | - '@event_dispatcher' 39 | 40 | happyr.mq2php.command.dispatch: 41 | class: Happyr\Mq2phpBundle\Command\MessageDispatchCommand 42 | arguments: ['@happyr.mq2php.consumer_wrapper', '%happyr.mq2php.secret_key%'] 43 | tags: 44 | - { name: 'console.command', command: 'happyr:mq2php:dispatch' } -------------------------------------------------------------------------------- /Consumer/ExtendableEnvelopeConsumer.php: -------------------------------------------------------------------------------- 1 | messageInEnvelopeSerializer = $messageInEnvelopeSerializer; 42 | $this->messageBus = $messageBus; 43 | $this->dispathcer = $dispathcer; 44 | } 45 | 46 | /** 47 | * @param string $serializedEnvelope 48 | */ 49 | public function consume($serializedEnvelope) 50 | { 51 | // Unserialize 52 | $envelope = $this->messageInEnvelopeSerializer->unwrapAndDeserialize($serializedEnvelope); 53 | 54 | // Tell the world 55 | $this->dispathcer->dispatch(PreHandleMessage::NAME, new PreHandleMessage($envelope)); 56 | 57 | // Handle the message 58 | $this->messageBus->handle($envelope->message()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Resources/bin/dispatch-message.php: -------------------------------------------------------------------------------- 1 | __DIR__.'/../app/', 11 | 'long' => __DIR__.'/../../../../../app/', 12 | 'vendor' => __DIR__.'/../../../../../../../app/', 13 | ); 14 | 15 | $appPath = null; 16 | $files = ['autoload.php', 'bootstrap.php.cache', 'AppKernel.php']; 17 | 18 | // Find the correct path 19 | foreach ($paths as $path) { 20 | foreach ($files as $file) { 21 | $appPath = (@include_once $path.$file); 22 | } 23 | if ($appPath !== false) { 24 | break; 25 | } 26 | } 27 | 28 | /* 29 | * Look both in $_SERVER, $_POST and $argv after some data. 30 | */ 31 | $data = null; 32 | if (isset($_SERVER['DEFERRED_DATA'])) { 33 | $data = $_SERVER['DEFERRED_DATA']; 34 | } elseif (isset($_POST['DEFERRED_DATA'])) { 35 | $data = $_POST['DEFERRED_DATA']; 36 | } elseif (isset($argv)) { 37 | // if shell 38 | if (isset($argv[1])) { 39 | $data = urldecode($argv[1]); 40 | } 41 | } 42 | 43 | if ($data === null) { 44 | trigger_error('No message data found', E_USER_WARNING); 45 | http_response_code(400); 46 | exit(1); 47 | } 48 | 49 | // Decode the message and get the data 50 | $message = json_decode($data, true); 51 | $headers = $message['headers']; 52 | $body = $message['body']; 53 | $queueName = null; 54 | $hash = null; 55 | foreach ($headers as $header){ 56 | if ($header['key'] === 'queue_name') { 57 | $queueName = $header['value']; 58 | } 59 | if ($header['key'] === 'hash') { 60 | $hash = $header['value']; 61 | } 62 | } 63 | 64 | // Prepare to call a Symfony command 65 | $input = new ArgvInput([$appPath.'console', 'happyr:mq2php:dispatch', $queueName, $body, $hash]); 66 | 67 | $kernel = new AppKernel('prod', false); 68 | $application = new Application($kernel); 69 | $application->setAutoExit(false); 70 | $exitCode = $application->run($input); 71 | 72 | if ($exitCode != 0) { 73 | trigger_error('Exception was thrown when executing the command', E_USER_WARNING); 74 | http_response_code(500); 75 | 76 | exit($exitCode); 77 | } 78 | -------------------------------------------------------------------------------- /Command/MessageDispatchCommand.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class MessageDispatchCommand extends ContainerAwareCommand 18 | { 19 | protected static $defaultName = 'happyr:mq2php:dispatch'; 20 | 21 | /** 22 | * @var ConsumerWrapper 23 | */ 24 | private $consumer; 25 | 26 | /** 27 | * @var string 28 | */ 29 | private $secretKey; 30 | 31 | /** 32 | * @param ConsumerWrapper $consumer 33 | * @param string $secretKey 34 | */ 35 | public function __construct(ConsumerWrapper $consumer, $secretKey) 36 | { 37 | $this->consumer = $consumer; 38 | $this->secretKey = $secretKey; 39 | 40 | parent::__construct(); 41 | } 42 | 43 | protected function configure() 44 | { 45 | $this 46 | ->setDescription('Dispatch a message from a queue to simple bus') 47 | ->addArgument('queue', InputArgument::REQUIRED, 'The name of the queue') 48 | ->addArgument('data', InputArgument::REQUIRED, 'A serialized event to dispatch') 49 | ->addArgument('hash', InputArgument::OPTIONAL, 'A hash that could be used to verify the message is valid') 50 | ; 51 | } 52 | 53 | protected function execute(InputInterface $input, OutputInterface $output) 54 | { 55 | $data = $input->getArgument('data'); 56 | $queueName = $input->getArgument('queue'); 57 | $hash = $input->getArgument('hash'); 58 | 59 | if (!empty($this->secretKey)) { 60 | // If we have a secret key we must validate the hash 61 | if (!hash_equals(sha1($this->secretKey.$data), $hash)) { 62 | throw new \Exception('Hash verification failed'); 63 | } 64 | } 65 | 66 | $this->consumer->consume($queueName, $data); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /DependencyInjection/HappyrMq2phpExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 24 | 25 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 26 | $loader->load('services.yml'); 27 | 28 | $this->requireBundle('SimpleBusAsynchronousBundle', $container); 29 | 30 | // Add the command and event queue names to the consumer wrapper 31 | $def = $container->getDefinition('happyr.mq2php.consumer_wrapper'); 32 | $def->replaceArgument(0, $config['command_queue']) 33 | ->replaceArgument(1, $config['event_queue']); 34 | 35 | $container->setParameter('happyr.mq2php.command_queue_name', $config['command_queue']); 36 | $container->setParameter('happyr.mq2php.event_queue_name', $config['event_queue']); 37 | 38 | $serializerId = 'happyr.mq2php.message_serializer'; 39 | if (!$config['enabled']) { 40 | $container->removeDefinition($serializerId); 41 | 42 | return; 43 | } 44 | 45 | // Add default headers to the serializer 46 | $def = $container->getDefinition($serializerId); 47 | $def->replaceArgument(2, $config['message_headers']); 48 | 49 | // Add the secret key as parameter 50 | $container->setParameter('happyr.mq2php.secret_key', $config['secret_key']); 51 | } 52 | 53 | /** 54 | * Make sure we have activated the required bundles. 55 | * 56 | * @param $bundleName 57 | * @param ContainerBuilder $container 58 | */ 59 | private function requireBundle($bundleName, ContainerBuilder $container) 60 | { 61 | $enabledBundles = $container->getParameter('kernel.bundles'); 62 | if (!isset($enabledBundles[$bundleName])) { 63 | throw new \LogicException(sprintf('You need to enable "%s" as well', $bundleName)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Service/MessageSerializerDecorator.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class MessageSerializerDecorator implements MessageInEnvelopeSerializer, HeaderAwareInterface 15 | { 16 | /** 17 | * @var MessageInEnvelopeSerializer 18 | */ 19 | private $serializer; 20 | 21 | /** 22 | * @var array 23 | */ 24 | private $headers; 25 | 26 | /** 27 | * @var string 28 | */ 29 | private $secretKey; 30 | 31 | /** 32 | * @var EventDispatcherInterface 33 | */ 34 | private $eventDispatcher; 35 | 36 | /** 37 | * @param MessageInEnvelopeSerializer $serializer 38 | * @param array $headers 39 | * @param string $secretKey 40 | * @param EventDispatcherInterface $eventDispatcher 41 | */ 42 | public function __construct( 43 | MessageInEnvelopeSerializer $serializer, 44 | EventDispatcherInterface $eventDispatcher, 45 | array $headers = [], 46 | $secretKey = null 47 | ) { 48 | $this->serializer = $serializer; 49 | $this->eventDispatcher = $eventDispatcher; 50 | $this->headers = $headers; 51 | $this->secretKey = empty($secretKey) ? '' : $secretKey; 52 | } 53 | 54 | /** 55 | * Serialize a Message by wrapping it in an Envelope and serializing the envelope. This decoration will 56 | * take the SimpleBus envelope and add it in a json message. 57 | * 58 | * {@inheritdoc} 59 | */ 60 | public function wrapAndSerialize($originalMessage) 61 | { 62 | $serializedMessage = $this->serializer->wrapAndSerialize($originalMessage); 63 | 64 | $message = []; 65 | foreach ($this->headers as $name => $value) { 66 | if (empty($value)) { 67 | continue; 68 | } 69 | 70 | $message['headers'][] = ['key' => $name, 'value' => $value]; 71 | } 72 | $message['body'] = $serializedMessage; 73 | 74 | // Add a hash where the secret key is baked in. 75 | $message['headers'][] = ['key' => 'hash', 'value' => sha1($this->secretKey.$serializedMessage)]; 76 | 77 | $event = new PrePublishMessage($message, is_object($originalMessage) ? get_class($originalMessage) : gettype($originalMessage)); 78 | $this->eventDispatcher->dispatch(PrePublishMessage::NAME, $event); 79 | 80 | return json_encode($event->getMessage()); 81 | } 82 | 83 | /** 84 | * Deserialize a Message that was wrapped in an Envelope. 85 | * 86 | * {@inheritdoc} 87 | */ 88 | public function unwrapAndDeserialize($serializedEnvelope) 89 | { 90 | return $this->serializer->unwrapAndDeserialize($serializedEnvelope); 91 | } 92 | 93 | /** 94 | * @param string $name 95 | * @param mixed $value 96 | * 97 | * @return $this 98 | */ 99 | public function setHeader($name, $value) 100 | { 101 | $this->headers[$name] = $value; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * @param string $name 108 | */ 109 | public function getHeader($name) 110 | { 111 | if (isset($this->headers[$name])) { 112 | return $this->headers[$name]; 113 | } 114 | 115 | return; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Service/ConsumerWrapper.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ConsumerWrapper implements LoggerAwareInterface 15 | { 16 | /** 17 | * @var SerializedEnvelopeConsumer 18 | */ 19 | private $commandConsumer; 20 | 21 | /** 22 | * @var SerializedEnvelopeConsumer 23 | */ 24 | private $eventConsumer; 25 | 26 | /** 27 | * @var string 28 | */ 29 | private $commandQueueName; 30 | 31 | /** 32 | * @var string 33 | */ 34 | private $eventQueueName; 35 | 36 | /** 37 | * @var LoggerInterface 38 | */ 39 | private $logger; 40 | 41 | /** 42 | * @param string $commandQueueName 43 | * @param string $eventQueueName 44 | * @param SerializedEnvelopeConsumer $commandConsumer 45 | * @param SerializedEnvelopeConsumer $eventConsumer 46 | */ 47 | public function __construct( 48 | $commandQueueName, 49 | $eventQueueName, 50 | SerializedEnvelopeConsumer $commandConsumer = null, 51 | SerializedEnvelopeConsumer $eventConsumer = null 52 | ) { 53 | $this->commandConsumer = $commandConsumer; 54 | $this->eventConsumer = $eventConsumer; 55 | $this->commandQueueName = $commandQueueName; 56 | $this->eventQueueName = $eventQueueName; 57 | } 58 | 59 | /** 60 | * @param $queueName 61 | * @param $message 62 | */ 63 | public function consume($queueName, $message) 64 | { 65 | $this->log('info', sprintf('Consuming data from queue: %s', $queueName)); 66 | 67 | if ($queueName === $this->eventQueueName) { 68 | $this->doConsume($queueName, $message, $this->eventConsumer); 69 | $this->log('info', sprintf('Data from queue %s was consumed by the event consumer', $queueName)); 70 | } elseif ($queueName === $this->commandQueueName) { 71 | $this->doConsume($queueName, $message, $this->commandConsumer); 72 | $this->log('info', sprintf('Data from queue %s was consumed by the command consumer', $queueName)); 73 | } else { 74 | $this->doConsume($queueName, $message); 75 | } 76 | } 77 | 78 | /** 79 | * @param LoggerInterface $logger 80 | */ 81 | public function setLogger(LoggerInterface $logger) 82 | { 83 | $this->logger = $logger; 84 | } 85 | 86 | /** 87 | * @param string $level 88 | * @param string $message 89 | * @param array $context 90 | */ 91 | private function log($level, $message, array $context = []) 92 | { 93 | if ($this->logger) { 94 | $this->logger->log($level, $message, $context); 95 | } 96 | } 97 | 98 | /** 99 | * Consume a message and make sure we log errors. 100 | * 101 | * @param string $queueName 102 | * @param mixed $message 103 | * @param SerializedEnvelopeConsumer $consumer 104 | * 105 | * @throws \Exception 106 | */ 107 | private function doConsume($queueName, $message, SerializedEnvelopeConsumer $consumer = null) 108 | { 109 | if ($consumer === null) { 110 | $exceptionMessage = sprintf('No consumer was found for queue named "%s"', $queueName); 111 | $this->log('alert', $exceptionMessage, ['message' => $message]); 112 | 113 | throw new \RuntimeException($exceptionMessage); 114 | } 115 | 116 | try { 117 | $consumer->consume($message); 118 | } catch (\Exception $e) { 119 | $this->log( 120 | 'error', 121 | sprintf('Tried to handle message from queue %s but failed', $queueName), 122 | [ 123 | 'exception' => $e, 124 | 'message' => $message, 125 | ] 126 | ); 127 | 128 | throw $e; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Mq2phpBundle for real asynchronous messages 2 | 3 | [![Latest Version](https://img.shields.io/github/release/Happyr/Mq2phpBundle.svg?style=flat-square)](https://github.com/Happyr/Mq2phpBundle/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 5 | [![Build Status](https://img.shields.io/travis/Happyr/Mq2phpBundle.svg?style=flat-square)](https://travis-ci.org/Happyr/Mq2phpBundle) 6 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/Happyr/Mq2phpBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/Happyr/Mq2phpBundle) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/Happyr/Mq2phpBundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/Happyr/Mq2phpBundle) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/happyr/mq2php-bundle.svg?style=flat-square)](https://packagist.org/packages/happyr/mq2php-bundle) 9 | 10 | 11 | This bundle is a bridge between [SimpleBus](http://simplebus.github.io/MessageBus/) and [mq2php](https://github.com/Happyr/mq2php). 12 | It could be used together with the [SimpleBusAsynchronousBundle](http://simplebus.github.io/AsynchronousBundle/doc/getting_started.html) 13 | to make the asynchronous messages independent of a cron job to consume the messages. Instead we utilize the 14 | power of PHP-FPM to schedule workload and resources. 15 | 16 | ## Consuming messages from the queue 17 | 18 | We do not want to run a cron command to consume messages from the queue because of two reasons. It takes a lot of 19 | computer resources to create a new thread and if we only do one task there will be a lot of overhead. The second reason 20 | is that we want to be able to do resource scheduling. With a cronjob we say "*Consume this message at the next minute no 21 | matter your current load*". Instead we would like to do something like: "*Consume this message as soon as possible*". 22 | 23 | The solution to these problems is nothing new. It is actually exact the problems PHP-FPM is solving. We just need a way to 24 | pull messages from the message queue and give those to PHP-FPM. 25 | 26 | This is where [mq2php](https://github.com/Happyr/mq2php) comes in. It is a Java application that will run in the 27 | background. Java is preferred because it is build to run for ever. (Compared with PHP that should never be running for 28 | more than 30 seconds.) 29 | 30 | ## Installation 31 | 32 | Fetch [mq2php.jar version 0.5.0](https://github.com/Happyr/mq2php/releases) or above and 33 | start the application with: 34 | ```bash 35 | java -Dexecutor=fastcgi -DmessageQueue=rabbitmq -DqueueNames=asynchronous_commands,asynchronous_events -jar mq2php.jar 36 | ``` 37 | 38 | You should consider using the init script when you using it on the production server. 39 | 40 | Install and enable this bundle 41 | 42 | ```bash 43 | composer require happyr/mq2php-bundle 44 | ``` 45 | 46 | ```php 47 | class AppKernel extends Kernel 48 | { 49 | public function registerBundles() 50 | { 51 | $bundles = array( 52 | // ... 53 | new Happyr\Mq2phpBundle\HappyrMq2phpBundle(), 54 | new SimpleBus\AsynchronousBundle\SimpleBusAsynchronousBundle(), 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ## Configuration 61 | 62 | ```yaml 63 | // config.yml 64 | 65 | old_sound_rabbit_mq: 66 | producers: 67 | asynchronous_commands: 68 | connection: default 69 | exchange_options: { name: 'asynchronous_commands', type: direct } 70 | queue_options: { name: 'asynchronous_commands' } 71 | 72 | asynchronous_events: 73 | connection: default 74 | exchange_options: { name: 'asynchronous_events', type: direct } 75 | queue_options: { name: 'asynchronous_events' } 76 | 77 | simple_bus_rabbit_mq_bundle_bridge: 78 | commands: 79 | producer_service_id: old_sound_rabbit_mq.asynchronous_commands_producer 80 | events: 81 | producer_service_id: old_sound_rabbit_mq.asynchronous_events_producer 82 | 83 | happyr_mq2php: 84 | enabled: true 85 | command_queue: 'asynchronous_commands' # The name of the RabbitMQ queue for commands 86 | event_queue: 'asynchronous_events' # The name of the RabbitMQ queue for events 87 | message_headers: 88 | fastcgi_host: localhost 89 | fastcgi_port: 9000 90 | dispatch_path: "%kernel.root_dir%/dispatch-message.php" 91 | ``` 92 | 93 | ### HTTP executor 94 | 95 | If you are not using fastcgi (eg PHP-FPM) you may use HTTP. 96 | 97 | ``` 98 | happyr_mq2php: 99 | message_headers: 100 | http_url: https://example.com/dispatch-message.php 101 | ``` 102 | 103 | ### Shell executor 104 | 105 | When debugging you may want to use the shell executor. This will require more CPU resources by mq2php since 106 | starting a new process to for each message is heavy. 107 | 108 | ``` 109 | happyr_mq2php: 110 | message_headers: 111 | dispatch_path: "%kernel.root_dir%/dispatch-message.php" 112 | ``` 113 | 114 | ## Verify the message 115 | 116 | If you want to be sure that your message is valid and from your application you should define a secret key. This 117 | key is used to hash the message and then verify the hash on the receiving end. If you are using multiple applications 118 | you should make sure they all have the same secret key. 119 | 120 | ``` 121 | happyr_mq2php: 122 | secret_key: '4e10upv918856xxp7g9c' 123 | ``` 124 | 125 | ## Reduce the number of messages 126 | 127 | In the SimpleBus documentation you may find that all events will be handled synchronous then 128 | asynchronous. That means that for every event your application dispatches there will be an async message. If 129 | you want to reduce the number of messages you may configure the SimpleBus AsyncBundle to only handle those events 130 | that are marked as asynchronous. 131 | 132 | ```yaml 133 | simple_bus_asynchronous: 134 | events: 135 | strategy: 'predefined' 136 | ``` 137 | --------------------------------------------------------------------------------