├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── Command ├── QueueAttrCommand.php ├── QueueCreateCommand.php ├── QueueDeleteCommand.php ├── QueueListCommand.php ├── QueuePingCommand.php ├── QueuePurgeCommand.php ├── QueueUpdateCommand.php └── QueueWorkerCommand.php ├── DependencyInjection ├── Compiler │ └── SQSQueuePass.php ├── Configuration.php └── TriTranSqsQueueExtension.php ├── README.md ├── Resources └── config │ └── services.xml ├── Service ├── BaseQueue.php ├── BaseWorker.php ├── Message.php ├── MessageCollection.php ├── QueueFactory.php ├── QueueManager.php └── Worker │ └── AbstractWorker.php ├── Tests ├── Functional │ └── Command │ │ ├── QueueAttrCommandTest.php │ │ ├── QueueCreateCommandTest.php │ │ ├── QueueDeleteCommandTest.php │ │ ├── QueueListCommandTest.php │ │ ├── QueuePingCommandTest.php │ │ ├── QueuePurgeCommandTest.php │ │ ├── QueueUpdateCommandTest.php │ │ └── QueueWorkerCommandTest.php ├── Unit │ ├── DependencyInjection │ │ ├── Compiler │ │ │ └── SQSQueuePassTest.php │ │ ├── ConfigurationTest.php │ │ └── TriTranSqsQueueExtensionTest.php │ └── Service │ │ ├── BaseQueueTest.php │ │ ├── MessageTest.php │ │ ├── QueueFactoryTest.php │ │ ├── QueueManagerTest.php │ │ └── Worker │ │ └── AbstractWorkerTest.php └── app │ ├── AppKernel.php │ ├── KernelTestCase.php │ ├── Worker │ └── BasicWorker.php │ └── config │ └── config_test.yml ├── TriTranSqsQueueBundle.php ├── composer.json └── phpunit.xml.dist /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: coverage.xml 2 | service_name: travis-ci 3 | json_path: ./ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /Tests/app/cache/ 3 | /Tests/app/logs/ 4 | /Tests/app/build/ 5 | /composer.lock 6 | /phpunit.xml 7 | /var/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: php 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | before_script: 8 | - composer install --no-interaction --prefer-dist 9 | script: 10 | - vendor/bin/phpunit --coverage-clover=coverage.xml 11 | - vendor/bin/phpcs -p --standard=PSR2 --ignore=var/,vendor/,Tests/app/,Resources/public/ ./ 12 | after_script: 13 | - travis_retry bash <(curl -s https://codecov.io/bash) 14 | -------------------------------------------------------------------------------- /Command/QueueAttrCommand.php: -------------------------------------------------------------------------------- 1 | setName('tritran:sqs_queue:attr') 28 | ->addArgument( 29 | 'url', 30 | InputArgument::REQUIRED, 31 | 'Queue Url which you want to retrieve its attributes' 32 | ) 33 | ->setDescription('Retrieve the attribute of a specified queue'); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function execute(InputInterface $input, OutputInterface $output) 40 | { 41 | $queueUrl = $input->getArgument('url'); 42 | 43 | $io = new SymfonyStyle($input, $output); 44 | $io->title(sprintf('Start getting the attributes of queue URL %s', $queueUrl)); 45 | 46 | /** @var QueueManager $queueManager */ 47 | $queueManager = $this->container->get('tritran.sqs_queue.queue_manager'); 48 | $result = $queueManager->getQueueAttributes($queueUrl); 49 | $io->table(['Attribute Name', 'Value'], array_map(function ($k, $v) { 50 | return [$k, $v]; 51 | }, array_keys($result), $result)); 52 | 53 | $io->text('Updated successfully'); 54 | $io->success('Done'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Command/QueueCreateCommand.php: -------------------------------------------------------------------------------- 1 | setName('tritran:sqs_queue:create') 29 | ->addArgument( 30 | 'name', 31 | InputArgument::REQUIRED, 32 | 'Queue ID which you want to send message' 33 | ) 34 | ->addOption( 35 | 'delay_seconds', 36 | null, 37 | InputOption::VALUE_REQUIRED, 38 | 'DelaySeconds', 39 | 0 40 | ) 41 | ->addOption( 42 | 'maximum_message_size', 43 | null, 44 | InputOption::VALUE_REQUIRED, 45 | 'MaximumMessageSize', 46 | 262144 47 | ) 48 | ->addOption( 49 | 'message_retention_period', 50 | null, 51 | InputOption::VALUE_REQUIRED, 52 | 'MessageRetentionPeriod', 53 | 345600 54 | ) 55 | ->addOption( 56 | 'receive_message_wait_time_seconds', 57 | null, 58 | InputOption::VALUE_REQUIRED, 59 | 'ReceiveMessageWaitTimeSeconds', 60 | 0 61 | ) 62 | ->addOption( 63 | 'visibility_timeout', 64 | null, 65 | InputOption::VALUE_REQUIRED, 66 | 'VisibilityTimeout', 67 | 30 68 | ) 69 | ->addOption( 70 | 'content_based_deduplication', 71 | null, 72 | InputOption::VALUE_NONE, 73 | 'ContentBasedDeduplication' 74 | ) 75 | ->setDescription('Create a queue by name and basic attributions'); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | protected function execute(InputInterface $input, OutputInterface $output) 82 | { 83 | $queueName = $input->getArgument('name'); 84 | if ($this->container->has(sprintf('tritran.sqs_queue.%s', $queueName))) { 85 | throw new \InvalidArgumentException(sprintf('Queue [%s] exists. Please use another name.', $queueName)); 86 | } 87 | 88 | $io = new SymfonyStyle($input, $output); 89 | $io->title(sprintf('Start creating a new queue which name is %s', $queueName)); 90 | 91 | /** @var QueueManager $queueManager */ 92 | $queueManager = $this->container->get('tritran.sqs_queue.queue_manager'); 93 | $queueUrl = $queueManager->createQueue($queueName, [ 94 | 'DelaySeconds' => $input->getOption('delay_seconds'), 95 | 'MaximumMessageSize' => $input->getOption('maximum_message_size'), 96 | 'MessageRetentionPeriod' => $input->getOption('message_retention_period'), 97 | 'ReceiveMessageWaitTimeSeconds' => $input->getOption('receive_message_wait_time_seconds'), 98 | 'VisibilityTimeout' => $input->getOption('visibility_timeout'), 99 | 'ContentBasedDeduplication' => $input->getOption('content_based_deduplication'), 100 | ]); 101 | 102 | $io->text(sprintf('Created successfully. New Queue URL: %s', $queueUrl)); 103 | $io->success('Done'); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Command/QueueDeleteCommand.php: -------------------------------------------------------------------------------- 1 | setName('tritran:sqs_queue:delete') 29 | ->addArgument( 30 | 'url', 31 | InputArgument::REQUIRED, 32 | 'Queue Url which you want to remove' 33 | ) 34 | ->addOption( 35 | 'force', 36 | 'f', 37 | InputOption::VALUE_NONE, 38 | 'Set this parameter to execute this command' 39 | ) 40 | ->setDescription('Delete a queue by url and all its messages'); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | protected function execute(InputInterface $input, OutputInterface $output) 47 | { 48 | $io = new SymfonyStyle($input, $output); 49 | if (!$input->getOption('force')) { 50 | $io->note('Option --force is mandatory to drop data.'); 51 | $io->warning('This action should not be used in the production environment.'); 52 | 53 | return; 54 | } 55 | 56 | $queueUrl = $input->getArgument('url'); 57 | 58 | $io->title(sprintf('Start deleting the specified queue by URL %s', $queueUrl)); 59 | 60 | /** @var QueueManager $queueManager */ 61 | $queueManager = $this->container->get('tritran.sqs_queue.queue_manager'); 62 | $queueManager->deleteQueue($queueUrl); 63 | 64 | $io->text('Deleted successfully'); 65 | $io->success('Done'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Command/QueueListCommand.php: -------------------------------------------------------------------------------- 1 | setName('tritran:sqs_queue:list') 28 | ->addOption( 29 | 'prefix', 30 | null, 31 | InputOption::VALUE_REQUIRED, 32 | 'Queues with a name that begins with the specified value are returned.', 33 | '' 34 | ) 35 | ->setDescription('Returns a list of your queues.'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function execute(InputInterface $input, OutputInterface $output) 42 | { 43 | $io = new SymfonyStyle($input, $output); 44 | $io->title('Start getting the list of existing queues in SQS'); 45 | 46 | /** @var QueueManager $queueManager */ 47 | $queueManager = $this->container->get('tritran.sqs_queue.queue_manager'); 48 | $result = $queueManager->listQueue($input->getOption('prefix')); 49 | 50 | if (empty($result)) { 51 | $io->text('You don\'t have any queue at this moment. Please go to AWS Console to create a new one.'); 52 | } else { 53 | $io->table(['Queue URL'], array_map(function ($value) { 54 | return [$value]; 55 | }, $result)); 56 | } 57 | 58 | $io->success('Done'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Command/QueuePingCommand.php: -------------------------------------------------------------------------------- 1 | setName('tritran:sqs_queue:ping') 28 | ->addArgument( 29 | 'name', 30 | InputArgument::REQUIRED, 31 | 'Queue ID which you want to send message' 32 | ) 33 | ->setDescription('Send a simply message to a queue, for DEBUG only'); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function execute(InputInterface $input, OutputInterface $output) 40 | { 41 | $queueName = $input->getArgument('name'); 42 | if (!$this->container->has(sprintf('tritran.sqs_queue.%s', $queueName))) { 43 | throw new \InvalidArgumentException(sprintf('Queue [%s] does not exist.', $queueName)); 44 | } 45 | 46 | $io = new SymfonyStyle($input, $output); 47 | $io->title(sprintf('Start sending a Hello message to SQS %s', $queueName)); 48 | 49 | /** @var BaseQueue $queue */ 50 | $queue = $this->container->get(sprintf('tritran.sqs_queue.%s', $queueName)); 51 | $messageId = $queue->ping(); 52 | 53 | $io->text(sprintf('Sent successfully. MessageID: %s', $messageId)); 54 | $io->success('Done'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Command/QueuePurgeCommand.php: -------------------------------------------------------------------------------- 1 | setName('tritran:sqs_queue:purge') 29 | ->addArgument( 30 | 'name', 31 | InputArgument::REQUIRED, 32 | 'Queue ID which you want to send message' 33 | ) 34 | ->addOption( 35 | 'force', 36 | 'f', 37 | InputOption::VALUE_NONE, 38 | 'Set this parameter to execute this command' 39 | ) 40 | ->setDescription('Deletes the messages in a queue specified by the QueueURL parameter.'); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | protected function execute(InputInterface $input, OutputInterface $output) 47 | { 48 | $io = new SymfonyStyle($input, $output); 49 | 50 | if (!$input->getOption('force')) { 51 | $io->note('Option --force is mandatory to drop data.'); 52 | $io->warning('This action should not be used in the production environment.'); 53 | 54 | return; 55 | } 56 | 57 | $queueName = $input->getArgument('name'); 58 | if (!$this->container->has(sprintf('tritran.sqs_queue.%s', $queueName))) { 59 | throw new \InvalidArgumentException(sprintf('Queue [%s] does not exist.', $queueName)); 60 | } 61 | 62 | $io->title(sprintf('Start purge all your message in SQS %s', $queueName)); 63 | 64 | /** @var BaseQueue $queue */ 65 | $queue = $this->container->get(sprintf('tritran.sqs_queue.%s', $queueName)); 66 | $queue->purge(); 67 | 68 | $io->text('All message in your specified queue were removed successfully'); 69 | $io->success('Done'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Command/QueueUpdateCommand.php: -------------------------------------------------------------------------------- 1 | setName('tritran:sqs_queue:update') 29 | ->addOption( 30 | 'force', 31 | 'f', 32 | InputOption::VALUE_NONE, 33 | 'Set this parameter to execute this command' 34 | ) 35 | ->setDescription('Update Queue attribute based on configuration'); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function execute(InputInterface $input, OutputInterface $output) 42 | { 43 | $io = new SymfonyStyle($input, $output); 44 | 45 | if (!$input->getOption('force')) { 46 | $io->note('Option --force is mandatory to update data.'); 47 | $io->warning('This action should not be used in the production environment.'); 48 | 49 | return; 50 | } 51 | 52 | if (!$this->container->hasParameter('tritran.sqs_queue.queues')) { 53 | $io->warning('Queue Configuration is missing.'); 54 | 55 | return; 56 | } 57 | 58 | /** @var QueueManager $queueManager */ 59 | $queueManager = $this->container->get('tritran.sqs_queue.queue_manager'); 60 | $awsQueues = $queueManager->listQueue(); 61 | 62 | /** @var array $localQueues */ 63 | $localQueues = $this->container->getParameter('tritran.sqs_queue.queues'); 64 | foreach ($localQueues as $queueName => $queueOption) { 65 | if (in_array($queueOption['queue_url'], $awsQueues, true)) { 66 | $io->text(sprintf('We will update %s', $queueOption['queue_url'])); 67 | 68 | /** @var BaseQueue $queue */ 69 | $queue = $this->container->get(sprintf('tritran.sqs_queue.%s', $queueName)); 70 | $queueManager->setQueueAttributes($queue->getQueueUrl(), $queue->getAttributes()); 71 | 72 | $io->table(['Attribute Name', 'Value'], array_map(function ($k, $v) { 73 | return [$k, $v]; 74 | }, array_keys($queue->getAttributes()), $queue->getAttributes())); 75 | } 76 | } 77 | 78 | $io->success('Done'); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Command/QueueWorkerCommand.php: -------------------------------------------------------------------------------- 1 | setName('tritran:sqs_queue:worker') 30 | ->addArgument('name', InputArgument::REQUIRED, 'Queue Name', null) 31 | ->addOption('messages', 'm', InputOption::VALUE_OPTIONAL, 'Messages to consume', 0) 32 | ->addOption('limit', 'l', InputOption::VALUE_OPTIONAL, 'Max messages to consume per request', 1) 33 | ->setDescription('Start a worker that will listen to a specified SQS queue'); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function execute(InputInterface $input, OutputInterface $output) 40 | { 41 | $queueName = $input->getArgument('name'); 42 | if (!$this->container->has(sprintf('tritran.sqs_queue.%s', $queueName))) { 43 | throw new \InvalidArgumentException(sprintf('Queue [%s] does not exist.', $queueName)); 44 | } 45 | $amount = $input->getOption('messages'); 46 | if ($amount < 0) { 47 | throw new \InvalidArgumentException('The -m option should be null or greater than 0'); 48 | } 49 | 50 | $limit = $input->getOption('limit'); 51 | if ($limit < 1) { 52 | throw new \InvalidArgumentException('The -l option should be null or greater than 1'); 53 | } 54 | 55 | $io = new SymfonyStyle($input, $output); 56 | $io->title(sprintf('Start listening to queue %s', $queueName)); 57 | 58 | /** @var BaseQueue $queue */ 59 | $queue = $this->container->get(sprintf('tritran.sqs_queue.%s', $queueName)); 60 | 61 | /** @var BaseWorker $worker */ 62 | $worker = $this->container->get('tritran.sqs_queue.queue_worker'); 63 | $worker->start($queue, $amount, $limit); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/SQSQueuePass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('aws.sqs')) { 26 | throw new \InvalidArgumentException( 27 | 'AWS SQSClient is required to use queue.' 28 | ); 29 | } 30 | 31 | if ($container->hasParameter('tritran.sqs_queue.queues')) { 32 | /** @var array $queues */ 33 | $queues = $container->getParameter('tritran.sqs_queue.queues'); 34 | foreach ($queues as $queueName => $queueOption) { 35 | $defaultQueueServices = ['queues', 'queue_factory', 'queue_manager', 'queue_worker']; 36 | if (in_array($queueName, $defaultQueueServices, true)) { 37 | throw new \InvalidArgumentException(sprintf( 38 | 'Invalid queue-name [%s]. Predefined queue-name: (%s)', 39 | $queueName, 40 | implode(', ', $defaultQueueServices) 41 | )); 42 | } 43 | 44 | $queueOption['worker'] = preg_replace('/^@/', '', $queueOption['worker']); 45 | if ($container->has($queueOption['worker'])) { 46 | $callable = new Reference($queueOption['worker']); 47 | } elseif (class_exists($queueOption['worker'])) { 48 | $callable = new Definition($queueOption['worker']); 49 | } else { 50 | throw new \InvalidArgumentException( 51 | sprintf('Invalid worker of queue [%s]', $queueName) 52 | ); 53 | } 54 | 55 | $queueAttr = [ 56 | 'DelaySeconds' => 57 | $queueOption['attributes']['delay_seconds'] ?? 0, 58 | 'MaximumMessageSize' => 59 | $queueOption['attributes']['maximum_message_size'] ?? 262144, 60 | 'MessageRetentionPeriod' => 61 | $queueOption['attributes']['message_retention_period'] ?? 345600, 62 | 'ReceiveMessageWaitTimeSeconds' => 63 | $queueOption['attributes']['receive_message_wait_time_seconds'] ?? 20, 64 | 'VisibilityTimeout' => 65 | $queueOption['attributes']['visibility_timeout'] ?? 30, 66 | 'RedrivePolicy' => !empty($queueOption['attributes']['redrive_policy']['dead_letter_queue']) 67 | ? json_encode([ 68 | 'deadLetterTargetArn' => 69 | $queueOption['attributes']['redrive_policy']['dead_letter_queue'] ?? '', 70 | 'maxReceiveCount' => 71 | $queueOption['attributes']['redrive_policy']['max_receive_count'] ?? 5, 72 | ]) : '' 73 | ]; 74 | if (QueueManager::isFifoQueue($queueName)) { 75 | $queueAttr = array_merge($queueAttr, [ 76 | 'ContentBasedDeduplication' => 77 | $queueOption['attributes']['content_based_deduplication'] ?? true 78 | ]); 79 | } 80 | 81 | $queueDefinition = new Definition(BaseQueue::class); 82 | $queueDefinition 83 | ->setFactory( 84 | [ 85 | new Reference('tritran.sqs_queue.queue_factory'), 86 | 'create' 87 | ] 88 | ) 89 | ->setPublic(true) 90 | ->setArguments([ 91 | $queueName, 92 | $queueOption['queue_url'], 93 | $callable, 94 | $queueAttr 95 | ]); 96 | 97 | $queueId = sprintf('tritran.sqs_queue.%s', $queueName); 98 | $container->setDefinition($queueId, $queueDefinition); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('tritran_sqs_queue'); 30 | } else { 31 | $rootNode = $treeBuilder->getRootNode(); 32 | } 33 | 34 | $rootNode 35 | ->children() 36 | ->append($this->getSQSQueueNodeDef()) 37 | ->end(); 38 | 39 | return $treeBuilder; 40 | } 41 | 42 | /** 43 | * @return ArrayNodeDefinition 44 | */ 45 | private function queuesNodeDef() 46 | { 47 | $node = new ArrayNodeDefinition('queues'); 48 | 49 | return $node 50 | ->useAttributeAsKey('name') 51 | ->prototype('array') 52 | ->children() 53 | ->append((new ScalarNodeDefinition('queue_url'))->isRequired()) 54 | ->append((new ScalarNodeDefinition('worker'))->isRequired()) 55 | ->append($this->getSQSQueueAttributesNodeDef()) 56 | ->end() 57 | ->end(); 58 | } 59 | 60 | /** 61 | * @return NodeDefinition|ParentNodeDefinitionInterface 62 | */ 63 | protected function getSQSQueueAttributesNodeDef() 64 | { 65 | $node = new ArrayNodeDefinition('attributes'); 66 | 67 | return $node 68 | ->children() 69 | ->append((new IntegerNodeDefinition('delay_seconds'))->defaultValue(0)->min(0)->max(900))// zero second 70 | ->append((new IntegerNodeDefinition('maximum_message_size'))->defaultValue(262144)->min(1024)->max(262144))// 256 KiB 71 | ->append((new IntegerNodeDefinition('message_retention_period'))->defaultValue(345600)->min(60)->max(1209600))// 4 days 72 | ->append((new IntegerNodeDefinition('receive_message_wait_time_seconds'))->defaultValue(20)->min(0)->max(20))// seconds 73 | ->append((new IntegerNodeDefinition('visibility_timeout'))->defaultValue(30)->min(0)->max(43200))// second 74 | ->append((new BooleanNodeDefinition('content_based_deduplication'))->defaultValue(false))// second 75 | ->append($this->getSQSRedrivePolicyNode()) 76 | ->end(); 77 | } 78 | 79 | /** 80 | * @return NodeDefinition|ParentNodeDefinitionInterface 81 | */ 82 | protected function getSQSRedrivePolicyNode() 83 | { 84 | $node = new ArrayNodeDefinition('redrive_policy'); 85 | 86 | return $node 87 | ->canBeUnset() 88 | ->children() 89 | ->append((new IntegerNodeDefinition('max_receive_count'))->defaultValue(5)->min(1)->max(1000)) 90 | ->append((new ScalarNodeDefinition('dead_letter_queue'))->isRequired()) 91 | ->end(); 92 | } 93 | 94 | /** 95 | * @return NodeDefinition|ParentNodeDefinitionInterface 96 | */ 97 | protected function getSQSQueueNodeDef() 98 | { 99 | $node = new ArrayNodeDefinition('sqs_queue'); 100 | 101 | return $node 102 | ->canBeUnset() 103 | ->children() 104 | ->append($this->queuesNodeDef()) 105 | ->end(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /DependencyInjection/TriTranSqsQueueExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 24 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 25 | $loader->load('services.xml'); 26 | 27 | $this->configSQSQueue($container, $config); 28 | } 29 | 30 | /** 31 | * @param ContainerBuilder $container 32 | * @param array $config 33 | */ 34 | private function configSQSQueue(ContainerBuilder $container, array $config) 35 | { 36 | $engineConfig = $config['sqs_queue'] ?? []; 37 | 38 | if (isset($engineConfig['queues']) && !empty($engineConfig['queues'])) { 39 | $container->setParameter('tritran.sqs_queue.queues', $config['sqs_queue']['queues']); 40 | } 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public function getAlias() 47 | { 48 | return 'tritran_sqs_queue'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple AWS SQS Queue for Symfony 2 | ================================ 3 | 4 | This bundle provides an easy way to work with AWS SQS 5 | 6 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/966e2515-0177-4b80-8ef0-cfc9bd800a81/mini.png)](https://insight.sensiolabs.com/projects/966e2515-0177-4b80-8ef0-cfc9bd800a81) 7 | [![Latest Stable Version](https://poser.pugx.org/tritran/sqs-queue-bundle/v/stable)](https://packagist.org/packages/tritran/sqs-queue-bundle) 8 | [![Latest Unstable Version](https://poser.pugx.org/tritran/sqs-queue-bundle/v/unstable)](https://packagist.org/packages/tritran/sqs-queue-bundle) 9 | [![Build Status](https://api.travis-ci.org/trandangtri/sqs-queue-bundle.svg?branch=master)](https://travis-ci.org/trandangtri/sqs-queue-bundle) 10 | [![codecov](https://codecov.io/gh/trandangtri/sqs-queue-bundle/branch/master/graph/badge.svg)](https://codecov.io/gh/trandangtri/sqs-queue-bundle) 11 | 12 | Installation 13 | --- 14 | 15 | Follow 5 quick steps to setup this bundle. 16 | 17 | ### Step 1: Download the Bundle 18 | 19 | Open a command console, enter your project directory and execute the following 20 | command to download the latest stable version of this bundle: 21 | 22 | ```bash 23 | $ composer require tritran/sqs-queue-bundle 24 | ``` 25 | 26 | > This command requires you to have Composer installed globally 27 | 28 | ### Step 2: Enable the Bundle 29 | 30 | Register bundles in `app/AppKernel.php`: 31 | 32 | ```php 33 | class AppKernel extends Kernel 34 | { 35 | public function registerBundles() 36 | { 37 | return [ 38 | // ... 39 | new \Aws\Symfony\AwsBundle(), 40 | new \TriTran\SqsQueueBundle\TriTranSqsQueueBundle(), 41 | ]; 42 | } 43 | 44 | // ... 45 | } 46 | ``` 47 | 48 | > In a default Symfony application that uses [Symfony Flex](https://symfony.com/doc/current/setup/flex.html), bundles are enabled/disabled automatically for you when installing/removing them, so you could ignore this step. 49 | 50 | ### Step 3: Update AWS SQS Credential 51 | 52 | This bundle is using [AWS SDK for PHP](https://github.com/aws/aws-sdk-php-symfony). Full documentation of the configuration options available can be read in the [SDK Guide](http://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/configuration.html). 53 | 54 | Below are sample configuration for AWS Credential in YAML format 55 | 56 | ```yml 57 | # app/config/config.yml 58 | 59 | aws: 60 | version: latest 61 | region: us-central-1 62 | credentials: 63 | key: not-a-real-key 64 | secret: "@not-a-real-secret" 65 | ``` 66 | 67 | ### Step 4: Configure the Queues 68 | 69 | Below are sample configuration for some queues in YAML format 70 | 71 | ```yml 72 | # app/config/config.yml 73 | 74 | tritran_sqs_queue: 75 | sqs_queue: 76 | queues: 77 | emailpool: 78 | queue_url: 'https://sqs.eu-central-1.amazonaws.com/49504XX59872/emailpool' 79 | worker: "@acl.service.emailpool" 80 | attributes: 81 | receive_message_wait_time_seconds: 20 82 | visibility_timeout: 30 83 | reminder: 84 | queue_url: 'https://sqs.eu-central-1.amazonaws.com/49504XX59872/reminder' 85 | worker: 'AclBundle\Service\Worker\ReminderWorker' 86 | ``` 87 | 88 | Full documentation of the queue options available can be read in the [Queue Attributes](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SetQueueAttributes.html). 89 | 90 | > Now, you could access to queue `emailpool` or `reminder` service via `tritran.sqs_queue.emailpool` or `tritran.sqs_queue.reminder`, it's an interface of [BaseQueue](https://github.com/trandangtri/sqs-queue-bundle/blob/master/Service/BaseQueue.php) 91 | 92 | Below are a sample implementation of sending a message to a specified queue 93 | 94 | ```php 95 | namespace AclBundle\Controller; 96 | 97 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 98 | use TriTran\SqsQueueBundle\Service\Message; 99 | 100 | /** 101 | * Class DefaultController 102 | * 103 | * @package AclBundle\Controller 104 | */ 105 | class DefaultController extends Controller 106 | { 107 | public function indexAction() 108 | { 109 | // ... 110 | 111 | $data = [ 112 | 'from' => 'sender@domain.com', 113 | 'to' => 'receiver@domain.com', 114 | 'subject' => 'Greeting Message', 115 | 'body' => 'Congratulation! You have just received a message which was sent from AWS SQS Queue' 116 | ]; 117 | $this->get('tritran.sqs_queue.emailpool') 118 | ->sendMessage((new Message())->setBody(serialize($data))); 119 | 120 | // ... 121 | } 122 | } 123 | ``` 124 | 125 | > For a FIFO queue, you must associate a non-empty `MessageGroupId` with a message. Otherwise, the action fails.
126 | > You may provide a `MessageDeduplicationId` explicitly. If you aren't able to provide a `MessageDeduplicationId` and you enable `ContentBasedDeduplication` for your queue, Amazon SQS uses a SHA-256 hash to generate the `MessageDeduplicationId` using the body of the message (but not the attributes of the message).
127 | > For more information about FIFO queue, please take a look at [Amazon SQS FIFO (First-In-First-Out) Queues](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html) 128 | 129 | #### Queue Behaviours 130 | 131 | |Behaviour|Arguments|Description| 132 | |---|---|---| 133 | |sendMessage|`Message` $message
`int` $delay = 0|Delivers a message to the specified queue.| 134 | |receiveMessage|`int` $limit = 1|Retrieves one or more messages (up to 10), from the specified queue. Using the WaitTimeSeconds parameter enables long-poll support. For more information, see [Amazon SQS Long Polling](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-long-polling.html) in the Amazon SQS Developer Guide.| 135 | |deleteMessage|`string` $receiptHandle|Deletes the specified message from the specified queue. You specify the message by using the *message's receipt handle* and not the *MessageId* you receive when you send the message. Even if the message is locked by another reader due to the visibility timeout setting, it is still deleted from the queue. If you leave a message in the queue for longer than the queue's configured retention period, Amazon SQS automatically deletes the message.| 136 | |purge| |Deletes the messages in a queue specified by the QueueURL parameter. **Note**: you can't retrieve a message deleted from a queue.| 137 | 138 | #### Queue Manager Behaviours 139 | 140 | > You could access [QueueManager](https://github.com/trandangtri/sqs-queue-bundle/blob/master/Service/QueueManager.php) via service `tritran.sqs_queue.queue_manager` 141 | 142 | |Behaviour|Arguments|Description| 143 | |---|---|---| 144 | |listQueue|`string` $prefix = ''|Returns a list of your queues. The maximum number of queues that can be returned is 1,000. If you specify a value for the optional *prefix* parameter, only queues with a name that begins with the specified value are returned.| 145 | |createQueue|`string` $queueName
`array` $queueAttribute|Creates a new standard or FIFO queue. You can pass one or more attributes in the request.| 146 | |deleteQueue|`string` $queueUrl|Deletes the queue specified by the **QueueUrl**, regardless of the queue's contents. If the specified queue doesn't exist, Amazon SQS returns a successful response.| 147 | |setQueueAttributes|`string` $queueUrl
`array` $queueAttribute|Sets the value of one or more queue attributes. When you change a queue's attributes, the change can take up to 60 seconds for most of the attributes to propagate throughout the Amazon SQS system| 148 | |getQueueAttributes|`string` $queueUrl|Gets attributes for the specified queue.| 149 | 150 | ### Step 5: Setup a worker 151 | 152 | Below are a sample implementation of a worker, which will listen to a queue to handle the messages inside. 153 | 154 | ```php 155 | namespace AclBundle\Service\Worker; 156 | 157 | use TriTran\SqsQueueBundle\Service\Message; 158 | use TriTran\SqsQueueBundle\Service\Worker\AbstractWorker; 159 | 160 | class ReminderWorker extends AbstractWorker 161 | { 162 | /** 163 | * @param Message $message 164 | * 165 | * @return boolean 166 | */ 167 | protected function execute(Message $message) 168 | { 169 | echo 'The message is: ' . $message->getBody(); 170 | 171 | return true; 172 | } 173 | } 174 | ``` 175 | 176 | And then you could make it executed as daemon in console via: 177 | 178 | ```bash 179 | bin/console tritran:sqs_queue:worker reminder 180 | ``` 181 | 182 | > Note: **reminder** is the name of queue which you configured in the config.yml in step 4. 183 | 184 | ### Appendix: Useful Console Commands 185 | 186 | |Behaviour|Description| 187 | |---|---| 188 | |tritran:sqs_queue:create|Creates a new standard or FIFO queue. You can pass one or more attributes in the request.| 189 | |tritran:sqs_queue:update|Update queue attribute based on its configuration which shown in config.yml| 190 | |tritran:sqs_queue:delete|Delete a queue by url and all its messages|| 191 | |tritran:sqs_queue:attr|Retrieve the attribute of a specified queue| 192 | |tritran:sqs_queue:purge|Deletes the messages in a queue specified by the QueueURL parameter.| 193 | |tritran:sqs_queue:worker|Start a worker that will listen to a specified SQS queue| 194 | |tritran:sqs_queue:ping|Send a simply message to a queue, for DEBUG only| 195 | 196 | > Note: Please using `-h` for more information for each command. 197 | -------------------------------------------------------------------------------- /Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Service/BaseQueue.php: -------------------------------------------------------------------------------- 1 | client = $client; 57 | $this->queueUrl = $queueUrl; 58 | $this->queueName = $queueName; 59 | $this->queueWorker = $queueWorker; 60 | $this->attributes = $attributes; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function ping() 67 | { 68 | $message = (new Message())->setBody('ping'); 69 | 70 | return $this->sendMessage($message); 71 | } 72 | 73 | /** 74 | * @param Message $message 75 | * @param int $delay 76 | * 77 | * @return string 78 | */ 79 | public function sendMessage(Message $message, int $delay = 0) 80 | { 81 | $params = [ 82 | 'QueueUrl' => $this->queueUrl, 83 | 'MessageBody' => $message->getBody(), 84 | 'MessageAttributes' => $message->getAttributes() 85 | ]; 86 | 87 | if ($this->isFIFO()) { 88 | if ($delay) { 89 | trigger_error('FIFO queues don\'t support per-message delays, only per-queue delays.', E_USER_WARNING); 90 | $delay = 0; 91 | } 92 | 93 | if (empty($message->getGroupId())) { 94 | throw new \InvalidArgumentException('MessageGroupId is required for FIFO queues.'); 95 | } 96 | $params['MessageGroupId'] = $message->getGroupId(); 97 | 98 | if (!empty($message->getDeduplicationId())) { 99 | $params['MessageDeduplicationId'] = $message->getDeduplicationId(); 100 | } 101 | } 102 | 103 | if ($delay) { 104 | $params['DelaySeconds'] = $delay; 105 | } 106 | 107 | try { 108 | $result = $this->client->sendMessage($params); 109 | $messageId = $result->get('MessageId'); 110 | } catch (AwsException $e) { 111 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 112 | } 113 | 114 | return $messageId; 115 | } 116 | 117 | /** 118 | * Retrieves one or more messages (up to 10), from the specified queue. 119 | * 120 | * @param int $limit 121 | * 122 | * @return MessageCollection|Message[] 123 | */ 124 | public function receiveMessage(int $limit = 1) 125 | { 126 | $collection = new MessageCollection([]); 127 | 128 | try { 129 | $result = $this->client->receiveMessage([ 130 | 'QueueUrl' => $this->queueUrl, 131 | 'AttributeNames' => ['All'], 132 | 'MessageAttributeNames' => ['All'], 133 | 'MaxNumberOfMessages' => $limit, 134 | 'VisibilityTimeout' => $this->attributes['VisibilityTimeout'] ?? 30, 135 | 'WaitTimeSeconds' => $this->attributes['ReceiveMessageWaitTimeSeconds'] ?? 0, 136 | ]); 137 | 138 | $messages = $result->get('Messages') ?? []; 139 | foreach ($messages as $message) { 140 | $collection->append( 141 | (new Message()) 142 | ->setId($message['MessageId']) 143 | ->setBody($message['Body']) 144 | ->setReceiptHandle($message['ReceiptHandle']) 145 | ->setAttributes($message['Attributes']) 146 | ); 147 | } 148 | } catch (AwsException $e) { 149 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 150 | } 151 | 152 | return $collection; 153 | } 154 | 155 | /** 156 | * Deletes the specified message from the specified queue 157 | * 158 | * @param Message $message 159 | * 160 | * @return bool 161 | */ 162 | public function deleteMessage(Message $message) 163 | { 164 | try { 165 | $this->client->deleteMessage([ 166 | 'QueueUrl' => $this->queueUrl, 167 | 'ReceiptHandle' => $message->getReceiptHandle() 168 | ]); 169 | 170 | return true; 171 | } catch (AwsException $e) { 172 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 173 | } 174 | } 175 | 176 | /** 177 | * Releases a message back to the queue, making it visible again 178 | * 179 | * @param Message $message 180 | * 181 | * @return bool 182 | */ 183 | public function releaseMessage(Message $message) 184 | { 185 | try { 186 | $this->client->changeMessageVisibility([ 187 | 'QueueUrl' => $this->queueUrl, 188 | 'ReceiptHandle' => $message->getReceiptHandle(), 189 | 'VisibilityTimeout' => 0 190 | ]); 191 | 192 | return true; 193 | } catch (AwsException $e) { 194 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 195 | } 196 | } 197 | 198 | /** 199 | * Deletes the messages in a queue. 200 | * When you use the this action, you can't retrieve a message deleted from a queue. 201 | * 202 | * @return bool 203 | */ 204 | public function purge() 205 | { 206 | try { 207 | $this->client->purgeQueue([ 208 | 'QueueUrl' => $this->queueUrl 209 | ]); 210 | 211 | return true; 212 | } catch (AwsException $e) { 213 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 214 | } 215 | } 216 | 217 | /** 218 | * @return string 219 | */ 220 | public function getQueueUrl(): string 221 | { 222 | return $this->queueUrl; 223 | } 224 | 225 | /** 226 | * @param string $queueUrl 227 | * 228 | * @return $this 229 | */ 230 | public function setQueueUrl(string $queueUrl) 231 | { 232 | $this->queueUrl = $queueUrl; 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * @return AbstractWorker 239 | */ 240 | public function getQueueWorker(): AbstractWorker 241 | { 242 | return $this->queueWorker; 243 | } 244 | 245 | /** 246 | * @return string 247 | */ 248 | public function getQueueName(): string 249 | { 250 | return $this->queueName; 251 | } 252 | 253 | /** 254 | * @return array 255 | */ 256 | public function getAttributes(): array 257 | { 258 | return $this->attributes; 259 | } 260 | 261 | /** 262 | * @param array $attributes 263 | * 264 | * @return $this 265 | */ 266 | public function setAttributes(array $attributes) 267 | { 268 | $this->attributes = $attributes; 269 | 270 | return $this; 271 | } 272 | 273 | /** 274 | * @return SqsClient 275 | */ 276 | public function getClient(): SqsClient 277 | { 278 | return $this->client; 279 | } 280 | 281 | /** 282 | * @return bool 283 | */ 284 | final public function isFIFO(): bool 285 | { 286 | return QueueManager::isFifoQueue($this->getQueueName()); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /Service/BaseWorker.php: -------------------------------------------------------------------------------- 1 | consumed = 0; 28 | $this->consume($queue, $amount, $limit); 29 | } 30 | 31 | /** 32 | * @param BaseQueue $queue 33 | * @param int $amount 34 | * @param int $limit 35 | */ 36 | private function consume(BaseQueue $queue, int $amount = 0, int $limit = 1) 37 | { 38 | while (true) { 39 | if ($amount && $this->consumed >= $amount) { 40 | break; 41 | } 42 | $this->fetchMessage($queue, $limit); 43 | } 44 | } 45 | 46 | /** 47 | * @param BaseQueue $queue 48 | * @param int $limit 49 | */ 50 | private function fetchMessage(BaseQueue $queue, int $limit = 1) 51 | { 52 | $consumer = $queue->getQueueWorker(); 53 | 54 | /** @var MessageCollection $result */ 55 | $messages = $queue->receiveMessage($limit); 56 | 57 | $messages->rewind(); 58 | while ($messages->valid()) { 59 | $this->consumed++; 60 | 61 | /** @var Message $message */ 62 | $message = $messages->current(); 63 | 64 | $this->logger && $this->logger->info(sprintf('Processing message ID: %s', $message->getId())); 65 | $result = $consumer->process($message); 66 | 67 | if ($result !== false) { 68 | $this->logger && $this->logger->info( 69 | sprintf('Successfully processed message ID: %s', $message->getId()) 70 | ); 71 | $queue->deleteMessage($message); 72 | } else { 73 | $this->logger && $this->logger->warning( 74 | sprintf('Cannot process message ID: %s, will release it back to queue', $message->getId()) 75 | ); 76 | $queue->releaseMessage($message); 77 | } 78 | 79 | $messages->next(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Service/Message.php: -------------------------------------------------------------------------------- 1 | body = $body; 56 | $this->attributes = $attributes; 57 | $this->groupId = $groupId; 58 | $this->deduplicationId = $deduplicationId; 59 | } 60 | 61 | /** 62 | * @return string 63 | */ 64 | public function getId(): string 65 | { 66 | return $this->id; 67 | } 68 | 69 | /** 70 | * @param string $id 71 | * 72 | * @return Message 73 | */ 74 | public function setId(string $id) 75 | { 76 | $this->id = $id; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getBody(): string 85 | { 86 | return $this->body; 87 | } 88 | 89 | /** 90 | * @param string $body 91 | * 92 | * @return Message 93 | */ 94 | public function setBody(string $body) 95 | { 96 | $this->body = $body; 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * @return array 103 | */ 104 | public function getAttributes(): array 105 | { 106 | return $this->attributes; 107 | } 108 | 109 | /** 110 | * @param array $attributes 111 | * 112 | * @return Message 113 | */ 114 | public function setAttributes(array $attributes) 115 | { 116 | $this->attributes = $attributes; 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * @return string 123 | */ 124 | public function getGroupId(): string 125 | { 126 | return $this->groupId; 127 | } 128 | 129 | /** 130 | * @param string $groupId 131 | * 132 | * @return Message 133 | */ 134 | public function setGroupId(string $groupId) 135 | { 136 | $this->groupId = $groupId; 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * @return string 143 | */ 144 | public function getDeduplicationId(): string 145 | { 146 | return $this->deduplicationId; 147 | } 148 | 149 | /** 150 | * @param string $deduplicationId 151 | * 152 | * @return Message 153 | */ 154 | public function setDeduplicationId(string $deduplicationId) 155 | { 156 | $this->deduplicationId = $deduplicationId; 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * @return string 163 | */ 164 | public function getReceiptHandle(): string 165 | { 166 | return $this->receiptHandle; 167 | } 168 | 169 | /** 170 | * @param string $receiptHandle 171 | * 172 | * @return Message 173 | */ 174 | public function setReceiptHandle(string $receiptHandle) 175 | { 176 | $this->receiptHandle = $receiptHandle; 177 | 178 | return $this; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /Service/MessageCollection.php: -------------------------------------------------------------------------------- 1 | client = $client; 32 | } 33 | 34 | /** 35 | * @param SqsClient $client 36 | * @param string $queueName 37 | * @param string $queueUrl 38 | * @param AbstractWorker $queueWorker 39 | * @param array $options 40 | * 41 | * @return BaseQueue 42 | */ 43 | public static function createQueue( 44 | SqsClient $client, 45 | string $queueName, 46 | string $queueUrl, 47 | AbstractWorker $queueWorker, 48 | array $options = [] 49 | ) { 50 | $instance = new self($client); 51 | 52 | return $instance->create($queueName, $queueUrl, $queueWorker, $options); 53 | } 54 | 55 | /** 56 | * @param string $queueName 57 | * @param string $queueUrl 58 | * @param AbstractWorker $queueWorker 59 | * @param array $options 60 | * 61 | * @return BaseQueue 62 | */ 63 | public function create( 64 | string $queueName, 65 | string $queueUrl, 66 | AbstractWorker $queueWorker, 67 | array $options = [] 68 | ) { 69 | if ($this->queues === null) { 70 | $this->queues = []; 71 | } 72 | 73 | if (isset($this->queues[$queueUrl])) { 74 | return $this->queues[$queueUrl]; 75 | } 76 | 77 | $queue = new BaseQueue($this->client, $queueName, $queueUrl, $queueWorker, $options); 78 | $this->queues[$queueUrl] = $queue; 79 | 80 | return $queue; 81 | } 82 | 83 | /** 84 | * @return SqsClient 85 | */ 86 | public function getClient(): SqsClient 87 | { 88 | return $this->client; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Service/QueueManager.php: -------------------------------------------------------------------------------- 1 | 0, 24 | 'MaximumMessageSize' => 262144, // 256 KiB 25 | 'MessageRetentionPeriod' => 345600, // 4 days 26 | 'ReceiveMessageWaitTimeSeconds' => 0, 27 | 'VisibilityTimeout' => 30, 28 | 'RedrivePolicy' => '' 29 | ]; 30 | 31 | const QUEUE_FIFO_ATTR_DEFAULT = [ 32 | 'ContentBasedDeduplication' => true 33 | ]; 34 | 35 | /** 36 | * QueueManager constructor. 37 | * 38 | * @param SqsClient $client 39 | */ 40 | public function __construct(SqsClient $client) 41 | { 42 | $this->client = $client; 43 | } 44 | 45 | /** 46 | * @param string $prefix 47 | * 48 | * @return array 49 | */ 50 | public function listQueue(string $prefix = '') 51 | { 52 | $queues = []; 53 | $attr = []; 54 | if (!empty($prefix)) { 55 | $attr['QueueNamePrefix'] = $prefix; 56 | } 57 | 58 | try { 59 | $result = $this->client->listQueues($attr); 60 | if ($result->count()) { 61 | $queues = $result->get('QueueUrls'); 62 | } 63 | } catch (AwsException $e) { 64 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 65 | } 66 | 67 | return $queues; 68 | } 69 | 70 | /** 71 | * @param string $queueName 72 | * @param array $queueAttribute 73 | * 74 | * @return string 75 | */ 76 | public function createQueue(string $queueName, array $queueAttribute = []) 77 | { 78 | $queryUrl = ''; 79 | 80 | $queueAttributeDefault = self::QUEUE_ATTR_DEFAULT; 81 | if (static::isFifoQueue($queueName)) { 82 | $queueAttributeDefault = array_merge($queueAttributeDefault, self::QUEUE_FIFO_ATTR_DEFAULT); 83 | } 84 | 85 | $queueAttribute = array_filter($queueAttribute, function ($key) use ($queueAttributeDefault) { 86 | return array_key_exists($key, $queueAttributeDefault); 87 | }, ARRAY_FILTER_USE_KEY); 88 | $attr = [ 89 | 'Attributes' => array_merge($queueAttributeDefault, $queueAttribute), 90 | 'QueueName' => $queueName 91 | ]; 92 | 93 | try { 94 | $result = $this->client->createQueue($attr); 95 | if ($result->count()) { 96 | $queryUrl = $result->get('QueueUrl'); 97 | } 98 | } catch (AwsException $e) { 99 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 100 | } 101 | 102 | return $queryUrl; 103 | } 104 | 105 | /** 106 | * @param string $queueUrl 107 | * 108 | * @return bool 109 | */ 110 | public function deleteQueue(string $queueUrl) 111 | { 112 | try { 113 | $this->client->deleteQueue(['QueueUrl' => $queueUrl]); 114 | 115 | return true; 116 | } catch (AwsException $e) { 117 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 118 | } 119 | } 120 | 121 | /** 122 | * @param string $queueUrl 123 | * @param array $queueAttribute 124 | * 125 | * @return bool 126 | */ 127 | public function setQueueAttributes(string $queueUrl, array $queueAttribute) 128 | { 129 | $queueAttribute = array_filter($queueAttribute, function ($key) { 130 | return array_key_exists($key, self::QUEUE_ATTR_DEFAULT); 131 | }, ARRAY_FILTER_USE_KEY); 132 | $attr = [ 133 | 'Attributes' => $queueAttribute, 134 | 'QueueUrl' => $queueUrl 135 | ]; 136 | 137 | try { 138 | $this->client->setQueueAttributes($attr); 139 | 140 | return true; 141 | } catch (AwsException $e) { 142 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 143 | } 144 | } 145 | 146 | /** 147 | * @param string $queueUrl 148 | * 149 | * @return array 150 | */ 151 | public function getQueueAttributes(string $queueUrl) 152 | { 153 | $attr = []; 154 | 155 | try { 156 | $result = $this->client->getQueueAttributes([ 157 | 'AttributeNames' => ['All'], 158 | 'QueueUrl' => $queueUrl 159 | ]); 160 | if ($result->count()) { 161 | $attr = $result->get('Attributes'); 162 | } 163 | } catch (AwsException $e) { 164 | throw new \InvalidArgumentException($e->getAwsErrorMessage()); 165 | } 166 | 167 | return $attr; 168 | } 169 | 170 | /** 171 | * @return SqsClient 172 | */ 173 | public function getClient(): SqsClient 174 | { 175 | return $this->client; 176 | } 177 | 178 | /** 179 | * @param $queueName 180 | * 181 | * @return bool 182 | */ 183 | public static function isFifoQueue($queueName): bool 184 | { 185 | return '.fifo' === substr($queueName, -5); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Service/Worker/AbstractWorker.php: -------------------------------------------------------------------------------- 1 | getBody() === 'ping') { 24 | echo 'Pong. Now is ' . (new \DateTime('now'))->format('M d, Y H:i:s') . PHP_EOL; 25 | 26 | return true; 27 | } 28 | 29 | $this->preExecute($message); 30 | try { 31 | $result = $this->execute($message); 32 | } catch (\Exception $e) { 33 | $result = false; 34 | $this->error = $e->getMessage(); 35 | } 36 | $this->postExecute($message); 37 | 38 | // Let worker does something on success or failure 39 | $result === true ? $this->onSucceeded() : $this->onFailed(); 40 | 41 | return $result; 42 | } 43 | 44 | /** 45 | * @param Message $message 46 | */ 47 | protected function preExecute(Message $message) 48 | { 49 | // Do something here 50 | } 51 | 52 | /** 53 | * Do something via post execution. It is better to proceed with task related to message. 54 | * 55 | * @param Message $message 56 | */ 57 | protected function postExecute(Message $message) 58 | { 59 | // Do something here 60 | } 61 | 62 | /** 63 | * @param Message $message 64 | * 65 | * @return boolean 66 | */ 67 | abstract protected function execute(Message $message); 68 | 69 | /** 70 | * Event fired when worker has processed message successfully. 71 | * 72 | * @return void 73 | */ 74 | protected function onSucceeded() 75 | { 76 | // Do something here 77 | } 78 | 79 | /** 80 | * Event fired when worker has failed to process message. 81 | * 82 | * @return void 83 | */ 84 | protected function onFailed() 85 | { 86 | // Do something here 87 | } 88 | 89 | /** 90 | * Check if worker has error after processing. 91 | * By default, error is set to null. 92 | * 93 | * @return bool 94 | */ 95 | public function hasError() 96 | { 97 | return $this->error !== null; 98 | } 99 | 100 | /** 101 | * Get worker error message. 102 | * 103 | * @return string 104 | */ 105 | public function error() 106 | { 107 | return $this->error; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Tests/Functional/Command/QueueAttrCommandTest.php: -------------------------------------------------------------------------------- 1 | queueManager = $this->getMockBuilder(QueueManager::class) 25 | ->disableOriginalConstructor() 26 | ->getMock(); 27 | $this->queueManager 28 | ->expects($this->any()) 29 | ->method('getQueueAttributes') 30 | ->with('my-queue-url') 31 | ->willReturn(['att1' => 'value1', 'att2' => 'value2']); 32 | 33 | $this->getContainer()->set('tritran.sqs_queue.queue_manager', $this->queueManager); 34 | } 35 | 36 | /** 37 | * Test: Retrieve the attribute of a specified queue. 38 | */ 39 | public function testExecute() 40 | { 41 | $commandTester = $this->createCommandTester(new QueueAttrCommand()); 42 | $commandTester->execute([ 43 | 'url' => 'my-queue-url', 44 | ]); 45 | 46 | $output = $commandTester->getDisplay(); 47 | 48 | $this->assertContains('value1', $output); 49 | $this->assertContains('value2', $output); 50 | $this->assertContains('Done', $output); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/Functional/Command/QueueCreateCommandTest.php: -------------------------------------------------------------------------------- 1 | queueManager === null) { 26 | $this->queueManager = $this->getMockBuilder(QueueManager::class) 27 | ->disableOriginalConstructor() 28 | ->getMock(); 29 | $this->queueManager 30 | ->expects($this->any()) 31 | ->method('createQueue') 32 | ->with('my-queue-name', [ 33 | 'DelaySeconds' => 1, 34 | 'MaximumMessageSize' => 1, 35 | 'MessageRetentionPeriod' => 1, 36 | 'ReceiveMessageWaitTimeSeconds' => 1, 37 | 'VisibilityTimeout' => 1, 38 | 'ContentBasedDeduplication' => true, 39 | ]) 40 | ->willReturn('new-queue-url'); 41 | 42 | $this->getContainer()->set('tritran.sqs_queue.queue_manager', $this->queueManager); 43 | } 44 | } 45 | 46 | /** 47 | * Test: Create a queue by name and basic attributions 48 | */ 49 | public function testExecute() 50 | { 51 | $commandTester = $this->createCommandTester(new QueueCreateCommand()); 52 | $commandTester->execute([ 53 | 'name' => 'my-queue-name', 54 | '--delay_seconds' => 1, 55 | '--maximum_message_size' => 1, 56 | '--message_retention_period' => 1, 57 | '--receive_message_wait_time_seconds' => 1, 58 | '--visibility_timeout' => 1, 59 | '--content_based_deduplication' => true, 60 | ]); 61 | 62 | $output = $commandTester->getDisplay(); 63 | $this->assertContains('Created successfully. New Queue URL: new-queue-url', $output); 64 | } 65 | 66 | /** 67 | * Test: Create a new queue which was configured or existed already. 68 | */ 69 | public function testExecuteWithAnExistingQueueName() 70 | { 71 | $commandTester = $this->createCommandTester(new QueueCreateCommand()); 72 | 73 | $this->expectException(\InvalidArgumentException::class); 74 | $commandTester->execute([ 75 | 'name' => 'basic_queue' // Existed 76 | ]); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Tests/Functional/Command/QueueDeleteCommandTest.php: -------------------------------------------------------------------------------- 1 | queueManager === null) { 26 | $this->queueManager = $this->getMockBuilder(QueueManager::class) 27 | ->disableOriginalConstructor() 28 | ->getMock(); 29 | $this->queueManager 30 | ->expects($this->any()) 31 | ->method('deleteQueue') 32 | ->with('my-queue-url') 33 | ->willReturn(true); 34 | 35 | $this->getContainer()->set('tritran.sqs_queue.queue_manager', $this->queueManager); 36 | } 37 | } 38 | 39 | /** 40 | * Test: Delete a queue without force option 41 | */ 42 | public function testExecuteWithoutForce() 43 | { 44 | $commandTester = $this->createCommandTester(new QueueDeleteCommand()); 45 | $commandTester->execute([ 46 | 'url' => 'my-queue-url' 47 | ]); 48 | 49 | $output = $commandTester->getDisplay(); 50 | $this->assertContains('Option --force is mandatory to drop data', $output); 51 | } 52 | 53 | /** 54 | * Test: Delete a queue without force option 55 | */ 56 | public function testExecute() 57 | { 58 | $commandTester = $this->createCommandTester(new QueueDeleteCommand()); 59 | $commandTester->execute([ 60 | 'url' => 'my-queue-url', 61 | '--force' => true 62 | ]); 63 | 64 | $output = $commandTester->getDisplay(); 65 | $this->assertContains('Done', $output); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/Functional/Command/QueueListCommandTest.php: -------------------------------------------------------------------------------- 1 | queueManager === null) { 26 | $this->queueManager = $this->getMockBuilder(QueueManager::class) 27 | ->disableOriginalConstructor() 28 | ->getMock(); 29 | $this->queueManager 30 | ->expects($this->any()) 31 | ->method('listQueue') 32 | ->willReturnCallback(function ($arg) { 33 | return $arg == 'invalid' ? [] : ['queue-url-1', 'queue-url-2']; 34 | }); 35 | 36 | $this->getContainer()->set('tritran.sqs_queue.queue_manager', $this->queueManager); 37 | } 38 | } 39 | 40 | /** 41 | * Test: Returns a list of your queues. 42 | */ 43 | public function testExecute() 44 | { 45 | $commandTester = $this->createCommandTester(new QueueListCommand()); 46 | $commandTester->execute([ 47 | '--prefix' => 'queue-prefix' 48 | ]); 49 | 50 | $output = $commandTester->getDisplay(); 51 | $this->assertContains('queue-url-1', $output); 52 | $this->assertContains('queue-url-2', $output); 53 | $this->assertContains('Done', $output); 54 | } 55 | 56 | /** 57 | * Test: There are not any queue 58 | */ 59 | public function testExecuteWithEmpty() 60 | { 61 | $commandTester = $this->createCommandTester(new QueueListCommand()); 62 | $commandTester->execute([ 63 | '--prefix' => 'invalid' 64 | ]); 65 | $output = $commandTester->getDisplay(); 66 | $this->assertContains( 67 | 'You don\'t have any queue at this moment. Please go to AWS Console to create a new one.', 68 | $output 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/Functional/Command/QueuePingCommandTest.php: -------------------------------------------------------------------------------- 1 | queue === null) { 26 | $this->queue = $this->getMockBuilder(BaseQueue::class) 27 | ->disableOriginalConstructor() 28 | ->getMock(); 29 | $this->queue 30 | ->expects($this->any()) 31 | ->method('ping') 32 | ->willReturn('new-message-id'); 33 | 34 | $this->getContainer()->set('tritran.sqs_queue.basic_queue', $this->queue); 35 | } 36 | } 37 | 38 | /** 39 | * Test: Purge a queue with a non-existing queue 40 | */ 41 | public function testExecuteWithNonExistingQueue() 42 | { 43 | $commandTester = $this->createCommandTester(new QueuePingCommand()); 44 | 45 | $this->expectException(\InvalidArgumentException::class); 46 | $commandTester->execute([ 47 | 'name' => 'non-existing-queue' 48 | ]); 49 | } 50 | 51 | /** 52 | * Test: Delete a queue without force option 53 | */ 54 | public function testExecute() 55 | { 56 | $commandTester = $this->createCommandTester(new QueuePingCommand()); 57 | $commandTester->execute([ 58 | 'name' => 'basic_queue' 59 | ]); 60 | 61 | $output = $commandTester->getDisplay(); 62 | $this->assertContains('Done', $output); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/Functional/Command/QueuePurgeCommandTest.php: -------------------------------------------------------------------------------- 1 | queue === null) { 26 | $this->queue = $this->getMockBuilder(BaseQueue::class) 27 | ->disableOriginalConstructor() 28 | ->getMock(); 29 | $this->queue 30 | ->expects($this->any()) 31 | ->method('purge') 32 | ->willReturn(true); 33 | 34 | $this->getContainer()->set('tritran.sqs_queue.basic_queue', $this->queue); 35 | } 36 | } 37 | 38 | /** 39 | * Test: Purge a queue without force option 40 | */ 41 | public function testExecuteWithoutForce() 42 | { 43 | $commandTester = $this->createCommandTester(new QueuePurgeCommand()); 44 | $commandTester->execute([ 45 | 'name' => 'my-queue-name' 46 | ]); 47 | 48 | $output = $commandTester->getDisplay(); 49 | $this->assertContains('Option --force is mandatory to drop data', $output); 50 | } 51 | 52 | /** 53 | * Test: Purge a queue with a non-existing queue 54 | */ 55 | public function testExecuteWithNonExistingQueue() 56 | { 57 | $commandTester = $this->createCommandTester(new QueuePurgeCommand()); 58 | 59 | $this->expectException(\InvalidArgumentException::class); 60 | $commandTester->execute([ 61 | 'name' => 'non-existing-queue', 62 | '--force' => true 63 | ]); 64 | } 65 | 66 | /** 67 | * Test: Delete a queue without force option 68 | */ 69 | public function testExecute() 70 | { 71 | $commandTester = $this->createCommandTester(new QueuePurgeCommand()); 72 | $commandTester->execute([ 73 | 'name' => 'basic_queue', 74 | '--force' => true 75 | ]); 76 | 77 | $output = $commandTester->getDisplay(); 78 | $this->assertContains('Done', $output); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/Functional/Command/QueueUpdateCommandTest.php: -------------------------------------------------------------------------------- 1 | queueManager === null) { 25 | $this->queueManager = $this->getMockBuilder(QueueManager::class) 26 | ->disableOriginalConstructor() 27 | ->getMock(); 28 | $this->queueManager 29 | ->expects($this->any()) 30 | ->method('listQueue') 31 | ->willReturn(['aws-basic-queue-url']); 32 | $this->queueManager 33 | ->expects($this->any()) 34 | ->method('setQueueAttributes') 35 | ->willReturn(true); 36 | 37 | $this->getContainer()->set('tritran.sqs_queue.queue_manager', $this->queueManager); 38 | } 39 | } 40 | 41 | /** 42 | * Test: Update Queue attribute based on configuration without force option. 43 | */ 44 | public function testExecuteWithoutForce() 45 | { 46 | $commandTester = $this->createCommandTester(new QueueUpdateCommand()); 47 | $commandTester->execute([]); 48 | 49 | $output = $commandTester->getDisplay(); 50 | $this->assertContains('Option --force is mandatory to update data', $output); 51 | } 52 | 53 | /** 54 | * Test: Delete a queue without force option. 55 | */ 56 | public function testExecute() 57 | { 58 | $commandTester = $this->createCommandTester(new QueueUpdateCommand()); 59 | $commandTester->execute(['--force' => true]); 60 | 61 | $output = $commandTester->getDisplay(); 62 | $this->assertContains('Done', $output); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/Functional/Command/QueueWorkerCommandTest.php: -------------------------------------------------------------------------------- 1 | baseWorker === null) { 25 | $this->baseWorker = $this->getMockBuilder(BaseWorker::class) 26 | ->disableOriginalConstructor() 27 | ->getMock(); 28 | $this->baseWorker 29 | ->expects($this->any()) 30 | ->method('start') 31 | ->willReturn(true); 32 | 33 | $this->getContainer()->set('tritran.sqs_queue.queue_worker', $this->baseWorker); 34 | } 35 | } 36 | 37 | /** 38 | * Test: start a worker for a non-existing queue. 39 | */ 40 | public function testExecuteWithNonExistingQueue() 41 | { 42 | $commandTester = $this->createCommandTester(new QueueWorkerCommand()); 43 | 44 | $this->expectException(\InvalidArgumentException::class); 45 | $commandTester->execute([ 46 | 'name' => 'non-existing-queue', 47 | ]); 48 | } 49 | 50 | /** 51 | * Test: start a worker with an invalid value of amount of messages. 52 | */ 53 | public function testExecuteWithInvalidAmountMessages() 54 | { 55 | $commandTester = $this->createCommandTester(new QueueWorkerCommand()); 56 | 57 | $this->expectException(\InvalidArgumentException::class); 58 | $commandTester->execute([ 59 | 'name' => 'basic_queue', 60 | '--messages' => -1, 61 | ]); 62 | } 63 | 64 | /** 65 | * Test: Start a worker for listening to a queue. 66 | */ 67 | public function testExecute() 68 | { 69 | $commandTester = $this->createCommandTester(new QueueWorkerCommand()); 70 | $commandTester->execute([ 71 | 'name' => 'basic_queue', 72 | ]); 73 | 74 | $output = $commandTester->getDisplay(); 75 | $this->assertContains('Start listening to queue', $output); 76 | } 77 | 78 | /** 79 | * Test: invalid input limit should throw an exception. 80 | */ 81 | public function testInvalidInputLimit() 82 | { 83 | $commandTester = $this->createCommandTester(new QueueWorkerCommand()); 84 | 85 | $this->expectException(\InvalidArgumentException::class); 86 | $commandTester->execute([ 87 | 'name' => 'basic_queue', 88 | '--limit' => 0, 89 | ]); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/Unit/DependencyInjection/Compiler/SQSQueuePassTest.php: -------------------------------------------------------------------------------- 1 | getContainer(); 32 | $compiler = new SQSQueuePass(); 33 | 34 | $this->expectException(\InvalidArgumentException::class); 35 | $compiler->process($container); 36 | } 37 | 38 | /** 39 | * Make sure the worker of queue should be a valid and callable service. 40 | */ 41 | public function testProcessFailureWithBadWorker() 42 | { 43 | $container = $this->getContainer(); 44 | $container->setDefinition('aws.sqs', new Definition()); 45 | $container->setParameter( 46 | 'tritran.sqs_queue.queues', 47 | ['queue-name' => ['queue_url' => 'my-url', 'worker' => 'bad-worker']] 48 | ); 49 | 50 | $compiler = new SQSQueuePass(); 51 | $this->expectException(\InvalidArgumentException::class); 52 | $compiler->process($container); 53 | } 54 | 55 | /** 56 | * Make sure the name of queue should be different with predefined queue-name 57 | */ 58 | public function testProcessFailureWithPredefinedQueueName() 59 | { 60 | $container = $this->getContainer(); 61 | $container->setDefinition('aws.sqs', new Definition()); 62 | 63 | $defaultQueueServices = ['queues', 'queue_factory', 'queue_manager', 'queue_worker', 'basic_queue']; 64 | $container->setParameter( 65 | 'tritran.sqs_queue.queues', 66 | [ 67 | $defaultQueueServices[array_rand($defaultQueueServices)] => [ 68 | 'queue_url' => 'my-url', 69 | 'worker' => 'bad-worker' 70 | ] 71 | ] 72 | ); 73 | 74 | $compiler = new SQSQueuePass(); 75 | $this->expectException(\InvalidArgumentException::class); 76 | $compiler->process($container); 77 | } 78 | 79 | /** 80 | * @return array 81 | */ 82 | public function configurationProvider(): array 83 | { 84 | $container = $this->getContainer(); 85 | $container->setDefinition('aws.sqs', new Definition()); 86 | 87 | $basicWorker = BasicWorker::class; 88 | $basicWorkerAsService = 'tritran.sqs_queue.fixture.basic_worker'; 89 | $container->setDefinition($basicWorkerAsService, new Definition($basicWorker)); 90 | 91 | return [ 92 | // Case #0: Load a worker with default attributes 93 | [ 94 | $container, 95 | [ 96 | 'basic-queue' => [ 97 | 'queue_url' => 'basic-url', 98 | 'worker' => $basicWorker, 99 | 'attributes' => [] 100 | ] 101 | ], 102 | [ 103 | 'basic-queue' => [ 104 | 'basic-url', 105 | new Definition($basicWorker), 106 | [ 107 | 'DelaySeconds' => 0, 108 | 'MaximumMessageSize' => 262144, 109 | 'MessageRetentionPeriod' => 345600, 110 | 'ReceiveMessageWaitTimeSeconds' => 20, 111 | 'VisibilityTimeout' => 30, 112 | 'RedrivePolicy' => '' 113 | ] 114 | ] 115 | ] 116 | ], 117 | // Case #1: Load a worker as a callable class with some attributes 118 | [ 119 | $container, 120 | [ 121 | 'basic-queue' => [ 122 | 'queue_url' => 'basic-url', 123 | 'worker' => $basicWorker, 124 | 'attributes' => [ 125 | 'delay_seconds' => 1, 126 | 'maximum_message_size' => 1, 127 | 'message_retention_period' => 1, 128 | 'receive_message_wait_time_seconds' => 1, 129 | 'visibility_timeout' => 1, 130 | 'redrive_policy' => [ 131 | 'dead_letter_queue' => 'basic_dead_letter_queue_1', 132 | 'max_receive_count' => 1 133 | ] 134 | ] 135 | ] 136 | ], 137 | [ 138 | 'basic-queue' => [ 139 | 'basic-url', 140 | new Definition($basicWorker), 141 | [ 142 | 'DelaySeconds' => 1, 143 | 'MaximumMessageSize' => 1, 144 | 'MessageRetentionPeriod' => 1, 145 | 'ReceiveMessageWaitTimeSeconds' => 1, 146 | 'VisibilityTimeout' => 1, 147 | 'RedrivePolicy' => json_encode([ 148 | 'deadLetterTargetArn' => 'basic_dead_letter_queue_1', 149 | 'maxReceiveCount' => 1 150 | ]) 151 | ] 152 | ] 153 | ] 154 | ], 155 | // Case #2: Load a worker as a service 156 | [ 157 | $container, 158 | [ 159 | 'basic-queue' => [ 160 | 'queue_url' => 'basic-url', 161 | 'worker' => $basicWorkerAsService, 162 | 'attributes' => [ 163 | 'delay_seconds' => 2, 164 | 'maximum_message_size' => 2, 165 | 'message_retention_period' => 2, 166 | 'receive_message_wait_time_seconds' => 2, 167 | 'visibility_timeout' => 2, 168 | 'redrive_policy' => [ 169 | 'dead_letter_queue' => 'basic_dead_letter_queue_2', 170 | 'max_receive_count' => 2 171 | ] 172 | ] 173 | ] 174 | ], 175 | [ 176 | 'basic-queue' => [ 177 | 'basic-url', 178 | new Reference($basicWorkerAsService), 179 | [ 180 | 'DelaySeconds' => 2, 181 | 'MaximumMessageSize' => 2, 182 | 'MessageRetentionPeriod' => 2, 183 | 'ReceiveMessageWaitTimeSeconds' => 2, 184 | 'VisibilityTimeout' => 2, 185 | 'RedrivePolicy' => json_encode([ 186 | 'deadLetterTargetArn' => 'basic_dead_letter_queue_2', 187 | 'maxReceiveCount' => 2 188 | ]) 189 | ] 190 | ] 191 | ] 192 | ], 193 | // Case #3: Load a FIFO queue 194 | [ 195 | $container, 196 | [ 197 | 'queue.fifo' => [ 198 | 'queue_url' => 'fifo-queue-url', 199 | 'worker' => $basicWorker, 200 | 'attributes' => [] 201 | ] 202 | ], 203 | [ 204 | 'queue.fifo' => [ 205 | 'fifo-queue-url', 206 | new Definition($basicWorker), 207 | [ 208 | 'DelaySeconds' => 0, 209 | 'MaximumMessageSize' => 262144, 210 | 'MessageRetentionPeriod' => 345600, 211 | 'ReceiveMessageWaitTimeSeconds' => 20, 212 | 'VisibilityTimeout' => 30, 213 | 'RedrivePolicy' => '', 214 | 'ContentBasedDeduplication' => true 215 | ] 216 | ] 217 | ] 218 | ], 219 | // Case #4: Load multi queues at the same time 220 | [ 221 | $container, 222 | [ 223 | 'basic-queue-1' => ['queue_url' => 'basic-url-1', 'worker' => $basicWorker], 224 | 'basic-queue-2' => ['queue_url' => 'basic-url-2', 'worker' => $basicWorkerAsService], 225 | 'queue.fifo' => ['queue_url' => 'fifo-queue-url', 'worker' => $basicWorkerAsService] 226 | ], 227 | [ 228 | 'basic-queue-1' => [ 229 | 'basic-url-1', 230 | new Definition($basicWorker), 231 | [ 232 | 'DelaySeconds' => 0, 233 | 'MaximumMessageSize' => 262144, 234 | 'MessageRetentionPeriod' => 345600, 235 | 'ReceiveMessageWaitTimeSeconds' => 20, 236 | 'VisibilityTimeout' => 30, 237 | 'RedrivePolicy' => '' 238 | ] 239 | ], 240 | 'basic-queue-2' => [ 241 | 'basic-url-2', 242 | new Reference($basicWorkerAsService), 243 | [ 244 | 'DelaySeconds' => 0, 245 | 'MaximumMessageSize' => 262144, 246 | 'MessageRetentionPeriod' => 345600, 247 | 'ReceiveMessageWaitTimeSeconds' => 20, 248 | 'VisibilityTimeout' => 30, 249 | 'RedrivePolicy' => '' 250 | ] 251 | ], 252 | 'queue.fifo' => [ 253 | 'fifo-queue-url', 254 | new Reference($basicWorkerAsService), 255 | [ 256 | 'DelaySeconds' => 0, 257 | 'MaximumMessageSize' => 262144, 258 | 'MessageRetentionPeriod' => 345600, 259 | 'ReceiveMessageWaitTimeSeconds' => 20, 260 | 'VisibilityTimeout' => 30, 261 | 'RedrivePolicy' => '', 262 | 'ContentBasedDeduplication' => true 263 | ] 264 | ] 265 | ] 266 | ], 267 | 268 | ]; 269 | } 270 | 271 | /** 272 | * Load configuration of Machine Engine 273 | * 274 | * @param ContainerBuilder $container 275 | * @param array $config 276 | * @param array $expectedArgs 277 | * 278 | * @dataProvider configurationProvider 279 | */ 280 | public function testProcess($container, $config, $expectedArgs) 281 | { 282 | $container->setParameter('tritran.sqs_queue.queues', $config); 283 | 284 | $compiler = new SQSQueuePass(); 285 | $compiler->process($container); 286 | 287 | foreach ($config as $queueName => $queueOption) { 288 | $queueId = sprintf('tritran.sqs_queue.%s', $queueName); 289 | 290 | $this->assertTrue($container->hasDefinition($queueId)); 291 | 292 | $definition = $container->getDefinition($queueId); 293 | $this->assertEquals([ 294 | new Reference('tritran.sqs_queue.queue_factory'), 295 | 'create' 296 | ], $definition->getFactory()); 297 | 298 | $this->assertEquals( 299 | array_merge([$queueName], $expectedArgs[$queueName]), 300 | $definition->getArguments() 301 | ); 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /Tests/Unit/DependencyInjection/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | processConfiguration(new Configuration(), []); 22 | $expectedConfiguration = []; 23 | $this->assertEquals($expectedConfiguration, $processorConfig); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/Unit/DependencyInjection/TriTranSqsQueueExtensionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('tritran_sqs_queue', $extension->getAlias()); 22 | } 23 | 24 | /** 25 | * @return ContainerBuilder 26 | */ 27 | protected function getContainer(): ContainerBuilder 28 | { 29 | return new ContainerBuilder(); 30 | } 31 | 32 | /** 33 | * Make sure the extension loaded all pre-defined services successfully 34 | */ 35 | public function testPredefinedServicesLoaded() 36 | { 37 | $container = $this->getContainer(); 38 | $extension = new TriTranSqsQueueExtension(); 39 | $extension->load([], $container); 40 | 41 | $this->assertTrue($container->hasDefinition('tritran.sqs_queue.queue_factory')); 42 | $this->assertTrue($container->hasDefinition('tritran.sqs_queue.queue_worker')); 43 | $this->assertTrue($container->hasDefinition('tritran.sqs_queue.queue_manager')); 44 | } 45 | 46 | /** 47 | * Make sure the extension loaded all pre-defined parameters successfully via configuration 48 | */ 49 | public function testPredefinedParametersLoaded() 50 | { 51 | $container = $this->getContainer(); 52 | $extension = new TriTranSqsQueueExtension(); 53 | $extension->load([ 54 | 'tritran_sqs_queue' => [ 55 | 'sqs_queue' => [ 56 | 'queues' => [ 57 | ['name' => 'queue-1', 'queue_url' => 'url-1', 'worker' => 'worker-1'], 58 | ['name' => 'queue-2', 'queue_url' => 'url-2', 'worker' => 'worker-2'], 59 | ] 60 | ] 61 | ] 62 | ], $container); 63 | 64 | $this->assertTrue($container->hasParameter('tritran.sqs_queue.queues')); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/Unit/Service/BaseQueueTest.php: -------------------------------------------------------------------------------- 1 | errors = []; 32 | set_error_handler([$this, "errorHandler"]); 33 | } 34 | 35 | /** 36 | * @param $errno 37 | * @param $errstr 38 | * @param $errfile 39 | * @param $errline 40 | * @param $errcontext 41 | */ 42 | public function errorHandler($errno, $errstr, $errfile, $errline, $errcontext) 43 | { 44 | $this->errors[] = compact('errno', 'errstr', 'errfile', 'errline', 'errcontext'); 45 | } 46 | 47 | /** 48 | * @param $errstr 49 | * @param $errno 50 | */ 51 | public function assertError($errstr, $errno) 52 | { 53 | foreach ($this->errors as $error) { 54 | if ($error["errstr"] === $errstr 55 | && $error["errno"] === $errno) { 56 | $this->assertTrue(true); 57 | 58 | return; 59 | } 60 | } 61 | $this->fail( 62 | "Error with level " . $errno . " and message '" . $errstr . "' not found in ", 63 | var_export($this->errors, true) 64 | ); 65 | } 66 | 67 | /** 68 | * @param array $entries 69 | * 70 | * @return \PHPUnit_Framework_MockObject_MockObject|Result 71 | */ 72 | private function getAwsResult($entries) 73 | { 74 | /** @var \PHPUnit_Framework_MockObject_MockObject|Result $result */ 75 | $result = $this->getMockBuilder(Result::class)->getMock(); 76 | $result->expects($this->any()) 77 | ->method('get') 78 | ->withAnyParameters() 79 | ->willReturnCallback(function ($arg) use ($entries) { 80 | return $entries[$arg] ?? []; 81 | }); 82 | 83 | return $result; 84 | } 85 | 86 | /** 87 | * @return \PHPUnit_Framework_MockObject_MockObject|SqsClient 88 | */ 89 | private function getAwsClient() 90 | { 91 | /** @var \PHPUnit_Framework_MockObject_MockObject|SqsClient $client */ 92 | $client = $this->getMockBuilder(SqsClient::class) 93 | ->disableOriginalConstructor() 94 | ->setMethods(['sendMessage', 'receiveMessage', 'deleteMessage', 'changeMessageVisibility', 'purgeQueue']) 95 | ->getMock(); 96 | 97 | return $client; 98 | } 99 | 100 | /** 101 | * @return BaseQueue 102 | */ 103 | public function getQueue() 104 | { 105 | $client = $this->getAwsClient(); 106 | $queueName = 'queue-name'; 107 | $queueUrl = 'queue-url'; 108 | $queueWorker = new BasicWorker(); 109 | $queueAttr = ['a', 'b', 'c', 'd']; 110 | 111 | return new BaseQueue($client, $queueName, $queueUrl, $queueWorker, $queueAttr); 112 | } 113 | 114 | /** 115 | * Test: Construction 116 | */ 117 | public function testConstruction() 118 | { 119 | $client = $this->getAwsClient(); 120 | $queueName = 'queue-name'; 121 | $queueUrl = 'queue-url'; 122 | $queueWorker = new BasicWorker(); 123 | $queueAttr = ['a', 'b', 'c', 'd']; 124 | $queue = new BaseQueue($client, $queueName, $queueUrl, $queueWorker, $queueAttr); 125 | 126 | $this->assertInstanceOf(BaseQueue::class, $queue); 127 | $this->assertEquals($client, $queue->getClient()); 128 | $this->assertEquals($queueName, $queue->getQueueName()); 129 | $this->assertEquals($queueUrl, $queue->getQueueUrl()); 130 | $this->assertEquals($queueWorker, $queue->getQueueWorker()); 131 | $this->assertEquals($queueAttr, $queue->getAttributes()); 132 | } 133 | 134 | /** 135 | * Test: Getter/Setter 136 | */ 137 | public function testGetterSetter() 138 | { 139 | $client = $this->getAwsClient(); 140 | $queueUrl = 'queue-url'; 141 | $queueAttr = ['a', 'b', 'c', 'd']; 142 | 143 | $queue = new BaseQueue($client, '', '', new BasicWorker(), []); 144 | 145 | $this->assertInstanceOf(BaseQueue::class, $queue->setQueueUrl($queueUrl)); 146 | $this->assertEquals($queueUrl, $queue->getQueueUrl()); 147 | $this->assertInstanceOf(BaseQueue::class, $queue->setAttributes($queueAttr)); 148 | $this->assertEquals($queueAttr, $queue->getAttributes()); 149 | } 150 | 151 | /** 152 | * Test: send message to a queue 153 | */ 154 | public function testSendMessage() 155 | { 156 | $delay = random_int(1, 10); 157 | $messageBody = 'my-message'; 158 | $messageAttr = ['x', 'y', 'z']; 159 | $queueUrl = 'queue-url'; 160 | 161 | $client = $this->getAwsClient(); 162 | $client->expects($this->any()) 163 | ->method('sendMessage') 164 | ->with([ 165 | 'DelaySeconds' => $delay, 166 | 'MessageAttributes' => $messageAttr, 167 | 'MessageBody' => $messageBody, 168 | 'QueueUrl' => $queueUrl 169 | ]) 170 | ->willReturn($this->getAwsResult(['MessageId' => 'new-message-id'])); 171 | 172 | $queue = new BaseQueue($client, 'queue-name', $queueUrl, new BasicWorker(), []); 173 | $this->assertEquals( 174 | 'new-message-id', 175 | $queue->sendMessage(new Message($messageBody, $messageAttr), $delay) 176 | ); 177 | } 178 | 179 | /** 180 | * Test: send message to a queue 181 | */ 182 | public function testSendPingMessage() 183 | { 184 | $queueUrl = 'queue-url'; 185 | 186 | $client = $this->getAwsClient(); 187 | $client->expects($this->any()) 188 | ->method('sendMessage') 189 | ->with([ 190 | 'MessageBody' => 'ping', 191 | 'MessageAttributes' => [], 192 | 'QueueUrl' => $queueUrl 193 | ]) 194 | ->willReturn($this->getAwsResult(['MessageId' => 'new-message-id'])); 195 | 196 | $queue = new BaseQueue($client, 'queue-name', $queueUrl, new BasicWorker(), []); 197 | $this->assertEquals('new-message-id', $queue->ping()); 198 | } 199 | 200 | /** 201 | * Test: send message to a FIFO queue 202 | */ 203 | public function testSendMessageToFifoQueue() 204 | { 205 | $messageBody = 'my-message'; 206 | $messageAttr = ['x', 'y', 'z']; 207 | $queueUrl = 'queue-url'; 208 | $groupId = 'group-name'; 209 | $deduplicationId = 'deduplication-id'; 210 | 211 | $client = $this->getAwsClient(); 212 | $client->expects($this->any()) 213 | ->method('sendMessage') 214 | ->with([ 215 | 'MessageAttributes' => $messageAttr, 216 | 'MessageBody' => $messageBody, 217 | 'QueueUrl' => $queueUrl, 218 | 'MessageGroupId' => $groupId, 219 | 'MessageDeduplicationId' => $deduplicationId, 220 | ]) 221 | ->willReturn($this->getAwsResult(['MessageId' => 'new-message-id'])); 222 | 223 | $queue = new BaseQueue($client, 'queue-name.fifo', $queueUrl, new BasicWorker(), []); 224 | $this->assertEquals( 225 | 'new-message-id', 226 | $queue->sendMessage(new Message($messageBody, $messageAttr, $groupId, $deduplicationId)) 227 | ); 228 | } 229 | 230 | /** 231 | * Test: send message to a FIFO queue 232 | */ 233 | public function testSendMessageToFifoQueueWithDelay() 234 | { 235 | $delay = random_int(0, 10); 236 | $messageBody = 'my-message'; 237 | $messageAttr = ['x', 'y', 'z']; 238 | $queueUrl = 'queue-url'; 239 | $groupId = 'group-name'; 240 | $deduplicationId = 'deduplication-id'; 241 | 242 | $client = $this->getAwsClient(); 243 | $client->expects($this->any()) 244 | ->method('sendMessage') 245 | ->with([ 246 | 'MessageAttributes' => $messageAttr, 247 | 'MessageBody' => $messageBody, 248 | 'QueueUrl' => $queueUrl, 249 | 'MessageGroupId' => $groupId, 250 | 'MessageDeduplicationId' => $deduplicationId, 251 | ]) 252 | ->willReturn($this->getAwsResult(['MessageId' => 'new-message-id'])); 253 | 254 | $queue = new BaseQueue($client, 'queue-name.fifo', $queueUrl, new BasicWorker(), []); 255 | $queue->sendMessage(new Message($messageBody, $messageAttr, $groupId, $deduplicationId), $delay); 256 | $this->assertError('FIFO queues don\'t support per-message delays, only per-queue delays.', E_USER_WARNING); 257 | } 258 | 259 | /** 260 | * Test: send message to a queue in failure 261 | */ 262 | public function testSendMessageFailure() 263 | { 264 | $client = $this->getAwsClient(); 265 | $client->expects($this->any()) 266 | ->method('sendMessage') 267 | ->withAnyParameters() 268 | ->willThrowException(new AwsException( 269 | 'AWS Client Exception', 270 | new Command('send-message-command') 271 | )); 272 | 273 | $queue = new BaseQueue($client, 'bad-queue-name', 'bad-queue-url', new BasicWorker(), []); 274 | 275 | $this->expectException(\InvalidArgumentException::class); 276 | $queue->sendMessage(new Message('my-message', [])); 277 | } 278 | 279 | /** 280 | * Test: send message to a FIFO queue 281 | */ 282 | public function testSendMessageToFifoQueueFailure() 283 | { 284 | $client = $this->getAwsClient(); 285 | $client->expects($this->any()) 286 | ->method('sendMessage') 287 | ->withAnyParameters() 288 | ->willThrowException(new AwsException( 289 | 'AWS Client Exception', 290 | new Command('send-message-command') 291 | )); 292 | 293 | $queue = new BaseQueue($client, 'queue-name.fifo', 'queue-url', new BasicWorker(), []); 294 | 295 | $this->expectException(\InvalidArgumentException::class); 296 | $this->expectExceptionMessage('MessageGroupId is required for FIFO queues.'); 297 | $queue->sendMessage(new Message('my-message', [], '')); 298 | } 299 | 300 | /** 301 | * Test: send message to a FIFO queue 302 | */ 303 | public function testSendMessageToFifoQueueWarning() 304 | { 305 | $client = $this->getAwsClient(); 306 | $client->expects($this->any()) 307 | ->method('sendMessage') 308 | ->withAnyParameters() 309 | ->willThrowException(new AwsException( 310 | 'AWS Client Exception', 311 | new Command('send-message-command') 312 | )); 313 | 314 | $queue = new BaseQueue($client, 'queue-name.fifo', 'queue-url', new BasicWorker(), []); 315 | 316 | $this->expectException(\InvalidArgumentException::class); 317 | $queue->sendMessage(new Message('my-message', [], ''), random_int(0, 10)); 318 | } 319 | 320 | /** 321 | * Test: receive Message 322 | */ 323 | public function testReceiveMessage() 324 | { 325 | $limit = random_int(1, 10); 326 | $queueUrl = 'queue-url'; 327 | $queueAttr = [ 328 | 'ReceiveMessageWaitTimeSeconds' => random_int(1, 10), 329 | 'VisibilityTimeout' => random_int(0, 30) 330 | ]; 331 | $expected = [ 332 | [ 333 | 'MessageId' => 'my-message-id', 334 | 'Body' => 'my-body', 335 | 'ReceiptHandle' => 'receipt-handle', 336 | 'Attributes' => [], 337 | ] 338 | ]; 339 | 340 | $client = $this->getAwsClient(); 341 | $client->expects($this->any()) 342 | ->method('receiveMessage') 343 | ->with([ 344 | 'AttributeNames' => ['All'], 345 | 'MaxNumberOfMessages' => $limit, 346 | 'MessageAttributeNames' => ['All'], 347 | 'QueueUrl' => $queueUrl, 348 | 'WaitTimeSeconds' => $queueAttr['ReceiveMessageWaitTimeSeconds'], 349 | 'VisibilityTimeout' => $queueAttr['VisibilityTimeout'], 350 | ]) 351 | ->willReturn($this->getAwsResult(['Messages' => $expected])); 352 | 353 | $queue = new BaseQueue($client, 'queue-name', $queueUrl, new BasicWorker(), $queueAttr); 354 | $result = $queue->receiveMessage($limit); 355 | $this->assertInstanceOf(MessageCollection::class, $result); 356 | $this->assertEquals($this->arrayMessageToCollection($expected), $result); 357 | } 358 | 359 | /** 360 | * @param array $messages 361 | * 362 | * @return MessageCollection 363 | */ 364 | private function arrayMessageToCollection($messages) 365 | { 366 | $collection = new MessageCollection([]); 367 | foreach ($messages as $message) { 368 | $collection->append( 369 | (new Message()) 370 | ->setId($message['MessageId']) 371 | ->setBody($message['Body']) 372 | ->setReceiptHandle($message['ReceiptHandle']) 373 | ->setAttributes($message['Attributes']) 374 | ); 375 | } 376 | 377 | return $collection; 378 | } 379 | 380 | /** 381 | * Test: receive Message in failure 382 | */ 383 | public function testReceiveMessageFailure() 384 | { 385 | $client = $this->getAwsClient(); 386 | $client->expects($this->any()) 387 | ->method('receiveMessage') 388 | ->withAnyParameters() 389 | ->willThrowException(new AwsException( 390 | 'AWS Client Exception', 391 | new Command('receive-message-command') 392 | )); 393 | 394 | $queue = new BaseQueue($client, 'bad-queue-name', 'bad-queue-url', new BasicWorker(), []); 395 | $this->expectException(\InvalidArgumentException::class); 396 | $queue->receiveMessage(); 397 | } 398 | 399 | /** 400 | * Test: Delete a message from queue 401 | */ 402 | public function testDeleteMessage() 403 | { 404 | $queueUrl = 'queue-url'; 405 | $message = (new Message())->setReceiptHandle('my-receipt-handle'); 406 | 407 | $client = $this->getAwsClient(); 408 | $client->expects($this->any()) 409 | ->method('deleteMessage') 410 | ->with([ 411 | 'QueueUrl' => $queueUrl, 412 | 'ReceiptHandle' => $message->getReceiptHandle() 413 | ]) 414 | ->willReturn(true); 415 | 416 | $queue = new BaseQueue($client, 'queue-name', $queueUrl, new BasicWorker(), []); 417 | $this->assertTrue($queue->deleteMessage($message)); 418 | } 419 | 420 | /** 421 | * Test: Delete a message from queue in failure 422 | */ 423 | public function testDeleteMessageFailure() 424 | { 425 | $queueUrl = 'bad-queue-url'; 426 | $message = (new Message())->setReceiptHandle('my-receipt-handle'); 427 | 428 | $client = $this->getAwsClient(); 429 | $client->expects($this->any()) 430 | ->method('deleteMessage') 431 | ->with([ 432 | 'QueueUrl' => $queueUrl, 433 | 'ReceiptHandle' => $message->getReceiptHandle() 434 | ]) 435 | ->willThrowException(new AwsException( 436 | 'AWS Client Exception', 437 | new Command('delete-message-command') 438 | )); 439 | 440 | $queue = new BaseQueue($client, 'bad-queue-name', $queueUrl, new BasicWorker(), []); 441 | $this->expectException(\InvalidArgumentException::class); 442 | $queue->deleteMessage($message); 443 | } 444 | 445 | /** 446 | * Test: Release a message from processing, making it visible again 447 | */ 448 | public function testReleaseMessage() 449 | { 450 | $queueUrl = 'queue-url'; 451 | $message = (new Message())->setReceiptHandle('my-receipt-handle'); 452 | 453 | $client = $this->getAwsClient(); 454 | $client->expects($this->any()) 455 | ->method('changeMessageVisibility') 456 | ->with([ 457 | 'QueueUrl' => $queueUrl, 458 | 'ReceiptHandle' => $message->getReceiptHandle(), 459 | 'VisibilityTimeout' => 0 460 | ]) 461 | ->willReturn(true); 462 | 463 | $queue = new BaseQueue($client, 'queue-name', $queueUrl, new BasicWorker(), []); 464 | $this->assertTrue($queue->releaseMessage($message)); 465 | } 466 | 467 | /** 468 | * Test: Release a message from processing in failure 469 | */ 470 | public function testReleaseMessageFailure() 471 | { 472 | $queueUrl = 'bad-queue-url'; 473 | $message = (new Message())->setReceiptHandle('my-receipt-handle'); 474 | 475 | $client = $this->getAwsClient(); 476 | $client->expects($this->any()) 477 | ->method('changeMessageVisibility') 478 | ->with([ 479 | 'QueueUrl' => $queueUrl, 480 | 'ReceiptHandle' => $message->getReceiptHandle(), 481 | 'VisibilityTimeout' => 0 482 | ]) 483 | ->willThrowException(new AwsException( 484 | 'AWS Client Exception', 485 | new Command('release-message-command') 486 | )); 487 | 488 | $queue = new BaseQueue($client, 'bad-queue-name', $queueUrl, new BasicWorker(), []); 489 | $this->expectException(\InvalidArgumentException::class); 490 | $queue->releaseMessage($message); 491 | } 492 | 493 | /** 494 | * Test: Delete a message from queue 495 | */ 496 | public function testPurge() 497 | { 498 | $queueUrl = 'queue-url'; 499 | 500 | $client = $this->getAwsClient(); 501 | $client->expects($this->any()) 502 | ->method('purgeQueue') 503 | ->with([ 504 | 'QueueUrl' => $queueUrl 505 | ]) 506 | ->willReturn(true); 507 | 508 | $queue = new BaseQueue($client, 'queue-name', $queueUrl, new BasicWorker(), []); 509 | $this->assertTrue($queue->purge()); 510 | } 511 | 512 | /** 513 | * Test: Delete a message from queue in failure 514 | */ 515 | public function testPurgeFailure() 516 | { 517 | $queueUrl = 'bad-queue-url'; 518 | 519 | $client = $this->getAwsClient(); 520 | $client->expects($this->any()) 521 | ->method('purgeQueue') 522 | ->with([ 523 | 'QueueUrl' => $queueUrl 524 | ]) 525 | ->willThrowException(new AwsException( 526 | 'AWS Client Exception', 527 | new Command('delete-message-command') 528 | )); 529 | 530 | $queue = new BaseQueue($client, 'bad-queue-name', $queueUrl, new BasicWorker(), []); 531 | $this->expectException(\InvalidArgumentException::class); 532 | $queue->purge(); 533 | } 534 | } 535 | -------------------------------------------------------------------------------- /Tests/Unit/Service/MessageTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Message::class, $message); 26 | $this->assertEquals($body, $message->getBody()); 27 | $this->assertEquals($attributes, $message->getAttributes()); 28 | $this->assertEquals($groupId, $message->getGroupId()); 29 | $this->assertEquals($deduplicationID, $message->getDeduplicationId()); 30 | } 31 | 32 | /** 33 | * Test: Getter/Setter 34 | */ 35 | public function testGetterSetter() 36 | { 37 | $message = new Message('', [], '', ''); 38 | 39 | $this->assertInstanceOf(Message::class, $message->setId('my-id')); 40 | $this->assertEquals('my-id', $message->getId()); 41 | $this->assertInstanceOf(Message::class, $message->setBody('my-body')); 42 | $this->assertEquals('my-body', $message->getBody()); 43 | $this->assertInstanceOf(Message::class, $message->setAttributes(['a', 'b', 'c'])); 44 | $this->assertEquals(['a', 'b', 'c'], $message->getAttributes()); 45 | $this->assertInstanceOf(Message::class, $message->setReceiptHandle('my-receipt')); 46 | $this->assertEquals('my-receipt', $message->getReceiptHandle()); 47 | $this->assertInstanceOf(Message::class, $message->setGroupId('group-id')); 48 | $this->assertEquals('group-id', $message->getGroupId()); 49 | $this->assertInstanceOf(Message::class, $message->setDeduplicationId('deduplication-id')); 50 | $this->assertEquals('deduplication-id', $message->getDeduplicationId()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/Unit/Service/QueueFactoryTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(SqsClient::class) 24 | ->disableOriginalConstructor() 25 | ->getMock(); 26 | 27 | return $client; 28 | } 29 | 30 | /** 31 | * Test: Construction 32 | */ 33 | public function testConstruction() 34 | { 35 | $client = $this->getAwsClient(); 36 | $factory = new QueueFactory($client); 37 | 38 | $this->assertInstanceOf(QueueFactory::class, $factory); 39 | $this->assertEquals($client, $factory->getClient()); 40 | } 41 | 42 | /** 43 | * Test: Create a new queue via Factory 44 | */ 45 | public function testCreate() 46 | { 47 | $client = $this->getAwsClient(); 48 | $worker = new BasicWorker(); 49 | $factory = new QueueFactory($client); 50 | 51 | $queue = $factory->create('queue-name', 'queue-url', $worker); 52 | $this->assertInstanceOf(BaseQueue::class, $queue); 53 | 54 | // Cached? 55 | $queue2 = $factory->create('queue-name', 'queue-url', $worker); 56 | $this->assertEquals($queue, $queue2); 57 | } 58 | 59 | /** 60 | * Test: Create a new queue via Factory 61 | */ 62 | public function testCreateQueue() 63 | { 64 | $client = $this->getAwsClient(); 65 | $this->assertInstanceOf( 66 | BaseQueue::class, 67 | QueueFactory::createQueue($client, 'queue-name', 'queue-url', new BasicWorker()) 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/Unit/Service/QueueManagerTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(Result::class)->getMock(); 27 | $result->expects($this->any()) 28 | ->method('count') 29 | ->willReturn(count($entries)); 30 | $result->expects($this->any()) 31 | ->method('get') 32 | ->withAnyParameters() 33 | ->willReturnCallback(function ($arg) use ($entries) { 34 | return $entries[$arg] ?? []; 35 | }); 36 | 37 | return $result; 38 | } 39 | 40 | /** 41 | * @return \PHPUnit_Framework_MockObject_MockObject|SqsClient 42 | */ 43 | private function getAwsClient() 44 | { 45 | /** @var \PHPUnit_Framework_MockObject_MockObject|SqsClient $client */ 46 | $client = $this->getMockBuilder(SqsClient::class) 47 | ->disableOriginalConstructor() 48 | ->setMethods(['listQueues', 'createQueue', 'deleteQueue', 'setQueueAttributes', 'getQueueAttributes']) 49 | ->getMock(); 50 | 51 | return $client; 52 | } 53 | 54 | /** 55 | * Test Construction 56 | */ 57 | public function testConstruction() 58 | { 59 | $client = $this->getAwsClient(); 60 | $manager = new QueueManager($client); 61 | 62 | $this->assertInstanceOf(QueueManager::class, $manager); 63 | $this->assertEquals($client, $manager->getClient()); 64 | } 65 | 66 | /** 67 | * Test: get the list of queue 68 | */ 69 | public function testListQueue() 70 | { 71 | $expected = ['queue-url-1', 'queue-url-2']; 72 | 73 | $client = $this->getAwsClient(); 74 | $client->expects($this->any()) 75 | ->method('listQueues') 76 | ->with(['QueueNamePrefix' => 'queue-prefix']) 77 | ->willReturn($this->getAwsResult(['QueueUrls' => $expected])); 78 | 79 | $manager = new QueueManager($client); 80 | $this->assertEquals($expected, $manager->listQueue('queue-prefix')); 81 | } 82 | 83 | /** 84 | * Test: get the list of queue in failure 85 | */ 86 | public function testListQueueFailure() 87 | { 88 | $client = $this->getAwsClient(); 89 | $client->expects($this->any()) 90 | ->method('listQueues') 91 | ->with(['QueueNamePrefix' => 'bad-queue-prefix']) 92 | ->willThrowException(new AwsException( 93 | 'AWS Client Exception', 94 | new Command('list-queue-command') 95 | )); 96 | 97 | $manager = new QueueManager($client); 98 | $this->expectException(\InvalidArgumentException::class); 99 | $manager->listQueue('bad-queue-prefix'); 100 | } 101 | 102 | /** 103 | * Test: try to create a new queue 104 | */ 105 | public function testCreateQueue() 106 | { 107 | $client = $this->getAwsClient(); 108 | $client->expects($this->any()) 109 | ->method('createQueue') 110 | ->with([ 111 | 'Attributes' => QueueManager::QUEUE_ATTR_DEFAULT, 112 | 'QueueName' => 'queue-url' 113 | ]) 114 | ->willReturn($this->getAwsResult(['QueueUrl' => 'queue-url'])); 115 | 116 | $manager = new QueueManager($client); 117 | $this->assertEquals('queue-url', $manager->createQueue('queue-url', QueueManager::QUEUE_ATTR_DEFAULT)); 118 | } 119 | 120 | /** 121 | * Test: try to create a new fifo queue 122 | */ 123 | public function testCreateFifoQueue() 124 | { 125 | $queueAttr = array_merge(QueueManager::QUEUE_ATTR_DEFAULT, QueueManager::QUEUE_FIFO_ATTR_DEFAULT); 126 | $queueName = 'queue-url.fifo'; 127 | 128 | $client = $this->getAwsClient(); 129 | $client->expects($this->any()) 130 | ->method('createQueue') 131 | ->with([ 132 | 'Attributes' => $queueAttr, 133 | 'QueueName' => $queueName 134 | ]) 135 | ->willReturn($this->getAwsResult(['QueueUrl' => 'queue-url'])); 136 | 137 | $manager = new QueueManager($client); 138 | $this->assertEquals('queue-url', $manager->createQueue($queueName, $queueAttr)); 139 | } 140 | 141 | /** 142 | * Test: try to create a new queue in failure 143 | */ 144 | public function testCreateFailure() 145 | { 146 | $client = $this->getAwsClient(); 147 | $client->expects($this->any()) 148 | ->method('createQueue') 149 | ->with([ 150 | 'Attributes' => QueueManager::QUEUE_ATTR_DEFAULT, 151 | 'QueueName' => 'bad-queue-url' 152 | ]) 153 | ->willThrowException(new AwsException( 154 | 'AWS Client Exception', 155 | new Command('create-queue-command') 156 | )); 157 | 158 | $manager = new QueueManager($client); 159 | $this->expectException(\InvalidArgumentException::class); 160 | $manager->createQueue('bad-queue-url', QueueManager::QUEUE_ATTR_DEFAULT); 161 | } 162 | 163 | /** 164 | * Test: try to delete a new queue 165 | */ 166 | public function testDeleteQueue() 167 | { 168 | $client = $this->getAwsClient(); 169 | $client->expects($this->any()) 170 | ->method('deleteQueue') 171 | ->with(['QueueUrl' => 'queue-url']) 172 | ->willReturn($this->getAwsResult([])); 173 | 174 | $manager = new QueueManager($client); 175 | $this->assertTrue($manager->deleteQueue('queue-url')); 176 | } 177 | 178 | /** 179 | * Test: try to delete a new queue in failure 180 | */ 181 | public function testDeleteFailure() 182 | { 183 | $client = $this->getAwsClient(); 184 | $client->expects($this->any()) 185 | ->method('deleteQueue') 186 | ->with(['QueueUrl' => 'bad-queue-url']) 187 | ->willThrowException(new AwsException( 188 | 'AWS Client Exception', 189 | new Command('delete-queue-command') 190 | )); 191 | 192 | $manager = new QueueManager($client); 193 | $this->expectException(\InvalidArgumentException::class); 194 | $manager->deleteQueue('bad-queue-url'); 195 | } 196 | 197 | /** 198 | * Test: Queue Attributes Setter/Getter 199 | */ 200 | public function testQueueAttributesSetterGetter() 201 | { 202 | $attributes = [ 203 | 'DelaySeconds' => rand(0, 100), 204 | 'MaximumMessageSize' => rand(0, 100), 205 | 'MessageRetentionPeriod' => rand(0, 100), 206 | 'ReceiveMessageWaitTimeSeconds' => rand(0, 100), 207 | 'VisibilityTimeout' => rand(0, 100) 208 | ]; 209 | 210 | $client = $this->getAwsClient(); 211 | $client->expects($this->any()) 212 | ->method('setQueueAttributes') 213 | ->with([ 214 | 'Attributes' => $attributes, 215 | 'QueueUrl' => 'queue-url' 216 | ]) 217 | ->willReturn($this->getAwsResult([])); 218 | 219 | $client->expects($this->any()) 220 | ->method('getQueueAttributes') 221 | ->with([ 222 | 'AttributeNames' => ['All'], 223 | 'QueueUrl' => 'queue-url' 224 | ]) 225 | ->willReturn($this->getAwsResult(['Attributes' => $attributes])); 226 | 227 | $manager = new QueueManager($client); 228 | $this->assertTrue($manager->setQueueAttributes('queue-url', $attributes)); 229 | $this->assertEquals($attributes, $manager->getQueueAttributes('queue-url')); 230 | } 231 | 232 | /** 233 | * Test: Queue Attributes Setter in failure 234 | */ 235 | public function testQueueAttributesSetterFailure() 236 | { 237 | $client = $this->getAwsClient(); 238 | $client->expects($this->any()) 239 | ->method('setQueueAttributes') 240 | ->withAnyParameters() 241 | ->willThrowException(new AwsException( 242 | 'AWS Client Exception', 243 | new Command('delete-queue-command') 244 | )); 245 | 246 | $manager = new QueueManager($client); 247 | $this->expectException(\InvalidArgumentException::class); 248 | $manager->setQueueAttributes('bad-queue-url', []); 249 | } 250 | 251 | /** 252 | * Test: Queue Attributes Getter in failure 253 | */ 254 | public function testQueueAttributesGetterFailure() 255 | { 256 | $client = $this->getAwsClient(); 257 | $client->expects($this->any()) 258 | ->method('getQueueAttributes') 259 | ->withAnyParameters() 260 | ->willThrowException(new AwsException( 261 | 'AWS Client Exception', 262 | new Command('delete-queue-command') 263 | )); 264 | 265 | $manager = new QueueManager($client); 266 | $this->expectException(\InvalidArgumentException::class); 267 | $manager->getQueueAttributes('bad-queue-url'); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /Tests/Unit/Service/Worker/AbstractWorkerTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(AbstractWorker::class) 22 | ->setMethods(['preExecute', 'postExecute', 'onSucceeded', 'onFailed']) 23 | ->getMockForAbstractClass(); 24 | 25 | return $worker; 26 | } 27 | 28 | /** 29 | * Test: Main processing of each worker with a ping pong message 30 | */ 31 | public function testProcessWithPingPong() 32 | { 33 | $worker = $this->getAbstractWorker(); 34 | 35 | $message = (new Message())->setBody('ping'); 36 | $expect = 'Pong. Now is ' . (new \DateTime('now'))->format('M d, Y H:i:s') . PHP_EOL; 37 | 38 | $this->expectOutputString($expect); 39 | $result = $worker->process($message); 40 | $this->assertTrue($result); 41 | } 42 | 43 | /** 44 | * Test: Main processing of each worker with a ping pong message 45 | */ 46 | public function testProcessWithNormalMessage() 47 | { 48 | $message = (new Message())->setBody('my-message'); 49 | 50 | $worker = $this->getAbstractWorker(); 51 | $worker->expects($this->once()) 52 | ->method('execute') 53 | ->with($message) 54 | ->willReturn(true); 55 | $worker->expects($this->once())->method(('preExecute'))->with($message); 56 | $worker->expects($this->once())->method(('postExecute'))->with($message); 57 | $worker->expects($this->once())->method('onSucceeded'); 58 | $worker->expects($this->never())->method('onFailed'); 59 | 60 | $result = $worker->process($message); 61 | 62 | $this->assertTrue($result); 63 | $this->assertFalse($worker->hasError()); 64 | $this->assertNull($worker->error()); 65 | } 66 | 67 | /** 68 | * Test: Main processing of each worker in failure 69 | */ 70 | public function testProcessInFailure() 71 | { 72 | $message = (new Message())->setBody('my-message'); 73 | 74 | $worker = $this->getAbstractWorker(); 75 | $worker->expects($this->once()) 76 | ->method('execute') 77 | ->with($message) 78 | ->willThrowException(new \Exception()); 79 | $worker->expects($this->once())->method(('preExecute'))->with($message); 80 | $worker->expects($this->once())->method(('postExecute'))->with($message); 81 | $worker->expects($this->once())->method('onFailed'); 82 | $worker->expects($this->never())->method('onSucceeded'); 83 | 84 | $result = $worker->process($message); 85 | 86 | $this->assertFalse($result); 87 | $this->assertTrue($worker->hasError()); 88 | $this->assertNotNull($worker->error()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Tests/app/AppKernel.php: -------------------------------------------------------------------------------- 1 | load(__DIR__ . '/config/config_' . $this->getEnvironment() . '.yml'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/app/KernelTestCase.php: -------------------------------------------------------------------------------- 1 | cont = $container; 30 | } 31 | 32 | /** 33 | * Returns service container. 34 | * 35 | * @param bool $reinitialize Force kernel reinitialization. 36 | * @param array $kernelOptions Options used passed to kernel if it needs to be initialized. 37 | * 38 | * @return ContainerInterface 39 | */ 40 | protected function getContainer($reinitialize = false, array $kernelOptions = []) 41 | { 42 | if ($this->cont === null || $reinitialize) { 43 | static::bootKernel($kernelOptions); 44 | $this->cont = static::$kernel->getContainer(); 45 | } 46 | 47 | return $this->cont; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | protected static function createKernel(array $options = []) 54 | { 55 | $kernel = new AppKernel('test', true); 56 | $kernel->boot(); 57 | 58 | return $kernel; 59 | } 60 | 61 | /** 62 | * @param Command $command 63 | * 64 | * @return CommandTester 65 | */ 66 | public function createCommandTester(Command $command) 67 | { 68 | $application = new Application(static::$kernel); 69 | $application->add($command); 70 | 71 | $commandTester = new CommandTester($application->find($command->getName())); 72 | 73 | return $commandTester; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/app/Worker/BasicWorker.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new SQSQueuePass()); 24 | } 25 | 26 | /** 27 | * @inheritdoc 28 | */ 29 | public function getContainerExtension() 30 | { 31 | if (null === $this->extension) { 32 | $this->extension = new TriTranSqsQueueExtension(); 33 | } 34 | 35 | return $this->extension; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tritran/sqs-queue-bundle", 3 | "description": "Simple SQS Queue for Symfony", 4 | "type": "symfony-bundle", 5 | "homepage": "http://www.ideason.vn", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Tri D. Tran", 10 | "homepage": "http://www.ideason.vn" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.0.0", 15 | "ext-json": "*", 16 | "symfony/dependency-injection": "~2.7|~3.3|~4.0", 17 | "symfony/config": "~2.8|~3.3|~4.0", 18 | "symfony/console": "~2.7|~3.3|~4.0", 19 | "aws/aws-sdk-php-symfony": "~2.0" 20 | }, 21 | "require-dev": { 22 | "symfony/framework-bundle": "~2.3|~3.0|~4.0", 23 | "symfony/yaml": "~2.3|~3.0|~4.0", 24 | "phpunit/phpunit": "6.0.*", 25 | "phpmd/phpmd": "2.6.*", 26 | "squizlabs/php_codesniffer": "^3.0" 27 | }, 28 | "suggest": { 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "TriTran\\SqsQueueBundle\\": "" 33 | }, 34 | "exclude-from-classmap": [ 35 | "/Tests/app/cache/", 36 | "/Tests/app/logs/" 37 | ] 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "TriTran\\SqsQueueBundle\\": "Tests/" 42 | }, 43 | "classmap": [ 44 | "Tests/app" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | ./Tests/Unit/ 17 | 18 | 19 | ./Tests/Functional/ 20 | 21 | 22 | ./Tests/ 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ./ 33 | 34 | ./Tests 35 | ./vendor 36 | 37 | 38 | 39 | 40 | 41 | --------------------------------------------------------------------------------