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..00dea824 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.result.cache
+.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 18973cb0..30a10df9 100644
--- a/README.md
+++ b/README.md
@@ -1,95 +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
```
-2. Add these properties to `.env` with proper values:
+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', '/'),
+ ],
+ // ...
+ ],
+
+ // ...
+ ],
+ // ...
+],
```
-QUEUE_DRIVER=rabbitmq
-RABBITMQ_QUEUE=queue_name
-RABBITMQ_DSN=amqp: # same as amqp://guest:guest@127.0.0.1:5672/%2F
-# or
-RABBITMQ_HOST=127.0.0.1
-RABBITMQ_PORT=5672
-RABBITMQ_VHOST=/
-RABBITMQ_LOGIN=guest
-RABBITMQ_PASSWORD=guest
-RABBITMQ_QUEUE=queue_name
+### 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,
+ ],
+ ],
+ ],
+
+ // ...
+],
```
-3. Optionally: if you want to to use an SSL connection, add these properties to the `.env` with proper values:
+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' => '',
+ ],
+ ],
+ ],
+
+ // ...
+],
```
-RABBITMQ_SSL=true
-RABBITMQ_SSL_CAFILE=/path_to_your_ca_file
-RABBITMQ_SSL_LOCALCERT=
-RABBITMQ_SSL_PASSPHRASE=
-RABBITMQ_SSL_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',
+ ],
+ ],
+ ],
+
+ // ...
+],
```
-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
+### Horizon support
-4. Other AMQP transports
+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`.
-The package uses [enqueue/amqp-lib](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/transport/amqp_lib.md) transport which is based on [php-amqplib](https://github.com/php-amqplib/php-amqplib).
-There is possibility to use any [amqp interop](https://github.com/queue-interop/queue-interop#amqp-interop) compatible transport, for example `enqueue/amqp-ext` or `enqueue/amqp-bunny`.
-Here's an example on how one can change the transport to `enqueue/amqp-bunny`.
+Horizon is depending on events dispatched by the worker.
+These events inform Horizon what was done with the message/job.
-First, install the package:
+This Library supports Horizon, but in the config you have to inform Laravel to use the QueueApi compatible with horizon.
-```bash
-$ composer require enqueue/amqp-bunny:^0.8
+```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,
+ ],
+
+ // ...
+],
```
-
-and change the factory class:
```php
[
- 'rabbitmq' => [
- 'driver' => 'rabbitmq',
- 'factory_class' => \Enqueue\AmqpBunny\AmqpConnectionFactory::class,
+namespace App\Queue;
+
+use VladimirYuldashev\LaravelQueueRabbitMQ\Queue\RabbitMQQueue as BaseRabbitMQQueue;
+
+class RabbitMQQueue extends BaseRabbitMQQueue
+{
+ // ...
+}
+```
+
+**For Example: A reconnect implementation.**
+
+If you want to reconnect to RabbitMQ, if the connection is dead.
+You can override the publishing and the createChannel methods.
+
+> 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,
+ ],
+ ],
+
+ // ...
+],
+```
+
+### 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),
+ ],
+ ],
+ ],
+
+ // ...
+],
+```
+
+### Events after Database commits
+
+To instruct Laravel workers to dispatch events after all database commits are completed.
+
+```php
+'connections' => [
+ // ...
+
+ 'rabbitmq' => [
+ // ...
+
+ 'after_commit' => true,
+ ],
+
+ // ...
+],
+```
+
+### 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,
+ ],
+
+ // ...
+],
+```
+
+### 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',
+ ],
+
+ // ...
+],
+```
+
+### 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,
],
],
-];
+
+ // ...
+],
+```
+
+### Octane support
+
+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
+
+## Laravel Usage
+
+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
+
+## Lumen Usage
+
+For Lumen usage the service provider should be registered manually as follow in `bootstrap/app.php`:
+
+```php
+$app->register(VladimirYuldashev\LaravelQueueRabbitMQ\LaravelQueueRabbitMQServiceProvider::class);
```
-#### Usage
+## Consuming Messages
+
+There are two ways of consuming messages.
+
+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.
-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
+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.
-#### Testing
+## Testing
-Run the tests with:
+Setup RabbitMQ using `docker-compose`:
-``` bash
-vendor/bin/phpunit
+```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
-#### Contribution
+# To run only style tests.
+composer test:style
-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)
+# To run only unit tests.
+composer test:unit
+```
-> 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.
+If you receive any errors from the style tests, you can automatically fix most,
+if not all the issues with the following command:
-#### Supported versions of Laravel (+Lumen)
+```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 477b9bf0..caf8a0fc 100644
--- a/composer.json
+++ b/composer.json
@@ -1,49 +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.*",
- "enqueue/amqp-lib": "0.8.5",
- "queue-interop/amqp-interop": "^0.7"
- },
- "require-dev": {
- "phpunit/phpunit": "~6.0",
- "illuminate/events": "5.5.*",
- "mockery/mockery": "~1.0"
- },
- "autoload": {
- "psr-4": {
- "VladimirYuldashev\\LaravelQueueRabbitMQ\\": "src/"
- }
- },
- "autoload-dev": {
- "psr-4": {
- "VladimirYuldashev\\LaravelQueueRabbitMQ\\Tests\\": "tests/"
- }
- },
- "extra": {
- "branch-alias": {
- "dev-master": "6.0-dev"
+ "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"
},
- "laravel": {
- "providers": [
- "VladimirYuldashev\\LaravelQueueRabbitMQ\\LaravelQueueRabbitMQServiceProvider"
- ]
- }
- },
- "scripts": {
- "test": "vendor/bin/phpunit"
- },
- "minimum-stability": "dev",
- "prefer-stable": true
+ "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 207d9e11..4c102ce8 100644
--- a/config/rabbitmq.php
+++ b/config/rabbitmq.php
@@ -3,94 +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',
-
- 'dsn' => env('RABBITMQ_DSN', null),
-
- /*
- * Could be one a class that implements \Interop\Amqp\AmqpConnectionFactory for example:
- * - \EnqueueAmqpExt\AmqpConnectionFactory if you install enqueue/amqp-ext
- * - \EnqueueAmqpLib\AmqpConnectionFactory if you install enqueue/amqp-lib
- * - \EnqueueAmqpBunny\AmqpConnectionFactory if you install enqueue/amqp-bunny
- */
- 'factory_class' => Enqueue\AmqpLib\AmqpConnectionFactory::class,
-
- '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'),
-
- 'options' => [
-
- 'exchange' => [
-
- 'name' => env('RABBITMQ_EXCHANGE_NAME'),
-
- /*
- * Determine if exchange should be created if it does not exist.
- */
- 'declare' => env('RABBITMQ_EXCHANGE_DECLARE', true),
-
- /*
- * Read more about possible values at https://www.rabbitmq.com/tutorials/amqp-concepts.html
- */
- 'type' => env('RABBITMQ_EXCHANGE_TYPE', \Interop\Amqp\AmqpTopic::TYPE_DIRECT),
- 'passive' => env('RABBITMQ_EXCHANGE_PASSIVE', false),
- 'durable' => env('RABBITMQ_EXCHANGE_DURABLE', true),
- 'auto_delete' => env('RABBITMQ_EXCHANGE_AUTODELETE', false),
- 'arguments' => env('RABBITMQ_EXCHANGE_ARGUMENTS'),
- ],
-
- 'queue' => [
-
- /*
- * The name of default queue.
- */
- 'name' => env('RABBITMQ_QUEUE', 'default'),
-
- /*
- * Determine if queue should be created if it does not exist.
- */
- 'declare' => env('RABBITMQ_QUEUE_DECLARE', true),
-
- /*
- * Determine if queue should be binded to the exchange created.
- */
- 'bind' => env('RABBITMQ_QUEUE_DECLARE_BIND', true),
-
- /*
- * Read more about possible values at https://www.rabbitmq.com/tutorials/amqp-concepts.html
- */
- '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'),
+ '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),
- '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'),
];
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 9ce256ab..ee46d6cd 100644
--- a/src/LaravelQueueRabbitMQServiceProvider.php
+++ b/src/LaravelQueueRabbitMQServiceProvider.php
@@ -2,34 +2,68 @@
namespace VladimirYuldashev\LaravelQueueRabbitMQ;
+use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Queue\QueueManager;
use Illuminate\Support\ServiceProvider;
+use VladimirYuldashev\LaravelQueueRabbitMQ\Console\ConsumeCommand;
use VladimirYuldashev\LaravelQueueRabbitMQ\Queue\Connectors\RabbitMQConnector;
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'];
-
+
$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 8e8e0232..a0f79e32 100644
--- a/src/Queue/Connectors/RabbitMQConnector.php
+++ b/src/Queue/Connectors/RabbitMQConnector.php
@@ -2,23 +2,21 @@
namespace VladimirYuldashev\LaravelQueueRabbitMQ\Queue\Connectors;
-use Enqueue\AmqpTools\DelayStrategyAware;
-use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy;
+use Exception;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\Connectors\ConnectorInterface;
+use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Events\WorkerStopping;
-use Interop\Amqp\AmqpConnectionFactory as InteropAmqpConnectionFactory;
-use Interop\Amqp\AmqpConnectionFactory;
-use Interop\Amqp\AmqpContext;
+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 Dispatcher
- */
- private $dispatcher;
+ protected Dispatcher $dispatcher;
public function __construct(Dispatcher $dispatcher)
{
@@ -28,48 +26,24 @@ public function __construct(Dispatcher $dispatcher)
/**
* Establish a queue connection.
*
- * @param array $config
+ * @return RabbitMQQueue
*
- * @return Queue
+ * @throws Exception
*/
public function connect(array $config): Queue
{
- if (false === array_key_exists('factory_class', $config)) {
- throw new \LogicException('The factory_class option is missing though it is required.');
- }
+ $connection = ConnectionFactory::make($config);
- $factoryClass = $config['factory_class'];
- if (false === class_exists($factoryClass) || false === (new \ReflectionClass($factoryClass))->implementsInterface(InteropAmqpConnectionFactory::class)) {
- throw new \LogicException(sprintf('The factory_class option has to be valid class that implements "%s"', InteropAmqpConnectionFactory::class));
- }
-
- /** @var AmqpConnectionFactory $factory */
- $factory = new $factoryClass([
- 'dsn' => $config['dsn'],
- 'host' => $config['host'],
- 'port' => $config['port'],
- 'user' => $config['login'],
- 'pass' => $config['password'],
- 'vhost' => $config['vhost'],
- 'ssl_on' => $config['ssl_params']['ssl_on'],
- 'ssl_verify' => $config['ssl_params']['verify_peer'],
- 'ssl_cacert' => $config['ssl_params']['cafile'],
- 'ssl_cert' => $config['ssl_params']['local_cert'],
- 'ssl_key' => $config['ssl_params']['local_key'],
- 'ssl_passphrase' => $config['ssl_params']['passphrase'],
- ]);
+ $queue = QueueFactory::make($config)->setConnection($connection);
- if ($factory instanceof DelayStrategyAware) {
- $factory->setDelayStrategy(new RabbitMqDlxDelayStrategy());
+ if ($queue instanceof HorizonRabbitMQQueue) {
+ $this->dispatcher->listen(JobFailed::class, RabbitMQFailedEvent::class);
}
- /** @var AmqpContext $context */
- $context = $factory->createContext();
-
- $this->dispatcher->listen(WorkerStopping::class, function () use ($context) {
- $context->close();
+ $this->dispatcher->listen(WorkerStopping::class, static function () use ($queue): void {
+ $queue->close();
});
- return new RabbitMQQueue($context, $config);
+ return $queue;
}
}
diff --git a/src/Queue/Jobs/RabbitMQJob.php b/src/Queue/Jobs/RabbitMQJob.php
index 92d713d3..abcdfab4 100644
--- a/src/Queue/Jobs/RabbitMQJob.php
+++ b/src/Queue/Jobs/RabbitMQJob.php
@@ -2,175 +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 Interop\Amqp\AmqpConsumer;
-use Interop\Amqp\AmqpMessage;
+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 $consumer;
+ /**
+ * 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,
- AmqpConsumer $consumer,
- AmqpMessage $message
+ RabbitMQQueue $rabbitmq,
+ AMQPMessage $message,
+ string $connectionName,
+ string $queue
) {
$this->container = $container;
- $this->connection = $connection;
- $this->consumer = $consumer;
+ $this->rabbitmq = $rabbitmq;
$this->message = $message;
- $this->queue = $consumer->getQueue()->getQueueName();
- $this->connectionName = $connection->getConnectionName();
+ $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
{
- // set default job attempts to 1 so that jobs can run without retry
- $defaultAttempts = 1;
+ if (! $data = $this->getRabbitMQMessageHeaders()) {
+ return 1;
+ }
+
+ $laravelAttempts = (int) Arr::get($data, 'laravel.attempts', 0);
- return $this->message->getProperty(self::ATTEMPT_COUNT_HEADERS_KEY, $defaultAttempts);
+ 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->getBody();
+ parent::markAsFailed();
+
+ // 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 delete()
+ /**
+ * {@inheritdoc}
+ *
+ * @throws BindingResolutionException
+ */
+ public function delete(): void
{
parent::delete();
- $this->consumer->acknowledge($this->message);
+ // 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);
+ }
+
+ // required for Laravel Horizon
+ if ($this->rabbitmq instanceof HorizonRabbitMQQueue) {
+ $this->rabbitmq->deleteReserved($this->queue, $this);
+ }
}
- /** @inheritdoc */
- public function release($delay = 0)
+ /**
+ * Release the job back into the queue.
+ *
+ * @param int $delay
+ *
+ * @throws AMQPProtocolChannelException
+ */
+ public function release($delay = 0): void
{
- parent::release($delay);
-
- $this->delete();
-
- $body = $this->payload();
+ parent::release();
- /*
- * 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();
- }
-
- $data = $body['data'];
+ // Always create a new message when this Job is released
+ $this->rabbitmq->laterRaw($delay, $this->message->getBody(), $this->queue, $this->attempts());
- $this->connection->release($delay, $job, $data, $this->getQueue(), $this->attempts() + 1);
+ // 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);
}
/**
- * Get the job identifier.
- *
- * @return string
+ * Get the underlying RabbitMQ connection.
*/
- public function getJobId(): string
+ public function getRabbitMQ(): RabbitMQQueue
{
- return $this->message->getCorrelationId();
+ return $this->rabbitmq;
}
/**
- * Sets the job identifier.
- *
- * @param string $id
- *
- * @return void
+ * Get the underlying RabbitMQ message.
*/
- public function setJobId($id)
+ public function getRabbitMQMessage(): AMQPMessage
{
- $this->connection->setCorrelationId($id);
+ return $this->message;
}
/**
- * Unserialize job.
- *
- * @param array $body
- *
- * @throws Exception
- *
- * @return mixed
+ * Get the headers from the rabbitMQ message.
*/
- private function unserialize(array $body)
+ protected function getRabbitMQMessageHeaders(): ?array
{
- 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;
+ /** @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 @@
+context = $context;
+ /**
+ * List of already bound queues to exchanges.
+ */
+ protected array $boundQueues = [];
- $this->queueOptions = $config['options']['queue'];
- $this->queueOptions['arguments'] = isset($this->queueOptions['arguments']) ?
- json_decode($this->queueOptions['arguments'], true) : [];
+ /**
+ * Current job being processed.
+ */
+ protected ?RabbitMQJob $currentJob = null;
- $this->exchangeOptions = $config['options']['exchange'];
- $this->exchangeOptions['arguments'] = isset($this->exchangeOptions['arguments']) ?
- json_decode($this->exchangeOptions['arguments'], true) : [];
+ /**
+ * Holds the Configuration
+ */
+ protected QueueConfig $config;
- $this->sleepOnError = $config['sleep_on_error'] ?? 5;
+ /**
+ * RabbitMQQueue constructor.
+ */
+ public function __construct(QueueConfig $config)
+ {
+ $this->config = $config;
+ $this->dispatchAfterCommit = $config->isDispatchAfterCommit();
}
- /** @inheritdoc */
- public function size($queueName = null): int
+ /**
+ * {@inheritdoc}
+ *
+ * @throws AMQPProtocolChannelException
+ */
+ public function size($queue = null): int
{
- /** @var AmqpQueue $queue */
- list($queue) = $this->declareEverything($queueName);
+ $queue = $this->getQueue($queue);
+
+ if (! $this->isQueueExists($queue)) {
+ return 0;
+ }
- return $this->context->declareQueue($queue);
+ // 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, $queueName = null, array $options = [])
+ /**
+ * {@inheritdoc}
+ *
+ * @throws AMQPProtocolChannelException
+ */
+ public function pushRaw($payload, $queue = null, array $options = []): int|string|null
{
- try {
- /**
- * @var AmqpTopic $topic
- * @var AmqpQueue $queue
- */
- list($queue, $topic) = $this->declareEverything($queueName);
-
- $message = $this->context->createMessage($payload);
- $message->setRoutingKey($queue->getQueueName());
- $message->setCorrelationId($this->getCorrelationId());
- $message->setContentType('application/json');
- $message->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT);
-
- if (isset($options['attempts'])) {
- $message->setProperty(RabbitMQJob::ATTEMPT_COUNT_HEADERS_KEY, $options['attempts']);
+ [$destination, $exchange, $exchangeType, $attempts] = $this->publishProperties($queue, $options);
+
+ $this->declareDestination($destination, $exchange, $exchangeType);
+
+ [$message, $correlationId] = $this->createMessage($payload, $attempts);
+
+ $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;
+
+ $this->declareQueue($destination, true, false, $this->getDelayQueueArguments($this->getQueue($queue), $ttl));
+
+ [$message, $correlationId] = $this->createMessage($payload, $attempts);
+
+ // Publish directly on the delayQueue, no need to publish through an exchange.
+ $this->publishBasic($message, null, $destination, true);
+
+ return $correlationId;
+ }
+
+ /**
+ * {@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]);
+ }
- $producer = $this->context->createProducer();
- if (isset($options['delay']) && $options['delay'] > 0) {
- $producer->setDeliveryDelay($options['delay'] * 1000);
+ $this->batchPublish();
+ }
+
+ /**
+ * @throws AMQPProtocolChannelException
+ */
+ public function bulkRaw(string $payload, ?string $queue = null, array $options = []): int|string|null
+ {
+ [$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}
+ *
+ * @throws Throwable
+ */
+ public function pop($queue = null)
+ {
+ try {
+ $queue = $this->getQueue($queue);
+
+ $job = $this->getJobClass();
+
+ /** @var AMQPMessage|null $message */
+ if ($message = $this->getChannel()->basic_get($queue)) {
+ return $this->currentJob = new $job(
+ $this->container,
+ $this,
+ $message,
+ $this->connectionName,
+ $queue
+ );
+ }
+ } 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;
}
- $producer->send($topic, $message);
+ 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 $message->getCorrelationId();
- } catch (\Exception $exception) {
- $this->reportConnectionError('pushRaw', $exception);
+ return null;
+ }
- return null;
+ /**
+ * @throws RuntimeException
+ */
+ public function getConnection(): AbstractConnection
+ {
+ if (! $this->connection) {
+ throw new RuntimeException('Queue has no AMQPConnection set.');
}
+
+ return $this->connection;
}
- /** @inheritdoc */
- public function later($delay, $job, $data = '', $queue = null)
+ public function setConnection(AbstractConnection $connection): RabbitMQQueue
{
- return $this->pushRaw($this->createPayload($job, $data), $queue, ['delay' => $this->secondsUntil($delay)]);
+ $this->connection = $connection;
+
+ return $this;
}
/**
- * Release a reserved job back onto the queue.
+ * Job class to use.
*
- * @param \DateTimeInterface|\DateInterval|int $delay
- * @param string|object $job
- * @param mixed $data
- * @param string $queue
- * @param int $attempts
- * @return mixed
+ *
+ * @throws Throwable
*/
- public function release($delay, $job, $data, $queue, $attempts = 0)
+ public function getJobClass(): string
{
- return $this->pushRaw($this->createPayload($job, $data), $queue, [
- 'delay' => $this->secondsUntil($delay),
- 'attempts' => $attempts
- ]);
+ $job = $this->getConfig()->getAbstractJob();
+
+ throw_if(
+ ! is_a($job, RabbitMQJob::class, true),
+ Exception::class,
+ sprintf('Class %s must extend: %s', $job, RabbitMQJob::class)
+ );
+
+ return $job;
}
- /** @inheritdoc */
- public function pop($queueName = null)
+ /**
+ * Gets a queue/destination, by default the queue option set on the connection.
+ */
+ public function getQueue($queue = null): string
{
+ return $queue ?: $this->getConfig()->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 {
- /** @var AmqpQueue $queue */
- list($queue) = $this->declareEverything($queueName);
+ // create a temporary channel, so the main channel will not be closed on exception
+ $channel = $this->createChannel();
+ $channel->exchange_declare($exchange, '', true);
+ $channel->close();
- $consumer = $this->context->createConsumer($queue);
+ $this->exchanges[] = $exchange;
- if ($message = $consumer->receiveNoWait()) {
- return new RabbitMQJob($this->container, $this, $consumer, $message);
+ return true;
+ } catch (AMQPProtocolChannelException $exception) {
+ if ($exception->amqp_reply_code === 404) {
+ return false;
}
- } catch (\Exception $exception) {
- $this->reportConnectionError('pop', $exception);
+
+ throw $exception;
}
+ }
- return null;
+ /**
+ * 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)
+ );
}
/**
- * Retrieves the correlation id, or a unique id.
+ * Delete an exchange from rabbitMQ, only when present in RabbitMQ.
*
- * @return string
+ *
+ * @throws AMQPProtocolChannelException
*/
- public function getCorrelationId(): string
+ public function deleteExchange(string $name, bool $unused = false): void
{
- return $this->correlationId ?: uniqid('', true);
+ if (! $this->isExchangeExists($name)) {
+ return;
+ }
+
+ $idx = array_search($name, $this->exchanges);
+ unset($this->exchanges[$idx]);
+
+ $this->getChannel()->exchange_delete(
+ $name,
+ $unused
+ );
}
/**
- * Sets the correlation id for a message to be published.
+ * Checks if the given queue already present/defined in RabbitMQ.
+ * Returns false when the queue is missing.
*
- * @param string $id
*
- * @return void
+ * @throws AMQPProtocolChannelException
*/
- public function setCorrelationId(string $id)
+ public function isQueueExists(?string $name = null): bool
{
- $this->correlationId = $id;
+ $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;
+ }
}
/**
- * @return AmqpContext
+ * Declare a queue in rabbitMQ, when not already declared.
*/
- public function getContext(): AmqpContext
- {
- return $this->context;
+ 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)
+ );
}
/**
- * @param string $queueName
+ * Delete a queue from rabbitMQ, only when present in RabbitMQ.
*
- * @return array [Interop\Amqp\AmqpQueue, Interop\Amqp\AmqpTopic]
+ *
+ * @throws AMQPProtocolChannelException
*/
- private function declareEverything(string $queueName = null): array
+ public function deleteQueue(string $name, bool $if_unused = false, bool $if_empty = false): void
{
- $queueName = $queueName ?: $this->queueOptions['name'];
- $exchangeName = $this->exchangeOptions['name'] ?: $queueName;
+ if (! $this->isQueueExists($name)) {
+ return;
+ }
- $topic = $this->context->createTopic($exchangeName);
- $topic->setType($this->exchangeOptions['type']);
- $topic->setArguments($this->exchangeOptions['arguments']);
- if ($this->exchangeOptions['passive']) {
- $topic->addFlag(AmqpTopic::FLAG_PASSIVE);
+ $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;
}
- if ($this->exchangeOptions['durable']) {
- $topic->addFlag(AmqpTopic::FLAG_DURABLE);
+
+ $this->getChannel()->queue_bind($queue, $exchange, $routingKey);
+ }
+
+ /**
+ * Purge the queue of messages.
+ */
+ public function purge(?string $queue = null): void
+ {
+ // 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();
+ }
+
+ /**
+ * Acknowledge the message.
+ */
+ public function ack(RabbitMQJob $job): void
+ {
+ $this->getChannel()->basic_ack($job->getRabbitMQMessage()->getDeliveryTag());
+ }
+
+ /**
+ * Reject the message.
+ */
+ public function reject(RabbitMQJob $job, bool $requeue = false): void
+ {
+ $this->getChannel()->basic_reject($job->getRabbitMQMessage()->getDeliveryTag(), $requeue);
+ }
+
+ /**
+ * Create a AMQP message.
+ */
+ protected function createMessage($payload, int $attempts = 0): array
+ {
+ $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->exchangeOptions['auto_delete']) {
- $topic->addFlag(AmqpTopic::FLAG_AUTODELETE);
+
+ if ($this->getConfig()->isPrioritizeDelayed()) {
+ $properties['priority'] = $attempts;
}
- if ($this->exchangeOptions['declare'] && !in_array($exchangeName, $this->declaredExchanges, true)) {
- $this->context->declareTopic($topic);
+ 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[] = $exchangeName;
+ $commandData = unserialize($currentPayload['data']['command']);
+ if (property_exists($commandData, 'priority')) {
+ $properties['priority'] = $commandData->priority;
+ }
}
- $queue = $this->context->createQueue($queueName);
- $queue->setArguments($this->queueOptions['arguments']);
- if ($this->queueOptions['passive']) {
- $queue->addFlag(AmqpQueue::FLAG_PASSIVE);
+ $message = new AMQPMessage($payload, $properties);
+
+ $message->set('application_headers', new AMQPTable([
+ 'laravel' => [
+ 'attempts' => $attempts,
+ ],
+ ]));
+
+ return [
+ $message,
+ $correlationId,
+ ];
+ }
+
+ /**
+ * 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);
+ }
+
+ try {
+ $this->getConnection()->close();
+ } catch (ErrorException) {
+ // Ignore the exception
}
- if ($this->queueOptions['durable']) {
- $queue->addFlag(AmqpQueue::FLAG_DURABLE);
+ }
+
+ /**
+ * Get the Queue arguments.
+ */
+ protected function getQueueArguments(string $destination): array
+ {
+ $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->getConfig()->isPrioritizeDelayed() && ! $this->getConfig()->isQuorum()) {
+ $arguments['x-max-priority'] = $this->getConfig()->getQueueMaxPriority();
}
- if ($this->queueOptions['exclusive']) {
- $queue->addFlag(AmqpQueue::FLAG_EXCLUSIVE);
+
+ if ($this->getConfig()->isRerouteFailed()) {
+ $arguments['x-dead-letter-exchange'] = $this->getFailedExchange();
+ $arguments['x-dead-letter-routing-key'] = $this->getFailedRoutingKey($destination);
}
- if ($this->queueOptions['auto_delete']) {
- $queue->addFlag(AmqpQueue::FLAG_AUTODELETE);
+
+ if ($this->getConfig()->isQuorum()) {
+ $arguments['x-queue-type'] = 'quorum';
}
- if ($this->queueOptions['declare'] && !in_array($queueName, $this->declaredQueues, true)) {
- $this->context->declareQueue($queue);
+ return $arguments;
+ }
- $this->declaredQueues[] = $queueName;
+ /**
+ * 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,
+ ];
+ }
+
+ /**
+ * Get the exchange name, or empty string; as default value.
+ */
+ protected function getExchange(?string $exchange = null): string
+ {
+ return $exchange ?? $this->getConfig()->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->getConfig()->getExchangeRoutingKey(), $destination), '.');
+ }
+
+ /**
+ * Get the exchangeType, or AMQPExchangeType::DIRECT as default.
+ */
+ protected function getExchangeType(?string $type = null): string
+ {
+ $constant = AMQPExchangeType::class.'::'.Str::upper($type ?: $this->getConfig()->getExchangeType());
+
+ return defined($constant) ? constant($constant) : AMQPExchangeType::DIRECT;
+ }
+
+ /**
+ * Get the exchange for failed messages.
+ */
+ protected function getFailedExchange(?string $exchange = null): string
+ {
+ return $exchange ?? $this->getConfig()->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->getConfig()->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);
}
- if ($this->queueOptions['bind']) {
- $this->context->bind(new AmqpBind($queue, $topic, $queue->getQueueName()));
+ // When an exchange is provided, just return.
+ if ($exchange) {
+ return;
}
- return [$queue, $topic];
+ // 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));
}
/**
- * @param string $action
- * @param \Exception $e
- * @throws \Exception
+ * Determine all publish properties.
*/
- protected function reportConnectionError($action, \Exception $e)
+ protected function publishProperties($queue, array $options = []): array
{
- /** @var LoggerInterface $logger */
- $logger = $this->container['log'];
+ $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];
+ }
- $logger->error('AMQP error while attempting ' . $action . ': ' . $e->getMessage());
+ protected function getConfig(): QueueConfig
+ {
+ return $this->config;
+ }
- // 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', null, $e);
+ /**
+ * @throws AMQPChannelClosedException
+ * @throws AMQPConnectionClosedException
+ * @throws AMQPConnectionBlockedException
+ */
+ protected function publishBasic($msg, $exchange = '', $destination = '', $mandatory = false, $immediate = false, $ticket = null): void
+ {
+ $this->getChannel()->basic_publish($msg, $exchange, $destination, $mandatory, $immediate, $ticket);
+ }
+
+ 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..02181c96
--- /dev/null
+++ b/tests/Feature/SslQueueTest.php
@@ -0,0 +1,50 @@
+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..5afda85d
--- /dev/null
+++ b/tests/Feature/TestCase.php
@@ -0,0 +1,449 @@
+connection()->isQueueExists()) {
+ $this->connection()->purge();
+ }
+ }
+
+ /**
+ * @throws AMQPProtocolChannelException
+ */
+ protected function tearDown(): void
+ {
+ 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..1c5d94fb
--- /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, 'config')->isRerouteFailed());
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertTrue($this->callProperty($queue, 'config')->isRerouteFailed());
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertFalse($this->callProperty($queue, 'config')->isRerouteFailed());
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertFalse($this->callProperty($queue, 'config')->isRerouteFailed());
+ }
+
+ public function testConfigPrioritizeDelayed(): void
+ {
+ $queue = $this->connection();
+ $this->assertFalse($this->callProperty($queue, 'config')->isPrioritizeDelayed());
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertTrue($this->callProperty($queue, 'config')->isPrioritizeDelayed());
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertFalse($this->callProperty($queue, 'config')->isPrioritizeDelayed());
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertFalse($this->callProperty($queue, 'config')->isPrioritizeDelayed());
+ }
+
+ public function testQueueMaxPriority(): void
+ {
+ $queue = $this->connection();
+ $this->assertIsInt($this->callProperty($queue, 'config')->getQueueMaxPriority());
+ $this->assertSame(2, $this->callProperty($queue, 'config')->getQueueMaxPriority());
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertIsInt($this->callProperty($queue, 'config')->getQueueMaxPriority());
+ $this->assertSame(20, $this->callProperty($queue, 'config')->getQueueMaxPriority());
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertIsInt($this->callProperty($queue, 'config')->getQueueMaxPriority());
+ $this->assertSame(2, $this->callProperty($queue, 'config')->getQueueMaxPriority());
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertIsInt($this->callProperty($queue, 'config')->getQueueMaxPriority());
+ $this->assertSame(2, $this->callProperty($queue, 'config')->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, 'config')->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, 'config')->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, 'config')->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, 'config')->isQuorum());
+
+ $queue = $this->connection('rabbitmq-with-options');
+ $this->assertFalse($this->callProperty($queue, 'config')->isQuorum());
+
+ $queue = $this->connection('rabbitmq-with-options-empty');
+ $this->assertFalse($this->callProperty($queue, 'config')->isQuorum());
+
+ $queue = $this->connection('rabbitmq-with-options-null');
+ $this->assertFalse($this->callProperty($queue, 'config')->isQuorum());
+
+ $queue = $this->connection('rabbitmq-with-quorum-options');
+ $this->assertTrue($this->callProperty($queue, 'config')->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/SendAndReceiveDelayedMessageTest.php b/tests/Functional/SendAndReceiveDelayedMessageTest.php
deleted file mode 100644
index 03c33c0b..00000000
--- a/tests/Functional/SendAndReceiveDelayedMessageTest.php
+++ /dev/null
@@ -1,96 +0,0 @@
- AmqpConnectionFactory::class,
- 'dsn' => null,
- 'host' => getenv('HOST'),
- 'port' => getenv('PORT'),
- 'login' => 'guest',
- 'password' => 'guest',
- 'vhost' => '/',
- 'options' => [
- 'exchange' => [
- 'name' => null,
- 'declare' => true,
- 'type' => \Interop\Amqp\AmqpTopic::TYPE_DIRECT,
- 'passive' => false,
- 'durable' => true,
- 'auto_delete' => false,
- ],
-
- 'queue' => [
- 'name' => 'default',
- 'declare' => true,
- 'bind' => true,
- 'passive' => false,
- 'durable' => true,
- 'exclusive' => false,
- 'auto_delete' => false,
- 'arguments' => '[]',
- ],
- ],
- 'ssl_params' => [
- 'ssl_on' => false,
- 'cafile' => null,
- 'local_cert' => null,
- 'local_key' => null,
- 'verify_peer' => true,
- 'passphrase' => null,
- ]
- ];
-
- $connector = new RabbitMQConnector(new Dispatcher());
- /** @var RabbitMQQueue $queue */
- $queue = $connector->connect($config);
- $queue->setContainer($this->createDummyContainer());
-
- // we need it to declare exchange\queue on RabbitMQ side.
- $queue->pushRaw('something');
-
- $queue->getContext()->purgeQueue($queue->getContext()->createQueue('default'));
-
- $expectedPayload = __METHOD__.microtime(true);
-
- $queue->pushRaw($expectedPayload, null, ['delay' => 3]);
-
- sleep(1);
-
- $this->assertNull($queue->pop());
-
- sleep(4);
-
- $job = $queue->pop();
-
- $this->assertInstanceOf(RabbitMQJob::class, $job);
- $this->assertSame($expectedPayload, $job->getRawBody());
-
- $job->delete();
- }
-
- private function createDummyContainer()
- {
- $container = new Container();
- $container['log'] = new NullLogger();
-
- return $container;
- }
-}
diff --git a/tests/Functional/SendAndReceiveMessageTest.php b/tests/Functional/SendAndReceiveMessageTest.php
deleted file mode 100644
index 562a9cc0..00000000
--- a/tests/Functional/SendAndReceiveMessageTest.php
+++ /dev/null
@@ -1,92 +0,0 @@
- AmqpConnectionFactory::class,
- 'dsn' => null,
- 'host' => getenv('HOST'),
- 'port' => getenv('PORT'),
- 'login' => 'guest',
- 'password' => 'guest',
- 'vhost' => '/',
- 'options' => [
- 'exchange' => [
- 'name' => null,
- 'declare' => true,
- 'type' => \Interop\Amqp\AmqpTopic::TYPE_DIRECT,
- 'passive' => false,
- 'durable' => true,
- 'auto_delete' => false,
- ],
-
- 'queue' => [
- 'name' => 'default',
- 'declare' => true,
- 'bind' => true,
- 'passive' => false,
- 'durable' => true,
- 'exclusive' => false,
- 'auto_delete' => false,
- 'arguments' => '[]',
- ],
- ],
- 'ssl_params' => [
- 'ssl_on' => false,
- 'cafile' => null,
- 'local_cert' => null,
- 'local_key' => null,
- 'verify_peer' => true,
- 'passphrase' => null,
- ]
- ];
-
- $connector = new RabbitMQConnector(new Dispatcher());
- /** @var RabbitMQQueue $queue */
- $queue = $connector->connect($config);
- $queue->setContainer($this->createDummyContainer());
-
- // we need it to declare exchange\queue on RabbitMQ side.
- $queue->pushRaw('something');
-
- $queue->getContext()->purgeQueue($queue->getContext()->createQueue('default'));
-
- $expectedPayload = __METHOD__.microtime(true);
-
- $queue->pushRaw($expectedPayload);
-
- sleep(1);
-
- $job = $queue->pop();
-
- $this->assertInstanceOf(RabbitMQJob::class, $job);
- $this->assertSame($expectedPayload, $job->getRawBody());
-
- $job->delete();
- }
-
- private function createDummyContainer()
- {
- $container = new Container();
- $container['log'] = new NullLogger();
-
- return $container;
- }
-}
diff --git a/tests/Functional/SslConnectionTest.php b/tests/Functional/SslConnectionTest.php
deleted file mode 100644
index 87310f34..00000000
--- a/tests/Functional/SslConnectionTest.php
+++ /dev/null
@@ -1,73 +0,0 @@
- AmqpConnectionFactory::class,
- 'dsn' => null,
- 'host' => getenv('HOST'),
- 'port' => getenv('PORT_SSL'),
- 'login' => 'guest',
- 'password' => 'guest',
- 'vhost' => '/',
- 'options' => [
- 'exchange' => [
- 'name' => null,
- 'declare' => true,
- 'type' => \Interop\Amqp\AmqpTopic::TYPE_DIRECT,
- 'passive' => false,
- 'durable' => true,
- 'auto_delete' => false,
- ],
-
- 'queue' => [
- 'name' => 'default',
- 'declare' => true,
- 'bind' => true,
- 'passive' => false,
- 'durable' => true,
- 'exclusive' => false,
- 'auto_delete' => false,
- 'arguments' => '[]',
- ],
- ],
- 'ssl_params' => [
- 'ssl_on' => true,
- 'cafile' => getenv('RABBITMQ_SSL_CAFILE'),
- 'local_cert' => null,
- 'local_key' => null,
- 'verify_peer' => false,
- 'passphrase' => null,
- ]
- ];
-
- $connector = new RabbitMQConnector(new Dispatcher());
- /** @var RabbitMQQueue $queue */
- $queue = $connector->connect($config);
-
- $this->assertInstanceOf(RabbitMQQueue::class, $queue);
-
- /** @var AmqpContext $context */
- $context = $queue->getContext();
- $this->assertInstanceOf(AmqpContext::class, $context);
-
- $this->assertInstanceOf(AMQPSSLConnection::class, $context->getLibChannel()->getConnection());
- $this->assertTrue($context->getLibChannel()->getConnection()->isConnected());
- }
-}
diff --git a/tests/Functional/StreamConnectionTest.php b/tests/Functional/StreamConnectionTest.php
deleted file mode 100644
index b1558baf..00000000
--- a/tests/Functional/StreamConnectionTest.php
+++ /dev/null
@@ -1,73 +0,0 @@
- AmqpConnectionFactory::class,
- 'dsn' => null,
- 'host' => getenv('HOST'),
- 'port' => getenv('PORT'),
- 'login' => 'guest',
- 'password' => 'guest',
- 'vhost' => '/',
- 'options' => [
- 'exchange' => [
- 'name' => null,
- 'declare' => true,
- 'type' => \Interop\Amqp\AmqpTopic::TYPE_DIRECT,
- 'passive' => false,
- 'durable' => true,
- 'auto_delete' => false,
- ],
-
- 'queue' => [
- 'name' => 'default',
- 'declare' => true,
- 'bind' => true,
- 'passive' => false,
- 'durable' => true,
- 'exclusive' => false,
- 'auto_delete' => false,
- 'arguments' => '[]',
- ],
- ],
- 'ssl_params' => [
- 'ssl_on' => false,
- 'cafile' => null,
- 'local_cert' => null,
- 'local_key' => null,
- 'verify_peer' => true,
- 'passphrase' => null,
- ]
- ];
-
- $connector = new RabbitMQConnector(new Dispatcher());
- /** @var RabbitMQQueue $queue */
- $queue = $connector->connect($config);
-
- $this->assertInstanceOf(RabbitMQQueue::class, $queue);
-
- /** @var AmqpContext $context */
- $context = $queue->getContext();
- $this->assertInstanceOf(AmqpContext::class, $context);
-
- $this->assertInstanceOf(AMQPStreamConnection::class, $context->getLibChannel()->getConnection());
- $this->assertTrue($context->getLibChannel()->getConnection()->isConnected());
- }
-}
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/LaravelQueueRabbitMQServiceProviderTest.php b/tests/LaravelQueueRabbitMQServiceProviderTest.php
deleted file mode 100644
index c74a440f..00000000
--- a/tests/LaravelQueueRabbitMQServiceProviderTest.php
+++ /dev/null
@@ -1,65 +0,0 @@
-assertTrue($rc->isSubclassOf(ServiceProvider::class));
- }
-
- public function testShouldMergeQueueConfigOnRegister()
- {
- $dir = realpath(__DIR__.'/../src');
-
- //guard
- $this->assertDirectoryExists($dir);
-
- $providerMock = $this->createPartialMock(LaravelQueueRabbitMQServiceProvider::class, ['mergeConfigFrom']);
-
- $providerMock
- ->expects($this->once())
- ->method('mergeConfigFrom')
- ->with($dir.'/../config/rabbitmq.php', 'queue.connections.rabbitmq')
- ;
-
- $providerMock->register();
- }
-
- public function testShouldAddRabbitMQConnectorOnBoot()
- {
- $dispatcherMock = $this->createMock(Dispatcher::class);
-
- $queueMock = $this->createMock(QueueManager::class);
- $queueMock
- ->expects($this->once())
- ->method('addConnector')
- ->with('rabbitmq', $this->isInstanceOf(\Closure::class))
- ->willReturnCallback(function ($driver, \Closure $resolver) use ($dispatcherMock) {
- $connector = $resolver();
-
- $this->assertInstanceOf(RabbitMQConnector::class, $connector);
- $this->assertAttributeSame($dispatcherMock, 'dispatcher', $connector);
- })
- ;
-
- $app = Container::getInstance();
- $app['queue'] = $queueMock;
- $app['events'] = $dispatcherMock;
-
- $providerMock = new LaravelQueueRabbitMQServiceProvider($app);
-
- $providerMock->boot();
- }
-}
diff --git a/tests/Mock/AmqpConnectionFactorySpy.php b/tests/Mock/AmqpConnectionFactorySpy.php
deleted file mode 100644
index 5db9fdf8..00000000
--- a/tests/Mock/AmqpConnectionFactorySpy.php
+++ /dev/null
@@ -1,20 +0,0 @@
-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/Queue/Connectors/RabbitMQConnectorTest.php b/tests/Queue/Connectors/RabbitMQConnectorTest.php
deleted file mode 100644
index 3d2ea1bc..00000000
--- a/tests/Queue/Connectors/RabbitMQConnectorTest.php
+++ /dev/null
@@ -1,193 +0,0 @@
-assertTrue($rc->implementsInterface(ConnectorInterface::class));
- }
-
- public function testCouldBeConstructedWithDispatcherAsFirstArgument()
- {
- new RabbitMQConnector($this->createMock(Dispatcher::class));
- }
-
- public function testThrowsIfFactoryClassIsMissing()
- {
- $connector = new RabbitMQConnector($this->createMock(Dispatcher::class));
-
- $this->expectException(\LogicException::class);
- $this->expectExceptionMessage('The factory_class option is missing though it is required.');
- $connector->connect([]);
- }
-
- public function testThrowsIfFactoryClassIsNotValidClass()
- {
- $connector = new RabbitMQConnector($this->createMock(Dispatcher::class));
-
- $this->expectException(\LogicException::class);
- $this->expectExceptionMessage('The factory_class option has to be valid class that implements "Interop\Amqp\AmqpConnectionFactory"');
- $connector->connect(['factory_class' => 'invalidClassName']);
- }
-
- public function testThrowsIfFactoryClassDoesNotImplementConnectorFactoryInterface()
- {
- $connector = new RabbitMQConnector($this->createMock(Dispatcher::class));
-
- $this->expectException(\LogicException::class);
- $this->expectExceptionMessage('The factory_class option has to be valid class that implements "Interop\Amqp\AmqpConnectionFactory"');
- $connector->connect(['factory_class' => \stdClass::class]);
- }
-
- public function testShouldPassExpectedConfigToConnectionFactory()
- {
- $called = false;
- AmqpConnectionFactorySpy::$spy = function ($config) use (&$called) {
- $called = true;
-
- $this->assertEquals([
- 'dsn' => 'theDsn',
- 'host' => 'theHost',
- 'port' => 'thePort',
- 'user' => 'theLogin',
- 'pass' => 'thePassword',
- 'vhost' => 'theVhost',
- 'ssl_on' => 'theSslOn',
- 'ssl_verify' => 'theVerifyPeer',
- 'ssl_cacert' => 'theCafile',
- 'ssl_cert' => 'theLocalCert',
- 'ssl_key' => 'theLocalKey',
- 'ssl_passphrase' => 'thePassPhrase',
- ], $config);
- };
-
- $connector = new RabbitMQConnector($this->createMock(Dispatcher::class));
-
- $config = $this->createDummyConfig();
- $config['factory_class'] = AmqpConnectionFactorySpy::class;
-
- $connector->connect($config);
-
- $this->assertTrue($called);
- }
-
- public function testShouldReturnExpectedInstanceOfQueueOnConnect()
- {
- $connector = new RabbitMQConnector($this->createMock(Dispatcher::class));
-
- $config = $this->createDummyConfig();
- $config['factory_class'] = AmqpConnectionFactorySpy::class;
-
- $queue = $connector->connect($config);
-
- $this->assertInstanceOf(RabbitMQQueue::class, $queue);
- }
-
- public function testShouldSetRabbitMqDlxDelayStrategyIfConnectionFactoryImplementsDelayStrategyAwareInterface()
- {
- $connector = new RabbitMQConnector($this->createMock(Dispatcher::class));
-
- $called = false;
- DelayStrategyAwareAmqpConnectionFactorySpy::$spy = function ($actualStrategy) use (&$called) {
- $this->assertInstanceOf(RabbitMqDlxDelayStrategy::class, $actualStrategy);
-
- $called = true;
- };
-
- $config = $this->createDummyConfig();
- $config['factory_class'] = DelayStrategyAwareAmqpConnectionFactorySpy::class;
-
- $connector->connect($config);
-
- $this->assertTrue($called);
- }
-
- public function testShouldCallContextCloseMethodOnWorkerStoppingEvent()
- {
- $contextMock = $this->createMock(AmqpContext::class);
- $contextMock
- ->expects($this->once())
- ->method('close')
- ;
-
- $dispatcherMock = $this->createMock(Dispatcher::class);
- $dispatcherMock
- ->expects($this->once())
- ->method('listen')
- ->with(WorkerStopping::class, $this->isInstanceOf(\Closure::class))
- ->willReturnCallback(function ($eventName, \Closure $listener) {
- $listener();
- })
- ;
-
- CustomContextAmqpConnectionFactoryMock::$context = $contextMock;
-
- $connector = new RabbitMQConnector($dispatcherMock);
-
- $config = $this->createDummyConfig();
- $config['factory_class'] = CustomContextAmqpConnectionFactoryMock::class;
-
- $connector->connect($config);
- }
-
- /**
- * @return array
- */
- private function createDummyConfig()
- {
- return [
- 'dsn' => 'theDsn',
- 'host' => 'theHost',
- 'port' => 'thePort',
- 'login' => 'theLogin',
- 'password' => 'thePassword',
- 'vhost' => 'theVhost',
- 'ssl_params' => [
- 'ssl_on' => 'theSslOn',
- 'verify_peer' => 'theVerifyPeer',
- 'cafile' => 'theCafile',
- 'local_cert' => 'theLocalCert',
- 'local_key' => 'theLocalKey',
- 'passphrase' => 'thePassPhrase',
- ],
- 'options' => [
- 'exchange' => [
- 'name' => 'anExchangeName',
- 'declare' => false,
- 'type' => \Interop\Amqp\AmqpTopic::TYPE_DIRECT,
- 'passive' => false,
- 'durable' => true,
- 'auto_delete' => false,
- ],
-
- 'queue' => [
- 'name' => 'aQueueName',
- 'declare' => false,
- 'bind' => false,
- 'passive' => false,
- 'durable' => true,
- 'exclusive' => false,
- 'auto_delete' => false,
- 'arguments' => '[]',
- ],
- ],
- 'sleep_on_error' => env('RABBITMQ_ERROR_SLEEP', 5),
- ];
- }
-}
diff --git a/tests/Queue/Jobs/RabbitMQJobTest.php b/tests/Queue/Jobs/RabbitMQJobTest.php
deleted file mode 100644
index 9f5695a4..00000000
--- a/tests/Queue/Jobs/RabbitMQJobTest.php
+++ /dev/null
@@ -1,88 +0,0 @@
-assertTrue($rc->implementsInterface(JobContract::class));
- }
-
- public function testShouldBeSubClassOfQueue()
- {
- $rc = new \ReflectionClass(RabbitMQJob::class);
-
- $this->assertTrue($rc->isSubclassOf(Job::class));
- }
-
- public function testShouldUseDetectDeadlocksTrait()
- {
- $rc = new \ReflectionClass(RabbitMQJob::class);
-
- $this->assertContains(DetectsDeadlocks::class, $rc->getTraitNames());
- }
-
- public function testCouldBeConstructedWithExpectedArguments()
- {
- $queue = $this->createMock(\Interop\Amqp\AmqpQueue::class);
- $queue
- ->expects($this->once())
- ->method('getQueueName')
- ->willReturn('theQueueName')
- ;
-
- $consumerMock = $this->createConsumerMock();
- $consumerMock
- ->expects($this->once())
- ->method('getQueue')
- ->willReturn($queue)
- ;
-
- $connectionMock = $this->createRabbitMQQueueMock();
- $connectionMock
- ->expects($this->any())
- ->method('getConnectionName')
- ->willReturn('theConnectionName')
- ;
-
- $job = new RabbitMQJob(
- new Container(),
- $connectionMock,
- $consumerMock,
- new AmqpMessage()
- );
-
- $this->assertAttributeSame('theQueueName', 'queue', $job);
- $this->assertSame('theConnectionName', $job->getConnectionName());
- }
-
- /**
- * @return AmqpConsumer|\PHPUnit_Framework_MockObject_MockObject|AmqpConsumer
- */
- private function createConsumerMock()
- {
- return $this->createMock(AmqpConsumer::class);
- }
-
- /**
- * @return \PHPUnit_Framework_MockObject_MockObject|RabbitMQQueue|RabbitMQQueue
- */
- private function createRabbitMQQueueMock()
- {
- return $this->createMock(RabbitMQQueue::class);
- }
-}
diff --git a/tests/Queue/RabbitMQQueueTest.php b/tests/Queue/RabbitMQQueueTest.php
deleted file mode 100644
index a07e79bd..00000000
--- a/tests/Queue/RabbitMQQueueTest.php
+++ /dev/null
@@ -1,498 +0,0 @@
-assertTrue($rc->implementsInterface(\Illuminate\Contracts\Queue\Queue::class));
- }
-
- public function testShouldBeSubClassOfQueue()
- {
- $rc = new \ReflectionClass(RabbitMQQueue::class);
-
- $this->assertTrue($rc->isSubclassOf(\Illuminate\Queue\Queue::class));
- }
-
- public function testCouldBeConstructedWithExpectedArguments()
- {
- new RabbitMQQueue($this->createAmqpContext(), $this->createDummyConfig());
- }
-
- public function testShouldGenerateNewCorrelationIdIfNotSet()
- {
- $queue = new RabbitMQQueue($this->createAmqpContext(), $this->createDummyConfig());
-
- $firstId = $queue->getCorrelationId();
- $secondId = $queue->getCorrelationId();
-
- $this->assertNotEmpty($firstId);
- $this->assertNotEmpty($secondId);
- $this->assertNotSame($firstId, $secondId);
- }
-
- public function testShouldReturnPreviouslySetCorrelationId()
- {
- $expectedId = 'theCorrelationId';
-
- $queue = new RabbitMQQueue($this->createAmqpContext(), $this->createDummyConfig());
-
- $queue->setCorrelationId($expectedId);
-
- $this->assertSame($expectedId, $queue->getCorrelationId());
- $this->assertSame($expectedId, $queue->getCorrelationId());
- }
-
- public function testShouldAllowGetContextSetInConstructor()
- {
- $context = $this->createAmqpContext();
-
- $queue = new RabbitMQQueue($context, $this->createDummyConfig());
-
- $this->assertSame($context, $queue->getContext());
- }
-
- public function testShouldReturnExpectedNumberOfMessages()
- {
- $expectedQueueName = 'theQueueName';
- $queue = $this->createMock(AmqpQueue::class);
- $expectedCount = 123321;
-
- $context = $this->createAmqpContext();
- $context
- ->expects($this->once())
- ->method('createTopic')
- ->willReturn($this->createMock(AmqpTopic::class))
- ;
- $context
- ->expects($this->once())
- ->method('createQueue')
- ->with($expectedQueueName)
- ->willReturn($queue)
- ;
- $context
- ->expects($this->once())
- ->method('declareQueue')
- ->with($this->identicalTo($queue))
- ->willReturn($expectedCount)
- ;
-
- $queue = new RabbitMQQueue($context, $this->createDummyConfig());
- $queue->setContainer($this->createDummyContainer());
-
- $this->assertSame($expectedCount, $queue->size($expectedQueueName));
- }
-
- public function testShouldSendExpectedMessageOnPushRaw()
- {
- $expectedQueueName = 'theQueueName';
- $expectedBody = 'thePayload';
- $topic = $this->createMock(AmqpTopic::class);
-
- $queue = $this->createMock(AmqpQueue::class);
- $queue->expects($this->any())->method('getQueueName')->willReturn('theQueueName');
-
- $producer = $this->createMock(AmqpProducer::class);
- $producer
- ->expects($this->once())
- ->method('send')
- ->with($this->identicalTo($topic), $this->isInstanceOf(AmqpMessage::class))
- ->willReturnCallback(function ($actualTopic, AmqpMessage $message) use ($expectedQueueName, $expectedBody, $topic) {
- $this->assertSame($topic, $actualTopic);
- $this->assertSame($expectedBody, $message->getBody());
- $this->assertSame($expectedQueueName, $message->getRoutingKey());
- $this->assertSame('application/json', $message->getContentType());
- $this->assertSame(AmqpMessage::DELIVERY_MODE_PERSISTENT, $message->getDeliveryMode());
- $this->assertNotEmpty($message->getCorrelationId());
- $this->assertNull($message->getProperty(RabbitMQJob::ATTEMPT_COUNT_HEADERS_KEY));
- })
- ;
- $producer
- ->expects($this->never())
- ->method('setDeliveryDelay')
- ;
-
- $context = $this->createAmqpContext();
- $context
- ->expects($this->once())
- ->method('createTopic')
- ->willReturn($topic)
- ;
- $context
- ->expects($this->once())
- ->method('createMessage')
- ->with($expectedBody)
- ->willReturn(new \Interop\Amqp\Impl\AmqpMessage($expectedBody))
- ;
-
- $context
- ->expects($this->once())
- ->method('createQueue')
- ->with($expectedQueueName)
- ->willReturn($queue)
- ;
- $context
- ->expects($this->once())
- ->method('createProducer')
- ->willReturn($producer)
- ;
-
- $queue = new RabbitMQQueue($context, $this->createDummyConfig());
- $queue->setContainer($this->createDummyContainer());
-
- $queue->pushRaw('thePayload', $expectedQueueName);
- }
-
- public function testShouldSetAttemptCountPropIfNotNull()
- {
- $expectedAttempts = 54321;
-
- $topic = $this->createMock(AmqpTopic::class);
-
- $producer = $this->createMock(AmqpProducer::class);
- $producer
- ->expects($this->once())
- ->method('send')
- ->with($this->identicalTo($topic), $this->isInstanceOf(AmqpMessage::class))
- ->willReturnCallback(function ($actualTopic, AmqpMessage $message) use ($expectedAttempts) {
- $this->assertSame($expectedAttempts, $message->getProperty(RabbitMQJob::ATTEMPT_COUNT_HEADERS_KEY));
- })
- ;
- $producer
- ->expects($this->never())
- ->method('setDeliveryDelay')
- ;
-
- $context = $this->createAmqpContext();
- $context
- ->expects($this->once())
- ->method('createTopic')
- ->willReturn($topic)
- ;
- $context
- ->expects($this->once())
- ->method('createMessage')
- ->with()
- ->willReturn(new \Interop\Amqp\Impl\AmqpMessage())
- ;
- $context
- ->expects($this->once())
- ->method('createQueue')
- ->willReturn($this->createMock(AmqpQueue::class))
- ;
- $context
- ->expects($this->once())
- ->method('createProducer')
- ->willReturn($producer)
- ;
-
- $queue = new RabbitMQQueue($context, $this->createDummyConfig());
- $queue->setContainer($this->createDummyContainer());
-
- $queue->pushRaw('thePayload', 'aQueue', ['attempts' => $expectedAttempts]);
- }
-
- public function testShouldSetDeliveryDelayIfDelayOptionPresent()
- {
- $expectedDelay = 56;
- $expectedDeliveryDelay = 56000;
-
- $topic = $this->createMock(AmqpTopic::class);
-
- $producer = $this->createMock(AmqpProducer::class);
- $producer
- ->expects($this->once())
- ->method('send')
- ;
- $producer
- ->expects($this->once())
- ->method('setDeliveryDelay')
- ->with($expectedDeliveryDelay)
- ;
-
- $context = $this->createAmqpContext();
- $context
- ->expects($this->once())
- ->method('createTopic')
- ->willReturn($topic)
- ;
- $context
- ->expects($this->once())
- ->method('createMessage')
- ->with()
- ->willReturn(new \Interop\Amqp\Impl\AmqpMessage())
- ;
- $context
- ->expects($this->once())
- ->method('createQueue')
- ->willReturn($this->createMock(AmqpQueue::class))
- ;
- $context
- ->expects($this->once())
- ->method('createProducer')
- ->willReturn($producer)
- ;
-
- $queue = new RabbitMQQueue($context, $this->createDummyConfig());
- $queue->setContainer($this->createDummyContainer());
-
- $queue->pushRaw('thePayload', 'aQueue', ['delay' => $expectedDelay]);
- }
-
- public function testShouldLogExceptionOnPushRaw()
- {
- $producer = $this->createMock(AmqpProducer::class);
- $producer
- ->expects($this->once())
- ->method('send')
- ->willReturnCallback(function () {
- throw new \LogicException('Something went wrong while sending a message');
- })
- ;
-
- $context = $this->createAmqpContext();
- $context
- ->expects($this->once())
- ->method('createTopic')
- ->willReturn($this->createMock(AmqpTopic::class))
- ;
- $context
- ->expects($this->once())
- ->method('createMessage')
- ->willReturn($this->createMock(AmqpMessage::class))
- ;
- $context
- ->expects($this->once())
- ->method('createQueue')
- ->willReturn($this->createMock(AmqpQueue::class))
- ;
- $context
- ->expects($this->once())
- ->method('createProducer')
- ->willReturn($producer)
- ;
-
- $logger = $this->createMock(LoggerInterface::class);
- $logger
- ->expects($this->once())
- ->method('error')
- ->with('AMQP error while attempting pushRaw: Something went wrong while sending a message')
- ;
-
- $container = new Container();
- $container['log'] = $logger;
-
-
- $queue = new RabbitMQQueue($context, $this->createDummyConfig());
- $queue->setContainer($container);
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('Error writing data to the connection with RabbitMQ');
- $queue->pushRaw('thePayload', 'aQueue');
- }
-
- public function testShouldReturnNullIfNoMessagesOnQueue()
- {
- $queue = $this->createMock(AmqpQueue::class);
-
- $consumer = $this->createMock(AmqpConsumer::class);
- $consumer
- ->expects($this->once())
- ->method('receiveNoWait')
- ->willReturn(null)
- ;
-
- $context = $this->createAmqpContext();
- $context
- ->expects($this->once())
- ->method('createTopic')
- ->willReturn($this->createMock(AmqpTopic::class))
- ;
- $context
- ->expects($this->once())
- ->method('createQueue')
- ->willReturn($queue)
- ;
- $context
- ->expects($this->once())
- ->method('createConsumer')
- ->with($this->identicalTo($queue))
- ->willReturn($consumer)
- ;
-
- $queue = new RabbitMQQueue($context, $this->createDummyConfig());
- $queue->setContainer($this->createDummyContainer());
-
- $this->assertNull($queue->pop('aQueue'));
- }
-
- public function testShouldReturnRabbitMQJobIfMessageReceivedFromQueue()
- {
- $queue = $this->createMock(AmqpQueue::class);
-
- $message = new \Interop\Amqp\Impl\AmqpMessage('thePayload');
-
- $consumer = $this->createMock(AmqpConsumer::class);
- $consumer
- ->expects($this->once())
- ->method('receiveNoWait')
- ->willReturn($message)
- ;
- $consumer
- ->expects($this->once())
- ->method('getQueue')
- ->willReturn($queue)
- ;
-
- $context = $this->createAmqpContext();
- $context
- ->expects($this->once())
- ->method('createTopic')
- ->willReturn($this->createMock(AmqpTopic::class))
- ;
- $context
- ->expects($this->once())
- ->method('createQueue')
- ->willReturn($queue)
- ;
- $context
- ->expects($this->once())
- ->method('createConsumer')
- ->with($this->identicalTo($queue))
- ->willReturn($consumer)
- ;
-
- $queue = new RabbitMQQueue($context, $this->createDummyConfig());
- $queue->setContainer($this->createDummyContainer());
-
- $job = $queue->pop('aQueue');
-
- $this->assertInstanceOf(RabbitMQJob::class, $job);
- }
-
- public function testShouldLogExceptionOnPop()
- {
- $queue = $this->createMock(AmqpQueue::class);
-
- $consumer = $this->createMock(AmqpConsumer::class);
- $consumer
- ->expects($this->once())
- ->method('receiveNoWait')
- ->willReturnCallback(function () {
- throw new \LogicException('Something went wrong while receiving a message');
- })
- ;
-
- $context = $this->createAmqpContext();
- $context
- ->expects($this->once())
- ->method('createTopic')
- ->willReturn($this->createMock(AmqpTopic::class))
- ;
- $context
- ->expects($this->once())
- ->method('createQueue')
- ->willReturn($queue)
- ;
- $context
- ->expects($this->once())
- ->method('createConsumer')
- ->with($this->identicalTo($queue))
- ->willReturn($consumer)
- ;
-
- $logger = $this->createMock(LoggerInterface::class);
- $logger
- ->expects($this->once())
- ->method('error')
- ->with('AMQP error while attempting pop: Something went wrong while receiving a message')
- ;
-
- $container = new Container();
- $container['log'] = $logger;
-
- $queue = new RabbitMQQueue($context, $this->createDummyConfig());
- $queue->setContainer($container);
-
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('Error writing data to the connection with RabbitMQ');
- $queue->pop('aQueue');
- }
-
- /**
- * @return AmqpContext|\PHPUnit_Framework_MockObject_MockObject|AmqpContext
- */
- private function createAmqpContext()
- {
- return $this->createMock(AmqpContext::class);
- }
-
- private function createDummyContainer()
- {
- $logger = $this->createMock(LoggerInterface::class);
-
- $container = new Container();
- $container['log'] = $logger;
-
- return $container;
- }
-
- /**
- * @return array
- */
- private function createDummyConfig()
- {
- return [
- 'dsn' => 'aDsn',
- 'host' => 'aHost',
- 'port' => 'aPort',
- 'login' => 'aLogin',
- 'password' => 'aPassword',
- 'vhost' => 'aVhost',
- 'ssl_params' => [
- 'ssl_on' => 'aSslOn',
- 'verify_peer' => 'aVerifyPeer',
- 'cafile' => 'aCafile',
- 'local_cert' => 'aLocalCert',
- 'local_key' => 'aLocalKey',
- ],
- 'options' => [
- 'exchange' => [
- 'name' => 'anExchangeName',
- 'declare' => false,
- 'type' => \Interop\Amqp\AmqpTopic::TYPE_DIRECT,
- 'passive' => false,
- 'durable' => true,
- 'auto_delete' => false,
- ],
-
- 'queue' => [
- 'name' => 'aQueueName',
- 'declare' => false,
- 'bind' => false,
- 'passive' => false,
- 'durable' => true,
- 'exclusive' => false,
- 'auto_delete' => false,
- 'arguments' => '[]',
- ],
- ],
- 'sleep_on_error' => false,
- ];
- }
-}
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 @@
-