├── .github └── CODEOWNERS ├── .gitignore ├── .styleci.yml ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── changelog.md ├── composer.json ├── docker-compose.yml ├── examples ├── GoogleCloudConsumerExample.php └── GoogleCloudPublishExample.php ├── phpunit.php ├── phpunit.xml ├── src └── GoogleCloudPubSubAdapter.php └── tests └── GoogleCloudPubSubAdapterTest.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 7 | gcloud-test-key.json 8 | your-gcloud-key.json -------------------------------------------------------------------------------- /.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 | - 7.3 8 | 9 | before_script: 10 | - composer install 11 | 12 | script: ./vendor/bin/phpunit --configuration phpunit.xml 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.0-fpm 2 | MAINTAINER Superbalist 3 | 4 | RUN mkdir /opt/php-pubsub 5 | WORKDIR /opt/php-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 | && rm -r /var/lib/apt/lists/* 14 | 15 | # PHP Extensions 16 | RUN docker-php-ext-install -j$(nproc) zip \ 17 | && pecl install grpc \ 18 | && docker-php-ext-enable grpc 19 | 20 | RUN pecl install protobuf \ 21 | && docker-php-ext-enable protobuf 22 | 23 | # Composer 24 | ENV COMPOSER_HOME /composer 25 | ENV PATH /composer/vendor/bin:$PATH 26 | ENV COMPOSER_ALLOW_SUPERUSER 1 27 | RUN curl -o /tmp/composer-setup.php https://getcomposer.org/installer \ 28 | && curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig \ 29 | && 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); }" \ 30 | && php /tmp/composer-setup.php --no-ansi --install-dir=/usr/local/bin --filename=composer --version=1.1.0 && rm -rf /tmp/composer-setup.php 31 | 32 | # Install Composer Application Dependencies 33 | COPY composer.json /opt/php-pubsub/ 34 | RUN composer install --no-autoloader --no-scripts --no-interaction 35 | 36 | COPY src /opt/php-pubsub/src 37 | COPY your-gcloud-key.json /opt/php-pubsub 38 | COPY examples /opt/php-pubsub/examples 39 | COPY phpunit.php /opt/php-pubsub 40 | COPY phpunit.xml /opt/php-pubsub 41 | COPY tests /opt/php-pubsub/tests 42 | 43 | RUN composer dump-autoload --no-interaction 44 | 45 | CMD ["/bin/bash"] 46 | -------------------------------------------------------------------------------- /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 php-pubsub-google-cloud /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 | # php-pubsub-google-cloud 2 | 3 | A Google Cloud adapter for the [php-pubsub](https://github.com/Superbalist/php-pubsub) package. 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/php-pubsub-google-cloud/master.svg?style=flat-square)](https://travis-ci.org/Superbalist/php-pubsub-google-cloud) 7 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 8 | [![Packagist Version](https://img.shields.io/packagist/v/superbalist/php-pubsub-google-cloud.svg?style=flat-square)](https://packagist.org/packages/superbalist/php-pubsub-google-cloud) 9 | [![Total Downloads](https://img.shields.io/packagist/dt/superbalist/php-pubsub-google-cloud.svg?style=flat-square)](https://packagist.org/packages/superbalist/php-pubsub-google-cloud) 10 | 11 | 12 | ## Installation 13 | 14 | ```bash 15 | composer require superbalist/php-pubsub-google-cloud 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```php 21 | putenv('GOOGLE_APPLICATION_CREDENTIALS=' . __DIR__ . '/../your-gcloud-key.json'); 22 | 23 | $client = new \Google\Cloud\PubSub\PubSubClient([ 24 | 'projectId' => 'your-project-id-here', 25 | ]); 26 | 27 | $adapter = new \Superbalist\PubSub\GoogleCloud\GoogleCloudPubSubAdapter($client); 28 | 29 | 30 | // disable auto topic & subscription creation 31 | $adapter->setAutoCreateTopics(false); // this is true by default 32 | $adapter->setAutoCreateSubscriptions(false); // this is true by default 33 | 34 | // set a unique client identifier for the subscriber 35 | $adapter->setClientIdentifier('search_service'); 36 | 37 | // consume messages 38 | // note: this is a blocking call 39 | $adapter->subscribe('my_channel', function ($message) { 40 | var_dump($message); 41 | }); 42 | 43 | // publish messages 44 | $adapter->publish('my_channel', 'HELLO WORLD'); 45 | $adapter->publish('my_channel', json_encode(['hello' => 'world'])); 46 | $adapter->publish('my_channel', 1); 47 | $adapter->publish('my_channel', false); 48 | ``` 49 | 50 | ## gRPC Support 51 | 52 | Google Cloud PHP v0.12.0 added support for communication over the gRPC protocol. 53 | 54 | > gRPC is great for high-performance, low-latency applications, and is highly recommended in cases where performance and latency are concerns. 55 | 56 | The library will automatically choose gRPC over REST if all dependencies are installed. 57 | * [gRPC PECL extension](https://pecl.php.net/package/gRPC) 58 | * [google/proto-client-php composer package](https://github.com/googleapis/gax-php) 59 | * [googleapis/proto-client-php composer package](https://github.com/googleapis/proto-client-php) 60 | 61 | ```bash 62 | pecl install grpc 63 | 64 | composer require google/gax 65 | composer require google/proto-client 66 | ``` 67 | 68 | ## Background Batch Message Support 69 | 70 | Google Cloud v0.33.0 added support for queueing messages and publishing in the background. This is available in 71 | version 5+ of this package which requires a min version of google/cloud ^0.33.0. 72 | 73 | You can enable background batch messaging by setting `$backgroundBatching` to `true` when constructing the 74 | `GoogleCloudPubSubAdapter` or by calling `setBackgroundBatching(true)` on an existing adapter. 75 | 76 | If the [semaphore](http://php.net/manual/en/book.sem.php) and [pcntl](http://php.net/manual/en/book.pcntl.php) PHP extensions are 77 | enabled AND the `IS_BATCH_DAEMON_RUNNING` ENV var is set to `true`, the library will queue messages to be published by 78 | the [Batch Daemon](https://github.com/GoogleCloudPlatform/google-cloud-php/blob/master/src/Core/Batch/BatchDaemon.php). 79 | The Batch Daemon needs to be manually run as a long-lived background process. 80 | 81 | For all other cases, messages will be queued in memory and will be published before the script terminates using a 82 | vendor registered shutdown handler. 83 | 84 | **Please Note** 85 | 86 | This is marked by google/cloud as an experimental feature & may change before release in backwards-incompatible ways. 87 | 88 | ## Examples 89 | 90 | The library comes with [examples](examples) for the adapter and a [Dockerfile](Dockerfile) for 91 | running the example scripts. 92 | 93 | Run `make up`. 94 | 95 | You will start at a `bash` prompt in the `/opt/php-pubsub` directory. 96 | 97 | If you need another shell to publish a message to a blocking consumer, you can run `docker-compose run php-pubsub-google-cloud /bin/bash` 98 | 99 | To run the examples: 100 | ```bash 101 | $ php examples/GoogleCloudConsumerExample.php 102 | $ php examples/GoogleCloudPublishExample.php (in a separate shell) 103 | ``` 104 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.2.0 - 2019-03-19 4 | 5 | * Add ability to use returnImmediately flag when pulling messages 6 | 7 | ## 5.1.0 - 2019-02-28 8 | 9 | * Bump up google/cloud requirement to ^0.95.0 10 | 11 | ## 5.0.1 - 2018-07-27 12 | 13 | * Allow setting of maximum number of messages to pull option 14 | 15 | ## 5.0.0 - 2017-07-25 16 | 17 | * Add support for using Google Cloud batch requests (aka background daemon) 18 | 19 | ## 4.0.1 - 2017-07-21 20 | 21 | * Allow for google/cloud ^0.29.0|^0.30.0|^0.31.0|^0.32.0|^0.33.0|^0.34.0|^0.35.0 22 | 23 | ## 4.0.0 - 2017-05-16 24 | 25 | * Allow for google/cloud ^0.26.0|^0.27.0|^0.28.0 26 | * Bump up to superbalist/php-pubsub ^2.0 27 | * Add new publishBatch method to GoogleCloudPubSubAdapter 28 | 29 | ## 3.0.1 - 2017-04-03 30 | 31 | * Fix to gRPC timeouts 32 | * Allow for google/cloud ^0.21.0|^0.22.0|^0.23.0|^0.24.0|^0.25.0 33 | 34 | ## 3.0.0 - 2017-01-03 35 | 36 | * Bump up google/cloud requirement to ^0.11.0|^0.12.0|^0.13.0|^0.20.0 37 | 38 | ## 2.0.2 - 2017-01-03 39 | 40 | * Allow for google/cloud ^0.10.0 41 | 42 | ## 2.0.1 - 2016-10-05 43 | 44 | * Fix to subscriber bug - client identifier needs to be unique across topics. 45 | 46 | ## 2.0.0 - 2016-10-05 47 | 48 | * Add new `$clientIdentifier` & `setClientIdentifier()` functionality to allow subscribers to use the same, or unique identifiers. 49 | 50 | ## 1.0.2 - 2016-09-26 51 | 52 | * Allow for google/cloud ^0.8.0 and ^0.9.0 53 | 54 | ## 1.0.1 - 2016-09-15 55 | 56 | * Ack messages individually after callable returns successfully 57 | * Add functionality to enable/disable auto topic & subscription creation 58 | 59 | ## 1.0.0 - 2016-09-05 60 | 61 | * Initial release 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superbalist/php-pubsub-google-cloud", 3 | "description": "A Google Cloud adapter for the php-pubsub package", 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 | "superbalist/php-pubsub": "^2.0", 14 | "google/cloud": "^0.95.0" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "Superbalist\\PubSub\\GoogleCloud\\": "src/", 19 | "Tests\\": "tests/" 20 | } 21 | }, 22 | "extra": { 23 | "branch-alias": { 24 | "dev-master": "1.0-dev" 25 | } 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^5.5", 29 | "mockery/mockery": "^0.9.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | php-pubsub-google-cloud: 4 | build: . 5 | volumes: 6 | - ./src:/opt/php-pubsub/src 7 | - ./examples:/opt/php-pubsub/examples -------------------------------------------------------------------------------- /examples/GoogleCloudConsumerExample.php: -------------------------------------------------------------------------------- 1 | 'your-project-id-here', 9 | ]); 10 | 11 | $adapter = new \Superbalist\PubSub\GoogleCloud\GoogleCloudPubSubAdapter($client); 12 | 13 | $adapter->subscribe('my_channel', function ($message) { 14 | var_dump($message); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/GoogleCloudPublishExample.php: -------------------------------------------------------------------------------- 1 | 'your-project-id-here', 9 | ]); 10 | 11 | $adapter = new \Superbalist\PubSub\GoogleCloud\GoogleCloudPubSubAdapter($client); 12 | 13 | $adapter->publish('my_channel', 'Hello World'); 14 | $adapter->publish('my_channel', ['lorem' => 'ipsum']); 15 | $adapter->publish('my_channel', '{"blah": "bleh"}'); 16 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | ./src/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/GoogleCloudPubSubAdapter.php: -------------------------------------------------------------------------------- 1 | client = $client; 72 | $this->clientIdentifier = $clientIdentifier; 73 | $this->autoCreateTopics = $autoCreateTopics; 74 | $this->autoCreateSubscriptions = $autoCreateSubscriptions; 75 | $this->backgroundBatching = $backgroundBatching; 76 | $this->maxMessages = $maxMessages; 77 | $this->returnImmediately = $returnImmediately; 78 | $this->returnImmediatelyPause = (int) $returnImmediatelyPause; 79 | } 80 | 81 | /** 82 | * Return the Google PubSubClient. 83 | * 84 | * @return PubSubClient 85 | */ 86 | public function getClient() 87 | { 88 | return $this->client; 89 | } 90 | 91 | /** 92 | * Set the unique client identifier. 93 | * 94 | * The client identifier is used when creating a subscription to a topic. 95 | * 96 | * A topic can have multiple subscribers connected. 97 | * If all subscribers use the same client identifier, the messages will load-balance across them. 98 | * If all subscribers have different client identifiers, the messages will be dispatched all of them. 99 | * 100 | * @param string $clientIdentifier 101 | */ 102 | public function setClientIdentifier($clientIdentifier) 103 | { 104 | $this->clientIdentifier = $clientIdentifier; 105 | } 106 | 107 | /** 108 | * Return the unique client identifier. 109 | * 110 | * @return string 111 | */ 112 | public function getClientIdentifier() 113 | { 114 | return $this->clientIdentifier; 115 | } 116 | 117 | /** 118 | * Set whether or not topics will be auto created. 119 | * 120 | * @param bool $autoCreateTopics 121 | */ 122 | public function setAutoCreateTopics($autoCreateTopics) 123 | { 124 | $this->autoCreateTopics = $autoCreateTopics; 125 | } 126 | 127 | /** 128 | * Check whether or not topics will be auto created. 129 | * 130 | * @return bool 131 | */ 132 | public function areTopicsAutoCreated() 133 | { 134 | return $this->autoCreateTopics; 135 | } 136 | 137 | /** 138 | * Set whether or not subscriptions will be auto created. 139 | * 140 | * @param bool $autoCreateSubscriptions 141 | */ 142 | public function setAutoCreateSubscriptions($autoCreateSubscriptions) 143 | { 144 | $this->autoCreateSubscriptions = $autoCreateSubscriptions; 145 | } 146 | 147 | /** 148 | * Check whether or not subscriptions will be auto created. 149 | * 150 | * @return bool 151 | */ 152 | public function areSubscriptionsAutoCreated() 153 | { 154 | return $this->autoCreateSubscriptions; 155 | } 156 | 157 | /** 158 | * Set if a pull should return immediately if there are no messages 159 | * @param bool $returnImmediately 160 | */ 161 | public function setReturnImmediately($returnImmediately) { 162 | $this->returnImmediately = $returnImmediately; 163 | } 164 | 165 | /** 166 | * Return the return immediately configuration 167 | * @return bool 168 | */ 169 | public function getReturnImmediately() { 170 | return $this->returnImmediately; 171 | } 172 | 173 | /** 174 | * Set the amount of time to pause between attempts to pull messages if return immediately is enabled. 175 | * Value is in microseconds 176 | * 177 | * @param int $returnImmediatelyPause 178 | */ 179 | public function setReturnImmediatelyPause($returnImmediatelyPause) { 180 | $this->returnImmediatelyPause = (int) $returnImmediatelyPause; 181 | } 182 | 183 | /** 184 | * Return the return immediately pause configuration 185 | * @return int 186 | */ 187 | public function getReturnImmediatelyPause() { 188 | return $this->returnImmediatelyPause; 189 | } 190 | 191 | /** 192 | * Set whether or not background batching is enabled. 193 | * 194 | * This is available from Google Cloud 0.33+ - https://github.com/GoogleCloudPlatform/google-cloud-php/releases/tag/v0.33.0 195 | * 196 | * If the http://php.net/manual/en/book.sem.php and http://php.net/manual/en/book.pcntl.php extensions are enabled 197 | * AND the IS_BATCH_DAEMON_RUNNING ENV var is set to true, the library will queue messages to be published by the 198 | * Batch Daemon (https://github.com/GoogleCloudPlatform/google-cloud-php/blob/master/src/Core/Batch/BatchDaemon.php) 199 | * 200 | * For all other cases, messages will be queued in memory and will be published before the script terminates using 201 | * a vendor registered shutdown handler. 202 | * 203 | * @param bool $backgroundBatching 204 | */ 205 | public function setBackgroundBatching($backgroundBatching) 206 | { 207 | $this->backgroundBatching = $backgroundBatching; 208 | } 209 | 210 | /** 211 | * Check whether or not background batching is enabled. 212 | * 213 | * @return bool 214 | */ 215 | public function isBackgroundBatchingEnabled() 216 | { 217 | return $this->backgroundBatching; 218 | } 219 | 220 | /** 221 | * Max messages to pull at a time. 222 | * https://googlecloudplatform.github.io/google-cloud-php/#/docs/google-cloud/v0.35.0/pubsub/subscription?method=pull 223 | * 224 | * @param int $maxMessages 225 | */ 226 | public function setMaxMessages($maxMessages) 227 | { 228 | $this->maxMessages = $maxMessages; 229 | } 230 | 231 | /** 232 | * Subscribe a handler to a channel. 233 | * 234 | * @param string $channel 235 | * @param callable $handler 236 | */ 237 | public function subscribe($channel, callable $handler) 238 | { 239 | $subscription = $this->getSubscriptionForChannel($channel); 240 | 241 | $isSubscriptionLoopActive = true; 242 | $isPauseEnabled = $this->returnImmediately && ($this->returnImmediatelyPause > 0); 243 | 244 | while ($isSubscriptionLoopActive) { 245 | $messages = $subscription->pull([ 246 | 'grpcOptions' => [ 247 | 'timeoutMillis' => null, 248 | ], 249 | 'maxMessages' => $this->maxMessages, 250 | 'returnImmediately' => $this->returnImmediately, 251 | ]); 252 | if ($isPauseEnabled && empty($messages)) { 253 | usleep($this->returnImmediatelyPause); 254 | continue; 255 | } 256 | foreach ($messages as $message) { 257 | /** @var Message $message */ 258 | $payload = Utils::unserializeMessagePayload($message->data()); 259 | 260 | if ($payload === 'unsubscribe') { 261 | $isSubscriptionLoopActive = false; 262 | } else { 263 | call_user_func($handler, $payload); 264 | } 265 | 266 | $subscription->acknowledge($message); 267 | } 268 | } 269 | } 270 | 271 | /** 272 | * Publish a message to a channel. 273 | * 274 | * @param string $channel 275 | * @param mixed $message 276 | */ 277 | public function publish($channel, $message) 278 | { 279 | $topic = $this->getTopicForChannel($channel); 280 | $payload = Utils::serializeMessage($message); 281 | 282 | if ($this->backgroundBatching) { 283 | $topic->batchPublisher()->publish(['data' => $payload]); 284 | } else { 285 | $topic->publish(['data' => $payload]); 286 | } 287 | } 288 | 289 | /** 290 | * Publish multiple messages to a channel. 291 | * 292 | * @param string $channel 293 | * @param array $messages 294 | */ 295 | public function publishBatch($channel, array $messages) 296 | { 297 | $topic = $this->getTopicForChannel($channel); 298 | $messages = array_map(function ($message) { 299 | return ['data' => Utils::serializeMessage($message)]; 300 | }, $messages); 301 | 302 | if ($this->backgroundBatching) { 303 | $batchPublisher = $topic->batchPublisher(); 304 | foreach ($messages as $message) { 305 | $batchPublisher->publish($message); 306 | } 307 | } else { 308 | $topic->publishBatch($messages); 309 | } 310 | } 311 | 312 | /** 313 | * Return a `Topic` instance from a channel name. 314 | * 315 | * If the topic doesn't exist, the topic is first created. 316 | * 317 | * @param string $channel 318 | * 319 | * @return \Google\Cloud\PubSub\Topic 320 | */ 321 | protected function getTopicForChannel($channel) 322 | { 323 | $topic = $this->client->topic($channel); 324 | if ($this->autoCreateTopics && !$topic->exists()) { 325 | $topic->create(); 326 | } 327 | return $topic; 328 | } 329 | 330 | /** 331 | * Return a `Subscription` instance from a channel name. 332 | * 333 | * If the subscription doesn't exist, the subscription is first created. 334 | * 335 | * @param string $channel 336 | * 337 | * @return \Google\Cloud\PubSub\Subscription 338 | */ 339 | protected function getSubscriptionForChannel($channel) 340 | { 341 | $topic = $this->getTopicForChannel($channel); 342 | $clientIdentifier = $this->clientIdentifier ? $this->clientIdentifier : 'default'; 343 | $clientIdentifier .= '.' . $channel; 344 | $subscription = $topic->subscription($clientIdentifier); 345 | if ($this->autoCreateSubscriptions && !$subscription->exists()) { 346 | $subscription->create(); 347 | } 348 | return $subscription; 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /tests/GoogleCloudPubSubAdapterTest.php: -------------------------------------------------------------------------------- 1 | assertSame($client, $adapter->getClient()); 21 | } 22 | 23 | public function testGetSetClientIdentifier() 24 | { 25 | $client = Mockery::mock(PubSubClient::class); 26 | $adapter = new GoogleCloudPubSubAdapter($client); 27 | $this->assertNull($adapter->getClientIdentifier()); 28 | 29 | $adapter->setClientIdentifier('my_identifier'); 30 | $this->assertEquals('my_identifier', $adapter->getClientIdentifier()); 31 | } 32 | 33 | public function testGetSetAutoCreateTopics() 34 | { 35 | $client = Mockery::mock(PubSubClient::class); 36 | $adapter = new GoogleCloudPubSubAdapter($client); 37 | $this->assertTrue($adapter->areTopicsAutoCreated()); 38 | 39 | $adapter->setAutoCreateTopics(false); 40 | $this->assertFalse($adapter->areTopicsAutoCreated()); 41 | } 42 | 43 | public function testGetSetAutoCreateSubscriptions() 44 | { 45 | $client = Mockery::mock(PubSubClient::class); 46 | $adapter = new GoogleCloudPubSubAdapter($client); 47 | $this->assertTrue($adapter->areSubscriptionsAutoCreated()); 48 | 49 | $adapter->setAutoCreateSubscriptions(false); 50 | $this->assertFalse($adapter->areSubscriptionsAutoCreated()); 51 | } 52 | 53 | public function testGetSetBackgroundBatching() 54 | { 55 | $client = Mockery::mock(PubSubClient::class); 56 | $adapter = new GoogleCloudPubSubAdapter($client); 57 | $this->assertFalse($adapter->isBackgroundBatchingEnabled()); 58 | 59 | $adapter->setBackgroundBatching(true); 60 | $this->assertTrue($adapter->isBackgroundBatchingEnabled()); 61 | 62 | $adapter = new GoogleCloudPubSubAdapter($client, null, true, true, true); 63 | $this->assertTrue($adapter->isBackgroundBatchingEnabled()); 64 | } 65 | 66 | public function testGetSetReturnImmediately() 67 | { 68 | $client = Mockery::mock(PubSubClient::class); 69 | $adapter = new GoogleCloudPubSubAdapter($client); 70 | $this->assertFalse($adapter->getReturnImmediately()); 71 | 72 | $adapter->setReturnImmediately(true); 73 | $this->assertTrue($adapter->getReturnImmediately()); 74 | } 75 | 76 | public function testGetSetReturnImmediatelyPause() 77 | { 78 | $client = Mockery::mock(PubSubClient::class); 79 | $adapter = new GoogleCloudPubSubAdapter($client); 80 | $this->assertEquals(500000, $adapter->getReturnImmediatelyPause()); 81 | 82 | $adapter->setReturnImmediatelyPause(1000000); 83 | $this->assertEquals(1000000, $adapter->getReturnImmediatelyPause()); 84 | } 85 | 86 | public function testPublishWhenTopicMustBeCreated() 87 | { 88 | $topic = Mockery::mock(Topic::class); 89 | $topic->shouldReceive('exists') 90 | ->once() 91 | ->andReturn(false); 92 | $topic->shouldReceive('create') 93 | ->once(); 94 | $topic->shouldReceive('publish') 95 | ->with([ 96 | 'data' => '{"hello":"world"}', 97 | ]) 98 | ->once(); 99 | 100 | $client = Mockery::mock(PubSubClient::class); 101 | $client->shouldReceive('topic') 102 | ->with('channel_name') 103 | ->once() 104 | ->andReturn($topic); 105 | 106 | $adapter = new GoogleCloudPubSubAdapter($client); 107 | 108 | $adapter->publish('channel_name', ['hello' => 'world']); 109 | } 110 | 111 | public function testPublishWhenTopicMustBeCreatedAndBackgroundBatchingIsEnabled() 112 | { 113 | $batchPublisher = Mockery::mock(BatchPublisher::class); 114 | 115 | $topic = Mockery::mock(Topic::class); 116 | $topic->shouldReceive('exists') 117 | ->once() 118 | ->andReturn(false); 119 | $topic->shouldReceive('create') 120 | ->once(); 121 | 122 | $topic->shouldReceive('batchPublisher') 123 | ->once() 124 | ->andReturn($batchPublisher); 125 | 126 | $batchPublisher->shouldReceive('publish') 127 | ->with([ 128 | 'data' => '{"hello":"world"}', 129 | ]) 130 | ->once(); 131 | 132 | $client = Mockery::mock(PubSubClient::class); 133 | $client->shouldReceive('topic') 134 | ->with('channel_name') 135 | ->once() 136 | ->andReturn($topic); 137 | 138 | $adapter = new GoogleCloudPubSubAdapter($client); 139 | $adapter->setBackgroundBatching(true); 140 | 141 | $adapter->publish('channel_name', ['hello' => 'world']); 142 | } 143 | 144 | public function testPublishWhenTopicExists() 145 | { 146 | $topic = Mockery::mock(Topic::class); 147 | $topic->shouldReceive('exists') 148 | ->once() 149 | ->andReturn(true); 150 | $topic->shouldNotHaveReceived('create'); 151 | $topic->shouldReceive('publish') 152 | ->with([ 153 | 'data' => '{"hello":"world"}', 154 | ]) 155 | ->once(); 156 | 157 | $client = Mockery::mock(PubSubClient::class); 158 | $client->shouldReceive('topic') 159 | ->with('channel_name') 160 | ->once() 161 | ->andReturn($topic); 162 | 163 | $adapter = new GoogleCloudPubSubAdapter($client); 164 | 165 | $adapter->publish('channel_name', ['hello' => 'world']); 166 | } 167 | 168 | public function testPublishWhenTopicExistsAndBackgroundBatchingIsEnabled() 169 | { 170 | $batchPublisher = Mockery::mock(BatchPublisher::class); 171 | 172 | $topic = Mockery::mock(Topic::class); 173 | $topic->shouldReceive('exists') 174 | ->once() 175 | ->andReturn(true); 176 | $topic->shouldNotHaveReceived('create'); 177 | $topic->shouldReceive('batchPublisher') 178 | ->once() 179 | ->andReturn($batchPublisher); 180 | 181 | $batchPublisher->shouldReceive('publish') 182 | ->with([ 183 | 'data' => '{"hello":"world"}', 184 | ]) 185 | ->once(); 186 | 187 | $client = Mockery::mock(PubSubClient::class); 188 | $client->shouldReceive('topic') 189 | ->with('channel_name') 190 | ->once() 191 | ->andReturn($topic); 192 | 193 | $adapter = new GoogleCloudPubSubAdapter($client); 194 | $adapter->setBackgroundBatching(true); 195 | 196 | $adapter->publish('channel_name', ['hello' => 'world']); 197 | } 198 | 199 | public function testPublishWhenAutoTopicCreationIsDisabled() 200 | { 201 | $topic = Mockery::mock(Topic::class); 202 | $topic->shouldNotHaveReceived('exists'); 203 | $topic->shouldNotHaveReceived('create'); 204 | $topic->shouldReceive('publish') 205 | ->with([ 206 | 'data' => '{"hello":"world"}', 207 | ]) 208 | ->once(); 209 | 210 | $client = Mockery::mock(PubSubClient::class); 211 | $client->shouldReceive('topic') 212 | ->with('channel_name') 213 | ->once() 214 | ->andReturn($topic); 215 | 216 | $adapter = new GoogleCloudPubSubAdapter($client, null, false); 217 | 218 | $adapter->publish('channel_name', ['hello' => 'world']); 219 | } 220 | 221 | public function testPublishWhenAutoTopicCreationIsDisabledAndBackgroundBatchingIsEnabled() 222 | { 223 | $batchPublisher = Mockery::mock(BatchPublisher::class); 224 | 225 | $topic = Mockery::mock(Topic::class); 226 | $topic->shouldNotHaveReceived('exists'); 227 | $topic->shouldNotHaveReceived('create'); 228 | $topic->shouldReceive('batchPublisher') 229 | ->once() 230 | ->andReturn($batchPublisher); 231 | 232 | $batchPublisher->shouldReceive('publish') 233 | ->with([ 234 | 'data' => '{"hello":"world"}', 235 | ]) 236 | ->once(); 237 | 238 | $client = Mockery::mock(PubSubClient::class); 239 | $client->shouldReceive('topic') 240 | ->with('channel_name') 241 | ->once() 242 | ->andReturn($topic); 243 | 244 | $adapter = new GoogleCloudPubSubAdapter($client, null, false); 245 | $adapter->setBackgroundBatching(true); 246 | 247 | $adapter->publish('channel_name', ['hello' => 'world']); 248 | } 249 | 250 | public function testPublishBatch() 251 | { 252 | $topic = Mockery::mock(Topic::class); 253 | $topic->shouldReceive('exists') 254 | ->once() 255 | ->andReturn(true); 256 | $topic->shouldReceive('publishBatch') 257 | ->with([ 258 | ['data' => '{"hello":"world"}'], 259 | ['data' => '"booo!"'], 260 | ]) 261 | ->once(); 262 | 263 | $client = Mockery::mock(PubSubClient::class); 264 | $client->shouldReceive('topic') 265 | ->with('channel_name') 266 | ->once() 267 | ->andReturn($topic); 268 | 269 | $adapter = new GoogleCloudPubSubAdapter($client); 270 | 271 | $messages = [ 272 | ['hello' => 'world'], 273 | 'booo!', 274 | ]; 275 | $adapter->publishBatch('channel_name', $messages); 276 | } 277 | 278 | public function testPublishBatchWhenBackgroundBatchingIsEnabled() 279 | { 280 | $batchPublisher = Mockery::mock(BatchPublisher::class); 281 | 282 | $topic = Mockery::mock(Topic::class); 283 | $topic->shouldReceive('exists') 284 | ->once() 285 | ->andReturn(true); 286 | $topic->shouldReceive('batchPublisher') 287 | ->once() 288 | ->andReturn($batchPublisher); 289 | 290 | $batchPublisher->shouldReceive('publish') 291 | ->with([ 292 | 'data' => '{"hello":"world"}', 293 | ]) 294 | ->once(); 295 | $batchPublisher->shouldReceive('publish') 296 | ->with([ 297 | 'data' => '"booo!"', 298 | ]) 299 | ->once(); 300 | 301 | $client = Mockery::mock(PubSubClient::class); 302 | $client->shouldReceive('topic') 303 | ->with('channel_name') 304 | ->once() 305 | ->andReturn($topic); 306 | 307 | $adapter = new GoogleCloudPubSubAdapter($client); 308 | $adapter->setBackgroundBatching(true); 309 | 310 | $messages = [ 311 | ['hello' => 'world'], 312 | 'booo!', 313 | ]; 314 | $adapter->publishBatch('channel_name', $messages); 315 | } 316 | 317 | public function testSubscribeWhenSubscriptionMustBeCreated() 318 | { 319 | $message1 = new Message(['data' => '{"hello":"world"}'], ['ackId' => 1]); 320 | $message2 = new Message(['data' => '"this is a string"'], ['ackId' => 2]); 321 | $message3 = new Message(['data' => '"unsubscribe"'], ['ackId' => 3]); 322 | 323 | $messageBatch1 = [ 324 | $message1, 325 | $message2, 326 | ]; 327 | $messageBatch2 = [ 328 | $message3, 329 | ]; 330 | 331 | $subscription = Mockery::mock(Subscription::class); 332 | $subscription->shouldReceive('exists') 333 | ->once() 334 | ->andReturn(false); 335 | $subscription->shouldReceive('create') 336 | ->once(); 337 | $subscription->shouldReceive('pull') 338 | ->with([ 339 | 'grpcOptions' => [ 340 | 'timeoutMillis' => null, 341 | ], 342 | 'maxMessages' => 1000, 343 | 'returnImmediately' => false 344 | ]) 345 | ->once() 346 | ->andReturn($messageBatch1); 347 | 348 | $subscription->shouldReceive('acknowledge') 349 | ->with($message1) 350 | ->once(); 351 | $subscription->shouldReceive('acknowledge') 352 | ->with($message2) 353 | ->once(); 354 | $subscription->shouldReceive('pull') 355 | ->with([ 356 | 'grpcOptions' => [ 357 | 'timeoutMillis' => null, 358 | ], 359 | 'maxMessages' => 1000, 360 | 'returnImmediately' => false 361 | ]) 362 | ->once() 363 | ->andReturn($messageBatch2); 364 | $subscription->shouldReceive('acknowledge') 365 | ->with($message3) 366 | ->once(); 367 | 368 | $topic = Mockery::mock(Topic::class); 369 | $topic->shouldReceive('exists') 370 | ->once() 371 | ->andReturn(true); 372 | $topic->shouldNotHaveReceived('create'); 373 | $topic->shouldReceive('subscription') 374 | ->with('default.channel_name') 375 | ->once() 376 | ->andReturn($subscription); 377 | 378 | $client = Mockery::mock(PubSubClient::class); 379 | $client->shouldReceive('topic') 380 | ->with('channel_name') 381 | ->once() 382 | ->andReturn($topic); 383 | 384 | $adapter = new GoogleCloudPubSubAdapter($client); 385 | 386 | $handler1 = Mockery::mock(\stdClass::class); 387 | $handler1->shouldReceive('handle') 388 | ->with(['hello' => 'world']) 389 | ->once(); 390 | $handler1->shouldReceive('handle') 391 | ->with('this is a string') 392 | ->once(); 393 | 394 | $adapter->subscribe('channel_name', [$handler1, 'handle']); 395 | } 396 | 397 | public function testSubscribeWhenSubscriptionExists() 398 | { 399 | $message1 = new Message(['data' => '{"hello":"world"}'], ['ackId' => 1]); 400 | $message2 = new Message(['data' => '"this is a string"'], ['ackId' => 2]); 401 | $message3 = new Message(['data' => '"unsubscribe"'], ['ackId' => 3]); 402 | 403 | $messageBatch1 = [ 404 | $message1, 405 | $message2, 406 | ]; 407 | $messageBatch2 = [ 408 | $message3, 409 | ]; 410 | 411 | $subscription = Mockery::mock(Subscription::class); 412 | $subscription->shouldReceive('exists') 413 | ->once() 414 | ->andReturn(true); 415 | $subscription->shouldNotHaveReceived('create'); 416 | $subscription->shouldReceive('pull') 417 | ->with([ 418 | 'grpcOptions' => [ 419 | 'timeoutMillis' => null, 420 | ], 421 | 'maxMessages' => 1000, 422 | 'returnImmediately' => false 423 | ]) 424 | ->once() 425 | ->andReturn($messageBatch1); 426 | $subscription->shouldReceive('acknowledge') 427 | ->with($message1) 428 | ->once(); 429 | $subscription->shouldReceive('acknowledge') 430 | ->with($message2) 431 | ->once(); 432 | $subscription->shouldReceive('pull') 433 | ->with([ 434 | 'grpcOptions' => [ 435 | 'timeoutMillis' => null, 436 | ], 437 | 'maxMessages' => 1000, 438 | 'returnImmediately' => false 439 | ]) 440 | ->once() 441 | ->andReturn($messageBatch2); 442 | $subscription->shouldReceive('acknowledge') 443 | ->with($message3) 444 | ->once(); 445 | 446 | $topic = Mockery::mock(Topic::class); 447 | $topic->shouldReceive('exists') 448 | ->once() 449 | ->andReturn(true); 450 | $topic->shouldNotHaveReceived('create'); 451 | $topic->shouldReceive('subscription') 452 | ->with('default.channel_name') 453 | ->once() 454 | ->andReturn($subscription); 455 | 456 | $client = Mockery::mock(PubSubClient::class); 457 | $client->shouldReceive('topic') 458 | ->with('channel_name') 459 | ->once() 460 | ->andReturn($topic); 461 | 462 | $adapter = new GoogleCloudPubSubAdapter($client); 463 | 464 | $handler1 = Mockery::mock(\stdClass::class); 465 | $handler1->shouldReceive('handle') 466 | ->with(['hello' => 'world']) 467 | ->once(); 468 | $handler1->shouldReceive('handle') 469 | ->with('this is a string') 470 | ->once(); 471 | 472 | $adapter->subscribe('channel_name', [$handler1, 'handle']); 473 | } 474 | 475 | public function testSubscribeWhenAutoTopicCreationIsDisabled() 476 | { 477 | $message1 = new Message(['data' => '{"hello":"world"}'], ['ackId' => 1]); 478 | $message2 = new Message(['data' => '"this is a string"'], ['ackId' => 2]); 479 | $message3 = new Message(['data' => '"unsubscribe"'], ['ackId' => 3]); 480 | 481 | $messageBatch1 = [ 482 | $message1, 483 | $message2, 484 | ]; 485 | $messageBatch2 = [ 486 | $message3, 487 | ]; 488 | 489 | $subscription = Mockery::mock(Subscription::class); 490 | $subscription->shouldNotHaveReceived('exists'); 491 | $subscription->shouldNotHaveReceived('create'); 492 | $subscription->shouldReceive('pull') 493 | ->with([ 494 | 'grpcOptions' => [ 495 | 'timeoutMillis' => null, 496 | ], 497 | 'maxMessages' => 1000, 498 | 'returnImmediately' => false 499 | ]) 500 | ->once() 501 | ->andReturn($messageBatch1); 502 | $subscription->shouldReceive('acknowledge') 503 | ->with($message1) 504 | ->once(); 505 | $subscription->shouldReceive('acknowledge') 506 | ->with($message2) 507 | ->once(); 508 | $subscription->shouldReceive('pull') 509 | ->with([ 510 | 'grpcOptions' => [ 511 | 'timeoutMillis' => null, 512 | ], 513 | 'maxMessages' => 1000, 514 | 'returnImmediately' => false 515 | ]) 516 | ->once() 517 | ->andReturn($messageBatch2); 518 | $subscription->shouldReceive('acknowledge') 519 | ->with($message3) 520 | ->once(); 521 | 522 | $topic = Mockery::mock(Topic::class); 523 | $topic->shouldReceive('exists') 524 | ->once() 525 | ->andReturn(true); 526 | $topic->shouldNotHaveReceived('create'); 527 | $topic->shouldReceive('subscription') 528 | ->with('default.channel_name') 529 | ->once() 530 | ->andReturn($subscription); 531 | 532 | $client = Mockery::mock(PubSubClient::class); 533 | $client->shouldReceive('topic') 534 | ->with('channel_name') 535 | ->once() 536 | ->andReturn($topic); 537 | 538 | $adapter = new GoogleCloudPubSubAdapter($client, null, true, false); 539 | 540 | $handler1 = Mockery::mock(\stdClass::class); 541 | $handler1->shouldReceive('handle') 542 | ->with(['hello' => 'world']) 543 | ->once(); 544 | $handler1->shouldReceive('handle') 545 | ->with('this is a string') 546 | ->once(); 547 | 548 | $adapter->subscribe('channel_name', [$handler1, 'handle']); 549 | } 550 | 551 | public function testSubscribeWhenReturnImmediatelyIsEnabled() 552 | { 553 | $message1 = new Message(['data' => '{"hello":"world"}'], ['ackId' => 1]); 554 | $message2 = new Message(['data' => '"this is a string"'], ['ackId' => 2]); 555 | $message3 = new Message(['data' => '"unsubscribe"'], ['ackId' => 3]); 556 | 557 | $messageBatch1 = [ 558 | $message1, 559 | $message2, 560 | ]; 561 | 562 | $messageBatch2 = [ 563 | $message3, 564 | ]; 565 | 566 | $subscription = Mockery::mock(Subscription::class); 567 | $subscription->shouldReceive('exists') 568 | ->once() 569 | ->andReturn(true); 570 | $subscription->shouldNotHaveReceived('create'); 571 | 572 | $expectedPullOptions = [ 573 | 'grpcOptions' => [ 574 | 'timeoutMillis' => null, 575 | ], 576 | 'maxMessages' => 1000, 577 | 'returnImmediately' => true 578 | ]; 579 | 580 | $subscription->shouldReceive('pull') 581 | ->with($expectedPullOptions) 582 | ->once() 583 | ->andReturn($messageBatch1); 584 | $subscription->shouldReceive('acknowledge') 585 | ->with($message1) 586 | ->once(); 587 | $subscription->shouldReceive('acknowledge') 588 | ->with($message2) 589 | ->once(); 590 | 591 | $subscription->shouldReceive('pull') 592 | ->with($expectedPullOptions) 593 | ->once() 594 | ->andReturn($messageBatch2); 595 | $subscription->shouldReceive('acknowledge') 596 | ->with($message3) 597 | ->once(); 598 | 599 | $topic = Mockery::mock(Topic::class); 600 | $topic->shouldReceive('exists') 601 | ->once() 602 | ->andReturn(true); 603 | $topic->shouldNotHaveReceived('create'); 604 | $topic->shouldReceive('subscription') 605 | ->with('default.channel_name') 606 | ->once() 607 | ->andReturn($subscription); 608 | 609 | $client = Mockery::mock(PubSubClient::class); 610 | $client->shouldReceive('topic') 611 | ->with('channel_name') 612 | ->once() 613 | ->andReturn($topic); 614 | 615 | $handler1 = Mockery::mock(\stdClass::class); 616 | $handler1->shouldReceive('handle') 617 | ->with(['hello' => 'world']) 618 | ->once(); 619 | $handler1->shouldReceive('handle') 620 | ->with('this is a string') 621 | ->once(); 622 | 623 | $adapter = new GoogleCloudPubSubAdapter($client); 624 | $adapter->setReturnImmediately(true); 625 | $adapter->subscribe('channel_name', [$handler1, 'handle']); 626 | } 627 | } 628 | --------------------------------------------------------------------------------