├── .github └── CODEOWNERS ├── .gitignore ├── .styleci.yml ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── changelog.md ├── composer.json ├── config └── pubsub.php ├── docker-compose.yml ├── phpunit.php ├── phpunit.xml ├── src ├── PubSubConnectionFactory.php ├── PubSubFacade.php ├── PubSubManager.php ├── PubSubServiceProvider.php ├── SubscriberMakeCommand.php └── stubs │ └── subscriber.stub └── tests ├── PubSubConnectionFactoryTest.php ├── PubSubManagerTest.php └── TestCase.php /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | # More details are here: https://help.github.com/articles/about-codeowners/ 4 | # The '*' pattern is global owners. 5 | # Order is important. The last matching pattern has the most precedence. 6 | 7 | * @Superbalist/core 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | vendor 4 | bin 5 | coverage 6 | coverage.xml -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | 3 | enabled: 4 | - alpha_ordered_imports 5 | - binary_operator_spaces 6 | - blank_line_after_opening_tag 7 | - cast_spaces 8 | - concat_with_spaces 9 | - const_visibility_required 10 | - declare_equal_normalize 11 | - function_typehint_space 12 | - hash_to_slash_comment 13 | - heredoc_to_nowdoc 14 | - include 15 | - lowercase_cast 16 | - method_separation 17 | - native_function_casing 18 | - new_with_braces 19 | - no_blank_lines_after_class_opening 20 | - no_blank_lines_after_phpdoc 21 | - no_blank_lines_after_return 22 | - no_blank_lines_after_throw 23 | - no_blank_lines_between_imports 24 | - no_blank_lines_between_traits 25 | - no_empty_statement 26 | - no_extra_consecutive_blank_lines 27 | - no_leading_import_slash 28 | - no_leading_namespace_whitespace 29 | - no_multiline_whitespace_around_double_arrow 30 | - no_short_bool_cast 31 | - no_short_echo_tag 32 | - no_singleline_whitespace_before_semicolons 33 | - no_spaces_inside_offset 34 | - no_spaces_outside_offset 35 | - no_trailing_comma_in_list_call 36 | - no_trailing_comma_in_singleline_array 37 | - no_unneeded_control_parentheses 38 | - no_unreachable_default_argument_value 39 | - no_unused_imports 40 | - no_useless_return 41 | - no_whitespace_before_comma_in_array 42 | - no_whitespace_in_blank_line 43 | - normalize_index_brace 44 | - object_operator_without_whitespace 45 | - phpdoc_add_missing_param_annotation 46 | - phpdoc_indent 47 | - phpdoc_inline_tag 48 | - phpdoc_link_to_see 49 | - phpdoc_no_access 50 | - phpdoc_no_empty_return 51 | - phpdoc_no_package 52 | - phpdoc_order 53 | - phpdoc_property 54 | - phpdoc_scalar 55 | - phpdoc_separation 56 | - phpdoc_single_line_var_spacing 57 | - phpdoc_to_comment 58 | - phpdoc_trim 59 | - phpdoc_type_to_var 60 | - phpdoc_types 61 | - phpdoc_var_without_name 62 | - print_to_echo 63 | - self_accessor 64 | - short_array_syntax 65 | - short_scalar_cast 66 | - single_blank_line_before_namespace 67 | - single_quote 68 | - space_after_semicolon 69 | - standardize_not_equals 70 | - ternary_operator_spaces 71 | - trailing_comma_in_multiline_array 72 | - trim_array_spaces 73 | - unalign_double_arrow 74 | - unalign_equals 75 | - unary_operator_spaces 76 | - whitespace_after_comma_in_array 77 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - 7.1 7 | - nightly 8 | 9 | before_script: 10 | - composer install 11 | 12 | script: ./vendor/bin/phpunit --configuration phpunit.xml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.0-fpm 2 | MAINTAINER Superbalist 3 | 4 | RUN mkdir /opt/laravel-pubsub 5 | WORKDIR /opt/laravel-pubsub 6 | 7 | # Packages 8 | RUN apt-get update \ 9 | && DEBIAN_FRONTEND=noninteractive apt-get install -y \ 10 | git \ 11 | zlib1g-dev \ 12 | unzip \ 13 | python \ 14 | && ( \ 15 | cd /tmp \ 16 | && mkdir librdkafka \ 17 | && cd librdkafka \ 18 | && git clone https://github.com/edenhill/librdkafka.git . \ 19 | && ./configure \ 20 | && make \ 21 | && make install \ 22 | ) \ 23 | && rm -r /var/lib/apt/lists/* 24 | 25 | # PHP Extensions 26 | RUN docker-php-ext-install -j$(nproc) zip \ 27 | && ( \ 28 | cd /tmp \ 29 | && mkdir php-rdkafka \ 30 | && cd php-rdkafka \ 31 | && git clone https://github.com/arnaud-lb/php-rdkafka.git . \ 32 | && git checkout php7 \ 33 | && phpize \ 34 | && ./configure \ 35 | && make -j$(nproc) \ 36 | && make install \ 37 | ) \ 38 | && rm -rf /tmp/php-rdkafka \ 39 | && docker-php-ext-enable rdkafka 40 | 41 | # Composer 42 | ENV COMPOSER_HOME /composer 43 | ENV PATH /composer/vendor/bin:$PATH 44 | ENV COMPOSER_ALLOW_SUPERUSER 1 45 | RUN curl -o /tmp/composer-setup.php https://getcomposer.org/installer \ 46 | && curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig \ 47 | && php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" \ 48 | && php /tmp/composer-setup.php --no-ansi --install-dir=/usr/local/bin --filename=composer --version=1.1.0 && rm -rf /tmp/composer-setup.php 49 | 50 | # Install Composer Application Dependencies 51 | COPY composer.json /opt/laravel-pubsub/ 52 | RUN composer install --no-autoloader --no-scripts --no-interaction 53 | RUN composer require superbalist/php-pubsub-kafka 54 | 55 | COPY config /opt/laravel-pubsub/config 56 | COPY src /opt/laravel-pubsub/src 57 | COPY tests /opt/laravel-pubsub/tests 58 | COPY phpunit.php /opt/laravel-pubsub/ 59 | COPY phpunit.xml /opt/laravel-pubsub/ 60 | 61 | RUN composer dump-autoload --no-interaction 62 | 63 | CMD ["/bin/bash"] 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Superbalist.com a division of Takealot Online (Pty) Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tests 2 | 3 | up: 4 | @docker-compose rm -f 5 | @docker-compose pull 6 | @sed -e "s/HOSTIP/$$(docker-machine ip)/g" docker-compose.yml | docker-compose --file - up --build -d 7 | @docker-compose run laravel-pubsub /bin/bash 8 | 9 | down: 10 | @docker-compose stop -t 1 11 | 12 | tests: 13 | @./vendor/bin/phpunit --configuration phpunit.xml 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-pubsub 2 | 3 | A Pub-Sub abstraction for Laravel. 4 | 5 | [![Author](http://img.shields.io/badge/author-@superbalist-blue.svg?style=flat-square)](https://twitter.com/superbalist) 6 | [![Build Status](https://img.shields.io/travis/Superbalist/laravel-pubsub/master.svg?style=flat-square)](https://travis-ci.org/Superbalist/laravel-pubsub) 7 | [![StyleCI](https://styleci.io/repos/67405993/shield?branch=master)](https://styleci.io/repos/67405993) 8 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 9 | [![Packagist Version](https://img.shields.io/packagist/v/superbalist/laravel-pubsub.svg?style=flat-square)](https://packagist.org/packages/superbalist/laravel-pubsub) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/superbalist/laravel-pubsub.svg?style=flat-square)](https://packagist.org/packages/superbalist/laravel-pubsub) 11 | 12 | This package is a wrapper bridging [php-pubsub](https://github.com/Superbalist/php-pubsub) into Laravel. 13 | 14 | For **Laravel 4** support, use the package https://github.com/Superbalist/laravel4-pubsub 15 | 16 | Please note that **Laravel 5.3** is only supported up until version 2.0.2. 17 | 18 | 2.0.3+ supports **Laravel 5.4 and up** moving forward. 19 | 20 | The following adapters are supported: 21 | * Local 22 | * /dev/null 23 | * Redis 24 | * Kafka (see separate installation instructions below) 25 | * Google Cloud 26 | * HTTP 27 | 28 | ## Installation 29 | 30 | ```bash 31 | composer require superbalist/laravel-pubsub 32 | ``` 33 | 34 | Register the service provider in app.php 35 | ```php 36 | 'providers' => [ 37 | // ... 38 | Superbalist\LaravelPubSub\PubSubServiceProvider::class, 39 | ] 40 | ``` 41 | 42 | Register the facade in app.php 43 | ```php 44 | 'aliases' => [ 45 | // ... 46 | 'PubSub' => Superbalist\LaravelPubSub\PubSubFacade::class, 47 | ] 48 | ``` 49 | 50 | The package has a default configuration which uses the following environment variables. 51 | ``` 52 | PUBSUB_CONNECTION=redis 53 | 54 | REDIS_HOST=localhost 55 | REDIS_PASSWORD=null 56 | REDIS_PORT=6379 57 | 58 | KAFKA_BROKERS=localhost 59 | 60 | GOOGLE_CLOUD_PROJECT_ID=your-project-id-here 61 | GOOGLE_CLOUD_KEY_FILE=path/to/your/gcloud-key.json 62 | 63 | HTTP_PUBSUB_URI=null 64 | HTTP_PUBSUB_SUBSCRIBE_CONNECTION=redis 65 | ``` 66 | 67 | To customize the configuration file, publish the package configuration using Artisan. 68 | ```bash 69 | php artisan vendor:publish --provider="Superbalist\LaravelPubSub\PubSubServiceProvider" 70 | ``` 71 | 72 | You can then edit the generated config at `app/config/pubsub.php`. 73 | 74 | ## Kafka Adapter Installation 75 | 76 | Please note that whilst the package is bundled with support for the [php-pubsub-kafka](https://github.com/Superbalist/php-pubsub-kafka) 77 | adapter, the adapter is not included by default. 78 | 79 | This is because the KafkaPubSubAdapter has an external dependency on the `librdkafka c library` and the `php-rdkafka` 80 | PECL extension. 81 | 82 | If you plan on using this adapter, you will need to install these dependencies by following these [installation instructions](https://github.com/Superbalist/php-pubsub-kafka). 83 | 84 | You can then include the adapter using: 85 | ```bash 86 | composer require superbalist/php-pubsub-kafka 87 | ``` 88 | 89 | ## Usage 90 | 91 | ```php 92 | // get the pub-sub manager 93 | $pubsub = app('pubsub'); 94 | 95 | // note: function calls on the manager are proxied through to the default connection 96 | // eg: you can do this on the manager OR a connection 97 | $pubsub->publish('channel_name', 'message'); 98 | 99 | // get the default connection 100 | $pubsub = app('pubsub.connection'); 101 | // or 102 | $pubsub = app(\Superbalist\PubSub\PubSubAdapterInterface::class); 103 | 104 | // get a specific connection 105 | $pubsub = app('pubsub')->connection('redis'); 106 | 107 | // publish a message 108 | // the message can be a string, array, bool, object - anything which can be json encoded 109 | $pubsub->publish('channel_name', 'this is where your message goes'); 110 | $pubsub->publish('channel_name', ['key' => 'value']); 111 | $pubsub->publish('channel_name', true); 112 | 113 | // publish multiple messages 114 | $messages = [ 115 | 'message 1', 116 | 'message 2', 117 | ]; 118 | $pubsub->publishBatch('channel_name', $messages); 119 | 120 | // subscribe to a channel 121 | $pubsub->subscribe('channel_name', function ($message) { 122 | var_dump($message); 123 | }); 124 | 125 | // all the above commands can also be done using the facade 126 | PubSub::connection('kafka')->publish('channel_name', 'Hello World!'); 127 | 128 | PubSub::connection('kafka')->subscribe('channel_name', function ($message) { 129 | var_dump($message); 130 | }); 131 | ``` 132 | 133 | ## Creating a Subscriber 134 | 135 | The package includes a helper command `php artisan make:subscriber MyExampleSubscriber` to stub new subscriber command classes. 136 | 137 | A lot of pub-sub adapters will contain blocking `subscribe()` calls, so these commands are best run as daemons running 138 | as a [supervisor](http://supervisord.org) process. 139 | 140 | This generator command will create the file `app/Console/Commands/MyExampleSubscriber.php` which will contain: 141 | ```php 142 | pubsub = $pubsub; 180 | } 181 | 182 | /** 183 | * Execute the console command. 184 | */ 185 | public function handle() 186 | { 187 | $this->pubsub->subscribe('channel_name', function ($message) { 188 | 189 | }); 190 | } 191 | } 192 | ``` 193 | 194 | ### Kafka Subscribers ### 195 | 196 | For subscribers which use the `php-pubsub-kafka` adapter, you'll likely want to change the `consumer_group_id` per 197 | subscriber. 198 | 199 | To do this, you need to use the `PubSubConnectionFactory` to create a new connection per subscriber. This is because 200 | the `consumer_group_id` cannot be changed once a connection is created. 201 | 202 | Here is an example of how you can do this: 203 | 204 | ```php 205 | pubsub = $factory->make('kafka', $config); 246 | } 247 | 248 | /** 249 | * Execute the console command. 250 | */ 251 | public function handle() 252 | { 253 | $this->pubsub->subscribe('channel_name', function ($message) { 254 | 255 | }); 256 | } 257 | } 258 | ``` 259 | 260 | ## Adding a Custom Driver 261 | 262 | Please see the [php-pubsub](https://github.com/Superbalist/php-pubsub) documentation **Writing an Adapter**. 263 | 264 | To include your custom driver, you can call the `extend()` function. 265 | 266 | ```php 267 | $manager = app('pubsub'); 268 | $manager->extend('custom_connection_name', function ($config) { 269 | // your callable must return an instance of the PubSubAdapterInterface 270 | return new MyCustomPubSubDriver($config); 271 | }); 272 | 273 | // get an instance of your custom connection 274 | $pubsub = $manager->connection('custom_connection_name'); 275 | ``` 276 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.0.0 - 2017-07-25 4 | 5 | * Bump up to superbalist/php-pubsub-google-cloud ^5.0 which allows for background daemon support 6 | * Add new background_batching and background_daemon options to Google Cloud adapter 7 | 8 | ## 2.0.5 - 2017-07-03 9 | 10 | * Add support for using a custom auth cache with the Google Cloud adapter 11 | 12 | ## 2.0.4 - 2017-05-24 13 | 14 | * Add support for HTTP adapter 15 | 16 | ## 2.0.3 - 2017-05-19 17 | 18 | * Bump illuminate/support & illuminate/console to ^5.4 19 | * Fix compatibility with Laravel 5.4 by switching to makeWith method on container (@mathieutu) 20 | 21 | ## 2.0.2 - 2017-05-16 22 | 23 | * Allow for superbalist/php-pubsub ^2.0 24 | * Allow for superbalist/php-pubsub-redis ^2.0 25 | * Allow for superbalist/php-pubsub-google-cloud ^4.0 26 | 27 | ## 2.0.1 - 2017-01-03 28 | 29 | * Allow for superbalist/php-pubsub-google-cloud ^3.0 30 | 31 | ## 2.0.0 - 2016-10-05 32 | 33 | * Bump up version ^2.0 of superbalist/php-pubsub-google-cloud 34 | * Add support for `client_identifier` config var added to superbalist/php-pubsub-google-cloud package 35 | 36 | ## 1.0.2 - 2016-09-15 37 | 38 | * Add support for configuring auto topic & subscription creation for the php-pubsub-google-cloud adapter 39 | 40 | ## 1.0.1 - 2016-09-08 41 | 42 | * Fix for breaking change in php-pubsub-kafka package 43 | * Fix params passed to Laravel's container->make() function 44 | 45 | ## 1.0.0 - 2016-09-06 46 | 47 | * Initial release -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superbalist/laravel-pubsub", 3 | "description": "A Pub-Sub abstraction for Laravel", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Superbalist.com a division of Takealot Online (Pty) Ltd", 8 | "email": "info@superbalist.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.6.0", 13 | "illuminate/support": "^5.4", 14 | "superbalist/php-pubsub": "^1.0|^2.0", 15 | "superbalist/php-pubsub-redis": "^1.0|^2.0", 16 | "superbalist/php-pubsub-google-cloud": "^5.0", 17 | "illuminate/console": "^5.4", 18 | "superbalist/php-pubsub-http": "^1.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Superbalist\\LaravelPubSub\\": "src/", 23 | "Tests\\": "tests/" 24 | } 25 | }, 26 | "extra": { 27 | "branch-alias": { 28 | "dev-master": "1.0-dev" 29 | } 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^5.5", 33 | "mockery/mockery": "^0.9.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /config/pubsub.php: -------------------------------------------------------------------------------- 1 | env('PUBSUB_CONNECTION', 'redis'), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Pub-Sub Connections 20 | |-------------------------------------------------------------------------- 21 | | 22 | | The available pub-sub connections to use. 23 | | 24 | | A default configuration has been provided for all adapters shipped with 25 | | the package. 26 | | 27 | */ 28 | 29 | 'connections' => [ 30 | 31 | '/dev/null' => [ 32 | 'driver' => '/dev/null', 33 | ], 34 | 35 | 'local' => [ 36 | 'driver' => 'local', 37 | ], 38 | 39 | 'redis' => [ 40 | 'driver' => 'redis', 41 | 'scheme' => 'tcp', 42 | 'host' => env('REDIS_HOST', 'localhost'), 43 | 'password' => env('REDIS_PASSWORD', null), 44 | 'port' => env('REDIS_PORT', 6379), 45 | 'database' => 0, 46 | 'read_write_timeout' => 0, 47 | ], 48 | 49 | 'kafka' => [ 50 | 'driver' => 'kafka', 51 | 'consumer_group_id' => 'php-pubsub', 52 | 'brokers' => env('KAFKA_BROKERS', 'localhost'), 53 | ], 54 | 55 | 'gcloud' => [ 56 | 'driver' => 'gcloud', 57 | 'project_id' => env('GOOGLE_CLOUD_PROJECT_ID'), 58 | 'key_file' => env('GOOGLE_CLOUD_KEY_FILE'), 59 | 'client_identifier' => null, 60 | 'auto_create_topics' => true, 61 | 'auto_create_subscriptions' => true, 62 | 'auth_cache' => null, // eg: \Google\Auth\Cache\MemoryCacheItemPool::class, 63 | 'background_batching' => false, 64 | 'background_daemon' => false, 65 | ], 66 | 67 | 'http' => [ 68 | 'driver' => 'http', 69 | 'uri' => env('HTTP_PUBSUB_URI'), 70 | 'subscribe_connection' => env('HTTP_PUBSUB_SUBSCRIBE_CONNECTION', 'redis'), 71 | ], 72 | 73 | ], 74 | 75 | ]; 76 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | laravel-pubsub: 4 | build: . 5 | volumes: 6 | - ./config:/opt/laravel-pubsub/config 7 | - ./src:/opt/laravel-pubsub/src 8 | - ./tests:/opt/laravel-pubsub/tests 9 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | ./src/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/PubSubConnectionFactory.php: -------------------------------------------------------------------------------- 1 | container = $container; 28 | } 29 | 30 | /** 31 | * Factory a PubSubAdapterInterface. 32 | * 33 | * @param string $driver 34 | * @param array $config 35 | * 36 | * @return PubSubAdapterInterface 37 | */ 38 | public function make($driver, array $config = []) 39 | { 40 | switch ($driver) { 41 | case '/dev/null': 42 | return new DevNullPubSubAdapter(); 43 | case 'local': 44 | return new LocalPubSubAdapter(); 45 | case 'redis': 46 | return $this->makeRedisAdapter($config); 47 | case 'kafka': 48 | return $this->makeKafkaAdapter($config); 49 | case 'gcloud': 50 | return $this->makeGoogleCloudAdapter($config); 51 | case 'http': 52 | return $this->makeHTTPAdapter($config); 53 | } 54 | 55 | throw new InvalidArgumentException(sprintf('The driver [%s] is not supported.', $driver)); 56 | } 57 | 58 | /** 59 | * Factory a RedisPubSubAdapter. 60 | * 61 | * @param array $config 62 | * 63 | * @return RedisPubSubAdapter 64 | */ 65 | protected function makeRedisAdapter(array $config) 66 | { 67 | if (!isset($config['read_write_timeout'])) { 68 | $config['read_write_timeout'] = 0; 69 | } 70 | 71 | $client = $this->container->makeWith('pubsub.redis.redis_client', ['config' => $config]); 72 | 73 | return new RedisPubSubAdapter($client); 74 | } 75 | 76 | /** 77 | * Factory a KafkaPubSubAdapter. 78 | * 79 | * @param array $config 80 | * 81 | * @return KafkaPubSubAdapter 82 | */ 83 | protected function makeKafkaAdapter(array $config) 84 | { 85 | // create producer 86 | $producer = $this->container->makeWith('pubsub.kafka.producer'); 87 | $producer->addBrokers($config['brokers']); 88 | 89 | // create consumer 90 | $topicConf = $this->container->makeWith('pubsub.kafka.topic_conf'); 91 | $topicConf->set('auto.offset.reset', 'smallest'); 92 | 93 | $conf = $this->container->makeWith('pubsub.kafka.conf'); 94 | $conf->set('group.id', array_get($config, 'consumer_group_id', 'php-pubsub')); 95 | $conf->set('metadata.broker.list', $config['brokers']); 96 | $conf->set('enable.auto.commit', 'false'); 97 | $conf->set('offset.store.method', 'broker'); 98 | $conf->setDefaultTopicConf($topicConf); 99 | 100 | $consumer = $this->container->makeWith('pubsub.kafka.consumer', ['conf' => $conf]); 101 | 102 | return new KafkaPubSubAdapter($producer, $consumer); 103 | } 104 | 105 | /** 106 | * Factory a GoogleCloudPubSubAdapter. 107 | * 108 | * @param array $config 109 | * 110 | * @return GoogleCloudPubSubAdapter 111 | */ 112 | protected function makeGoogleCloudAdapter(array $config) 113 | { 114 | $clientConfig = [ 115 | 'projectId' => $config['project_id'], 116 | 'keyFilePath' => $config['key_file'], 117 | ]; 118 | if (isset($config['auth_cache'])) { 119 | $clientConfig['authCache'] = $this->container->make($config['auth_cache']); 120 | } 121 | 122 | $client = $this->container->makeWith('pubsub.gcloud.pub_sub_client', ['config' => $clientConfig]); 123 | 124 | $clientIdentifier = array_get($config, 'client_identifier'); 125 | $autoCreateTopics = array_get($config, 'auto_create_topics', true); 126 | $autoCreateSubscriptions = array_get($config, 'auto_create_subscriptions', true); 127 | $backgroundBatching = array_get($config, 'background_batching', false); 128 | $backgroundDaemon = array_get($config, 'background_daemon', false); 129 | 130 | if ($backgroundDaemon) { 131 | putenv('IS_BATCH_DAEMON_RUNNING=true'); 132 | } 133 | return new GoogleCloudPubSubAdapter( 134 | $client, 135 | $clientIdentifier, 136 | $autoCreateTopics, 137 | $autoCreateSubscriptions, 138 | $backgroundBatching 139 | ); 140 | } 141 | 142 | /** 143 | * Factory a HTTPPubSubAdapter. 144 | * 145 | * @param array $config 146 | * 147 | * @return HTTPPubSubAdapter 148 | */ 149 | protected function makeHTTPAdapter(array $config) 150 | { 151 | $client = $this->container->make('pubsub.http.client'); 152 | $adapter = $this->make( 153 | $config['subscribe_connection_config']['driver'], 154 | $config['subscribe_connection_config'] 155 | ); 156 | return new HTTPPubSubAdapter($client, $config['uri'], $adapter); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/PubSubFacade.php: -------------------------------------------------------------------------------- 1 | app = $app; 38 | $this->factory = $factory; 39 | } 40 | 41 | /** 42 | * Return a pub-sub adapter instance. 43 | * 44 | * @param string $name 45 | * 46 | * @return PubSubAdapterInterface 47 | */ 48 | public function connection($name = null) 49 | { 50 | if ($name === null) { 51 | $name = $this->getDefaultConnection(); 52 | } 53 | 54 | if (!isset($this->connections[$name])) { 55 | $this->connections[$name] = $this->makeConnection($name); 56 | } 57 | 58 | return $this->connections[$name]; 59 | } 60 | 61 | /** 62 | * Make an instance of a pub-sub adapter interface. 63 | * 64 | * @param string $name 65 | * 66 | * @return PubSubAdapterInterface 67 | */ 68 | protected function makeConnection($name) 69 | { 70 | $config = $this->getConnectionConfig($name); 71 | 72 | if (isset($this->extensions[$name])) { 73 | return call_user_func($this->extensions[$name], $config, $name); 74 | } 75 | 76 | if (!isset($config['driver'])) { 77 | throw new InvalidArgumentException( 78 | sprintf('The pub-sub connection [%s] is missing a "driver" config var.', $name) 79 | ); 80 | } 81 | 82 | return $this->factory->make($config['driver'], array_except($config, ['driver'])); 83 | } 84 | 85 | /** 86 | * Return the pubsub config for the given connection. 87 | * 88 | * @param string $name 89 | * 90 | * @return array 91 | */ 92 | protected function getConnectionConfig($name) 93 | { 94 | $connections = $this->getConfig()['connections']; 95 | if (!isset($connections[$name])) { 96 | throw new InvalidArgumentException(sprintf('The pub-sub connection [%s] is not configured.', $name)); 97 | } 98 | 99 | $config = $connections[$name]; 100 | 101 | if (isset($config['subscribe_connection'])) { 102 | $config['subscribe_connection_config'] = $this->getConnectionConfig($config['subscribe_connection']); 103 | } 104 | 105 | return $config; 106 | } 107 | 108 | /** 109 | * Return the pubsub config array. 110 | * 111 | * @return array 112 | */ 113 | protected function getConfig() 114 | { 115 | $config = $this->app->make('config'); /* @var \Illuminate\Contracts\Config\Repository $config */ 116 | return $config->get('pubsub'); 117 | } 118 | 119 | /** 120 | * Return the default connection name. 121 | * 122 | * @return string 123 | */ 124 | public function getDefaultConnection() 125 | { 126 | return $this->getConfig()['default']; 127 | } 128 | 129 | /** 130 | * Set the default connection name. 131 | * 132 | * @param string $name 133 | */ 134 | public function setDefaultConnection($name) 135 | { 136 | $config = $this->app->make('config'); /* @var \Illuminate\Contracts\Config\Repository $config */ 137 | $config->set('pubsub.default', $name); 138 | } 139 | 140 | /** 141 | * Register an extension connection resolver. 142 | * 143 | * @param string $name 144 | * @param callable $resolver 145 | */ 146 | public function extend($name, callable $resolver) 147 | { 148 | $this->extensions[$name] = $resolver; 149 | } 150 | 151 | /** 152 | * Return all registered extension connection resolvers. 153 | * 154 | * @return array 155 | */ 156 | public function getExtensions() 157 | { 158 | return $this->extensions; 159 | } 160 | 161 | /** 162 | * Return all the created connections. 163 | * 164 | * @return array 165 | */ 166 | public function getConnections() 167 | { 168 | return $this->connections; 169 | } 170 | 171 | /** 172 | * Dynamically pass methods to the default connection. 173 | * 174 | * @param string $method 175 | * @param array $parameters 176 | * 177 | * @return mixed 178 | */ 179 | public function __call($method, $parameters) 180 | { 181 | return $this->connection()->$method(...$parameters); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/PubSubServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 19 | __DIR__ . '/../config/pubsub.php' => config_path('pubsub.php'), 20 | ]); 21 | } 22 | 23 | /** 24 | * Register bindings in the container. 25 | */ 26 | public function register() 27 | { 28 | $this->mergeConfigFrom(__DIR__ . '/../config/pubsub.php', 'pubsub'); 29 | 30 | $this->app->singleton('pubsub.factory', function ($app) { 31 | return new PubSubConnectionFactory($app); 32 | }); 33 | 34 | $this->app->singleton('pubsub', function ($app) { 35 | return new PubSubManager($app, $app['pubsub.factory']); 36 | }); 37 | 38 | $this->app->bind('pubsub.connection', PubSubAdapterInterface::class); 39 | 40 | $this->app->bind(PubSubAdapterInterface::class, function ($app) { 41 | $manager = $app['pubsub']; /* @var PubSubManager $manager */ 42 | return $manager->connection(); 43 | }); 44 | 45 | $this->registerAdapterDependencies(); 46 | 47 | $this->commands(SubscriberMakeCommand::class); 48 | } 49 | 50 | /** 51 | * Register adapter dependencies in the container. 52 | */ 53 | protected function registerAdapterDependencies() 54 | { 55 | $this->app->bind('pubsub.redis.redis_client', function ($app, $parameters) { 56 | return new RedisClient($parameters['config']); 57 | }); 58 | 59 | $this->app->bind('pubsub.gcloud.pub_sub_client', function ($app, $parameters) { 60 | return new GoogleCloudPubSubClient($parameters['config']); 61 | }); 62 | 63 | $this->app->bind('pubsub.kafka.topic_conf', function () { 64 | return new \RdKafka\TopicConf(); 65 | }); 66 | 67 | $this->app->bind('pubsub.kafka.producer', function () { 68 | return new \RdKafka\Producer(); 69 | }); 70 | 71 | $this->app->bind('pubsub.kafka.conf', function () { 72 | return new \RdKafka\Conf(); 73 | }); 74 | 75 | $this->app->bind('pubsub.kafka.consumer', function ($app, $parameters) { 76 | return new \RdKafka\KafkaConsumer($parameters['conf']); 77 | }); 78 | 79 | $this->app->bind('pubsub.http.client', function () { 80 | return new Client(); 81 | }); 82 | } 83 | 84 | /** 85 | * Get the services provided by the provider. 86 | * 87 | * @return array 88 | */ 89 | public function provides() 90 | { 91 | return [ 92 | 'pubsub', 93 | 'pubsub.factory', 94 | 'pubsub.connection', 95 | 'pubsub.redis.redis_client', 96 | 'pubsub.gcloud.pub_sub_client', 97 | 'pubsub.kafka.topic_conf', 98 | 'pubsub.kafka.producer', 99 | 'pubsub.kafka.consumer', 100 | 'pubsub.http.client', 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/SubscriberMakeCommand.php: -------------------------------------------------------------------------------- 1 | option('command'), $stub); 45 | } 46 | 47 | /** 48 | * Get the stub file for the generator. 49 | * 50 | * @return string 51 | */ 52 | protected function getStub() 53 | { 54 | return __DIR__ . '/stubs/subscriber.stub'; 55 | } 56 | 57 | /** 58 | * Get the default namespace for the class. 59 | * 60 | * @param string $rootNamespace 61 | * 62 | * @return string 63 | */ 64 | protected function getDefaultNamespace($rootNamespace) 65 | { 66 | return $rootNamespace . '\Console\Commands'; 67 | } 68 | 69 | /** 70 | * Get the console command arguments. 71 | * 72 | * @return array 73 | */ 74 | protected function getArguments() 75 | { 76 | return [ 77 | ['name', InputArgument::REQUIRED, 'The name of the subscriber.'], 78 | ]; 79 | } 80 | 81 | /** 82 | * Get the console command options. 83 | * 84 | * @return array 85 | */ 86 | protected function getOptions() 87 | { 88 | return [ 89 | [ 90 | 'command', 91 | null, 92 | InputOption::VALUE_OPTIONAL, 93 | 'The terminal command that should be assigned.', 94 | 'subscriber:name', 95 | ], 96 | ]; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/stubs/subscriber.stub: -------------------------------------------------------------------------------- 1 | pubsub = $pubsub; 39 | } 40 | 41 | /** 42 | * Execute the console command. 43 | */ 44 | public function handle() 45 | { 46 | $this->pubsub->subscribe('channel_name', function ($message) { 47 | 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/PubSubConnectionFactoryTest.php: -------------------------------------------------------------------------------- 1 | make('/dev/null'); 30 | $this->assertInstanceOf(DevNullPubSubAdapter::class, $adapter); 31 | } 32 | 33 | public function testMakeLocalAdapter() 34 | { 35 | $container = Mockery::mock(Container::class); 36 | 37 | $factory = new PubSubConnectionFactory($container); 38 | 39 | $adapter = $factory->make('local'); 40 | $this->assertInstanceOf(LocalPubSubAdapter::class, $adapter); 41 | } 42 | 43 | public function testMakeRedisAdapter() 44 | { 45 | $config = [ 46 | 'scheme' => 'tcp', 47 | 'host' => 'localhost', 48 | 'password' => null, 49 | 'port' => 6379, 50 | 'database' => 0, 51 | 'read_write_timeout' => 0, 52 | ]; 53 | 54 | $container = Mockery::mock(Container::class); 55 | $container->shouldReceive('makeWith') 56 | ->withArgs([ 57 | 'pubsub.redis.redis_client', 58 | ['config' => $config], 59 | ]) 60 | ->once() 61 | ->andReturn(Mockery::mock(RedisClient::class)); 62 | 63 | $factory = new PubSubConnectionFactory($container); 64 | 65 | $adapter = $factory->make('redis', $config); 66 | $this->assertInstanceOf(RedisPubSubAdapter::class, $adapter); 67 | } 68 | 69 | public function testMakeKafkaAdapter() 70 | { 71 | if (!class_exists('\Superbalist\PubSub\Kafka\KafkaPubSubAdapter')) { 72 | $this->markTestSkipped('KafkaPubSubAdapter is not installed'); 73 | } 74 | 75 | $config = [ 76 | 'consumer_group_id' => 'php-pubsub', 77 | 'brokers' => 'localhost', 78 | ]; 79 | 80 | $container = Mockery::mock(Container::class); 81 | 82 | $producer = Mockery::mock(\RdKafka\Producer::class); 83 | $producer->shouldReceive('addBrokers') 84 | ->with('localhost') 85 | ->once(); 86 | 87 | $container->shouldReceive('makeWith') 88 | ->with('pubsub.kafka.producer') 89 | ->once() 90 | ->andReturn($producer); 91 | 92 | $topicConf = Mockery::mock(\RdKafka\TopicConf::class); 93 | $topicConf->shouldReceive('set'); 94 | 95 | $container->shouldReceive('makeWith') 96 | ->with('pubsub.kafka.topic_conf') 97 | ->once() 98 | ->andReturn($topicConf); 99 | 100 | $conf = Mockery::mock(\RdKafka\Conf::class); 101 | $conf->shouldReceive('set') 102 | ->withArgs([ 103 | 'metadata.broker.list', 104 | 'localhost', 105 | ]) 106 | ->once(); 107 | $conf->shouldReceive('set') 108 | ->withArgs([ 109 | 'group.id', 110 | 'php-pubsub', 111 | ]) 112 | ->once(); 113 | $conf->shouldReceive('set') 114 | ->withArgs([ 115 | 'enable.auto.commit', 116 | 'false', 117 | ]) 118 | ->once(); 119 | $conf->shouldReceive('set') 120 | ->withArgs([ 121 | 'offset.store.method', 122 | 'broker', 123 | ]) 124 | ->once(); 125 | $conf->shouldReceive('setDefaultTopicConf') 126 | ->with($topicConf) 127 | ->once(); 128 | 129 | $container->shouldReceive('make') 130 | ->with('pubsub.kafka.conf') 131 | ->once() 132 | ->andReturn($conf); 133 | 134 | $consumer = Mockery::mock(\RdKafka\KafkaConsumer::class); 135 | 136 | $container->shouldReceive('make') 137 | ->withArgs([ 138 | 'pubsub.kafka.consumer', 139 | ['conf' => $conf], 140 | ]) 141 | ->once() 142 | ->andReturn($consumer); 143 | 144 | $factory = new PubSubConnectionFactory($container); 145 | 146 | $adapter = $factory->make('kafka', $config); 147 | $this->assertInstanceOf(KafkaPubSubAdapter::class, $adapter); 148 | } 149 | 150 | public function testMakeGoogleCloudAdapter() 151 | { 152 | $container = Mockery::mock(Container::class); 153 | $container->shouldReceive('makeWith') 154 | ->withArgs([ 155 | 'pubsub.gcloud.pub_sub_client', 156 | [ 157 | 'config' => [ 158 | 'projectId' => 12345, 159 | 'keyFilePath' => 'my_key_file.json', 160 | ], 161 | ], 162 | ]) 163 | ->andReturn(Mockery::mock(GoogleCloudPubSubClient::class)); 164 | 165 | $factory = new PubSubConnectionFactory($container); 166 | 167 | $config = [ 168 | 'project_id' => '12345', 169 | 'key_file' => 'my_key_file.json', 170 | 'client_identifier' => 'blah', 171 | 'auto_create_topics' => false, 172 | 'background_batching' => true, 173 | 'background_daemon' => false, 174 | ]; 175 | $adapter = $factory->make('gcloud', $config); 176 | $this->assertInstanceOf(GoogleCloudPubSubAdapter::class, $adapter); 177 | 178 | $adapter = $factory->make('gcloud', $config); /* @var GoogleCloudPubSubAdapter $adapter */ 179 | $this->assertInstanceOf(GoogleCloudPubSubAdapter::class, $adapter); 180 | $this->assertEquals('blah', $adapter->getClientIdentifier()); 181 | $this->assertFalse($adapter->areTopicsAutoCreated()); 182 | $this->assertTrue($adapter->areSubscriptionsAutoCreated()); 183 | $this->assertTrue($adapter->isBackgroundBatchingEnabled()); 184 | $this->assertFalse(getenv('IS_BATCH_DAEMON_RUNNING')); 185 | } 186 | 187 | public function testMakeGoogleCloudAdapterWithBackgroundBatchingAndDaemonEnabled() 188 | { 189 | $container = Mockery::mock(Container::class); 190 | $container->shouldReceive('makeWith') 191 | ->withArgs([ 192 | 'pubsub.gcloud.pub_sub_client', 193 | [ 194 | 'config' => [ 195 | 'projectId' => 12345, 196 | 'keyFilePath' => 'my_key_file.json', 197 | ], 198 | ], 199 | ]) 200 | ->andReturn(Mockery::mock(GoogleCloudPubSubClient::class)); 201 | 202 | $factory = new PubSubConnectionFactory($container); 203 | 204 | $config = [ 205 | 'project_id' => '12345', 206 | 'key_file' => 'my_key_file.json', 207 | 'client_identifier' => 'blah', 208 | 'auto_create_topics' => false, 209 | 'background_batching' => true, 210 | 'background_daemon' => true, 211 | ]; 212 | $adapter = $factory->make('gcloud', $config); 213 | $this->assertInstanceOf(GoogleCloudPubSubAdapter::class, $adapter); 214 | 215 | $adapter = $factory->make('gcloud', $config); /* @var GoogleCloudPubSubAdapter $adapter */ 216 | $this->assertInstanceOf(GoogleCloudPubSubAdapter::class, $adapter); 217 | $this->assertEquals('blah', $adapter->getClientIdentifier()); 218 | $this->assertFalse($adapter->areTopicsAutoCreated()); 219 | $this->assertTrue($adapter->areSubscriptionsAutoCreated()); 220 | $this->assertTrue($adapter->isBackgroundBatchingEnabled()); 221 | $this->assertEquals('true', getenv('IS_BATCH_DAEMON_RUNNING')); 222 | } 223 | 224 | public function testMakeGoogleCloudAdapterWithAuthCache() 225 | { 226 | $cacheImplementation = Mockery::mock(CacheItemPoolInterface::class); 227 | 228 | $container = Mockery::mock(Container::class); 229 | $container->shouldReceive('make') 230 | ->with('MyPSR6CacheImplementation::class') 231 | ->andReturn($cacheImplementation); 232 | $container->shouldReceive('makeWith') 233 | ->withArgs([ 234 | 'pubsub.gcloud.pub_sub_client', 235 | [ 236 | 'config' => [ 237 | 'projectId' => 12345, 238 | 'keyFilePath' => 'my_key_file.json', 239 | 'authCache' => $cacheImplementation, 240 | ], 241 | ], 242 | ]) 243 | ->andReturn(Mockery::mock(GoogleCloudPubSubClient::class)); 244 | 245 | $factory = new PubSubConnectionFactory($container); 246 | 247 | $config = [ 248 | 'project_id' => '12345', 249 | 'key_file' => 'my_key_file.json', 250 | 'auth_cache' => 'MyPSR6CacheImplementation::class', 251 | ]; 252 | 253 | $adapter = $factory->make('gcloud', $config); 254 | $this->assertInstanceOf(GoogleCloudPubSubAdapter::class, $adapter); 255 | } 256 | 257 | public function testMakeHTTPAdapter() 258 | { 259 | $container = Mockery::mock(Container::class); 260 | $container->shouldReceive('make') 261 | ->with('pubsub.http.client') 262 | ->andReturn(Mockery::mock(Client::class)); 263 | 264 | $factory = new PubSubConnectionFactory($container); 265 | 266 | $config = [ 267 | 'uri' => 'http://127.0.0.1', 268 | 'subscribe_connection_config' => [ 269 | 'driver' => '/dev/null', 270 | ], 271 | ]; 272 | $adapter = $factory->make('http', $config); /* @var HTTPPubSubAdapter $adapter */ 273 | $this->assertInstanceOf(HTTPPubSubAdapter::class, $adapter); 274 | $this->assertEquals('http://127.0.0.1', $adapter->getUri()); 275 | $this->assertInstanceOf(PubSubAdapterInterface::class, $adapter->getAdapter()); 276 | } 277 | 278 | public function testMakeInvalidAdapterThrowsInvalidArgumentException() 279 | { 280 | $this->expectException(InvalidArgumentException::class); 281 | $this->expectExceptionMessage('The driver [rubbish] is not supported.'); 282 | 283 | $container = Mockery::mock(Container::class); 284 | 285 | $factory = new PubSubConnectionFactory($container); 286 | 287 | $factory->make('rubbish'); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /tests/PubSubManagerTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('get') 20 | ->with('pubsub') 21 | ->once() 22 | ->andReturn($this->getMockPubSubConfig()); 23 | 24 | $app = Mockery::mock(Application::class); 25 | $app->shouldReceive('make') 26 | ->with('config') 27 | ->once() 28 | ->andReturn($config); 29 | 30 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 31 | $factory->shouldReceive('make') 32 | ->withArgs([ 33 | '/dev/null', 34 | [], 35 | ]) 36 | ->once() 37 | ->andReturn(PubSubAdapterInterface::class); 38 | 39 | $manager = new PubSubManager($app, $factory); 40 | 41 | $this->assertEmpty($manager->getConnections()); 42 | $manager->connection(); 43 | $connections = $manager->getConnections(); 44 | $this->assertEquals(1, count($connections)); 45 | $this->assertArrayHasKey('/dev/null', $connections); 46 | } 47 | 48 | public function testConnectionWithInvalidConnectionNameThrowsInvalidArgumentException() 49 | { 50 | $this->expectException(InvalidArgumentException::class); 51 | $this->expectExceptionMessage('The pub-sub connection [invalid_name] is not configured.'); 52 | 53 | $config = Mockery::mock(ConfigRepository::class); 54 | $config->shouldReceive('get') 55 | ->with('pubsub') 56 | ->once() 57 | ->andReturn($this->getMockPubSubConfig()); 58 | 59 | $app = Mockery::mock(Application::class); 60 | $app->shouldReceive('make') 61 | ->with('config') 62 | ->once() 63 | ->andReturn($config); 64 | 65 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 66 | 67 | $manager = new PubSubManager($app, $factory); 68 | 69 | $manager->connection('invalid_name'); 70 | } 71 | 72 | public function testConnectionWithMissingDriverConfigThrowsInvalidArgumentException() 73 | { 74 | $this->expectException(InvalidArgumentException::class); 75 | $this->expectExceptionMessage('The pub-sub connection [missing_driver] is missing a "driver" config var.'); 76 | 77 | $config = Mockery::mock(ConfigRepository::class); 78 | $config->shouldReceive('get') 79 | ->with('pubsub') 80 | ->once() 81 | ->andReturn($this->getMockPubSubConfig()); 82 | 83 | $app = Mockery::mock(Application::class); 84 | $app->shouldReceive('make') 85 | ->with('config') 86 | ->once() 87 | ->andReturn($config); 88 | 89 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 90 | 91 | $manager = new PubSubManager($app, $factory); 92 | 93 | $manager->connection('missing_driver'); 94 | } 95 | 96 | public function testConnectionWithNameReturnsSpecifiedConnection() 97 | { 98 | $config = Mockery::mock(ConfigRepository::class); 99 | $config->shouldReceive('get') 100 | ->with('pubsub') 101 | ->once() 102 | ->andReturn($this->getMockPubSubConfig()); 103 | 104 | $app = Mockery::mock(Application::class); 105 | $app->shouldReceive('make') 106 | ->with('config') 107 | ->once() 108 | ->andReturn($config); 109 | 110 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 111 | $factory->shouldReceive('make') 112 | ->withArgs([ 113 | 'kafka', 114 | [ 115 | 'consumer_group_id' => 'php-pubsub', 116 | 'brokers' => 'localhost', 117 | ], 118 | ]) 119 | ->once() 120 | ->andReturn(PubSubAdapterInterface::class); 121 | 122 | $manager = new PubSubManager($app, $factory); 123 | 124 | $this->assertEmpty($manager->getConnections()); 125 | $manager->connection('kafka'); 126 | $connections = $manager->getConnections(); 127 | $this->assertEquals(1, count($connections)); 128 | $this->assertArrayHasKey('kafka', $connections); 129 | } 130 | 131 | public function testConnectionWithExistingConnectionReturnsThatConnection() 132 | { 133 | $config = Mockery::mock(ConfigRepository::class); 134 | $config->shouldReceive('get') 135 | ->with('pubsub') 136 | ->once() 137 | ->andReturn($this->getMockPubSubConfig()); 138 | 139 | $app = Mockery::mock(Application::class); 140 | $app->shouldReceive('make') 141 | ->with('config') 142 | ->once() 143 | ->andReturn($config); 144 | 145 | $adapter = Mockery::mock(DevNullPubSubAdapter::class); 146 | 147 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 148 | $factory->shouldReceive('make') 149 | ->withArgs([ 150 | '/dev/null', 151 | [], 152 | ]) 153 | ->once() 154 | ->andReturn($adapter); 155 | 156 | $manager = new PubSubManager($app, $factory); 157 | 158 | $this->assertEmpty($manager->getConnections()); 159 | 160 | $connection1 = $manager->connection('/dev/null'); 161 | $this->assertSame($adapter, $connection1); 162 | $connections = $manager->getConnections(); 163 | $this->assertEquals(1, count($connections)); 164 | $this->assertArrayHasKey('/dev/null', $connections); 165 | 166 | $connection2 = $manager->connection('/dev/null'); 167 | $this->assertSame($connection1, $connection2); 168 | $this->assertEquals(1, count($manager->getConnections())); 169 | } 170 | 171 | public function testConnectionWithCustomExtension() 172 | { 173 | $config = Mockery::mock(ConfigRepository::class); 174 | $config->shouldReceive('get') 175 | ->with('pubsub') 176 | ->once() 177 | ->andReturn($this->getMockPubSubConfig()); 178 | 179 | $app = Mockery::mock(Application::class); 180 | $app->shouldReceive('make') 181 | ->with('config') 182 | ->once() 183 | ->andReturn($config); 184 | 185 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 186 | $factory->shouldNotReceive('make'); 187 | 188 | $manager = new PubSubManager($app, $factory); 189 | 190 | $callable = Mockery::mock(\stdClass::class); 191 | $callable->shouldReceive('make'); 192 | 193 | $manager->extend('custom_connection', [$callable, 'make']); 194 | 195 | $this->assertEmpty($manager->getConnections()); 196 | $manager->connection('custom_connection'); 197 | $connections = $manager->getConnections(); 198 | $this->assertEquals(1, count($connections)); 199 | $this->assertArrayHasKey('custom_connection', $connections); 200 | } 201 | 202 | public function testConnectionShouldResolveSubscribeConnectionToSubConfig() 203 | { 204 | $config = Mockery::mock(ConfigRepository::class); 205 | $config->shouldReceive('get') 206 | ->with('pubsub') 207 | ->once() 208 | ->andReturn($this->getMockPubSubConfig()); 209 | 210 | $app = Mockery::mock(Application::class); 211 | $app->shouldReceive('make') 212 | ->with('config') 213 | ->once() 214 | ->andReturn($config); 215 | 216 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 217 | $factory->shouldReceive('make') 218 | ->withArgs([ 219 | 'http', 220 | [ 221 | 'uri' => 'http://127.0.0.1', 222 | 'subscribe_connection' => '/dev/null', 223 | 'subscribe_connection_config' => [ 224 | 'driver' => '/dev/null', 225 | ], 226 | ], 227 | ]) 228 | ->once() 229 | ->andReturn(PubSubAdapterInterface::class); 230 | 231 | $manager = new PubSubManager($app, $factory); 232 | 233 | $manager->connection('http'); 234 | } 235 | 236 | public function testGetDefaultConnection() 237 | { 238 | $config = Mockery::mock(ConfigRepository::class); 239 | $config->shouldReceive('get') 240 | ->with('pubsub') 241 | ->once() 242 | ->andReturn([ 243 | 'default' => '/dev/null', 244 | ]); 245 | 246 | $app = Mockery::mock(Application::class); 247 | $app->shouldReceive('make') 248 | ->with('config') 249 | ->once() 250 | ->andReturn($config); 251 | 252 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 253 | 254 | $manager = new PubSubManager($app, $factory); 255 | 256 | $this->assertEquals('/dev/null', $manager->getDefaultConnection()); 257 | } 258 | 259 | public function testSetDefaultConnection() 260 | { 261 | $config = Mockery::mock(ConfigRepository::class); 262 | $config->shouldReceive('set') 263 | ->withArgs([ 264 | 'pubsub.default', 265 | '/dev/null', 266 | ]) 267 | ->once(); 268 | 269 | $app = Mockery::mock(Application::class); 270 | $app->shouldReceive('make') 271 | ->with('config') 272 | ->once() 273 | ->andReturn($config); 274 | 275 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 276 | 277 | $manager = new PubSubManager($app, $factory); 278 | 279 | $manager->setDefaultConnection('/dev/null'); 280 | } 281 | 282 | public function testExtend() 283 | { 284 | $app = Mockery::mock(Application::class); 285 | 286 | $factory = Mockery::mock(PubSubConnectionFactory::class, [$app]); 287 | 288 | $manager = new PubSubManager($app, $factory); 289 | 290 | $extensions = $manager->getExtensions(); 291 | $this->assertInternalType('array', $extensions); 292 | 293 | $callable = function () { 294 | // 295 | }; 296 | $manager->extend('custom_connection', $callable); 297 | 298 | $extensions = $manager->getExtensions(); 299 | 300 | $this->assertEquals(1, count($extensions)); 301 | $this->assertArrayHasKey('custom_connection', $extensions); 302 | $this->assertSame($callable, $extensions['custom_connection']); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | '/dev/null', 14 | 'connections' => [ 15 | '/dev/null' => [ 16 | 'driver' => '/dev/null', 17 | ], 18 | 'local' => [ 19 | 'driver' => 'local', 20 | ], 21 | 'redis' => [ 22 | 'driver' => 'redis', 23 | 'scheme' => 'tcp', 24 | 'host' => 'localhost', 25 | 'password' => null, 26 | 'port' => 6379, 27 | 'database' => 0, 28 | 'read_write_timeout' => 0, 29 | ], 30 | 'kafka' => [ 31 | 'driver' => 'kafka', 32 | 'consumer_group_id' => 'php-pubsub', 33 | 'brokers' => 'localhost', 34 | ], 35 | 'http' => [ 36 | 'driver' => 'http', 37 | 'uri' => 'http://127.0.0.1', 38 | 'subscribe_connection' => '/dev/null', 39 | ], 40 | 'missing_driver' => [ 41 | 42 | ], 43 | 'custom_connection' => [ 44 | 45 | ], 46 | ], 47 | ]; 48 | } 49 | } 50 | --------------------------------------------------------------------------------