├── VERSION
├── .styleci.yml
├── .gitignore
├── .editorconfig
├── src
├── Traits
│ └── HasOrderingKey.php
├── PubSubQueueServiceProvider.php
├── Connectors
│ └── PubSubConnector.php
├── Jobs
│ └── PubSubJob.php
└── PubSubQueue.php
├── phpunit.xml.dist
├── .github
└── workflows
│ └── main.yml
├── LICENSE
├── composer.json
├── README.md
└── tests
└── Unit
├── Connectors
└── PubSubConnectorTests.php
├── Jobs
└── PubSubJobTests.php
└── PubSubQueueTests.php
/VERSION:
--------------------------------------------------------------------------------
1 | 0.10.0
2 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: laravel
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 | /.phpunit.cache
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 4
6 | indent_style = space
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/src/Traits/HasOrderingKey.php:
--------------------------------------------------------------------------------
1 | orderingKey = $orderingKey;
12 |
13 | return $this;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/PubSubQueueServiceProvider.php:
--------------------------------------------------------------------------------
1 | app['queue']->addConnector('pubsub', function () {
18 | return new PubSubConnector;
19 | });
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
19 | src/
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | paths-ignore: ['*.md']
6 | pull_request:
7 | branches: [master]
8 | paths-ignore: ['*.md']
9 |
10 | jobs:
11 | test:
12 | name: Test (PHP ${{ matrix.php }})
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | php: [8.1, 8.2, 8.3]
17 | steps:
18 | - uses: actions/checkout@v3
19 | - name: Setup PHP
20 | uses: shivammathur/setup-php@v2
21 | with:
22 | php-version: ${{ matrix.php }}
23 | extensions: json
24 | coverage: none
25 | - name: Install composer dependencies
26 | run: composer install --prefer-dist --no-interaction --no-progress
27 | - name: Run the test suite
28 | run: vendor/bin/phpunit
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Kendryck Abdou
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kainxspirits/laravel-pubsub-queue",
3 | "description": "Queue driver for Google Cloud Pub/Sub.",
4 | "keywords": [
5 | "kainxspirits",
6 | "laravel",
7 | "queue",
8 | "gcp",
9 | "google",
10 | "pubsub"
11 | ],
12 | "license": "MIT",
13 | "type": "library",
14 | "authors": [
15 | {
16 | "name": "Kendryck",
17 | "email": "kainxspirits@users.noreply.github.com"
18 | }
19 | ],
20 | "require": {
21 | "php": ">=8.1",
22 | "ext-json": "*",
23 | "illuminate/queue": "5.7.* | 5.8.* | ^6.0 | ^7.0 | ^8.0 | ^9.0| ^10.0 | ^11.0 | ^12.0",
24 | "illuminate/support": "5.7.* | 5.8.* | ^6.0 | ^7.0 | ^8.0 | ^9.0| ^10.0 | ^11.0 | ^12.0",
25 | "google/cloud-pubsub": "^1.1",
26 | "ramsey/uuid": "^2.0|^3.0|^4.0"
27 | },
28 | "require-dev": {
29 | "phpunit/phpunit": "^10.0"
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "Kainxspirits\\PubSubQueue\\": "src/"
34 | }
35 | },
36 | "autoload-dev": {
37 | "psr-4": {
38 | "Kainxspirits\\PubSubQueue\\Tests\\": "tests/"
39 | }
40 | },
41 | "scripts": {
42 | "test": "vendor/bin/phpunit"
43 | },
44 | "extra": {
45 | "laravel": {
46 | "providers": [
47 | "Kainxspirits\\PubSubQueue\\PubSubQueueServiceProvider"
48 | ]
49 | }
50 | },
51 | "minimum-stability": "stable"
52 | }
53 |
--------------------------------------------------------------------------------
/src/Connectors/PubSubConnector.php:
--------------------------------------------------------------------------------
1 | transformConfig($config);
28 |
29 | return new PubSubQueue(
30 | new PubSubClient($gcp_config),
31 | $config['queue'] ?? $this->default_queue,
32 | $config['subscriber'] ?? 'subscriber',
33 | $config['create_topics'] ?? true,
34 | $config['create_subscriptions'] ?? true,
35 | $config['queue_prefix'] ?? ''
36 | );
37 | }
38 |
39 | /**
40 | * Transform the config to key => value array.
41 | *
42 | * @param array $config
43 | * @return array
44 | */
45 | protected function transformConfig($config)
46 | {
47 | return array_reduce(array_map([$this, 'transformConfigKeys'], $config, array_keys($config)), function ($carry, $item) {
48 | $carry[$item[0]] = $item[1];
49 |
50 | return $carry;
51 | }, []);
52 | }
53 |
54 | /**
55 | * Transform the keys of config to camelCase.
56 | *
57 | * @param string $item
58 | * @param string $key
59 | * @return array
60 | */
61 | protected function transformConfigKeys($item, $key)
62 | {
63 | return [Str::camel($key), $item];
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel PubSub Queue
2 |
3 | 
4 | [](https://styleci.io/repos/131718560)
5 |
6 | This package is a Laravel queue driver that uses the [Google PubSub](https://github.com/GoogleCloudPlatform/google-cloud-php-pubsub) service.
7 |
8 | ## Installation
9 |
10 | You can easily install this package with [Composer](https://getcomposer.org) by running this command :
11 |
12 | ```bash
13 | composer require kainxspirits/laravel-pubsub-queue
14 | ```
15 |
16 | If you disabled package discovery, you can still manually register this package by adding the following line to the providers of your `config/app.php` file :
17 |
18 | ```php
19 | Kainxspirits\PubSubQueue\PubSubQueueServiceProvider::class,
20 | ```
21 |
22 | ## Configuration
23 |
24 | Add a `pubsub` connection to your `config/queue.php` file. From there, you can use any configuration values from the original pubsub client. Just make sure to use snake_case for the keys name.
25 |
26 | You can check [Google Cloud PubSub client](http://googleapis.github.io/google-cloud-php/#/docs/cloud-pubsub/master/pubsub/pubsubclient?method=__construct) for more details about the different options.
27 |
28 | ```php
29 | 'pubsub' => [
30 | 'driver' => 'pubsub',
31 | 'queue' => env('PUBSUB_QUEUE', 'default'),
32 | 'queue_prefix' => env('PUBSUB_QUEUE_PREFIX', ''),
33 | 'project_id' => env('PUBSUB_PROJECT_ID', 'your-project-id'),
34 | 'retries' => 3,
35 | 'request_timeout' => 60,
36 | 'subscriber' => 'subscriber-name',
37 | 'create_topics' => true,
38 | 'create_subscriptions' => true,
39 | ],
40 | ```
41 |
42 | ## Avoiding Administrator Operations Limits
43 |
44 | To avoid limit issues, change the `create_topics` and `create_subscriptions` flags to false.
45 |
46 | ## Testing
47 |
48 | You can run the tests with :
49 |
50 | ```bash
51 | vendor/bin/phpunit
52 | ```
53 |
54 | ## License
55 |
56 | This project is licensed under the terms of the MIT license. See [License File](LICENSE) for more information.
57 |
--------------------------------------------------------------------------------
/tests/Unit/Connectors/PubSubConnectorTests.php:
--------------------------------------------------------------------------------
1 | assertTrue($reflection->implementsInterface(ConnectorInterface::class));
18 | }
19 |
20 | public function testConnectReturnsPubSubQueueInstance(): void
21 | {
22 | $connector = new PubSubConnector;
23 | $config = $this->createFakeConfig();
24 | $queue = $connector->connect($config);
25 |
26 | $this->assertTrue($queue instanceof PubSubQueue);
27 | $this->assertEquals($queue->getSubscriberName(), 'test-subscriber');
28 | }
29 |
30 | public function testQueuePrefixAdded(): void
31 | {
32 | $connector = new PubSubConnector();
33 | $config = $this->createFakeConfig() + ['queue_prefix' => 'prefix-'];
34 | $queue = $connector->connect($config);
35 |
36 | $this->assertEquals('prefix-my-queue', $queue->getQueue('my-queue'));
37 | }
38 |
39 | public function testNotQueuePrefixAddedMultipleTimes(): void
40 | {
41 | $connector = new PubSubConnector();
42 | $config = $this->createFakeConfig() + ['queue_prefix' => 'prefix-'];
43 | $queue = $connector->connect($config);
44 |
45 | $this->assertEquals('prefix-default', $queue->getQueue($queue->getQueue('default')));
46 | }
47 |
48 | private function createFakeConfig()
49 | {
50 | return [
51 | 'queue' => 'test',
52 | 'project_id' => 'the-project-id',
53 | 'subscriber' => 'test-subscriber',
54 | 'retries' => 1,
55 | 'request_timeout' => 60,
56 | ];
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Jobs/PubSubJob.php:
--------------------------------------------------------------------------------
1 | pubsub = $pubsub;
46 | $this->job = $job;
47 | $this->queue = $queue;
48 | $this->container = $container;
49 | $this->connectionName = $connectionName;
50 |
51 | $this->decoded = $this->payload();
52 | }
53 |
54 | /**
55 | * Get the job identifier.
56 | *
57 | * @return string
58 | */
59 | public function getJobId()
60 | {
61 | return $this->decoded['id'] ?? null;
62 | }
63 |
64 | /**
65 | * Get the raw body of the job.
66 | *
67 | * @return string
68 | */
69 | public function getRawBody()
70 | {
71 | return base64_decode($this->job->data());
72 | }
73 |
74 | /**
75 | * Get the number of times the job has been attempted.
76 | *
77 | * @return int
78 | */
79 | public function attempts()
80 | {
81 | return ((int) $this->job->attribute('attempts') ?? 0) + 1;
82 | }
83 |
84 | /**
85 | * Release the job back into the queue.
86 | *
87 | * @param int $delay
88 | * @return void
89 | */
90 | public function release($delay = 0)
91 | {
92 | parent::release($delay);
93 |
94 | $attempts = $this->attempts();
95 | $this->pubsub->republish(
96 | $this->job,
97 | $this->queue,
98 | ['attempts' => (string) $attempts],
99 | $delay
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/tests/Unit/Jobs/PubSubJobTests.php:
--------------------------------------------------------------------------------
1 | messageId = '1234';
59 | $this->messageData = json_encode(['id' => $this->messageId, 'foo' => 'bar']);
60 | $this->messageEncodedData = base64_encode($this->messageData);
61 |
62 | $this->container = $this->createMock(Container::class);
63 | $this->queue = $this->createMock(PubSubQueue::class);
64 | $this->client = $this->createMock(PubSubClient::class);
65 |
66 | $this->message = $this->getMockBuilder(Message::class)
67 | ->setConstructorArgs([[], []])
68 | ->onlyMethods(['data', 'id', 'attributes'])
69 | ->getMock();
70 |
71 | $this->message->method('data')
72 | ->willReturn($this->messageEncodedData);
73 |
74 | $this->message->method('id')
75 | ->willReturn($this->messageId);
76 |
77 | $this->message->method('attributes')
78 | ->with($this->equalTo('attempts'))
79 | ->willReturn(42);
80 |
81 | $this->job = $this->getMockBuilder(PubSubJob::class)
82 | ->setConstructorArgs([$this->container, $this->queue, $this->message, 'test', 'test'])
83 | ->onlyMethods([])
84 | ->getMock();
85 | }
86 |
87 | public function testImplementsJobInterface(): void
88 | {
89 | $reflection = new ReflectionClass(PubSubJob::class);
90 | $this->assertTrue($reflection->implementsInterface(JobContract::class));
91 | }
92 |
93 | public function testGetJobId(): void
94 | {
95 | $this->assertEquals($this->job->getJobId(), $this->messageId);
96 | }
97 |
98 | public function testGetRawBody(): void
99 | {
100 | $this->assertEquals($this->job->getRawBody(), $this->messageData);
101 | }
102 |
103 | public function testDeleteMethodSetDeletedProperty(): void
104 | {
105 | $this->job->delete();
106 | $this->assertTrue($this->job->isDeleted());
107 | }
108 |
109 | public function testAttempts(): void
110 | {
111 | $this->assertTrue(is_int($this->job->attempts()));
112 | }
113 |
114 | public function testReleaseAndPublish(): void
115 | {
116 | $this->queue->expects($this->once())
117 | ->method('republish')
118 | ->with(
119 | $this->anything(),
120 | $this->anything(),
121 | $this->callback(function ($options) {
122 | if (! is_array($options)) {
123 | return false;
124 | }
125 |
126 | foreach ($options as $key => $option) {
127 | if (! is_string($option) || ! is_string($key)) {
128 | return false;
129 | }
130 | }
131 |
132 | return true;
133 | })
134 | );
135 |
136 | $this->job->release();
137 | }
138 |
139 | public function testReleaseMethodSetReleasedProperty(): void
140 | {
141 | $this->job->release();
142 | $this->assertTrue($this->job->isReleased());
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/PubSubQueue.php:
--------------------------------------------------------------------------------
1 | pubsub = $pubsub;
66 | $this->default = $default;
67 | $this->subscriber = $subscriber;
68 | $this->topicAutoCreation = $topicAutoCreation;
69 | $this->subscriptionAutoCreation = $subscriptionAutoCreation;
70 | $this->queuePrefix = $queuePrefix;
71 | }
72 |
73 | /**
74 | * Get the size of the queue.
75 | * PubSubClient have no method to retrieve the size of the queue.
76 | * To be updated if the API allow to get that data.
77 | *
78 | * @param string $queue
79 | * @return int
80 | */
81 | public function size($queue = null)
82 | {
83 | return 0;
84 | }
85 |
86 | /**
87 | * Push a new job onto the queue.
88 | *
89 | * @param string|object $job
90 | * @param mixed $data
91 | * @param string $queue
92 | * @return mixed
93 | */
94 | public function push($job, $data = '', $queue = null)
95 | {
96 | $options = [];
97 |
98 | if (isset($job->orderingKey)) {
99 | $options['orderingKey'] = $job->orderingKey;
100 | }
101 |
102 | return $this->enqueueUsing(
103 | $job,
104 | $this->createPayload($job, $this->getQueue($queue), $data),
105 | $queue,
106 | null,
107 | function ($payload, $queue) use ($options) {
108 | return $this->pushRaw($payload, $queue, $options);
109 | }
110 | );
111 | }
112 |
113 | /**
114 | * Push a raw payload onto the queue.
115 | *
116 | * @param string $payload
117 | * @param string $queue
118 | * @param array $options
119 | * @return array
120 | */
121 | public function pushRaw($payload, $queue = null, array $options = [])
122 | {
123 | $topic = $this->getTopic($queue, $this->topicAutoCreation);
124 |
125 | $this->subscribeToTopic($topic);
126 |
127 | $publish = ['data' => base64_encode($payload)];
128 |
129 | if (! empty($options)) {
130 | $publish['attributes'] = $this->validateMessageAttributes($options);
131 |
132 | if (isset($options['orderingKey'])) {
133 | $publish['orderingKey'] = $options['orderingKey'];
134 | }
135 | }
136 | $topic->publish($publish);
137 |
138 | $decoded_payload = json_decode($payload, true);
139 |
140 | return $decoded_payload['id'];
141 | }
142 |
143 | /**
144 | * Push a new job onto the queue after a delay.
145 | *
146 | * @param \DateTimeInterface|\DateInterval|int $delay
147 | * @param string|object $job
148 | * @param mixed $data
149 | * @param string $queue
150 | * @return mixed
151 | */
152 | public function later($delay, $job, $data = '', $queue = null)
153 | {
154 | return $this->pushRaw(
155 | $this->createPayload($job, $this->getQueue($queue), $data),
156 | $queue,
157 | ['available_at' => (string) $this->availableAt($delay)]
158 | );
159 | }
160 |
161 | /**
162 | * Pop the next job off of the queue.
163 | *
164 | * @param string $queue
165 | * @return \Illuminate\Contracts\Queue\Job|null
166 | */
167 | public function pop($queue = null)
168 | {
169 | $topic = $this->getTopic($this->getQueue($queue));
170 |
171 | if ($this->topicAutoCreation && ! $topic->exists()) {
172 | return;
173 | }
174 |
175 | $subscription = $topic->subscription($this->getSubscriberName());
176 | $messages = $subscription->pull([
177 | 'returnImmediately' => true,
178 | 'maxMessages' => 1,
179 | ]);
180 |
181 | if (empty($messages) || count($messages) < 1) {
182 | return;
183 | }
184 |
185 | $available_at = $messages[0]->attribute('available_at');
186 | if ($available_at && $available_at > time()) {
187 | return;
188 | }
189 |
190 | $this->acknowledge($messages[0], $queue);
191 |
192 | return new PubSubJob(
193 | $this->container,
194 | $this,
195 | $messages[0],
196 | $this->connectionName,
197 | $this->getQueue($queue)
198 | );
199 | }
200 |
201 | /**
202 | * Push an array of jobs onto the queue.
203 | *
204 | * @param array $jobs
205 | * @param mixed $data
206 | * @param string $queue
207 | * @return mixed
208 | */
209 | public function bulk($jobs, $data = '', $queue = null)
210 | {
211 | $payloads = [];
212 |
213 | foreach ((array) $jobs as $job) {
214 | $payload = $this->createPayload($job, $this->getQueue($queue), $data);
215 | $payloads[] = ['data' => base64_encode($payload)] + (isset($job->orderingKey) ? ['orderingKey' => $job->orderingKey] : []);
216 | }
217 |
218 | $topic = $this->getTopic($this->getQueue($queue), $this->topicAutoCreation);
219 |
220 | $this->subscribeToTopic($topic);
221 |
222 | return $topic->publishBatch($payloads);
223 | }
224 |
225 | /**
226 | * Acknowledge a message.
227 | *
228 | * @param \Google\Cloud\PubSub\Message $message
229 | * @param string $queue
230 | */
231 | public function acknowledge(Message $message, $queue = null)
232 | {
233 | $subscription = $this->getTopic($this->getQueue($queue))->subscription($this->getSubscriberName());
234 | $subscription->acknowledge($message);
235 | }
236 |
237 | /**
238 | * Republish a message onto the queue.
239 | *
240 | * @param \Google\Cloud\PubSub\Message $message
241 | * @param string $queue
242 | * @return mixed
243 | */
244 | public function republish(Message $message, $queue = null, $options = [], $delay = 0)
245 | {
246 | $topic = $this->getTopic($this->getQueue($queue));
247 |
248 | $options = array_merge([
249 | 'available_at' => (string) $this->availableAt($delay),
250 | ], $this->validateMessageAttributes($options));
251 |
252 | return $topic->publish([
253 | 'data' => $message->data(),
254 | 'attributes' => $options,
255 | ]);
256 | }
257 |
258 | /**
259 | * Create a payload array from the given job and data.
260 | *
261 | * @param mixed $job
262 | * @param string $queue
263 | * @param mixed $data
264 | * @return array
265 | */
266 | protected function createPayloadArray($job, $queue, $data = '')
267 | {
268 | return array_merge(parent::createPayloadArray($job, $this->getQueue($queue), $data), [
269 | 'id' => $this->getRandomId(),
270 | ]);
271 | }
272 |
273 | /**
274 | * Check if the attributes array only contains key-values
275 | * pairs made of strings.
276 | *
277 | * @param array $attributes
278 | * @return array
279 | *
280 | * @throws \UnexpectedValueException
281 | */
282 | private function validateMessageAttributes($attributes): array
283 | {
284 | $attributes_values = array_filter($attributes, 'is_string');
285 |
286 | if (count($attributes_values) !== count($attributes)) {
287 | throw new \UnexpectedValueException('PubSubMessage attributes only accept key-value pairs and all values must be string.');
288 | }
289 |
290 | $attributes_keys = array_filter(array_keys($attributes), 'is_string');
291 |
292 | if (count($attributes_keys) !== count(array_keys($attributes))) {
293 | throw new \UnexpectedValueException('PubSubMessage attributes only accept key-value pairs and all keys must be string.');
294 | }
295 |
296 | return $attributes;
297 | }
298 |
299 | /**
300 | * Get the current topic.
301 | *
302 | * @param string $queue
303 | * @param string $create
304 | * @return \Google\Cloud\PubSub\Topic
305 | */
306 | public function getTopic($queue, $create = false)
307 | {
308 | $queue = $this->getQueue($queue);
309 | $topic = $this->pubsub->topic($queue);
310 |
311 | // don't check topic if automatic creation is not required, to avoid additional administrator operations calls
312 | if ($create && ! $topic->exists()) {
313 | $topic->create();
314 | }
315 |
316 | return $topic;
317 | }
318 |
319 | /**
320 | * Create a new subscription to a topic.
321 | *
322 | * @param \Google\Cloud\PubSub\Topic $topic
323 | * @return \Google\Cloud\PubSub\Subscription
324 | */
325 | public function subscribeToTopic(Topic $topic)
326 | {
327 | $subscription = $topic->subscription($this->getSubscriberName());
328 |
329 | // don't check subscription if automatic creation is not required, to avoid additional administrator operations calls
330 | if ($this->subscriptionAutoCreation && ! $subscription->exists()) {
331 | $subscription = $topic->subscribe($this->getSubscriberName());
332 | }
333 |
334 | return $subscription;
335 | }
336 |
337 | /**
338 | * Get subscriber name.
339 | *
340 | * @param \Google\Cloud\PubSub\Topic $topic
341 | * @return string
342 | */
343 | public function getSubscriberName()
344 | {
345 | return $this->subscriber;
346 | }
347 |
348 | /**
349 | * Get the PubSub instance.
350 | *
351 | * @return \Google\Cloud\PubSub\PubSubClient
352 | */
353 | public function getPubSub()
354 | {
355 | return $this->pubsub;
356 | }
357 |
358 | /**
359 | * Get the queue or return the default.
360 | *
361 | * @param string|null $queue
362 | * @return string
363 | */
364 | public function getQueue($queue)
365 | {
366 | $queue = $queue ?: $this->default;
367 |
368 | if (! $this->queuePrefix || Str::startsWith($queue, $this->queuePrefix)) {
369 | return $queue;
370 | }
371 |
372 | return $this->queuePrefix.$queue;
373 | }
374 |
375 | /**
376 | * Get a random ID string.
377 | *
378 | * @return string
379 | */
380 | protected function getRandomId()
381 | {
382 | return Str::random(32);
383 | }
384 | }
385 |
--------------------------------------------------------------------------------
/tests/Unit/PubSubQueueTests.php:
--------------------------------------------------------------------------------
1 | expectedResult = 'message-id';
52 |
53 | $this->topic = $this->createMock(Topic::class);
54 | $this->client = $this->createMock(PubSubClient::class);
55 | $this->subscription = $this->createMock(Subscription::class);
56 | $this->message = $this->createMock(Message::class);
57 |
58 | $this->queue = $this->getMockBuilder(PubSubQueue::class)
59 | ->setConstructorArgs([$this->client, 'default'])
60 | ->onlyMethods([
61 | 'pushRaw',
62 | 'getTopic',
63 | 'availableAt',
64 | 'subscribeToTopic',
65 | ])->getMock();
66 | }
67 |
68 | public function testImplementsQueueInterface(): void
69 | {
70 | $reflection = new ReflectionClass(PubSubQueue::class);
71 | $this->assertTrue($reflection->implementsInterface(QueueContract::class));
72 | }
73 |
74 | public function testPushNewJob(): void
75 | {
76 | $job = 'test';
77 | $data = ['foo' => 'bar'];
78 |
79 | $this->queue->setContainer(Container::getInstance());
80 |
81 | $this->queue->expects($this->once())
82 | ->method('pushRaw')
83 | ->willReturn($this->expectedResult)
84 | ->with($this->callback(function ($payload) use ($job, $data) {
85 | $decoded_payload = json_decode($payload, true);
86 |
87 | return $decoded_payload['data'] === $data && $decoded_payload['job'] === $job;
88 | }));
89 |
90 | $this->assertEquals($this->expectedResult, $this->queue->push('test', $data));
91 | }
92 |
93 | public function testPushRaw(): void
94 | {
95 | /** @var \PHPUnit\Framework\MockObject\MockObject&PubSubQueue $queue */
96 | $queue = $this->getMockBuilder(PubSubQueue::class)
97 | ->setConstructorArgs([$this->client, 'default'])
98 | ->onlyMethods(['getTopic', 'subscribeToTopic'])
99 | ->getMock();
100 |
101 | $payload = json_encode(['id' => $this->expectedResult]);
102 |
103 | $this->topic->method('publish')
104 | ->willReturn($this->expectedResult)
105 | ->with($this->callback(function ($publish) use ($payload) {
106 | $decoded_payload = base64_decode($publish['data']);
107 |
108 | return $decoded_payload === $payload;
109 | }));
110 |
111 | $queue->method('getTopic')
112 | ->willReturn($this->topic);
113 |
114 | $queue->method('subscribeToTopic')
115 | ->willReturn($this->subscription);
116 |
117 | $this->assertEquals($this->expectedResult, $queue->pushRaw($payload));
118 | }
119 |
120 | public function testPushRawOptionsOnlyAcceptKeyValueStrings(): void
121 | {
122 | $this->expectException(\UnexpectedValueException::class);
123 |
124 | /** @var \PHPUnit\Framework\MockObject\MockObject&PubSubQueue $queue */
125 | $queue = $this->getMockBuilder(PubSubQueue::class)
126 | ->setConstructorArgs([$this->client, 'default'])
127 | ->onlyMethods(['getTopic', 'subscribeToTopic'])
128 | ->getMock();
129 |
130 | $this->topic->method('publish')
131 | ->willReturn($this->expectedResult);
132 |
133 | $queue->method('getTopic')
134 | ->willReturn($this->topic);
135 |
136 | $queue->method('subscribeToTopic')
137 | ->willReturn($this->subscription);
138 |
139 | $payload = json_encode(['id' => $this->expectedResult]);
140 |
141 | $options = [
142 | 'integer' => 42,
143 | 'array' => [
144 | 'foo' => 'bar',
145 | ],
146 | 1 => 'wrong key',
147 | 'object' => new \stdClass,
148 | ];
149 |
150 | $queue->pushRaw($payload, '', $options);
151 | }
152 |
153 | public function testLater(): void
154 | {
155 | $job = 'test';
156 | $delay = 60;
157 | $delay_timestamp = Carbon::now()->addSeconds($delay)->getTimestamp();
158 |
159 | $this->queue->method('availableAt')
160 | ->willReturn($delay_timestamp);
161 |
162 | $this->queue->expects($this->once())
163 | ->method('pushRaw')
164 | ->willReturn($this->expectedResult)
165 | ->with(
166 | $this->isType('string'),
167 | $this->anything(),
168 | $this->callback(function ($options) use ($delay_timestamp) {
169 | if (! is_array($options)) {
170 | return false;
171 | }
172 |
173 | foreach ($options as $key => $option) {
174 | if (! is_string($option) || ! is_string($key)) {
175 | return false;
176 | }
177 | }
178 |
179 | if (! isset($options['available_at']) || $options['available_at'] !== (string) $delay_timestamp) {
180 | return false;
181 | }
182 |
183 | return true;
184 | })
185 | );
186 |
187 | $this->assertEquals($this->expectedResult, $this->queue->later($delay, $job, ['foo' => 'bar']));
188 | }
189 |
190 | public function testPopWhenJobsAvailable(): void
191 | {
192 | $this->subscription->expects($this->once())
193 | ->method('acknowledge');
194 |
195 | $this->subscription->method('pull')
196 | ->willReturn([$this->message]);
197 |
198 | $this->topic->method('subscription')
199 | ->willReturn($this->subscription);
200 |
201 | $this->topic->method('exists')
202 | ->willReturn(true);
203 |
204 | $this->queue->method('getTopic')
205 | ->willReturn($this->topic);
206 |
207 | $this->message->method('data')
208 | ->willReturn(base64_encode(json_encode(['foo' => 'bar'])));
209 |
210 | $this->queue->setContainer($this->createMock(Container::class));
211 |
212 | $this->assertTrue($this->queue->pop('test') instanceof PubSubJob);
213 | }
214 |
215 | public function testPopWhenNoJobAvailable(): void
216 | {
217 | $this->subscription->expects($this->exactly(0))
218 | ->method('acknowledge');
219 |
220 | $this->subscription->method('pull')
221 | ->willReturn([]);
222 |
223 | $this->topic->method('subscription')
224 | ->willReturn($this->subscription);
225 |
226 | $this->topic->method('exists')
227 | ->willReturn(true);
228 |
229 | $this->queue->method('getTopic')
230 | ->willReturn($this->topic);
231 |
232 | $this->assertTrue(is_null($this->queue->pop('test')));
233 | }
234 |
235 | public function testPopWhenTopicDoesNotExist(): void
236 | {
237 | $this->queue->method('getTopic')
238 | ->willReturn($this->topic);
239 |
240 | $this->topic->method('exists')
241 | ->willReturn(false);
242 |
243 | $this->assertTrue(is_null($this->queue->pop('test')));
244 | }
245 |
246 | public function testPopWhenJobDelayed(): void
247 | {
248 | $delay = 60;
249 | $timestamp = Carbon::now()->addSeconds($delay)->getTimestamp();
250 |
251 | $message = $this->message->method('attribute')
252 | ->willReturn($timestamp);
253 |
254 | $this->subscription->method('pull')
255 | ->willReturn([$this->message]);
256 |
257 | $this->topic->method('subscription')
258 | ->willReturn($this->subscription);
259 |
260 | $this->topic->method('exists')
261 | ->willReturn(true);
262 |
263 | $this->queue->method('getTopic')
264 | ->willReturn($this->topic);
265 |
266 | $this->queue->setContainer($this->createMock(Container::class));
267 |
268 | $this->assertTrue(is_null($this->queue->pop('test')));
269 | }
270 |
271 | public function testBulk(): void
272 | {
273 | $jobs = ['test'];
274 | $data = ['foo' => 'bar'];
275 |
276 | $this->topic->expects($this->once())
277 | ->method('publishBatch')
278 | ->willReturn($this->expectedResult)
279 | ->with($this->callback(function ($payloads) use ($jobs, $data) {
280 | $decoded_payload = json_decode(base64_decode($payloads[0]['data']), true);
281 |
282 | return $decoded_payload['job'] === $jobs[0] && $decoded_payload['data'] === $data;
283 | }));
284 |
285 | $this->queue->method('getTopic')
286 | ->willReturn($this->topic);
287 |
288 | $this->queue->method('subscribeToTopic')
289 | ->willReturn($this->subscription);
290 |
291 | $this->assertEquals($this->expectedResult, $this->queue->bulk($jobs, $data));
292 | }
293 |
294 | public function testAcknowledge(): void
295 | {
296 | $this->subscription->expects($this->once())
297 | ->method('acknowledge');
298 |
299 | $this->topic->method('subscription')
300 | ->willReturn($this->subscription);
301 |
302 | $this->queue->method('getTopic')
303 | ->willReturn($this->topic);
304 |
305 | $this->queue->acknowledge($this->message);
306 | }
307 |
308 | public function testRepublish(): void
309 | {
310 | $options = ['foo' => 'bar'];
311 | $delay = 60;
312 | $delay_timestamp = Carbon::now()->addSeconds($delay)->getTimestamp();
313 |
314 | $this->queue->method('getTopic')
315 | ->willReturn($this->topic);
316 |
317 | $this->queue->method('availableAt')
318 | ->willReturn($delay_timestamp);
319 |
320 | $this->topic->expects($this->once())
321 | ->method('publish')
322 | ->willReturn($this->expectedResult)
323 | ->with(
324 | $this->callback(function ($message) use ($options, $delay_timestamp) {
325 | if (! isset($message['attributes']) || ! is_array($message['attributes'])) {
326 | return false;
327 | }
328 |
329 | foreach ($message['attributes'] as $key => $attribute) {
330 | if (! is_string($attribute) || ! is_string($key)) {
331 | return false;
332 | }
333 | }
334 |
335 | if (! isset($message['attributes']['available_at']) || $message['attributes']['available_at'] !== (string) $delay_timestamp) {
336 | return false;
337 | }
338 |
339 | if (! isset($message['attributes']['foo']) || $message['attributes']['foo'] != $options['foo']) {
340 | return false;
341 | }
342 |
343 | return true;
344 | }),
345 | $this->callback(function ($options) {
346 | if (! is_array($options)) {
347 | return false;
348 | }
349 |
350 | foreach ($options as $key => $option) {
351 | if (! is_string($option) || ! is_string($key)) {
352 | return false;
353 | }
354 | }
355 |
356 | return true;
357 | })
358 | );
359 |
360 | $this->queue->republish($this->message, 'test', $options, $delay);
361 | }
362 |
363 | public function testRepublishOptionsOnlyAcceptString(): void
364 | {
365 | $this->expectException(\UnexpectedValueException::class);
366 |
367 | $delay = 60;
368 | $delay_timestamp = Carbon::now()->addSeconds($delay)->getTimestamp();
369 |
370 | $this->topic->method('subscription')
371 | ->willReturn($this->subscription);
372 |
373 | $this->queue->method('getTopic')
374 | ->willReturn($this->topic);
375 |
376 | $this->queue->method('availableAt')
377 | ->willReturn($delay_timestamp);
378 |
379 | $this->topic->method('publish')
380 | ->willReturn($this->expectedResult);
381 |
382 | $options = [
383 | 'integer' => 42,
384 | 'array' => [
385 | 'foo' => 'bar',
386 | ],
387 | 1 => 'wrong key',
388 | 'object' => new \stdClass,
389 | ];
390 |
391 | $this->queue->republish($this->message, 'test', $options, $delay);
392 | }
393 |
394 | public function testGetTopic(): void
395 | {
396 | $this->topic->method('exists')
397 | ->willReturn(true);
398 |
399 | $this->client->method('topic')
400 | ->willReturn($this->topic);
401 |
402 | /** @var \PHPUnit\Framework\MockObject\MockObject&PubSubQueue $queue */
403 | $queue = $this->getMockBuilder(PubSubQueue::class)
404 | ->setConstructorArgs([$this->client, 'default'])
405 | ->onlyMethods([])
406 | ->getMock();
407 |
408 | $this->assertTrue($queue->getTopic('test') instanceof Topic);
409 | }
410 |
411 | public function testCreateTopicAndReturnIt(): void
412 | {
413 | $this->topic->method('exists')
414 | ->willReturn(false);
415 |
416 | $this->topic->expects($this->once())
417 | ->method('create')
418 | ->willReturn(true);
419 |
420 | $this->client->method('topic')
421 | ->willReturn($this->topic);
422 |
423 | /** @var \PHPUnit\Framework\MockObject\MockObject&PubSubQueue $queue */
424 | $queue = $this->getMockBuilder(PubSubQueue::class)
425 | ->setConstructorArgs([$this->client, 'default'])
426 | ->onlyMethods([])
427 | ->getMock();
428 |
429 | $this->assertTrue($queue->getTopic('test', true) instanceof Topic);
430 | }
431 |
432 | public function testSubscribtionIsCreated(): void
433 | {
434 | $this->topic->method('subscription')
435 | ->willReturn($this->subscription);
436 |
437 | $this->topic->method('subscribe')
438 | ->willReturn($this->subscription);
439 |
440 | $this->subscription->method('exists')
441 | ->willReturn(false);
442 |
443 | /** @var \PHPUnit\Framework\MockObject\MockObject&PubSubQueue $queue */
444 | $queue = $this->getMockBuilder(PubSubQueue::class)
445 | ->setConstructorArgs([$this->client, 'default'])
446 | ->onlyMethods([])
447 | ->getMock();
448 |
449 | $this->assertTrue($queue->subscribeToTopic($this->topic) instanceof Subscription);
450 | }
451 |
452 | public function testSubscriptionIsRetrieved(): void
453 | {
454 | $this->topic->method('subscription')
455 | ->willReturn($this->subscription);
456 |
457 | $this->subscription->method('exists')
458 | ->willReturn(true);
459 |
460 | /** @var \PHPUnit\Framework\MockObject\MockObject&PubSubQueue $queue */
461 | $queue = $this->getMockBuilder(PubSubQueue::class)
462 | ->setConstructorArgs([$this->client, 'default'])
463 | ->onlyMethods([])
464 | ->getMock();
465 |
466 | $this->assertTrue($queue->subscribeToTopic($this->topic) instanceof Subscription);
467 | }
468 |
469 | public function testGetSubscriberName(): void
470 | {
471 | /** @var \PHPUnit\Framework\MockObject\MockObject&PubSubQueue $queue */
472 | $queue = $this->getMockBuilder(PubSubQueue::class)
473 | ->setConstructorArgs([$this->client, 'default', 'test-subscriber'])
474 | ->onlyMethods([])
475 | ->getMock();
476 |
477 | $this->assertTrue(is_string($queue->getSubscriberName()));
478 | $this->assertEquals($queue->getSubscriberName(), 'test-subscriber');
479 | }
480 |
481 | public function testGetPubSub(): void
482 | {
483 | $this->assertTrue($this->queue->getPubSub() instanceof PubSubClient);
484 | }
485 | }
486 |
--------------------------------------------------------------------------------