diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..6537ca46
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..f9ab78ed
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,14 @@
+* text=auto
+
+/.github export-ignore
+/tests export-ignore
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.styleci.yml export-ignore
+.php_cs.dist export-ignore
+CHANGELOG-* export-ignore
+CODE_OF_CONDUCT.md export-ignore
+CONTRIBUTING.md export-ignore
+phpunit.xml.dist export-ignore
+docker-compose.yml export-ignore
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..d7f499d0
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,36 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: vyuldashev
+
+---
+
+- Laravel/Lumen version: #.#.#
+- RabbitMQ version: #.#.#
+- Package version: #.#.#
+
+**Describe the bug**
+
+A clear and concise description of what the bug is.
+
+**Steps To Reproduce**
+
+What steps needed, to reproduce this bug.
+
+**Current behavior**
+
+- What happens with the worker?
+- Is the message retried or put back into the queue?
+- Is the message acknowledged or rejected?
+- Is the message unacked?
+- Is the message gone?
+
+**Expected behavior**
+
+What is the expected behavior
+
+**Additional context**
+
+Add any other context about the problem or describe the use-case.
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 00000000..4aad6d20
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,48 @@
+name: Tests
+
+on:
+ push:
+ pull_request:
+ schedule:
+ - cron: '0 0 * * *'
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: true
+ matrix:
+ php: ['8.1', '8.2', '8.3', '8.4']
+ stability: ['prefer-lowest', 'prefer-stable']
+ laravel: ['^10.0', '^11.0', '^12.0']
+ exclude:
+ - php: '8.1'
+ laravel: '^11.0'
+ - php: '8.1'
+ laravel: '^12.0'
+
+ name: 'PHP ${{ matrix.php }} - Laravel: ${{matrix.laravel}} - ${{ matrix.stability }}'
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: dom, curl, libxml, mbstring, zip
+ coverage: none
+
+ - name: Start Docker container
+ run: docker compose up -d rabbitmq
+
+ - name: Install dependencies
+ run: composer update --with='laravel/framework:${{matrix.laravel}}' --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress
+
+ - name: Run Laravel Pint
+ run: ./vendor/bin/pint --test
+
+ - name: Execute tests
+ run: sleep 10 && vendor/bin/phpunit
diff --git a/.gitignore b/.gitignore
index e22b4623..a2dcf885 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,6 @@
/vendor
composer.lock
.phpstorm.meta.php
-phpunit.xml
\ No newline at end of file
+phpunit.xml
+.phpunit.*
+.php_cs.cache
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 3cee34b0..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-dist: xenial
-
-language: php
-
-php:
- - 7.0
- - 7.1
-
-env:
- - RABBITMQ_CONFIG_FILE="/tmp/rabbitmq.config" SSL_CAFILE="/tmp/rootCA.pem
-
-services:
- - rabbitmq
-
-before_script:
- - COMPOSER_DISCARD_CHANGES=1 composer update --prefer-dist --no-interaction --no-suggest
- - sed -i 's///g' phpunit.xml.dist
- - openssl genrsa -out /tmp/rootCA.key 2048
- - openssl req -x509 -new -nodes -key /tmp/rootCA.key -sha256 -days 1024 -out /tmp/rootCA.pem -subj "/C=US/ST=Arizona/L=Scottsdale/O=Example Company Inc./CN=127.0.0.1"
- - openssl genrsa -out /tmp/device.key 2048
- - openssl req -new -key /tmp/device.key -out /tmp/device.csr -subj "/C=US/ST=Arizona/L=Scottsdale/O=Example Company Inc./CN=127.0.0.1"
- - openssl x509 -req -in /tmp/device.csr -CA /tmp/rootCA.pem -CAkey /tmp/rootCA.key -CAcreateserial -out /tmp/device.crt -days 500 -sha256
- - sudo service rabbitmq-server stop
- - echo "[{rabbit,[{ssl_listeners, [5671]},{ssl_options,[{cacertfile,\"/tmp/rootCA.pem\"},{certfile,\"/tmp/rootCA.pem\"},{keyfile,\"/tmp/rootCA.key\"},{verify,verify_none},{fail_if_no_peer_cert,false}]}]}]." > /tmp/rabbitmq.config
- - cp /tmp/rabbitmq.config /tmp/rabbitmq.config.config
- - sudo rabbitmq-server &
- - sleep 10
-
-script:
- - composer test
\ No newline at end of file
diff --git a/CHANGELOG-10x.md b/CHANGELOG-10x.md
new file mode 100644
index 00000000..a396c6d6
--- /dev/null
+++ b/CHANGELOG-10x.md
@@ -0,0 +1,72 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.2.3...v10.0)
+
+## [10.2.3 (2021-02-12)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.2.2...v10.2.3)
+
+- Fix Worker is getting killed by timeout when no more jobs available [#404](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/404)
+
+## [10.2.2 (2020-07-18)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.2.1...v10.2.2)
+
+- Fix: Make Artisan commands visible not just in console [#348](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/348)
+- Fix: Disable heartbeat when not configured [#346](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/346)
+
+## [10.2.1 (2020-05-11)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.2.0...v10.2.1)
+
+- Fix: When a job fails it tries to reject it twice [#322](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/322)
+
+## [10.2.0 (2020-04-24)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.1.3...v10.2.0)
+
+Huge thanks to [adm-bone](https://github.com/adm-bome) for this release.
+
+- Added support for Laravel 7.0 [#319](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/319)
+- Added `rabbitmq:exchange-delete` artisan command [#317](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/317)
+- Added `rabbitmq:queue-delete` artisan command [#317](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/317)
+- Failed jobs can be rerouted to another exchange [#317](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/317)
+- Exchange type is configurable [#317](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/317)
+- Job attempts are fixed [#304](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/304)
+- Added prioritization for failed jobs [#304](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/304)
+- Fixed: if delay is not set when releasing a job, job will be lost [#304](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/304)
+- Fix loosing messages when forced to close connection [#311](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/311)
+- Fixed unacked message when class not found [#314](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/314)
+
+## [10.1.3 (2020-01-12)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.1.2...v10.1.3)
+
+- Fix 100% CPU usage of `rabbitmq:consume` command by adding sleep to consumer when no messages are got from the queue.
+- Fix `stop-on-empty` flag for `rabbitmq:consume` command.
+
+## [10.1.2 (2019-12-24)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.1.1...v10.1.2)
+
+- Fix `rabbitmq:queue-bind` command. [#294](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/294)
+
+## [10.1.1 (2019-12-18)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.1.0...v10.1.1)
+
+- Fix `rabbitmq:exchange-declare` command. [#293](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/293)
+
+## [10.1.0 (2019-12-16)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.0.2...v10.1.0)
+
+- Add `rabbitmq:consume` command which uses `basic_consume` instead of `basic_get` used by `queue:work`. [#289](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/289)
+- Heartbeat disabled globally
+- Shuffle hosts before connecting to get better load balancing
+
+## [10.0.2 (2019-12-13)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.0.1...v10.0.2)
+
+- Finally fix [#235](https://github.com/vyuldashev/laravel-queue-rabbitmq/issues/235)
+
+## [10.0.1 (2019-12-13)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.0.0...v10.0.1)
+
+- Add missing container instance and connectionName to RabbitMQJob
+
+## [10.0.0 (2019-12-12)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v9.0...v10.0.0)
+
+- Switch from enqueue to [php-amqplib](https://github.com/php-amqplib/php-amqplib)
+- Fix [#235](https://github.com/vyuldashev/laravel-queue-rabbitmq/issues/235)
+- Add support for multiple hosts
+- Added `exchange:declare` artisan command
+- Added `queue:bind` artisan command
+- Added `queue:declare` artisan command
+- Added `queue:purge` artisan command
+- Bulk push messages using `batch_basic_publish`
+- No more “sleeps”. Exception will be thrown on lost connection or if any other exception occurs and process manager should be configured properly to manage such situations.
diff --git a/CHANGELOG-11x.md b/CHANGELOG-11x.md
new file mode 100644
index 00000000..cbbc691e
--- /dev/null
+++ b/CHANGELOG-11x.md
@@ -0,0 +1,47 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.4.0...master)
+
+## [11.4.0 (2021-07-27)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.3.0...v11.4.0)
+
+- Randomize consumer tag [#432](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/432)
+
+## [11.3.0 (2021-07-06)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.2.0...v11.3.0)
+
+- Quorum queues support [#359](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/359)
+- max-priority support [#422](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/422)
+- Ability to specify exchange and exchange_type when using pushRaw() [#420](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/420)
+- Remember exchanges once they have been verified [#407](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/407)
+
+## [11.2.0 (2021-03-16)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.1.2...v11.2.0)
+
+- PHP 8 support
+- Fix missing rest option in `php artisan rabbitmq:consume` command [#416](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/416)
+
+## [11.1.2 (2021-03-07)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.1.1...v11.1.2)
+
+- Update Consumer to stop when stopIfNecessary() returns exit code [#409](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/409)
+
+## [11.1.1 (2020-12-07)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.1.0...v11.1.1)
+
+- Fix worker is stopped by timeout when no new jobs available [#352](https://github.com/vyuldashev/laravel-queue-rabbitmq/issues/352)
+
+## [11.1.0 (2020-12-05)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.0.2...v11.1.0)
+
+- Custom job class [#370](https://github.com/vyuldashev/laravel-queue-rabbitmq/issues/370)
+
+## [11.0.2 (2020-09-20)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.0.1...v11.0.2)
+
+- Add missing options to rabbitmq:consume command [#363](https://github.com/vyuldashev/laravel-queue-rabbitmq/issues/363)
+
+## [11.0.1 (2020-09-19)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.0.0...v11.0.1)
+
+- Fix rabbitmq:consume name option does not exist [#363](https://github.com/vyuldashev/laravel-queue-rabbitmq/issues/363)
+- Fix Class 'Laravel\Horizon\JobId' not found [#362](https://github.com/vyuldashev/laravel-queue-rabbitmq/issues/362)
+
+## [11.0.0 (2020-09-09)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v10.2.2...v11.0.0)
+
+- Laravel 8 support
+- Minimum PHP version is set to 7.3
diff --git a/CHANGELOG-12x.md b/CHANGELOG-12x.md
new file mode 100644
index 00000000..3887c2d1
--- /dev/null
+++ b/CHANGELOG-12x.md
@@ -0,0 +1,14 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v12.0.1...master)
+
+## [12.0.1 (2022-04-06)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v12.0.0...v12.0.1)
+
+- Allow laravel to end workers with lost connection [#457](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/457)
+
+## [12.0.0 (2022-02-23)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v11.4.0...v12.0.0)
+
+- Laravel 9 support
+- Minimum PHP version is set to 8.0
diff --git a/CHANGELOG-13x.md b/CHANGELOG-13x.md
new file mode 100644
index 00000000..64e448ce
--- /dev/null
+++ b/CHANGELOG-13x.md
@@ -0,0 +1,45 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v13.3.0...master)
+
+## [13.3.1](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v13.3.0...13.3.3)
+- Fix a bug when no job / message is available on the queue initially [#543](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/543)
+
+## [13.3.0](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v13.2.0...13.3.0)
+
+- Refactor the creation of RabbitMQ Connection and
+ QueueAPI. [#528](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/528)
+- Added configuration object as single dependency for RabbitMQQueue in
+ constructor. [#528](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/528)
+- Fix method getExchangeType, not throwing an
+ exception. [#528](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/528)
+- Separating the api logic from the actual publishing to
+ RabbitMQ. [#528](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/528)
+- Added a reconnect method. [#528](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/528)
+- Fix the connection and channel not being fully lazy, when QueueAPI was
+ created. [#528](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/528)
+- Keep track of declared queue's within RabbitMQ. [#528](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/528)
+- Implemented the 'rest' option to the consumer [#530](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/530)
+- Added ability to reconnect to RabbitMQ, by creating your
+ own `RabbitMQQueue:class` [#531](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/531)
+
+## [13.2.0](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v13.1.0...13.2.0)
+
+- Compatibility with Laravel 10 [#525](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/525)
+
+## [13.1.0 (2023-01-25)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v13.0.1...v13.1.0)
+
+- Fix delay parameter not being used [#502](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/502)
+- Resolve Laravel 9 incompatabilities [#502](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/502)
+- Fix Horizon invalid delay property [#502](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/502)
+
+## [13.0.1 (2022-09-16)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v13.0.0...v13.0.1)
+
+- Add $dispatchAfterCommit when running via
+ Horizon [#484](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/484)
+
+## [13.0.0 (2022-09-15)](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v12.0.1...v13.0.0)
+
+- Dispatch a job after DB transaction commit [#468](https://github.com/vyuldashev/laravel-queue-rabbitmq/pull/468)
diff --git a/CHANGELOG-14x.md b/CHANGELOG-14x.md
new file mode 100644
index 00000000..c94b3dd2
--- /dev/null
+++ b/CHANGELOG-14x.md
@@ -0,0 +1,8 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [unreleased](https://github.com/vyuldashev/laravel-queue-rabbitmq/compare/v13.3.0...master)
+
+## [14.0.0]
+- First release compatible with Laravel 11
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 00000000..07fbf64c
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) Vladimir Yuldashev
+
+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:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+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.
diff --git a/README.md b/README.md
index 00496e1a..30a10df9 100644
--- a/README.md
+++ b/README.md
@@ -1,64 +1,618 @@
RabbitMQ Queue driver for Laravel
======================
[](https://packagist.org/packages/vladimir-yuldashev/laravel-queue-rabbitmq)
-[](https://travis-ci.org/vyuldashev/laravel-queue-rabbitmq)
+[](https://github.com/vyuldashev/laravel-queue-rabbitmq/actions/workflows/tests.yml)
[](https://packagist.org/packages/vladimir-yuldashev/laravel-queue-rabbitmq)
-[](https://styleci.io/repos/14976752)
[](https://packagist.org/packages/vladimir-yuldashev/laravel-queue-rabbitmq)
-#### Installation
+## Support Policy
-1. Install this package via composer using:
+Only the latest version will get new features. Bug fixes will be provided using the following scheme:
+| Package Version | Laravel Version | Bug Fixes Until | |
+|-----------------|-----------------|------------------|---------------------------------------------------------------------------------------------|
+| 13 | 9 | August 8th, 2023 | [Documentation](https://github.com/vyuldashev/laravel-queue-rabbitmq/blob/master/README.md) |
+
+## Installation
+
+You can install this package via composer using this command:
+
+```
+composer require vladimir-yuldashev/laravel-queue-rabbitmq
+```
+
+The package will automatically register itself.
+
+### Configuration
+
+Add connection to `config/queue.php`:
+
+> This is the minimal config for the rabbitMQ connection/driver to work.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+
+ 'driver' => 'rabbitmq',
+ 'hosts' => [
+ [
+ 'host' => env('RABBITMQ_HOST', '127.0.0.1'),
+ 'port' => env('RABBITMQ_PORT', 5672),
+ 'user' => env('RABBITMQ_USER', 'guest'),
+ 'password' => env('RABBITMQ_PASSWORD', 'guest'),
+ 'vhost' => env('RABBITMQ_VHOST', '/'),
+ ],
+ // ...
+ ],
+
+ // ...
+ ],
+
+ // ...
+],
+```
+
+### Optional Queue Config
+
+Optionally add queue options to the config of a connection.
+Every queue created for this connection, gets the properties.
+
+When you want to prioritize messages when they were delayed, then this is possible by adding extra options.
+
+- When max-priority is omitted, the max priority is set with 2 when used.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'options' => [
+ 'queue' => [
+ // ...
+
+ 'prioritize_delayed' => false,
+ 'queue_max_priority' => 10,
+ ],
+ ],
+ ],
+
+ // ...
+],
+```
+
+When you want to publish messages against an exchange with routing-keys, then this is possible by adding extra options.
+
+- When the exchange is omitted, RabbitMQ will use the `amq.direct` exchange for the routing-key
+- When routing-key is omitted the routing-key by default is the `queue` name.
+- When using `%s` in the routing-key the queue_name will be substituted.
+
+> Note: when using an exchange with routing-key, you probably create your queues with bindings yourself.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'options' => [
+ 'queue' => [
+ // ...
+
+ 'exchange' => 'application-x',
+ 'exchange_type' => 'topic',
+ 'exchange_routing_key' => '',
+ ],
+ ],
+ ],
+
+ // ...
+],
+```
+
+In Laravel failed jobs are stored into the database. But maybe you want to instruct some other process to also do
+something with the message.
+When you want to instruct RabbitMQ to reroute failed messages to a exchange or a specific queue, then this is possible
+by adding extra options.
+
+- When the exchange is omitted, RabbitMQ will use the `amq.direct` exchange for the routing-key
+- When routing-key is omitted, the routing-key by default the `queue` name is substituted with `'.failed'`.
+- When using `%s` in the routing-key the queue_name will be substituted.
+
+> Note: When using failed_job exchange with routing-key, you probably need to create your exchange/queue with bindings
+> yourself.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'options' => [
+ 'queue' => [
+ // ...
+
+ 'reroute_failed' => true,
+ 'failed_exchange' => 'failed-exchange',
+ 'failed_routing_key' => 'application-x.%s',
+ ],
+ ],
+ ],
+
+ // ...
+],
+```
+
+### Horizon support
+
+Starting with 8.0, this package supports [Laravel Horizon](https://laravel.com/docs/horizon) out of the box. Firstly,
+install Horizon and then set `RABBITMQ_WORKER` to `horizon`.
+
+Horizon is depending on events dispatched by the worker.
+These events inform Horizon what was done with the message/job.
+
+This Library supports Horizon, but in the config you have to inform Laravel to use the QueueApi compatible with horizon.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ /* Set to "horizon" if you wish to use Laravel Horizon. */
+ 'worker' => env('RABBITMQ_WORKER', 'default'),
+ ],
+
+ // ...
+],
+```
+
+### Use your own RabbitMQJob class
+
+Sometimes you have to work with messages published by another application.
+Those messages probably won't respect Laravel's job payload schema.
+The problem with these messages is that, Laravel workers won't be able to determine the actual job or class to execute.
+
+You can extend the build-in `RabbitMQJob::class` and within the queue connection config, you can define your own class.
+When you specify a `job` key in the config, with your own class name, every message retrieved from the broker will get
+wrapped by your own class.
+
+An example for the config:
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'options' => [
+ 'queue' => [
+ // ...
+
+ 'job' => \App\Queue\Jobs\RabbitMQJob::class,
+ ],
+ ],
+ ],
+
+ // ...
+],
+```
+
+An example of your own job class:
+
+```php
+payload();
+
+ $class = WhatheverClassNameToExecute::class;
+ $method = 'handle';
+
+ ($this->instance = $this->resolve($class))->{$method}($this, $payload);
+
+ $this->delete();
+ }
+}
+
+```
+
+Or maybe you want to add extra properties to the payload:
+
+```php
+ 'WhatheverFullyQualifiedClassNameToExecute@handle',
+ 'data' => json_decode($this->getRawBody(), true)
+ ];
+ }
+}
+```
+
+If you want to handle raw message, not in JSON format or without 'job' key in JSON,
+you should add stub for `getName` method:
+
+```php
+getRawBody();
+ Log::info($anyMessage);
+
+ $this->delete();
+ }
+
+ public function getName()
+ {
+ return '';
+ }
+}
+```
+
+### Use your own Connection
+
+You can extend the built-in `PhpAmqpLib\Connection\AMQPStreamConnection::class`
+or `PhpAmqpLib\Connection\AMQPSLLConnection::class` and within the connection config, you can define your own class.
+When you specify a `connection` key in the config, with your own class name, every connection will use your own class.
+
+An example for the config:
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'connection' = > \App\Queue\Connection\MyRabbitMQConnection::class,
+ ],
+
+ // ...
+],
+```
+
+### Use your own Worker class
+
+If you want to use your own `RabbitMQQueue::class` this is possible by
+extending `VladimirYuldashev\LaravelQueueRabbitMQ\Queue\RabbitMQQueue`.
+and inform laravel to use your class by setting `RABBITMQ_WORKER` to `\App\Queue\RabbitMQQueue::class`.
+
+> Note: Worker classes **must** extend `VladimirYuldashev\LaravelQueueRabbitMQ\Queue\RabbitMQQueue`
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ /* Set to a class if you wish to use your own. */
+ 'worker' => \App\Queue\RabbitMQQueue::class,
+ ],
+
+ // ...
+],
+```
+
+```php
+ Note: this is not best practice, it is an example.
+
+```php
+reconnect();
+ parent::publishBasic($msg, $exchange, $destination, $mandatory, $immediate, $ticket);
+ }
+ }
+
+ protected function publishBatch($jobs, $data = '', $queue = null): void
+ {
+ try {
+ parent::publishBatch($jobs, $data, $queue);
+ } catch (AMQPConnectionClosedException|AMQPChannelClosedException) {
+ $this->reconnect();
+ parent::publishBatch($jobs, $data, $queue);
+ }
+ }
+
+ protected function createChannel(): AMQPChannel
+ {
+ try {
+ return parent::createChannel();
+ } catch (AMQPConnectionClosedException) {
+ $this->reconnect();
+ return parent::createChannel();
+ }
+ }
+}
+```
+
+### Default Queue
+
+The connection does use a default queue with value 'default', when no queue is provided by laravel.
+It is possible to change te default queue by adding an extra parameter in the connection config.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'queue' => env('RABBITMQ_QUEUE', 'default'),
+ ],
+
+ // ...
+],
+```
+
+### Heartbeat
+
+By default, your connection will be created with a heartbeat setting of `0`.
+You can alter the heartbeat settings by changing the config.
+
+```php
+
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'options' => [
+ // ...
+
+ 'heartbeat' => 10,
+ ],
+ ],
+
+ // ...
+],
```
-composer require vladimir-yuldashev/laravel-queue-rabbitmq:5.5
+
+### SSL Secure
+
+If you need a secure connection to rabbitMQ server(s), you will need to add these extra config options.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'secure' = > true,
+ 'options' => [
+ // ...
+
+ 'ssl_options' => [
+ 'cafile' => env('RABBITMQ_SSL_CAFILE', null),
+ 'local_cert' => env('RABBITMQ_SSL_LOCALCERT', null),
+ 'local_key' => env('RABBITMQ_SSL_LOCALKEY', null),
+ 'verify_peer' => env('RABBITMQ_SSL_VERIFY_PEER', true),
+ 'passphrase' => env('RABBITMQ_SSL_PASSPHRASE', null),
+ ],
+ ],
+ ],
+
+ // ...
+],
```
-2. Add these properties to `.env` with proper values:
+### Events after Database commits
+
+To instruct Laravel workers to dispatch events after all database commits are completed.
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'after_commit' => true,
+ ],
+
+ // ...
+],
```
-QUEUE_DRIVER=rabbitmq
-RABBITMQ_HOST=127.0.0.1
-RABBITMQ_PORT=5672
-RABBITMQ_VHOST=/
-RABBITMQ_LOGIN=guest
-RABBITMQ_PASSWORD=guest
-RABBITMQ_QUEUE=queue_name
+### Lazy Connection
+
+By default, your connection will be created as a lazy connection.
+If for some reason you don't want the connection lazy you can turn it off by setting the following config.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'lazy' = > false,
+ ],
+
+ // ...
+],
```
-3. Optionally: if you want to to use an SSL connection, add these properties to the `.env` with proper values:
+### Network Protocol
+
+By default, the network protocol used for connection is tcp.
+If for some reason you want to use another network protocol, you can add the extra value in your config options.
+Available protocols : `tcp`, `ssl`, `tls`
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'network_protocol' => 'tcp',
+ ],
+
+ // ...
+],
```
-RABBITMQ_SSL=true
-RABBITMQ_SSL_CAFILE=/path_to_your_ca_file
-RABBITMQ_SSL_LOCALCERT=
-RABBITMQ_SSL_PASSPHRASE=
-RABBITMQ_SSL_KEY=
+
+### Network Timeouts
+
+For network timeouts configuration you can use option parameters.
+All float values are in seconds and zero value can mean infinite timeout.
+Example contains default values.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'options' => [
+ // ...
+
+ 'connection_timeout' => 3.0,
+ 'read_timeout' => 3.0,
+ 'write_timeout' => 3.0,
+ 'channel_rpc_timeout' => 0.0,
+ ],
+ ],
+
+ // ...
+],
```
-Using an SSL connection will also require to configure your RabbitMQ to enable SSL. More details can be founds here: https://www.rabbitmq.com/ssl.html
+### Octane support
-#### Usage
+Starting with 13.3.0, this package supports [Laravel Octane](https://laravel.com/docs/octane) out of the box.
+Firstly, install Octane and don't forget to warm 'rabbitmq' connection in the octane config.
+> See: https://github.com/vyuldashev/laravel-queue-rabbitmq/issues/460#issuecomment-1469851667
-Once you completed the configuration you can use Laravel Queue API. If you used other queue drivers you do not need to change anything else. If you do not know how to use Queue API, please refer to the official Laravel documentation: http://laravel.com/docs/queues
+## Laravel Usage
-#### Testing
+Once you completed the configuration you can use the Laravel Queue API. If you used other queue drivers you do not
+need to change anything else. If you do not know how to use the Queue API, please refer to the official Laravel
+documentation: http://laravel.com/docs/queues
-Run the tests with:
+## Lumen Usage
-``` bash
-vendor/bin/phpunit
+For Lumen usage the service provider should be registered manually as follow in `bootstrap/app.php`:
+
+```php
+$app->register(VladimirYuldashev\LaravelQueueRabbitMQ\LaravelQueueRabbitMQServiceProvider::class);
```
+## Consuming Messages
+
+There are two ways of consuming messages.
-#### Contribution
+1. `queue:work` command which is Laravel's built-in command. This command utilizes `basic_get`. Use this if you want to consume multiple queues.
-You can contribute to this package by discovering bugs and opening issues. Please, add to which version of package you create pull request or issue. (e.g. [5.2] Fatal error on delayed job)
+2. `rabbitmq:consume` command which is provided by this package. This command utilizes `basic_consume` and is more performant than `basic_get` by ~2x, but does not support multiple queues.
-> If you want to make feature for several versions (for example: 5.2, 5.3, 5.4 and 5.5). Create PR for the lowest version (5.2). Hence, you should use branch v5.2.
+## Testing
-#### Supported versions of Laravel (+Lumen)
+Setup RabbitMQ using `docker-compose`:
+
+```bash
+docker compose up -d
+```
+
+To run the test suite you can use the following commands:
+
+```bash
+# To run both style and unit tests.
+composer test
+
+# To run only style tests.
+composer test:style
+
+# To run only unit tests.
+composer test:unit
+```
+
+If you receive any errors from the style tests, you can automatically fix most,
+if not all the issues with the following command:
+
+```bash
+composer fix:style
+```
-`4.0, 4.1, 4.2, 5.0, 5.1, 5.2, 5.3, 5.4, 5.5`
+## Contribution
-The version is being matched by the release tag of this library.
+You can contribute to this package by discovering bugs and opening issues. Please, add to which version of package you
+create pull request or issue. (e.g. [5.2] Fatal error on delayed job)
diff --git a/composer.json b/composer.json
index a105dc3c..caf8a0fc 100644
--- a/composer.json
+++ b/composer.json
@@ -1,39 +1,59 @@
{
- "name": "vladimir-yuldashev/laravel-queue-rabbitmq",
- "description": "RabbitMQ driver for Laravel Queue",
- "license": "MIT",
- "authors": [
- {
- "name": "Vladimir Yuldashev",
- "email": "misterio92@gmail.com"
- }
- ],
- "require": {
- "php": ">=7.0",
- "illuminate/database": "5.5.*",
- "illuminate/support": "5.5.*",
- "illuminate/queue": "5.5.*",
- "php-amqplib/php-amqplib": "2.6.*"
- },
- "require-dev": {
- "phpunit/phpunit": "~6.0",
- "mockery/mockery": "^0.9.5"
- },
- "autoload": {
- "psr-4": {
- "VladimirYuldashev\\LaravelQueueRabbitMQ\\": "src/"
- }
- },
- "extra": {
- "laravel": {
- "providers": [
- "VladimirYuldashev\\LaravelQueueRabbitMQ\\LaravelQueueRabbitMQServiceProvider"
- ]
- }
- },
- "scripts": {
- "test": "vendor/bin/phpunit"
- },
- "minimum-stability": "dev",
- "prefer-stable": true
+ "name": "vladimir-yuldashev/laravel-queue-rabbitmq",
+ "description": "RabbitMQ driver for Laravel Queue. Supports Laravel Horizon.",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Vladimir Yuldashev",
+ "email": "misterio92@gmail.com"
+ }
+ ],
+ "require": {
+ "php": "^8.0",
+ "ext-json": "*",
+ "illuminate/queue": "^10.0|^11.0|^12.0",
+ "php-amqplib/php-amqplib": "^v3.6"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.0|^11.0",
+ "mockery/mockery": "^1.0",
+ "laravel/horizon": "^5.0",
+ "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
+ "laravel/pint": "^1.2",
+ "laravel/framework": "^9.0|^10.0|^11.0|^12.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "VladimirYuldashev\\LaravelQueueRabbitMQ\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "VladimirYuldashev\\LaravelQueueRabbitMQ\\Tests\\": "tests/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "13.0-dev"
+ },
+ "laravel": {
+ "providers": [
+ "VladimirYuldashev\\LaravelQueueRabbitMQ\\LaravelQueueRabbitMQServiceProvider"
+ ]
+ }
+ },
+ "suggest": {
+ "ext-pcntl": "Required to use all features of the queue consumer."
+ },
+ "scripts": {
+ "test": [
+ "@test:style",
+ "@test:unit"
+ ],
+ "test:style": "@php vendor/bin/pint --test -v",
+ "test:unit": "@php vendor/bin/phpunit",
+ "fix:style": "@php vendor/bin/pint -v"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
}
diff --git a/config/rabbitmq.php b/config/rabbitmq.php
index 1c4f0a44..4c102ce8 100644
--- a/config/rabbitmq.php
+++ b/config/rabbitmq.php
@@ -3,67 +3,30 @@
/**
* This is an example of queue connection configuration.
* It will be merged into config/queue.php.
- * You need to set proper values in `.env`
+ * You need to set proper values in `.env`.
*/
return [
'driver' => 'rabbitmq',
-
- 'host' => env('RABBITMQ_HOST', '127.0.0.1'),
- 'port' => env('RABBITMQ_PORT', 5672),
-
- 'vhost' => env('RABBITMQ_VHOST', '/'),
- 'login' => env('RABBITMQ_LOGIN', 'guest'),
- 'password' => env('RABBITMQ_PASSWORD', 'guest'),
-
- /*
- * The name of default queue.
- */
- 'queue' => env('RABBITMQ_QUEUE'),
-
- /*
- * Determine if exchange should be created if it does not exist.
- */
- 'exchange_declare' => env('RABBITMQ_EXCHANGE_DECLARE', true),
-
- /*
- * Determine if queue should be created and binded to the exchange if it does not exist.
- */
- 'queue_declare_bind' => env('RABBITMQ_QUEUE_DECLARE_BIND', true),
-
- /*
- * Read more about possible values at https://www.rabbitmq.com/tutorials/amqp-concepts.html
- */
- 'queue_params' => [
- 'passive' => env('RABBITMQ_QUEUE_PASSIVE', false),
- 'durable' => env('RABBITMQ_QUEUE_DURABLE', true),
- 'exclusive' => env('RABBITMQ_QUEUE_EXCLUSIVE', false),
- 'auto_delete' => env('RABBITMQ_QUEUE_AUTODELETE', false),
- 'arguments' => env('RABBITMQ_QUEUE_ARGUMENTS'),
- ],
- 'exchange_params' => [
- 'name' => env('RABBITMQ_EXCHANGE_NAME'),
- 'type' => env('RABBITMQ_EXCHANGE_TYPE', 'direct'),
- 'passive' => env('RABBITMQ_EXCHANGE_PASSIVE', false),
- 'durable' => env('RABBITMQ_EXCHANGE_DURABLE', true),
- 'auto_delete' => env('RABBITMQ_EXCHANGE_AUTODELETE', false),
+ 'queue' => env('RABBITMQ_QUEUE', 'default'),
+ 'connection' => 'default',
+
+ 'hosts' => [
+ [
+ 'host' => env('RABBITMQ_HOST', '127.0.0.1'),
+ 'port' => env('RABBITMQ_PORT', 5672),
+ 'user' => env('RABBITMQ_USER', 'guest'),
+ 'password' => env('RABBITMQ_PASSWORD', 'guest'),
+ 'vhost' => env('RABBITMQ_VHOST', '/'),
+ ],
],
- /*
- * Determine the number of seconds to sleep if there's an error communicating with rabbitmq
- * If set to false, it'll throw an exception rather than doing the sleep for X seconds.
- */
- 'sleep_on_error' => env('RABBITMQ_ERROR_SLEEP', 5),
+ 'options' => [
+ ],
/*
- * Optional SSL params if an SSL connection is used
+ * Set to "horizon" if you wish to use Laravel Horizon.
*/
- 'ssl_params' => [
- 'ssl_on' => env('RABBITMQ_SSL', false),
- 'cafile' => env('RABBITMQ_SSL_CAFILE', null),
- 'local_cert' => env('RABBITMQ_SSL_LOCALCERT', null),
- 'verify_peer' => env('RABBITMQ_SSL_VERIFY_PEER', true),
- 'passphrase' => env('RABBITMQ_SSL_PASSPHRASE', null),
- ]
+ 'worker' => env('RABBITMQ_WORKER', 'default'),
];
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..4ac32caa
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,43 @@
+version: '3.7'
+
+services:
+
+ rabbitmq:
+ image: rabbitmq:3.8
+ environment:
+ RABBITMQ_DEFAULT_USER: guest
+ RABBITMQ_DEFAULT_PASSWORD: guest
+ RABBITMQ_DEFAULT_VHOST: /
+ RABBITMQ_SSL_CACERTFILE: /rootCA.pem
+ RABBITMQ_SSL_CERTFILE: /rootCA.pem
+ RABBITMQ_SSL_KEYFILE: /rootCA.key
+ RABBITMQ_SSL_VERIFY: verify_none
+ RABBITMQ_SSL_FAIL_IF_NO_PEER_CERT: "false"
+ volumes:
+ - "./tests/files/rootCA.pem:/rootCA.pem:ro"
+ - "./tests/files/rootCA.key:/rootCA.key:ro"
+ ports:
+ - "15671:15671"
+ - "15672:15672"
+ - "5671:5671"
+ - "5672:5672"
+
+ rabbitmq-management:
+ image: rabbitmq:management
+ environment:
+ RABBITMQ_DEFAULT_USER: guest
+ RABBITMQ_DEFAULT_PASSWORD: guest
+ RABBITMQ_DEFAULT_VHOST: /
+ RABBITMQ_MANAGEMENT_SSL_CACERTFILE: /rootCA.pem
+ RABBITMQ_MANAGEMENT_SSL_CERTFILE: /rootCA.pem
+ RABBITMQ_MANAGEMENT_SSL_KEYFILE: /rootCA.key
+ RABBITMQ_MANAGEMENT_SSL_VERIFY: verify_none
+ RABBITMQ_MANAGEMENT_SSL_FAIL_IF_NO_PEER_CERT: "false"
+ volumes:
+ - "./tests/files/rootCA.pem:/rootCA.pem:ro"
+ - "./tests/files/rootCA.key:/rootCA.key:ro"
+ ports:
+ - 15671:15671
+ - 15672:15672
+ - 5671:5671
+ - 5672:5672
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index ed6096f3..d213fd63 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,35 +1,16 @@
-
-
-
- ./tests/
-
-
-
-
-
-
-
-
-
-
- src/
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+ ./tests/
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pint.json b/pint.json
new file mode 100644
index 00000000..05f4b41e
--- /dev/null
+++ b/pint.json
@@ -0,0 +1,8 @@
+{
+ "preset": "laravel",
+ "rules": {
+ "php_unit_method_casing": {
+ "case": "camel_case"
+ }
+ }
+}
diff --git a/src/Console/ConsumeCommand.php b/src/Console/ConsumeCommand.php
new file mode 100644
index 00000000..4072132a
--- /dev/null
+++ b/src/Console/ConsumeCommand.php
@@ -0,0 +1,66 @@
+worker;
+
+ $consumer->setContainer($this->laravel);
+ $consumer->setName($this->option('name'));
+ $consumer->setConsumerTag($this->consumerTag());
+ $consumer->setMaxPriority((int) $this->option('max-priority'));
+ $consumer->setPrefetchSize((int) $this->option('prefetch-size'));
+ $consumer->setPrefetchCount((int) $this->option('prefetch-count'));
+
+ parent::handle();
+ }
+
+ protected function consumerTag(): string
+ {
+ if ($consumerTag = $this->option('consumer-tag')) {
+ return $consumerTag;
+ }
+
+ $consumerTag = implode('_', [
+ Str::slug(config('app.name', 'laravel')),
+ Str::slug($this->option('name')),
+ md5(serialize($this->options()).Str::random(16).getmypid()),
+ ]);
+
+ return Str::substr($consumerTag, 0, 255);
+ }
+}
diff --git a/src/Console/ExchangeDeclareCommand.php b/src/Console/ExchangeDeclareCommand.php
new file mode 100644
index 00000000..43aa9c5c
--- /dev/null
+++ b/src/Console/ExchangeDeclareCommand.php
@@ -0,0 +1,44 @@
+laravel['config']->get('queue.connections.'.$this->argument('connection'));
+
+ $queue = $connector->connect($config);
+
+ if ($queue->isExchangeExists($this->argument('name'))) {
+ $this->warn('Exchange already exists.');
+
+ return;
+ }
+
+ $queue->declareExchange(
+ $this->argument('name'),
+ $this->option('type'),
+ (bool) $this->option('durable'),
+ (bool) $this->option('auto-delete')
+ );
+
+ $this->info('Exchange declared successfully.');
+ }
+}
diff --git a/src/Console/ExchangeDeleteCommand.php b/src/Console/ExchangeDeleteCommand.php
new file mode 100644
index 00000000..d5a8d8d4
--- /dev/null
+++ b/src/Console/ExchangeDeleteCommand.php
@@ -0,0 +1,40 @@
+laravel['config']->get('queue.connections.'.$this->argument('connection'));
+
+ $queue = $connector->connect($config);
+
+ if (! $queue->isExchangeExists($this->argument('name'))) {
+ $this->warn('Exchange does not exist.');
+
+ return;
+ }
+
+ $queue->deleteExchange(
+ $this->argument('name'),
+ (bool) $this->option('unused')
+ );
+
+ $this->info('Exchange deleted successfully.');
+ }
+}
diff --git a/src/Console/QueueBindCommand.php b/src/Console/QueueBindCommand.php
new file mode 100644
index 00000000..ccbbd706
--- /dev/null
+++ b/src/Console/QueueBindCommand.php
@@ -0,0 +1,36 @@
+laravel['config']->get('queue.connections.'.$this->argument('connection'));
+
+ $queue = $connector->connect($config);
+
+ $queue->bindQueue(
+ $this->argument('queue'),
+ $this->argument('exchange'),
+ (string) $this->option('routing-key')
+ );
+
+ $this->info('Queue bound to exchange successfully.');
+ }
+}
diff --git a/src/Console/QueueDeclareCommand.php b/src/Console/QueueDeclareCommand.php
new file mode 100644
index 00000000..54d6ea32
--- /dev/null
+++ b/src/Console/QueueDeclareCommand.php
@@ -0,0 +1,56 @@
+laravel['config']->get('queue.connections.'.$this->argument('connection'));
+
+ $queue = $connector->connect($config);
+
+ if ($queue->isQueueExists($this->argument('name'))) {
+ $this->warn('Queue already exists.');
+
+ return;
+ }
+
+ $arguments = [];
+
+ $maxPriority = (int) $this->option('max-priority');
+ if ($maxPriority) {
+ $arguments['x-max-priority'] = $maxPriority;
+ }
+
+ if ($this->option('quorum')) {
+ $arguments['x-queue-type'] = 'quorum';
+ }
+
+ $queue->declareQueue(
+ $this->argument('name'),
+ (bool) $this->option('durable'),
+ (bool) $this->option('auto-delete'),
+ $arguments
+ );
+
+ $this->info('Queue declared successfully.');
+ }
+}
diff --git a/src/Console/QueueDeleteCommand.php b/src/Console/QueueDeleteCommand.php
new file mode 100644
index 00000000..b2586ecd
--- /dev/null
+++ b/src/Console/QueueDeleteCommand.php
@@ -0,0 +1,42 @@
+laravel['config']->get('queue.connections.'.$this->argument('connection'));
+
+ $queue = $connector->connect($config);
+
+ if (! $queue->isQueueExists($this->argument('name'))) {
+ $this->warn('Queue does not exist.');
+
+ return;
+ }
+
+ $queue->deleteQueue(
+ $this->argument('name'),
+ (bool) $this->option('unused'),
+ (bool) $this->option('empty')
+ );
+
+ $this->info('Queue deleted successfully.');
+ }
+}
diff --git a/src/Console/QueuePurgeCommand.php b/src/Console/QueuePurgeCommand.php
new file mode 100644
index 00000000..49be839b
--- /dev/null
+++ b/src/Console/QueuePurgeCommand.php
@@ -0,0 +1,38 @@
+confirmToProceed()) {
+ return;
+ }
+
+ $config = $this->laravel['config']->get('queue.connections.'.$this->argument('connection'));
+
+ $queue = $connector->connect($config);
+
+ $queue->purge($this->argument('queue'));
+
+ $this->info('Queue purged successfully.');
+ }
+}
diff --git a/src/Consumer.php b/src/Consumer.php
new file mode 100644
index 00000000..ed3d8099
--- /dev/null
+++ b/src/Consumer.php
@@ -0,0 +1,210 @@
+container = $value;
+ }
+
+ public function setConsumerTag(string $value): void
+ {
+ $this->consumerTag = $value;
+ }
+
+ public function setMaxPriority(int $value): void
+ {
+ $this->maxPriority = $value;
+ }
+
+ public function setPrefetchSize(int $value): void
+ {
+ $this->prefetchSize = $value;
+ }
+
+ public function setPrefetchCount(int $value): void
+ {
+ $this->prefetchCount = $value;
+ }
+
+ /**
+ * Listen to the given queue in a loop.
+ *
+ * @param string $connectionName
+ * @param string $queue
+ * @return int
+ *
+ * @throws Throwable
+ */
+ public function daemon($connectionName, $queue, WorkerOptions $options)
+ {
+ if ($this->supportsAsyncSignals()) {
+ $this->listenForSignals();
+ }
+
+ $lastRestart = $this->getTimestampOfLastQueueRestart();
+
+ [$startTime, $jobsProcessed] = [hrtime(true) / 1e9, 0];
+
+ /** @var RabbitMQQueue $connection */
+ $connection = $this->manager->connection($connectionName);
+
+ $this->channel = $connection->getChannel();
+
+ $this->channel->basic_qos(
+ $this->prefetchSize,
+ $this->prefetchCount,
+ false
+ );
+
+ $jobClass = $connection->getJobClass();
+ $arguments = [];
+ if ($this->maxPriority) {
+ $arguments['priority'] = ['I', $this->maxPriority];
+ }
+
+ $this->channel->basic_consume(
+ $queue,
+ $this->consumerTag,
+ false,
+ false,
+ false,
+ false,
+ function (AMQPMessage $message) use ($connection, $options, $connectionName, $queue, $jobClass, &$jobsProcessed): void {
+ $job = new $jobClass(
+ $this->container,
+ $connection,
+ $message,
+ $connectionName,
+ $queue
+ );
+
+ $this->currentJob = $job;
+
+ if ($this->supportsAsyncSignals()) {
+ $this->registerTimeoutHandler($job, $options);
+ }
+
+ $jobsProcessed++;
+
+ $this->runJob($job, $connectionName, $options);
+
+ if ($this->supportsAsyncSignals()) {
+ $this->resetTimeoutHandler();
+ }
+
+ if ($options->rest > 0) {
+ $this->sleep($options->rest);
+ }
+ },
+ null,
+ $arguments
+ );
+
+ while ($this->channel->is_consuming()) {
+ // Before reserving any jobs, we will make sure this queue is not paused and
+ // if it is we will just pause this worker for a given amount of time and
+ // make sure we do not need to kill this worker process off completely.
+ if (! $this->daemonShouldRun($options, $connectionName, $queue)) {
+ $this->pauseWorker($options, $lastRestart);
+
+ continue;
+ }
+
+ // If the daemon should run (not in maintenance mode, etc.), then we can wait for a job.
+ try {
+ $this->channel->wait(null, true, (int) $options->timeout);
+ } catch (AMQPRuntimeException $exception) {
+ $this->exceptions->report($exception);
+
+ $this->kill(self::EXIT_ERROR, $options);
+ } catch (Exception|Throwable $exception) {
+ $this->exceptions->report($exception);
+
+ $this->stopWorkerIfLostConnection($exception);
+ }
+
+ // If no job is got off the queue, we will need to sleep the worker.
+ if ($this->currentJob === null) {
+ $this->sleep($options->sleep);
+ }
+
+ // Finally, we will check to see if we have exceeded our memory limits or if
+ // the queue should restart based on other indications. If so, we'll stop
+ // this worker and let whatever is "monitoring" it restart the process.
+ $status = $this->stopIfNecessary(
+ $options,
+ $lastRestart,
+ $startTime,
+ $jobsProcessed,
+ $this->currentJob
+ );
+
+ if (! is_null($status)) {
+ return $this->stop($status, $options);
+ }
+
+ $this->currentJob = null;
+ }
+ }
+
+ /**
+ * Determine if the daemon should process on this iteration.
+ *
+ * @param string $connectionName
+ * @param string $queue
+ */
+ protected function daemonShouldRun(WorkerOptions $options, $connectionName, $queue): bool
+ {
+ return ! ((($this->isDownForMaintenance)() && ! $options->force) || $this->paused);
+ }
+
+ /**
+ * Stop listening and bail out of the script.
+ *
+ * @param int $status
+ * @param WorkerOptions|null $options
+ * @return int
+ */
+ public function stop($status = 0, $options = null)
+ {
+ // Tell the server you are going to stop consuming.
+ // It will finish up the last message and not send you any more.
+ $this->channel->basic_cancel($this->consumerTag, false, true);
+
+ return parent::stop($status, $options);
+ }
+}
diff --git a/src/Contracts/RabbitMQQueueContract.php b/src/Contracts/RabbitMQQueueContract.php
new file mode 100644
index 00000000..cf04a66e
--- /dev/null
+++ b/src/Contracts/RabbitMQQueueContract.php
@@ -0,0 +1,14 @@
+events = $events;
+ }
+
+ /**
+ * Handle the event.
+ */
+ public function handle(LaravelJobFailed $event): void
+ {
+ if (! $event->job instanceof RabbitMQJob) {
+ return;
+ }
+
+ $this->events->dispatch((new HorizonJobFailed(
+ $event->exception,
+ $event->job,
+ $event->job->getRawBody()
+ ))->connection($event->connectionName)->queue($event->job->getQueue()));
+ }
+}
diff --git a/src/Horizon/RabbitMQQueue.php b/src/Horizon/RabbitMQQueue.php
new file mode 100644
index 00000000..e2ca400d
--- /dev/null
+++ b/src/Horizon/RabbitMQQueue.php
@@ -0,0 +1,121 @@
+size($queue);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function push($job, $data = '', $queue = null)
+ {
+ $this->lastPushed = $job;
+
+ return parent::push($job, $data, $queue);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws BindingResolutionException
+ */
+ public function pushRaw($payload, $queue = null, array $options = []): int|string|null
+ {
+ $payload = (new JobPayload($payload))->prepare($this->lastPushed ?? null)->value;
+
+ return tap(parent::pushRaw($payload, $queue, $options), function () use ($queue, $payload): void {
+ $this->event($this->getQueue($queue), new JobPushed($payload));
+ });
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws BindingResolutionException
+ */
+ public function later($delay, $job, $data = '', $queue = null): mixed
+ {
+ $payload = (new JobPayload($this->createPayload($job, $data)))->prepare($job)->value;
+
+ return tap(parent::laterRaw($delay, $payload, $queue), function () use ($payload, $queue): void {
+ $this->event($this->getQueue($queue), new JobPushed($payload));
+ });
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function pop($queue = null)
+ {
+ return tap(parent::pop($queue), function ($result) use ($queue): void {
+ if (is_a($result, RabbitMQJob::class, true)) {
+ $this->event($this->getQueue($queue), new JobReserved($result->getRawBody()));
+ }
+ });
+ }
+
+ /**
+ * Fire the job deleted event.
+ *
+ * @param string $queue
+ * @param RabbitMQJob $job
+ *
+ * @throws BindingResolutionException
+ */
+ public function deleteReserved($queue, $job): void
+ {
+ $this->event($this->getQueue($queue), new JobDeleted($job, $job->getRawBody()));
+ }
+
+ /**
+ * Fire the given event if a dispatcher is bound.
+ *
+ * @param string $queue
+ * @param mixed $event
+ *
+ * @throws BindingResolutionException
+ */
+ protected function event($queue, $event): void
+ {
+ if ($this->container && $this->container->bound(Dispatcher::class)) {
+ $this->container->make(Dispatcher::class)->dispatch(
+ $event->connection($this->getConnectionName())->queue($queue)
+ );
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getRandomId(): string
+ {
+ return Str::uuid();
+ }
+}
diff --git a/src/LaravelQueueRabbitMQServiceProvider.php b/src/LaravelQueueRabbitMQServiceProvider.php
index e1143e3c..ee46d6cd 100644
--- a/src/LaravelQueueRabbitMQServiceProvider.php
+++ b/src/LaravelQueueRabbitMQServiceProvider.php
@@ -2,50 +2,70 @@
namespace VladimirYuldashev\LaravelQueueRabbitMQ;
+use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Queue\QueueManager;
use Illuminate\Support\ServiceProvider;
-use PhpAmqpLib\Connection\AMQPStreamConnection;
+use VladimirYuldashev\LaravelQueueRabbitMQ\Console\ConsumeCommand;
use VladimirYuldashev\LaravelQueueRabbitMQ\Queue\Connectors\RabbitMQConnector;
-use VladimirYuldashev\LaravelQueueRabbitMQ\Queue\Connectors\RabbitMQConnectorSSL;
class LaravelQueueRabbitMQServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
- *
- * @return void
*/
- public function register()
+ public function register(): void
{
$this->mergeConfigFrom(
- __DIR__ . '/../config/rabbitmq.php', 'queue.connections.rabbitmq'
+ __DIR__.'/../config/rabbitmq.php',
+ 'queue.connections.rabbitmq'
);
+
+ if ($this->app->runningInConsole()) {
+ $this->app->singleton('rabbitmq.consumer', function () {
+ $isDownForMaintenance = function () {
+ return $this->app->isDownForMaintenance();
+ };
+
+ return new Consumer(
+ $this->app['queue'],
+ $this->app['events'],
+ $this->app[ExceptionHandler::class],
+ $isDownForMaintenance
+ );
+ });
+
+ $this->app->singleton(ConsumeCommand::class, static function ($app) {
+ return new ConsumeCommand(
+ $app['rabbitmq.consumer'],
+ $app['cache.store']
+ );
+ });
+
+ $this->commands([
+ Console\ConsumeCommand::class,
+ ]);
+ }
+
+ $this->commands([
+ Console\ExchangeDeclareCommand::class,
+ Console\ExchangeDeleteCommand::class,
+ Console\QueueBindCommand::class,
+ Console\QueueDeclareCommand::class,
+ Console\QueueDeleteCommand::class,
+ Console\QueuePurgeCommand::class,
+ ]);
}
/**
* Register the application's event listeners.
- *
- * @return void
*/
- public function boot()
+ public function boot(): void
{
/** @var QueueManager $queue */
$queue = $this->app['queue'];
-
- if ($this->app['config']['rabbitmq']['ssl_params']['ssl_on'] === true) {
- $connector = new RabbitMQConnectorSSL();
- } else {
- $connector = new RabbitMQConnector();
- }
-
- $queue->stopping(function () use ($connector) {
- if ($connector->connection() instanceof AMQPStreamConnection) {
- $connector->connection()->close();
- }
- });
- $queue->addConnector('rabbitmq', function () use ($connector) {
- return $connector;
+ $queue->addConnector('rabbitmq', function () {
+ return new RabbitMQConnector($this->app['events']);
});
}
}
diff --git a/src/Queue/Connection/ConfigFactory.php b/src/Queue/Connection/ConfigFactory.php
new file mode 100644
index 00000000..06c8080d
--- /dev/null
+++ b/src/Queue/Connection/ConfigFactory.php
@@ -0,0 +1,126 @@
+setIsLazy(! in_array(
+ Arr::get($config, 'lazy') ?? true,
+ [false, 0, '0', 'false', 'no'],
+ true)
+ );
+
+ // Set the connection to unsecure by default
+ $connectionConfig->setIsSecure(in_array(
+ Arr::get($config, 'secure'),
+ [true, 1, '1', 'true', 'yes'],
+ true)
+ );
+
+ if ($connectionConfig->isSecure()) {
+ self::getSLLOptionsFromConfig($connectionConfig, $config);
+ }
+
+ self::getHostFromConfig($connectionConfig, $config);
+ self::getHeartbeatFromConfig($connectionConfig, $config);
+ self::getNetworkProtocolFromConfig($connectionConfig, $config);
+ self::getTimeoutsFromConfig($connectionConfig, $config);
+ });
+ }
+
+ protected static function getHostFromConfig(AMQPConnectionConfig $connectionConfig, array $config): void
+ {
+ $hostConfig = Arr::first(Arr::shuffle(Arr::get($config, self::CONFIG_HOSTS, [])), null, []);
+
+ if ($location = Arr::get($hostConfig, 'host')) {
+ $connectionConfig->setHost($location);
+ }
+ if ($port = Arr::get($hostConfig, 'port')) {
+ $connectionConfig->setPort($port);
+ }
+ if ($vhost = Arr::get($hostConfig, 'vhost')) {
+ $connectionConfig->setVhost($vhost);
+ }
+ if ($user = Arr::get($hostConfig, 'user')) {
+ $connectionConfig->setUser($user);
+ }
+ if ($password = Arr::get($hostConfig, 'password')) {
+ $connectionConfig->setPassword($password);
+ }
+ }
+
+ protected static function getSLLOptionsFromConfig(AMQPConnectionConfig $connectionConfig, array $config): void
+ {
+ $sslConfig = Arr::get($config, self::CONFIG_OPTIONS.'.ssl_options', []);
+
+ if ($caFile = Arr::get($sslConfig, 'cafile')) {
+ $connectionConfig->setSslCaCert($caFile);
+ }
+ if ($cert = Arr::get($sslConfig, 'local_cert')) {
+ $connectionConfig->setSslCert($cert);
+ }
+ if ($key = Arr::get($sslConfig, 'local_key')) {
+ $connectionConfig->setSslKey($key);
+ }
+ if (Arr::has($sslConfig, 'verify_peer')) {
+ $verifyPeer = Arr::get($sslConfig, 'verify_peer');
+ $connectionConfig->setSslVerify($verifyPeer);
+ }
+ if ($passphrase = Arr::get($sslConfig, 'passphrase')) {
+ $connectionConfig->setSslPassPhrase($passphrase);
+ }
+ }
+
+ protected static function getHeartbeatFromConfig(AMQPConnectionConfig $connectionConfig, array $config): void
+ {
+ $heartbeat = Arr::get($config, self::CONFIG_OPTIONS.'.heartbeat');
+
+ if (is_numeric($heartbeat) && intval($heartbeat) > 0) {
+ $connectionConfig->setHeartbeat((int) $heartbeat);
+ }
+ }
+
+ protected static function getNetworkProtocolFromConfig(AMQPConnectionConfig $connectionConfig, array $config): void
+ {
+ if ($networkProtocol = Arr::get($config, 'network_protocol')) {
+ $connectionConfig->setNetworkProtocol($networkProtocol);
+ }
+ }
+
+ protected static function getTimeoutsFromConfig(AMQPConnectionConfig $connectionConfig, array $config): void
+ {
+ $connectionTimeout = Arr::get($config, self::CONFIG_OPTIONS.'.connection_timeout');
+ if (is_numeric($connectionTimeout) && floatval($connectionTimeout) >= 0) {
+ $connectionConfig->setConnectionTimeout((float) $connectionTimeout);
+ }
+
+ $readTimeout = Arr::get($config, self::CONFIG_OPTIONS.'.read_timeout');
+ if (is_numeric($readTimeout) && floatval($readTimeout) >= 0) {
+ $connectionConfig->setReadTimeout((float) $readTimeout);
+ }
+
+ $writeTimeout = Arr::get($config, self::CONFIG_OPTIONS.'.write_timeout');
+ if (is_numeric($writeTimeout) && floatval($writeTimeout) >= 0) {
+ $connectionConfig->setWriteTimeout((float) $writeTimeout);
+ }
+
+ $chanelRpcTimeout = Arr::get($config, self::CONFIG_OPTIONS.'.channel_rpc_timeout');
+ if (is_numeric($chanelRpcTimeout) && floatval($chanelRpcTimeout) >= 0) {
+ $connectionConfig->setChannelRPCTimeout((float) $chanelRpcTimeout);
+ }
+ }
+}
diff --git a/src/Queue/Connection/ConnectionFactory.php b/src/Queue/Connection/ConnectionFactory.php
new file mode 100644
index 00000000..f531e761
--- /dev/null
+++ b/src/Queue/Connection/ConnectionFactory.php
@@ -0,0 +1,223 @@
+getIoType() === AMQPConnectionConfig::IO_TYPE_SOCKET) {
+ return self::createSocketConnection($connection, $config);
+ }
+
+ return self::createStreamConnection($connection, $config);
+ }
+
+ protected static function createSocketConnection($connection, AMQPConnectionConfig $config): AMQPSocketConnection
+ {
+ self::assertSocketConnection($connection, $config);
+
+ return new $connection(
+ $config->getHost(),
+ $config->getPort(),
+ $config->getUser(),
+ $config->getPassword(),
+ $config->getVhost(),
+ $config->isInsist(),
+ $config->getLoginMethod(),
+ $config->getLoginResponse(),
+ $config->getLocale(),
+ $config->getReadTimeout(),
+ $config->isKeepalive(),
+ $config->getWriteTimeout(),
+ $config->getHeartbeat(),
+ $config->getChannelRPCTimeout(),
+ $config
+ );
+ }
+
+ protected static function createStreamConnection($connection, AMQPConnectionConfig $config): AMQPStreamConnection
+ {
+ self::assertStreamConnection($connection);
+
+ if ($config->isSecure()) {
+ self::assertSSLConnection($connection);
+
+ return new $connection(
+ $config->getHost(),
+ $config->getPort(),
+ $config->getUser(),
+ $config->getPassword(),
+ $config->getVhost(),
+ self::getSslOptions($config),
+ [
+ 'insist' => $config->isInsist(),
+ 'login_method' => $config->getLoginMethod(),
+ 'login_response' => $config->getLoginResponse(),
+ 'locale' => $config->getLocale(),
+ 'connection_timeout' => $config->getConnectionTimeout(),
+ 'read_write_timeout' => self::getReadWriteTimeout($config),
+ 'keepalive' => $config->isKeepalive(),
+ 'heartbeat' => $config->getHeartbeat(),
+ ],
+ $config
+ );
+ }
+
+ return new $connection(
+ $config->getHost(),
+ $config->getPort(),
+ $config->getUser(),
+ $config->getPassword(),
+ $config->getVhost(),
+ $config->isInsist(),
+ $config->getLoginMethod(),
+ $config->getLoginResponse(),
+ $config->getLocale(),
+ $config->getConnectionTimeout(),
+ self::getReadWriteTimeout($config),
+ $config->getStreamContext(),
+ $config->isKeepalive(),
+ $config->getHeartbeat(),
+ $config->getChannelRPCTimeout(),
+ $config->getNetworkProtocol(),
+ $config
+ );
+ }
+
+ protected static function getReadWriteTimeout(AMQPConnectionConfig $config): float
+ {
+ return min($config->getReadTimeout(), $config->getWriteTimeout());
+ }
+
+ protected static function getSslOptions(AMQPConnectionConfig $config): array
+ {
+ return array_filter([
+ 'cafile' => $config->getSslCaCert(),
+ 'capath' => $config->getSslCaPath(),
+ 'local_cert' => $config->getSslCert(),
+ 'local_pk' => $config->getSslKey(),
+ 'verify_peer' => $config->getSslVerify(),
+ 'verify_peer_name' => $config->getSslVerifyName(),
+ 'passphrase' => $config->getSslPassPhrase(),
+ 'ciphers' => $config->getSslCiphers(),
+ 'security_level' => $config->getSslSecurityLevel(),
+ ], static function ($value) {
+ return $value !== null;
+ });
+ }
+
+ protected static function assertConnectionFromConfig(string $connection): void
+ {
+ if ($connection !== self::CONNECTION_TYPE_DEFAULT && ! is_subclass_of($connection, self::CONNECTION_TYPE_EXTENDED)) {
+ throw new AMQPLogicException(sprintf('The config property \'%s\' must contain \'%s\' or must extend: %s', self::CONFIG_CONNECTION, self::CONNECTION_TYPE_DEFAULT, class_basename(self::CONNECTION_TYPE_EXTENDED)));
+ }
+ }
+
+ protected static function assertSocketConnection($connection, AMQPConnectionConfig $config): void
+ {
+ self::assertExtendedOf($connection, self::CONNECTION_SUB_TYPE_SOCKET);
+
+ if ($config->isSecure()) {
+ throw new AMQPLogicException('The socket connection implementation does not support secure connections.');
+ }
+ }
+
+ protected static function assertStreamConnection($connection): void
+ {
+ self::assertExtendedOf($connection, self::CONNECTION_SUB_TYPE_STREAM);
+ }
+
+ protected static function assertSSLConnection($connection): void
+ {
+ self::assertExtendedOf($connection, self::CONNECTION_SUB_TYPE_SSL);
+ }
+
+ protected static function assertExtendedOf($connection, string $parent): void
+ {
+ if (! is_subclass_of($connection, $parent) && $connection !== $parent) {
+ throw new AMQPLogicException(sprintf('The connection must extend: %s', class_basename($parent)));
+ }
+ }
+
+ /**
+ * @return mixed
+ *
+ * @throws Exception
+ *
+ * @deprecated This is the fallback method, update your config asap. (example: connection => 'default')
+ */
+ protected static function _createLazyConnection($connection, array $config): AbstractConnection
+ {
+ return $connection::create_connection(
+ Arr::shuffle(Arr::get($config, ConfigFactory::CONFIG_HOSTS, [])),
+ Arr::add(Arr::get($config, 'options', []), 'heartbeat', 0)
+ );
+ }
+}
diff --git a/src/Queue/Connectors/RabbitMQConnector.php b/src/Queue/Connectors/RabbitMQConnector.php
index ae5bc147..a0f79e32 100644
--- a/src/Queue/Connectors/RabbitMQConnector.php
+++ b/src/Queue/Connectors/RabbitMQConnector.php
@@ -2,42 +2,48 @@
namespace VladimirYuldashev\LaravelQueueRabbitMQ\Queue\Connectors;
+use Exception;
+use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\Connectors\ConnectorInterface;
-use PhpAmqpLib\Connection\AMQPStreamConnection;
+use Illuminate\Queue\Events\JobFailed;
+use Illuminate\Queue\Events\WorkerStopping;
+use VladimirYuldashev\LaravelQueueRabbitMQ\Horizon\Listeners\RabbitMQFailedEvent;
+use VladimirYuldashev\LaravelQueueRabbitMQ\Horizon\RabbitMQQueue as HorizonRabbitMQQueue;
+use VladimirYuldashev\LaravelQueueRabbitMQ\Queue\Connection\ConnectionFactory;
+use VladimirYuldashev\LaravelQueueRabbitMQ\Queue\QueueFactory;
use VladimirYuldashev\LaravelQueueRabbitMQ\Queue\RabbitMQQueue;
class RabbitMQConnector implements ConnectorInterface
{
- /** @var AMQPStreamConnection */
- private $connection;
+ protected Dispatcher $dispatcher;
+
+ public function __construct(Dispatcher $dispatcher)
+ {
+ $this->dispatcher = $dispatcher;
+ }
/**
* Establish a queue connection.
*
- * @param array $config
+ * @return RabbitMQQueue
*
- * @return Queue
+ * @throws Exception
*/
public function connect(array $config): Queue
{
- // create connection with AMQP
- $this->connection = new AMQPStreamConnection(
- $config['host'],
- $config['port'],
- $config['login'],
- $config['password'],
- $config['vhost']
- );
-
- return new RabbitMQQueue(
- $this->connection,
- $config
- );
- }
+ $connection = ConnectionFactory::make($config);
- public function connection()
- {
- return $this->connection;
+ $queue = QueueFactory::make($config)->setConnection($connection);
+
+ if ($queue instanceof HorizonRabbitMQQueue) {
+ $this->dispatcher->listen(JobFailed::class, RabbitMQFailedEvent::class);
+ }
+
+ $this->dispatcher->listen(WorkerStopping::class, static function () use ($queue): void {
+ $queue->close();
+ });
+
+ return $queue;
}
}
diff --git a/src/Queue/Connectors/RabbitMQConnectorSSL.php b/src/Queue/Connectors/RabbitMQConnectorSSL.php
deleted file mode 100644
index d73c55b5..00000000
--- a/src/Queue/Connectors/RabbitMQConnectorSSL.php
+++ /dev/null
@@ -1,50 +0,0 @@
- $option) {
- if ($option === null || empty($option)) {
- unset($config['ssl_params'][$idx]);
- }
- }
-
- // Create connection with AMQP
- $this->connection = new AMQPSSLConnection(
- $config['host'],
- $config['port'],
- $config['login'],
- $config['password'],
- $config['vhost'],
- $config['ssl_params']
- );
-
- return new RabbitMQQueue(
- $this->connection,
- $config
- );
- }
-
- public function connection()
- {
- return $this->connection;
- }
-}
diff --git a/src/Queue/Jobs/RabbitMQJob.php b/src/Queue/Jobs/RabbitMQJob.php
index 88633b08..abcdfab4 100644
--- a/src/Queue/Jobs/RabbitMQJob.php
+++ b/src/Queue/Jobs/RabbitMQJob.php
@@ -2,187 +2,165 @@
namespace VladimirYuldashev\LaravelQueueRabbitMQ\Queue\Jobs;
-use Exception;
use Illuminate\Container\Container;
+use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Queue\Job as JobContract;
-use Illuminate\Database\DetectsDeadlocks;
use Illuminate\Queue\Jobs\Job;
-use Illuminate\Queue\Jobs\JobName;
-use Illuminate\Support\Str;
-use PhpAmqpLib\Channel\AMQPChannel;
+use Illuminate\Support\Arr;
+use PhpAmqpLib\Exception\AMQPProtocolChannelException;
use PhpAmqpLib\Message\AMQPMessage;
+use PhpAmqpLib\Wire\AMQPTable;
+use VladimirYuldashev\LaravelQueueRabbitMQ\Horizon\RabbitMQQueue as HorizonRabbitMQQueue;
use VladimirYuldashev\LaravelQueueRabbitMQ\Queue\RabbitMQQueue;
class RabbitMQJob extends Job implements JobContract
{
- use DetectsDeadlocks;
-
/**
- * Same as RabbitMQQueue, used for attempt counts.
+ * The RabbitMQ queue instance.
+ *
+ * @var RabbitMQQueue
*/
- const ATTEMPT_COUNT_HEADERS_KEY = 'attempts_count';
+ protected $rabbitmq;
- protected $connection;
- protected $channel;
+ /**
+ * The RabbitMQ message instance.
+ *
+ * @var AMQPMessage
+ */
protected $message;
+ /**
+ * The JSON decoded version of "$message".
+ *
+ * @var array
+ */
+ protected $decoded;
+
public function __construct(
Container $container,
- RabbitMQQueue $connection,
- AMQPChannel $channel,
- string $queue,
+ RabbitMQQueue $rabbitmq,
AMQPMessage $message,
- string $connectionName = null
+ string $connectionName,
+ string $queue
) {
$this->container = $container;
- $this->connection = $connection;
- $this->channel = $channel;
- $this->queue = $queue;
+ $this->rabbitmq = $rabbitmq;
$this->message = $message;
$this->connectionName = $connectionName;
+ $this->queue = $queue;
+ $this->decoded = $this->payload();
}
/**
- * Fire the job.
- *
- * @throws Exception
- *
- * @return void
+ * {@inheritdoc}
*/
- public function fire()
+ public function getJobId()
{
- try {
- $payload = $this->payload();
-
- list($class, $method) = JobName::parse($payload['job']);
-
- with($this->instance = $this->resolve($class))->{$method}($this, $payload['data']);
- } catch (Exception $exception) {
- if (
- $this->causedByDeadlock($exception) ||
- Str::contains($exception->getMessage(), ['detected deadlock'])
- ) {
- sleep(2);
- $this->fire();
-
- return;
- }
+ return $this->decoded['id'] ?? null;
+ }
- throw $exception;
- }
+ /**
+ * {@inheritdoc}
+ */
+ public function getRawBody(): string
+ {
+ return $this->message->getBody();
}
/**
- * Get the number of times the job has been attempted.
- *
- * @return int
+ * {@inheritdoc}
*/
public function attempts(): int
{
- if ($this->message->has('application_headers') === true) {
- $headers = $this->message->get('application_headers')->getNativeData();
-
- if (isset($headers[self::ATTEMPT_COUNT_HEADERS_KEY]) === true) {
- return $headers[self::ATTEMPT_COUNT_HEADERS_KEY];
- }
+ if (! $data = $this->getRabbitMQMessageHeaders()) {
+ return 1;
}
- // set default job attempts to 1 so that jobs can run without retry
- return 1;
+ $laravelAttempts = (int) Arr::get($data, 'laravel.attempts', 0);
+
+ return $laravelAttempts + 1;
}
/**
- * Get the raw body string for the job.
- *
- * @return string
+ * {@inheritdoc}
*/
- public function getRawBody(): string
+ public function markAsFailed(): void
{
- return $this->message->body;
- }
+ parent::markAsFailed();
- /** @inheritdoc */
- public function delete()
- {
- parent::delete();
- $this->channel->basic_ack($this->message->get('delivery_tag'));
+ // We must tel rabbitMQ this Job is failed
+ // The message must be rejected when the Job marked as failed, in case rabbitMQ wants to do some extra magic.
+ // like: Death lettering the message to an other exchange/routing-key.
+ $this->rabbitmq->reject($this);
}
- /** @inheritdoc */
- public function release($delay = 0)
+ /**
+ * {@inheritdoc}
+ *
+ * @throws BindingResolutionException
+ */
+ public function delete(): void
{
- parent::release($delay);
-
- $this->delete();
- $this->connection->setAttempts($this->attempts() + 1);
-
- $body = $this->payload();
+ parent::delete();
- /*
- * Some jobs don't have the command set, so fall back to just sending it the job name string
- */
- if (isset($body['data']['command']) === true) {
- $job = $this->unserialize($body);
- } else {
- $job = $this->getName();
+ // When delete is called and the Job was not failed, the message must be acknowledged.
+ // This is because this is a controlled call by a developer. So the message was handled correct.
+ if (! $this->failed) {
+ $this->rabbitmq->ack($this);
}
- $data = $body['data'];
-
- if ($delay > 0) {
- $this->connection->later($delay, $job, $data, $this->getQueue());
- } else {
- $this->connection->push($job, $data, $this->getQueue());
+ // required for Laravel Horizon
+ if ($this->rabbitmq instanceof HorizonRabbitMQQueue) {
+ $this->rabbitmq->deleteReserved($this->queue, $this);
}
}
/**
- * Get the job identifier.
+ * Release the job back into the queue.
+ *
+ * @param int $delay
*
- * @return string
+ * @throws AMQPProtocolChannelException
*/
- public function getJobId(): string
+ public function release($delay = 0): void
{
- return $this->message->get('correlation_id');
+ parent::release();
+
+ // Always create a new message when this Job is released
+ $this->rabbitmq->laterRaw($delay, $this->message->getBody(), $this->queue, $this->attempts());
+
+ // Releasing a Job means the message was failed to process.
+ // Because this Job message is always recreated and pushed as new message, this Job message is correctly handled.
+ // We must tell rabbitMQ this job message can be removed by acknowledging the message.
+ $this->rabbitmq->ack($this);
}
/**
- * Sets the job identifier.
- *
- * @param string $id
- *
- * @return void
+ * Get the underlying RabbitMQ connection.
*/
- public function setJobId($id)
+ public function getRabbitMQ(): RabbitMQQueue
{
- $this->connection->setCorrelationId($id);
+ return $this->rabbitmq;
}
/**
- * Unserialize job.
- *
- * @param array $body
- *
- * @throws Exception
- *
- * @return mixed
+ * Get the underlying RabbitMQ message.
*/
- private function unserialize(array $body)
+ public function getRabbitMQMessage(): AMQPMessage
{
- try {
- /** @noinspection UnserializeExploitsInspection */
- return unserialize($body['data']['command']);
- } catch (Exception $exception) {
- if (
- $this->causedByDeadlock($exception) ||
- Str::contains($exception->getMessage(), ['detected deadlock'])
- ) {
- sleep(2);
-
- return $this->unserialize($body);
- }
-
- throw $exception;
+ return $this->message;
+ }
+
+ /**
+ * Get the headers from the rabbitMQ message.
+ */
+ protected function getRabbitMQMessageHeaders(): ?array
+ {
+ /** @var AMQPTable|null $headers */
+ if (! $headers = Arr::get($this->message->get_properties(), 'application_headers')) {
+ return null;
}
+
+ return $headers->getNativeData();
}
}
diff --git a/src/Queue/QueueConfig.php b/src/Queue/QueueConfig.php
new file mode 100644
index 00000000..e7ec27c4
--- /dev/null
+++ b/src/Queue/QueueConfig.php
@@ -0,0 +1,278 @@
+queue;
+ }
+
+ public function setQueue(string $queue): QueueConfig
+ {
+ $this->queue = $queue;
+
+ return $this;
+ }
+
+ /**
+ * Returns &true; as indication that jobs should be dispatched after all database transactions
+ * have been committed.
+ */
+ public function isDispatchAfterCommit(): bool
+ {
+ return $this->dispatchAfterCommit;
+ }
+
+ public function setDispatchAfterCommit($dispatchAfterCommit): QueueConfig
+ {
+ $this->dispatchAfterCommit = $this->toBoolean($dispatchAfterCommit);
+
+ return $this;
+ }
+
+ /**
+ * Get the Job::class to use when processing messages
+ */
+ public function getAbstractJob(): string
+ {
+ return $this->abstractJob;
+ }
+
+ public function setAbstractJob(string $abstract): QueueConfig
+ {
+ $this->abstractJob = $abstract;
+
+ return $this;
+ }
+
+ /**
+ * Returns &true;, if delayed messages should be prioritized.
+ *
+ * RabbitMQ queues work with the FIFO method. So when there are 10000 messages in the queue and
+ * the delayed message is put back to the queue (at the end) for further processing the delayed message won´t
+ * process before all 10000 messages are processed. The same is true for requeueing.
+ *
+ * This may not what you desire.
+ * When you want the message to get processed immediately after the delayed time expires or when requeueing, we can
+ * use prioritization.
+ *
+ * @see[https://www.rabbitmq.com/queues.html#basics]
+ */
+ public function isPrioritizeDelayed(): bool
+ {
+ return $this->prioritizeDelayed;
+ }
+
+ public function setPrioritizeDelayed($prioritizeDelayed): QueueConfig
+ {
+ $this->prioritizeDelayed = $this->toBoolean($prioritizeDelayed);
+
+ return $this;
+ }
+
+ /**
+ * Returns a integer with a default of '2' for when using prioritization on delayed messages.
+ * If priority queues are desired, we recommend using between 1 and 10.
+ * Using more priority layers, will consume more CPU resources and would affect runtimes.
+ *
+ * @see https://www.rabbitmq.com/priority.html
+ */
+ public function getQueueMaxPriority(): int
+ {
+ return $this->queueMaxPriority;
+ }
+
+ public function setQueueMaxPriority($queueMaxPriority): QueueConfig
+ {
+ if (is_numeric($queueMaxPriority) && intval($queueMaxPriority) > 1) {
+ $this->queueMaxPriority = (int) $queueMaxPriority;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the exchange name, or empty string; as default value.
+ *
+ * The default exchange is an unnamed pre-declared direct exchange. Usually, an empty string
+ * is frequently used to indicate it. If you choose default exchange, your message will be delivered
+ * to a queue with the same name as the routing key.
+ * With a routing key that is the same as the queue name, every queue is immediately tied to the default exchange.
+ */
+ public function getExchange(): string
+ {
+ return $this->exchange;
+ }
+
+ public function setExchange(string $exchange): QueueConfig
+ {
+ $this->exchange = $exchange;
+
+ return $this;
+ }
+
+ /**
+ * Get the exchange type
+ *
+ * There are four basic RabbitMQ exchange types in RabbitMQ, each of which uses different parameters
+ * and bindings to route messages in various ways, These are: 'direct', 'topic', 'fanout', 'headers'
+ *
+ * The default type is set as 'direct'
+ */
+ public function getExchangeType(): string
+ {
+ return $this->exchangeType;
+ }
+
+ public function setExchangeType(string $exchangeType): QueueConfig
+ {
+ $this->exchangeType = $exchangeType;
+
+ return $this;
+ }
+
+ /**
+ * Get the routing key when using an exchange other than the direct exchange.
+ * The routing key is a message attribute taken into account by the exchange when deciding how to route a message.
+ *
+ * The default routing-key is the given destination: '%s'.
+ */
+ public function getExchangeRoutingKey(): string
+ {
+ return $this->exchangeRoutingKey;
+ }
+
+ public function setExchangeRoutingKey(string $exchangeRoutingKey): QueueConfig
+ {
+ $this->exchangeRoutingKey = $exchangeRoutingKey;
+
+ return $this;
+ }
+
+ /**
+ * Returns &true;, if failed messages should be rerouted.
+ */
+ public function isRerouteFailed(): bool
+ {
+ return $this->rerouteFailed;
+ }
+
+ public function setRerouteFailed($rerouteFailed): QueueConfig
+ {
+ $this->rerouteFailed = $this->toBoolean($rerouteFailed);
+
+ return $this;
+ }
+
+ /**
+ * Get the exchange name with messages are published against.
+ * The default exchange is empty, so messages will be published directly to a queue.
+ */
+ public function getFailedExchange(): string
+ {
+ return $this->failedExchange;
+ }
+
+ public function setFailedExchange(string $failedExchange): QueueConfig
+ {
+ $this->failedExchange = $failedExchange;
+
+ return $this;
+ }
+
+ /**
+ * Get the substitution string for failed messages
+ * The default routing-key is the given destination substituted by '%s.failed'.
+ */
+ public function getFailedRoutingKey(): string
+ {
+ return $this->failedRoutingKey;
+ }
+
+ public function setFailedRoutingKey(string $failedRoutingKey): QueueConfig
+ {
+ $this->failedRoutingKey = $failedRoutingKey;
+
+ return $this;
+ }
+
+ /**
+ * Returns &true;, if queue is marked or set as quorum queue.
+ */
+ public function isQuorum(): bool
+ {
+ return $this->quorum;
+ }
+
+ public function setQuorum($quorum): QueueConfig
+ {
+ $this->quorum = $this->toBoolean($quorum);
+
+ return $this;
+ }
+
+ /**
+ * Holds all unknown queue options provided in the connection config
+ */
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+
+ public function setOptions(array $options): QueueConfig
+ {
+ $this->options = $options;
+
+ return $this;
+ }
+
+ /**
+ * Filters $value to boolean value
+ *
+ * Returns: &true;
+ * For values: 1, '1', true, 'true', 'yes'
+ *
+ * Returns: &false;
+ * For values: 0, '0', false, 'false', '', null, [] , 'ok', 'no', 'no not a bool', 'yes a bool'
+ */
+ protected function toBoolean($value): bool
+ {
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN);
+ }
+}
diff --git a/src/Queue/QueueConfigFactory.php b/src/Queue/QueueConfigFactory.php
new file mode 100644
index 00000000..6f2befc5
--- /dev/null
+++ b/src/Queue/QueueConfigFactory.php
@@ -0,0 +1,74 @@
+setQueue($queue);
+ }
+ if (! empty($afterCommit = Arr::get($config, 'after_commit'))) {
+ $queueConfig->setDispatchAfterCommit($afterCommit);
+ }
+
+ self::getOptionsFromConfig($queueConfig, $config);
+ });
+ }
+
+ protected static function getOptionsFromConfig(QueueConfig $queueConfig, array $config): void
+ {
+ $queueOptions = Arr::get($config, self::CONFIG_OPTIONS.'.queue', []) ?: [];
+
+ if ($job = Arr::pull($queueOptions, 'job')) {
+ $queueConfig->setAbstractJob($job);
+ }
+
+ // Feature: Prioritize delayed messages.
+ if ($prioritizeDelayed = Arr::pull($queueOptions, 'prioritize_delayed')) {
+ $queueConfig->setPrioritizeDelayed($prioritizeDelayed);
+ }
+ if ($maxPriority = Arr::pull($queueOptions, 'queue_max_priority')) {
+ $queueConfig->setQueueMaxPriority($maxPriority);
+ }
+
+ // Feature: Working with Exchange and routing-keys
+ if ($exchange = Arr::pull($queueOptions, 'exchange')) {
+ $queueConfig->setExchange($exchange);
+ }
+ if ($exchangeType = Arr::pull($queueOptions, 'exchange_type')) {
+ $queueConfig->setExchangeType($exchangeType);
+ }
+ if ($exchangeRoutingKey = Arr::pull($queueOptions, 'exchange_routing_key')) {
+ $queueConfig->setExchangeRoutingKey($exchangeRoutingKey);
+ }
+
+ // Feature: Reroute failed messages
+ if ($rerouteFailed = Arr::pull($queueOptions, 'reroute_failed')) {
+ $queueConfig->setRerouteFailed($rerouteFailed);
+ }
+ if ($failedExchange = Arr::pull($queueOptions, 'failed_exchange')) {
+ $queueConfig->setFailedExchange($failedExchange);
+ }
+ if ($failedRoutingKey = Arr::pull($queueOptions, 'failed_routing_key')) {
+ $queueConfig->setFailedRoutingKey($failedRoutingKey);
+ }
+
+ // Feature: Mark queue as quorum
+ if ($quorum = Arr::pull($queueOptions, 'quorum')) {
+ $queueConfig->setQuorum($quorum);
+ }
+
+ // All extra options not defined
+ $queueConfig->setOptions($queueOptions);
+ }
+}
diff --git a/src/Queue/QueueFactory.php b/src/Queue/QueueFactory.php
new file mode 100644
index 00000000..75bde1ed
--- /dev/null
+++ b/src/Queue/QueueFactory.php
@@ -0,0 +1,25 @@
+connection = $connection;
- $this->defaultQueue = $config['queue'];
- $this->queueParameters = $config['queue_params'];
- $this->queueArguments = isset($this->queueParameters['arguments']) ? json_decode($this->queueParameters['arguments'], true) : [];
- $this->configExchange = $config['exchange_params'];
- $this->declareExchange = $config['exchange_declare'];
- $this->declareBindQueue = $config['queue_declare_bind'];
- $this->sleepOnError = $config['sleep_on_error'] ?? 5;
+ /**
+ * Holds the Configuration
+ */
+ protected QueueConfig $rabbitMQConfig;
- $this->channel = $this->getChannel();
+ /**
+ * RabbitMQQueue constructor.
+ */
+ public function __construct(QueueConfig $config)
+ {
+ $this->rabbitMQConfig = $config;
+ $this->dispatchAfterCommit = $config->isDispatchAfterCommit();
}
- /** @inheritdoc */
+ /**
+ * {@inheritdoc}
+ *
+ * @throws AMQPProtocolChannelException
+ */
public function size($queue = null): int
{
- list(, $messageCount) = $this->channel->queue_declare($this->getQueueName($queue), true);
+ $queue = $this->getQueue($queue);
- return $messageCount;
+ if (! $this->isQueueExists($queue)) {
+ return 0;
+ }
+
+ // create a temporary channel, so the main channel will not be closed on exception
+ $channel = $this->createChannel();
+ [, $size] = $channel->queue_declare($queue, true);
+ $channel->close();
+
+ return $size;
}
- /** @inheritdoc */
+ /**
+ * {@inheritdoc}
+ *
+ * @throws AMQPProtocolChannelException
+ */
public function push($job, $data = '', $queue = null)
{
- return $this->pushRaw($this->createPayload($job, $data), $queue, []);
+ return $this->enqueueUsing(
+ $job,
+ $this->createPayload($job, $this->getQueue($queue), $data),
+ $queue,
+ null,
+ function ($payload, $queue) {
+ return $this->pushRaw($payload, $queue);
+ }
+ );
}
- /** @inheritdoc */
- public function pushRaw($payload, $queue = null, array $options = [])
+ /**
+ * {@inheritdoc}
+ *
+ * @throws AMQPProtocolChannelException
+ */
+ public function pushRaw($payload, $queue = null, array $options = []): int|string|null
{
- try {
- $queue = $this->getQueueName($queue);
- if (isset($options['delay']) && $options['delay'] > 0) {
- list($queue, $exchange) = $this->declareDelayedQueue($queue, $options['delay']);
- } else {
- list($queue, $exchange) = $this->declareQueue($queue);
- }
+ [$destination, $exchange, $exchangeType, $attempts] = $this->publishProperties($queue, $options);
+
+ $this->declareDestination($destination, $exchange, $exchangeType);
- $headers = [
- 'Content-Type' => 'application/json',
- 'delivery_mode' => 2,
- ];
+ [$message, $correlationId] = $this->createMessage($payload, $attempts);
- if ($this->retryAfter !== null) {
- $headers['application_headers'] = [self::ATTEMPT_COUNT_HEADERS_KEY => ['I', $this->retryAfter]];
+ $this->publishBasic($message, $exchange, $destination, true);
+
+ return $correlationId;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws AMQPProtocolChannelException
+ */
+ public function later($delay, $job, $data = '', $queue = null): mixed
+ {
+ return $this->enqueueUsing(
+ $job,
+ $this->createPayload($job, $this->getQueue($queue), $data),
+ $queue,
+ $delay,
+ function ($payload, $queue, $delay) {
+ return $this->laterRaw($delay, $payload, $queue);
}
+ );
+ }
+
+ /**
+ * @throws AMQPProtocolChannelException
+ */
+ public function laterRaw($delay, string $payload, $queue = null, int $attempts = 0): int|string|null
+ {
+ $ttl = $this->secondsUntil($delay) * 1000;
+
+ // default options
+ $options = ['delay' => $delay, 'attempts' => $attempts];
+
+ // When no ttl just publish a new message to the exchange or queue
+ if ($ttl <= 0) {
+ return $this->pushRaw($payload, $queue, $options);
+ }
+
+ // Create a main queue to handle delayed messages
+ [$mainDestination, $exchange, $exchangeType, $attempts] = $this->publishProperties($queue, $options);
+ $this->declareDestination($mainDestination, $exchange, $exchangeType);
+
+ $destination = $this->getQueue($queue).'.delay.'.$ttl;
- // push job to a queue
- $message = new AMQPMessage($payload, $headers);
+ $this->declareQueue($destination, true, false, $this->getDelayQueueArguments($this->getQueue($queue), $ttl));
- $correlationId = $this->getCorrelationId();
- $message->set('correlation_id', $correlationId);
+ [$message, $correlationId] = $this->createMessage($payload, $attempts);
- // push task to a queue
- $this->channel->basic_publish($message, $exchange, $queue);
+ // Publish directly on the delayQueue, no need to publish through an exchange.
+ $this->publishBasic($message, null, $destination, true);
- return $correlationId;
- } catch (ErrorException $exception) {
- $this->reportConnectionError('pushRaw', $exception);
+ return $correlationId;
+ }
- return null;
+ /**
+ * {@inheritdoc}
+ *
+ * @throws AMQPProtocolChannelException
+ */
+ public function bulk($jobs, $data = '', $queue = null): void
+ {
+ $this->publishBatch($jobs, $data, $queue);
+ }
+
+ /**
+ * @throws AMQPProtocolChannelException
+ */
+ protected function publishBatch($jobs, $data = '', $queue = null): void
+ {
+ foreach ($jobs as $job) {
+ $this->bulkRaw($this->createPayload($job, $queue, $data), $queue, ['job' => $job]);
}
+
+ $this->batchPublish();
}
- /** @inheritdoc */
- public function later($delay, $job, $data = '', $queue = null)
+ /**
+ * @throws AMQPProtocolChannelException
+ */
+ public function bulkRaw(string $payload, ?string $queue = null, array $options = []): int|string|null
{
- return $this->pushRaw($this->createPayload($job, $data), $queue, ['delay' => $this->secondsUntil($delay)]);
+ [$destination, $exchange, $exchangeType, $attempts] = $this->publishProperties($queue, $options);
+
+ $this->declareDestination($destination, $exchange, $exchangeType);
+
+ [$message, $correlationId] = $this->createMessage($payload, $attempts);
+
+ $this->getChannel()->batch_basic_publish($message, $exchange, $destination);
+
+ return $correlationId;
}
- /** @inheritdoc */
+ /**
+ * {@inheritdoc}
+ *
+ * @throws Throwable
+ */
public function pop($queue = null)
{
- $queue = $this->getQueueName($queue);
-
try {
- // declare queue if not exists
- $this->declareQueue($queue);
+ $queue = $this->getQueue($queue);
- // get envelope
- $message = $this->channel->basic_get($queue);
+ $job = $this->getJobClass();
- if ($message instanceof AMQPMessage) {
- return new RabbitMQJob(
+ /** @var AMQPMessage|null $message */
+ if ($message = $this->getChannel()->basic_get($queue)) {
+ return $this->currentJob = new $job(
$this->container,
$this,
- $this->channel,
- $queue,
$message,
- $this->connectionName
+ $this->connectionName,
+ $queue
);
}
- } catch (ErrorException $exception) {
- $this->reportConnectionError('pop', $exception);
+ } catch (AMQPProtocolChannelException $exception) {
+ // If there is no exchange or queue AMQP will throw exception with code 404
+ // We need to catch it and return null
+ if ($exception->amqp_reply_code === 404) {
+ // Because of the channel exception the channel was closed and removed.
+ // We have to open a new channel. Because else the worker(s) are stuck in a loop, without processing.
+ $this->getChannel(true);
+
+ return null;
+ }
+
+ throw $exception;
+ } catch (AMQPChannelClosedException|AMQPConnectionClosedException $exception) {
+ // Queue::pop used by worker to receive new job
+ // Thrown exception is checked by Illuminate\Database\DetectsLostConnections::causedByLostConnection
+ // Is has to contain one of the several phrases in exception message in order to restart worker
+ // Otherwise worker continues to work with broken connection
+ throw new AMQPRuntimeException(
+ 'Lost connection: '.$exception->getMessage(),
+ $exception->getCode(),
+ $exception
+ );
}
return null;
}
/**
- * Sets the attempts member variable to be used in message generation.
+ * @throws RuntimeException
+ */
+ public function getConnection(): AbstractConnection
+ {
+ if (! $this->connection) {
+ throw new RuntimeException('Queue has no AMQPConnection set.');
+ }
+
+ return $this->connection;
+ }
+
+ public function setConnection(AbstractConnection $connection): RabbitMQQueue
+ {
+ $this->connection = $connection;
+
+ return $this;
+ }
+
+ /**
+ * Job class to use.
+ *
+ *
+ * @throws Throwable
+ */
+ public function getJobClass(): string
+ {
+ $job = $this->getRabbitMQConfig()->getAbstractJob();
+
+ throw_if(
+ ! is_a($job, RabbitMQJob::class, true),
+ Exception::class,
+ sprintf('Class %s must extend: %s', $job, RabbitMQJob::class)
+ );
+
+ return $job;
+ }
+
+ /**
+ * Gets a queue/destination, by default the queue option set on the connection.
+ */
+ public function getQueue($queue = null): string
+ {
+ return $queue ?: $this->getRabbitMQConfig()->getQueue();
+ }
+
+ /**
+ * Checks if the given exchange already present/defined in RabbitMQ.
+ * Returns false when the exchange is missing.
+ *
+ *
+ * @throws AMQPProtocolChannelException
+ */
+ public function isExchangeExists(string $exchange): bool
+ {
+ if ($this->isExchangeDeclared($exchange)) {
+ return true;
+ }
+
+ try {
+ // create a temporary channel, so the main channel will not be closed on exception
+ $channel = $this->createChannel();
+ $channel->exchange_declare($exchange, '', true);
+ $channel->close();
+
+ $this->exchanges[] = $exchange;
+
+ return true;
+ } catch (AMQPProtocolChannelException $exception) {
+ if ($exception->amqp_reply_code === 404) {
+ return false;
+ }
+
+ throw $exception;
+ }
+ }
+
+ /**
+ * Declare an exchange in rabbitMQ, when not already declared.
+ */
+ public function declareExchange(
+ string $name,
+ string $type = AMQPExchangeType::DIRECT,
+ bool $durable = true,
+ bool $autoDelete = false,
+ array $arguments = []
+ ): void {
+ if ($this->isExchangeDeclared($name)) {
+ return;
+ }
+
+ $this->getChannel()->exchange_declare(
+ $name,
+ $type,
+ false,
+ $durable,
+ $autoDelete,
+ false,
+ true,
+ new AMQPTable($arguments)
+ );
+ }
+
+ /**
+ * Delete an exchange from rabbitMQ, only when present in RabbitMQ.
*
- * @param int $count
*
- * @return void
+ * @throws AMQPProtocolChannelException
*/
- public function setAttempts(int $count)
+ public function deleteExchange(string $name, bool $unused = false): void
{
- $this->retryAfter = $count;
+ if (! $this->isExchangeExists($name)) {
+ return;
+ }
+
+ $idx = array_search($name, $this->exchanges);
+ unset($this->exchanges[$idx]);
+
+ $this->getChannel()->exchange_delete(
+ $name,
+ $unused
+ );
}
/**
- * Retrieves the correlation id, or a unique id.
+ * Checks if the given queue already present/defined in RabbitMQ.
+ * Returns false when the queue is missing.
+ *
*
- * @return string
+ * @throws AMQPProtocolChannelException
*/
- public function getCorrelationId(): string
+ public function isQueueExists(?string $name = null): bool
{
- return $this->correlationId ?: uniqid('', true);
+ $queueName = $this->getQueue($name);
+
+ if ($this->isQueueDeclared($queueName)) {
+ return true;
+ }
+
+ try {
+ // create a temporary channel, so the main channel will not be closed on exception
+ $channel = $this->createChannel();
+ $channel->queue_declare($queueName, true);
+ $channel->close();
+
+ $this->queues[] = $queueName;
+
+ return true;
+ } catch (AMQPProtocolChannelException $exception) {
+ if ($exception->amqp_reply_code === 404) {
+ return false;
+ }
+
+ throw $exception;
+ }
+ }
+
+ /**
+ * Declare a queue in rabbitMQ, when not already declared.
+ */
+ public function declareQueue(
+ string $name,
+ bool $durable = true,
+ bool $autoDelete = false,
+ array $arguments = []
+ ): void {
+ if ($this->isQueueDeclared($name)) {
+ return;
+ }
+
+ $this->getChannel()->queue_declare(
+ $name,
+ false,
+ $durable,
+ false,
+ $autoDelete,
+ false,
+ new AMQPTable($arguments)
+ );
}
/**
- * Sets the correlation id for a message to be published.
+ * Delete a queue from rabbitMQ, only when present in RabbitMQ.
*
- * @param string $id
*
- * @return void
+ * @throws AMQPProtocolChannelException
+ */
+ public function deleteQueue(string $name, bool $if_unused = false, bool $if_empty = false): void
+ {
+ if (! $this->isQueueExists($name)) {
+ return;
+ }
+
+ $idx = array_search($name, $this->queues);
+ unset($this->queues[$idx]);
+
+ $this->getChannel()->queue_delete($name, $if_unused, $if_empty);
+ }
+
+ /**
+ * Bind a queue to an exchange.
+ */
+ public function bindQueue(string $queue, string $exchange, string $routingKey = ''): void
+ {
+ if (in_array(
+ implode('', compact('queue', 'exchange', 'routingKey')),
+ $this->boundQueues,
+ true
+ )) {
+ return;
+ }
+
+ $this->getChannel()->queue_bind($queue, $exchange, $routingKey);
+ }
+
+ /**
+ * Purge the queue of messages.
*/
- public function setCorrelationId(string $id)
+ public function purge(?string $queue = null): void
{
- $this->correlationId = $id;
+ // create a temporary channel, so the main channel will not be closed on exception
+ $channel = $this->createChannel();
+ $channel->queue_purge($this->getQueue($queue));
+ $channel->close();
}
- private function getQueueName(string $queue = null): string
+ /**
+ * Acknowledge the message.
+ */
+ public function ack(RabbitMQJob $job): void
{
- return $queue ?: $this->defaultQueue;
+ $this->getChannel()->basic_ack($job->getRabbitMQMessage()->getDeliveryTag());
}
- private function getChannel(): AMQPChannel
+ /**
+ * Reject the message.
+ */
+ public function reject(RabbitMQJob $job, bool $requeue = false): void
{
- return $this->connection->channel();
+ $this->getChannel()->basic_reject($job->getRabbitMQMessage()->getDeliveryTag(), $requeue);
}
- private function declareQueue(string $name): array
+ /**
+ * Create a AMQP message.
+ */
+ protected function createMessage($payload, int $attempts = 0): array
{
- $name = $this->getQueueName($name);
- $exchange = $this->configExchange['name'] ?: $name;
+ $properties = [
+ 'content_type' => 'application/json',
+ 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
+ ];
+
+ $currentPayload = json_decode($payload, true);
+ if ($correlationId = $currentPayload['id'] ?? null) {
+ $properties['correlation_id'] = $correlationId;
+ }
- if ($this->declareExchange && !in_array($exchange, $this->declaredExchanges, true)) {
- // declare exchange
- $this->channel->exchange_declare(
- $exchange,
- $this->configExchange['type'],
- $this->configExchange['passive'],
- $this->configExchange['durable'],
- $this->configExchange['auto_delete']
- );
+ if ($this->getRabbitMQConfig()->isPrioritizeDelayed()) {
+ $properties['priority'] = $attempts;
+ }
+
+ if (isset($currentPayload['data']['command'])) {
+ // If the command data is encrypted, decrypt it first before attempting to unserialize
+ if (is_subclass_of($currentPayload['data']['commandName'], ShouldBeEncrypted::class)) {
+ $currentPayload['data']['command'] = Crypt::decrypt($currentPayload['data']['command']);
+ }
- $this->declaredExchanges[] = $exchange;
+ $commandData = unserialize($currentPayload['data']['command']);
+ if (property_exists($commandData, 'priority')) {
+ $properties['priority'] = $commandData->priority;
+ }
}
- if ($this->declareBindQueue && !in_array($name, $this->declaredQueues, true)) {
- // declare queue
- $this->channel->queue_declare(
- $name,
- $this->queueParameters['passive'],
- $this->queueParameters['durable'],
- $this->queueParameters['exclusive'],
- $this->queueParameters['auto_delete'],
- false,
- new AMQPTable($this->queueArguments)
- );
+ $message = new AMQPMessage($payload, $properties);
+
+ $message->set('application_headers', new AMQPTable([
+ 'laravel' => [
+ 'attempts' => $attempts,
+ ],
+ ]));
- // bind queue to the exchange
- $this->channel->queue_bind($name, $exchange, $name);
+ return [
+ $message,
+ $correlationId,
+ ];
+ }
- $this->declaredQueues[] = $name;
+ /**
+ * Create a payload array from the given job and data.
+ *
+ * @param string|object $job
+ * @param string $queue
+ * @param mixed $data
+ */
+ protected function createPayloadArray($job, $queue, $data = ''): array
+ {
+ return array_merge(parent::createPayloadArray($job, $queue, $data), [
+ 'id' => $this->getRandomId(),
+ ]);
+ }
+
+ /**
+ * Get a random ID string.
+ */
+ protected function getRandomId(): string
+ {
+ return Str::uuid();
+ }
+
+ /**
+ * Close the connection to RabbitMQ.
+ *
+ *
+ * @throws Exception
+ */
+ public function close(): void
+ {
+ if (isset($this->currentJob) && ! $this->currentJob->isDeletedOrReleased()) {
+ $this->reject($this->currentJob, true);
}
- return [$name, $exchange];
+ try {
+ $this->getConnection()->close();
+ } catch (ErrorException) {
+ // Ignore the exception
+ }
}
- private function declareDelayedQueue(string $destination, $delay): array
+ /**
+ * Get the Queue arguments.
+ */
+ protected function getQueueArguments(string $destination): array
{
- $delay = $this->secondsUntil($delay);
- $destination = $this->getQueueName($destination);
- $destinationExchange = $this->configExchange['name'] ?: $destination;
- $name = $this->getQueueName($destination) . '_deferred_' . $delay;
- $exchange = $this->configExchange['name'] ?: $destination;
+ $arguments = [];
+
+ // Messages without a priority property are treated as if their priority were 0.
+ // Messages with a priority which is higher than the queue's maximum, are treated as if they were
+ // published with the maximum priority.
+ // Quorum queues does not support priority.
+ if ($this->getRabbitMQConfig()->isPrioritizeDelayed() && ! $this->getRabbitMQConfig()->isQuorum()) {
+ $arguments['x-max-priority'] = $this->getRabbitMQConfig()->getQueueMaxPriority();
+ }
- // declare exchange
- if (!in_array($exchange, $this->declaredExchanges, true)) {
- $this->channel->exchange_declare(
- $exchange,
- $this->configExchange['type'],
- $this->configExchange['passive'],
- $this->configExchange['durable'],
- $this->configExchange['auto_delete']
- );
+ if ($this->getRabbitMQConfig()->isRerouteFailed()) {
+ $arguments['x-dead-letter-exchange'] = $this->getFailedExchange();
+ $arguments['x-dead-letter-routing-key'] = $this->getFailedRoutingKey($destination);
}
- // declare queue
- if (!in_array($name, $this->declaredQueues, true)) {
- $queueArguments = array_merge([
- 'x-dead-letter-exchange' => $destinationExchange,
- 'x-dead-letter-routing-key' => $destination,
- 'x-message-ttl' => $delay * 1000,
- ], (array)$this->queueArguments);
-
- $this->channel->queue_declare(
- $name,
- $this->queueParameters['passive'],
- $this->queueParameters['durable'],
- $this->queueParameters['exclusive'],
- $this->queueParameters['auto_delete'],
- false,
- new AMQPTable($queueArguments)
- );
+ if ($this->getRabbitMQConfig()->isQuorum()) {
+ $arguments['x-queue-type'] = 'quorum';
}
- // bind queue to the exchange
- $this->channel->queue_bind($name, $exchange, $name);
+ return $arguments;
+ }
- return [$name, $exchange];
+ /**
+ * Get the Delay queue arguments.
+ */
+ protected function getDelayQueueArguments(string $destination, int $ttl): array
+ {
+ return [
+ 'x-dead-letter-exchange' => $this->getExchange(),
+ 'x-dead-letter-routing-key' => $this->getRoutingKey($destination),
+ 'x-message-ttl' => $ttl,
+ 'x-expires' => $ttl * 2,
+ ];
}
/**
- * @param string $action
- * @param Exception $e
- * @throws Exception
+ * Get the exchange name, or empty string; as default value.
+ */
+ protected function getExchange(?string $exchange = null): string
+ {
+ return $exchange ?? $this->getRabbitMQConfig()->getExchange();
+ }
+
+ /**
+ * Get the routing-key for when you use exchanges
+ * The default routing-key is the given destination.
+ */
+ protected function getRoutingKey(string $destination): string
+ {
+ return ltrim(sprintf($this->getRabbitMQConfig()->getExchangeRoutingKey(), $destination), '.');
+ }
+
+ /**
+ * Get the exchangeType, or AMQPExchangeType::DIRECT as default.
+ */
+ protected function getExchangeType(?string $type = null): string
+ {
+ $constant = AMQPExchangeType::class.'::'.Str::upper($type ?: $this->getRabbitMQConfig()->getExchangeType());
+
+ return defined($constant) ? constant($constant) : AMQPExchangeType::DIRECT;
+ }
+
+ /**
+ * Get the exchange for failed messages.
+ */
+ protected function getFailedExchange(?string $exchange = null): string
+ {
+ return $exchange ?? $this->getRabbitMQConfig()->getFailedExchange();
+ }
+
+ /**
+ * Get the routing-key for failed messages
+ * The default routing-key is the given destination substituted by '.failed'.
+ */
+ protected function getFailedRoutingKey(string $destination): string
+ {
+ return ltrim(sprintf($this->getRabbitMQConfig()->getFailedRoutingKey(), $destination), '.');
+ }
+
+ /**
+ * Checks if the exchange was already declared.
+ */
+ protected function isExchangeDeclared(string $name): bool
+ {
+ return in_array($name, $this->exchanges, true);
+ }
+
+ /**
+ * Checks if the queue was already declared.
+ */
+ protected function isQueueDeclared(string $name): bool
+ {
+ return in_array($name, $this->queues, true);
+ }
+
+ /**
+ * Declare the destination when necessary.
+ *
+ * @throws AMQPProtocolChannelException
+ */
+ protected function declareDestination(string $destination, ?string $exchange = null, string $exchangeType = AMQPExchangeType::DIRECT): void
+ {
+ // When an exchange is provided and no exchange is present in RabbitMQ, create an exchange.
+ if ($exchange && ! $this->isExchangeExists($exchange)) {
+ $this->declareExchange($exchange, $exchangeType);
+ }
+
+ // When an exchange is provided, just return.
+ if ($exchange) {
+ return;
+ }
+
+ // When the queue already exists, just return.
+ if ($this->isQueueExists($destination)) {
+ return;
+ }
+
+ // Create a queue for amq.direct publishing.
+ $this->declareQueue($destination, true, false, $this->getQueueArguments($destination));
+ }
+
+ /**
+ * Determine all publish properties.
+ */
+ protected function publishProperties($queue, array $options = []): array
+ {
+ $queue = $this->getQueue($queue);
+ $attempts = Arr::get($options, 'attempts') ?: 0;
+
+ $destination = $this->getRoutingKey($queue);
+ $exchange = $this->getExchange(Arr::get($options, 'exchange'));
+ $exchangeType = $this->getExchangeType(Arr::get($options, 'exchange_type'));
+
+ return [$destination, $exchange, $exchangeType, $attempts];
+ }
+
+ protected function getRabbitMQConfig(): QueueConfig
+ {
+ return $this->rabbitMQConfig;
+ }
+
+ /**
+ * @throws AMQPChannelClosedException
+ * @throws AMQPConnectionClosedException
+ * @throws AMQPConnectionBlockedException
*/
- protected function reportConnectionError($action, Exception $e)
+ protected function publishBasic($msg, $exchange = '', $destination = '', $mandatory = false, $immediate = false, $ticket = null): void
{
- Log::error('AMQP error while attempting ' . $action . ': ' . $e->getMessage());
+ $this->getChannel()->basic_publish($msg, $exchange, $destination, $mandatory, $immediate, $ticket);
+ }
- // If it's set to false, throw an error rather than waiting
- if ($this->sleepOnError === false) {
- throw new RuntimeException('Error writing data to the connection with RabbitMQ');
+ protected function batchPublish(): void
+ {
+ $this->getChannel()->publish_batch();
+ }
+
+ public function getChannel($forceNew = false): AMQPChannel
+ {
+ if (! $this->channel || $forceNew) {
+ $this->channel = $this->createChannel();
}
- // Sleep so that we don't flood the log file
- sleep($this->sleepOnError);
+ return $this->channel;
+ }
+
+ protected function createChannel(): AMQPChannel
+ {
+ return $this->getConnection()->channel();
+ }
+
+ /**
+ * @throws Exception
+ */
+ protected function reconnect(): void
+ {
+ // Reconnects using the original connection settings.
+ $this->getConnection()->reconnect();
+ // Create a new main channel because all old channels are removed.
+ $this->getChannel(true);
}
}
diff --git a/tests/Feature/ConnectorTest.php b/tests/Feature/ConnectorTest.php
new file mode 100644
index 00000000..91660ad2
--- /dev/null
+++ b/tests/Feature/ConnectorTest.php
@@ -0,0 +1,187 @@
+app['config']->set('queue.connections.rabbitmq', [
+ 'driver' => 'rabbitmq',
+ 'queue' => env('RABBITMQ_QUEUE', 'default'),
+ 'connection' => AMQPLazyConnection::class,
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT'),
+ 'user' => 'guest',
+ 'password' => 'guest',
+ 'vhost' => '/',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => env('RABBITMQ_SSL_CAFILE', null),
+ 'local_cert' => env('RABBITMQ_SSL_LOCALCERT', null),
+ 'local_key' => env('RABBITMQ_SSL_LOCALKEY', null),
+ 'verify_peer' => env('RABBITMQ_SSL_VERIFY_PEER', true),
+ 'passphrase' => env('RABBITMQ_SSL_PASSPHRASE', null),
+ ],
+ ],
+
+ 'worker' => env('RABBITMQ_WORKER', 'default'),
+ ]);
+
+ /** @var QueueManager $queue */
+ $queue = $this->app['queue'];
+
+ /** @var RabbitMQQueue $connection */
+ $connection = $queue->connection('rabbitmq');
+
+ $this->assertInstanceOf(RabbitMQQueue::class, $connection);
+ $this->assertInstanceOf(AMQPLazyConnection::class, $connection->getConnection());
+ $this->assertFalse($connection->getConnection()->isConnected());
+ $this->assertTrue($connection->getChannel()->is_open());
+ $this->assertTrue($connection->getConnection()->isConnected());
+ }
+
+ public function testLazyStreamConnection(): void
+ {
+ $this->app['config']->set('queue.connections.rabbitmq', [
+ 'driver' => 'rabbitmq',
+ 'queue' => env('RABBITMQ_QUEUE', 'default'),
+ 'connection' => 'default',
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT'),
+ 'user' => 'guest',
+ 'password' => 'guest',
+ 'vhost' => '/',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => env('RABBITMQ_SSL_CAFILE', null),
+ 'local_cert' => env('RABBITMQ_SSL_LOCALCERT', null),
+ 'local_key' => env('RABBITMQ_SSL_LOCALKEY', null),
+ 'verify_peer' => env('RABBITMQ_SSL_VERIFY_PEER', true),
+ 'passphrase' => env('RABBITMQ_SSL_PASSPHRASE', null),
+ ],
+ ],
+
+ 'worker' => env('RABBITMQ_WORKER', 'default'),
+ ]);
+
+ /** @var QueueManager $queue */
+ $queue = $this->app['queue'];
+
+ /** @var RabbitMQQueue $connection */
+ $connection = $queue->connection('rabbitmq');
+
+ $this->assertInstanceOf(RabbitMQQueue::class, $connection);
+ $this->assertInstanceOf(AMQPStreamConnection::class, $connection->getConnection());
+ $this->assertFalse($connection->getConnection()->isConnected());
+ $this->assertTrue($connection->getChannel()->is_open());
+ $this->assertTrue($connection->getConnection()->isConnected());
+ }
+
+ public function testSslConnection(): void
+ {
+ $this->markTestSkipped();
+
+ $this->app['config']->set('queue.connections.rabbitmq', [
+ 'driver' => 'rabbitmq',
+ 'queue' => env('RABBITMQ_QUEUE', 'default'),
+ 'connection' => AMQPSSLConnection::class,
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT_SSL'),
+ 'user' => 'guest',
+ 'password' => 'guest',
+ 'vhost' => '/',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => getenv('RABBITMQ_SSL_CAFILE'),
+ 'local_cert' => null,
+ 'local_key' => null,
+ 'verify_peer' => true,
+ 'passphrase' => null,
+ ],
+ ],
+
+ 'worker' => env('RABBITMQ_WORKER', 'default'),
+ ]);
+
+ /** @var QueueManager $queue */
+ $queue = $this->app['queue'];
+
+ /** @var RabbitMQQueue $connection */
+ $connection = $queue->connection('rabbitmq');
+ $this->assertInstanceOf(RabbitMQQueue::class, $connection);
+ $this->assertInstanceOf(AMQPSSLConnection::class, $connection->getConnection());
+ $this->assertTrue($connection->getConnection()->isConnected());
+ $this->assertTrue($connection->getChannel()->is_open());
+ }
+
+ // Test to validate ssl connection params
+ public function testNoVerificationSslConnection(): void
+ {
+ $this->app['config']->set('queue.connections.rabbitmq', [
+ 'driver' => 'rabbitmq',
+ 'queue' => env('RABBITMQ_QUEUE', 'default'),
+ 'connection' => TestSSLConnection::class,
+ 'secure' => true,
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT_SSL'),
+ 'user' => 'guest',
+ 'password' => 'guest',
+ 'vhost' => '/',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => getenv('RABBITMQ_SSL_CAFILE'),
+ 'local_cert' => null,
+ 'local_key' => null,
+ 'verify_peer' => false,
+ 'passphrase' => null,
+ ],
+ ],
+
+ 'worker' => env('RABBITMQ_WORKER', 'default'),
+ ]);
+
+ /** @var QueueManager $queue */
+ $queue = $this->app['queue'];
+
+ /** @var RabbitMQQueue $connection */
+ $connection = $queue->connection('rabbitmq');
+ $this->assertInstanceOf(RabbitMQQueue::class, $connection);
+ $this->assertInstanceOf(AMQPSSLConnection::class, $connection->getConnection());
+ /** @var AMQPConnectionConfig */
+ $config = $connection->getConnection()->getConfig();
+ $this->assertFalse($config->getSslVerify());
+ }
+}
diff --git a/tests/Feature/QueueTest.php b/tests/Feature/QueueTest.php
new file mode 100644
index 00000000..ee324c9a
--- /dev/null
+++ b/tests/Feature/QueueTest.php
@@ -0,0 +1,43 @@
+withoutExceptionHandling([
+ AMQPChannelClosedException::class, AMQPConnectionClosedException::class,
+ AMQPProtocolChannelException::class,
+ ]);
+ }
+
+ public function testConnection(): void
+ {
+ $this->assertInstanceOf(AMQPStreamConnection::class, $this->connection()->getChannel()->getConnection());
+ }
+
+ public function testWithoutReconnect(): void
+ {
+ $queue = $this->connection('rabbitmq');
+
+ $queue->push(new TestJob);
+ sleep(1);
+ $this->assertSame(1, $queue->size());
+
+ // close connection
+ $queue->getConnection()->close();
+ $this->assertFalse($queue->getConnection()->isConnected());
+
+ $this->expectException(AMQPChannelClosedException::class);
+ $queue->push(new TestJob);
+ }
+}
diff --git a/tests/Feature/SslQueueTest.php b/tests/Feature/SslQueueTest.php
new file mode 100644
index 00000000..11c93f5a
--- /dev/null
+++ b/tests/Feature/SslQueueTest.php
@@ -0,0 +1,54 @@
+markTestSkipped();
+ }
+
+ protected function getEnvironmentSetUp($app): void
+ {
+ $app['config']->set('queue.default', 'rabbitmq');
+ $app['config']->set('queue.connections.rabbitmq', [
+ 'driver' => 'rabbitmq',
+ 'queue' => 'default',
+ 'connection' => AMQPSSLConnection::class,
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT_SSL'),
+ 'vhost' => '/',
+ 'user' => 'guest',
+ 'password' => 'guest',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => getenv('RABBITMQ_SSL_CAFILE'),
+ 'local_cert' => null,
+ 'local_key' => null,
+ 'verify_peer' => true,
+ 'passphrase' => null,
+ ],
+ ],
+
+ 'worker' => 'default',
+ ]);
+ }
+
+ public function testConnection(): void
+ {
+ $this->assertInstanceOf(AMQPSSLConnection::class, $this->connection()->getChannel()->getConnection());
+ }
+}
diff --git a/tests/Feature/TestCase.php b/tests/Feature/TestCase.php
new file mode 100644
index 00000000..f8a9cb05
--- /dev/null
+++ b/tests/Feature/TestCase.php
@@ -0,0 +1,458 @@
+interactsWithConnection) {
+ if ($this->connection()->isQueueExists()) {
+ $this->connection()->purge();
+ }
+ }
+ }
+
+ /**
+ * @throws AMQPProtocolChannelException
+ */
+ protected function tearDown(): void
+ {
+ if ($this->interactsWithConnection) {
+ if ($this->connection()->isQueueExists()) {
+ $this->connection()->purge();
+ }
+
+ self::assertSame(0, Queue::size());
+ }
+
+ parent::tearDown();
+ }
+
+ public function testSizeDoesNotThrowExceptionOnUnknownQueue(): void
+ {
+ $this->assertEmpty(0, Queue::size(Str::random()));
+ }
+
+ public function testPopNothing(): void
+ {
+ $this->assertNull(Queue::pop('foo'));
+ }
+
+ public function testPushRaw(): void
+ {
+ Queue::pushRaw($payload = Str::random());
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+ $this->assertSame(1, $job->attempts());
+ $this->assertInstanceOf(RabbitMQJob::class, $job);
+ $this->assertSame($payload, $job->getRawBody());
+
+ $this->assertNull($job->getJobId());
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testPush(): void
+ {
+ Queue::push(new TestJob);
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+ $this->assertSame(1, $job->attempts());
+ $this->assertInstanceOf(RabbitMQJob::class, $job);
+ $this->assertSame(TestJob::class, $job->resolveName());
+ $this->assertNotNull($job->getJobId());
+
+ $payload = $job->payload();
+
+ $this->assertSame(TestJob::class, $payload['displayName']);
+ $this->assertSame('Illuminate\Queue\CallQueuedHandler@call', $payload['job']);
+ $this->assertNull($payload['maxTries']);
+ $this->assertNull($payload['backoff']);
+ $this->assertNull($payload['timeout']);
+ $this->assertNull($payload['retryUntil']);
+ $this->assertSame($job->getJobId(), $payload['id']);
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testPushAfterCommit(): void
+ {
+ $transaction = new DatabaseTransactionsManager;
+
+ $this->app->singleton('db.transactions', function ($app) use ($transaction) {
+ $transaction->begin('FakeDBConnection', 1);
+
+ return $transaction;
+ });
+
+ TestJob::dispatch()->afterCommit();
+
+ sleep(1);
+ $this->assertSame(0, Queue::size());
+ $this->assertNull(Queue::pop());
+
+ $transaction->commit('FakeDBConnection', 1, 0);
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testLaterRaw(): void
+ {
+ $payload = Str::random();
+ $data = [Str::random() => Str::random()];
+
+ Queue::later(3, $payload, $data);
+
+ sleep(1);
+
+ $this->assertSame(0, Queue::size());
+ $this->assertNull(Queue::pop());
+
+ sleep(3);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+
+ $this->assertInstanceOf(RabbitMQJob::class, $job);
+ $this->assertSame($payload, $job->getName());
+
+ $body = json_decode($job->getRawBody(), true);
+
+ $this->assertSame($payload, $body['displayName']);
+ $this->assertSame($payload, $body['job']);
+ $this->assertSame($data, $body['data']);
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testLater(): void
+ {
+ Queue::later(3, new TestJob);
+
+ sleep(1);
+
+ $this->assertSame(0, Queue::size());
+ $this->assertNull(Queue::pop());
+
+ sleep(3);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+
+ $this->assertInstanceOf(RabbitMQJob::class, $job);
+
+ $body = json_decode($job->getRawBody(), true);
+
+ $this->assertSame(TestJob::class, $body['displayName']);
+ $this->assertSame('Illuminate\Queue\CallQueuedHandler@call', $body['job']);
+ $this->assertSame(TestJob::class, $body['data']['commandName']);
+ $this->assertNotNull($job->getJobId());
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testBulk(): void
+ {
+ $count = 100;
+ $jobs = [];
+
+ for ($i = 0; $i < $count; $i++) {
+ $jobs[$i] = new TestJob($i);
+ }
+
+ Queue::bulk($jobs);
+
+ sleep(1);
+
+ $this->assertSame($count, Queue::size());
+ }
+
+ public function testPushEncrypted(): void
+ {
+ Queue::push(new TestEncryptedJob);
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+ $this->assertSame(1, $job->attempts());
+ $this->assertInstanceOf(RabbitMQJob::class, $job);
+ $this->assertSame(TestEncryptedJob::class, $job->resolveName());
+ $this->assertNotNull($job->getJobId());
+
+ $payload = $job->payload();
+
+ $this->assertSame(TestEncryptedJob::class, $payload['displayName']);
+ $this->assertSame('Illuminate\Queue\CallQueuedHandler@call', $payload['job']);
+ $this->assertNull($payload['maxTries']);
+ $this->assertNull($payload['backoff']);
+ $this->assertNull($payload['timeout']);
+ $this->assertNull($payload['retryUntil']);
+ $this->assertSame($job->getJobId(), $payload['id']);
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testPushEncryptedAfterCommit(): void
+ {
+ $transaction = new DatabaseTransactionsManager;
+
+ $this->app->singleton('db.transactions', function ($app) use ($transaction) {
+ $transaction->begin('FakeDBConnection', 1);
+
+ return $transaction;
+ });
+
+ TestEncryptedJob::dispatch()->afterCommit();
+
+ sleep(1);
+ $this->assertSame(0, Queue::size());
+ $this->assertNull(Queue::pop());
+
+ $transaction->commit('FakeDBConnection', 1, 0);
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testEncryptedLater(): void
+ {
+ Queue::later(3, new TestEncryptedJob);
+
+ sleep(1);
+
+ $this->assertSame(0, Queue::size());
+ $this->assertNull(Queue::pop());
+
+ sleep(3);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+
+ $this->assertInstanceOf(RabbitMQJob::class, $job);
+
+ $body = json_decode($job->getRawBody(), true);
+
+ $this->assertSame(TestEncryptedJob::class, $body['displayName']);
+ $this->assertSame('Illuminate\Queue\CallQueuedHandler@call', $body['job']);
+ $this->assertSame(TestEncryptedJob::class, $body['data']['commandName']);
+ $this->assertNotNull($job->getJobId());
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testEncryptedBulk(): void
+ {
+ $count = 100;
+ $jobs = [];
+
+ for ($i = 0; $i < $count; $i++) {
+ $jobs[$i] = new TestEncryptedJob($i);
+ }
+
+ Queue::bulk($jobs);
+
+ sleep(1);
+
+ $this->assertSame($count, Queue::size());
+ }
+
+ public function testReleaseRaw(): void
+ {
+ Queue::pushRaw($payload = Str::random());
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+ $this->assertSame(1, $job->attempts());
+
+ for ($attempt = 2; $attempt <= 4; $attempt++) {
+ $job->release();
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+
+ $job = Queue::pop();
+
+ $this->assertSame($attempt, $job->attempts());
+ }
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testRelease(): void
+ {
+ Queue::push(new TestJob);
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+ $this->assertSame(1, $job->attempts());
+
+ for ($attempt = 2; $attempt <= 4; $attempt++) {
+ $job->release();
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+
+ $job = Queue::pop();
+
+ $this->assertSame($attempt, $job->attempts());
+ }
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testReleaseWithDelayRaw(): void
+ {
+ Queue::pushRaw($payload = Str::random());
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+ $this->assertSame(1, $job->attempts());
+
+ for ($attempt = 2; $attempt <= 4; $attempt++) {
+ $job->release(4);
+
+ sleep(1);
+
+ $this->assertSame(0, Queue::size());
+ $this->assertNull(Queue::pop());
+
+ sleep(4);
+
+ $this->assertSame(1, Queue::size());
+
+ $job = Queue::pop();
+
+ $this->assertSame($attempt, $job->attempts());
+ }
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testReleaseInThePast(): void
+ {
+ Queue::push(new TestJob);
+
+ $job = Queue::pop();
+ $job->release(-3);
+
+ sleep(1);
+
+ $this->assertInstanceOf(RabbitMQJob::class, $job = Queue::pop());
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testReleaseAndReleaseWithDelayAttempts(): void
+ {
+ Queue::push(new TestJob);
+
+ sleep(1);
+
+ $this->assertSame(1, Queue::size());
+ $this->assertNotNull($job = Queue::pop());
+
+ $job->release();
+
+ sleep(1);
+
+ $this->assertNotNull($job = Queue::pop());
+ $this->assertSame(2, $job->attempts());
+
+ $job->release(3);
+
+ sleep(4);
+
+ $this->assertNotNull($job = Queue::pop());
+ $this->assertSame(3, $job->attempts());
+
+ $job->delete();
+ $this->assertSame(0, Queue::size());
+ }
+
+ public function testDelete(): void
+ {
+ Queue::push(new TestJob);
+
+ $job = Queue::pop();
+
+ $job->delete();
+
+ sleep(1);
+
+ $this->assertSame(0, Queue::size());
+ $this->assertNull(Queue::pop());
+ }
+
+ public function testFailed(): void
+ {
+ Queue::push(new TestJob);
+
+ $job = Queue::pop();
+
+ $job->fail(new RuntimeException($job->resolveName().' has an exception.'));
+
+ sleep(1);
+
+ $this->assertSame(true, $job->hasFailed());
+ $this->assertSame(true, $job->isDeleted());
+ $this->assertSame(0, Queue::size());
+ $this->assertNull(Queue::pop());
+ }
+}
diff --git a/tests/Functional/RabbitMQQueueTest.php b/tests/Functional/RabbitMQQueueTest.php
new file mode 100644
index 00000000..f3ca4005
--- /dev/null
+++ b/tests/Functional/RabbitMQQueueTest.php
@@ -0,0 +1,313 @@
+connection();
+ $this->assertInstanceOf(RabbitMQQueue::class, $queue);
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertInstanceOf(RabbitMQQueue::class, $queue);
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertInstanceOf(RabbitMQQueue::class, $queue);
+ }
+
+ public function testConfigRerouteFailed(): void
+ {
+ $queue = $this->connection();
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isRerouteFailed());
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertTrue($this->callProperty($queue, 'rabbitMQConfig')->isRerouteFailed());
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isRerouteFailed());
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isRerouteFailed());
+ }
+
+ public function testConfigPrioritizeDelayed(): void
+ {
+ $queue = $this->connection();
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isPrioritizeDelayed());
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertTrue($this->callProperty($queue, 'rabbitMQConfig')->isPrioritizeDelayed());
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isPrioritizeDelayed());
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isPrioritizeDelayed());
+ }
+
+ public function testQueueMaxPriority(): void
+ {
+ $queue = $this->connection();
+ $this->assertIsInt($this->callProperty($queue, 'rabbitMQConfig')->getQueueMaxPriority());
+ $this->assertSame(2, $this->callProperty($queue, 'rabbitMQConfig')->getQueueMaxPriority());
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertIsInt($this->callProperty($queue, 'rabbitMQConfig')->getQueueMaxPriority());
+ $this->assertSame(20, $this->callProperty($queue, 'rabbitMQConfig')->getQueueMaxPriority());
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertIsInt($this->callProperty($queue, 'rabbitMQConfig')->getQueueMaxPriority());
+ $this->assertSame(2, $this->callProperty($queue, 'rabbitMQConfig')->getQueueMaxPriority());
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertIsInt($this->callProperty($queue, 'rabbitMQConfig')->getQueueMaxPriority());
+ $this->assertSame(2, $this->callProperty($queue, 'rabbitMQConfig')->getQueueMaxPriority());
+ }
+
+ public function testConfigExchangeType(): void
+ {
+ $queue = $this->connection();
+ $this->assertSame(AMQPExchangeType::DIRECT, $this->callMethod($queue, 'getExchangeType'));
+ $this->assertSame(AMQPExchangeType::DIRECT, $this->callMethod($queue, 'getExchangeType', ['']));
+ $this->assertSame(AMQPExchangeType::TOPIC, $this->callMethod($queue, 'getExchangeType', ['topic']));
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertSame(AMQPExchangeType::TOPIC, $this->callMethod($queue, 'getExchangeType'));
+ $this->assertSame(AMQPExchangeType::DIRECT, $this->callMethod($queue, 'getExchangeType', ['direct']));
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertSame(AMQPExchangeType::DIRECT, $this->callMethod($queue, 'getExchangeType'));
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertSame(AMQPExchangeType::DIRECT, $this->callMethod($queue, 'getExchangeType'));
+
+ // testing an unkown type with a default
+ $this->callProperty($queue, 'rabbitMQConfig')->setExchangeType('unknown');
+ $this->assertSame(AMQPExchangeType::DIRECT, $this->callMethod($queue, 'getExchangeType'));
+ }
+
+ public function testExchange(): void
+ {
+ $queue = $this->connection();
+ $this->assertSame('test', $this->callMethod($queue, 'getExchange', ['test']));
+ $this->assertSame('', $this->callMethod($queue, 'getExchange', ['']));
+ $this->assertSame('', $this->callMethod($queue, 'getExchange', [null]));
+ $this->assertSame('', $this->callMethod($queue, 'getExchange'));
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertSame('application-x', $this->callMethod($queue, 'getExchange'));
+ $this->assertSame('application-x', $this->callMethod($queue, 'getExchange', [null]));
+ $this->assertSame('test', $this->callMethod($queue, 'getExchange', ['test']));
+ $this->assertSame('', $this->callMethod($queue, 'getExchange', ['']));
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertSame('', $this->callMethod($queue, 'getExchange'));
+ $this->assertSame('', $this->callMethod($queue, 'getExchange', [null]));
+ $this->assertSame('test', $this->callMethod($queue, 'getExchange', ['test']));
+ $this->assertSame('', $this->callMethod($queue, 'getExchange', ['']));
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertSame('', $this->callMethod($queue, 'getExchange'));
+ $this->assertSame('', $this->callMethod($queue, 'getExchange', [null]));
+ $this->assertSame('test', $this->callMethod($queue, 'getExchange', ['test']));
+ $this->assertSame('', $this->callMethod($queue, 'getExchange', ['']));
+ }
+
+ public function testFailedExchange(): void
+ {
+ $queue = $this->connection();
+ $this->assertSame('test', $this->callMethod($queue, 'getFailedExchange', ['test']));
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange', ['']));
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange', [null]));
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange'));
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertSame('failed-exchange', $this->callMethod($queue, 'getFailedExchange'));
+ $this->assertSame('failed-exchange', $this->callMethod($queue, 'getFailedExchange', [null]));
+ $this->assertSame('test', $this->callMethod($queue, 'getFailedExchange', ['test']));
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange', ['']));
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange'));
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange', [null]));
+ $this->assertSame('test', $this->callMethod($queue, 'getFailedExchange', ['test']));
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange', ['']));
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange'));
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange', [null]));
+ $this->assertSame('test', $this->callMethod($queue, 'getFailedExchange', ['test']));
+ $this->assertSame('', $this->callMethod($queue, 'getFailedExchange', ['']));
+ }
+
+ public function testRoutingKey(): void
+ {
+ $queue = $this->connection();
+ $this->assertSame('test', $this->callMethod($queue, 'getRoutingKey', ['test']));
+ $this->assertSame('test', $this->callMethod($queue, 'getRoutingKey', ['.test']));
+ $this->assertSame('', $this->callMethod($queue, 'getRoutingKey', ['']));
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertSame('process.test', $this->callMethod($queue, 'getRoutingKey', ['test']));
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertSame('test', $this->callMethod($queue, 'getRoutingKey', ['test']));
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertSame('test', $this->callMethod($queue, 'getRoutingKey', ['test']));
+ $this->callProperty($queue, 'rabbitMQConfig')->setExchangeRoutingKey('.an.alternate.routing-key');
+ $this->assertSame('an.alternate.routing-key', $this->callMethod($queue, 'getRoutingKey', ['test']));
+ }
+
+ public function testFailedRoutingKey(): void
+ {
+ $queue = $this->connection();
+ $this->assertSame('test.failed', $this->callMethod($queue, 'getFailedRoutingKey', ['test']));
+ $this->assertSame('test.failed', $this->callMethod($queue, 'getFailedRoutingKey', ['.test']));
+ $this->assertSame('failed', $this->callMethod($queue, 'getFailedRoutingKey', ['']));
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertSame('application-x.test.failed', $this->callMethod($queue, 'getFailedRoutingKey', ['test']));
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertSame('test.failed', $this->callMethod($queue, 'getFailedRoutingKey', ['test']));
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertSame('test.failed', $this->callMethod($queue, 'getFailedRoutingKey', ['test']));
+ $this->callProperty($queue, 'rabbitMQConfig')->setFailedRoutingKey('.an.alternate.routing-key');
+ $this->assertSame('an.alternate.routing-key', $this->callMethod($queue, 'getFailedRoutingKey', ['test']));
+ }
+
+ public function testConfigQuorum(): void
+ {
+ $queue = $this->connection();
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isQuorum());
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isQuorum());
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isQuorum());
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertFalse($this->callProperty($queue, 'rabbitMQConfig')->isQuorum());
+
+ $queue = $this->connection('rabbitmq-with-quorum-options');
+ $this->assertTrue($this->callProperty($queue, 'rabbitMQConfig')->isQuorum());
+ }
+
+ public function testDeclareDeleteExchange(): void
+ {
+ $queue = $this->connection();
+
+ $name = Str::random();
+
+ $this->assertFalse($queue->isExchangeExists($name));
+
+ $queue->declareExchange($name);
+ $this->assertTrue($queue->isExchangeExists($name));
+
+ $queue->deleteExchange($name);
+ $this->assertFalse($queue->isExchangeExists($name));
+ }
+
+ public function testDeclareDeleteQueue(): void
+ {
+ $queue = $this->connection();
+
+ $name = Str::random();
+
+ $this->assertFalse($queue->isQueueExists($name));
+
+ $queue->declareQueue($name);
+ $this->assertTrue($queue->isQueueExists($name));
+
+ $queue->deleteQueue($name);
+ $this->assertFalse($queue->isQueueExists($name));
+ }
+
+ public function testQueueArguments(): void
+ {
+ $name = Str::random();
+
+ $queue = $this->connection();
+ $actual = $this->callMethod($queue, 'getQueueArguments', [$name]);
+ $expected = [];
+ $this->assertEquals(array_keys($expected), array_keys($actual));
+ $this->assertEquals(array_values($expected), array_values($actual));
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $actual = $this->callMethod($queue, 'getQueueArguments', [$name]);
+ $expected = [
+ 'x-max-priority' => 20,
+ 'x-dead-letter-exchange' => 'failed-exchange',
+ 'x-dead-letter-routing-key' => sprintf('application-x.%s.failed', $name),
+ ];
+
+ $this->assertEquals(array_keys($expected), array_keys($actual));
+ $this->assertEquals(array_values($expected), array_values($actual));
+
+ $queue = $this->connection('rabbitmq-with-quorum-options');
+ $actual = $this->callMethod($queue, 'getQueueArguments', [$name]);
+ $expected = [
+ 'x-dead-letter-exchange' => 'failed-exchange',
+ 'x-dead-letter-routing-key' => sprintf('application-x.%s.failed', $name),
+ 'x-queue-type' => 'quorum',
+ ];
+
+ $this->assertEquals(array_keys($expected), array_keys($actual));
+ $this->assertEquals(array_values($expected), array_values($actual));
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $actual = $this->callMethod($queue, 'getQueueArguments', [$name]);
+ $expected = [];
+
+ $this->assertEquals(array_keys($expected), array_keys($actual));
+ $this->assertEquals(array_values($expected), array_values($actual));
+ }
+
+ public function testDelayQueueArguments(): void
+ {
+ $name = Str::random();
+ $ttl = 12000;
+
+ $queue = $this->connection();
+ $actual = $this->callMethod($queue, 'getDelayQueueArguments', [$name, $ttl]);
+ $expected = [
+ 'x-dead-letter-exchange' => '',
+ 'x-dead-letter-routing-key' => $name,
+ 'x-message-ttl' => $ttl,
+ 'x-expires' => $ttl * 2,
+ ];
+ $this->assertEquals(array_keys($expected), array_keys($actual));
+ $this->assertEquals(array_values($expected), array_values($actual));
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $actual = $this->callMethod($queue, 'getDelayQueueArguments', [$name, $ttl]);
+ $expected = [
+ 'x-dead-letter-exchange' => 'application-x',
+ 'x-dead-letter-routing-key' => sprintf('process.%s', $name),
+ 'x-message-ttl' => $ttl,
+ 'x-expires' => $ttl * 2,
+ ];
+ $this->assertEquals(array_keys($expected), array_keys($actual));
+ $this->assertEquals(array_values($expected), array_values($actual));
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $actual = $this->callMethod($queue, 'getDelayQueueArguments', [$name, $ttl]);
+ $expected = [
+ 'x-dead-letter-exchange' => '',
+ 'x-dead-letter-routing-key' => $name,
+ 'x-message-ttl' => $ttl,
+ 'x-expires' => $ttl * 2,
+ ];
+ $this->assertEquals(array_keys($expected), array_keys($actual));
+ $this->assertEquals(array_values($expected), array_values($actual));
+ }
+}
diff --git a/tests/Functional/TestCase.php b/tests/Functional/TestCase.php
new file mode 100644
index 00000000..8b843561
--- /dev/null
+++ b/tests/Functional/TestCase.php
@@ -0,0 +1,270 @@
+set('queue.default', 'rabbitmq');
+ $app['config']->set('queue.connections.rabbitmq', [
+ 'driver' => 'rabbitmq',
+ 'queue' => 'order',
+ 'connection' => 'default',
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT'),
+ 'vhost' => '/',
+ 'user' => 'guest',
+ 'password' => 'guest',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => null,
+ 'local_cert' => null,
+ 'local_key' => null,
+ 'verify_peer' => true,
+ 'passphrase' => null,
+ ],
+ ],
+
+ 'worker' => 'default',
+
+ ]);
+ $app['config']->set('queue.connections.rabbitmq-with-options', [
+ 'driver' => 'rabbitmq',
+ 'queue' => 'order',
+ 'connection' => 'default',
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT'),
+ 'vhost' => '/',
+ 'user' => 'guest',
+ 'password' => 'guest',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => null,
+ 'local_cert' => null,
+ 'local_key' => null,
+ 'verify_peer' => true,
+ 'passphrase' => null,
+ ],
+
+ 'queue' => [
+ 'prioritize_delayed' => true,
+ 'queue_max_priority' => 20,
+ 'exchange' => 'application-x',
+ 'exchange_type' => 'topic',
+ 'exchange_routing_key' => 'process.%s',
+ 'reroute_failed' => true,
+ 'failed_exchange' => 'failed-exchange',
+ 'failed_routing_key' => 'application-x.%s.failed',
+ ],
+ ],
+
+ 'worker' => 'default',
+
+ ]);
+ $app['config']->set('queue.connections.rabbitmq-with-options-empty', [
+ 'driver' => 'rabbitmq',
+ 'queue' => 'order',
+ 'connection' => 'default',
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT'),
+ 'vhost' => '/',
+ 'user' => 'guest',
+ 'password' => 'guest',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => null,
+ 'local_cert' => null,
+ 'local_key' => null,
+ 'verify_peer' => true,
+ 'passphrase' => null,
+ ],
+
+ 'queue' => [
+ 'prioritize_delayed' => '',
+ 'queue_max_priority' => '',
+ 'exchange' => '',
+ 'exchange_type' => '',
+ 'exchange_routing_key' => '',
+ 'reroute_failed' => '',
+ 'failed_exchange' => '',
+ 'failed_routing_key' => '',
+ 'quorum' => '',
+ ],
+ ],
+
+ 'worker' => 'default',
+
+ ]);
+ $app['config']->set('queue.connections.rabbitmq-with-options-null', [
+ 'driver' => 'rabbitmq',
+ 'queue' => 'order',
+ 'connection' => 'default',
+
+ 'hosts' => [
+ [
+ 'host' => null,
+ 'port' => null,
+ 'vhost' => null,
+ 'user' => null,
+ 'password' => null,
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => null,
+ 'local_cert' => null,
+ 'local_key' => null,
+ 'verify_peer' => null,
+ 'passphrase' => null,
+ ],
+
+ 'queue' => [
+ 'prioritize_delayed' => null,
+ 'queue_max_priority' => null,
+ 'exchange' => null,
+ 'exchange_type' => null,
+ 'exchange_routing_key' => null,
+ 'reroute_failed' => null,
+ 'failed_exchange' => null,
+ 'failed_routing_key' => null,
+ 'quorum' => null,
+ ],
+ ],
+
+ 'worker' => 'default',
+
+ ]);
+ $app['config']->set('queue.connections.rabbitmq-with-quorum-options', [
+ 'driver' => 'rabbitmq',
+ 'queue' => 'order',
+ 'connection' => 'default',
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT'),
+ 'vhost' => '/',
+ 'user' => 'guest',
+ 'password' => 'guest',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => null,
+ 'local_cert' => null,
+ 'local_key' => null,
+ 'verify_peer' => true,
+ 'passphrase' => null,
+ ],
+
+ 'queue' => [
+ 'exchange' => 'application-x',
+ 'exchange_type' => 'topic',
+ 'exchange_routing_key' => 'process.%s',
+ 'reroute_failed' => true,
+ 'failed_exchange' => 'failed-exchange',
+ 'failed_routing_key' => 'application-x.%s.failed',
+ 'quorum' => true,
+ ],
+ ],
+
+ 'worker' => 'default',
+
+ ]);
+ }
+
+ /**
+ * @throws Exception
+ */
+ protected function callMethod($object, string $method, array $parameters = []): mixed
+ {
+ try {
+ $className = get_class($object);
+ $reflection = new ReflectionClass($className);
+ } catch (ReflectionException $e) {
+ throw new Exception($e->getMessage());
+ }
+
+ $method = $reflection->getMethod($method);
+ $method->setAccessible(true);
+
+ return $method->invokeArgs($object, $parameters);
+ }
+
+ /**
+ * @throws Exception
+ */
+ protected function callProperty($object, string $property): mixed
+ {
+ try {
+ $className = get_class($object);
+ $reflection = new ReflectionClass($className);
+ } catch (ReflectionException $e) {
+ throw new Exception($e->getMessage());
+ }
+
+ $property = $reflection->getProperty($property);
+ $property->setAccessible(true);
+
+ return $property->getValue($object);
+ }
+
+ public function testConnectChannel(): void
+ {
+ $queue = $this->connection();
+ $this->assertFalse($queue->getConnection()->isConnected());
+
+ /** @var AMQPChannel $channel */
+ $channel = $this->callMethod($queue, 'getChannel');
+ $this->assertTrue($queue->getConnection()->isConnected());
+ $this->assertSame($channel, $this->callProperty($queue, 'channel'));
+ $this->assertTrue($channel->is_open());
+ }
+
+ public function testReconnect(): void
+ {
+ $queue = $this->connection();
+ $this->assertFalse($queue->getConnection()->isConnected());
+
+ // connect
+ $channel = $this->callMethod($queue, 'getChannel');
+ $this->assertTrue($queue->getConnection()->isConnected());
+ $this->assertSame($channel, $this->callProperty($queue, 'channel'));
+
+ // close
+ $queue->getConnection()->close();
+ $this->assertFalse($queue->getConnection()->isConnected());
+
+ // reconnect
+ $this->callMethod($queue, 'reconnect');
+ $this->assertTrue($queue->getConnection()->isConnected());
+ $this->assertTrue($queue->getChannel()->is_open());
+ }
+}
diff --git a/tests/Mocks/TestEncryptedJob.php b/tests/Mocks/TestEncryptedJob.php
new file mode 100644
index 00000000..1c0e1762
--- /dev/null
+++ b/tests/Mocks/TestEncryptedJob.php
@@ -0,0 +1,25 @@
+i = $i;
+ }
+
+ public function handle(): void
+ {
+ //
+ }
+}
diff --git a/tests/Mocks/TestJob.php b/tests/Mocks/TestJob.php
new file mode 100644
index 00000000..f97c7e12
--- /dev/null
+++ b/tests/Mocks/TestJob.php
@@ -0,0 +1,24 @@
+i = $i;
+ }
+
+ public function handle(): void
+ {
+ //
+ }
+}
diff --git a/tests/Mocks/TestSSLConnection.php b/tests/Mocks/TestSSLConnection.php
new file mode 100644
index 00000000..c1586475
--- /dev/null
+++ b/tests/Mocks/TestSSLConnection.php
@@ -0,0 +1,14 @@
+config;
+ }
+}
diff --git a/tests/RabbitMQConnectorSSLTest.php b/tests/RabbitMQConnectorSSLTest.php
deleted file mode 100644
index 10b27d90..00000000
--- a/tests/RabbitMQConnectorSSLTest.php
+++ /dev/null
@@ -1,48 +0,0 @@
- getenv('HOST'),
- 'port' => getenv('PORT_SSL'),
- 'login' => 'guest',
- 'password' => 'guest',
- 'vhost' => '/',
-
- 'queue' => 'queue_name',
- 'exchange_declare' => true,
- 'queue_declare_bind' => true,
-
- 'queue_params' => [
- 'passive' => false,
- 'durable' => true,
- 'exclusive' => false,
- 'auto_delete' => false,
- 'arguments' => null,
- ],
- 'exchange_params' => [
- 'name' => null,
- 'type' => 'direct',
- 'passive' => false,
- 'durable' => true,
- 'auto_delete' => false,
- ],
- 'ssl_params' => [
- 'cafile' => getenv('RABBITMQ_SSL_CAFILE')
- ]
- ];
-
- $connector = new RabbitMQConnectorSSL();
- $queue = $connector->connect($config);
-
- $this->assertInstanceOf(RabbitMQQueue::class, $queue);
- $this->assertInstanceOf(AMQPSSLConnection::class, $connector->connection());
- }
-}
diff --git a/tests/RabbitMQConnectorTest.php b/tests/RabbitMQConnectorTest.php
deleted file mode 100644
index 4cd76564..00000000
--- a/tests/RabbitMQConnectorTest.php
+++ /dev/null
@@ -1,45 +0,0 @@
- getenv('HOST'),
- 'port' => getenv('PORT'),
- 'login' => 'guest',
- 'password' => 'guest',
- 'vhost' => '/',
-
- 'queue' => 'queue_name',
- 'exchange_declare' => true,
- 'queue_declare_bind' => true,
-
- 'queue_params' => [
- 'passive' => false,
- 'durable' => true,
- 'exclusive' => false,
- 'auto_delete' => false,
- 'arguments' => null,
- ],
- 'exchange_params' => [
- 'name' => null,
- 'type' => 'direct',
- 'passive' => false,
- 'durable' => true,
- 'auto_delete' => false,
- ],
- ];
-
- $connector = new RabbitMQConnector();
- $queue = $connector->connect($config);
-
- $this->assertInstanceOf(RabbitMQQueue::class, $queue);
- $this->assertInstanceOf(AMQPStreamConnection::class, $connector->connection());
- }
-}
diff --git a/tests/RabbitMQQueueTest.php b/tests/RabbitMQQueueTest.php
deleted file mode 100644
index a4c897a5..00000000
--- a/tests/RabbitMQQueueTest.php
+++ /dev/null
@@ -1,196 +0,0 @@
-connection = Mockery::mock(AMQPStreamConnection::class);
- $this->channel = Mockery::mock(AMQPChannel::class);
-
- $this->connection->shouldReceive('channel')->andReturn($this->channel);
-
- $this->config = [
- 'queue' => str_random(),
- 'queue_params' => [
- 'passive' => false,
- 'durable' => true,
- 'exclusive' => false,
- 'auto_delete' => false,
- 'arguments' => null,
- ],
- 'exchange_params' => [
- 'name' => 'exchange_name',
- 'type' => 'direct',
- 'passive' => false,
- 'durable' => true,
- 'auto_delete' => false,
- ],
- 'exchange_declare' => true,
- 'queue_declare_bind' => true,
- ];
-
- $this->queue = new RabbitMQQueue($this->connection, $this->config);
- }
-
- public function testSize()
- {
- $messageCount = 5;
- $consumerCount = 1;
-
- $this->channel->shouldReceive('queue_declare')->with(
- $this->config['queue'],
- true
- )->once()
- ->andReturn([$this->config['queue'], $messageCount, $consumerCount]);
-
- $size = $this->queue->size();
-
- $this->assertEquals($messageCount, $size);
- }
-
- public function testPush()
- {
- $job = new TestJob();
- $data = [];
-
- $this->channel->shouldReceive('exchange_declare')->with(
- $this->config['exchange_params']['name'],
- $this->config['exchange_params']['type'],
- $this->config['exchange_params']['passive'],
- $this->config['exchange_params']['durable'],
- $this->config['exchange_params']['auto_delete']
- )->once();
-
- $this->channel->shouldReceive('queue_declare')->with(
- $this->config['queue'],
- $this->config['queue_params']['passive'],
- $this->config['queue_params']['durable'],
- $this->config['queue_params']['exclusive'],
- $this->config['queue_params']['auto_delete'],
- false,
- Mockery::any()
- )->once();
-
- $this->channel->shouldReceive('queue_bind')->with(
- $this->config['queue'],
- $this->config['exchange_params']['name'],
- $this->config['queue']
- )->once();
-
- $this->channel->shouldReceive('basic_publish')->once();
-
- $correlationId = $this->queue->push($job, $data);
-
- $this->assertEquals(23, strlen($correlationId));
- }
-
- public function testLater()
- {
- $job = new TestJob();
- $data = [];
- $delay = random_int(10, 60);
-
- $this->channel->shouldReceive('exchange_declare')->with(
- $this->config['exchange_params']['name'],
- $this->config['exchange_params']['type'],
- $this->config['exchange_params']['passive'],
- $this->config['exchange_params']['durable'],
- $this->config['exchange_params']['auto_delete']
- )->once();
-
- $this->channel->shouldReceive('queue_declare')->once();
-
- // main queue
- $this->channel->shouldReceive('queue_bind')->with(
- $this->config['queue'],
- $this->config['exchange_params']['name'],
- $this->config['queue']
- )->once();
-
- // delayed queue
- $this->channel->shouldReceive('queue_bind')->with(
- $this->config['queue'] . '_deferred_' . $delay,
- $this->config['exchange_params']['name'],
- $this->config['queue'] . '_deferred_' . $delay
- )->once();
-
- $this->channel->shouldReceive('basic_publish')->once();
-
- $correlationId = $this->queue->later($delay, $job, $data);
-
- $this->assertEquals(23, strlen($correlationId));
- }
-
- public function testPop()
- {
- $message = Mockery::mock(AMQPMessage::class);
-
- $this->channel->shouldReceive('exchange_declare')->with(
- $this->config['exchange_params']['name'],
- $this->config['exchange_params']['type'],
- $this->config['exchange_params']['passive'],
- $this->config['exchange_params']['durable'],
- $this->config['exchange_params']['auto_delete']
- )->once();
-
- $this->channel->shouldReceive('queue_declare')->with(
- $this->config['queue'],
- $this->config['queue_params']['passive'],
- $this->config['queue_params']['durable'],
- $this->config['queue_params']['exclusive'],
- $this->config['queue_params']['auto_delete'],
- false,
- Mockery::any()
- )->once();
-
- $this->channel->shouldReceive('queue_bind')->with(
- $this->config['queue'],
- $this->config['exchange_params']['name'],
- $this->config['queue']
- )->once();
-
- $this->channel->shouldReceive('basic_get')->with($this->config['queue'])->andReturn($message)->once();
-
- /** @var Mock|mixed $container */
- $container = Mockery::mock(Container::class);
-
- $this->queue->setContainer($container);
- $this->queue->pop();
- }
-
- public function testSetAttempts()
- {
- $count = mt_rand();
-
- $this->queue->setAttempts($count);
- }
-
- public function testSetCorrelationId()
- {
- $id = str_random();
-
- $this->queue->setCorrelationId($id);
- }
-}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 00000000..ec49a1ac
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,57 @@
+set('queue.default', 'rabbitmq');
+ $app['config']->set('queue.connections.rabbitmq', [
+ 'driver' => 'rabbitmq',
+ 'queue' => 'default',
+ 'connection' => AMQPLazyConnection::class,
+
+ 'hosts' => [
+ [
+ 'host' => getenv('HOST'),
+ 'port' => getenv('PORT'),
+ 'vhost' => '/',
+ 'user' => 'guest',
+ 'password' => 'guest',
+ ],
+ ],
+
+ 'options' => [
+ 'ssl_options' => [
+ 'cafile' => null,
+ 'local_cert' => null,
+ 'local_key' => null,
+ 'verify_peer' => false,
+ 'passphrase' => null,
+ ],
+ ],
+
+ 'worker' => 'default',
+
+ ]);
+ }
+
+ protected function connection(?string $name = null): RabbitMQQueue
+ {
+ return Queue::connection($name);
+ }
+}
diff --git a/tests/files/device.crt b/tests/files/device.crt
new file mode 100644
index 00000000..9589bcc4
--- /dev/null
+++ b/tests/files/device.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjICCQDZfnTLkt080jANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJV
+UzEQMA4GA1UECAwHQXJpem9uYTETMBEGA1UEBwwKU2NvdHRzZGFsZTEdMBsGA1UE
+CgwURXhhbXBsZSBDb21wYW55IEluYy4xEjAQBgNVBAMMCTEyNy4wLjAuMTAeFw0x
+OTAyMjUyMDU5MDRaFw0yMDA3MDkyMDU5MDRaMGcxCzAJBgNVBAYTAlVTMRAwDgYD
+VQQIDAdBcml6b25hMRMwEQYDVQQHDApTY290dHNkYWxlMR0wGwYDVQQKDBRFeGFt
+cGxlIENvbXBhbnkgSW5jLjESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxaYllNmtkze8H/v6Vn0qIm16j+zujp605j1s
+YCzUBKYxFPkoG7jzl7xC5k1P2DLHf7pJoUAPheBg19+0+lVijIU8f00OjQ5LRxGh
+a6bpvhdR1yrQzw/ys8OOA7PIx2RRKB012GeTgKN93WDdWIJrr+EQMhMOob6562uv
+GDOYu+/P/HU4WJGLBga0r8f49/iwwVpz30WLc6SpjKdTloAWsiAZ9ZU7Zb+HN2y+
+TpoXtwU15lFcKTfPVcvOi1iW/ypUiZX9e1KrpD66yptPnhQzijqkDHt2mUOJj8Nj
+KR1URndCIAmpmglwghGpv9kIDpH9sMWxH8qUel33efIc6waigwIDAQABMA0GCSqG
+SIb3DQEBCwUAA4IBAQCbPLDMc1dKlqfi75qxRpIaVD5rvIsaWiSwMYJJudWgUpbV
+MS/w1RAkVRNpyC+qTFGivWTauNuGQyZOJBt7XLjXCQ7piALQKAXL4hvP+1wlRqlv
+KQvi0rCeqBV21T8u1wkuF/yrdpDpxmDvLSxv/bKLTVYmESFMaez0rDtDSCbaBTeX
+GqEJraVlfWJUE3PiscQA5gozPHxHfuiPSzBi1+4AaU/MSFc5hh45OapUs2lg4nmo
+X6dyN2/rTmVBfUb87Ppl3z1IhjJMN7RqmmDdmJWRX5x0sRljpnIlA6WGmp6enDA3
+OMfP7ZZXqD/u5NnmAau6khcX8TAELkNrwBNC/fRJ
+-----END CERTIFICATE-----
diff --git a/tests/files/device.csr b/tests/files/device.csr
new file mode 100644
index 00000000..b449d550
--- /dev/null
+++ b/tests/files/device.csr
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICrDCCAZQCAQAwZzELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB0FyaXpvbmExEzAR
+BgNVBAcMClNjb3R0c2RhbGUxHTAbBgNVBAoMFEV4YW1wbGUgQ29tcGFueSBJbmMu
+MRIwEAYDVQQDDAkxMjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDFpiWU2a2TN7wf+/pWfSoibXqP7O6OnrTmPWxgLNQEpjEU+SgbuPOXvELm
+TU/YMsd/ukmhQA+F4GDX37T6VWKMhTx/TQ6NDktHEaFrpum+F1HXKtDPD/Kzw44D
+s8jHZFEoHTXYZ5OAo33dYN1Ygmuv4RAyEw6hvrnra68YM5i778/8dThYkYsGBrSv
+x/j3+LDBWnPfRYtzpKmMp1OWgBayIBn1lTtlv4c3bL5Omhe3BTXmUVwpN89Vy86L
+WJb/KlSJlf17UqukPrrKm0+eFDOKOqQMe3aZQ4mPw2MpHVRGd0IgCamaCXCCEam/
+2QgOkf2wxbEfypR6Xfd58hzrBqKDAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEA
+X7NrRZ7gHhrxb97p41fwPvQGZbJdfotHcuTAq+zG06b9HyWMRQRp2aGCaPxz9Lrr
+xJJex51O7zFE+F7rdQtQQhvB9NJjseStEHJIxhWyf45JVmI9e+TtljMrHTjiuMNZ
+4cP5vR8Nf+PdYfRzGGWIS8W12XQ2gRy48QMmUBjwz6iE80byBIb2Upg3XEZvvJsy
+28SeXvxV+IZr/gLWjLqW8CDJNCNp0shOKvvDzOda2nThorxvuZLhg0ykcaxFfr6R
+yjzGAjFr++PZXAkwqkEeUz/1DN/1yQu5F6okaUfeOjkEFF96Zeez5KbXXF5kBCyw
+XAF3lYeYmCOWGfzEYkVVHw==
+-----END CERTIFICATE REQUEST-----
diff --git a/tests/files/device.key b/tests/files/device.key
new file mode 100644
index 00000000..e54a2074
--- /dev/null
+++ b/tests/files/device.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAxaYllNmtkze8H/v6Vn0qIm16j+zujp605j1sYCzUBKYxFPko
+G7jzl7xC5k1P2DLHf7pJoUAPheBg19+0+lVijIU8f00OjQ5LRxGha6bpvhdR1yrQ
+zw/ys8OOA7PIx2RRKB012GeTgKN93WDdWIJrr+EQMhMOob6562uvGDOYu+/P/HU4
+WJGLBga0r8f49/iwwVpz30WLc6SpjKdTloAWsiAZ9ZU7Zb+HN2y+TpoXtwU15lFc
+KTfPVcvOi1iW/ypUiZX9e1KrpD66yptPnhQzijqkDHt2mUOJj8NjKR1URndCIAmp
+mglwghGpv9kIDpH9sMWxH8qUel33efIc6waigwIDAQABAoIBAExlsFkc2s7w2DK0
+v0r3DnZIQvum5X8TMXFdhKqYKUuywX4N4Mb2cpHQHzvN3nL/DcX9R8Cgdl+VH1nS
+Cq5ImtMeHQhHzLwRLl/GHNLzrZ3gfa3hytx+mZ2KlTYxJAaObCBJSirfvlAW4evU
+KTqxDtbo882nOBylEFBDS2bbasoZdalEG/fosjXafyi0oObDO9gXlyLQRuQIPZPK
+afTIB4LQM6K5o92y6qxJbFu+qPC46vzSeToOOeAUyOSA0XQ+9eNbPdFDPJnp5Pre
+aL0bfr8Qmh+gufaA4UIANAlgVa7IiG5ccfxMg7nJxUWDR6H7MnQX7Zjuv/i7qP+P
+dExyykECgYEA80w0PNpfRS0Eq87r2tKhtFJgHxAvC4m4Y8nhDoVSwYEaaJVYInnG
+dHH9CyvV7TqAKWEkwBqJMo04AVQdOrOg2HJ+nsxrMa3RtUZFGpRYYbMVEX3o5VbL
+sY3eQZhhgxzgcWkohHtDv/NhVksL97MW9KAGo+Qfi88Ma+SJTH/zyEkCgYEAz/fT
+TUTURHzqs0Um4mgTvGj15H0ySyiMFuj5d+MdcYoy3HpW0htm0YlJISfo8bK1W4eq
+bR7ltb3g9aqVFIh9szQ5mK91oDtSeYYUxBm9J7a75O/h+57tekcBAKUaarLYBpwR
+7qmkGQcs1sILTdQIy4xqyu4sYP/EKvroT23KjGsCgYBhDH6x33GtSF3aormWGfsC
+0PEisvPxKEhzFa4+epQeN/9uxFPZvLWa8XU8pYm6DWHeH6/nKS6dCZPTg9f8+HYq
+oNE9StFfibRjGNqr1YzDvAmlZpImGU87ThngFIahJD2rP4U4A2ttAApNv7XQYpG/
+lq3PZknnHPoZd5oE9+0ocQKBgGNf9KQzg4rGdg/7tzzwpp2dOgJYoLOxSF+aK7rR
+17vtYahg/SOg3Fy70Sn6vCDiWC7IgPNrlDBn7xr0zA/nuMjs56jCDt7l+d7/5uRd
+uDlF5DrdNYrawndvflckjZ72nqtp4Fe+0B71gsOMLYKfEyTQkCcv8BzZmo8/Hcr2
+l5bJAoGAAzNnI8T2zdCZc5SAHYCrxwWQNX9F5Uwb15TMOHGU2xvd/G2DkvKIpv8f
+NL0tX6KdRt5yVvV1YWqNhhRqjpocohsnzMqU5UHz2U6ejjr6ry2pQlGokTvnxyCL
+526noBHZdTLPf09bcG/F7Sdq5/Q5GB+SQTW6mBYcwJIVG2oYxF0=
+-----END RSA PRIVATE KEY-----
diff --git a/tests/files/rootCA.key b/tests/files/rootCA.key
new file mode 100644
index 00000000..483433b9
--- /dev/null
+++ b/tests/files/rootCA.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAw+xfX1g9xN1TGuRGD5xfpvIY8o8vGF8iWyBVd52YzCybnyPT
+v5gmS7tndPkhfnL356gqJS/wHf7yD21YQhSzQZmOW1x9IxYsx4/mmmxpCYp9jzYo
+/Rt2siNHB5ZH3Z8lwSGn6mnFQs40juIPS5dbngGDs5xDl0NkTO4jZRGiuS70OHHJ
+NTRe9TsFs/12JggGVtIUmlPfpyflZBkhalD9mohoDw1egM0vLxaHMWYhIPEOVP4r
+taMEYk2Y0wNDuOldqWMVzwBZFK5td8f4wqeYPBMfk9cK448XWYDZzShARnYTiGmc
+evxgd+sFuEXQr/U7O/HUWTY3/YPFwSrx/ksL6QIDAQABAoIBAGPu6Sakx7zmd0E8
+NlA4HsH0sqzmQ8tWmxuH/pAonotmJWqix5rubHoseLS9bkwlMDXFHNoi/YMPS0B7
+MY1jKZvIS0hmgJ2o7eZMi/8wVNM9BJZLtdSEcaKjQ9Om37k3N/auyAtVL/zHWR+Y
+RtzzsxOBCkBO8FrzUPG8delTeYCigIQM13rF3qsc5G7XEN5ypP0gz+m03EicVaXb
+tMzzy4HwJq4T3ICj3TL6J5AzLHrUXl4g+DyakOfLCFqB1brWsWW12+2oYDeWOC6C
+rzCsdSHGTWqSNC9GZwcgwy6PwVhykurTo9gFHBNLyxZHtKcKdlcpCrV/+oRRuZLT
+6WY42AECgYEA+5F8a+w9K9dM2FebqXck3HupS3eku4FVRhreu8iZgCIvz+M1bb6N
+bDtpjjz5N+CYYVp89esP0RjLCzcQUWkakb7Ek5QIA+srI/TmfGOZYFeQipgClAGr
++izoLRDSNEO7/RKzbWAh+tk9brzjhuIteegOtNRoXJSh/2ajHg+SgSkCgYEAx1/w
+1ZVyJkYvfX+0qf64fvyodMC+mHsJ/UgtHM/gSi1b7b4obJqrHbnv+mBixDNh02dh
+Jl7wlrwEQI4tk4cwZDh+dDwZ5F8px3/LqX+m2UfjS+rU30SwfZhdaa9qyUKBZBK3
+gutdWjDMa0C3Ng4dyu8b/onG14wzLs7VLKjzzMECgYEAzdzKUo6pqDyxd8CJc246
+TessKMOpnH9DxvCqIEURyBcxxQ8LY9kxZcZgpLMkxiMWz2P7KkrHULbXQUA4LEa0
+JVxVBOd4f6xsSypXiqb+liZR8/hc871CfKFPBcHkIjzjkz9AcVrfs6UeboZIMtLX
+oBDUKApBtLE0uAnHpgvcObECgYEAsZDA7XgsMepQYXVbcgtqRa7AWTtQhH0QaIPf
+qcl5+JZtSVASsKcPv2naUSOG0zbv6VgpLgNgQt8w6k22Sa4dayTleqAMb1hR3Vv0
+BwGpl9pulS6QaEjE5xbMG1Qfxx90HayNxAvbGHhdlygMBBiOcC6EwC306gPzkbyk
+HyJaAAECgYEAjjcdQiQu3HhMidSMeZt8lBGSTLDIHujIY32+mOlBkIZsYiK2fnNZ
+JZUPPmC9qDH+VmHyXZyKb2dxYUdiknptKUUu3NBrBP9LKqM78/yxFEdVSxiqlIBx
+Dapv4fqsRreipdqFewo2YUvIgvowJj7p/EhlirX46XSDV7+zru3UBy0=
+-----END RSA PRIVATE KEY-----
diff --git a/tests/files/rootCA.pem b/tests/files/rootCA.pem
new file mode 100644
index 00000000..094d8241
--- /dev/null
+++ b/tests/files/rootCA.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjICCQCBIl41W59qTDANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJV
+UzEQMA4GA1UECAwHQXJpem9uYTETMBEGA1UEBwwKU2NvdHRzZGFsZTEdMBsGA1UE
+CgwURXhhbXBsZSBDb21wYW55IEluYy4xEjAQBgNVBAMMCTEyNy4wLjAuMTAeFw0x
+OTAyMjUyMDU3MjBaFw0yMTEyMTUyMDU3MjBaMGcxCzAJBgNVBAYTAlVTMRAwDgYD
+VQQIDAdBcml6b25hMRMwEQYDVQQHDApTY290dHNkYWxlMR0wGwYDVQQKDBRFeGFt
+cGxlIENvbXBhbnkgSW5jLjESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw+xfX1g9xN1TGuRGD5xfpvIY8o8vGF8iWyBV
+d52YzCybnyPTv5gmS7tndPkhfnL356gqJS/wHf7yD21YQhSzQZmOW1x9IxYsx4/m
+mmxpCYp9jzYo/Rt2siNHB5ZH3Z8lwSGn6mnFQs40juIPS5dbngGDs5xDl0NkTO4j
+ZRGiuS70OHHJNTRe9TsFs/12JggGVtIUmlPfpyflZBkhalD9mohoDw1egM0vLxaH
+MWYhIPEOVP4rtaMEYk2Y0wNDuOldqWMVzwBZFK5td8f4wqeYPBMfk9cK448XWYDZ
+zShARnYTiGmcevxgd+sFuEXQr/U7O/HUWTY3/YPFwSrx/ksL6QIDAQABMA0GCSqG
+SIb3DQEBCwUAA4IBAQBT49Qz3kxu62oyk7xHvFinxrieF4oNqjivwHHHssEHsdb4
+N67YcMu5HHu78u035TH84jnsCjgKRgkzv9dSK5Pmqa9Qvt7rH3ziwdm9vr3Qg9NE
+GIC09UrvNXmSfNirgIJAbmXyZvaGvLEUjenI7LNghdWsPZTNwwAwVSTiR7X6fmLe
+Bci9wW7+oyjAJK+ct6mNPWe6s0x7TEJL1BhvfH1secFDF9dcq4UJ8/8sM3hbpmtb
+MkdLpqm+rYBGazUCkiL1Rp9sRHDho03RORP9H0wOIyX6JPWHIeMZrXsQnEYhGFEM
+6RDT11Rv4HdXJuIbVFVEgJK0v1Df8ZQS7I99Oh3H
+-----END CERTIFICATE-----
diff --git a/tests/files/rootCA.srl b/tests/files/rootCA.srl
new file mode 100644
index 00000000..93842342
--- /dev/null
+++ b/tests/files/rootCA.srl
@@ -0,0 +1 @@
+D97E74CB92DD3CD2
diff --git a/tests/jobs/TestJob.php b/tests/jobs/TestJob.php
deleted file mode 100644
index a0439470..00000000
--- a/tests/jobs/TestJob.php
+++ /dev/null
@@ -1,8 +0,0 @@
-