├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── docs ├── Makefile ├── aws-provider.rst ├── conf.py ├── configuration.rst ├── console-commands.rst ├── custom-provider.rst ├── file-provider.rst ├── index.rst ├── installation.rst ├── iron-mq-provider.rst ├── sync-provider.rst └── usage.rst ├── integration_tests └── Provider │ └── IronMqProviderTest.php ├── phpunit.xml.dist ├── src ├── Command │ ├── QueueBuildCommand.php │ ├── QueueDestroyCommand.php │ ├── QueuePublishCommand.php │ └── QueueReceiveCommand.php ├── DependencyInjection │ ├── Configuration.php │ └── UecodeQPushExtension.php ├── Entity │ └── DoctrineMessage.php ├── Event │ ├── Events.php │ ├── MessageEvent.php │ └── NotificationEvent.php ├── EventListener │ └── RequestListener.php ├── Message │ ├── Message.php │ └── Notification.php ├── Provider │ ├── AbstractProvider.php │ ├── AwsProvider.php │ ├── CustomProvider.php │ ├── DoctrineProvider.php │ ├── FileProvider.php │ ├── IronMqProvider.php │ ├── ProviderInterface.php │ ├── ProviderRegistry.php │ └── SyncProvider.php ├── Resources │ └── config │ │ ├── config.yml │ │ ├── parameters.yml │ │ └── services.yml └── UecodeQPushBundle.php └── tests ├── DependencyInjection └── UecodeQPushExtensionTest.php ├── Event ├── EventsTest.php ├── MessageEventTest.php └── NotificationEventTest.php ├── EventListener └── RequestListenerTest.php ├── Fixtures └── config_test.yml ├── Message ├── BaseMessageTest.php ├── MessageTest.php └── NotificationTest.php ├── MockClient ├── AwsMockClient.php ├── CustomMockClient.php ├── IronMqMockClient.php ├── SnsMockClient.php └── SqsMockClient.php ├── Provider ├── AbstractProviderTest.php ├── AwsProviderTest.php ├── CustomProviderTest.php ├── FileProviderTest.php ├── IronMqProviderTest.php ├── ProviderRegisteryTest.php ├── SyncProviderTest.php └── TestProvider.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | /docs/_build 4 | /coverage 5 | .idea 6 | /nbproject 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer/cache/files 8 | 9 | php: 10 | - hhvm 11 | 12 | matrix: 13 | fast_finish: true 14 | include: 15 | - php: 5.6 16 | env: SYMFONY_VERSION=2.7.* 17 | - php: 5.6 18 | env: SYMFONY_VERSION=2.8.* 19 | - php: 5.6 20 | env: SYMFONY_VERSION=3.4.* 21 | - php: 7.0 22 | env: COVERAGE=yes 23 | allow_failures: 24 | - php: hhvm 25 | 26 | before_install: 27 | - if [ "$COVERAGE" != "yes" -a "$TRAVIS_PHP_VERSION" != "hhvm" ]; then phpenv config-rm xdebug.ini; fi 28 | - composer self-update 29 | - if [ "$SYOMFONY_VERSION" != "" ]; then composer require symfony/framework-bundle:${SYMFONY_VERSION} --no-update; fi 30 | 31 | install: 32 | - composer install --prefer-dist 33 | 34 | script: 35 | - if [ "$COVERAGE" = "yes" ]; then ./vendor/bin/simple-phpunit --coverage-text --coverage-clover=coverage.clover --testsuite "UecodeQPushBundle Test Suite"; else ./vendor/bin/simple-phpunit --testsuite "UecodeQPushBundle Test Suite"; fi 36 | 37 | after_script: 38 | - if [ "$COVERAGE" = "yes" ]; then wget https://scrutinizer-ci.com/ocular.phar; fi 39 | - if [ "$COVERAGE" = "yes" ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QPush - Symfony2 Push Queue Bundle 2 | ================================== 3 | 4 | [![Build Status](https://img.shields.io/travis/uecode/qpush-bundle/master.svg?style=flat-square)](https://travis-ci.org/uecode/qpush-bundle) 5 | [![Quality Score](https://img.shields.io/scrutinizer/g/uecode/qpush-bundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/uecode/qpush-bundle/) 6 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/uecode/qpush-bundle.svg?style=flat-square)](https://scrutinizer-ci.com/g/uecode/qpush-bundle/) 7 | [![Total Downloads](http://img.shields.io/packagist/dt/uecode/qpush-bundle.svg?style=flat-square)](https://packagist.org/packages/uecode/qpush-bundle) 8 | 9 | ## Overview 10 | This bundle allows you to easily consume messages from Push Queues by simply 11 | tagging your services and relying on Symfony's event dispatcher - without 12 | needing to run a daemon or background process to continuously poll your queue. 13 | 14 | **Full Documentation:** [qpush-bundle.readthedocs.org](http://qpush-bundle.rtfd.org) 15 | 16 | ## Installation 17 | 18 | #### The bundle should be installed through composer. 19 | 20 | ``` 21 | composer require uecode/qpush-bundle 22 | ``` 23 | 24 | #### Update AppKernel.php of your Symfony Application 25 | 26 | Add the `UecodeQPushBundle` to your kernel bootstrap sequence, in the `$bundles` 27 | array. 28 | 29 | ```php 30 | public function registerBundles() 31 | { 32 | $bundles = array( 33 | // ... 34 | new Uecode\Bundle\QPushBundle\UecodeQPushBundle(), 35 | ); 36 | 37 | return $bundles; 38 | } 39 | ``` 40 | 41 | ## Basic Configuration: 42 | 43 | Here is a basic configuration that would create a push queue called 44 | `my_queue_name` using AWS or IronMQ. You can read about the supported providers 45 | and provider options in the [full documentation](http://qpush-bundle.rtfd.org). 46 | 47 | ###### Example 48 | 49 | ```yaml 50 | #app/config.yml 51 | 52 | uecode_qpush: 53 | providers: 54 | ironmq: 55 | token: YOUR_IRON_MQ_TOKEN_HERE 56 | project_id: YOUR_IRON_MQ_PROJECT_ID_HERE 57 | aws: 58 | key: YOUR_AWS_KEY_HERE 59 | secret: YOUR_AWS_SECRET_HERE 60 | region: YOUR_AWS_REGION_HERE 61 | queues: 62 | my_queue_key: 63 | provider: ironmq #or aws 64 | options: 65 | queue_name: my_queue_name #optional. the queue name used on the provider 66 | push_notifications: true 67 | subscribers: 68 | - { endpoint: http://example.com/qpush, protocol: http } 69 | ``` 70 | 71 | You may exclude aws key and secret to default to IAM role on the EC2 machine. 72 | 73 | ## Publishing messages to your Queue 74 | 75 | Publishing messages is simple - fetch the registered Provider service from the 76 | container and call the `publish` method on the respective queue. 77 | 78 | This bundle stores your messages as a json object and the publish method expects 79 | an array, typically associative. 80 | 81 | ###### Example 82 | 83 | ```php 84 | // src/My/Bundle/ExampleBundle/Controller/MyController.php 85 | 86 | public function publishAction() 87 | { 88 | $message = ['foo' => 'bar']; 89 | 90 | // fetch your provider service from the container 91 | $this->get('uecode_qpush')->get('my_queue_key')->publish($message); 92 | 93 | // you can also fetch it directly 94 | $this->get('uecode_qpush.my_queue_key')->publish($message); 95 | } 96 | 97 | ``` 98 | 99 | ## Working with messages from your Queue 100 | 101 | When a message hits your application, this bundle will dispatch a `MessageEvent` 102 | which can be handled by your services. You need to tag your services to handle 103 | these events. 104 | 105 | ###### Example 106 | ```yaml 107 | services: 108 | my_example_service: 109 | class: My\Bundle\ExampleBundle\Service\ExampleService 110 | tags: 111 | - { name: uecode_qpush.event_listener, event: my_queue_key.message_received, method: onMessageReceived } 112 | ``` 113 | 114 | ###### Example 115 | ```php 116 | // src/My/Bundle/ExampleBundle/Service/ExampleService.php 117 | 118 | use Uecode\Bundle\QPushBundle\Event\MessageEvent; 119 | 120 | public function onMessageReceived(MessageEvent $event) 121 | { 122 | $queue_name = $event->getQueueName(); 123 | $message = $event->getMessage(); 124 | 125 | // do some processing 126 | } 127 | ``` 128 | 129 | The `Message` objects contain the provider specific message id, a message body, 130 | and a collection of provider specific metadata. 131 | 132 | These properties are accessible through simple getters from the message object. 133 | 134 | ###### Example 135 | ```php 136 | // src/My/Bundle/ExampleBundle/Service/ExampleService.php 137 | 138 | use Uecode\Bundle\QPushBundle\Event\MessageEvent; 139 | use Uecode\Bundle\QPushBundle\Message\Message; 140 | 141 | public function onMessageReceived(MessageEvent $event) 142 | { 143 | $id = $event->getMessage()->getId(); 144 | $body = $event->getMessage()->getBody(); 145 | $metadata = $event->getMessage()->getMetadata(); 146 | 147 | // do some processing 148 | } 149 | ``` 150 | 151 | ### Cleaning up the Queue 152 | 153 | Once all other Event Listeners have been invoked on a `MessageEvent`, the Bundle 154 | will automatically attempt to remove the Message from your Queue for you. 155 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uecode/qpush-bundle", 3 | "type": "library", 4 | "description": "Asynchronous processing for Symfony using Push Queues", 5 | "keywords": ["qpush", "pub-sub", "aws", "iron mq", "asynch", "push", "symfony", "bundle" ], 6 | "homepage": "https://github.com/uecode/qpush-bundle", 7 | "license": "Apache-2.0", 8 | "authors": [ 9 | { 10 | "name": "Keith Kirk", 11 | "email": "kkirk@undergroundelephant.com", 12 | "role": "developer", 13 | "homepage": "http://undergroundelephant.com" 14 | } 15 | ], 16 | "support": { 17 | "email": "kkirk@undergroundelephant.com" 18 | }, 19 | "require": { 20 | "php": ">=5.6.0", 21 | "doctrine/common": "~2.4", 22 | "symfony/dependency-injection": "~2.3|^3.0|^4.0", 23 | "symfony/config": "~2.3|^3.0|^4.0", 24 | "symfony/http-kernel": "~2.3|^3.0|^4.0", 25 | "symfony/console": "~2.3|^3.0|^4.0", 26 | "symfony/monolog-bundle": "~2.3|^3.0|^4.0" 27 | }, 28 | "require-dev": { 29 | "aws/aws-sdk-php": "~2.5", 30 | "iron-io/iron_mq": "^4.0", 31 | "symfony/finder": "~2.3|^3.0|^4.0", 32 | "symfony/filesystem": "~2.3|^3.0|^4.0", 33 | "symfony/phpunit-bridge": "^4.0", 34 | "symfony/yaml": "~2.8|^3.0|^4.0", 35 | "doctrine/orm": "^2.4.8", 36 | "stof/doctrine-extensions-bundle": "^1.2" 37 | }, 38 | "suggest": { 39 | "aws/aws-sdk-php": "Required to use AWS as a Queue Provider", 40 | "iron-io/iron_mq": "Required to use IronMQ as a Queue Provider", 41 | "symfony/finder": "Required to use File as a Queue Provider", 42 | "symfony/filesystem": "Required to use File as a Queue Provider", 43 | "doctrine/orm": "Required to use Doctrine as a Queue Provider", 44 | "stof/doctrine-extensions-bundle": "Required to use Doctrine as a Queue Provider" 45 | }, 46 | "autoload": { 47 | "psr-4": { 48 | "Uecode\\Bundle\\QPushBundle\\": "src/" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/QPushBundle.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/QPushBundle.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/QPushBundle" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/QPushBundle" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/aws-provider.rst: -------------------------------------------------------------------------------- 1 | AWS Provider 2 | ------------ 3 | 4 | The AWS Provider uses SQS & SNS to create a Push Queue model. SNS is optional with 5 | this provider and its possible to use just SQS by utilizing the provided Console 6 | Command (``uecode:qpush:receive``) to poll the queue. 7 | 8 | Configuration 9 | ^^^^^^^^^^^^^ 10 | 11 | This provider relies on the `AWS SDK PHP `_ library, which 12 | needs to be required in your ``composer.json`` file. 13 | 14 | This bundle will support both v2 and v3 of the AWS SDK. 15 | 16 | .. code-block:: js 17 | 18 | { 19 | require: { 20 | "aws/aws-sdk-php": : "2.*" #OR "3.*" 21 | } 22 | } 23 | 24 | From there, the rest of the configuration is simple. You need to provide your 25 | credentials in your configuration. 26 | 27 | .. code-block:: yaml 28 | 29 | #app/config.yml 30 | 31 | uecode_qpush: 32 | providers: 33 | my_provider: 34 | driver: aws 35 | key: 36 | secret: 37 | region: us-east-1 38 | queues: 39 | my_queue_name: 40 | provider: my_provider 41 | options: 42 | push_notifications: true 43 | subscribers: 44 | - { endpoint: http://example.com/qpush, protocol: http } 45 | 46 | You may exclude the aws key and secret if you are using IAM role in EC2. 47 | 48 | Using SNS 49 | ^^^^^^^^^ 50 | 51 | If you set ``push_notifications`` to ``true`` in your queue config, this provider 52 | will automatically create the SNS Topic, subscribe your SQS queue to it, as well 53 | as loop over your list of ``subscribers``, adding them to your Topic. 54 | 55 | This provider automatically handles Subscription Confirmations sent from SNS, as 56 | long as the HTTP endpoint you've listed is externally accessible and has the QPush Bundle 57 | properly installed and configured. 58 | 59 | Overriding Queue Options 60 | ^^^^^^^^^^^^^^^^^^^^^^^^ 61 | 62 | It's possible to override the default queue options that are set in your config file 63 | when sending or receiving messages. 64 | 65 | **Publishing** 66 | 67 | The ``publish()`` method takes an array as a second argument. For the AWS Provider 68 | you are able to change the options listed below per publish. 69 | 70 | If you disable ``push_notifications`` for a message, it will skip using SNS and 71 | only write the message to SQS. You will need to manually poll the SQS queue to 72 | fetch those messages. 73 | 74 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 75 | | Option | Description | Default Value | 76 | +==========================+===========================================================================================+===============+ 77 | | ``push_notifications`` | Whether or not to POST notifications to subscribers of a Queue | ``false`` | 78 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 79 | | ``message_delay`` | Time in seconds before a published Message is available to be read in a Queue | ``0`` | 80 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 81 | 82 | .. code-block:: php 83 | 84 | $message = ['foo' => 'bar']; 85 | 86 | // Optional config to override default options 87 | $options = [ 88 | 'push_notifications' => 0, 89 | 'message_delay' => 1 90 | ]; 91 | 92 | $this->get('uecode_qpush.my_queue_name')->publish($message, $options); 93 | 94 | 95 | **Receiving** 96 | 97 | The ``receive()`` method takes an array as a second argument. For the AWS Provider 98 | you are able to change the options listed below per attempt to receive messages. 99 | 100 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 101 | | Option | Description | Default Value | 102 | +==========================+===========================================================================================+===============+ 103 | | ``messages_to_receive`` | Maximum amount of messages that can be received when polling the queue | ``1`` | 104 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 105 | | ``receive_wait_time`` | If supported, time in seconds to leave the polling request open - for long polling | ``3`` | 106 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 107 | 108 | .. code-block:: php 109 | 110 | // Optional config to override default options 111 | $options = [ 112 | 'messages_to_receive' => 3, 113 | 'receive_wait_time' => 10 114 | ]; 115 | 116 | $messages = $this->get('uecode_qpush.my_queue_name')->receive($options); 117 | 118 | foreach ($messages as $message) { 119 | echo $message->getBody(); 120 | } 121 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # QPush Bundle documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Feb 22 19:40:44 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'QPush Bundle' 44 | copyright = u'2014, Keith Kirk' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '1.1.3' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '1.1.3' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'QPushBundledoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'QPushBundle.tex', u'QPush Bundle Documentation', 187 | u'Keith Kirk', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'qpushbundle', u'QPush Bundle Documentation', 217 | [u'Keith Kirk'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'QPushBundle', u'QPush Bundle Documentation', 231 | u'Keith Kirk', 'QPushBundle', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | Configure the Bundle 2 | ==================== 3 | 4 | The bundle allows you to specify different Message Queue providers - however, 5 | Amazon AWS and IronMQ are the only ones currently supported. Blocking, synchronous queues 6 | are also supported through the ``sync`` driver to aid development and debugging. 7 | 8 | We are actively looking to add more and would be more than happy to accept contributions. 9 | 10 | Providers 11 | --------- 12 | 13 | This bundle allows you to configure and use multiple supported providers with in the same 14 | application. Each queue that you create is attached to one of your registered providers 15 | and can have its own configuration options. 16 | 17 | Providers may have their own dependencies that should be added to your ``composer.json`` file. 18 | 19 | For specific instructions on how to configure each provider, please view their documents. 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | 24 | aws-provider 25 | iron-mq-provider 26 | sync-provider 27 | file-provider 28 | custom-provider 29 | 30 | Caching 31 | ------- 32 | 33 | Providers can leverage a caching layer to limit the amount of calls to the Message Queue 34 | for basic lookup functionality - this is important for things like AWS's ARN values, etc. 35 | 36 | By default the library will attempt to use file cache, however you can pass your 37 | own cache service, as long as its an instance of ``Doctrine\Common\Cache\Cache``. 38 | 39 | The configuration parameter ``cache_service`` expects the container service id of a registered 40 | Cache service. See below. 41 | 42 | .. code-block:: yaml 43 | 44 | #app/config.yml 45 | 46 | services: 47 | my_cache_service: 48 | class: My\Caching\CacheService 49 | 50 | uecode_qpush: 51 | cache_service: my_cache_service 52 | 53 | **Note:** *Though the Queue Providers will attempt to create queues if they do not exist when publishing or receiving messages, 54 | it is highly recommended that you run the included console command to build queues and warm cache from the CLI beforehand.* 55 | 56 | Queue Options 57 | ------------- 58 | 59 | Each queue can have their own options that determine how messages are published or received. 60 | The options and their descriptions are listed below. 61 | 62 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 63 | | Option | Description | Default Value | 64 | +=================================+============================================================================================+===============+ 65 | | ``queue_name`` | The name used to describe the queue on the Provider's side | ``null`` | 66 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 67 | | ``push_notifications`` | Whether or not to POST notifications to subscribers of a Queue | ``false`` | 68 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 69 | | ``notification_retries`` | How many attempts notifications are resent in case of errors - if supported | ``3`` | 70 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 71 | | ``message_delay`` | Time in seconds before a published Message is available to be read in a Queue | ``0`` | 72 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 73 | | ``message_timeout`` | Time in seconds a worker has to delete a Message before it is available to other workers | ``30`` | 74 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 75 | | ``message_expiration`` | Time in seconds that Messages may remain in the Queue before being removed | ``604800`` | 76 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 77 | | ``messages_to_receive`` | Maximum amount of messages that can be received when polling the queue | ``1`` | 78 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 79 | | ``receive_wait_time`` | If supported, time in seconds to leave the polling request open - for long polling | ``3`` | 80 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 81 | | ``fifo`` | If supported (only aws), sets queue into FIFO mode | ``false`` | 82 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 83 | | ``content_based_deduplication`` | If supported (only aws), turns on automatic deduplication id based on the message content | ``false`` | 84 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 85 | | ``subscribers`` | An array of Subscribers, containing an ``endpoint`` and ``protocol`` | ``empty`` | 86 | +---------------------------------+--------------------------------------------------------------------------------------------+---------------+ 87 | 88 | Symfony Application as a Subscriber 89 | ----------------------------------- 90 | 91 | The QPush Bundle uses a Request Listener which will capture and dispatch notifications from your queue providers for you. The specific route you use does not matter. 92 | 93 | In most cases, it is recommended to just list the host or domain for your Symfony application as the ``endpoint`` of your subscriber. You do not need to create a new action for QPush to receive messages. 94 | 95 | Logging with Monolog 96 | -------------------- 97 | 98 | By default, logging is enabled in the Qpush Bundle and uses Monolog, configured 99 | via the MonologBundle. You can toggle the logging behavior by setting 100 | ``logging_enabled`` to ``false``. 101 | 102 | Logs will output to your default Symfony environment logs using the 'qpush' channel. 103 | 104 | Example Configuration 105 | --------------------- 106 | 107 | A working configuration would look like the following 108 | 109 | .. code-block:: yaml 110 | 111 | uecode_qpush: 112 | cache_service: null 113 | logging_enabled: true 114 | providers: 115 | aws: 116 | driver: aws #optional for providers named 'aws' or 'ironmq' 117 | key: YOUR_AWS_KEY_HERE 118 | secret: YOUR_AWS_SECRET_HERE 119 | region: YOUR_AWS_REGION_HERE 120 | another_aws_provider: 121 | driver: aws #required for named providers 122 | key: YOUR_AWS_KEY_HERE 123 | secret: YOUR_AWS_SECRET_HERE 124 | region: YOUR_AWS_REGION_HERE 125 | ironmq: 126 | driver: aws #optional for providers named 'aws' or 'ironmq' 127 | token: YOUR_IRONMQ_TOKEN_HERE 128 | project_id: YOUR_IRONMQ_PROJECT_ID_HERE 129 | in_band: 130 | driver: sync 131 | custom_provider: 132 | driver: custom 133 | service: YOUR_CUSTOM_SERVICE_ID 134 | queues: 135 | my_queue_key: 136 | provider: ironmq #or aws or in_band or another_aws_provider 137 | options: 138 | queue_name: my_actual_queue_name 139 | push_notifications: true 140 | notification_retries: 3 141 | message_delay: 0 142 | message_timeout: 30 143 | message_expiration: 604800 144 | messages_to_receive: 1 145 | receive_wait_time: 3 146 | fifo: false 147 | content_based_deduplication: false 148 | subscribers: 149 | - { endpoint: http://example1.com/, protocol: http } 150 | - { endpoint: http://example2.com/, protocol: http } 151 | my_fifo_queue_key: 152 | provider: aws 153 | options: 154 | queue_name: my_actual_queue_name.fifo 155 | push_notifications: false 156 | notification_retries: 3 157 | message_delay: 0 158 | message_timeout: 30 159 | message_expiration: 604800 160 | messages_to_receive: 1 161 | receive_wait_time: 3 162 | fifo: true 163 | content_based_deduplication: true 164 | subscribers: 165 | - { endpoint: http://example1.com/, protocol: http } 166 | - { endpoint: http://example2.com/, protocol: http } 167 | 168 | Note that `FIFO` queues are not currently compatible with `push_notifications`. For more information, see: http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-subscribe-queue-sns-topic.html 169 | -------------------------------------------------------------------------------- /docs/console-commands.rst: -------------------------------------------------------------------------------- 1 | Console Commands 2 | ================ 3 | 4 | This bundle includes some Console Commands which can be used for building, destroying and polling your queues 5 | as well as sending simple messages. 6 | 7 | Build Command 8 | ------------- 9 | 10 | You can use the ``uecode:qpush:build`` command to create the queues on your providers. You can specify the name of a queue 11 | as an argument to build a single queue. This command will also warm cache which avoids the need to query the provider's API 12 | to ensure that the queue exists. Most queue providers create commands are idempotent, so running this multiple times is not an issue.:: 13 | 14 | $ php app/console uecode:qpush:build my_queue_name 15 | 16 | **Note:** *By default, this bundle uses File Cache. If you clear cache, it is highly recommended you re-run the build command to warm the cache!* 17 | 18 | Destroy Command 19 | --------------- 20 | 21 | You can use the ``uecode:qpush:destroy`` command to completely remove queues. You can specify the name of a queue as an argument to destroy 22 | a single queue. If you do not specify an argument, this will destroy all queues after confirmation.:: 23 | 24 | $ php app/console uecode:qpush:destroy my_queue_name 25 | 26 | **Note:** *This will remove queues, even if there are still unreceived messages in the queue!* 27 | 28 | Receive Command 29 | --------------- 30 | 31 | You can use the ``uecode:qpush:receive`` command to poll the specified queue. This command takes the name of a queue as an argument. 32 | Messages received from this command are dispatched through the ``EventDispatcher`` and can be handled by your tagged services the same 33 | as Push Notifications would be.:: 34 | 35 | $ php app/console uecode:qpush:receive my_queue_name 36 | 37 | Publish Command 38 | --------------- 39 | 40 | You can use the ``uecode:qpush:publish`` command to send messages to your queue from the CLI. This command takes two arguments, the name of 41 | the queue and the message to publish. The message needs to be a json encoded string.:: 42 | 43 | $ php app/console uecode:qpush:publish my_queue_name '{"foo": "bar"}' 44 | -------------------------------------------------------------------------------- /docs/custom-provider.rst: -------------------------------------------------------------------------------- 1 | Custom Provider 2 | ------------- 3 | 4 | The custom provider allows you to use your own provider. When using this provider, your implementation must implement 5 | ``Uecode\Bundle\QPushBundle\Provider\ProviderInterface`` 6 | 7 | Configuration 8 | ^^^^^^^^^^^^^ 9 | 10 | To designate a queue as custom, set the ``driver`` of its provider to ``custom``, and the ``service`` to your service id. 11 | 12 | .. code-block:: yaml 13 | 14 | #app/config_dev.yml 15 | 16 | uecode_qpush: 17 | providers: 18 | custom_provider: 19 | driver: custom 20 | service: YOUR_CUSTOM_SERVICE_ID 21 | queues: 22 | my_queue_name: 23 | provider: custom_provider -------------------------------------------------------------------------------- /docs/file-provider.rst: -------------------------------------------------------------------------------- 1 | File Provider 2 | ------------- 3 | 4 | The file provider uses the filesystem to dispatch and resolve queued messages. 5 | 6 | Configuration 7 | ^^^^^^^^^^^^^ 8 | 9 | To designate a queue as file, set the ``driver`` of its provider to ``file``. You will 10 | need to configure a readable and writable path to store the messages. 11 | 12 | .. code-block:: yaml 13 | 14 | #app/config_dev.yml 15 | 16 | uecode_qpush: 17 | providers: 18 | file_based: 19 | driver: file 20 | path: [Path to store messages] 21 | queues: 22 | my_queue_name: 23 | provider: file_based -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | The QPush Bundle relies on the Push Queue model of Message Queues to provide asynchronous 5 | processing in your Symfony application. This allows you to remove blocking processes from the 6 | immediate flow of your application and delegate them to another part of your application or, say, a 7 | cluster of workers. 8 | 9 | This bundle allows you to easily consume and process messages by simply tagging your service or 10 | services and relying on Symfony's event dispatcher - without needing to run a daemon or background 11 | process to continuously poll your queue. 12 | 13 | Content 14 | ======== 15 | 16 | .. toctree:: 17 | :maxdepth: 4 18 | 19 | installation 20 | configuration 21 | usage 22 | console-commands 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | **The bundle should be installed through composer.** 5 | 6 | :: 7 | 8 | composer require uecode/qpush-bundle 9 | 10 | **Update AppKernel.php of your Symfony Application** 11 | 12 | Add the ``UecodeQPushBundle`` to your kernel bootstrap sequence, in the ``$bundles`` array 13 | 14 | .. code-block:: php 15 | 16 | public function registerBundles() 17 | { 18 | $bundles = array( 19 | // ... 20 | new Uecode\Bundle\QPushBundle\UecodeQPushBundle(), 21 | ); 22 | 23 | return $bundles; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /docs/iron-mq-provider.rst: -------------------------------------------------------------------------------- 1 | IronMQ Provider 2 | --------------- 3 | 4 | The IronMQ Provider uses its Push Queues to notify subscribers of new queued 5 | messages without needing to continually poll the queue. 6 | 7 | Using a Push Queue is optional with this provider and its possible to use simple 8 | Pull queues by utilizing the provided Console Command (``uecode:qpush::receive``) 9 | to poll the queue. 10 | 11 | Configuration 12 | ^^^^^^^^^^^^^ 13 | 14 | This provider relies on the `Iron MQ `_ classes 15 | and needs to have the library included in your ``composer.json`` file. 16 | 17 | .. code-block:: js 18 | 19 | { 20 | require: { 21 | "iron-io/iron_mq": "^4.0" 22 | } 23 | } 24 | 25 | 26 | Configuring the provider is very easy. It requires that you have already created 27 | an account and have a project id. 28 | 29 | `Iron.io `_ provides free accounts for Development, which makes 30 | testing and using this service extremely easy. 31 | 32 | Just include your OAuth `token` and `project_id` in the configuration and set your 33 | queue to use a provider using the `ironmq` driver. 34 | 35 | .. code-block:: yaml 36 | 37 | #app/config.yml 38 | 39 | uecode_qpush: 40 | providers: 41 | my_provider: 42 | driver: ironmq 43 | token: YOUR_TOKEN_HERE 44 | project_id: YOUR_PROJECT_ID_HERE 45 | host: YOUR_OPTIONAL_HOST_HERE 46 | port: YOUR_OPTIONAL_PORT_HERE 47 | version_id: YOUR_OPTIONAL_VERSION_HERE 48 | queues: 49 | my_queue_name: 50 | provider: my_provider 51 | options: 52 | push_notifications: true 53 | subscribers: 54 | - { endpoint: http://example.com/qpush, protocol: http } 55 | 56 | IronMQ Push Queues 57 | ^^^^^^^^^^^^^^^^^^ 58 | 59 | If you set ``push_notifications`` to ``true`` in your queue config, this provider 60 | will automatically create your Queue as a Push Queue and loop over your list of ``subscribers``, 61 | adding them to your Queue. 62 | 63 | This provider only supports ``http`` and ``https`` subscribers. This provider also uses the 64 | ``multicast`` setting for its Push Queues, meaning that all ``subscribers`` are notified of 65 | the same new messages. 66 | 67 | You can chose to have your IronMQ queues work as a Pull Queue by setting ``push_notifications`` to ``false``. 68 | This would require you to use the ``uecode:qpush:receive`` Console Command to poll the queue. 69 | 70 | Overriding Queue Options 71 | ^^^^^^^^^^^^^^^^^^^^^^^^ 72 | 73 | It's possible to override the default queue options that are set in your config file 74 | when sending or receiving messages. 75 | 76 | **Publishing** 77 | 78 | The ``publish()`` method takes an array as a second argument. For the IronMQ 79 | Provider you are able to change the options listed below per publish. 80 | 81 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 82 | | Option | Description | Default Value | 83 | +==========================+===========================================================================================+===============+ 84 | | ``message_delay`` | Time in seconds before a published Message is available to be read in a Queue | ``0`` | 85 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 86 | | ``message_timeout`` | Time in seconds a worker has to delete a Message before it is available to other workers | ``30`` | 87 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 88 | | ``message_expiration`` | Time in seconds that Messages may remain in the Queue before being removed | ``604800`` | 89 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 90 | 91 | .. code-block:: php 92 | 93 | $message = ['foo' => 'bar']; 94 | 95 | // Optional config to override default options 96 | $options = [ 97 | 'message_delay' => 1, 98 | 'message_timeout' => 1, 99 | 'message_expiration' => 60 100 | ]; 101 | 102 | $this->get('uecode_qpush.my_queue_name')->publish($message, $options); 103 | 104 | 105 | **Receiving** 106 | 107 | The ``receive()`` method takes an array as a second argument. For the AWS Provider 108 | you are able to change the options listed below per attempt to receive messages. 109 | 110 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 111 | | Option | Description | Default Value | 112 | +==========================+===========================================================================================+===============+ 113 | | ``messages_to_receive`` | Maximum amount of messages that can be received when polling the queue | ``1`` | 114 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 115 | | ``message_timeout`` | Time in seconds a worker has to delete a Message before it is available to other workers | ``30`` | 116 | +--------------------------+-------------------------------------------------------------------------------------------+---------------+ 117 | 118 | .. code-block:: php 119 | 120 | // Optional config to override default options 121 | $options = [ 122 | 'messages_to_receive' => 3, 123 | 'message_timeout' => 10 124 | ]; 125 | 126 | $messages = $this->get('uecode_qpush.my_queue_name')->receive($options); 127 | 128 | foreach ($messages as $message) { 129 | echo $message->getBody(); 130 | } 131 | -------------------------------------------------------------------------------- /docs/sync-provider.rst: -------------------------------------------------------------------------------- 1 | Sync Provider 2 | ------------- 3 | 4 | The sync provider immediately dispatches and resolves queued events. It is not intended 5 | for production use but instead to support local development, debugging and testing 6 | of queue-based code paths. 7 | 8 | Configuration 9 | ^^^^^^^^^^^^^ 10 | 11 | To designate a queue as synchronous, set the ``driver`` of its provider to ``sync``. No further 12 | configuration is necessary. 13 | 14 | .. code-block:: yaml 15 | 16 | #app/config_dev.yml 17 | 18 | uecode_qpush: 19 | providers: 20 | in_band: 21 | driver: sync 22 | queues: 23 | my_queue_name: 24 | provider: in_band -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Once configured, you can create messages and publish them to the queue. You may also 5 | create services that will automatically be fired as messages are pushed to your application. 6 | 7 | For your convenience, a custom ``Provider`` service will be created and registered 8 | in the Container for each of your defined Queues. The container queue service id will be 9 | in the format of ``uecode_qpush.{your queue name}``. 10 | 11 | Publishing messages to your Queue 12 | --------------------------------- 13 | 14 | Publishing messages is simple - fetch your ``Provider`` service from the container and 15 | call the ``publish`` method on the respective queue, which accepts an array. 16 | 17 | .. code-block:: php 18 | 19 | #src/My/Bundle/ExampleBundle/Controller/MyController.php 20 | 21 | public function publishAction() 22 | { 23 | $message = [ 24 | 'messages should be an array', 25 | 'they can be flat arrays' => [ 26 | 'or multidimensional' 27 | ] 28 | ]; 29 | 30 | $this->get('uecode_qpush.my_queue_name')->publish($message); 31 | } 32 | 33 | Working with messages from your Queue 34 | ------------------------------------- 35 | 36 | Messages are either automatically received by your application and events dispatched 37 | (setting ``push_notification`` to ``true``), or can be picked up by Cron jobs through an included 38 | command if you are not using a Message Queue provider that supports Push notifications. 39 | 40 | When the notifications or messages are Pushed to your application, the QPush Bundle automatically 41 | catches the request and dispatches an event which can be easily hooked into. 42 | 43 | MessageEvents 44 | ^^^^^^^^^^^^^ 45 | 46 | Once a message is received via POST from your Message Queue, a ``MessageEvent`` is dispatched 47 | which can be handled by your services. Each ``MessageEvent`` contains the name of the queue 48 | and a ``Uecode\Bundle\QPushBundle\Message\Message`` object, accessible through getters. 49 | 50 | .. code-block:: php 51 | 52 | #src/My/Bundle/ExampleBundle/Service/ExampleService.php 53 | 54 | use Uecode\Bundle\QPushBundle\Event\MessageEvent 55 | 56 | public function onMessageReceived(MessageEvent $event) 57 | { 58 | $queue_name = $event->getQueueName(); 59 | $message = $event->getMessage(); 60 | } 61 | 62 | The ``Message`` objects contain the provider specific message id, a message body, 63 | and a collection of provider specific metadata. 64 | 65 | These properties are accessible through simple getters. 66 | 67 | The message ``body`` is an array matching your original message. The ``metadata`` property is an 68 | ``ArrayCollection`` of varying fields sent with your message from your Queue Provider. 69 | 70 | .. code-block:: php 71 | 72 | #src/My/Bundle/ExampleBundle/Service/ExampleService.php 73 | 74 | use Uecode\Bundle\QPushBundle\Event\MessageEvent; 75 | use Uecode\Bundle\QPushBundle\Message\Message; 76 | 77 | public function onMessageReceived(MessageEvent $event) 78 | { 79 | $id = $event->getMessage()->getId(); 80 | $body = $event->getMessage()->getBody(); 81 | $metadata = $event->getMessage()->getMetadata(); 82 | 83 | // do some processing 84 | } 85 | 86 | Tagging Your Services 87 | ^^^^^^^^^^^^^^^^^^^^^ 88 | 89 | For your Services to be called on QPush events, they must be tagged with the name 90 | ``uecode_qpush.event_listener``. A complete tag is made up of the following properties: 91 | 92 | ============ ================================= ========================================================================================== 93 | Tag Property Example Description 94 | ============ ================================= ========================================================================================== 95 | ``name`` ``uecode_qpush.event_listener`` The Qpush Event Listener Tag 96 | ``event`` ``{queue name}.message_received`` The `message_received` event, prefixed with the Queue name 97 | ``method`` ``onMessageReceived`` A publicly accessible method on your service 98 | ``priority`` ``100`` Priority, ``1``-``100`` to control order of services. Higher priorities are called earlier 99 | ============ ================================= ========================================================================================== 100 | 101 | The ``priority`` is useful to chain services, ensuring that they fire in a certain order - the higher priorities fire earlier. 102 | 103 | Each event fired by the Qpush Bundle is prefixed with the name of your queue, ex: ``my_queue_name.message_received``. 104 | 105 | This allows you to assign services to fire only on certain queues, based on the queue name. 106 | However, you may also have multiple tags on a single service, so that one service can handle 107 | events from multiple queues. 108 | 109 | .. code-block:: yaml 110 | 111 | services: 112 | my_example_service: 113 | class: My\Example\ExampleService 114 | tags: 115 | - { name: uecode_qpush.event_listener, event: my_queue_name.message_received, method: onMessageReceived } 116 | 117 | The method listed in the tag must be publicly available in your service and should 118 | take a single argument, an instance of ``Uecode\Bundle\QPushBundle\Event\MessageEvent``. 119 | 120 | .. code-block:: php 121 | 122 | #src/My/Bundle/ExampleBundle/Service/MyService.php 123 | 124 | use Uecode\Bundle\QPushBundle\Event\MessageEvent; 125 | 126 | // ... 127 | 128 | public function onMessageReceived(MessageEvent $event) 129 | { 130 | $queueName = $event->getQueueName(); 131 | $message = $event->getMessage(); 132 | $metadata = $message()->getMetadata(); 133 | 134 | // Process ... 135 | } 136 | 137 | Cleaning Up the Queue 138 | --------------------- 139 | 140 | Once all other Event Listeners have been invoked on a ``MessageEvent``, the QPush Bundle 141 | will automatically attempt to remove the Message from your Queue for you. 142 | 143 | If an error or exception is thrown, or event propagation is stopped earlier in the chain, 144 | the Message will not be removed automatically and may be picked up by other workers. 145 | 146 | If you would like to remove the message inside your service, you can do so by calling the ``delete`` 147 | method on your provider and passing it the message ``id``. However, you must also stop 148 | the event propagation to avoid other services (including the Provider service) from firing on that 149 | ``MessageEvent``. 150 | 151 | .. code-block:: php 152 | 153 | #src/My/Bundle/ExampleBundle/Service/MyService.php 154 | 155 | use Uecode\Bundle\QPushBundle\Event\MessageEvent; 156 | 157 | // ... 158 | 159 | public function onMessageReceived(MessageEvent $event) 160 | { 161 | $id = $event->getMessage()->getId(); 162 | // Removes the message from the queue 163 | $awsProvider->delete($id); 164 | 165 | // Stops the event from propagating 166 | $event->stopPropagation(); 167 | } 168 | 169 | Push Queues in Development 170 | -------------------------- 171 | 172 | It is recommended to use your ``config_dev.yml`` file to disable the 173 | ``push_notifications`` settings on your queues. This will make the queue a simple 174 | Pull queue. You can then use the ``uecode:qpush:receive`` Console Command to receive 175 | messages from your Queue. 176 | 177 | If you need to test the Push Queue functionality from a local stack or internal 178 | machine, it's possible to use `ngrok `_ to tunnel to your development 179 | environment, so its reachable by your Queue Provider. 180 | 181 | You would need to update your `config_dev.yml` configuration to use the `ngrok` url for 182 | your subscriber(s). 183 | -------------------------------------------------------------------------------- /integration_tests/Provider/IronMqProviderTest.php: -------------------------------------------------------------------------------- 1 | client = new IronMQ([ 38 | 'token' => IRONMQ_TOKEN, 39 | 'project_id' => IRONMQ_PROJECT_ID, 40 | 'host' => IRONMQ_HOST 41 | ]); 42 | 43 | $this->provider = $this->getIronMqProvider(); 44 | } 45 | 46 | public function tearDown() 47 | { 48 | if (!is_null($this->provider)) { 49 | $this->provider->destroy(); 50 | $this->provider = null; 51 | } 52 | } 53 | 54 | private function getIronMqProvider(array $options = []) 55 | { 56 | $options = array_merge( 57 | [ 58 | 'logging_enabled' => false, 59 | 'push_notifications' => true, 60 | 'push_type' => 'multicast', 61 | 'notification_retries' => 3, 62 | 'notification_retries_delay' => 60, 63 | 'message_delay' => 0, 64 | 'message_timeout' => 30, 65 | 'message_expiration' => 604800, 66 | 'messages_to_receive' => 1, 67 | 'rate_limit' => -1, 68 | 'receive_wait_time' => 3, 69 | 'subscribers' => [ 70 | [ 'protocol' => 'http', 'endpoint' => 'http://fake.com' ] 71 | ] 72 | ], 73 | $options 74 | ); 75 | 76 | return new IronMqProvider( 77 | 'test', 78 | $options, 79 | $this->client, 80 | $this->getMock( 81 | 'Doctrine\Common\Cache\PhpFileCache', 82 | [], 83 | ['/tmp', 'qpush.ironmq.test.php'] 84 | ), 85 | $this->getMock( 86 | 'Symfony\Bridge\Monolog\Logger', 87 | [], 88 | ['qpush.test'] 89 | ) 90 | ); 91 | } 92 | 93 | public function testGetProviderReturnsTheNameOfTheProvider() 94 | { 95 | $provider = $this->provider->getProvider(); 96 | 97 | $this->assertEquals('IronMQ', $provider); 98 | } 99 | 100 | public function testCreateWillCreateAQueue() 101 | { 102 | $this->assertFalse($this->provider->queueExists()); 103 | $this->assertTrue($this->provider->create()); 104 | $this->assertTrue($this->provider->queueExists()); 105 | } 106 | 107 | public function testCreateFailsWithEmailTypeSubscriber() 108 | { 109 | $provider = $this->getIronMqProvider([ 110 | 'subscribers' => [ 111 | [ 'protocol' => 'email', 'endpoint' => 'test@foo.com' ] 112 | ] 113 | ]); 114 | 115 | $this->setExpectedException('InvalidArgumentException', 'IronMQ only supports `http` or `https` subscribers!'); 116 | $provider->create(); 117 | $this->assertTrue($this->provider->queueExists()); 118 | 119 | } 120 | 121 | public function testDestroyWillDestroyAQueue() 122 | { 123 | $this->provider->create(); 124 | $this->assertTrue($this->provider->queueExists(), 'fail1'); 125 | 126 | $this->assertTrue($this->provider->destroy(), 'fail2'); 127 | 128 | $this->assertFalse($this->provider->queueExists(), 'fail3'); 129 | 130 | } 131 | 132 | public function testPublishWillPublishAMessage() 133 | { 134 | $message = $this->provider->publish(['foo' => 'bar']); 135 | $this->assertInternalType("int", $message); 136 | } 137 | 138 | public function testReceiveWillReserveAndReturnAMessageFromTheQueue() 139 | { 140 | $this->provider = $this->getIronMqProvider(['push_notifications' => false]); 141 | $this->provider->publish(['baz' => 'bar']); 142 | 143 | $messages = $this->provider->receive(); 144 | 145 | $this->assertInternalType('array', $messages); 146 | $this->assertCount(1, $messages); 147 | $this->assertEquals(['baz' => 'bar'], $messages[0]->getBody()); 148 | } 149 | 150 | public function testDeleteAnUnreservedMessage() 151 | { 152 | $messageId = $this->provider->publish(['bat' => 'ball']); 153 | 154 | $this->assertTrue($this->provider->delete($messageId)); 155 | } 156 | 157 | public function testDeleteAReservedMessage() 158 | { 159 | $this->provider = $this->getIronMqProvider(['push_notifications' => false]); 160 | $this->provider->publish(['Hello' => 'World']); 161 | $messages = $this->provider->receive(); 162 | 163 | $this->assertTrue($this->provider->delete($messages[0]->getId())); 164 | } 165 | 166 | public function testOnNotification() 167 | { 168 | $event = new NotificationEvent( 169 | 'test', 170 | NotificationEvent::TYPE_MESSAGE, 171 | new Notification(123, "test", []) 172 | ); 173 | 174 | $this->provider->onNotification( 175 | $event, 176 | NotificationEvent::TYPE_MESSAGE, 177 | $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface') 178 | ); 179 | } 180 | 181 | public function testOnMessageReceived() 182 | { 183 | $this->provider = $this->getIronMqProvider(['push_notifications' => false]); 184 | $this->provider->destroy(); 185 | $this->provider->publish(['bob' => 'ball']); 186 | $messages = $this->provider->receive(); 187 | 188 | $this->provider->onMessageReceived(new MessageEvent( 189 | 'test', 190 | $messages[0] 191 | )); 192 | } 193 | 194 | public function testQueueInfo() 195 | { 196 | $this->provider->destroy(); 197 | $this->assertNull($this->provider->queueInfo()); 198 | 199 | $this->provider->create(); 200 | $queue = $this->provider->queueInfo(); 201 | $this->assertEquals('qpush_test', $queue->name); 202 | $this->assertEquals(IRONMQ_PROJECT_ID, $queue->project_id); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | src/ 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ./tests/ 27 | 28 | 29 | ./integration_tests/ 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Command/QueueBuildCommand.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | class QueueBuildCommand extends Command implements ContainerAwareInterface 36 | { 37 | /** 38 | * @var ContainerInterface 39 | * 40 | * @api 41 | */ 42 | protected $container; 43 | 44 | /** 45 | * Sets the Container associated with this Controller. 46 | * 47 | * @param ContainerInterface $container A ContainerInterface instance 48 | * 49 | * @api 50 | */ 51 | public function setContainer(ContainerInterface $container = null) 52 | { 53 | $this->container = $container; 54 | } 55 | 56 | protected $output; 57 | 58 | protected function configure() 59 | { 60 | $this 61 | ->setName('uecode:qpush:build') 62 | ->setDescription('Builds the configured Queues') 63 | ->addArgument( 64 | 'name', 65 | InputArgument::OPTIONAL, 66 | 'Name of a specific queue to build', 67 | null 68 | ) 69 | ; 70 | } 71 | 72 | protected function execute(InputInterface $input, OutputInterface $output) 73 | { 74 | $this->output = $output; 75 | $registry = $this->container->get('uecode_qpush'); 76 | 77 | $name = $input->getArgument('name'); 78 | 79 | if (null !== $name) { 80 | return $this->buildQueue($registry, $name); 81 | } 82 | 83 | foreach ($registry->all() as $queue) { 84 | $this->buildQueue($registry, $queue->getName()); 85 | } 86 | 87 | return 0; 88 | } 89 | 90 | private function buildQueue($registry, $name) 91 | { 92 | if (!$registry->has($name)) { 93 | return $this->output->writeln( 94 | sprintf("The [%s] queue you have specified does not exists!", $name) 95 | ); 96 | } 97 | 98 | $registry->get($name)->create(); 99 | $this->output->writeln(sprintf("The %s queue has been built successfully.", $name)); 100 | 101 | return 0; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Command/QueueDestroyCommand.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | class QueueDestroyCommand extends Command implements ContainerAwareInterface 38 | { 39 | /** 40 | * @var ContainerInterface 41 | * 42 | * @api 43 | */ 44 | protected $container; 45 | 46 | /** 47 | * Sets the Container associated with this Controller. 48 | * 49 | * @param ContainerInterface $container A ContainerInterface instance 50 | * 51 | * @api 52 | */ 53 | public function setContainer(ContainerInterface $container = null) 54 | { 55 | $this->container = $container; 56 | } 57 | 58 | protected $output; 59 | 60 | protected function configure() 61 | { 62 | $this 63 | ->setName('uecode:qpush:destroy') 64 | ->setDescription('Destroys the configured Queues and cleans Cache') 65 | ->addArgument( 66 | 'name', 67 | InputArgument::OPTIONAL, 68 | 'Name of a specific queue to destroy', 69 | null 70 | ) 71 | ->addOption( 72 | 'force', 73 | null, 74 | InputOption::VALUE_NONE, 75 | 'Set this parameter to force this action' 76 | ) 77 | ; 78 | } 79 | 80 | protected function execute(InputInterface $input, OutputInterface $output) 81 | { 82 | $this->output = $output; 83 | $registry = $this->container->get('uecode_qpush'); 84 | $questionHelper = $this->getHelperSet()->get('question'); 85 | $name = $input->getArgument('name'); 86 | 87 | if (null !== $name) { 88 | if (!$input->getOption('force')) { 89 | $question = new ConfirmationQuestion(sprintf( 90 | 'This will remove the %s queue, even if it has messages! Are you sure? ', 91 | $name 92 | )); 93 | $confirmation = $questionHelper->ask($input, $output, $question); 94 | 95 | if (!$confirmation) { 96 | return 0; 97 | } 98 | } 99 | 100 | return $this->destroyQueue($registry, $name); 101 | } 102 | 103 | if (!$input->getOption('force')) { 104 | $question = new ConfirmationQuestion('This will remove ALL queues, even if they have messages. Are you sure? '); 105 | $confirmation = $questionHelper->ask($input, $output, $question); 106 | 107 | if (!$confirmation) { 108 | return 0; 109 | } 110 | } 111 | 112 | foreach ($registry->all() as $queue) { 113 | $this->destroyQueue($registry, $queue->getName()); 114 | } 115 | 116 | return 0; 117 | } 118 | 119 | private function destroyQueue($registry, $name) 120 | { 121 | if (!$registry->has($name)) { 122 | return $this->output->writeln( 123 | sprintf("The [%s] queue you have specified does not exists!", $name) 124 | ); 125 | } 126 | 127 | $registry->get($name)->destroy(); 128 | $this->output->writeln(sprintf("The %s queue has been successfully destroyed.", $name)); 129 | 130 | return 0; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Command/QueuePublishCommand.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | class QueuePublishCommand extends Command implements ContainerAwareInterface 36 | { 37 | /** 38 | * @var ContainerInterface 39 | * 40 | * @api 41 | */ 42 | protected $container; 43 | 44 | /** 45 | * Sets the Container associated with this Controller. 46 | * 47 | * @param ContainerInterface $container A ContainerInterface instance 48 | * 49 | * @api 50 | */ 51 | public function setContainer(ContainerInterface $container = null) 52 | { 53 | $this->container = $container; 54 | } 55 | 56 | protected $output; 57 | 58 | protected function configure() 59 | { 60 | $this 61 | ->setName('uecode:qpush:publish') 62 | ->setDescription('Sends a Message to a Queue') 63 | ->addArgument( 64 | 'name', 65 | InputArgument::REQUIRED, 66 | 'Name of the Queue' 67 | ) 68 | ->addArgument( 69 | 'message', 70 | InputArgument::REQUIRED, 71 | 'A JSON encoded Message to send to the Queue' 72 | ) 73 | ; 74 | } 75 | 76 | protected function execute(InputInterface $input, OutputInterface $output) 77 | { 78 | $this->output = $output; 79 | $registry = $this->container->get('uecode_qpush'); 80 | 81 | $name = $input->getArgument('name'); 82 | $message = $input->getArgument('message'); 83 | 84 | return $this->sendMessage($registry, $name, $message); 85 | } 86 | 87 | private function sendMessage($registry, $name, $message) 88 | { 89 | if (!$registry->has($name)) { 90 | return $this->output->writeln( 91 | sprintf("The [%s] queue you have specified does not exists!", $name) 92 | ); 93 | } 94 | 95 | $registry->get($name)->publish(json_decode($message, true)); 96 | $this->output->writeln("The message has been sent."); 97 | 98 | return 0; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Command/QueueReceiveCommand.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | class QueueReceiveCommand extends Command implements ContainerAwareInterface 38 | { 39 | /** 40 | * @var ContainerInterface 41 | * 42 | * @api 43 | */ 44 | protected $container; 45 | 46 | /** 47 | * Sets the Container associated with this Controller. 48 | * 49 | * @param ContainerInterface $container A ContainerInterface instance 50 | * 51 | * @api 52 | */ 53 | public function setContainer(ContainerInterface $container = null) 54 | { 55 | $this->container = $container; 56 | } 57 | 58 | protected $output; 59 | 60 | protected function configure() 61 | { 62 | $this 63 | ->setName('uecode:qpush:receive') 64 | ->setDescription('Polls the configured Queues') 65 | ->addArgument( 66 | 'name', 67 | InputArgument::OPTIONAL, 68 | 'Name of a specific queue to poll', 69 | null 70 | ) 71 | ; 72 | } 73 | 74 | protected function execute(InputInterface $input, OutputInterface $output) 75 | { 76 | $this->output = $output; 77 | $registry = $this->container->get('uecode_qpush'); 78 | 79 | $name = $input->getArgument('name'); 80 | 81 | if (null !== $name) { 82 | return $this->pollQueue($registry, $name); 83 | } 84 | 85 | foreach ($registry->all() as $queue) { 86 | $this->pollQueue($registry, $queue->getName()); 87 | } 88 | 89 | return 0; 90 | } 91 | 92 | private function pollQueue($registry, $name) 93 | { 94 | if (!$registry->has($name)) { 95 | return $this->output->writeln( 96 | sprintf("The [%s] queue you have specified does not exists!", $name) 97 | ); 98 | } 99 | 100 | $dispatcher = $this->container->get('event_dispatcher'); 101 | $messages = $registry->get($name)->receive(); 102 | 103 | if($messages) { 104 | foreach ($messages as $message) { 105 | $messageEvent = new MessageEvent($name, $message); 106 | $dispatcher->dispatch(Events::Message($name), $messageEvent); 107 | } 108 | } 109 | 110 | $msg = "Finished polling %s Queue, %d messages fetched."; 111 | $this->output->writeln(sprintf($msg, $name, sizeof($messages))); 112 | 113 | return 0; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Entity/DoctrineMessage.php: -------------------------------------------------------------------------------- 1 | id; 91 | } 92 | 93 | /** 94 | * Set message 95 | * 96 | * @param array $message 97 | * 98 | * @return DoctrineMessage 99 | */ 100 | public function setMessage($message) 101 | { 102 | $this->message = $message; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Get message 109 | * 110 | * @return array 111 | */ 112 | public function getMessage() 113 | { 114 | return $this->message; 115 | } 116 | 117 | /** 118 | * Set queue 119 | * 120 | * @param string $queue 121 | * 122 | * @return DoctrineMessage 123 | */ 124 | public function setQueue($queue) 125 | { 126 | $this->queue = $queue; 127 | 128 | return $this; 129 | } 130 | 131 | /** 132 | * Get queue 133 | * 134 | * @return string 135 | */ 136 | public function getQueue() 137 | { 138 | return $this->queue; 139 | } 140 | 141 | /** 142 | * Set delivered 143 | * 144 | * @param boolean $delivered 145 | * 146 | * @return DoctrineMessage 147 | */ 148 | public function setDelivered($delivered) 149 | { 150 | $this->delivered = $delivered; 151 | 152 | return $this; 153 | } 154 | 155 | /** 156 | * Get delivered 157 | * 158 | * @return boolean 159 | */ 160 | public function getDelivered() 161 | { 162 | return $this->delivered; 163 | } 164 | 165 | /** 166 | * Set created 167 | * 168 | * @param \DateTime $created 169 | * 170 | * @return DoctrineMessage 171 | */ 172 | public function setCreated($created) 173 | { 174 | $this->created = $created; 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * Get created 181 | * 182 | * @return \DateTime 183 | */ 184 | public function getCreated() 185 | { 186 | return $this->created; 187 | } 188 | 189 | /** 190 | * Set updated 191 | * 192 | * @param \DateTime $updated 193 | * 194 | * @return DoctrineMessage 195 | */ 196 | public function setUpdated($updated) 197 | { 198 | $this->updated = $updated; 199 | 200 | return $this; 201 | } 202 | 203 | /** 204 | * Get updated 205 | * 206 | * @return \DateTime 207 | */ 208 | public function getUpdated() 209 | { 210 | return $this->updated; 211 | } 212 | 213 | /** 214 | * Set length 215 | * 216 | * @param integer $length 217 | * 218 | * @return DoctrineMessage 219 | */ 220 | public function setLength($length) 221 | { 222 | $this->length = $length; 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * Get length 229 | * 230 | * @return integer 231 | */ 232 | public function getLength() 233 | { 234 | return $this->length; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Event/Events.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | abstract class Events 29 | { 30 | const ON_NOTIFICATION = 'on_notification'; 31 | const ON_MESSAGE = 'message_received'; 32 | 33 | /** 34 | * @codeCoverageIgnore 35 | */ 36 | final private function __construct() { } 37 | 38 | /** 39 | * Returns a QPush Notification Event Name 40 | * 41 | * @param string $name The name of the Queue for this Event 42 | * 43 | * @return string 44 | */ 45 | public static function Notification($name) 46 | { 47 | return sprintf('%s.%s', $name, self::ON_NOTIFICATION); 48 | } 49 | 50 | /** 51 | * Returns a QPush Notification Event Name 52 | * 53 | * @param string $name The name of the Queue for this Event 54 | * 55 | * @return string 56 | */ 57 | public static function Message($name) 58 | { 59 | return sprintf('%s.%s', $name, self::ON_MESSAGE); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Event/MessageEvent.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class MessageEvent extends Event 32 | { 33 | /** 34 | * Queue name 35 | * 36 | * @var string 37 | */ 38 | protected $queueName; 39 | 40 | /** 41 | * Message 42 | * 43 | * @var mixed 44 | */ 45 | protected $message; 46 | 47 | /** 48 | * Constructor. 49 | * 50 | * @param string $queueName The queue name 51 | * @param Message $message The Message 52 | */ 53 | public function __construct($queueName, Message $message) 54 | { 55 | $this->queueName = $queueName; 56 | $this->message = $message; 57 | } 58 | 59 | /** 60 | * Return the SQS Queue Name 61 | * 62 | * @return string 63 | */ 64 | public function getQueueName() 65 | { 66 | return $this->queueName; 67 | } 68 | 69 | /** 70 | * Return the Full SQS Message 71 | * 72 | * @return Message 73 | */ 74 | public function getMessage() 75 | { 76 | return $this->message; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Event/NotificationEvent.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class NotificationEvent extends Event 32 | { 33 | /** 34 | * A Subscription Notification Type 35 | */ 36 | const TYPE_SUBSCRIPTION = 'SubscriptionNotification'; 37 | /** 38 | * A Message Notification Type 39 | */ 40 | const TYPE_MESSAGE = 'MessageNotification'; 41 | 42 | /** 43 | * Queue name 44 | * 45 | * @var string 46 | */ 47 | protected $queueName; 48 | 49 | /** 50 | * Notification Type 51 | * 52 | * @var string 53 | */ 54 | protected $type; 55 | 56 | /** 57 | * Notification 58 | * 59 | * @var array 60 | */ 61 | protected $notification; 62 | 63 | /** 64 | * Constructor 65 | * 66 | * @param string $queueName The Queue Name 67 | * @param string $type The Notification Type 68 | * @param Notification $notification The Notification 69 | */ 70 | public function __construct($queueName, $type, Notification $notification) 71 | { 72 | if (!in_array($type, [self::TYPE_SUBSCRIPTION, self::TYPE_MESSAGE])) { 73 | throw new \InvalidArgumentException( 74 | sprintf("Invalid notification type given! (%s)", $type) 75 | ); 76 | } 77 | 78 | $this->queueName = $queueName; 79 | $this->type = $type; 80 | $this->notification = $notification; 81 | } 82 | 83 | /** 84 | * Returns the Queue name 85 | * 86 | * return string 87 | */ 88 | public function getQueueName() 89 | { 90 | return $this->queueName; 91 | } 92 | 93 | /** 94 | * Returns the Notification Type 95 | * 96 | * @return string 97 | */ 98 | public function getType() 99 | { 100 | return $this->type; 101 | } 102 | 103 | /** 104 | * Returns the Notification 105 | * 106 | * return array 107 | */ 108 | public function getNotification() 109 | { 110 | return $this->notification; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/EventListener/RequestListener.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | 37 | class RequestListener { 38 | /** 39 | * Symfony Event Dispatcher 40 | * 41 | * @var EventDispatcherInterface 42 | */ 43 | private $dispatcher; 44 | 45 | /** 46 | * Constructor. 47 | * 48 | * @param EventDispatcherInterface $dispatcher A Symfony Event Dispatcher 49 | */ 50 | public function __construct(EventDispatcherInterface $dispatcher) { 51 | $this->dispatcher = $dispatcher; 52 | } 53 | 54 | /** 55 | * Kernel Request Event Handler for QPush Notifications 56 | * 57 | * @param GetResponseEvent $event The Kernel Request's GetResponseEvent 58 | */ 59 | public function onKernelRequest(GetResponseEvent $event) { 60 | if (HttpKernel::MASTER_REQUEST != $event->getRequestType()) { 61 | return; 62 | } 63 | 64 | if ($event->getRequest()->headers->has('x-amz-sns-message-type')) { 65 | $result = $this->handleSnsNotifications($event); 66 | $event->setResponse(new Response($result, 200)); 67 | } 68 | 69 | if ($event->getRequest()->headers->has('iron-message-id')) { 70 | $result = $this->handleIronMqNotifications($event); 71 | $event->setResponse(new Response($result, 200)); 72 | } 73 | } 74 | 75 | /** 76 | * Handles Messages sent from a IronMQ Push Queue 77 | * 78 | * @param GetResponseEvent $event The Kernel Request's GetResponseEvent 79 | * @return string|void 80 | */ 81 | private function handleIronMqNotifications(GetResponseEvent $event) { 82 | $headers = $event->getRequest()->headers; 83 | $messageId = $headers->get('iron-message-id'); 84 | 85 | if (null === ($message = json_decode($event->getRequest()->getContent(), true))) { 86 | throw new \InvalidArgumentException('Unable to decode JSON'); 87 | } 88 | 89 | $queue = $this->getIronMqQueueName($event, $message); 90 | $metadata = [ 91 | 'iron-subscriber-message-id' => $headers->get('iron-subscriber-message-id'), 92 | 'iron-subscriber-message-url' => $headers->get('iron-subscriber-message-url') 93 | ]; 94 | 95 | unset($message['_qpush_queue']); 96 | 97 | $notification = new Notification( 98 | $messageId, 99 | $message, 100 | $metadata 101 | ); 102 | 103 | $this->dispatcher->dispatch( 104 | Events::Notification($queue), 105 | new NotificationEvent($queue, NotificationEvent::TYPE_MESSAGE, $notification) 106 | ); 107 | 108 | return "IronMQ Notification Received."; 109 | } 110 | 111 | /** 112 | * Handles Notifications sent from AWS SNS 113 | * 114 | * @param GetResponseEvent $event The Kernel Request's GetResponseEvent 115 | * @return string 116 | */ 117 | private function handleSnsNotifications(GetResponseEvent $event) { 118 | $notification = json_decode((string) $event->getRequest()->getContent(), true); 119 | 120 | $type = $event->getRequest()->headers->get('x-amz-sns-message-type'); 121 | 122 | $metadata = [ 123 | 'Type' => $notification['Type'], 124 | 'TopicArn' => $notification['TopicArn'], 125 | 'Timestamp' => $notification['Timestamp'], 126 | ]; 127 | 128 | if ($type === 'Notification') { 129 | 130 | // We put the queue name in the Subject field 131 | $queue = $notification['Subject']; 132 | $metadata['Subject'] = $queue; 133 | 134 | $notification = new Notification( 135 | $notification['MessageId'], 136 | $notification['Message'], 137 | $metadata 138 | ); 139 | 140 | $this->dispatcher->dispatch( 141 | Events::Notification($queue), 142 | new NotificationEvent($queue, NotificationEvent::TYPE_MESSAGE, $notification) 143 | ); 144 | 145 | return "SNS Message Notification Received."; 146 | } 147 | 148 | // For subscription notifications, we need to parse the Queue from 149 | // the Topic ARN 150 | $arnParts = explode(':', $notification['TopicArn']); 151 | $last = end($arnParts); 152 | $queue = str_replace('qpush_', '', $last); 153 | 154 | // Get the token for the Subscription Confirmation 155 | $metadata['Token'] = $notification['Token']; 156 | 157 | $notification = new Notification( 158 | $notification['MessageId'], 159 | $notification['Message'], 160 | $metadata 161 | ); 162 | 163 | $this->dispatcher->dispatch( 164 | Events::Notification($queue), 165 | new NotificationEvent($queue, NotificationEvent::TYPE_SUBSCRIPTION, $notification) 166 | ); 167 | 168 | return "SNS Subscription Confirmation Received."; 169 | } 170 | 171 | /** 172 | * Get the name of the IronMq queue. 173 | * 174 | * @param GetResponseEvent $event 175 | * @param array $message 176 | * 177 | * @return string 178 | */ 179 | private function getIronMqQueueName(GetResponseEvent $event, array&$message) { 180 | if (array_key_exists('_qpush_queue', $message)) { 181 | return $message['_qpush_queue']; 182 | } else if (null !== ($subscriberUrl = $event->getRequest()->headers->get('iron-subscriber-message-url'))) { 183 | if (preg_match('#/queues/([a-z0-9_-]+)/messages/#i', $subscriberUrl, $matches)) { 184 | $queue = $matches[1]; 185 | if (substr($queue, 0, 6) == 'qpush_') { 186 | $queue = substr($queue, 6); 187 | } 188 | 189 | return $queue; 190 | } 191 | } 192 | 193 | throw new \RuntimeException('Unable to get queue name'); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Message/Message.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class Message 31 | { 32 | /** 33 | * Message Id 34 | * 35 | * @var int|string 36 | */ 37 | protected $id; 38 | 39 | /** 40 | * Message Body 41 | * 42 | * @var string|array 43 | */ 44 | protected $body; 45 | 46 | /** 47 | * Message Metadata 48 | * 49 | * @var ArrayCollection 50 | */ 51 | protected $metadata; 52 | 53 | /** 54 | * Constructor. 55 | * 56 | * Sets the Message Id, Message Body, and any Message Metadata 57 | * 58 | * @param int|string $id The Message Id 59 | * @param string|array $body The Message Message 60 | * @param array $metadata The Message Metadata 61 | */ 62 | public function __construct($id, $body, array $metadata) 63 | { 64 | $this->id = $id; 65 | $this->metadata = new ArrayCollection($metadata); 66 | 67 | $message = is_string($body) ? json_decode($body, true) : $body; 68 | if (json_last_error() !== JSON_ERROR_NONE) { 69 | $message = $body; 70 | } 71 | 72 | $this->body = $message; 73 | } 74 | 75 | /** 76 | * Returns the Message Id 77 | * 78 | * @return int|string 79 | */ 80 | public function getId() 81 | { 82 | return $this->id; 83 | } 84 | 85 | /** 86 | * Returns the Message Body 87 | * 88 | * @return string|array 89 | */ 90 | public function getBody() 91 | { 92 | return $this->body; 93 | } 94 | 95 | /** 96 | * Returns the Message Metadata 97 | * 98 | * @return ArrayCollection 99 | */ 100 | public function getMetadata() 101 | { 102 | return $this->metadata; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Message/Notification.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class Notification 31 | { 32 | /** 33 | * Notification Id 34 | * 35 | * @var int|string 36 | */ 37 | protected $id; 38 | 39 | /** 40 | * Notification Body 41 | * 42 | * @var string|array 43 | */ 44 | protected $body; 45 | 46 | /** 47 | * Notification Metadata 48 | * 49 | * @var ArrayCollection 50 | */ 51 | protected $metadata; 52 | 53 | /** 54 | * Constructor. 55 | * 56 | * Sets the Notification Id, Notification Body, and any Notification Metadata 57 | * 58 | * @param int|string $id The Notification Id 59 | * @param string|array $body The Notification Message 60 | * @param array $metadata The Notification Metadata 61 | */ 62 | public function __construct($id, $body, array $metadata) 63 | { 64 | $this->id = $id; 65 | $this->metadata = new ArrayCollection($metadata); 66 | 67 | $message = is_string($body) ? json_decode($body, true) : $body; 68 | if (json_last_error() !== JSON_ERROR_NONE) { 69 | $message = $body; 70 | } 71 | 72 | $this->body = $message; 73 | } 74 | 75 | /** 76 | * Returns the Notification Id 77 | * 78 | * @return int|string 79 | */ 80 | public function getId() 81 | { 82 | return $this->id; 83 | } 84 | 85 | /** 86 | * Returns the Notification Body 87 | * 88 | * @return string|array 89 | */ 90 | public function getBody() 91 | { 92 | return $this->body; 93 | } 94 | 95 | /** 96 | * Returns the Notification Metadata 97 | * 98 | * @return ArrayCollection 99 | */ 100 | public function getMetadata() 101 | { 102 | return $this->metadata; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Provider/AbstractProvider.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | abstract class AbstractProvider implements ProviderInterface 35 | { 36 | /** 37 | * QPush Queue Name 38 | * 39 | * @var string 40 | */ 41 | protected $name; 42 | 43 | /** 44 | * QPush Queue Options 45 | * 46 | * @var array 47 | */ 48 | protected $options; 49 | 50 | /** 51 | * Doctrine APC Cache Driver 52 | * 53 | * @var Cache 54 | */ 55 | protected $cache; 56 | 57 | /** 58 | * Monolog Logger 59 | * 60 | * @var Logger 61 | */ 62 | protected $logger; 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | public function getName() 68 | { 69 | return $this->name; 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | public function getNameWithPrefix() 76 | { 77 | if (!empty($this->options['queue_name'])) { 78 | return $this->options['queue_name']; 79 | } 80 | 81 | return sprintf("%s_%s", self::QPUSH_PREFIX, $this->name); 82 | } 83 | 84 | /** 85 | * {@inheritDoc} 86 | */ 87 | public function getOptions() 88 | { 89 | return $this->options; 90 | } 91 | 92 | /** 93 | * {@inheritDoc} 94 | */ 95 | public function getCache() 96 | { 97 | return $this->cache; 98 | } 99 | 100 | /** 101 | * {@inheritDoc} 102 | */ 103 | public function getlogger() 104 | { 105 | return $this->logger; 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | public function log($level, $message, array $context = []) 112 | { 113 | if (!$this->options['logging_enabled']) { 114 | return false; 115 | } 116 | 117 | // Add the queue name and provider to the context 118 | $context = array_merge(['queue' => $this->name, 'provider' => $this->getProvider()], $context); 119 | 120 | return $this->logger->addRecord($level, $message, $context); 121 | } 122 | 123 | /** 124 | * @param NotificationEvent $event 125 | * @param string $eventName Name of the event 126 | * @param EventDispatcherInterface $dispatcher 127 | * @return bool 128 | */ 129 | public function onNotification(NotificationEvent $event, $eventName, EventDispatcherInterface $dispatcher) 130 | { 131 | return false; 132 | } 133 | 134 | /** 135 | * @param MessageEvent $event 136 | * @return bool 137 | */ 138 | public function onMessageReceived(MessageEvent $event) 139 | { 140 | return false; 141 | } 142 | 143 | /** 144 | * Merge override options while restricting what keys are allowed 145 | * 146 | * @param array $options An array of options that override the queue defaults 147 | * 148 | * @return array 149 | */ 150 | public function mergeOptions(array $options = []) 151 | { 152 | return array_merge($this->options, array_intersect_key($options, $this->options)); 153 | } 154 | 155 | abstract public function getProvider(); 156 | 157 | abstract public function create(); 158 | 159 | abstract public function publish(array $message, array $options = []); 160 | 161 | abstract public function receive(array $options = []); 162 | 163 | abstract public function delete($id); 164 | 165 | abstract public function destroy(); 166 | } 167 | -------------------------------------------------------------------------------- /src/Provider/CustomProvider.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class CustomProvider extends AbstractProvider 32 | { 33 | /** 34 | * @type ProviderInterface 35 | */ 36 | private $client; 37 | 38 | /** 39 | * @param string $name 40 | * @param array $options 41 | * @param mixed $client 42 | * @param Cache $cache 43 | * @param Logger $logger 44 | */ 45 | public function __construct($name, array $options, $client, Cache $cache, Logger $logger) 46 | { 47 | $this->name = $name; 48 | $this->options = $options; 49 | $this->cache = $cache; 50 | $this->logger = $logger; 51 | 52 | $this->setClient($client); 53 | } 54 | 55 | /** 56 | * @param ProviderInterface $client 57 | * 58 | * @return CustomProvider 59 | */ 60 | public function setClient(ProviderInterface $client) 61 | { 62 | $this->client = $client; 63 | 64 | return $this; 65 | } 66 | 67 | public function getProvider() 68 | { 69 | return 'Custom'; 70 | } 71 | 72 | /** 73 | * Builds the configured queues 74 | * 75 | * If a Queue name is passed and configured, this method will build only that 76 | * Queue. 77 | * 78 | * All Create methods are idempotent, if the resource exists, the current ARN 79 | * will be returned 80 | * 81 | */ 82 | public function create() 83 | { 84 | return $this->client->create(); 85 | } 86 | 87 | /** 88 | * @return Boolean 89 | */ 90 | public function destroy() 91 | { 92 | return $this->client->destroy(); 93 | } 94 | 95 | /** 96 | * {@inheritDoc} 97 | * 98 | * This method will either use a SNS Topic to publish a queued message or 99 | * straight to SQS depending on the application configuration. 100 | * 101 | * @return string 102 | */ 103 | public function publish(array $message, array $options = []) 104 | { 105 | return $this->client->publish($message, $options); 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | public function receive(array $options = []) 112 | { 113 | return $this->client->receive($options); 114 | } 115 | 116 | /** 117 | * {@inheritDoc} 118 | * 119 | * @return bool 120 | */ 121 | public function delete($id) 122 | { 123 | return $this->client->delete($id); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Provider/DoctrineProvider.php: -------------------------------------------------------------------------------- 1 | name = $name; 51 | $this->options = $options; 52 | $this->cache = $cache; 53 | $this->logger = $logger; 54 | $this->em = $client; 55 | $this->repository = $this->em->getRepository(self::$entityName); 56 | } 57 | 58 | /** 59 | * Returns the name of the Queue that this Provider is for 60 | * 61 | * @return string 62 | */ 63 | public function getName() 64 | { 65 | return $this->name; 66 | } 67 | 68 | /** 69 | * Returns the Queue Provider name 70 | * 71 | * @return string 72 | */ 73 | public function getProvider() 74 | { 75 | return 'Doctrine'; 76 | } 77 | 78 | /** 79 | * Returns the Provider's Configuration Options 80 | * 81 | * @return array 82 | */ 83 | public function getOptions() 84 | { 85 | return $this->options; 86 | } 87 | 88 | /** 89 | * Returns the Cache service 90 | * 91 | * @return Cache 92 | */ 93 | public function getCache() 94 | { 95 | return $this->cache; 96 | } 97 | 98 | /** 99 | * Returns the Logger service 100 | * 101 | * @return Logger 102 | */ 103 | public function getLogger() 104 | { 105 | return $this->logger; 106 | } 107 | 108 | /** 109 | * Get repository 110 | * 111 | * @return array 112 | */ 113 | public function getRepository() 114 | { 115 | if (!$this->repository) { 116 | return; 117 | } 118 | 119 | return $this->repository; 120 | } 121 | 122 | /** 123 | * Creates the Queue 124 | * Checks to see if the underlying table has been created or not 125 | * 126 | * @return bool 127 | */ 128 | public function create() 129 | { 130 | $sm = $this->em->getConnection()->getSchemaManager(); 131 | $table = $this->em->getClassMetadata(self::$entityName)->getTableName(); 132 | 133 | return $sm->tablesExist(array($table)); 134 | } 135 | 136 | /** 137 | * Publishes a message to the Queue 138 | * 139 | * This method should return a string MessageId or Response 140 | * 141 | * @param array $message The message to queue 142 | * @param array $options An array of options that override the queue defaults 143 | * 144 | * @return string 145 | */ 146 | public function publish(array $message, array $options = []) 147 | { 148 | if (!$this->em) { 149 | return ''; 150 | } 151 | 152 | $doctrineMessage = new DoctrineMessage(); 153 | $doctrineMessage->setQueue($this->name) 154 | ->setDelivered(false) 155 | ->setMessage($message) 156 | ->setLength(strlen(serialize($message))); 157 | 158 | $this->em->persist($doctrineMessage); 159 | $this->em->flush(); 160 | 161 | return (string) $doctrineMessage->getId(); 162 | } 163 | 164 | /** 165 | * Polls the Queue for Messages 166 | * 167 | * Depending on the Provider, this method may keep the connection open for 168 | * a configurable amount of time, to allow for long polling. In most cases, 169 | * this method is not meant to be used to long poll indefinitely, but should 170 | * return in reasonable amount of time 171 | * 172 | * @param array $options An array of options that override the queue defaults 173 | * 174 | * @return array 175 | */ 176 | public function receive(array $options = []) 177 | { 178 | if (!$this->em) { 179 | return []; 180 | } 181 | 182 | $doctrineMessages = $this->repository->findBy( 183 | array('delivered' => false, 'queue' => $this->name), array('id' => 'ASC') 184 | ); 185 | 186 | $messages = []; 187 | foreach ($doctrineMessages as $doctrineMessage) { 188 | $messages[] = new Message($doctrineMessage->getId(), $doctrineMessage->getMessage(), []); 189 | $doctrineMessage->setDelivered(true); 190 | } 191 | $this->em->flush(); 192 | 193 | return $messages; 194 | } 195 | 196 | /** 197 | * Deletes the Queue Message 198 | * 199 | * @param mixed $id A message identifier or resource 200 | */ 201 | public function delete($id) 202 | { 203 | $doctrineMessage = $this->repository->findById($id); 204 | $doctrineMessage->setDelivered(true); 205 | $this->em->flush(); 206 | 207 | return true; 208 | } 209 | 210 | /** 211 | * Destroys a Queue and clears any Queue related Cache 212 | * 213 | * @return bool 214 | */ 215 | public function destroy() 216 | { 217 | $qb = $this->repository->createQueryBuilder('dm'); 218 | $qb->delete(); 219 | $qb->where('dm.queue = :queue'); 220 | $qb->setParameter('queue', $this->name); 221 | $qb->getQuery()->execute(); 222 | 223 | return true; 224 | } 225 | 226 | /** 227 | * Returns a specific message 228 | * 229 | * @param integer $id 230 | * 231 | * @return Message 232 | */ 233 | public function getById($id) 234 | { 235 | return $this->repository->find($id); 236 | } 237 | 238 | /** 239 | * Returns a query of the message queue 240 | * 241 | * @param array $data ['field'=>'id', 'search'=>'text', 'to'=>date, from=>date] 242 | * @return Query 243 | * 244 | */ 245 | public function findBy($data) 246 | { 247 | $qb = $this->repository->createQueryBuilder('p'); 248 | $qb->select('p'); 249 | $qb->where('p.queue = :queue'); 250 | $qb->setParameter('queue', $this->name); 251 | 252 | $field = (isset($data['field'])) ? $data['field'] : 'message'; 253 | 254 | if (isset($data['search']) && $data['search'] !== null) { 255 | $qb->andWhere('p.' . $field . ' LIKE :contains'); 256 | $qb->setParameter('contains', '%' . $data['search'] . '%'); 257 | } 258 | 259 | if (isset($data['from']) && $data['from'] !== null && isset($data['to']) && $data['to'] !== null) { 260 | $qb->andWhere('p.created BETWEEN :from AND :to'); 261 | $qb->setParameter('from', $data['from']); 262 | $qb->setParameter('to', $data['to']); 263 | } 264 | 265 | return $qb->getQuery(); 266 | } 267 | 268 | /* 269 | * Returns an array of times and messgae counts 270 | * @praram $data ['from' => date, 'to' => date, 'period' => seconds 271 | * @return ['time', 'count'] 272 | */ 273 | 274 | public function counts($data=null) 275 | { 276 | if (isset($data['period']) && $data['period'] !== null) { 277 | $period = $data['period']; 278 | } else { 279 | $period = self::DEFAULT_PERIOD; 280 | } 281 | $sql = 'SELECT from_unixtime(floor(unix_timestamp(created)/' 282 | . $period . ') * ' . $period . ') as time, 283 | count(*) as count 284 | FROM uecode_qpush_message 285 | where queue = "' . $this->name . '"'; 286 | if (isset($data['from']) && $data['from'] !== null) { 287 | $sql = $sql . ' and created >= "' . $data['from'] . '"'; 288 | } 289 | if (isset($data['to']) && $data['to'] !== null) { 290 | $sql = $sql . ' and created <= "' . $data['to'] . '"'; 291 | } 292 | $sql = $sql . ' group by floor(unix_timestamp(created)/' . $period . ')'; 293 | $sql = $sql . ' order by floor(unix_timestamp(created)/' . $period . ') ASC'; 294 | $statement = $this->em->getConnection()->prepare($sql); 295 | $statement->execute(); 296 | $results = $statement->fetchAll(); 297 | 298 | return $results; 299 | } 300 | 301 | } 302 | -------------------------------------------------------------------------------- /src/Provider/FileProvider.php: -------------------------------------------------------------------------------- 1 | name = $name; 19 | /* md5 only contain numeric and A to F, so it is file system safe */ 20 | $this->queuePath = $options['path'].DIRECTORY_SEPARATOR.str_replace('-', '', hash('md5', $name)); 21 | $this->options = $options; 22 | $this->cache = $cache; 23 | $this->logger = $logger; 24 | } 25 | 26 | public function getProvider() 27 | { 28 | return 'File'; 29 | } 30 | 31 | public function create() 32 | { 33 | $fs = new Filesystem(); 34 | if (!$fs->exists($this->queuePath)) { 35 | $fs->mkdir($this->queuePath); 36 | return $fs->exists($this->queuePath); 37 | } 38 | return true; 39 | } 40 | 41 | public function publish(array $message, array $options = []) 42 | { 43 | $fileName = microtime(false); 44 | $fileName = str_replace(' ', '', $fileName); 45 | $path = substr(hash('md5', $fileName), 0, 3); 46 | 47 | $fs = new Filesystem(); 48 | if (!$fs->exists($this->queuePath.DIRECTORY_SEPARATOR.$path)) { 49 | $fs->mkdir($this->queuePath.DIRECTORY_SEPARATOR.$path); 50 | } 51 | 52 | $fs->dumpFile( 53 | $this->queuePath.DIRECTORY_SEPARATOR.$path.DIRECTORY_SEPARATOR.$fileName.'.json', 54 | json_encode($message) 55 | ); 56 | return $fileName; 57 | } 58 | 59 | /** 60 | * @param array $options 61 | * @return Message[] 62 | */ 63 | public function receive(array $options = []) 64 | { 65 | $finder = new Finder(); 66 | $finder 67 | ->files() 68 | ->ignoreDotFiles(true) 69 | ->ignoreUnreadableDirs(true) 70 | ->ignoreVCS(true) 71 | ->name('*.json') 72 | ->in($this->queuePath) 73 | ; 74 | if ($this->options['message_delay'] > 0) { 75 | $finder->date( 76 | sprintf('< %d seconds ago', $this->options['message_delay']) 77 | ); 78 | } 79 | $finder 80 | ->date( 81 | sprintf('> %d seconds ago', $this->options['message_expiration']) 82 | ) 83 | ; 84 | $messages = []; 85 | /** @var SplFileInfo $file */ 86 | foreach ($finder as $file) { 87 | $filePointer = fopen($file->getRealPath(), 'r+'); 88 | $id = substr($file->getFilename(), 0, -5); 89 | if (!isset($this->filePointerList[$id]) && flock($filePointer, LOCK_EX | LOCK_NB)) { 90 | $this->filePointerList[$id] = $filePointer; 91 | $messages[] = new Message($id, json_decode($file->getContents(), true), []); 92 | } else { 93 | fclose($filePointer); 94 | } 95 | if (count($messages) === (int) $this->options['messages_to_receive']) { 96 | break; 97 | } 98 | } 99 | return $messages; 100 | } 101 | 102 | public function delete($id) 103 | { 104 | $success = false; 105 | if (isset($this->filePointerList[$id])) { 106 | $fileName = $id; 107 | $path = substr(hash('md5', (string)$fileName), 0, 3); 108 | $fs = new Filesystem(); 109 | $fs->remove( 110 | $this->queuePath . DIRECTORY_SEPARATOR . $path . DIRECTORY_SEPARATOR . $fileName . '.json' 111 | ); 112 | fclose($this->filePointerList[$id]); 113 | unset($this->filePointerList[$id]); 114 | $success = true; 115 | } 116 | if (rand(1,10) === 5) { 117 | $this->cleanUp(); 118 | } 119 | return $success; 120 | } 121 | 122 | public function cleanUp() 123 | { 124 | $finder = new Finder(); 125 | $finder 126 | ->files() 127 | ->in($this->queuePath) 128 | ->ignoreDotFiles(true) 129 | ->ignoreUnreadableDirs(true) 130 | ->ignoreVCS(true) 131 | ->depth('< 2') 132 | ->name('*.json') 133 | ; 134 | $finder->date( 135 | sprintf('< %d seconds ago', $this->options['message_expiration']) 136 | ); 137 | /** @var SplFileInfo $file */ 138 | foreach ($finder as $file) { 139 | @unlink($file->getRealPath()); 140 | } 141 | } 142 | 143 | public function destroy() 144 | { 145 | $fs = new Filesystem(); 146 | $fs->remove($this->queuePath); 147 | $this->filePointerList = []; 148 | return !is_dir($this->queuePath); 149 | } 150 | 151 | /** 152 | * Removes the message from queue after all other listeners have fired 153 | * 154 | * If an earlier listener has erred or stopped propagation, this method 155 | * will not fire and the Queued Message should become visible in queue again. 156 | * 157 | * Stops Event Propagation after removing the Message 158 | * 159 | * @param MessageEvent $event The SQS Message Event 160 | * @return bool|void 161 | */ 162 | public function onMessageReceived(MessageEvent $event) 163 | { 164 | $id = $event->getMessage()->getId(); 165 | $this->delete($id); 166 | $event->stopPropagation(); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Provider/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | interface ProviderInterface 32 | { 33 | /** 34 | * Prefix prepended to the queue names 35 | */ 36 | const QPUSH_PREFIX = 'qpush'; 37 | 38 | /** 39 | * Constructor for Provider classes 40 | * 41 | * @param string $name Name of the Queue the provider is for 42 | * @param array $options An array of configuration options for the Queue 43 | * @param mixed $client A Queue Client for the provider 44 | * @param Cache $cache An instance of Doctrine\Common\Cache\Cache 45 | * @param Logger $logger An instance of Mongolog\Logger 46 | */ 47 | public function __construct($name, array $options, $client, Cache $cache, Logger $logger); 48 | 49 | /** 50 | * Returns the name of the Queue that this Provider is for 51 | * 52 | * @return string 53 | */ 54 | public function getName(); 55 | 56 | /** 57 | * Returns the Queue Name prefixed with the QPush Prefix 58 | * 59 | * If a Queue name is explicitly set in the configuration, use just that 60 | * name - which is beneficial for reuising existing queues not created by 61 | * qpush. Otherwise, create the queue with the qpush prefix/ 62 | * 63 | * @return string 64 | */ 65 | public function getNameWithPrefix(); 66 | 67 | /** 68 | * Returns the Queue Provider name 69 | * 70 | * @return string 71 | */ 72 | public function getProvider(); 73 | 74 | /** 75 | * Returns the Provider's Configuration Options 76 | * 77 | * @return array 78 | */ 79 | public function getOptions(); 80 | 81 | /** 82 | * Returns the Cache service 83 | * 84 | * @return Cache 85 | */ 86 | public function getCache(); 87 | 88 | /** 89 | * Returns the Logger service 90 | * 91 | * @return Logger 92 | */ 93 | public function getLogger(); 94 | 95 | /** 96 | * Creates the Queue 97 | * 98 | * All Create methods are idempotent, if the resource exists, the current ARN 99 | * will be returned 100 | */ 101 | public function create(); 102 | 103 | /** 104 | * Publishes a message to the Queue 105 | * 106 | * This method should return a string MessageId or Response 107 | * 108 | * @param array $message The message to queue 109 | * @param array $options An array of options that override the queue defaults 110 | * 111 | * @return string 112 | */ 113 | public function publish(array $message, array $options = []); 114 | 115 | /** 116 | * Polls the Queue for Messages 117 | * 118 | * Depending on the Provider, this method may keep the connection open for 119 | * a configurable amount of time, to allow for long polling. In most cases, 120 | * this method is not meant to be used to long poll indefinitely, but should 121 | * return in reasonable amount of time 122 | * 123 | * @param array $options An array of options that override the queue defaults 124 | * 125 | * @return array 126 | */ 127 | public function receive(array $options = []); 128 | 129 | /** 130 | * Deletes the Queue Message 131 | * 132 | * @param mixed $id A message identifier or resource 133 | */ 134 | public function delete($id); 135 | 136 | /** 137 | * Destroys a Queue and clears any Queue related Cache 138 | * 139 | * @return bool 140 | */ 141 | public function destroy(); 142 | 143 | /** 144 | * Logs data from the library 145 | * 146 | * This method wraps the Logger to check if logging is enabled and adds 147 | * the Queue name and Provider automatically to the context 148 | * 149 | * @param int $level The log level 150 | * @param string $message The message to log 151 | * @param array $context The log context 152 | * 153 | * @return bool Whether the record was logged 154 | */ 155 | public function log($level, $message, array $context); 156 | } 157 | -------------------------------------------------------------------------------- /src/Provider/ProviderRegistry.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class ProviderRegistry 29 | { 30 | /** 31 | * All services tagged with `uecode_qpush.receive` 32 | * @var array 33 | */ 34 | private $queues; 35 | 36 | /** 37 | * Constructor. 38 | */ 39 | public function __construct() 40 | { 41 | $this->queues = []; 42 | } 43 | 44 | /** 45 | * Adds a Listener to the chain based on priority 46 | * 47 | * @param string $name The name of the Queue 48 | * @param ProviderInterface $service The QueueProvider 49 | */ 50 | public function addProvider($name, ProviderInterface $service) 51 | { 52 | $this->queues[$name] = $service; 53 | } 54 | 55 | /** 56 | * Returns the Queues 57 | * 58 | * @return array 59 | */ 60 | public function all() 61 | { 62 | return $this->queues; 63 | } 64 | 65 | /** 66 | * Checks whether a Queue Provider exists in the Regisitry 67 | * 68 | * @param string $name The name of the Queue to check for 69 | * 70 | * @return Boolean 71 | */ 72 | public function has($name) 73 | { 74 | return array_key_exists($name, $this->queues); 75 | } 76 | 77 | /** 78 | * Returns a Single QueueProvider by Queue Name 79 | * 80 | * @param string $name 81 | * 82 | * @throws \InvalidArgumentException 83 | * 84 | * @return ProviderInterface 85 | */ 86 | public function get($name) 87 | { 88 | if (!array_key_exists($name, $this->queues)) { 89 | throw new \InvalidArgumentException("The queue does not exist. {$name}"); 90 | } 91 | 92 | return $this->queues[$name]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Provider/SyncProvider.php: -------------------------------------------------------------------------------- 1 | name = $name; 47 | $this->options = $options; 48 | $this->dispatcher = $client; 49 | $this->cache = $cache; 50 | $this->logger = $logger; 51 | } 52 | 53 | public function getProvider() 54 | { 55 | return 'Sync'; 56 | } 57 | 58 | public function publish(array $message, array $options = []) 59 | { 60 | $message = new Message(time(), $message, []); 61 | 62 | $this->dispatcher->dispatch( 63 | Events::Message($this->name), 64 | new MessageEvent($this->name, $message) 65 | ); 66 | 67 | $context = ['MessageId' => $message->getId()]; 68 | $this->log(200, 'Message received and dispatched on Sync Queue', $context); 69 | } 70 | 71 | public function create() {} 72 | 73 | public function destroy() {} 74 | 75 | public function delete($id) {} 76 | 77 | public function receive(array $options = []) {} 78 | } 79 | -------------------------------------------------------------------------------- /src/Resources/config/config.yml: -------------------------------------------------------------------------------- 1 | #Example Configuration 2 | uecode_qpush: 3 | cache_service: null 4 | logging_enabled: true 5 | providers: 6 | aws: 7 | key: 8 | secret: 9 | region: 10 | ironmq: 11 | token: 12 | project_id: 13 | host: 14 | queues: 15 | default: 16 | provider: aws #or ironmq 17 | options: 18 | push_notifications: true 19 | notification_retries: 3 20 | message_delay: 0 21 | message_timeout: 30 22 | message_expiration: 604800 23 | messages_to_receive: 1 24 | receive_wait_time: 3 25 | fifo: false 26 | content_based_deduplication: false 27 | subscribers: 28 | - { endpoint: http://example1.com/qpush, protocol: http } 29 | - { endpoint: http://example2.com/qpush, protocol: http } 30 | -------------------------------------------------------------------------------- /src/Resources/config/parameters.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | uecode_qpush.request_listener.priority: 254 3 | uecode_qpush.request_listener.class: Uecode\Bundle\QPushBundle\EventListener\RequestListener 4 | uecode_qpush.registry.class: Uecode\Bundle\QPushBundle\Provider\ProviderRegistry 5 | uecode_qpush.provider.aws: Uecode\Bundle\QPushBundle\Provider\AwsProvider 6 | uecode_qpush.provider.ironmq: Uecode\Bundle\QPushBundle\Provider\IronMqProvider 7 | uecode_qpush.provider.sync: Uecode\Bundle\QPushBundle\Provider\SyncProvider 8 | uecode_qpush.provider.custom: Uecode\Bundle\QPushBundle\Provider\CustomProvider 9 | uecode_qpush.provider.file: Uecode\Bundle\QPushBundle\Provider\FileProvider 10 | uecode_qpush.provider.doctrine: Uecode\Bundle\QPushBundle\Provider\DoctrineProvider 11 | -------------------------------------------------------------------------------- /src/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ### QPush Registry 3 | uecode_qpush.registry: 4 | class: Uecode\Bundle\QPushBundle\Provider\ProviderRegistry 5 | uecode_qpush: 6 | alias: uecode_qpush.registry 7 | public: true 8 | 9 | ### QPush Default File Cache 10 | uecode_qpush.file_cache: 11 | class: Doctrine\Common\Cache\PhpFileCache 12 | arguments: ['%kernel.cache_dir%/qpush', qpush.php] 13 | public: false 14 | 15 | ### QPush Event Listeners 16 | uecode_qpush.request_listener: 17 | class: Uecode\Bundle\QPushBundle\EventListener\RequestListener 18 | arguments: 19 | - '@event_dispatcher' 20 | tags: 21 | - { name: kernel.event_listener, event: kernel.request, priority: '%uecode_qpush.request_listener.priority%' } 22 | 23 | ### QPush Commands 24 | uecode_qpush.build_command: 25 | class: Uecode\Bundle\QPushBundle\Command\QueueBuildCommand 26 | tags: 27 | - { name: console.command } 28 | 29 | uecode_qpush.destroy_command: 30 | class: Uecode\Bundle\QPushBundle\Command\QueueDestroyCommand 31 | tags: 32 | - { name: console.command } 33 | 34 | uecode_qpush.publish_command: 35 | class: Uecode\Bundle\QPushBundle\Command\QueuePublishCommand 36 | tags: 37 | - { name: console.command } 38 | 39 | uecode_qpush.receive_command: 40 | class: Uecode\Bundle\QPushBundle\Command\QueueReceiveCommand 41 | tags: 42 | - { name: console.command } 43 | -------------------------------------------------------------------------------- /src/UecodeQPushBundle.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class UecodeQPushBundle extends Bundle 34 | { 35 | /** 36 | * {@inlineDoc} 37 | */ 38 | public function __construct() 39 | { 40 | // Setting extension to bypass alias convention check 41 | $this->extension = new UecodeQPushExtension(); 42 | } 43 | 44 | /** 45 | * Adds the Compiler Passes for the QPushBundle 46 | * 47 | * @param ContainerBuilder $container 48 | */ 49 | public function build(ContainerBuilder $container) 50 | { 51 | parent::build($container); 52 | 53 | $container->addCompilerPass( 54 | new RegisterListenersPass( 55 | 'event_dispatcher', 56 | 'uecode_qpush.event_listener', 57 | 'uecode_qpush.event_subscriber' 58 | ) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/DependencyInjection/UecodeQPushExtensionTest.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | class UecodeQPushExtensionTest extends \PHPUnit_Framework_TestCase 35 | { 36 | /** 37 | * QPush Extension 38 | * 39 | * @var UecodeQPushExtension 40 | */ 41 | private $extension; 42 | 43 | /** 44 | * Container 45 | * 46 | * @var ContainerBuilder 47 | */ 48 | private $container; 49 | 50 | public function setUp() 51 | { 52 | $this->extension = new UecodeQPushExtension(); 53 | $this->container = new ContainerBuilder(new ParameterBag(['kernel.cache_dir' => '/tmp'])); 54 | 55 | $this->container->registerExtension($this->extension); 56 | } 57 | 58 | public function testConfiguration() 59 | { 60 | $loader = new YamlFileLoader($this->container, new FileLocator(__DIR__.'/../Fixtures/')); 61 | $loader->load('config_test.yml'); 62 | 63 | $this->container->compile(); 64 | 65 | $this->assertTrue($this->container->has('uecode_qpush')); 66 | 67 | $this->assertTrue($this->container->has('uecode_qpush.test_aws')); 68 | $this->assertTrue($this->container->has('uecode_qpush.test_file')); 69 | $this->assertTrue($this->container->has('uecode_qpush.test_aws_fifo')); 70 | $this->assertTrue($this->container->has('uecode_qpush.test_secondary_aws')); 71 | $this->assertNotSame( 72 | $this->container->get('uecode_qpush.test_aws'), 73 | $this->container->get('uecode_qpush.test_secondary_aws') 74 | ); 75 | 76 | $this->assertTrue($this->container->has('uecode_qpush.test_ironmq')); 77 | $this->assertTrue($this->container->has('uecode_qpush.test_secondary_ironmq')); 78 | $this->assertNotSame( 79 | $this->container->get('uecode_qpush.test_ironmq'), 80 | $this->container->get('uecode_qpush.test_secondary_ironmq') 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Event/EventsTest.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class EventsTest extends \PHPUnit_Framework_TestCase 31 | { 32 | public function testConstants() 33 | { 34 | $this->assertEquals('message_received', Events::ON_MESSAGE); 35 | $this->assertEquals('on_notification', Events::ON_NOTIFICATION); 36 | } 37 | 38 | public function testMessageEvent() 39 | { 40 | $event = Events::Message('test'); 41 | 42 | $this->assertEquals(sprintf('%s.%s', 'test', Events::ON_MESSAGE), $event); 43 | } 44 | 45 | public function testNotificationEvent() 46 | { 47 | $event = Events::Notification('test'); 48 | 49 | $this->assertEquals(sprintf('%s.%s', 'test', Events::ON_NOTIFICATION), $event); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Event/MessageEventTest.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class MessageEventTest extends \PHPUnit_Framework_TestCase 33 | { 34 | protected $event; 35 | 36 | public function setUp() 37 | { 38 | $this->event = new MessageEvent('test', new Message(123, ['foo' => 'bar'], ['bar' => 'baz'])); 39 | } 40 | 41 | public function tearDown() 42 | { 43 | $this->event = null; 44 | } 45 | 46 | public function testMessageEventConstructor() 47 | { 48 | $event = new MessageEvent('test', new Message(123, ['foo' => 'bar'], ['bar' => 'baz'])); 49 | $this->assertInstanceOf('Uecode\Bundle\QPushBundle\Event\MessageEvent', $event); 50 | 51 | if (version_compare(PHP_VERSION, '7.0', '>=')) { 52 | // This will throw an Fatal error, not an exception 53 | return; 54 | } 55 | 56 | $this->setExpectedException('PHPUnit_Framework_Error'); 57 | 58 | new MessageEvent('test', ['bad argument']); 59 | } 60 | 61 | public function testGetQueueName() 62 | { 63 | $name = $this->event->getQueueName(); 64 | 65 | $this->assertEquals('test', $name); 66 | } 67 | 68 | public function testGetMessage() 69 | { 70 | $message = $this->event->getMessage(); 71 | 72 | $this->assertInstanceOf('Uecode\Bundle\QPushBundle\Message\Message', $message); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Event/NotificationEventTest.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class NotificationEventTest extends \PHPUnit_Framework_TestCase 33 | { 34 | protected $event; 35 | 36 | public function setUp() 37 | { 38 | $this->event = new NotificationEvent( 39 | 'test', 40 | NotificationEvent::TYPE_SUBSCRIPTION, 41 | new Notification(123, ['foo' => 'bar'], ['bar' => 'baz']) 42 | ); 43 | } 44 | 45 | public function tearDown() 46 | { 47 | $this->event = null; 48 | } 49 | 50 | public function testNotificationEventConstructor() 51 | { 52 | $event = new NotificationEvent( 53 | 'test', 54 | NotificationEvent::TYPE_SUBSCRIPTION, 55 | new Notification(123, ['foo' => 'bar'], ['bar' => 'baz']) 56 | ); 57 | $this->assertInstanceOf('Uecode\Bundle\QPushBundle\Event\NotificationEvent', $event); 58 | 59 | $event = new NotificationEvent( 60 | 'test', 61 | NotificationEvent::TYPE_MESSAGE, 62 | new Notification(123, ['foo' => 'bar'], ['bar' => 'baz']) 63 | ); 64 | $this->assertInstanceOf('Uecode\Bundle\QPushBundle\Event\NotificationEvent', $event); 65 | 66 | $this->setExpectedException('InvalidArgumentException'); 67 | $event = new NotificationEvent( 68 | 'test', 69 | 'InvalidNotificationType', 70 | new Notification(123, ['foo' => 'bar'], ['bar' => 'baz']) 71 | ); 72 | 73 | $this->setExpectedException('PHPUnit_Framework_Error'); 74 | $event = new NotificationEvent( 75 | 'test', 76 | NotificationEvent::TYPE_SUBSCRIPTION, 77 | ['bad argument'] 78 | ); 79 | } 80 | 81 | public function testGetQueueName() 82 | { 83 | $name = $this->event->getQueueName(); 84 | 85 | $this->assertEquals('test', $name); 86 | } 87 | 88 | public function testGetType() 89 | { 90 | $type = $this->event->getType(); 91 | 92 | $this->assertContains( 93 | $type, 94 | [ 95 | NotificationEvent::TYPE_SUBSCRIPTION, 96 | NotificationEvent::TYPE_MESSAGE 97 | ] 98 | ); 99 | } 100 | 101 | public function testGetNotification() 102 | { 103 | $notification = $this->event->getNotification(); 104 | 105 | $this->assertInstanceOf('Uecode\Bundle\QPushBundle\Message\Notification', $notification); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/EventListener/RequestListenerTest.php: -------------------------------------------------------------------------------- 1 | 37 | */ 38 | class RequestListenerTest extends \PHPUnit_Framework_TestCase 39 | { 40 | /** 41 | * @var EventDispatcher 42 | */ 43 | protected $dispatcher; 44 | 45 | /** 46 | * @var MockInterface 47 | */ 48 | protected $event; 49 | 50 | public function setUp() 51 | { 52 | $this->dispatcher = new EventDispatcher('UTF-8'); 53 | $listener = new RequestListener($this->dispatcher); 54 | 55 | $this->dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelRequest']); 56 | $this->dispatcher->addListener(QPushEvents::Notification('ironmq-test'), [$this, 'IronMqOnNotificationReceived']); 57 | $this->dispatcher->addListener(QPushEvents::Notification('aws-test'), [$this, 'AwsOnNotificationReceived']); 58 | 59 | $this->kernel = $this->createMock('Symfony\Component\HttpKernel\HttpKernelInterface'); 60 | } 61 | 62 | public function testListenerDoesNothingForSubRequests() 63 | { 64 | $event = new GetResponseEvent($this->kernel, new Request(), HttpKernelInterface::SUB_REQUEST); 65 | $this->dispatcher->dispatch(KernelEvents::REQUEST, $event); 66 | 67 | $this->assertFalse($event->hasResponse()); 68 | } 69 | 70 | public function testListenerHandlesIronMQMessageRequests() 71 | { 72 | $message = '{"foo": "bar","_qpush_queue":"ironmq-test"}'; 73 | 74 | $request = new Request([],[],[],[],[],[], $message); 75 | $request->headers->set('iron-message-id', 123); 76 | $request->headers->set('iron-subscriber-message-id', 456); 77 | $request->headers->set('iron-subscriber-message-url', 'http://foo.bar'); 78 | 79 | $event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST); 80 | $this->dispatcher->dispatch(KernelEvents::REQUEST, $event); 81 | 82 | $this->assertTrue($event->hasResponse()); 83 | $this->assertEquals("IronMQ Notification Received.", $event->getResponse()->getContent()); 84 | } 85 | 86 | public function IronMqOnNotificationReceived(NotificationEvent $event) 87 | { 88 | $notification = $event->getNotification(); 89 | $this->assertInstanceOf('\Uecode\Bundle\QPushBundle\Message\Notification', $notification); 90 | 91 | $this->assertEquals(123, $notification->getId()); 92 | 93 | $this->assertInternalType('array', $notification->getBody()); 94 | $this->assertEquals($notification->getBody(), ['foo' => 'bar']); 95 | 96 | $this->assertInstanceOf('\Doctrine\Common\Collections\ArrayCollection', $notification->getMetadata()); 97 | $this->assertEquals( 98 | [ 99 | 'iron-subscriber-message-id' => 456, 100 | 'iron-subscriber-message-url' => 'http://foo.bar' 101 | ], 102 | $notification->getMetadata()->toArray() 103 | ); 104 | } 105 | 106 | public function testListenerHandlesAwsNotificationRequests() 107 | { 108 | $message = [ 109 | 'Type' => 'Notification', 110 | 'MessageId' => 123, 111 | 'TopicArn' => 'SomeArn', 112 | 'Subject' => 'aws-test', 113 | 'Message' => '{"foo": "bar"}', 114 | 'Timestamp' => date('Y-m-d H:i:s', 1422040603) 115 | ]; 116 | 117 | $request = new Request([],[],[],[],[],[], json_encode($message)); 118 | $request->headers->set('x-amz-sns-message-type', 'Notification'); 119 | 120 | $event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST); 121 | 122 | $this->dispatcher->dispatch(KernelEvents::REQUEST, $event); 123 | 124 | $this->assertTrue($event->hasResponse()); 125 | $this->assertEquals("SNS Message Notification Received.", $event->getResponse()->getContent()); 126 | } 127 | 128 | public function AwsOnNotificationReceived(NotificationEvent $event) 129 | { 130 | $notification = $event->getNotification(); 131 | $this->assertInstanceOf('\Uecode\Bundle\QPushBundle\Message\Notification', $notification); 132 | 133 | $this->assertEquals(123, $notification->getId()); 134 | 135 | $this->assertInternalType('array', $notification->getBody()); 136 | $this->assertEquals($notification->getBody(), ['foo' => 'bar']); 137 | 138 | $this->assertInstanceOf('\Doctrine\Common\Collections\ArrayCollection', $notification->getMetadata()); 139 | $this->assertEquals( 140 | [ 141 | 'Type' => 'Notification', 142 | 'TopicArn' => 'SomeArn', 143 | 'Timestamp' => date('Y-m-d H:i:s', 1422040603), 144 | 'Subject' => 'aws-test' 145 | ], 146 | $notification->getMetadata()->toArray() 147 | ); 148 | } 149 | 150 | public function testListenerHandlesAwsSubscriptionRequests() 151 | { 152 | $message = [ 153 | 'Type' => 'SubscriptionConfirmation', 154 | 'MessageId' => 123, 155 | 'Token' => 456, 156 | 'TopicArn' => 'SomeArn', 157 | 'SubscribeUrl' => 'http://foo.bar', 158 | 'Subject' => 'aws-test', 159 | 'Message' => '{"foo": "bar"}', 160 | 'Timestamp' => date('Y-m-d H:i:s', 1422040603) 161 | ]; 162 | 163 | $request = new Request([],[],[],[],[],[], json_encode($message)); 164 | $request->headers->set('x-amz-sns-message-type', 'SubscriptionConfirmation'); 165 | 166 | $event = new GetResponseEvent($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST); 167 | $this->dispatcher->dispatch(KernelEvents::REQUEST, $event); 168 | 169 | $this->assertTrue($event->hasResponse()); 170 | $this->assertEquals("SNS Subscription Confirmation Received.", $event->getResponse()->getContent()); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /tests/Fixtures/config_test.yml: -------------------------------------------------------------------------------- 1 | uecode_qpush: 2 | cache_service: null 3 | logging_enabled: true 4 | providers: 5 | aws: 6 | driver: aws 7 | key: 123 8 | secret: 123 9 | region: us-east-1 10 | secondary_aws: 11 | driver: aws 12 | key: 234 13 | secret: 432 14 | region: eu-west-1 15 | ironmq: 16 | driver: ironmq 17 | token: 123 18 | project_id: 123 19 | secondary_ironmq: 20 | driver: ironmq 21 | token: 234 22 | project_id: 234 23 | file: 24 | driver: file 25 | path: /tmp/my_queue 26 | queues: 27 | test_file: 28 | provider: file 29 | options: 30 | message_delay: 0 31 | message_timeout: 30 32 | message_expiration: 604800 33 | messages_to_receive: 1 34 | receive_wait_time: 3 35 | test_aws: 36 | provider: aws 37 | options: 38 | push_notifications: true 39 | notification_retries: 3 40 | message_delay: 0 41 | message_timeout: 30 42 | message_expiration: 604800 43 | messages_to_receive: 1 44 | receive_wait_time: 3 45 | subscribers: 46 | - { endpoint: http://example.com/qpush, protocol: http } 47 | test_secondary_aws: 48 | provider: secondary_aws 49 | options: 50 | push_notifications: true 51 | notification_retries: 3 52 | message_delay: 0 53 | message_timeout: 30 54 | message_expiration: 604800 55 | messages_to_receive: 1 56 | receive_wait_time: 3 57 | subscribers: 58 | - { endpoint: http://example.com/qpush, protocol: http } 59 | test_aws_fifo: 60 | provider: aws 61 | options: 62 | push_notifications: true 63 | notification_retries: 3 64 | message_delay: 0 65 | message_timeout: 30 66 | message_expiration: 604800 67 | messages_to_receive: 1 68 | receive_wait_time: 3 69 | fifo: true 70 | content_based_deduplication: true 71 | subscribers: 72 | - { endpoint: http://example.com/qpush, protocol: http } 73 | test_ironmq: 74 | provider: ironmq 75 | options: 76 | push_notifications: true 77 | notification_retries: 3 78 | message_delay: 0 79 | message_timeout: 30 80 | message_expiration: 604800 81 | messages_to_receive: 1 82 | receive_wait_time: 3 83 | subscribers: 84 | - { endpoint: http://example.com/qpush, protocol: http } 85 | test_secondary_ironmq: 86 | provider: secondary_ironmq 87 | options: 88 | push_notifications: true 89 | notification_retries: 3 90 | message_delay: 0 91 | message_timeout: 30 92 | message_expiration: 604800 93 | messages_to_receive: 1 94 | receive_wait_time: 3 95 | subscribers: 96 | - { endpoint: http://example.com/qpush, protocol: http } 97 | 98 | services: 99 | event_dispatcher: 100 | class: stdClass 101 | logger: 102 | class: Symfony\Bridge\Monolog\Logger 103 | arguments: 104 | - 'test' 105 | -------------------------------------------------------------------------------- /tests/Message/BaseMessageTest.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | abstract class BaseMessageTest extends \PHPUnit_Framework_TestCase 31 | { 32 | protected $message; 33 | 34 | /** 35 | * Tests that the Constructor accepts only an array Metadata property 36 | * 37 | * @expectedException PHPUnit_Framework_Error 38 | */ 39 | abstract public function testConstructor(); 40 | 41 | /** 42 | * Test that the Message Id is a String or Integer 43 | */ 44 | public function testGetId() 45 | { 46 | $id = $this->message->getId(); 47 | 48 | $this->assertContains(gettype($id), ['string', 'integer']); 49 | } 50 | 51 | /** 52 | * Test that the Message Body is a String or Array 53 | */ 54 | public function testGetBody() 55 | { 56 | $body = $this->message->getBody(); 57 | 58 | $this->assertContains(gettype($body), ['string', 'array']); 59 | } 60 | 61 | /** 62 | * Test that the Message Metadata is an ArrayCollection 63 | */ 64 | public function testGetMetadata() 65 | { 66 | $metadata = $this->message->getMetadata(); 67 | 68 | $this->assertInstanceOf('Doctrine\\Common\\Collections\\ArrayCollection', $metadata); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Message/MessageTest.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class MessageTest extends BaseMessageTest 31 | { 32 | public function setUp() 33 | { 34 | $this->message = new Message(123, ['foo' => 'bar'], ['baz' => 'qux']); 35 | } 36 | 37 | public function tearDown() 38 | { 39 | $this->message = null; 40 | } 41 | 42 | public function testConstructor() 43 | { 44 | $message = new Message(123, ['foo' => 'bar'], ['baz' => 'qux']); 45 | $this->assertInstanceOf('Uecode\Bundle\QPushBundle\Message\Message', $message); 46 | 47 | if (version_compare(PHP_VERSION, '7.0', '>=')) { 48 | // This will throw an Fatal error, not an exception 49 | return; 50 | } 51 | 52 | $this->setExpectedException('PHPUnit_Framework_Error'); 53 | 54 | new Message(123, ['foo' => 'bar'], 'invalid argument'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Message/NotificationTest.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class NotificationTest extends BaseMessageTest 31 | { 32 | public function setUp() 33 | { 34 | $this->message = new Notification(123, ['foo' => 'bar'], ['baz' => 'qux']); 35 | } 36 | 37 | public function tearDown() 38 | { 39 | $this->message = null; 40 | } 41 | 42 | public function testConstructor() 43 | { 44 | $notification = new Notification(123, ['foo' => 'bar'], ['baz' => 'qux']); 45 | $this->assertInstanceOf('Uecode\Bundle\QPushBundle\Message\Notification', $notification); 46 | 47 | if (version_compare(PHP_VERSION, '7.0', '>=')) { 48 | // This will throw an Fatal error, not an exception 49 | return; 50 | } 51 | 52 | $this->setExpectedException('PHPUnit_Framework_Error'); 53 | 54 | new Notification(123, ['foo' => 'bar'], 'invalid argument'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/MockClient/AwsMockClient.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class AwsMockClient extends \Aws\Common\Aws 31 | { 32 | public function get($name, $throwAway = false) 33 | { 34 | if (!in_array($name, ['Sns', 'Sqs'])) { 35 | throw new \InvalidArgumentException( 36 | sprintf('Only supports Sns and Sqs as options, %s given.', $name) 37 | ); 38 | } 39 | 40 | if ($name == "Sns") { 41 | return new SnsMockClient; 42 | } 43 | 44 | return new SqsMockClient; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/MockClient/CustomMockClient.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | class CustomMockClient extends AbstractProvider 35 | { 36 | /** 37 | * Constructor for Provider classes 38 | * 39 | * @param string $name Name of the Queue the provider is for 40 | * @param array $options An array of configuration options for the Queue 41 | * @param mixed $client A Queue Client for the provider 42 | * @param Cache $cache An instance of Doctrine\Common\Cache\Cache 43 | * @param Logger $logger An instance of Mongolog\Logger 44 | */ 45 | public function __construct($name, array $options, $client, Cache $cache, Logger $logger) 46 | { 47 | } 48 | 49 | public function getProvider() 50 | { 51 | } 52 | 53 | public function create() 54 | { 55 | } 56 | 57 | public function publish(array $message, array $options = []) 58 | { 59 | } 60 | 61 | public function receive(array $options = []) 62 | { 63 | } 64 | 65 | public function delete($id) 66 | { 67 | } 68 | 69 | public function destroy() 70 | { 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/MockClient/IronMqMockClient.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class IronMqMockClient 31 | { 32 | private $deleteCount = 0; 33 | 34 | public function createQueue($queue, array $params = []) 35 | { 36 | $response = new \stdClass; 37 | $response->id = '530295fe3c94fbcf0c79cffe'; 38 | $response->name = 'test'; 39 | $response->size = 0; 40 | $response->total_messages = 0; 41 | $response->project_id = '52f67d032001c00005000057'; 42 | 43 | return $response; 44 | } 45 | 46 | public function deleteQueue($queue) 47 | { 48 | if ($this->deleteCount == 0) { 49 | $this->deleteCount++; 50 | 51 | return true; 52 | } 53 | 54 | if ($this->deleteCount == 1) { 55 | $this->deleteCount++; 56 | 57 | throw new \Exception('http error: 404 | {"msg":"Queue not found"}'); 58 | } 59 | 60 | throw new \Exception('Random Exception'); 61 | } 62 | 63 | public function postMessage($queue, $message, $options) 64 | { 65 | $response = new \stdClass; 66 | $response->id = 123; 67 | $response->ids = [123]; 68 | $response->msg = "Messages put on queue."; 69 | 70 | return $response; 71 | } 72 | 73 | public function reserveMessages($queue, $count, $timeout) 74 | { 75 | $response = new \stdClass; 76 | $response->id = 123; 77 | $response->body = '{"foo":"bar","_qpush_queue":"test"}'; 78 | $response->reservation_id = 'def456'; 79 | $response->reserved_count = 1; 80 | 81 | return [$response]; 82 | } 83 | 84 | public function deleteMessage($queue, $id) 85 | { 86 | $response = new \stdClass; 87 | $response->id = $id; 88 | 89 | if ($id == 456) { 90 | throw new \Exception('http error: 404 | {"msg":"Queue not found"}'); 91 | } 92 | 93 | if ($id == 789) { 94 | throw new \Exception('Random Exception'); 95 | } 96 | 97 | return $response; 98 | } 99 | 100 | public function getQueue($queue) 101 | { 102 | $response = new \stdClass; 103 | $response->id = '530295fe3c94fbcf0c79cffe'; 104 | $response->name = 'test'; 105 | $response->size = 0; 106 | $response->total_messages = 0; 107 | $response->project_id = '52f67d032001c00005000057'; 108 | 109 | return $response; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/MockClient/SnsMockClient.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class SnsMockClient 34 | { 35 | public function deleteTopic(array $args) 36 | { 37 | return true; 38 | } 39 | 40 | public function publish(array $args) 41 | { 42 | return new ArrayCollection([ 43 | 'MessageId' => 123 44 | ]); 45 | } 46 | 47 | public function createTopic(array $args) 48 | { 49 | return new ArrayCollection([ 50 | 'TopicArn' => 'long_topic_arn_string' 51 | ]); 52 | } 53 | 54 | public function getTopicAttributes(array $args) 55 | { 56 | if ($args['TopicArn'] == null) { 57 | throw new SnsException; 58 | } 59 | 60 | return new ArrayCollection([ 61 | 'Attributes' => [ 62 | 'TopicArn' => 'long_topic_arn_string' 63 | ] 64 | ]); 65 | } 66 | 67 | public function listSubscriptionsByTopic(array $args) 68 | { 69 | return new ArrayCollection([ 70 | 'Subscriptions' => [ 71 | [ 72 | 'SubscriptionArn' => 'long_subscription_arn_string', 73 | 'Owner' => 'owner_string', 74 | 'Protocol' => 'http', 75 | 'Endpoint' => 'http://long_url_string.com', 76 | 'TopicArn' => 'long_topic_arn_string' 77 | ] 78 | ] 79 | ]); 80 | } 81 | 82 | public function subscribe(array $args) 83 | { 84 | return new ArrayCollection([ 85 | 'SubscriptionArn' => 'long_subscription_arn_string' 86 | ]); 87 | } 88 | 89 | public function unsubscribe(array $args) 90 | { 91 | return true; 92 | } 93 | 94 | public function confirmSubscription(array $args) 95 | { 96 | return true; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/MockClient/SqsMockClient.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class SqsMockClient 33 | { 34 | public function getQueueArn($url) 35 | { 36 | return "long_queue_arn_string"; 37 | } 38 | 39 | public function getQueueUrl($name) 40 | { 41 | return new ArrayCollection([ 42 | 'QueueUrl' => 'long_queue_url_string' 43 | ]); 44 | } 45 | 46 | public function deleteQueue(array $args) 47 | { 48 | return true; 49 | } 50 | 51 | public function sendMessage(array $args) 52 | { 53 | return new ArrayCollection([ 54 | 'MessageId' => 123 55 | ]); 56 | } 57 | 58 | public function receiveMessage(array $args) 59 | { 60 | return new ArrayCollection([ 61 | 'Messages' => [ 62 | [ 63 | 'MessageId' => 123, 64 | 'ReceiptHandle' => 'long_receipt_handle_string', 65 | 'MD5OfBody' => 'long_md5_hash_string', 66 | 'Body' => json_encode(['foo' => 'bar']) 67 | ], 68 | [ 69 | 'MessageId' => 123, 70 | 'ReceiptHandle' => 'long_receipt_handle_string', 71 | 'MD5OfBody' => 'long_md5_hash_string', 72 | 'Body' => json_encode(['Message' => json_encode(['foo' => 'bar'])]) 73 | ] 74 | ] 75 | ]); 76 | } 77 | 78 | public function deleteMessage(array $args) 79 | { 80 | return true; 81 | } 82 | 83 | public function createQueue(array $args) 84 | { 85 | return new ArrayCollection([ 86 | 'QueueUrl' => 'long_queue_url_string' 87 | ]); 88 | } 89 | 90 | public function setQueueAttributes(array $args) 91 | { 92 | return true; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Provider/AbstractProviderTest.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | class AbstractProviderTest extends \PHPUnit_Framework_TestCase 37 | { 38 | protected $provider; 39 | 40 | public function setUp() 41 | { 42 | $this->provider = $this->getTestProvider(); 43 | } 44 | 45 | public function tearDown() 46 | { 47 | $this->provider = null; 48 | 49 | if (file_exists('/tmp/qpush.provider.test.php')) { 50 | unlink('/tmp/qpush.provider.test.php'); 51 | } 52 | } 53 | 54 | private function getTestProvider(array $options = []) 55 | { 56 | 57 | $options = array_merge( 58 | [ 59 | 'logging_enabled' => false, 60 | 'push_notifications' => true, 61 | 'notification_retries' => 3, 62 | 'message_delay' => 0, 63 | 'message_timeout' => 30, 64 | 'message_expiration' => 604800, 65 | 'messages_to_receive' => 1, 66 | 'receive_wait_time' => 3, 67 | 'subscribers' => [ 68 | [ 'protocol' => 'http', 'endpoint' => 'http://fake.com' ] 69 | ] 70 | ], 71 | $options 72 | ); 73 | 74 | return new TestProvider( 75 | 'test', 76 | $options, 77 | new \stdClass, 78 | $this->createMock( 79 | 'Doctrine\Common\Cache\PhpFileCache', 80 | [], 81 | ['/tmp', 'qpush.aws.test.php'] 82 | ), 83 | $this->createMock( 84 | 'Symfony\Bridge\Monolog\Logger', 85 | [], 86 | ['qpush.test'] 87 | ) 88 | ); 89 | } 90 | 91 | public function testGetName() 92 | { 93 | $name = $this->provider->getName(); 94 | 95 | $this->assertEquals($name, 'test'); 96 | } 97 | 98 | public function testGetNameWithPrefix() 99 | { 100 | $name = $this->provider->getNameWithPrefix(); 101 | 102 | $this->assertEquals(sprintf('%s_%s', ProviderInterface::QPUSH_PREFIX, 'test'), $name); 103 | } 104 | 105 | public function testGetNameWithPrefixProvidedName() 106 | { 107 | $provider = $this->getTestProvider(['queue_name' => 'foo']); 108 | $name = $provider->getNameWithPrefix(); 109 | 110 | $this->assertEquals('foo', $name); 111 | } 112 | 113 | public function testGetOptions() 114 | { 115 | $options = $this->provider->getOptions(); 116 | 117 | $this->assertTrue(is_array($options)); 118 | } 119 | 120 | public function testGetCache() 121 | { 122 | $cache = $this->provider->getCache(); 123 | 124 | $this->assertInstanceOf('Doctrine\\Common\\Cache\\Cache', $cache); 125 | } 126 | 127 | public function testGetLogger() 128 | { 129 | $logger = $this->provider->getLogger(); 130 | 131 | $this->assertInstanceOf('Monolog\\Logger', $logger); 132 | } 133 | 134 | public function testLogEnabled() 135 | { 136 | $this->assertFalse($this->provider->log(100, 'test log', [])); 137 | 138 | $provider = $this->getTestProvider(['logging_enabled' => true]); 139 | 140 | $this->assertNull($provider->log(100, 'test log', [])); 141 | } 142 | 143 | public function testGetProvider() 144 | { 145 | $provider = $this->provider->getProvider(); 146 | 147 | $this->assertEquals('TestProvider', $provider); 148 | } 149 | 150 | public function testOnNotification() 151 | { 152 | $dispatcher = $this->getMockForAbstractClass('Symfony\Component\EventDispatcher\EventDispatcherInterface'); 153 | $result = $this->provider->onNotification(new NotificationEvent( 154 | 'test', 155 | NotificationEvent::TYPE_SUBSCRIPTION, 156 | new Notification(123, "test", []) 157 | ), NotificationEvent::TYPE_SUBSCRIPTION, $dispatcher); 158 | 159 | $this->assertFalse($result); 160 | } 161 | 162 | public function testOnMessageReceived() 163 | { 164 | $result = $this->provider->onMessageReceived(new MessageEvent( 165 | 'test', 166 | new Message(123, ['foo' => 'bar'], []) 167 | )); 168 | 169 | $this->assertFalse($result); 170 | } 171 | 172 | public function testMergeOptions() 173 | { 174 | $options = ['message_delay' => 1, 'not_an_option' => false]; 175 | $merged = $this->provider->mergeOptions($options); 176 | 177 | $this->assertTrue($merged['message_delay'] === 1); 178 | $this->assertFalse(isset($merged['not_an_option'])); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/Provider/CustomProviderTest.php: -------------------------------------------------------------------------------- 1 | provider = $this->getCustomProvider(); 26 | } 27 | 28 | public function testGetProvider() 29 | { 30 | $provider = $this->provider->getProvider(); 31 | 32 | $this->assertEquals('Custom', $provider); 33 | } 34 | 35 | public function testPublish() 36 | { 37 | $this->setNoOpExpectation(); 38 | 39 | $this->provider->publish(['foo' => 'bar']); 40 | } 41 | 42 | public function testCreate() 43 | { 44 | $this->setNoOpExpectation(); 45 | 46 | $this->provider->create(); 47 | } 48 | 49 | public function testDestroy() 50 | { 51 | $this->setNoOpExpectation(); 52 | 53 | $this->provider->destroy(); 54 | } 55 | 56 | public function testDelete() 57 | { 58 | $this->setNoOpExpectation(); 59 | 60 | $this->provider->delete('foo'); 61 | } 62 | 63 | public function testReceive() 64 | { 65 | $this->setNoOpExpectation(); 66 | 67 | $this->provider->receive(); 68 | } 69 | 70 | 71 | protected function getCustomProvider() 72 | { 73 | $options = [ 74 | 'logging_enabled' => false, 75 | 'push_notifications' => true, 76 | 'notification_retries' => 3, 77 | 'message_delay' => 0, 78 | 'message_timeout' => 30, 79 | 'message_expiration' => 604800, 80 | 'messages_to_receive' => 1, 81 | 'receive_wait_time' => 3, 82 | 'subscribers' => [] 83 | ]; 84 | 85 | $cache = $this->createMock( 86 | 'Doctrine\Common\Cache\PhpFileCache', 87 | [], 88 | ['/tmp', 'qpush.custom.test.php'] 89 | ); 90 | 91 | $this->logger = $this->createMock( 92 | 'Monolog\Logger', [], ['qpush.test'] 93 | ); 94 | 95 | $this->mock = new CustomMockClient('custom', $options, null, $cache, $this->logger); 96 | 97 | return new CustomProvider('test', $options, $this->mock, $cache, $this->logger); 98 | } 99 | 100 | protected function setNoOpExpectation() 101 | { 102 | $this->logger 103 | ->expects($this->never()) 104 | ->method(new \PHPUnit_Framework_Constraint_IsAnything()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Provider/FileProviderTest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class FileProviderTest extends \PHPUnit_Framework_TestCase 13 | { 14 | /** @var FileProvider */ 15 | protected $provider; 16 | protected $basePath; 17 | protected $queueHash; 18 | protected $umask; 19 | 20 | public function setUp() 21 | { 22 | $this->umask = umask(0); 23 | $this->basePath = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.time().rand(0, 1000); 24 | mkdir($this->basePath); 25 | $this->provider = $this->getFileProvider(); 26 | } 27 | 28 | public function tearDown() 29 | { 30 | $this->clean($this->basePath); 31 | umask($this->umask); 32 | } 33 | 34 | /** 35 | * @param string $file 36 | */ 37 | protected function clean($file) 38 | { 39 | if (is_dir($file) && !is_link($file)) { 40 | $dir = new \FilesystemIterator($file); 41 | foreach ($dir as $childFile) { 42 | $this->clean($childFile); 43 | } 44 | 45 | rmdir($file); 46 | } else if (is_file($file)) { 47 | unlink($file); 48 | } 49 | } 50 | 51 | private function getFileProvider(array $options = []) 52 | { 53 | $options = array_merge( 54 | [ 55 | 'path' => $this->basePath, 56 | 'logging_enabled' => false, 57 | 'message_delay' => 0, 58 | 'message_timeout' => 30, 59 | 'message_expiration' => 604800, 60 | 'messages_to_receive' => 1, 61 | ], 62 | $options 63 | ); 64 | 65 | $cache = $this->createMock( 66 | 'Doctrine\Common\Cache\PhpFileCache', 67 | [], 68 | ['/tmp', 'qpush.aws.test.php'] 69 | ); 70 | 71 | $logger = $this->createMock( 72 | 'Symfony\Bridge\Monolog\Logger', [], ['qpush.test'] 73 | ); 74 | 75 | $this->queueHash = str_replace('-', '', md5('test')); 76 | 77 | return new FileProvider('test', $options, null, $cache, $logger); 78 | } 79 | 80 | public function testGetProvider() 81 | { 82 | $provider = $this->provider->getProvider(); 83 | 84 | $this->assertEquals('File', $provider); 85 | } 86 | 87 | public function testCreate() 88 | { 89 | $this->assertTrue($this->provider->create()); 90 | $this->assertTrue(is_readable($this->basePath.DIRECTORY_SEPARATOR.$this->queueHash)); 91 | $this->assertTrue(is_writable($this->basePath.DIRECTORY_SEPARATOR.$this->queueHash)); 92 | } 93 | 94 | public function testDestroy() 95 | { 96 | $this->provider->destroy(); 97 | $this->assertFalse(is_dir($this->basePath.DIRECTORY_SEPARATOR.$this->queueHash)); 98 | } 99 | 100 | public function testReceive() 101 | { 102 | $this->provider->create(); 103 | $this->assertTrue(is_array($this->provider->receive())); 104 | } 105 | 106 | public function testDelete() 107 | { 108 | $this->provider->create(); 109 | 110 | $path = substr(hash('md5', '123'), 0, 3); 111 | mkdir($this->basePath.DIRECTORY_SEPARATOR.$this->queueHash.DIRECTORY_SEPARATOR.$path); 112 | touch($this->basePath.DIRECTORY_SEPARATOR.$this->queueHash.DIRECTORY_SEPARATOR.$path.DIRECTORY_SEPARATOR.'123.json'); 113 | 114 | $messages = $this->provider->receive(); 115 | $this->assertNotEmpty($messages); 116 | $this->assertTrue($this->provider->delete(123)); 117 | } 118 | 119 | public function testPublish() 120 | { 121 | $this->provider->create(); 122 | $content = [ 123 | ['testing'], 124 | ['testing 123'] 125 | ]; 126 | $this->provider->publish($content[0]); 127 | $this->provider->publish($content[1]); 128 | $messagesA = $this->provider->receive(); 129 | $this->assertEquals(1, count($messagesA)); 130 | $this->assertContains($messagesA[0]->getBody(), $content); 131 | $messagesB = $this->provider->receive(); 132 | $this->assertEquals(1, count($messagesB)); 133 | $this->assertContains($messagesB[0]->getBody(), $content); 134 | $this->assertNotEquals($messagesA[0]->getBody(), $messagesB[0]->getBody()); 135 | } 136 | 137 | public function testPublishDelay() { 138 | $this->provider->create(); 139 | $provider = $this->getFileProvider([ 140 | 'message_delay' => 2, 141 | ]); 142 | $provider->publish(['testing']); 143 | $messages = $provider->receive(); 144 | $this->assertEmpty($messages); 145 | } 146 | 147 | public function testOnMessageReceived() 148 | { 149 | $this->provider->create(); 150 | $id = $this->provider->publish(['foo' => 'bar']); 151 | $path = substr(hash('md5', $id), 0, 3); 152 | $this->assertTrue(is_file($this->basePath.DIRECTORY_SEPARATOR.$this->queueHash.DIRECTORY_SEPARATOR.$path.DIRECTORY_SEPARATOR.$id.'.json')); 153 | $this->provider->onMessageReceived(new MessageEvent( 154 | 'test', 155 | $this->provider->receive()[0] 156 | )); 157 | $this->assertFalse(is_file($this->basePath.DIRECTORY_SEPARATOR.$this->queueHash.DIRECTORY_SEPARATOR.$path.DIRECTORY_SEPARATOR.$id.'.json')); 158 | } 159 | 160 | public function testCleanUp() 161 | { 162 | $this->provider->create(); 163 | $provider = $this->getFileProvider([ 164 | 'message_expiration' => 10, 165 | ]); 166 | 167 | $id = $provider->publish(['testing']); 168 | $this->mockMessageAge($id, 3600); 169 | $id = $provider->publish(['testing 123']); 170 | $this->mockMessageAge($id, 3600); 171 | 172 | $provider->cleanUp(); 173 | 174 | $finder = new Finder(); 175 | $files = $finder->files()->in($this->basePath . DIRECTORY_SEPARATOR . $this->queueHash); 176 | $this->assertCount(0, $files); 177 | } 178 | 179 | /** 180 | * @see https://github.com/uecode/qpush-bundle/issues/93 181 | */ 182 | public function testCleanUpDoesNotRemoveCurrentMessages() { 183 | $this->provider->create(); 184 | $provider = $this->getFileProvider([ 185 | 'message_expiration' => 10, 186 | ]); 187 | $currentMessage = ['dont remove me']; 188 | 189 | $id = $provider->publish(['testing']); 190 | $this->mockMessageAge($id, 3600); 191 | $id = $provider->publish(['testing 123']); 192 | $this->mockMessageAge($id, 3600); 193 | $provider->publish($currentMessage); 194 | 195 | $provider->cleanUp(); 196 | $messages = $provider->receive(); 197 | $this->assertCount(1, $messages); 198 | $this->assertSame($currentMessage, $messages[0]->getBody()); 199 | } 200 | 201 | /** 202 | * @param string $id 203 | * @param int $ageInSeconds 204 | * @return string 205 | */ 206 | protected function mockMessageAge($id, $ageInSeconds) { 207 | $path = substr(hash('md5', $id), 0, 3); 208 | touch( 209 | $this->basePath.DIRECTORY_SEPARATOR.$this->queueHash.DIRECTORY_SEPARATOR.$path.DIRECTORY_SEPARATOR.$id.'.json', 210 | time() - $ageInSeconds 211 | ); 212 | } 213 | } -------------------------------------------------------------------------------- /tests/Provider/IronMqProviderTest.php: -------------------------------------------------------------------------------- 1 | 37 | */ 38 | class IronMqProviderTest extends \PHPUnit_Framework_TestCase 39 | { 40 | /** 41 | * Mock Client 42 | * 43 | * @var \stdClass 44 | */ 45 | protected $provider; 46 | 47 | public function setUp() 48 | { 49 | $this->provider = $this->getIronMqProvider(); 50 | } 51 | 52 | public function tearDown() 53 | { 54 | $this->provider = null; 55 | } 56 | 57 | private function getIronMqProvider(array $options = []) 58 | { 59 | $options = array_merge( 60 | [ 61 | 'logging_enabled' => false, 62 | 'push_notifications' => true, 63 | 'push_type' => 'multicast', 64 | 'notification_retries' => 3, 65 | 'notification_retries_delay' => 60, 66 | 'message_delay' => 0, 67 | 'message_timeout' => 30, 68 | 'message_expiration' => 604800, 69 | 'messages_to_receive' => 1, 70 | 'rate_limit' => -1, 71 | 'receive_wait_time' => 3, 72 | 'subscribers' => [ 73 | [ 'protocol' => 'http', 'endpoint' => 'http://fake.com' ] 74 | ] 75 | ], 76 | $options 77 | ); 78 | 79 | $client = new IronMqMockClient([ 80 | 'token' => '123_this_is_a_token', 81 | 'project_id' => '123_this_is_a_project_id', 82 | ]); 83 | 84 | return new IronMqProvider( 85 | 'test', 86 | $options, 87 | $client, 88 | $this->createMock( 89 | 'Doctrine\Common\Cache\PhpFileCache', 90 | [], 91 | ['/tmp', 'qpush.ironmq.test.php'] 92 | ), 93 | $this->createMock( 94 | 'Symfony\Bridge\Monolog\Logger', 95 | [], 96 | ['qpush.test'] 97 | ) 98 | ); 99 | } 100 | 101 | public function testGetProvider() 102 | { 103 | $provider = $this->provider->getProvider(); 104 | 105 | $this->assertEquals('IronMQ', $provider); 106 | } 107 | 108 | public function testCreate() 109 | { 110 | $this->assertFalse($this->provider->queueExists()); 111 | 112 | $this->assertTrue($this->provider->queueExists()); 113 | 114 | $this->assertTrue($this->provider->create()); 115 | $this->assertTrue($this->provider->queueExists()); 116 | 117 | $provider = $this->getIronMqProvider([ 118 | 'subscribers' => [ 119 | [ 'protocol' => 'email', 'endpoint' => 'test@foo.com' ] 120 | ] 121 | ]); 122 | 123 | $this->setExpectedException('InvalidArgumentException'); 124 | $provider->create(); 125 | } 126 | 127 | public function testDestroy() 128 | { 129 | // First call returns true when the queue exists 130 | $this->assertTrue($this->provider->destroy()); 131 | 132 | // Second call catches exception and returns true when the queue 133 | // does not exists 134 | $this->assertTrue($this->provider->destroy()); 135 | 136 | // Last call throws an exception if there is an exception outside 137 | // of a HTTP 404 138 | $this->setExpectedException('Exception'); 139 | $this->provider->destroy(); 140 | } 141 | 142 | public function testPublish() 143 | { 144 | $provider = $this->getIronMqProvider([ 145 | 'push_notifications' => false 146 | ]); 147 | 148 | $this->assertEquals(123, $provider->publish(['foo' => 'bar'])); 149 | } 150 | 151 | public function testReceive() 152 | { 153 | $messages = $this->provider->receive(); 154 | $this->assertInternalType('array', $messages); 155 | $this->assertEquals(['foo' => 'bar'], $messages[0]->getBody()); 156 | } 157 | 158 | public function testDelete() 159 | { 160 | // First call returns true when the queue exists 161 | $this->assertTrue($this->provider->delete(123)); 162 | 163 | // Second call catches exception and returns true when the queue 164 | // does not exists 165 | $this->assertTrue($this->provider->delete(456)); 166 | 167 | // Last call throws an exception if there is an exception outside 168 | // of a HTTP 404 169 | $this->setExpectedException('Exception'); 170 | $this->provider->delete(789); 171 | } 172 | 173 | public function testOnNotification() 174 | { 175 | $event = new NotificationEvent( 176 | 'test', 177 | NotificationEvent::TYPE_MESSAGE, 178 | new Notification(123, "test", []) 179 | ); 180 | 181 | $this->provider->onNotification( 182 | $event, 183 | NotificationEvent::TYPE_MESSAGE, 184 | $this->createMock('Symfony\Component\EventDispatcher\EventDispatcherInterface') 185 | ); 186 | } 187 | 188 | public function testOnMessageReceived() 189 | { 190 | $this->provider->onMessageReceived(new MessageEvent( 191 | 'test', 192 | new Message(123, ['foo' => 'bar'], []) 193 | )); 194 | } 195 | 196 | public function testQueueInfo() 197 | { 198 | $this->assertNull($this->provider->queueInfo()); 199 | 200 | $this->provider->create(); 201 | $queue = $this->provider->queueInfo(); 202 | $this->assertEquals('530295fe3c94fbcf0c79cffe', $queue->id); 203 | $this->assertEquals('test', $queue->name); 204 | $this->assertEquals('52f67d032001c00005000057', $queue->project_id); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /tests/Provider/ProviderRegisteryTest.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class ProviderRegistryTest extends \PHPUnit_Framework_TestCase 31 | { 32 | public function testRegistry() 33 | { 34 | $registry = new ProviderRegistry(); 35 | $interface = 'Uecode\Bundle\QPushBundle\Provider\ProviderInterface'; 36 | 37 | $registry->addProvider('test', $this->createMock($interface)); 38 | 39 | $this->assertEquals(['test' => $this->createMock($interface)], $registry->all()); 40 | 41 | $this->assertTrue($registry->has('test')); 42 | 43 | $this->assertEquals($this->createMock($interface), $registry->get('test')); 44 | 45 | $this->setExpectedException('InvalidArgumentException'); 46 | $registry->get('foo'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Provider/SyncProviderTest.php: -------------------------------------------------------------------------------- 1 | dispatcher = $this->createMock( 29 | 'Symfony\Component\EventDispatcher\EventDispatcherInterface' 30 | ); 31 | 32 | $this->provider = $this->getSyncProvider(); 33 | } 34 | 35 | public function testGetProvider() 36 | { 37 | $provider = $this->provider->getProvider(); 38 | 39 | $this->assertEquals('Sync', $provider); 40 | } 41 | 42 | public function testPublish() 43 | { 44 | $this->dispatcher 45 | ->expects($this->once()) 46 | ->method('dispatch') 47 | ->with( 48 | Events::Message($this->provider->getName()), 49 | new \PHPUnit_Framework_Constraint_IsInstanceOf('Uecode\Bundle\QPushBundle\Event\MessageEvent') 50 | ); 51 | 52 | $this->provider->publish(['foo' => 'bar']); 53 | } 54 | 55 | public function testCreate() 56 | { 57 | $this->setNoOpExpectation(); 58 | 59 | $this->provider->create(); 60 | } 61 | 62 | public function testDestroy() 63 | { 64 | $this->setNoOpExpectation(); 65 | 66 | $this->provider->destroy(); 67 | } 68 | 69 | public function testDelete() 70 | { 71 | $this->setNoOpExpectation(); 72 | 73 | $this->provider->delete('foo'); 74 | } 75 | 76 | public function testReceive() 77 | { 78 | $this->setNoOpExpectation(); 79 | 80 | $this->provider->receive(); 81 | } 82 | 83 | 84 | protected function getSyncProvider() 85 | { 86 | $options = [ 87 | 'logging_enabled' => false, 88 | 'push_notifications' => true, 89 | 'notification_retries' => 3, 90 | 'message_delay' => 0, 91 | 'message_timeout' => 30, 92 | 'message_expiration' => 604800, 93 | 'messages_to_receive' => 1, 94 | 'receive_wait_time' => 3, 95 | 'subscribers' => [ 96 | [ 'protocol' => 'http', 'endpoint' => 'http://fake.com' ] 97 | ] 98 | ]; 99 | 100 | $cache = $this->createMock( 101 | 'Doctrine\Common\Cache\PhpFileCache', 102 | [], 103 | ['/tmp', 'qpush.aws.test.php'] 104 | ); 105 | 106 | $this->logger = $this->createMock( 107 | 'Monolog\Logger', [], ['qpush.test'] 108 | ); 109 | 110 | return new SyncProvider('test', $options, $this->dispatcher, $cache, $this->logger); 111 | } 112 | 113 | protected function setNoOpExpectation() 114 | { 115 | $this->dispatcher 116 | ->expects($this->never()) 117 | ->method(new \PHPUnit_Framework_Constraint_IsAnything()); 118 | 119 | $this->logger 120 | ->expects($this->never()) 121 | ->method(new \PHPUnit_Framework_Constraint_IsAnything()); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Provider/TestProvider.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | class TestProvider extends AbstractProvider 35 | { 36 | /** 37 | * Mock Client 38 | * 39 | * @var stdClass 40 | */ 41 | protected $client; 42 | 43 | public function __construct($name, array $options, $client, Cache $cache, Logger $logger) 44 | { 45 | $this->name = $name; 46 | $this->options = $options; 47 | $this->client = $client; 48 | $this->cache = $cache; 49 | $this->logger = $logger; 50 | } 51 | 52 | public function getProvider() 53 | { 54 | return 'TestProvider'; 55 | } 56 | 57 | /** 58 | * @codeCoverageIgnore 59 | */ 60 | public function create() { } 61 | 62 | /** 63 | * @codeCoverageIgnore 64 | */ 65 | public function publish(array $message, array $options = []) { } 66 | 67 | /** 68 | * @codeCoverageIgnore 69 | */ 70 | public function receive(array $options = []) { } 71 | 72 | /** 73 | * @codeCoverageIgnore 74 | */ 75 | public function delete($id) { } 76 | 77 | /** 78 | * @codeCoverageIgnore 79 | */ 80 | public function destroy() { } 81 | } 82 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('Uecode\\Bundle\\QPushBundle\\Tests\\', __DIR__); 35 | --------------------------------------------------------------------------------