diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..a19e39d81 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +[*.js] +indent_style = space +indent_size = 2 + +[*.php] +indent_style = space +indent_size = 4 + +[{package.json, *.yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..9d23fb5a9 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 30 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: wontfix +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..36c3d5d90 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,171 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + static_analysis: + name: Static analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: technote-space/get-diff-action@v4 + with: + PATTERNS: | + pkg/**/*.php + + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + extensions: mongodb, redis, :xdebug + ini-values: memory_limit=2048M + + - run: php ./bin/fix-symfony-version.php "5.4.*" + + - uses: "ramsey/composer-install@v1" + + - run: sed -i 's/525568/16777471/' vendor/kwn/php-rdkafka-stubs/stubs/constants.php + + - run: cd docker && docker build --rm --force-rm --no-cache --pull --tag "enqueue/dev:latest" -f Dockerfile . + - run: docker run --workdir="/mqdev" -v "`pwd`:/mqdev" --rm enqueue/dev:latest php -d memory_limit=1024M bin/phpstan analyse -l 1 -c phpstan.neon --error-format=github -- ${{ env.GIT_DIFF_FILTERED }} + if: env.GIT_DIFF_FILTERED + + code_style_check: + name: Code style check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: technote-space/get-diff-action@v4 + with: + PATTERNS: | + pkg/**/*.php + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: composer-cs-check-${{ hashFiles('**/composer.json') }} + restore-keys: | + composer-cs-check- + + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + extensions: mongodb, redis, :xdebug + ini-values: memory_limit=2048M + + - run: php ./bin/fix-symfony-version.php "5.4.*" + + - run: composer update --no-progress + + - run: sed -i 's/525568/16777471/' vendor/kwn/php-rdkafka-stubs/stubs/constants.php + + - run: ./bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --no-interaction --dry-run --diff -v --path-mode=intersection -- ${{ env.GIT_DIFF_FILTERED }} + if: env.GIT_DIFF_FILTERED + + unit_tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['8.1', '8.2'] + symfony_version: ['6.2.*', '6.3.*', '6.4.*', '7.0.*'] + dependencies: ['--prefer-lowest', '--prefer-dist'] + exclude: + - php: '8.1' + symfony_version: '7.0.*' + + name: PHP ${{ matrix.php }} unit tests on Sf ${{ matrix.symfony_version }}, deps=${{ matrix.dependencies }} + + steps: + - uses: actions/checkout@v2 + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: composer-${{ matrix.php }}-${{ matrix.symfony_version }}-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + composer-${{ matrix.php }}-${{ matrix.symfony_version }}-${{ matrix.dependencies }}- + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: mongodb, redis, :xdebug + ini-values: memory_limit=2048M + + - run: php ./bin/fix-symfony-version.php "${{ matrix.symfony_version }}" + + - run: composer update --no-progress ${{ matrix.dependencies }} + + - run: sed -i 's/525568/16777471/' vendor/kwn/php-rdkafka-stubs/stubs/constants.php + + - run: bin/phpunit --exclude-group=functional + + functional_tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [ '8.1', '8.2' ] + symfony_version: [ '6.2.*', '6.3.*', '6.4.*', '7.0.*' ] + dependencies: [ '--prefer-lowest', '--prefer-dist' ] + exclude: + - php: '8.1' + symfony_version: '7.0.*' + + name: PHP ${{ matrix.php }} functional tests on Sf ${{ matrix.symfony_version }}, deps=${{ matrix.dependencies }} + + steps: + - uses: actions/checkout@v2 + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + + - uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: composer-${{ matrix.php }}-${{ matrix.symfony_version }}-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + composer-${{ matrix.php }}-${{ matrix.symfony_version }}-${{ matrix.dependencies }}- + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: mongodb, redis, :xdebug + ini-values: memory_limit=2048M + + - run: php ./bin/fix-symfony-version.php "${{ matrix.symfony_version }}" + + - run: composer update --no-progress ${{ matrix.dependencies }} + + - run: sed -i 's/525568/16777471/' vendor/kwn/php-rdkafka-stubs/stubs/constants.php + + - run: bin/dev -b + env: + PHP_VERSION: ${{ matrix.php }} + + # TODO: convert these two steps into one w/o excludes when Gearman extension gets a release for PHP 8.1 + # See https://github.com/php/pecl-networking-gearman/issues/16 + - run: bin/test.sh + if: ${{ matrix.php != '8.1' && matrix.php != '8.2' }} + + - run: bin/test.sh --exclude-group=gearman + if: ${{ matrix.php == '8.1' && matrix.php != '8.2' }} diff --git a/.gitignore b/.gitignore index 6ac624141..7a2e2ec9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,21 @@ *~ /.idea/ bin/doctrine* -bin/php-cs-fixer -bin/phpunit -bin/sql-formatter -bin/phpstan -bin/jp.php -bin/php-parse -bin/google-cloud-batch +bin/php-cs-fixer* +bin/phpunit* +bin/sql-formatter* +bin/phpstan* +bin/jp.php* +bin/php-parse* +bin/google-cloud-batch* +bin/patch-type-declarations* +bin/thruway +bin/var-dump-server* +bin/yaml-lint* vendor var .php_cs .php_cs.cache -composer.lock \ No newline at end of file +composer.lock +.phpunit.result.cache +.php-cs-fixer.cache diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 51% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 99a418499..b9316b59b 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -1,6 +1,7 @@ setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) ->setRiskyAllowed(true) ->setRules(array( '@Symfony' => true, @@ -8,9 +9,13 @@ 'array_syntax' => array('syntax' => 'short'), 'combine_consecutive_unsets' => true, // one should use PHPUnit methods to set up expected exception instead of annotations - 'general_phpdoc_annotation_remove' => array('expectedException', 'expectedExceptionMessage', 'expectedExceptionMessageRegExp'), + 'general_phpdoc_annotation_remove' => ['annotations' => + ['expectedException', 'expectedExceptionMessage', 'expectedExceptionMessageRegExp'] + ], 'heredoc_to_nowdoc' => true, - 'no_extra_consecutive_blank_lines' => array('break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block'), + 'no_extra_blank_lines' => ['tokens' => [ + 'break', 'continue', 'extra', 'return', 'throw', 'use', 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block'] + ], 'no_unreachable_default_argument_value' => true, 'no_useless_else' => true, 'no_useless_return' => true, @@ -18,11 +23,14 @@ 'ordered_imports' => true, 'phpdoc_add_missing_param_annotation' => true, 'phpdoc_order' => true, - 'psr4' => true, + 'psr_autoloading' => true, 'strict_param' => true, + 'native_function_invocation' => false, )) + ->setCacheFile(getenv('TRAVIS') ? getenv('HOME') . '/php-cs-fixer/.php-cs-fixer' : __DIR__.'/var/.php_cs.cache') ->setFinder( PhpCsFixer\Finder::create() + ->name('/\.php$/') ->in(__DIR__) ) -; \ No newline at end of file +; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ed2082dca..000000000 --- a/.travis.yml +++ /dev/null @@ -1,74 +0,0 @@ -sudo: required - -git: - depth: 10 - -language: php - -matrix: - include: - - php: 7.1 - env: SYMFONY_VERSION=3.0.* PHPSTAN=true - - php: 7.1 - env: SYMFONY_VERSION=3.0.* PHP_CS_FIXER=true - - php: 7.0 - env: SYMFONY_VERSION=2.8.* UNIT_TESTS=true - - php: 7.0 - env: SYMFONY_VERSION=3.0.* UNIT_TESTS=true - - php: 7.1 - env: SYMFONY_VERSION=4.0.* UNIT_TESTS=true - - php: 7.1 - env: SYMFONY_VERSION=3.4.* UNIT_TESTS=true - - php: 7.2 - services: docker - env: SYMFONY_VERSION=2.8.* FUNCTIONAL_TESTS=true PREPARE_CONTAINER=true - - php: 7.1 - services: docker - env: SYMFONY_VERSION=3.0.* FUNCTIONAL_TESTS=true PREPARE_CONTAINER=true - - php: 7.1 - services: docker - env: SYMFONY_VERSION=3.2.* FUNCTIONAL_TESTS=true PREPARE_CONTAINER=true - - php: 7.1 - services: docker - env: SYMFONY_VERSION=3.3.* FUNCTIONAL_TESTS=true PREPARE_CONTAINER=true - - php: 7.1 - services: docker - env: SYMFONY_VERSION=4.0.* FUNCTIONAL_TESTS=true PREPARE_CONTAINER=true - - php: 7.1 - services: docker - env: SYMFONY_VERSION=3.3.* RDKAFKA_TESTS=true PREPARE_CONTAINER=true - allow_failures: - - env: SYMFONY_VERSION=3.3.* RDKAFKA_TESTS=true PREPARE_CONTAINER=true - -cache: - directories: - - $HOME/.composer/cache - -before_install: - - echo "extension = mongodb.so" >> $HOME/.phpenv/versions/$(phpenv version-name)/etc/php.ini - -install: - - rm $HOME/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini; - - echo "memory_limit=2048M" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - - composer require symfony/symfony:${SYMFONY_VERSION} --no-update - - composer install - - if [ "$PREPARE_CONTAINER" = true ]; then docker --version; fi - - if [ "$PREPARE_CONTAINER" = true ]; then docker-compose --version; fi - - if [ "$PREPARE_CONTAINER" = true ]; then bin/dev -b; fi - -script: - - if [ "$PHPSTAN" = true ]; then composer require "phpstan/phpstan:^0.8" ; php -d memory_limit=512M bin/phpstan analyse -l 1 -c phpstan.neon pkg/gps pkg/amqp-ext pkg/async-event-dispatcher pkg/dbal pkg/enqueue pkg/enqueue-bundle pkg/fs pkg/gearman pkg/job-queue pkg/null pkg/pheanstalk pkg/redis pkg/simple-client pkg/sqs pkg/stomp pkg/test pkg/rdkafka; fi - - if [ "$PHP_CS_FIXER" = true ]; then IFS=$'\n'; COMMIT_SCA_FILES=($(git diff --name-only --diff-filter=ACMRTUXB "${TRAVIS_COMMIT_RANGE}")); unset IFS; fi - - if [ "$PHP_CS_FIXER" = true ]; then ./bin/php-cs-fixer fix --config=.php_cs.dist -v --dry-run --stop-on-violation --using-cache=no --path-mode=intersection -- "${COMMIT_SCA_FILES[@]}"; fi - - if [ "$UNIT_TESTS" = true ]; then bin/phpunit --exclude-group=functional; fi - - if [ "$FUNCTIONAL_TESTS" = true ]; then bin/run-fun-test.sh --exclude-group=rdkafka; fi - - if [ "RDKAFKA_TESTS" = true ]; then bin/run-fun-test.sh --group=rdkafka; fi - -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/3f8b3668e7792de23a49 - on_success: change - on_failure: always - on_start: never - diff --git a/CHANGELOG.md b/CHANGELOG.md index ad817f19f..ebba0c73f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,575 @@ # Change Log +## [0.10.25](https://github.com/php-enqueue/enqueue-dev/tree/0.10.25) (2025-04-18) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.24...0.10.25) + +**Merged pull requests:** + +- Bugfix/static drift [\#1373](https://github.com/php-enqueue/enqueue-dev/pull/1373) ([JimTools](https://github.com/JimTools)) +- CS Fixes [\#1372](https://github.com/php-enqueue/enqueue-dev/pull/1372) ([JimTools](https://github.com/JimTools)) +- Fixing risky tests [\#1371](https://github.com/php-enqueue/enqueue-dev/pull/1371) ([JimTools](https://github.com/JimTools)) + +## [0.10.24](https://github.com/php-enqueue/enqueue-dev/tree/0.10.24) (2024-11-30) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.23...0.10.24) + +**Merged pull requests:** + +- SF7 deprecations fix [\#1364](https://github.com/php-enqueue/enqueue-dev/pull/1364) ([zavitkov](https://github.com/zavitkov)) +- add symfony 7 support for enqueue-bundle [\#1362](https://github.com/php-enqueue/enqueue-dev/pull/1362) ([zavitkov](https://github.com/zavitkov)) + +## [0.10.23](https://github.com/php-enqueue/enqueue-dev/tree/0.10.23) (2024-10-01) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.22...0.10.23) + +**Merged pull requests:** + +- Drop useless call to end method [\#1359](https://github.com/php-enqueue/enqueue-dev/pull/1359) ([ddziaduch](https://github.com/ddziaduch)) + +## [0.10.22](https://github.com/php-enqueue/enqueue-dev/tree/0.10.22) (2024-08-13) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.21...0.10.22) + +**Merged pull requests:** + +- GPS: revert the attributes and use the headers instead. [\#1355](https://github.com/php-enqueue/enqueue-dev/pull/1355) ([p-pichet](https://github.com/p-pichet)) + +## [0.10.21](https://github.com/php-enqueue/enqueue-dev/tree/0.10.21) (2024-08-12) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.20...0.10.21) + +**Merged pull requests:** + +- feat\(GPS\): allow send attributes in Google PubSub message. [\#1349](https://github.com/php-enqueue/enqueue-dev/pull/1349) ([p-pichet](https://github.com/p-pichet)) + +## [0.10.19](https://github.com/php-enqueue/enqueue-dev/tree/0.10.19) (2023-07-15) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.18...0.10.19) + +**Merged pull requests:** + +- fix: do not reset attemps header when message is requeue [\#1301](https://github.com/php-enqueue/enqueue-dev/pull/1301) ([eortiz-tracktik](https://github.com/eortiz-tracktik)) +- Allow doctrine/persistence 3.1 version [\#1300](https://github.com/php-enqueue/enqueue-dev/pull/1300) ([xNarkon](https://github.com/xNarkon)) +- Add support for rediss and phpredis [\#1297](https://github.com/php-enqueue/enqueue-dev/pull/1297) ([splagemann](https://github.com/splagemann)) +- Replaced `json\_array` with `json` due to Doctrine Dbal 3.0 [\#1294](https://github.com/php-enqueue/enqueue-dev/pull/1294) ([NovakHonza](https://github.com/NovakHonza)) +- pkg PHP 8.1 and 8.2 support [\#1292](https://github.com/php-enqueue/enqueue-dev/pull/1292) ([snapshotpl](https://github.com/snapshotpl)) +- Update doctrine/persistence [\#1290](https://github.com/php-enqueue/enqueue-dev/pull/1290) ([jlabedo](https://github.com/jlabedo)) +- Add PHP 8.1 and 8.2, Symfony 6.2 to CI [\#1285](https://github.com/php-enqueue/enqueue-dev/pull/1285) ([andrewmy](https://github.com/andrewmy)) +- \[SNSQS\] added possibility to send FIFO-related parameters using snsqs transport [\#1278](https://github.com/php-enqueue/enqueue-dev/pull/1278) ([onatskyy](https://github.com/onatskyy)) + +## [0.10.18](https://github.com/php-enqueue/enqueue-dev/tree/0.10.18) (2023-03-18) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.17...0.10.18) + +**Merged pull requests:** + +- Fix Shield URLs in READMEs [\#1289](https://github.com/php-enqueue/enqueue-dev/pull/1289) ([amayer5125](https://github.com/amayer5125)) +- Fix AWS SDK token parameter [\#1284](https://github.com/php-enqueue/enqueue-dev/pull/1284) ([andrewmy](https://github.com/andrewmy)) +- MongoDB - Add combined index [\#1283](https://github.com/php-enqueue/enqueue-dev/pull/1283) ([ddziaduch](https://github.com/ddziaduch)) +- Add setting subscription attributes to Sns and SnsQs [\#1281](https://github.com/php-enqueue/enqueue-dev/pull/1281) ([andrewmy](https://github.com/andrewmy)) +- code style fix \(native\_constant\_invocation\) [\#1276](https://github.com/php-enqueue/enqueue-dev/pull/1276) ([EmilMassey](https://github.com/EmilMassey)) +- \[amqp-lib\] Replace amqp-lib deprecated public property with getters [\#1273](https://github.com/php-enqueue/enqueue-dev/pull/1273) ([ramunasd](https://github.com/ramunasd)) +- fix: parenthesis missing allowed invalid delays [\#1266](https://github.com/php-enqueue/enqueue-dev/pull/1266) ([aldenw](https://github.com/aldenw)) +- Allow rdkafka falsy keys [\#1264](https://github.com/php-enqueue/enqueue-dev/pull/1264) ([qkdreyer](https://github.com/qkdreyer)) +- Symfony config allow null [\#1263](https://github.com/php-enqueue/enqueue-dev/pull/1263) ([h0raz](https://github.com/h0raz)) +- Ensure pass consumer tag as string to bunny amqp [\#1255](https://github.com/php-enqueue/enqueue-dev/pull/1255) ([snapshotpl](https://github.com/snapshotpl)) +- chore: Update dependency dbal [\#1253](https://github.com/php-enqueue/enqueue-dev/pull/1253) ([meidlinga](https://github.com/meidlinga)) + +## [0.10.17](https://github.com/php-enqueue/enqueue-dev/tree/0.10.17) (2022-05-17) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.16...0.10.17) + +**Merged pull requests:** + +- Disable sleep while queue items available [\#1250](https://github.com/php-enqueue/enqueue-dev/pull/1250) ([mordilion](https://github.com/mordilion)) + +## [0.10.16](https://github.com/php-enqueue/enqueue-dev/tree/0.10.16) (2022-04-28) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.15...0.10.16) + +**Merged pull requests:** + +- Upgrade ext-rdkafka to 6.0 [\#1241](https://github.com/php-enqueue/enqueue-dev/pull/1241) ([lucasrivoiro](https://github.com/lucasrivoiro)) +- Replace rabbitmq-management-api with a packagist source and fixed small github actions typo [\#1240](https://github.com/php-enqueue/enqueue-dev/pull/1240) ([oreillysean](https://github.com/oreillysean)) +- Add support for Symfony 6; drop \< 5.1 [\#1239](https://github.com/php-enqueue/enqueue-dev/pull/1239) ([andrewmy](https://github.com/andrewmy)) +- Replace rabbitmq-management-api with a packagist source [\#1238](https://github.com/php-enqueue/enqueue-dev/pull/1238) ([andrewmy](https://github.com/andrewmy)) +- Fix CI [\#1237](https://github.com/php-enqueue/enqueue-dev/pull/1237) ([jdecool](https://github.com/jdecool)) +- Allow ext-rdkafka 6 usage [\#1233](https://github.com/php-enqueue/enqueue-dev/pull/1233) ([jdecool](https://github.com/jdecool)) +- Fix types for Symfony 5.4 [\#1225](https://github.com/php-enqueue/enqueue-dev/pull/1225) ([shyim](https://github.com/shyim)) + +## [0.10.15](https://github.com/php-enqueue/enqueue-dev/tree/0.10.15) (2021-12-11) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.14...0.10.15) + +**Merged pull requests:** + +- feat\(snsqs\): allow client http configuration for sns and sqs [\#1216](https://github.com/php-enqueue/enqueue-dev/pull/1216) ([eortiz-tracktik](https://github.com/eortiz-tracktik)) +- Add FIFO logic to SNS [\#1214](https://github.com/php-enqueue/enqueue-dev/pull/1214) ([kate-simozhenko](https://github.com/kate-simozhenko)) +- Fix falling tests [\#1211](https://github.com/php-enqueue/enqueue-dev/pull/1211) ([snapshotpl](https://github.com/snapshotpl)) +- RdKafka; Replace composer-modifying for testing with --ignore-platform-req argument [\#1210](https://github.com/php-enqueue/enqueue-dev/pull/1210) ([maartenderie](https://github.com/maartenderie)) +- Allow psr/container v2 [\#1206](https://github.com/php-enqueue/enqueue-dev/pull/1206) ([ADmad](https://github.com/ADmad)) + +## [0.10.14](https://github.com/php-enqueue/enqueue-dev/tree/0.10.14) (2021-10-29) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.13...0.10.14) + +**Merged pull requests:** + +- Fix passed parameters for compatibility with newest version of dbal [\#1203](https://github.com/php-enqueue/enqueue-dev/pull/1203) ([dgafka](https://github.com/dgafka)) +- Allow psr/log v2 and v3 [\#1198](https://github.com/php-enqueue/enqueue-dev/pull/1198) ([snapshotpl](https://github.com/snapshotpl)) +- Fix partition's choice for the cases when partition number is zero [\#1196](https://github.com/php-enqueue/enqueue-dev/pull/1196) ([rodrigosarmentopicpay](https://github.com/rodrigosarmentopicpay)) +- Added getter for offset field in RdKafkaConsumer class [\#1184](https://github.com/php-enqueue/enqueue-dev/pull/1184) ([DigitVE](https://github.com/DigitVE)) + +## [0.10.13](https://github.com/php-enqueue/enqueue-dev/tree/0.10.13) (2021-08-25) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.12...0.10.13) + +**Merged pull requests:** + +- \[SNSQS\] added possibility to send message attributes using snsqs transport [\#1195](https://github.com/php-enqueue/enqueue-dev/pull/1195) ([onatskyy](https://github.com/onatskyy)) +- Add in missing arg [\#1194](https://github.com/php-enqueue/enqueue-dev/pull/1194) ([gdsmith](https://github.com/gdsmith)) +- \#1190 add index on delivery\_id to prevent slow queries [\#1191](https://github.com/php-enqueue/enqueue-dev/pull/1191) ([commercewerft](https://github.com/commercewerft)) +- Add setTopicArn methods to SnsContext and SnsQsContext [\#1189](https://github.com/php-enqueue/enqueue-dev/pull/1189) ([gdsmith](https://github.com/gdsmith)) + +## [0.10.11](https://github.com/php-enqueue/enqueue-dev/tree/0.10.11) (2021-04-28) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.10...0.10.11) + +**Merged pull requests:** + +- Perform at least once delivery when rejecting with requeue [\#1165](https://github.com/php-enqueue/enqueue-dev/pull/1165) ([dgafka](https://github.com/dgafka)) +- Fix dbal delivery delay to always keep integer value [\#1161](https://github.com/php-enqueue/enqueue-dev/pull/1161) ([dgafka](https://github.com/dgafka)) +- Add SqsConsumer methods to SnsQsConsumer [\#1160](https://github.com/php-enqueue/enqueue-dev/pull/1160) ([gdsmith](https://github.com/gdsmith)) +- add subscription\_interval as config for dbal subscription consumer [\#1159](https://github.com/php-enqueue/enqueue-dev/pull/1159) ([mordilion](https://github.com/mordilion)) +- register worker callback only once, move to constructor [\#1157](https://github.com/php-enqueue/enqueue-dev/pull/1157) ([cturbelin](https://github.com/cturbelin)) +- Try to change doctrine/orm version for supporting 2.8 \(PHP 8 support\). [\#1155](https://github.com/php-enqueue/enqueue-dev/pull/1155) ([GothShoot](https://github.com/GothShoot)) +- sns context - fallback for not breaking BC with 10.10 previous versions [\#1149](https://github.com/php-enqueue/enqueue-dev/pull/1149) ([bafor](https://github.com/bafor)) + +## [0.10.10](https://github.com/php-enqueue/enqueue-dev/tree/0.10.10) (2021-03-24) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.9...0.10.10) + +**Merged pull requests:** + +- \[sns\] added possibility to define already existing topics \(prevent create topic call\) \#1022 [\#1147](https://github.com/php-enqueue/enqueue-dev/pull/1147) ([paramonov](https://github.com/paramonov)) +- \[gps\] Add support for consuming message from external publisher in non-standard format [\#1118](https://github.com/php-enqueue/enqueue-dev/pull/1118) ([maciejzgadzaj](https://github.com/maciejzgadzaj)) + +## [0.10.9](https://github.com/php-enqueue/enqueue-dev/tree/0.10.9) (2021-03-17) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.8...0.10.9) + +**Merged pull requests:** + +- Upgrade php-amqplib to v3.0 [\#1146](https://github.com/php-enqueue/enqueue-dev/pull/1146) ([masterjus](https://github.com/masterjus)) +- Split tests into different matrices; fix highest/lowest dependencies [\#1139](https://github.com/php-enqueue/enqueue-dev/pull/1139) ([andrewmy](https://github.com/andrewmy)) + +## [0.10.8](https://github.com/php-enqueue/enqueue-dev/tree/0.10.8) (2021-02-17) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.7...0.10.8) + +**Merged pull requests:** + +- Fix package CI [\#1138](https://github.com/php-enqueue/enqueue-dev/pull/1138) ([andrewmy](https://github.com/andrewmy)) +- add sns driver + use profile to establish connection [\#1134](https://github.com/php-enqueue/enqueue-dev/pull/1134) ([fbaudry](https://github.com/fbaudry)) +- Add PHP 8 [\#1132](https://github.com/php-enqueue/enqueue-dev/pull/1132) ([andrewmy](https://github.com/andrewmy)) + +## [0.10.7](https://github.com/php-enqueue/enqueue-dev/tree/0.10.7) (2021-02-03) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.6...0.10.7) + +**Merged pull requests:** + +- PHPUnit 9.5 [\#1131](https://github.com/php-enqueue/enqueue-dev/pull/1131) ([andrewmy](https://github.com/andrewmy)) +- Fix the build matrix [\#1130](https://github.com/php-enqueue/enqueue-dev/pull/1130) ([andrewmy](https://github.com/andrewmy)) +- Disable Travis CI [\#1129](https://github.com/php-enqueue/enqueue-dev/pull/1129) ([makasim](https://github.com/makasim)) +- Add GitHub Action CI [\#1127](https://github.com/php-enqueue/enqueue-dev/pull/1127) ([andrewmy](https://github.com/andrewmy)) +- Allow ext-rdkafka 5 [\#1126](https://github.com/php-enqueue/enqueue-dev/pull/1126) ([andrewmy](https://github.com/andrewmy)) +- Fix - Bad parameter for exception [\#1124](https://github.com/php-enqueue/enqueue-dev/pull/1124) ([atrauzzi](https://github.com/atrauzzi)) +- \[fix\] queue consumption: catch throwable for processing errors [\#1114](https://github.com/php-enqueue/enqueue-dev/pull/1114) ([macghriogair](https://github.com/macghriogair)) +- Ramsey dependency removed in favor to \Enqueue\Util\UUID::generate [\#1110](https://github.com/php-enqueue/enqueue-dev/pull/1110) ([inri13666](https://github.com/inri13666)) +- Added: ability to choose different entity manager [\#1081](https://github.com/php-enqueue/enqueue-dev/pull/1081) ([balabis](https://github.com/balabis)) + +## [0.10.6](https://github.com/php-enqueue/enqueue-dev/tree/0.10.6) (2020-10-16) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.5...0.10.6) + +**Merged pull requests:** + +- fixing issue \#1085 [\#1105](https://github.com/php-enqueue/enqueue-dev/pull/1105) ([nivpenso](https://github.com/nivpenso)) +- Fix DoctrineConnectionFactoryFactory due to doctrine/common changes [\#1089](https://github.com/php-enqueue/enqueue-dev/pull/1089) ([kdefives](https://github.com/kdefives)) + +## [0.10.5](https://github.com/php-enqueue/enqueue-dev/tree/0.10.5) (2020-10-09) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.4...0.10.5) + +**Merged pull requests:** + +- update image [\#1104](https://github.com/php-enqueue/enqueue-dev/pull/1104) ([nick-zh](https://github.com/nick-zh)) +- \[rdkafka\]use supported librdkafka version of ext [\#1103](https://github.com/php-enqueue/enqueue-dev/pull/1103) ([nick-zh](https://github.com/nick-zh)) +- \[rdkafka\] add non-blocking poll call to serve cb's [\#1102](https://github.com/php-enqueue/enqueue-dev/pull/1102) ([nick-zh](https://github.com/nick-zh)) +- \[rdkafka\] remove topic conf, deprecated [\#1101](https://github.com/php-enqueue/enqueue-dev/pull/1101) ([nick-zh](https://github.com/nick-zh)) +- \[stomp\] Fix - Add automatic reconnect support for STOMP producers [\#1099](https://github.com/php-enqueue/enqueue-dev/pull/1099) ([atrauzzi](https://github.com/atrauzzi)) +- fix localstack version \(one that worked\) [\#1094](https://github.com/php-enqueue/enqueue-dev/pull/1094) ([makasim](https://github.com/makasim)) +- Allow false-y values for unsupported options [\#1093](https://github.com/php-enqueue/enqueue-dev/pull/1093) ([atrauzzi](https://github.com/atrauzzi)) +- Lock doctrine perisistence version. Fix tests. [\#1092](https://github.com/php-enqueue/enqueue-dev/pull/1092) ([makasim](https://github.com/makasim)) + +## [0.10.4](https://github.com/php-enqueue/enqueue-dev/tree/0.10.4) (2020-09-24) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.3...0.10.4) + +**Merged pull requests:** + +- \[stomp\] Add first pass for Apache ActiveMQ Artemis support [\#1091](https://github.com/php-enqueue/enqueue-dev/pull/1091) ([atrauzzi](https://github.com/atrauzzi)) +- \[amqp\]Solves binding Headers Exchange with Queue using custom arguments [\#1087](https://github.com/php-enqueue/enqueue-dev/pull/1087) ([dgafka](https://github.com/dgafka)) +- \[async-command\] Fix service definition to apply the timeout [\#1084](https://github.com/php-enqueue/enqueue-dev/pull/1084) ([jcrombez](https://github.com/jcrombez)) +- \[mongodb\] fix\(MongoDB\) Redelivery not working \(fixes \#1077\) [\#1078](https://github.com/php-enqueue/enqueue-dev/pull/1078) ([josefsabl](https://github.com/josefsabl)) +- Add php 7.3 and 7.4 travis env to every package [\#1076](https://github.com/php-enqueue/enqueue-dev/pull/1076) ([snapshotpl](https://github.com/snapshotpl)) +- Docs: update Supported Brokers [\#1074](https://github.com/php-enqueue/enqueue-dev/pull/1074) ([Nebual](https://github.com/Nebual)) +- \[rdkafka\] Compatibility with Phprdkafka 4.0 [\#959](https://github.com/php-enqueue/enqueue-dev/pull/959) ([Steveb-p](https://github.com/Steveb-p)) + +## [0.10.3](https://github.com/php-enqueue/enqueue-dev/tree/0.10.3) (2020-07-31) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.2...0.10.3) + +**Merged pull requests:** + +- Allow to install ramsey/uuid:^4 [\#1075](https://github.com/php-enqueue/enqueue-dev/pull/1075) ([snapshotpl](https://github.com/snapshotpl)) +- chore: add typehint to RdKafkaConsumer\#getQueue [\#1071](https://github.com/php-enqueue/enqueue-dev/pull/1071) ([qkdreyer](https://github.com/qkdreyer)) +- Fixes typo on client messages exemples doc [\#1065](https://github.com/php-enqueue/enqueue-dev/pull/1065) ([brunousml](https://github.com/brunousml)) +- Fix contact us link [\#1058](https://github.com/php-enqueue/enqueue-dev/pull/1058) ([andrew-demb](https://github.com/andrew-demb)) +- Fix typos [\#1049](https://github.com/php-enqueue/enqueue-dev/pull/1049) ([pgrimaud](https://github.com/pgrimaud)) +- Added support for ramsey/uuid 4.0 [\#1043](https://github.com/php-enqueue/enqueue-dev/pull/1043) ([a-menshchikov](https://github.com/a-menshchikov)) +- Changed: cast redelivery\_delay to int [\#1034](https://github.com/php-enqueue/enqueue-dev/pull/1034) ([balabis](https://github.com/balabis)) +- Add php 7.4 to test matrix [\#991](https://github.com/php-enqueue/enqueue-dev/pull/991) ([snapshotpl](https://github.com/snapshotpl)) + +## [0.10.2](https://github.com/php-enqueue/enqueue-dev/tree/0.10.2) (2020-03-20) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.1...0.10.2) + +**Merged pull requests:** + +- Implement DeliveryDelay, Priority and TimeToLive in PheanstalkProducer [\#1033](https://github.com/php-enqueue/enqueue-dev/pull/1033) ([likeuntomurphy](https://github.com/likeuntomurphy)) +- fix\(mongodb\): Exception throwing fatal error, Broken handling of Mong… [\#1032](https://github.com/php-enqueue/enqueue-dev/pull/1032) ([josefsabl](https://github.com/josefsabl)) +- RUN\_COMMAND Option example [\#1030](https://github.com/php-enqueue/enqueue-dev/pull/1030) ([gam6itko](https://github.com/gam6itko)) +- typo [\#1026](https://github.com/php-enqueue/enqueue-dev/pull/1026) ([sebastianneubert](https://github.com/sebastianneubert)) +- Add extension tag parameter note [\#1023](https://github.com/php-enqueue/enqueue-dev/pull/1023) ([Steveb-p](https://github.com/Steveb-p)) +- STOMP. add additional configuration [\#1018](https://github.com/php-enqueue/enqueue-dev/pull/1018) ([versh23](https://github.com/versh23)) + +## [0.10.1](https://github.com/php-enqueue/enqueue-dev/tree/0.10.1) (2020-01-31) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.10.0...0.10.1) + +**Merged pull requests:** + +- \[dbal\] fix: allow absolute paths for sqlite transport [\#1015](https://github.com/php-enqueue/enqueue-dev/pull/1015) ([cawolf](https://github.com/cawolf)) +- \[tests\] Add schema declaration to phpunit files [\#1014](https://github.com/php-enqueue/enqueue-dev/pull/1014) ([Steveb-p](https://github.com/Steveb-p)) +- \[rdkafka\] Catch consume error "Local: Broker transport failure" and continue consume [\#1009](https://github.com/php-enqueue/enqueue-dev/pull/1009) ([rdotter](https://github.com/rdotter)) +- \[sqs\] SQS Transport - Add support for AWS profiles. [\#1008](https://github.com/php-enqueue/enqueue-dev/pull/1008) ([bgaillard](https://github.com/bgaillard)) +- \[amqp\] fixes \#1003 Return value of Enqueue\AmqpLib\AmqpContext::declareQueue() must be of the type int [\#1004](https://github.com/php-enqueue/enqueue-dev/pull/1004) ([kalyabin](https://github.com/kalyabin)) +- \[gearman\] Gearman Consumer receive should only fetch one message [\#998](https://github.com/php-enqueue/enqueue-dev/pull/998) ([arep](https://github.com/arep)) +- \[sqs\] add messageId to the sqsMessage [\#992](https://github.com/php-enqueue/enqueue-dev/pull/992) ([BenoitLeveque](https://github.com/BenoitLeveque)) + +## [0.10.0](https://github.com/php-enqueue/enqueue-dev/tree/0.10.0) (2019-12-19) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.15...0.10.0) + +**Merged pull requests:** + +- Symfony 5 [\#997](https://github.com/php-enqueue/enqueue-dev/pull/997) ([kuraobi](https://github.com/kuraobi)) +- Replace the Magento 1 code into the Magento 2 documentation [\#999](https://github.com/php-enqueue/enqueue-dev/pull/999) ([hochgenug](https://github.com/hochgenug)) +- Wrong parameter description [\#994](https://github.com/php-enqueue/enqueue-dev/pull/994) ([bramstroker](https://github.com/bramstroker)) + +## [0.9.15](https://github.com/php-enqueue/enqueue-dev/tree/0.9.15) (2019-11-28) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.14...0.9.15) + +**Merged pull requests:** + +- Fix Incompatibility for doctrine [\#988](https://github.com/php-enqueue/enqueue-dev/pull/988) ([Baachi](https://github.com/Baachi)) +- Prefer early returns in consumer code [\#982](https://github.com/php-enqueue/enqueue-dev/pull/982) ([Steveb-p](https://github.com/Steveb-p)) +- \#977 - Fix issues with MS SQL server and dbal transport [\#979](https://github.com/php-enqueue/enqueue-dev/pull/979) ([NeilWhitworth](https://github.com/NeilWhitworth)) +- Add header support for Symfony's produce command [\#965](https://github.com/php-enqueue/enqueue-dev/pull/965) ([TiMESPLiNTER](https://github.com/TiMESPLiNTER)) + +## [0.9.14](https://github.com/php-enqueue/enqueue-dev/tree/0.9.14) (2019-10-14) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.13...0.9.14) + +**Merged pull requests:** + +- Fix deprecated heartbeat check method [\#967](https://github.com/php-enqueue/enqueue-dev/pull/967) ([ramunasd](https://github.com/ramunasd)) +- Add missing rabbitmq DSN example [\#966](https://github.com/php-enqueue/enqueue-dev/pull/966) ([ramunasd](https://github.com/ramunasd)) +- Fix empty class for autowired services \(Fix \#957\) [\#958](https://github.com/php-enqueue/enqueue-dev/pull/958) ([NicolasGuilloux](https://github.com/NicolasGuilloux)) +- Add header support for kafka [\#955](https://github.com/php-enqueue/enqueue-dev/pull/955) ([TiMESPLiNTER](https://github.com/TiMESPLiNTER)) +- Kafka singleton consumer [\#947](https://github.com/php-enqueue/enqueue-dev/pull/947) ([dirk39](https://github.com/dirk39)) + +## [0.9.13](https://github.com/php-enqueue/enqueue-dev/tree/0.9.13) (2019-09-03) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.12...0.9.13) + +**Merged pull requests:** + +- docs: describe drawbacks of using amqp extension [\#942](https://github.com/php-enqueue/enqueue-dev/pull/942) ([gnumoksha](https://github.com/gnumoksha)) +- Add a service to reset doctrine/odm identity maps [\#933](https://github.com/php-enqueue/enqueue-dev/pull/933) ([Lctrs](https://github.com/Lctrs)) +- Add an extension to stop consumption on closed entity manager [\#932](https://github.com/php-enqueue/enqueue-dev/pull/932) ([Lctrs](https://github.com/Lctrs)) +- Add an extension to reset services [\#929](https://github.com/php-enqueue/enqueue-dev/pull/929) ([Lctrs](https://github.com/Lctrs)) +- \[DoctrineClearIdentityMapExtension\] allow instances of ManagerRegistry [\#927](https://github.com/php-enqueue/enqueue-dev/pull/927) ([Lctrs](https://github.com/Lctrs)) +- Link to documentation from logo [\#926](https://github.com/php-enqueue/enqueue-dev/pull/926) ([Steveb-p](https://github.com/Steveb-p)) +- DBAL Change ParameterType class to Type class [\#916](https://github.com/php-enqueue/enqueue-dev/pull/916) ([Nevoss](https://github.com/Nevoss)) +- async\_commands: extended configuration proposal [\#914](https://github.com/php-enqueue/enqueue-dev/pull/914) ([uro](https://github.com/uro)) + +## [0.9.12](https://github.com/php-enqueue/enqueue-dev/tree/0.9.12) (2019-06-25) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.11...0.9.12) + +**Merged pull requests:** + +- \[SNSQS\] Fix issue with delay [\#909](https://github.com/php-enqueue/enqueue-dev/pull/909) ([uro](https://github.com/uro)) +- \[SNS\] Fix: Missing throw issue [\#908](https://github.com/php-enqueue/enqueue-dev/pull/908) ([uro](https://github.com/uro)) +- \[SNS\] Adding generic driver for schema SNS [\#906](https://github.com/php-enqueue/enqueue-dev/pull/906) ([Nyholm](https://github.com/Nyholm)) +- \[SQS\] deserialize sqs message attributes [\#901](https://github.com/php-enqueue/enqueue-dev/pull/901) ([bendavies](https://github.com/bendavies)) +- \[SNS\] Updates dependencies requirements for sns\(qs\) [\#899](https://github.com/php-enqueue/enqueue-dev/pull/899) ([xavismeh](https://github.com/xavismeh)) +- Cast int for redelivery\_delay and polling\_interval [\#896](https://github.com/php-enqueue/enqueue-dev/pull/896) ([linh4github](https://github.com/linh4github)) +- \[doc\] Move support note to an external include file [\#892](https://github.com/php-enqueue/enqueue-dev/pull/892) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Allow reading headers from Kafka Message headers [\#891](https://github.com/php-enqueue/enqueue-dev/pull/891) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Fix Code Style in all files [\#889](https://github.com/php-enqueue/enqueue-dev/pull/889) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Move "key concepts" to second position in menu. Fix typos. [\#886](https://github.com/php-enqueue/enqueue-dev/pull/886) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\]\[Bundle\] Expand quick tour for Symfony Bundle [\#885](https://github.com/php-enqueue/enqueue-dev/pull/885) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Fix link for cli commands [\#882](https://github.com/php-enqueue/enqueue-dev/pull/882) ([samnela](https://github.com/samnela)) +- Add composer runnable scripts for PHPStan & PHP-CS [\#881](https://github.com/php-enqueue/enqueue-dev/pull/881) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Fixed quick tour link [\#878](https://github.com/php-enqueue/enqueue-dev/pull/878) ([samnela](https://github.com/samnela)) +- \[doc\] Fix documentation links [\#877](https://github.com/php-enqueue/enqueue-dev/pull/877) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Add editor config settings for IDE's that support it [\#875](https://github.com/php-enqueue/enqueue-dev/pull/875) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Prefer github pages in packages' readme files [\#874](https://github.com/php-enqueue/enqueue-dev/pull/874) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Add Amazon SNS documentation placeholder [\#873](https://github.com/php-enqueue/enqueue-dev/pull/873) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Prefer github pages in readme [\#872](https://github.com/php-enqueue/enqueue-dev/pull/872) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Github Pages - Match topic order from index.md [\#870](https://github.com/php-enqueue/enqueue-dev/pull/870) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Github pages navigation structure [\#869](https://github.com/php-enqueue/enqueue-dev/pull/869) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Fixed the service id for Transport [\#868](https://github.com/php-enqueue/enqueue-dev/pull/868) ([samnela](https://github.com/samnela)) +- \[doc\] Use organization repository for doc hosting [\#867](https://github.com/php-enqueue/enqueue-dev/pull/867) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Switch documentation to github pages [\#866](https://github.com/php-enqueue/enqueue-dev/pull/866) ([Steveb-p](https://github.com/Steveb-p)) +- Prefer stable dependencies for development [\#865](https://github.com/php-enqueue/enqueue-dev/pull/865) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] Key concepts [\#863](https://github.com/php-enqueue/enqueue-dev/pull/863) ([sylfabre](https://github.com/sylfabre)) +- \[doc\] Better Symfony doc nav [\#862](https://github.com/php-enqueue/enqueue-dev/pull/862) ([sylfabre](https://github.com/sylfabre)) + +## [0.9.11](https://github.com/php-enqueue/enqueue-dev/tree/0.9.11) (2019-05-24) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.10...0.9.11) + +**Merged pull requests:** + +- \[client\] Fix --logger option. Removed unintentionally set console logger. [\#861](https://github.com/php-enqueue/enqueue-dev/pull/861) ([makasim](https://github.com/makasim)) +- \[client\] Fix reference to logger service. [\#860](https://github.com/php-enqueue/enqueue-dev/pull/860) ([makasim](https://github.com/makasim)) +- \[consumption\] Fix bindCallback method will require new arg deprecation notice [\#859](https://github.com/php-enqueue/enqueue-dev/pull/859) ([makasim](https://github.com/makasim)) +- \[amqp-bunny\] Revert "Fix heartbeat configuration in bunny with 0 \(off\) value" [\#855](https://github.com/php-enqueue/enqueue-dev/pull/855) ([DamienHarper](https://github.com/DamienHarper)) +- \[sqs\] Requeue with a visibility timeout [\#852](https://github.com/php-enqueue/enqueue-dev/pull/852) ([deguif](https://github.com/deguif)) +- \[monitoring\] Send topic and command for consumed messages [\#849](https://github.com/php-enqueue/enqueue-dev/pull/849) ([mariusbalcytis](https://github.com/mariusbalcytis)) +- Fixed typo [\#856](https://github.com/php-enqueue/enqueue-dev/pull/856) ([samnela](https://github.com/samnela)) + +## [0.9.10](https://github.com/php-enqueue/enqueue-dev/tree/0.9.10) (2019-05-14) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.9...0.9.10) + +**Merged pull requests:** + +- \[client\] Lazy producer. [\#845](https://github.com/php-enqueue/enqueue-dev/pull/845) ([makasim](https://github.com/makasim)) +- \[kafka\] Fix consumption errors in kafka against recent versions in librdkafka/phprdkafka [\#842](https://github.com/php-enqueue/enqueue-dev/pull/842) ([Steveb-p](https://github.com/Steveb-p)) +- \[amqp-lib\] Fix un-initialized property use [\#836](https://github.com/php-enqueue/enqueue-dev/pull/836) ([Steveb-p](https://github.com/Steveb-p)) +- \[amqp-bunny\] Fix heartbeat configuration in bunny with 0 \(off\) value [\#820](https://github.com/php-enqueue/enqueue-dev/pull/820) ([nightlinus](https://github.com/nightlinus)) +- \[stomp\] Add support for using the /topic prefix instead of /exchange. [\#826](https://github.com/php-enqueue/enqueue-dev/pull/826) ([alessandroniciforo](https://github.com/alessandroniciforo)) +- \[sns\] Allow setting SNS message attributes, other fields [\#799](https://github.com/php-enqueue/enqueue-dev/pull/799) ([aldenw](https://github.com/aldenw)) +- Fixed docs [\#822](https://github.com/php-enqueue/enqueue-dev/pull/822) ([Toflar](https://github.com/Toflar)) +- Typo on the tag [\#818](https://github.com/php-enqueue/enqueue-dev/pull/818) ([appeltaert](https://github.com/appeltaert)) + +## [0.9.9](https://github.com/php-enqueue/enqueue-dev/tree/0.9.9) (2019-04-04) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.8...0.9.9) + +**Merged pull requests:** + +- \[amqp-bunny\] Fix bunny producer to properly map headers to expected by bunny headers [\#816](https://github.com/php-enqueue/enqueue-dev/pull/816) ([nightlinus](https://github.com/nightlinus)) +- \[amqp-bunny\]\[doc\] Update amqp\_bunny.md [\#797](https://github.com/php-enqueue/enqueue-dev/pull/797) ([enumag](https://github.com/enumag)) +- \[dbal\] Fix DBAL Consumer duplicating messages when rejecting with requeue [\#815](https://github.com/php-enqueue/enqueue-dev/pull/815) ([Steveb-p](https://github.com/Steveb-p)) +- \[rdkafka\] Set `commit\_async` as true by default for Kafka, update docs [\#810](https://github.com/php-enqueue/enqueue-dev/pull/810) ([Steveb-p](https://github.com/Steveb-p)) +- \[rdkafka\] stats\_cb support [\#798](https://github.com/php-enqueue/enqueue-dev/pull/798) ([fkulakov](https://github.com/fkulakov)) +- \[Monitoring\]\[InfluxDB\] Allow passing Client as configuration option. [\#809](https://github.com/php-enqueue/enqueue-dev/pull/809) ([Steveb-p](https://github.com/Steveb-p)) +- \[doc\] better doc for traceable message producer [\#813](https://github.com/php-enqueue/enqueue-dev/pull/813) ([sylfabre](https://github.com/sylfabre)) +- \[doc\] Minor typo fix in docblock [\#805](https://github.com/php-enqueue/enqueue-dev/pull/805) ([gpenverne](https://github.com/gpenverne)) +- fix comment on QueueConsumer constructor [\#796](https://github.com/php-enqueue/enqueue-dev/pull/796) ([kaznovac](https://github.com/kaznovac)) + +## [0.9.8](https://github.com/php-enqueue/enqueue-dev/tree/0.9.8) (2019-02-27) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.7...0.9.8) + +**Merged pull requests:** + +- Add upgrade instructions [\#787](https://github.com/php-enqueue/enqueue-dev/pull/787) ([KDederichs](https://github.com/KDederichs)) +- \[consumption\] Fix exception loop in QueueConsumer [\#776](https://github.com/php-enqueue/enqueue-dev/pull/776) ([enumag](https://github.com/enumag)) +- \[consumption\] Add ability to change process exit status from within queue consumer extension [\#766](https://github.com/php-enqueue/enqueue-dev/pull/766) ([greblov](https://github.com/greblov)) +- \[amqp-tools\] Fix amqp-tools dependency [\#785](https://github.com/php-enqueue/enqueue-dev/pull/785) ([TomPradat](https://github.com/TomPradat)) +- \[amqp-tools\] Enable 'ssl\_on' param for 'ssl' scheme extension [\#781](https://github.com/php-enqueue/enqueue-dev/pull/781) ([Leprechaunz](https://github.com/Leprechaunz)) +- \[amqp-bunny\] Catch signal in Bunny adapter [\#771](https://github.com/php-enqueue/enqueue-dev/pull/771) ([snapshotpl](https://github.com/snapshotpl)) +- \[amqp-lib\] supporting channel\_rpc\_timeout option [\#755](https://github.com/php-enqueue/enqueue-dev/pull/755) ([derek9gag](https://github.com/derek9gag)) +- \[dbal\]: make dbal connection config usable again [\#765](https://github.com/php-enqueue/enqueue-dev/pull/765) ([ssiergl](https://github.com/ssiergl)) +- \[fs\] polling\_interval config should be milliseconds not microseconds [\#764](https://github.com/php-enqueue/enqueue-dev/pull/764) ([ssiergl](https://github.com/ssiergl)) +- \[simple-client\] Fix Logger Initialisation [\#752](https://github.com/php-enqueue/enqueue-dev/pull/752) ([ajbonner](https://github.com/ajbonner)) +- \[snsqs\] Corrected the installation part in the docs/transport/snsqs.md [\#791](https://github.com/php-enqueue/enqueue-dev/pull/791) ([dgreda](https://github.com/dgreda)) +- \[sqs\] Update SqsConnectionFactory.php [\#751](https://github.com/php-enqueue/enqueue-dev/pull/751) ([Orkin](https://github.com/Orkin)) +- correct typo in composer.json [\#767](https://github.com/php-enqueue/enqueue-dev/pull/767) ([greblov](https://github.com/greblov)) + +## [0.9.7](https://github.com/php-enqueue/enqueue-dev/tree/0.9.7) (2019-02-01) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.6...0.9.7) + +**Merged pull requests:** + +- Avoid OutOfMemoryException [\#725](https://github.com/php-enqueue/enqueue-dev/pull/725) ([DamienHarper](https://github.com/DamienHarper)) +- \[async-event-dispatcher\] Add default to php\_serializer\_event\_transformer [\#748](https://github.com/php-enqueue/enqueue-dev/pull/748) ([GCalmels](https://github.com/GCalmels)) +- \[async-event-dispatcher\] Fixed param on EventTransformer [\#736](https://github.com/php-enqueue/enqueue-dev/pull/736) ([samnela](https://github.com/samnela)) +- \[job-queue\] Install stable dependencies [\#745](https://github.com/php-enqueue/enqueue-dev/pull/745) ([mbabic131](https://github.com/mbabic131)) +- \[job-queue\] Fix job status processor [\#735](https://github.com/php-enqueue/enqueue-dev/pull/735) ([ASKozienko](https://github.com/ASKozienko)) +- \[redis\] Fix messages sent with incorrect delivery delay [\#738](https://github.com/php-enqueue/enqueue-dev/pull/738) ([niels-nijens](https://github.com/niels-nijens)) +- \[dbal\] Exception on affected record !=1 [\#733](https://github.com/php-enqueue/enqueue-dev/pull/733) ([otzy](https://github.com/otzy)) +- \[bundle\]\[dbal\] Use doctrine bundle configured connections [\#732](https://github.com/php-enqueue/enqueue-dev/pull/732) ([ASKozienko](https://github.com/ASKozienko)) +- \[pheanstalk\] Add unit tests for PheanstalkConsumer [\#726](https://github.com/php-enqueue/enqueue-dev/pull/726) ([alanpoulain](https://github.com/alanpoulain)) +- \[pheanstalk\] Requeuing a message should not acknowledge it beforehand [\#722](https://github.com/php-enqueue/enqueue-dev/pull/722) ([alanpoulain](https://github.com/alanpoulain)) +- \[sqs\] Dead Letter Queue Adoption [\#720](https://github.com/php-enqueue/enqueue-dev/pull/720) ([cshum](https://github.com/cshum)) + +## [0.9.6](https://github.com/php-enqueue/enqueue-dev/tree/0.9.6) (2019-01-09) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.5...0.9.6) + +**Merged pull requests:** + +- Fix async command/event pkgs [\#717](https://github.com/php-enqueue/enqueue-dev/pull/717) ([GCalmels](https://github.com/GCalmels)) +- Use database from config in PRedis driver [\#715](https://github.com/php-enqueue/enqueue-dev/pull/715) ([lalov](https://github.com/lalov)) +- \[monitoring\] Add support of Datadog [\#716](https://github.com/php-enqueue/enqueue-dev/pull/716) ([uro](https://github.com/uro)) +- \[monitoring\] Fixed influxdb write on sentMessageStats [\#712](https://github.com/php-enqueue/enqueue-dev/pull/712) ([uro](https://github.com/uro)) +- \[monitoring\] Add support for minimum stability - stable [\#711](https://github.com/php-enqueue/enqueue-dev/pull/711) ([uro](https://github.com/uro)) +- \[consumption\] fix wrong niceness extension param [\#709](https://github.com/php-enqueue/enqueue-dev/pull/709) ([ramunasd](https://github.com/ramunasd)) + +## [0.9.5](https://github.com/php-enqueue/enqueue-dev/tree/0.9.5) (2018-12-21) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.4...0.9.5) + +**Merged pull requests:** + +- \[dbal\] Run tests on PostgreSQS [\#705](https://github.com/php-enqueue/enqueue-dev/pull/705) ([makasim](https://github.com/makasim)) +- \[dbal\] Use string-based UUIDs instead of binary [\#698](https://github.com/php-enqueue/enqueue-dev/pull/698) ([jverdeyen](https://github.com/jverdeyen)) + +## [0.9.4](https://github.com/php-enqueue/enqueue-dev/tree/0.9.4) (2018-12-20) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.3...0.9.4) + +**Merged pull requests:** + +- \[client\] sendToProcessor should able to send message to router processor. [\#703](https://github.com/php-enqueue/enqueue-dev/pull/703) ([makasim](https://github.com/makasim)) +- \[client\] Fix SetRouterPropertiesExtension should skip no topic messages. [\#702](https://github.com/php-enqueue/enqueue-dev/pull/702) ([makasim](https://github.com/makasim)) +- \[client\] Fix Exclusive Command Extension ignores route queue prefix option. [\#701](https://github.com/php-enqueue/enqueue-dev/pull/701) ([makasim](https://github.com/makasim)) +- \[amqp\] fix \#696 parsing vhost from amqp dsn [\#697](https://github.com/php-enqueue/enqueue-dev/pull/697) ([rpanfili](https://github.com/rpanfili)) +- \[doc\] Fix link to declare queue [\#699](https://github.com/php-enqueue/enqueue-dev/pull/699) ([samnela](https://github.com/samnela)) + +## [0.9.3](https://github.com/php-enqueue/enqueue-dev/tree/0.9.3) (2018-12-17) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.2...0.9.3) + +**Merged pull requests:** + +- Fix async command package [\#694](https://github.com/php-enqueue/enqueue-dev/pull/694) ([makasim](https://github.com/makasim)) +- Fix async events package [\#694](https://github.com/php-enqueue/enqueue-dev/pull/694) ([makasim](https://github.com/makasim)) +- Add commands for single transport\client with typed arguments. [\#693](https://github.com/php-enqueue/enqueue-dev/pull/693) ([makasim](https://github.com/makasim)) +- Fix TreeBuilder in Symfony 4.2 [\#692](https://github.com/php-enqueue/enqueue-dev/pull/692) ([angelsk](https://github.com/angelsk)) +- [doc] update docs [\#689](https://github.com/php-enqueue/enqueue-dev/pull/689) ([OskarStark](https://github.com/OskarStark)) + +## [0.9.2](https://github.com/php-enqueue/enqueue-dev/tree/0.9.2) (2018-12-13) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.1...0.9.2) + +**Merged pull requests:** + +- Allow 0.8.x Queue Interop \(without deprecated Psr prefixed interfaces\) [\#688](https://github.com/php-enqueue/enqueue-dev/pull/688) ([makasim](https://github.com/makasim)) +- \[dsn\] remove commented out code [\#661](https://github.com/php-enqueue/enqueue-dev/pull/661) ([kunicmarko20](https://github.com/kunicmarko20)) +- \[fs\]: fix: Wrong parameters for Exception [\#678](https://github.com/php-enqueue/enqueue-dev/pull/678) ([ssiergl](https://github.com/ssiergl)) +- \[fs\] Do not throw error in jsonUnserialize on deprecation notice [\#671](https://github.com/php-enqueue/enqueue-dev/pull/671) ([ssiergl](https://github.com/ssiergl)) +- \[mongodb\] polling\_integer type not correctly handled when using DSN [\#673](https://github.com/php-enqueue/enqueue-dev/pull/673) ([jak](https://github.com/jak)) +- \[dbal\] Use ordered bytes time uuid codec on message id decode. [\#665](https://github.com/php-enqueue/enqueue-dev/pull/665) ([makasim](https://github.com/makasim)) +- \[dbal\] fix: Wrong parameters for Exception [\#676](https://github.com/php-enqueue/enqueue-dev/pull/676) ([Nommyde](https://github.com/Nommyde)) +- \[sqs\] Add ability to use another aws account per queue. [\#666](https://github.com/php-enqueue/enqueue-dev/pull/666) ([makasim](https://github.com/makasim)) +- \[sqs\] Multi region support [\#664](https://github.com/php-enqueue/enqueue-dev/pull/664) ([makasim](https://github.com/makasim)) +- \[sqs\] Use a queue created in another AWS account. [\#662](https://github.com/php-enqueue/enqueue-dev/pull/662) ([makasim](https://github.com/makasim)) +- \[job-queue\] Fix tests on newer dbal versions. [\#687](https://github.com/php-enqueue/enqueue-dev/pull/687) ([makasim](https://github.com/makasim)) +- [doc] typo [\#686](https://github.com/php-enqueue/enqueue-dev/pull/686) ([OskarStark](https://github.com/OskarStark)) +- [doc] typo [\#683](https://github.com/php-enqueue/enqueue-dev/pull/683) ([OskarStark](https://github.com/OskarStark)) +- [doc] Fix package name for redis [\#680](https://github.com/php-enqueue/enqueue-dev/pull/680) ([gnumoksha](https://github.com/gnumoksha)) + +## [0.9.1](https://github.com/php-enqueue/enqueue-dev/tree/0.9.1) (2018-11-27) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.9.0...0.9.1) + +**Merged pull requests:** + +- Allow installing stable dependencies. [\#660](https://github.com/php-enqueue/enqueue-dev/pull/660) ([makasim](https://github.com/makasim)) + +## [0.9.0](https://github.com/php-enqueue/enqueue-dev/tree/0.9) (2018-11-27) +[Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.8.42...0.9) + +**Merged pull requests:** + +- \[amqp\]\[lib\] Improve heartbeat handling. Introduce heartbeat on tick. Fixes "Invalid frame type 65" and "Broken pipe or closed connection" [\#658](https://github.com/php-enqueue/enqueue-dev/pull/658) ([makasim](https://github.com/makasim)) +- Redis dsn and password fixes [\#656](https://github.com/php-enqueue/enqueue-dev/pull/656) ([makasim](https://github.com/makasim)) +- Fix ping to check each connection, not only first one [\#651](https://github.com/php-enqueue/enqueue-dev/pull/651) ([webmake](https://github.com/webmake)) +- Rework DriverFactory, add separator option to Client Config. [\#646](https://github.com/php-enqueue/enqueue-dev/pull/646) ([makasim](https://github.com/makasim)) +- \[dsn\] Parse DSN Cluster [\#643](https://github.com/php-enqueue/enqueue-dev/pull/643) ([makasim](https://github.com/makasim)) +- \[dbal\] Use RetryableException, wrap fetchMessage exception to it too. [\#642](https://github.com/php-enqueue/enqueue-dev/pull/642) ([makasim](https://github.com/makasim)) +- \[bundle\] Add BC for topic\command subscribers. [\#641](https://github.com/php-enqueue/enqueue-dev/pull/641) ([makasim](https://github.com/makasim)) +- \[dbal\] handle gracefully concurrency issues or 3rd party interruptions. [\#640](https://github.com/php-enqueue/enqueue-dev/pull/640) ([makasim](https://github.com/makasim)) +- Fix compiler pass [\#639](https://github.com/php-enqueue/enqueue-dev/pull/639) ([ASKozienko](https://github.com/ASKozienko)) +- Fix wrong exceptions in transports [\#637](https://github.com/php-enqueue/enqueue-dev/pull/637) ([FrankGiesecke](https://github.com/FrankGiesecke)) +- Enable job-queue for default configuration [\#636](https://github.com/php-enqueue/enqueue-dev/pull/636) ([ASKozienko](https://github.com/ASKozienko)) +- better readability [\#632](https://github.com/php-enqueue/enqueue-dev/pull/632) ([OskarStark](https://github.com/OskarStark)) +- Fixed headline [\#631](https://github.com/php-enqueue/enqueue-dev/pull/631) ([OskarStark](https://github.com/OskarStark)) +- \[bundle\] Multi Client Configuration [\#628](https://github.com/php-enqueue/enqueue-dev/pull/628) ([ASKozienko](https://github.com/ASKozienko)) +- removed some dots [\#627](https://github.com/php-enqueue/enqueue-dev/pull/627) ([OskarStark](https://github.com/OskarStark)) +- Avoid receiveNoWait when only one subscriber [\#626](https://github.com/php-enqueue/enqueue-dev/pull/626) ([deguif](https://github.com/deguif)) +- Add context services to locator [\#623](https://github.com/php-enqueue/enqueue-dev/pull/623) ([Gnucki](https://github.com/Gnucki)) +- \[doc\]\[skip ci\] Add sponsoring section. [\#618](https://github.com/php-enqueue/enqueue-dev/pull/618) ([makasim](https://github.com/makasim)) +- Merge 0.8x -\> 0.9x [\#617](https://github.com/php-enqueue/enqueue-dev/pull/617) ([ASKozienko](https://github.com/ASKozienko)) +- Compatibility with 0.8x [\#616](https://github.com/php-enqueue/enqueue-dev/pull/616) ([ASKozienko](https://github.com/ASKozienko)) +- \[dbal\] Use concurrent fetch message approach \(no transaction, no pessimistic lock\) [\#613](https://github.com/php-enqueue/enqueue-dev/pull/613) ([makasim](https://github.com/makasim)) +- \[fs\] Use enqueue/dsn to parse DSN [\#612](https://github.com/php-enqueue/enqueue-dev/pull/612) ([makasim](https://github.com/makasim)) +- \[client\]\[bundle\] Take queue prefix into account while queue binding. [\#611](https://github.com/php-enqueue/enqueue-dev/pull/611) ([makasim](https://github.com/makasim)) +- Add support for the 'ciphers' ssl option [\#607](https://github.com/php-enqueue/enqueue-dev/pull/607) ([eperazzo](https://github.com/eperazzo)) +- Queue monitoring. [\#606](https://github.com/php-enqueue/enqueue-dev/pull/606) ([ASKozienko](https://github.com/ASKozienko)) +- Fix comment about queue deletion [\#604](https://github.com/php-enqueue/enqueue-dev/pull/604) ([a-ast](https://github.com/a-ast)) +- \[docs\] Fixed docs. Removed prefix Psr. [\#603](https://github.com/php-enqueue/enqueue-dev/pull/603) ([yurez](https://github.com/yurez)) +- fix wamp [\#597](https://github.com/php-enqueue/enqueue-dev/pull/597) ([ASKozienko](https://github.com/ASKozienko)) +- \[doc\]\[skip ci\] Add supporting section [\#595](https://github.com/php-enqueue/enqueue-dev/pull/595) ([makasim](https://github.com/makasim)) +- Do not export non source files [\#588](https://github.com/php-enqueue/enqueue-dev/pull/588) ([webmake](https://github.com/webmake)) +- Redis New Implementation [\#585](https://github.com/php-enqueue/enqueue-dev/pull/585) ([ASKozienko](https://github.com/ASKozienko)) +- Fix Redis Tests [\#582](https://github.com/php-enqueue/enqueue-dev/pull/582) ([ASKozienko](https://github.com/ASKozienko)) +- \[dbal\] Introduce redelivery support based on visibility approach. [\#581](https://github.com/php-enqueue/enqueue-dev/pull/581) ([rosamarsky](https://github.com/rosamarsky)) +- fix redis tests [\#578](https://github.com/php-enqueue/enqueue-dev/pull/578) ([ASKozienko](https://github.com/ASKozienko)) +- \[client\] Make symfony compiler passes multi client [\#577](https://github.com/php-enqueue/enqueue-dev/pull/577) ([makasim](https://github.com/makasim)) +- Removed predis from composer.json [\#576](https://github.com/php-enqueue/enqueue-dev/pull/576) ([rosamarsky](https://github.com/rosamarsky)) +- Added index for queue field in the enqueue collection [\#574](https://github.com/php-enqueue/enqueue-dev/pull/574) ([rosamarsky](https://github.com/rosamarsky)) +- WAMP [\#573](https://github.com/php-enqueue/enqueue-dev/pull/573) ([ASKozienko](https://github.com/ASKozienko)) +- Bundle multi transport configuration [\#572](https://github.com/php-enqueue/enqueue-dev/pull/572) ([makasim](https://github.com/makasim)) +- \[client\] Move client config to the factory. [\#571](https://github.com/php-enqueue/enqueue-dev/pull/571) ([makasim](https://github.com/makasim)) +- Update quick\_tour.md [\#569](https://github.com/php-enqueue/enqueue-dev/pull/569) ([luceos](https://github.com/luceos)) +- \[rdkafka\] Use default queue as router topic [\#567](https://github.com/php-enqueue/enqueue-dev/pull/567) ([rosamarsky](https://github.com/rosamarsky)) +- Fixing composer.json to require enqueue/dsn [\#566](https://github.com/php-enqueue/enqueue-dev/pull/566) ([adumas37](https://github.com/adumas37)) +- MongoDB Subscription Consumer feature [\#565](https://github.com/php-enqueue/enqueue-dev/pull/565) ([rosamarsky](https://github.com/rosamarsky)) +- Remove deprecated testcase implementation [\#564](https://github.com/php-enqueue/enqueue-dev/pull/564) ([samnela](https://github.com/samnela)) +- Dbal Subscription Consumer feature [\#563](https://github.com/php-enqueue/enqueue-dev/pull/563) ([rosamarsky](https://github.com/rosamarsky)) +- \[client\] Move services definition to ClientFactory. [\#556](https://github.com/php-enqueue/enqueue-dev/pull/556) ([makasim](https://github.com/makasim)) +- Fixed exception message in testThrowErrorIfServiceDoesNotImplementProcessorReturnType [\#559](https://github.com/php-enqueue/enqueue-dev/pull/559) ([rosamarsky](https://github.com/rosamarsky)) +- Update supported\_brokers.md [\#558](https://github.com/php-enqueue/enqueue-dev/pull/558) ([edgji](https://github.com/edgji)) +- \[consumption\] Logging improvements [\#555](https://github.com/php-enqueue/enqueue-dev/pull/555) ([makasim](https://github.com/makasim)) +- \[consumption\] Rework QueueConsumer extension points. [\#554](https://github.com/php-enqueue/enqueue-dev/pull/554) ([makasim](https://github.com/makasim)) +- \[STOMP\] make getStomp public [\#552](https://github.com/php-enqueue/enqueue-dev/pull/552) ([versh23](https://github.com/versh23)) +- \[consumption\] Add ability to consume from multiple transports. [\#548](https://github.com/php-enqueue/enqueue-dev/pull/548) ([makasim](https://github.com/makasim)) +- \[client\] Rename config options. [\#547](https://github.com/php-enqueue/enqueue-dev/pull/547) ([makasim](https://github.com/makasim)) +- Remove config parameters [\#545](https://github.com/php-enqueue/enqueue-dev/pull/545) ([makasim](https://github.com/makasim)) +- Remove transport factories [\#544](https://github.com/php-enqueue/enqueue-dev/pull/544) ([makasim](https://github.com/makasim)) +- Remove psr prefix [\#543](https://github.com/php-enqueue/enqueue-dev/pull/543) ([makasim](https://github.com/makasim)) +- \[amqp\] Set delay strategy if rabbitmq scheme extension present. [\#536](https://github.com/php-enqueue/enqueue-dev/pull/536) ([makasim](https://github.com/makasim)) +- \[client\] Add type hints to driver interface and its implementations. [\#535](https://github.com/php-enqueue/enqueue-dev/pull/535) ([makasim](https://github.com/makasim)) +- \[client\] Introduce routes. Foundation for multi transport support. [\#534](https://github.com/php-enqueue/enqueue-dev/pull/534) ([makasim](https://github.com/makasim)) +- \[gps\] enhance connection configuration. [\#531](https://github.com/php-enqueue/enqueue-dev/pull/531) ([makasim](https://github.com/makasim)) +- \[sqs\] Configuration enhancements [\#530](https://github.com/php-enqueue/enqueue-dev/pull/530) ([makasim](https://github.com/makasim)) +- \[redis\] Improve redis config, use enqueue/dsn [\#528](https://github.com/php-enqueue/enqueue-dev/pull/528) ([makasim](https://github.com/makasim)) +- \[dsn\] Add typed methods for query parameters. [\#527](https://github.com/php-enqueue/enqueue-dev/pull/527) ([makasim](https://github.com/makasim)) +- \[redis\] Revert timeout change. [\#526](https://github.com/php-enqueue/enqueue-dev/pull/526) ([makasim](https://github.com/makasim)) +- \[Redis\] Add support of secure\TLS connections \(based on PR 515\) [\#524](https://github.com/php-enqueue/enqueue-dev/pull/524) ([makasim](https://github.com/makasim)) +- Simplify Enqueue configuration. [\#522](https://github.com/php-enqueue/enqueue-dev/pull/522) ([makasim](https://github.com/makasim)) +- \[client\] Add typehints to producer interface, its implementations [\#521](https://github.com/php-enqueue/enqueue-dev/pull/521) ([makasim](https://github.com/makasim)) +- \[client\] Improve client extension. [\#517](https://github.com/php-enqueue/enqueue-dev/pull/517) ([makasim](https://github.com/makasim)) +- Add declare strict [\#516](https://github.com/php-enqueue/enqueue-dev/pull/516) ([makasim](https://github.com/makasim)) +- PHP 7.1+. Queue Interop typed interfaces. [\#512](https://github.com/php-enqueue/enqueue-dev/pull/512) ([makasim](https://github.com/makasim)) +- \[Symfony\] default factory should resolve DSN in runtime [\#510](https://github.com/php-enqueue/enqueue-dev/pull/510) ([makasim](https://github.com/makasim)) +- Fixed password auth for predis [\#509](https://github.com/php-enqueue/enqueue-dev/pull/509) ([Toflar](https://github.com/Toflar)) +- Allow either subscribe or assign in RdKafkaConsumer [\#508](https://github.com/php-enqueue/enqueue-dev/pull/508) ([Engerim](https://github.com/Engerim)) +- Remove deprecated in 0.8 code [\#507](https://github.com/php-enqueue/enqueue-dev/pull/507) ([makasim](https://github.com/makasim)) +- Run tests on rabbitmq 3.7 [\#506](https://github.com/php-enqueue/enqueue-dev/pull/506) ([makasim](https://github.com/makasim)) +- Symfony add default command name [\#505](https://github.com/php-enqueue/enqueue-dev/pull/505) ([makasim](https://github.com/makasim)) +- \[Consumption\] Add QueueConsumerInterface, make QueueConsumer final. [\#504](https://github.com/php-enqueue/enqueue-dev/pull/504) ([makasim](https://github.com/makasim)) +- Redis subscription consumer [\#503](https://github.com/php-enqueue/enqueue-dev/pull/503) ([makasim](https://github.com/makasim)) +- Remove support of old Symfony versions. [\#502](https://github.com/php-enqueue/enqueue-dev/pull/502) ([makasim](https://github.com/makasim)) +- \[BC break\]\[dbal\] Convert between Message::$expire and DbalMessage::$timeToLive [\#501](https://github.com/php-enqueue/enqueue-dev/pull/501) ([makasim](https://github.com/makasim)) +- \[BC break\]\[dbal\] Change columns type from int to bigint. [\#500](https://github.com/php-enqueue/enqueue-dev/pull/500) ([makasim](https://github.com/makasim)) +- \[BC break\]\[dbal\] Fix time conversion in DbalDriver. [\#499](https://github.com/php-enqueue/enqueue-dev/pull/499) ([makasim](https://github.com/makasim)) +- \[BC break\]\[dbal\] Add index, fix performance issue. [\#498](https://github.com/php-enqueue/enqueue-dev/pull/498) ([makasim](https://github.com/makasim)) +- \[redis\] Authentication support added [\#497](https://github.com/php-enqueue/enqueue-dev/pull/497) ([makasim](https://github.com/makasim)) +- add subscription consumer specs to amqp pkgs [\#495](https://github.com/php-enqueue/enqueue-dev/pull/495) ([makasim](https://github.com/makasim)) +- add contribution to subtree split message [\#494](https://github.com/php-enqueue/enqueue-dev/pull/494) ([makasim](https://github.com/makasim)) +- Get rid of path repository [\#493](https://github.com/php-enqueue/enqueue-dev/pull/493) ([makasim](https://github.com/makasim)) +- Move subscription related logic to SubscriptionConsumer class. [\#492](https://github.com/php-enqueue/enqueue-dev/pull/492) ([makasim](https://github.com/makasim)) +- remove bc layer. [\#489](https://github.com/php-enqueue/enqueue-dev/pull/489) ([makasim](https://github.com/makasim)) +- Job Queue: Throw orphan job exception when child job cleanup fails. [\#496](https://github.com/php-enqueue/enqueue-dev/pull/496) ([garrettrayj](https://github.com/garrettrayj)) +- \[bundle\] Fix panel rendering when message body is an object [\#442](https://github.com/php-enqueue/enqueue-dev/pull/442) ([thePanz](https://github.com/thePanz)) +- \[symfony\] Async commands [\#403](https://github.com/php-enqueue/enqueue-dev/pull/403) ([makasim](https://github.com/makasim)) + ## [0.8.42](https://github.com/php-enqueue/enqueue-dev/tree/0.8.42) (2018-11-22) [Full Changelog](https://github.com/php-enqueue/enqueue-dev/compare/0.8.41...0.8.42) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..21bb45cc5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +Contributing +------------ + +Enqueue is an open source, community-driven project. + +If you'd like to contribute, please read the following documents: + +* [Contribution](docs/contribution.md) diff --git a/README.md b/README.md index 08edc9a1b..5e0dacec3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,28 @@ -**Enqueue** is production ready, battle-tested messaging solution for PHP. Provides a common way for programs to create, send, read messages. +[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) -This is a main development repository. It provides a friendly environment for productive development and testing of all Enqueue related features&packages. +

Enqueue logo

+ +

+ Enqueue Chat + Build Status + Total Downloads + Latest Stable Version + License +

+ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become our client](http://forma-pro.com/) -[![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/enqueue-dev.png?branch=master)](https://travis-ci.org/php-enqueue/enqueue-dev) -[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) +--- + +## Introduction + +**Enqueue** is production ready, battle-tested messaging solution for PHP. Provides a common way for programs to create, send, read messages. + +This is a main development repository. It provides a friendly environment for productive development and testing of all Enqueue related features&packages. Features: @@ -12,89 +30,102 @@ Features: * Adopts [queue interoperable](https://github.com/queue-interop/queue-interop) interfaces (inspired by [Java JMS](https://docs.oracle.com/javaee/7/api/javax/jms/package-summary.html)). * Battle-tested. Used in production. -* Supported transports - * [AMQP(s)](docs/transport/amqp.md) based on [PHP AMQP extension](https://github.com/pdezwart/php-amqp). -[![Build Status](https://travis-ci.org/php-enqueue/amqp-ext.png?branch=master)](https://travis-ci.org/php-enqueue/amqp-ext) +* Supported transports + * [AMQP(s)](https://php-enqueue.github.io/transport/amqp/) based on [PHP AMQP extension](https://github.com/pdezwart/php-amqp) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/amqp-ext/ci.yml?branch=master)](https://github.com/php-enqueue/amqp-ext/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/amqp-ext/d/total.png)](https://packagist.org/packages/enqueue/amqp-ext/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/amqp-ext/version.png)](https://packagist.org/packages/enqueue/amqp-ext) - * [AMQP](docs/transport/amqp_bunny.md) based on [bunny](https://github.com/jakubkulhan/bunny). -[![Build Status](https://travis-ci.org/php-enqueue/amqp-bunny.png?branch=master)](https://travis-ci.org/php-enqueue/amqp-bunny) + * [AMQP](https://php-enqueue.github.io/transport/amqp_bunny/) based on [bunny](https://github.com/jakubkulhan/bunny) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/amqp-bunny/ci.yml?branch=master)](https://github.com/php-enqueue/amqp-bunny/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/amqp-bunny/d/total.png)](https://packagist.org/packages/enqueue/amqp-bunny/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/amqp-bunny/version.png)](https://packagist.org/packages/enqueue/amqp-bunny) - * [AMQP(s)](docs/transport/amqp_lib.md) based on [php-amqplib](https://github.com/php-amqplib/php-amqplib). -[![Build Status](https://travis-ci.org/php-enqueue/amqp-lib.png?branch=master)](https://travis-ci.org/php-enqueue/amqp-lib) + * [AMQP(s)](https://php-enqueue.github.io/transport/amqp_lib/) based on [php-amqplib](https://github.com/php-amqplib/php-amqplib) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/amqp-lib/ci.yml?branch=master)](https://github.com/php-enqueue/amqp-lib/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/amqp-lib/d/total.png)](https://packagist.org/packages/enqueue/amqp-lib/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/amqp-lib/version.png)](https://packagist.org/packages/enqueue/amqp-lib) - * [Beanstalk](docs/transport/pheanstalk.md). -[![Build Status](https://travis-ci.org/php-enqueue/pheanstalk.png?branch=master)](https://travis-ci.org/php-enqueue/pheanstalk) + * [Beanstalk](https://php-enqueue.github.io/transport/pheanstalk/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/pheanstalk/ci.yml?branch=master)](https://github.com/php-enqueue/pheanstalk/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/pheanstalk/d/total.png)](https://packagist.org/packages/enqueue/pheanstalk/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/pheanstalk/version.png)](https://packagist.org/packages/enqueue/pheanstalk) - * [STOMP](docs/transport/stomp.md) -[![Build Status](https://travis-ci.org/php-enqueue/stomp.png?branch=master)](https://travis-ci.org/php-enqueue/stomp) + * [STOMP](https://php-enqueue.github.io/transport/stomp/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/stomp/ci.yml?branch=master)](https://github.com/php-enqueue/stomp/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/stomp/d/total.png)](https://packagist.org/packages/enqueue/stomp/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/stomp/version.png)](https://packagist.org/packages/enqueue/stomp) - * [Amazon SQS](docs/transport/sqs.md) -[![Build Status](https://travis-ci.org/php-enqueue/sqs.png?branch=master)](https://travis-ci.org/php-enqueue/sqs) + * [Amazon SQS](https://php-enqueue.github.io/transport/sqs/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/sqs/ci.yml?branch=master)](https://github.com/php-enqueue/sqs/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/sqs/d/total.png)](https://packagist.org/packages/enqueue/sqs/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/sqs/version.png)](https://packagist.org/packages/enqueue/sqs) - * [Google PubSub](docs/transport/gps.md) -[![Build Status](https://travis-ci.org/php-enqueue/gps.png?branch=master)](https://travis-ci.org/php-enqueue/gps) + * [Amazon SNS](https://php-enqueue.github.io/transport/sns/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/sns/ci.yml?branch=master)](https://github.com/php-enqueue/sns/actions?query=workflow%3ACI) +[![Total Downloads](https://poser.pugx.org/enqueue/sns/d/total.png)](https://packagist.org/packages/enqueue/sns/stats) +[![Latest Stable Version](https://poser.pugx.org/enqueue/sns/version.png)](https://packagist.org/packages/enqueue/sns) + * [Amazon SNS\SQS](https://php-enqueue.github.io/transport/snsqs/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/snsqs/ci.yml?branch=master)](https://github.com/php-enqueue/snsqs/actions?query=workflow%3ACI) +[![Total Downloads](https://poser.pugx.org/enqueue/snsqs/d/total.png)](https://packagist.org/packages/enqueue/snsqs/stats) +[![Latest Stable Version](https://poser.pugx.org/enqueue/snsqs/version.png)](https://packagist.org/packages/enqueue/snsqs) + * [Google PubSub](https://php-enqueue.github.io/transport/gps/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/gps/ci.yml?branch=master)](https://github.com/php-enqueue/gps/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/gps/d/total.png)](https://packagist.org/packages/enqueue/gps/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/gps/version.png)](https://packagist.org/packages/enqueue/gps) - * [Kafka](docs/transport/kafka.md) -[![Build Status](https://travis-ci.org/php-enqueue/rdkafka.png?branch=master)](https://travis-ci.org/php-enqueue/rdkafka) + * [Kafka](https://php-enqueue.github.io/transport/kafka/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/rdkafka/ci.yml?branch=master)](https://github.com/php-enqueue/rdkafka/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/rdkafka/d/total.png)](https://packagist.org/packages/enqueue/rdkafka/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/rdkafka/version.png)](https://packagist.org/packages/enqueue/rdkafka) - * [Redis](docs/transport/redis.md) -[![Build Status](https://travis-ci.org/php-enqueue/redis.png?branch=master)](https://travis-ci.org/php-enqueue/redis) + * [Redis](https://php-enqueue.github.io/transport/redis/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/redis/ci.yml?branch=master)](https://github.com/php-enqueue/redis/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/redis/d/total.png)](https://packagist.org/packages/enqueue/redis/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/redis/version.png)](https://packagist.org/packages/enqueue/redis) - * [Gearman](docs/transport/gearman.md) -[![Build Status](https://travis-ci.org/php-enqueue/gearman.png?branch=master)](https://travis-ci.org/php-enqueue/gearman) + * [Gearman](https://php-enqueue.github.io/transport/gearman/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/gearman/ci.yml?branch=master)](https://github.com/php-enqueue/gearman/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/gearman/d/total.png)](https://packagist.org/packages/enqueue/gearman/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/gearman/version.png)](https://packagist.org/packages/enqueue/gearman) - * [Doctrine DBAL](docs/transport/dbal.md) -[![Build Status](https://travis-ci.org/php-enqueue/dbal.png?branch=master)](https://travis-ci.org/php-enqueue/dbal) + * [Doctrine DBAL](https://php-enqueue.github.io/transport/dbal/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/dbal/ci.yml?branch=master)](https://github.com/php-enqueue/dbal/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/dbal/d/total.png)](https://packagist.org/packages/enqueue/dbal/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/dbal/version.png)](https://packagist.org/packages/enqueue/dbal) - * [Filesystem](docs/transport/filesystem.md) -[![Build Status](https://travis-ci.org/php-enqueue/fs.png?branch=master)](https://travis-ci.org/php-enqueue/fs) + * [Filesystem](https://php-enqueue.github.io/transport/filesystem/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/fs/ci.yml?branch=master)](https://github.com/php-enqueue/fs/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/fs/d/total.png)](https://packagist.org/packages/enqueue/fs/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/fs/version.png)](https://packagist.org/packages/enqueue/fs) - * [Mongodb](docs/transport/mongodb.md) -[![Build Status](https://travis-ci.org/php-enqueue/mongodb.png?branch=master)](https://travis-ci.org/php-enqueue/mongodb) + * [Mongodb](https://php-enqueue.github.io/transport/mongodb/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/mongodb/ci.yml?branch=master)](https://github.com/php-enqueue/mongodb/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/mongodb/d/total.png)](https://packagist.org/packages/enqueue/mongodb/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/mongodb/version.png)](https://packagist.org/packages/enqueue/mongodb) - * [Null](docs/transport/null.md). -[![Build Status](https://travis-ci.org/php-enqueue/null.png?branch=master)](https://travis-ci.org/php-enqueue/null) + * [WAMP](https://php-enqueue.github.io/transport/wamp/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/wamp/ci.yml?branch=master)](https://github.com/php-enqueue/wamp/actions?query=workflow%3ACI) +[![Total Downloads](https://poser.pugx.org/enqueue/wamp/d/total.png)](https://packagist.org/packages/enqueue/wamp/stats) +[![Latest Stable Version](https://poser.pugx.org/enqueue/wamp/version.png)](https://packagist.org/packages/enqueue/wamp) + * [Null](https://php-enqueue.github.io/transport/null/) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/null/ci.yml?branch=master)](https://github.com/php-enqueue/null/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/null/d/total.png)](https://packagist.org/packages/enqueue/null/stats) [![Latest Stable Version](https://poser.pugx.org/enqueue/null/version.png)](https://packagist.org/packages/enqueue/null) - * [the others are comming](https://github.com/php-enqueue/enqueue-dev/issues/284) -* [Symfony bundle](docs/bundle/quick_tour.md) -* [Magento1 extension](docs/magento/quick_tour.md) -* [Magento2 module](docs/magento2/quick_tour.md) -* [Laravel extension](docs/laravel/quick_tour.md) -* [Yii2. Amqp driver](docs/yii/amqp_driver.md) -* [Message bus](docs/quick_tour.md#client) support. -* [RPC over MQ](docs/quick_tour.md#remote-procedure-call-rpc) support. + * [the others are coming](https://github.com/php-enqueue/enqueue-dev/issues/284) +* [Symfony bundle](https://php-enqueue.github.io/bundle/quick_tour/) +* [Magento1 extension](https://php-enqueue.github.io/magento/quick_tour/) +* [Magento2 module](https://php-enqueue.github.io/magento2/quick_tour/) +* [Laravel extension](https://php-enqueue.github.io/laravel/quick_tour/) +* [Yii2. Amqp driver](https://php-enqueue.github.io/yii/amqp_driver/) +* [Message bus](https://php-enqueue.github.io/quick_tour/#client) support. +* [RPC over MQ](https://php-enqueue.github.io/quick_tour/#remote-procedure-call-rpc) support. +* [Monitoring](https://php-enqueue.github.io/monitoring/) * Temporary queues support. * Well designed, decoupled and reusable components. * Carefully tested (unit & functional). -* For more visit [quick tour](docs/quick_tour.md). +* For more visit [quick tour](https://php-enqueue.github.io/quick_tour/). ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Quick tour](docs/quick_tour.md) -* [Documentation](docs/index.md) -* [Blog](docs/index.md#blogs) -* [Questions](https://gitter.im/php-enqueue/Lobby) +* [Quick tour](https://php-enqueue.github.io/quick_tour/) +* [Documentation](https://php-enqueue.github.io/) +* [Blog](https://php-enqueue.github.io/#blogs) +* [Chat\Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 000000000..6b64e44d9 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,89 @@ +# Upgrading Enqueue: + +From `0.8.x` to `0.9.x`: + +## Processor declaration + +`Interop\Queue\PsrProcessor` interface has been replaced by `Interop\Queue\Processor` +`Interop\Queue\PsrMessage` interface has been replaced by `Interop\Queue\Message` +`Interop\Queue\PsrContext` interface has been replaced by `Interop\Queue\Context` + + + +## Symfony Bundle + +### Configuration changes: + +`0.8.x` + + +``` +enqueue: + transport: + default: ... +``` + +`0.9.x` + + +``` +enqueue: + default: + transport: ... +``` + +In `0.9.x` the client name is a root config node. + +The `default_processor_queue` Client option was removed. + +### Service declarations: + +`0.8.x` + + +``` +tags: + - { name: 'enqueue.client.processor' } +``` + +`0.9.x` + + +``` +tags: + - { name: 'enqueue.command_subscriber' } + - { name: 'enqueue.topic_subscriber' } + - { name: 'enqueue.processor' } +``` + +The tag to register message processors has changed and is now split into processor sub types. + +### CommandSubscriberInterface `getSubscribedCommand` + + +`0.8.x` + +return `aCommandName` or +``` + [ + 'processorName' => 'aCommandName', + 'queueName' => 'a_client_queue_name', + 'queueNameHardcoded' => true, + 'exclusive' => true, + ] +``` + +`0.9.x` + + +return `aCommandName` or +``` + [ + 'command' => 'aSubscribedCommand', + 'processor' => 'aProcessorName', + 'queue' => 'a_client_queue_name', + 'prefix_queue' => true, + 'exclusive' => true, + ] +``` + diff --git a/bin/build-rabbitmq-image.sh b/bin/build-rabbitmq-image.sh index fb6d6bb20..1045ff787 100755 --- a/bin/build-rabbitmq-image.sh +++ b/bin/build-rabbitmq-image.sh @@ -3,6 +3,7 @@ set -e set -x -(cd docker && docker build --rm --force-rm --no-cache --pull --squash --tag "enqueue/rabbitmq:latest" -f Dockerfile.rabbitmq .) +(cd docker && docker build --rm --force-rm --no-cache --pull --squash --tag "enqueue/rabbitmq-local-build" -f Dockerfile."$1"-rabbitmq .) (cd docker && docker login --username="$DOCKER_USER" --password="$DOCKER_PASSWORD") -(cd docker && docker push "enqueue/rabbitmq:latest") \ No newline at end of file +(cd docker && docker tag enqueue/rabbitmq-local-build enqueue/rabbitmq:"$1") +(cd docker && docker push "enqueue/rabbitmq:$1") diff --git a/bin/changelog b/bin/changelog index 8a9296175..ba2db0813 100755 --- a/bin/changelog +++ b/bin/changelog @@ -8,6 +8,6 @@ then exit 1 fi -docker-compose run -e CHANGELOG_GITHUB_TOKEN=${CHANGELOG_GITHUB_TOKEN:-""} --workdir="/mqdev" --rm generate-changelog github_changelog_generator --future-release "$1" --no-issues --unreleased-only --output "CHANGELOG_FUTURE.md" +docker compose run -e CHANGELOG_GITHUB_TOKEN=${CHANGELOG_GITHUB_TOKEN:-""} --workdir="/mqdev" --rm generate-changelog github_changelog_generator --future-release "$1" --no-issues --unreleased-only --output "CHANGELOG_FUTURE.md" -#git add CHANGELOG.md && git commit -m "Release $1" -S && git push origin "$CURRENT_BRANCH" \ No newline at end of file + git add CHANGELOG.md && git commit -m "Release $1" -S && git push origin "$CURRENT_BRANCH" diff --git a/bin/dev b/bin/dev index e5f40ffbd..45a3e7124 100755 --- a/bin/dev +++ b/bin/dev @@ -6,13 +6,13 @@ set -e while getopts "bustefdp" OPTION; do case $OPTION in b) - docker-compose pull && docker-compose build + docker compose pull -q && docker compose build ;; u) - docker-compose up + docker compose up ;; s) - docker-compose stop + docker compose stop ;; e) docker exec -it mqdev_dev_1 /bin/bash @@ -21,7 +21,7 @@ while getopts "bustefdp" OPTION; do ./bin/php-cs-fixer fix ;; - d) docker-compose run --workdir="/mqdev" --rm dev php pkg/enqueue-bundle/Tests/Functional/app/console.php config:dump-reference enqueue -vvv + d) docker compose run --workdir="/mqdev" --rm dev php pkg/enqueue-bundle/Tests/Functional/app/console.php config:dump-reference enqueue -vvv ;; \?) echo "Invalid option: -$OPTARG" >&2 diff --git a/bin/fix-symfony-version.php b/bin/fix-symfony-version.php new file mode 100644 index 000000000..aac84f081 --- /dev/null +++ b/bin/fix-symfony-version.php @@ -0,0 +1,14 @@ +/dev/null', $phpBin, $projectRootDir.'/'.$file), $output, $returnCode); + exec(sprintf('%s -l %s', $phpBin, $projectRootDir.'/'.$file), $commandOutput, $returnCode); if ($returnCode) { - $filesWithErrors[] = $file; + $output[] = $commandOutput; } } - return $filesWithErrors; + return $output; } function runPhpCsFixer() @@ -101,56 +103,71 @@ function runPhpCsFixer() } $filesWithErrors = array(); - foreach (getFilesToFix() as $file) { - $output = ''; - $returnCode = null; + $output = ''; + $returnCode = null; + + exec(sprintf( + '%s %s fix --config=.php_cs.php --dry-run --no-interaction --path-mode=intersection -- %s', + $phpBin, + $phpCsFixerBin, + implode(' ', getFilesToFix()) + ), $output, $returnCode); + if ($returnCode) { exec(sprintf( - '%s %s fix %s --dry-run', + '%s %s fix --config=.php_cs.php --no-interaction -v --path-mode=intersection -- %s', $phpBin, $phpCsFixerBin, - $projectRootDir.'/'.$file - ), $output, $returnCode); + implode(' ', getFilesToFix()) + ), $output); + } - if ($returnCode) { - $output = ''; + return $returnCode; +} - exec(sprintf( - '%s %s fix %s', - $phpBin, - $phpCsFixerBin, - $projectRootDir.'/'.$file - ), $output); +function runPhpstan() +{ + $output = ''; + $returnCode = null; - $filesWithErrors[] = $file; - } - } + exec(sprintf( + 'docker run --workdir="/mqdev" -v "`pwd`:/mqdev" --rm enqueue/dev:latest php -d memory_limit=1024M bin/phpstan analyse -l 1 -c phpstan.neon %s', + implode(' ', getFilesToFix()) + ), $output, $returnCode); - return $filesWithErrors; + return $returnCode ? $output : false; } +echo sprintf('Found %s staged files', count(getFilesToFix())).PHP_EOL; + $phpSyntaxErrors = runPhpLint(); if ($phpSyntaxErrors) { echo "Php syntax errors were found in next files:" . PHP_EOL; - - foreach ($phpSyntaxErrors as $error) { - echo $error . PHP_EOL; + foreach ($phpSyntaxErrors as $phpSyntaxErrors) { + echo array_walk_recursive($phpSyntaxErrors, function($item) { + echo $item.PHP_EOL; + }) . PHP_EOL; } exit(1); } -$phpCSFixerErrors = runPhpCsFixer(); -if ($phpCSFixerErrors) { +$phpCSFixed = runPhpCsFixer(); +if ($phpCSFixed) { echo "Incorrect coding standards were detected and fixed." . PHP_EOL; echo "Please stash changes and run commit again." . PHP_EOL; - echo "List of changed files:" . PHP_EOL; - foreach ($phpCSFixerErrors as $error) { - echo $error . PHP_EOL; - } + exit(1); +} + +$phpStanErrors = runPhpstan(); +if ($phpStanErrors) { + echo array_walk_recursive($phpStanErrors, function($item) { + echo $item.PHP_EOL; + }) . PHP_EOL; + echo PHP_EOL; exit(1); } -exit(0); \ No newline at end of file +exit(0); diff --git a/bin/release b/bin/release index 818f0c27f..f1f7a07f1 100755 --- a/bin/release +++ b/bin/release @@ -18,11 +18,11 @@ rm ./CHANGELOG_FUTURE.md CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD` -git add CHANGELOG.md && git commit -m "Release $1" -S && git push origin "$CURRENT_BRANCH" +git add CHANGELOG.md && git commit -s -m "Release $1" -S && git push origin "$CURRENT_BRANCH" ./bin/subtree-split -for REMOTE in origin stomp amqp-ext amqp-lib amqp-bunny amqp-tools pheanstalk gearman sqs gps fs redis dbal null rdkafka enqueue simple-client enqueue-bundle job-queue test async-event-dispatcher mongodb +for REMOTE in origin stomp amqp-ext amqp-lib amqp-bunny amqp-tools pheanstalk gearman sqs sns snsqs gps fs redis dbal null rdkafka enqueue simple-client enqueue-bundle job-queue test async-event-dispatcher async-command mongodb wamp monitoring dsn do echo "" echo "" @@ -43,7 +43,8 @@ do if [[ -z "$LAST_RELEASE" ]]; then echo "There has not been any releases. Releasing $1"; - git tag $1 -s -m "Release $1" + #git tag $1 -a -s -m "Release $1" + git tag $1 -a -m "Release $1" git push origin --tags else echo "Last release $LAST_RELEASE"; @@ -53,7 +54,8 @@ do if [[ ! -z "$CHANGES_SINCE_LAST_RELEASE" ]]; then echo "There are changes since last release. Releasing $1"; - git tag $1 -s -m "Release $1" + #git tag $1 -s -m "Release $1" + git tag $1 -m "Release $1" git push origin --tags else echo "No change since last release."; diff --git a/bin/run-fun-test.sh b/bin/run-fun-test.sh deleted file mode 100755 index 120ef3ffe..000000000 --- a/bin/run-fun-test.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -set -x -set -e - -docker-compose run --workdir="/mqdev" --rm dev ./bin/test "$@" diff --git a/bin/splitsh-lite-m1 b/bin/splitsh-lite-m1 new file mode 100755 index 000000000..55814ab43 Binary files /dev/null and b/bin/splitsh-lite-m1 differ diff --git a/bin/subtree-split b/bin/subtree-split index ae411e4c9..1d73a4e8c 100755 --- a/bin/subtree-split +++ b/bin/subtree-split @@ -10,7 +10,7 @@ function split() # split_new_repo $1 $2 - SHA1=`./bin/splitsh-lite --prefix=$1` + SHA1=`./bin/splitsh-lite-m1 --prefix=$1` git push $2 "$SHA1:refs/heads/$CURRENT_BRANCH" } @@ -32,7 +32,7 @@ function split_new_repo() git push origin master; ); - SHA1=`./bin/splitsh-lite --prefix=$1` + SHA1=`./bin/splitsh-lite-m1 --prefix=$1` git fetch $2 git push $2 "$SHA1:$CURRENT_BRANCH" -f } @@ -44,6 +44,7 @@ function remote() } remote enqueue git@github.com:php-enqueue/enqueue.git +remote php-enqueue git@github.com:php-enqueue/php-enqueue.github.io.git remote simple-client git@github.com:php-enqueue/simple-client.git remote stomp git@github.com:php-enqueue/stomp.git remote amqp-ext git@github.com:php-enqueue/amqp-ext.git @@ -58,14 +59,21 @@ remote rdkafka git@github.com:php-enqueue/rdkafka.git remote dbal git@github.com:php-enqueue/dbal.git remote null git@github.com:php-enqueue/null.git remote sqs git@github.com:php-enqueue/sqs.git +remote sns git@github.com:php-enqueue/sns.git +remote snsqs git@github.com:php-enqueue/snsqs.git remote gps git@github.com:php-enqueue/gps.git remote enqueue-bundle git@github.com:php-enqueue/enqueue-bundle.git remote job-queue git@github.com:php-enqueue/job-queue.git remote test git@github.com:php-enqueue/test.git remote async-event-dispatcher git@github.com:php-enqueue/async-event-dispatcher.git +remote async-command git@github.com:php-enqueue/async-command.git remote mongodb git@github.com:php-enqueue/mongodb.git +remote dsn git@github.com:php-enqueue/dsn.git +remote wamp git@github.com:php-enqueue/wamp.git +remote monitoring git@github.com:php-enqueue/monitoring.git split 'pkg/enqueue' enqueue +split 'docs' php-enqueue split 'pkg/simple-client' simple-client split 'pkg/stomp' stomp split 'pkg/amqp-ext' amqp-ext @@ -80,9 +88,15 @@ split 'pkg/redis' redis split 'pkg/dbal' dbal split 'pkg/null' null split 'pkg/sqs' sqs +split 'pkg/sns' sns +split 'pkg/snsqs' snsqs split 'pkg/gps' gps split 'pkg/enqueue-bundle' enqueue-bundle split 'pkg/job-queue' job-queue split 'pkg/test' test split 'pkg/async-event-dispatcher' async-event-dispatcher -split 'pkg/mongodb' mongodb \ No newline at end of file +split 'pkg/async-command' async-command +split 'pkg/mongodb' mongodb +split 'pkg/dsn' dsn +split 'pkg/wamp' wamp +split 'pkg/monitoring' monitoring diff --git a/bin/test.sh b/bin/test.sh new file mode 100755 index 000000000..5cb858ad6 --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -x +set -e + +docker compose run --workdir="/mqdev" --rm dev ./docker/bin/test.sh $@ diff --git a/composer.json b/composer.json index c5bbe060f..f06367b09 100644 --- a/composer.json +++ b/composer.json @@ -3,50 +3,113 @@ "type": "project", "minimum-stability": "beta", "homepage": "https://enqueue.forma-pro.com/", + "scripts": { + "cs-fix": "bin/php-cs-fixer fix", + "cs-lint": "bin/php-cs-fixer fix --no-interaction --dry-run --diff", + "phpstan": "bin/phpstan analyse --memory-limit=512M -c phpstan.neon" + }, "require": { - "php": ">=5.6", - "enqueue/enqueue": "*@dev", - "enqueue/stomp": "*@dev", - "enqueue/amqp-ext": "*@dev", - "enqueue/amqp-lib": "*@dev", - "enqueue/amqp-bunny": "*@dev", - "enqueue/amqp-tools": "*@dev", - "php-amqplib/php-amqplib": "^2.7@dev", - "enqueue/redis": "*@dev", - "enqueue/fs": "*@dev", - "enqueue/null": "*@dev", - "enqueue/dbal": "*@dev", - "enqueue/mongodb": "*@dev", - "enqueue/sqs": "*@dev", - "enqueue/pheanstalk": "*@dev", - "enqueue/gearman": "*@dev", - "enqueue/rdkafka": "*@dev", - "kwn/php-rdkafka-stubs": "^1.0.2", - "enqueue/gps": "*@dev", - "enqueue/enqueue-bundle": "*@dev", - "enqueue/job-queue": "*@dev", - "enqueue/simple-client": "*@dev", - "enqueue/test": "*@dev", - "enqueue/async-event-dispatcher": "*@dev", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1", - "queue-interop/amqp-interop": "^0.7@dev", - "queue-interop/queue-spec": "^0.5.4@dev", + "php": "^8.1", + + "ext-amqp": "^1.9.3|^2.0.0", + "ext-gearman": "^2.0", + "ext-mongodb": "^1.17", + "ext-rdkafka": "^4.0|^5.0|^6.0", - "phpunit/phpunit": "^5", - "doctrine/doctrine-bundle": "~1.2", + "queue-interop/amqp-interop": "^0.8.2", + "queue-interop/queue-interop": "^0.8.1", + "bunny/bunny": "^0.4|^0.5", + "php-amqplib/php-amqplib": "^3.0", + "doctrine/dbal": "^2.12|^3.1", + "ramsey/uuid": "^3.5|^4", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "psr/container": "^1.1 || ^2.0", + "makasim/temp-file": "^0.2", + "google/cloud-pubsub": "^1.4.3", + "doctrine/orm": "^2.12", + "doctrine/persistence": "^2.0|^3.0", + "mongodb/mongodb": "^1.2", + "pda/pheanstalk": "^3.1", + "aws/aws-sdk-php": "^3.290", + "stomp-php/stomp-php": "^4.5|^5", + "php-http/guzzle7-adapter": "^0.1.1", + "php-http/client-common": "^2.2.1", + "andrewmy/rabbitmq-management-api": "^2.1.2", "predis/predis": "^1.1", - "symfony/monolog-bundle": "^2.8|^3|^4", - "symfony/browser-kit": "^2.8|^3|^4", - "symfony/expression-language": "^2.8|^3|^4", - "symfony/event-dispatcher": "^2.8|^3|^4", - "symfony/console": "^2.8|^3|^4", - "friendsofphp/php-cs-fixer": "^2", + "thruway/client": "^0.5.5", + "thruway/pawl-transport": "^0.5.1", + "influxdb/influxdb-php": "^1.14", + "datadog/php-datadogstatsd": "^1.3", + "guzzlehttp/guzzle": "^7.0.1", + "guzzlehttp/psr7": "^1.0", + "php-http/discovery": "^1.13", + "voryx/thruway-common": "^1.0.1", + "react/dns": "^1.4", + "react/event-loop": "^1.2", + "react/promise": "^2.8" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^1.0", + "queue-interop/queue-spec": "^0.6.2", + "symfony/browser-kit": "^6.2|^7.0", + "symfony/config": "^6.2|^7.0", + "symfony/process": "^6.2|^7.0", + "symfony/console": "^6.2|^7.0", + "symfony/dependency-injection": "^6.2|^7.0", + "symfony/event-dispatcher": "^6.2|^7.0", + "symfony/expression-language": "^6.2|^7.0", + "symfony/http-kernel": "^6.2|^7.0", + "symfony/filesystem": "^6.2|^7.0", + "symfony/framework-bundle": "^6.2|^7.0", + "symfony/validator": "^6.2|^7.0", + "symfony/yaml": "^6.2|^7.0", "empi89/php-amqp-stubs": "*@dev", - "php-http/client-common": "^1.7@dev" + "doctrine/doctrine-bundle": "^2.3.2", + "doctrine/mongodb-odm-bundle": "^3.5|^4.3|^5.0", + "alcaeus/mongo-php-adapter": "^1.0", + "kwn/php-rdkafka-stubs": "^2.0.3", + "friendsofphp/php-cs-fixer": "^3.4", + "dms/phpunit-arraysubset-asserts": "^0.2.1", + "phpspec/prophecy-phpunit": "^2.0" }, "autoload": { + "psr-4": { + "Enqueue\\AmqpBunny\\": "pkg/amqp-bunny/", + "Enqueue\\AmqpExt\\": "pkg/amqp-ext/", + "Enqueue\\AmqpLib\\": "pkg/amqp-lib/", + "Enqueue\\AmqpTools\\": "pkg/amqp-tools/", + "Enqueue\\AsyncEventDispatcher\\": "pkg/async-event-dispatcher/", + "Enqueue\\AsyncCommand\\": "pkg/async-command/", + "Enqueue\\Dbal\\": "pkg/dbal/", + "Enqueue\\Bundle\\": "pkg/enqueue-bundle/", + "Enqueue\\Fs\\": "pkg/fs/", + "Enqueue\\Gearman\\": "pkg/gearman/", + "Enqueue\\Gps\\": "pkg/gps/", + "Enqueue\\JobQueue\\": "pkg/job-queue/", + "Enqueue\\Mongodb\\": "pkg/mongodb/", + "Enqueue\\Null\\": "pkg/null/", + "Enqueue\\Pheanstalk\\": "pkg/pheanstalk/", + "Enqueue\\RdKafka\\": "pkg/rdkafka/", + "Enqueue\\Redis\\": "pkg/redis/", + "Enqueue\\SimpleClient\\": "pkg/simple-client/", + "Enqueue\\Sqs\\": "pkg/sqs/", + "Enqueue\\Sns\\": "pkg/sns/", + "Enqueue\\SnsQs\\": "pkg/snsqs/", + "Enqueue\\Stomp\\": "pkg/stomp/", + "Enqueue\\Test\\": "pkg/test/", + "Enqueue\\Dsn\\": "pkg/dsn/", + "Enqueue\\Wamp\\": "pkg/wamp/", + "Enqueue\\Monitoring\\": "pkg/monitoring/", + "Enqueue\\": "pkg/enqueue/" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "autoload-dev": { "files": [ - "pkg/enqueue/functions_include.php", "pkg/rdkafka/Tests/bootstrap.php" ], "psr-0": { @@ -60,95 +123,16 @@ "bin-dir": "bin", "platform": { "ext-amqp": "1.9.3", - "ext-gearman": "1.1", - "ext-rdkafka": "3.3", - "ext-mongodb": "1.3" - } - }, - "repositories": [ - { - "type": "path", - "url": "pkg/test" - }, - { - "type": "path", - "url": "pkg/enqueue" - }, - { - "type": "path", - "url": "pkg/stomp" - }, - { - "type": "path", - "url": "pkg/amqp-ext" - }, - { - "type": "path", - "url": "pkg/amqp-lib" - }, - { - "type": "path", - "url": "pkg/amqp-bunny" - }, - { - "type": "path", - "url": "pkg/amqp-tools" - }, - { - "type": "path", - "url": "pkg/redis" - }, - { - "type": "path", - "url": "pkg/enqueue-bundle" - }, - { - "type": "path", - "url": "pkg/job-queue" - }, - { - "type": "path", - "url": "pkg/fs" - }, - { - "type": "path", - "url": "pkg/null" - }, - { - "type": "path", - "url": "pkg/dbal" - }, - { - "type": "path", - "url": "pkg/sqs" - }, - { - "type": "path", - "url": "pkg/pheanstalk" - }, - { - "type": "path", - "url": "pkg/gearman" - }, - { - "type": "path", - "url": "pkg/rdkafka" - }, - { - "type": "path", - "url": "pkg/gps" - }, - { - "type": "path", - "url": "pkg/simple-client" - }, - { - "type": "path", - "url": "pkg/async-event-dispatcher" - }, - { - "type": "path", - "url": "pkg/mongodb" + "ext-gearman": "2.0.3", + "ext-rdkafka": "4.0", + "ext-bcmath": "1", + "ext-mbstring": "1", + "ext-mongodb": "1.17.3", + "ext-sockets": "1" + }, + "prefer-stable": true, + "allow-plugins": { + "php-http/discovery": false } - ] + } } diff --git a/docker-compose.yml b/docker-compose.yml index 02d8dd1bc..8851efc28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,10 +2,16 @@ version: '2' services: dev: - image: enqueue/dev:latest + # when image publishing gets sorted: +# image: enqueue/dev:${PHP_VERSION:-7.4} + build: + context: docker + args: + PHP_VERSION: "${PHP_VERSION:-8.1}" depends_on: - rabbitmq - mysql + - postgres - redis - beanstalkd - gearmand @@ -14,44 +20,43 @@ services: - google-pubsub - rabbitmqssl - mongo + - thruway - localstack volumes: - './:/mqdev' environment: - AMQP_DSN=amqp://guest:guest@rabbitmq:5672/mqdev + - RABBITMQ_AMQP_DSN=amqp+rabbitmq://guest:guest@rabbitmq:5672/mqdev - AMQPS_DSN=amqps://guest:guest@rabbitmqssl:5671 + - STOMP_DSN=stomp://guest:guest@rabbitmq:61613/mqdev + - RABITMQ_STOMP_DSN=stomp+rabbitmq://guest:guest@rabbitmq:61613/mqdev + - RABBITMQ_MANAGMENT_DSN=http://guest:guest@rabbitmq:15672/mqdev - DOCTRINE_DSN=mysql://root:rootpass@mysql/mqdev - - RABBITMQ_HOST=rabbitmq - - RABBITMQ_USER=guest - - RABBITMQ_PASSWORD=guest - - RABBITMQ_VHOST=mqdev - - RABBITMQ_AMQP__PORT=5672 - - RABBITMQ_STOMP_PORT=61613 - - DOCTRINE_DRIVER=pdo_mysql - - DOCTRINE_HOST=mysql - - DOCTRINE_PORT=3306 - - DOCTRINE_DB_NAME=mqdev - - DOCTRINE_USER=root - - DOCTRINE_PASSWORD=rootpass + - DOCTRINE_POSTGRES_DSN=postgres://postgres:pass@postgres/template1 + - MYSQL_DSN=mysql://root:rootpass@mysql/mqdev + - POSTGRES_DSN=postgres://postgres:pass@postgres/postgres + - PREDIS_DSN=redis+predis://redis + - PHPREDIS_DSN=redis+phpredis://redis + - GPS_DSN=gps:?projectId=mqdev&emulatorHost=http://google-pubsub:8085 + - SQS_DSN=sqs:?key=key&secret=secret®ion=us-east-1&endpoint=http://localstack:4566&version=latest + - SNS_DSN=sns:?key=key&secret=secret®ion=us-east-1&endpoint=http://localstack:4566&version=latest + - SNSQS_DSN=snsqs:?key=key&secret=secret®ion=us-east-1&sns_endpoint=http://localstack:4566&sqs_endpoint=http://localstack:4566&version=latest + - WAMP_DSN=wamp://thruway:9090 - REDIS_HOST=redis - REDIS_PORT=6379 - AWS_SQS_KEY=key - AWS_SQS_SECRET=secret - AWS_SQS_REGION=us-east-1 - - AWS_SQS_ENDPOINT=http://localstack:4576 + - AWS_SQS_ENDPOINT=http://localstack:4566 - AWS_SQS_VERSION=latest - - BEANSTALKD_HOST=beanstalkd - - BEANSTALKD_PORT=11300 - BEANSTALKD_DSN=beanstalk://beanstalkd:11300 - GEARMAN_DSN=gearman://gearmand:4730 + - MONGO_DSN=mongodb://mongo - RDKAFKA_HOST=kafka - RDKAFKA_PORT=9092 - - PUBSUB_EMULATOR_HOST=http://google-pubsub:8085 - - GCLOUD_PROJECT=mqdev - - MONGO_DSN=mongodb://mongo rabbitmq: - image: 'enqueue/rabbitmq:latest' + image: 'enqueue/rabbitmq:3.7' environment: - RABBITMQ_DEFAULT_USER=guest - RABBITMQ_DEFAULT_PASS=guest @@ -77,11 +82,16 @@ services: - "6379:6379" mysql: - image: mariadb:10 - volumes: - - mysql-data:/var/lib/mysql + image: mysql:5.7 + platform: linux/amd64 environment: MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: mqdev + + postgres: + image: postgres + environment: + POSTGRES_PASSWORD: pass generate-changelog: image: enqueue/generate-changelog:latest @@ -95,13 +105,13 @@ services: - '2181:2181' kafka: - image: 'wurstmeister/kafka:0.10.2.1' + image: 'wurstmeister/kafka' ports: - '9092:9092' environment: KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' - volumes: - - '/var/run/docker.sock:/var/run/docker.sock' + KAFKA_ADVERTISED_HOST_NAME: kafka + KAFKA_ADVERTISED_PORT: 9092 google-pubsub: image: 'google/cloud-sdk:latest' @@ -112,14 +122,30 @@ services: ports: - "27017:27017" + thruway: + build: './docker/thruway' + ports: + - '9090:9090' + localstack: - image: 'localstack/localstack:latest' + image: 'localstack/localstack:3.6.0' ports: - - '4576:4576' + - "127.0.0.1:4566:4566" # LocalStack Gateway + - "127.0.0.1:4510-4559:4510-4559" # external services port range environment: HOSTNAME_EXTERNAL: 'localstack' - SERVICES: 'sqs' + SERVICES: 's3,sqs,sns' + + influxdb: + image: 'influxdb:latest' -volumes: - mysql-data: - driver: local + chronograf: + image: 'chronograf:latest' + entrypoint: 'chronograf --influxdb-url=http://influxdb:8086' + ports: + - '8888:8888' + + grafana: + image: 'grafana/grafana:latest' + ports: + - '3000:3000' diff --git a/docker/Dockerfile b/docker/Dockerfile index 323e241d5..b2f30c62e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,36 +1,76 @@ -FROM formapro/nginx-php-fpm:latest-all-exts +ARG PHP_VERSION=8.2 +FROM makasim/nginx-php-fpm:${PHP_VERSION}-all-exts + +ARG PHP_VERSION ## libs RUN set -x && \ apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests wget curl openssl ca-certificates nano netcat php-dev php-redis git python + apt-get install -y --no-install-recommends --no-install-suggests \ + wget \ + curl \ + openssl \ + ca-certificates \ + nano \ + netcat \ + php${PHP_VERSION}-dev \ + php${PHP_VERSION}-redis \ + php${PHP_VERSION}-pgsql \ + git \ + python \ + php${PHP_VERSION}-amqp \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-mysql \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-mongodb \ + php${PHP_VERSION}-mbstring \ + make \ + g++ \ + unzip \ + && \ + update-alternatives --install /usr/bin/php php /usr/bin/php${PHP_VERSION} 100 +## gearman RUN set -x && \ - apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests php-dev librabbitmq-dev make && \ - mkdir -p $HOME/php-amqp && \ - cd $HOME/php-amqp && \ - git clone https://github.com/pdezwart/php-amqp.git . && git checkout v1.9.3 && \ - phpize --clean && phpize && ./configure && make install + apt-get install -y --no-install-recommends --no-install-suggests \ + libgearman-dev \ + && \ + mkdir -p $HOME/gearman && \ + cd $HOME/gearman && \ + git clone https://github.com/php/pecl-networking-gearman.git . && \ + git checkout gearman-2.1.0 && \ + phpize && ./configure && make && make install && \ + if [ ! -f /etc/php/${PHP_VERSION}/cli/conf.d/20-gearman.ini ]; then \ + echo "extension=gearman.so" > /etc/php/${PHP_VERSION}/cli/conf.d/20-gearman.ini && \ + echo "extension=gearman.so" > /etc/php/${PHP_VERSION}/fpm/conf.d/20-gearman.ini \ + ; \ + fi; ## librdkafka RUN set -x && \ - apt-get update && \ - apt-get install -y --no-install-recommends --no-install-suggests g++ php-pear php-dev && \ mkdir -p $HOME/librdkafka && \ cd $HOME/librdkafka && \ git clone https://github.com/edenhill/librdkafka.git . && \ - git checkout v0.11.1 && \ - ./configure && make && make install && \ - pecl install rdkafka && \ - echo "extension=rdkafka.so" > /etc/php/7.2/cli/conf.d/10-rdkafka.ini && \ - echo "extension=rdkafka.so" > /etc/php/7.2/fpm/conf.d/10-rdkafka.ini + git checkout v1.0.0 && \ + ./configure && make && make install -COPY ./php/cli.ini /etc/php/7.2/cli/conf.d/1-dev_cli.ini +## php-rdkafka +RUN set -x && \ + mkdir -p $HOME/php-rdkafka && \ + cd $HOME/php-rdkafka && \ + git clone https://github.com/arnaud-lb/php-rdkafka.git . && \ + git checkout 5.0.1 && \ + phpize && ./configure && make all && make install && \ + echo "extension=rdkafka.so" > /etc/php/${PHP_VERSION}/cli/conf.d/10-rdkafka.ini && \ + echo "extension=rdkafka.so" > /etc/php/${PHP_VERSION}/fpm/conf.d/10-rdkafka.ini + +COPY ./php/cli.ini /etc/php/${PHP_VERSION}/cli/conf.d/1-dev_cli.ini COPY ./bin/dev_entrypoiny.sh /usr/local/bin/entrypoint.sh RUN chmod u+x /usr/local/bin/entrypoint.sh RUN mkdir -p /mqdev WORKDIR /mqdev +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + CMD /usr/local/bin/entrypoint.sh diff --git a/docker/Dockerfile.3.6-rabbitmq b/docker/Dockerfile.3.6-rabbitmq new file mode 100644 index 000000000..9c9cc61b3 --- /dev/null +++ b/docker/Dockerfile.3.6-rabbitmq @@ -0,0 +1,18 @@ +FROM rabbitmq:3.6-management + +RUN apt-get update && \ + apt-get -y --no-install-recommends --no-install-suggests install ca-certificates curl unzip && \ + rm -rf /var/lib/apt/lists/* + +RUN curl https://dl.bintray.com/rabbitmq/community-plugins/3.6.x/rabbitmq_delayed_message_exchange/rabbitmq_delayed_message_exchange-20171215-3.6.x.zip > /tmp/delayed_plugin.zip +RUN cd /tmp && \ + unzip delayed_plugin.zip && \ + rm delayed_plugin.zip && \ + mv rabbitmq_delayed_message_exchange-20171215-3.6.x.ez $RABBITMQ_HOME/plugins/rabbitmq_delayed_message_exchange-20171215-3.6.x.ez + +RUN rabbitmq-plugins enable --offline rabbitmq_delayed_message_exchange +RUN rabbitmq-plugins enable --offline rabbitmq_stomp + +RUN apt-get purge -y --auto-remove ca-certificates curl unzip + +EXPOSE 61613 \ No newline at end of file diff --git a/docker/Dockerfile.3.7-rabbitmq b/docker/Dockerfile.3.7-rabbitmq new file mode 100644 index 000000000..116a3a96c --- /dev/null +++ b/docker/Dockerfile.3.7-rabbitmq @@ -0,0 +1,18 @@ +FROM rabbitmq:3.7-management + +RUN apt-get update && \ + apt-get -y --no-install-recommends --no-install-suggests install ca-certificates curl unzip && \ + rm -rf /var/lib/apt/lists/* + +RUN curl https://dl.bintray.com/rabbitmq/community-plugins/3.7.x/rabbitmq_delayed_message_exchange/rabbitmq_delayed_message_exchange-20171201-3.7.x.zip > /tmp/delayed_plugin.zip +RUN cd /tmp && \ + unzip delayed_plugin.zip && \ + rm delayed_plugin.zip && \ + mv rabbitmq_delayed_message_exchange-20171201-3.7.x.ez $RABBITMQ_HOME/plugins/rabbitmq_delayed_message_exchange-20171201-3.7.x.ez + +RUN rabbitmq-plugins enable --offline rabbitmq_delayed_message_exchange +RUN rabbitmq-plugins enable --offline rabbitmq_stomp + +RUN apt-get purge -y --auto-remove ca-certificates curl unzip + +EXPOSE 61613 \ No newline at end of file diff --git a/docker/Dockerfile.rabbitmq b/docker/Dockerfile.rabbitmq deleted file mode 100644 index 96a41dc34..000000000 --- a/docker/Dockerfile.rabbitmq +++ /dev/null @@ -1,10 +0,0 @@ -FROM rabbitmq:3-management - -RUN apt-get update -RUN apt-get install -y curl - -RUN curl http://www.rabbitmq.com/community-plugins/v3.6.x/rabbitmq_delayed_message_exchange-0.0.1.ez > $RABBITMQ_HOME/plugins/rabbitmq_delayed_message_exchange-0.0.1.ez -RUN rabbitmq-plugins enable --offline rabbitmq_delayed_message_exchange -RUN rabbitmq-plugins enable --offline rabbitmq_stomp - -EXPOSE 61613 \ No newline at end of file diff --git a/docker/bin/refresh-mysql-database.php b/docker/bin/refresh-mysql-database.php new file mode 100644 index 000000000..05e78f43d --- /dev/null +++ b/docker/bin/refresh-mysql-database.php @@ -0,0 +1,16 @@ +createContext(); + +$dbalContext->getDbalConnection()->getSchemaManager()->dropAndCreateDatabase($database); +$dbalContext->getDbalConnection()->exec('USE '.$database); +$dbalContext->createDataBaseTable(); + +echo 'MySQL Database is updated'.\PHP_EOL; diff --git a/docker/bin/refresh-postgres-database.php b/docker/bin/refresh-postgres-database.php new file mode 100644 index 000000000..1d96c3c07 --- /dev/null +++ b/docker/bin/refresh-postgres-database.php @@ -0,0 +1,14 @@ +createContext(); + +$dbalContext->getDbalConnection()->getSchemaManager()->dropAndCreateDatabase('postgres'); +$dbalContext->createDataBaseTable(); + +echo 'Postgresql Database is updated'.\PHP_EOL; diff --git a/bin/test b/docker/bin/test.sh similarity index 78% rename from bin/test rename to docker/bin/test.sh index c9de45600..b78e1c1a8 100755 --- a/bin/test +++ b/docker/bin/test.sh @@ -32,15 +32,19 @@ trap "FORCE_EXIT=true" SIGTERM SIGINT waitForService rabbitmq 5672 50 waitForService rabbitmqssl 5671 50 waitForService mysql 3306 50 +waitForService postgres 5432 50 waitForService redis 6379 50 waitForService beanstalkd 11300 50 waitForService gearmand 4730 50 waitForService kafka 9092 50 waitForService mongo 27017 50 -waitForService localstack 4576 50 +waitForService thruway 9090 50 +waitForService localstack 4566 50 -php pkg/job-queue/Tests/Functional/app/console doctrine:database:create --if-not-exists -php pkg/job-queue/Tests/Functional/app/console doctrine:schema:update --force +php docker/bin/refresh-mysql-database.php || exit 1 +php docker/bin/refresh-postgres-database.php || exit 1 +php pkg/job-queue/Tests/Functional/app/console doctrine:database:create --if-not-exists || exit 1 +php pkg/job-queue/Tests/Functional/app/console doctrine:schema:update --force --complete || exit 1 #php pkg/enqueue-bundle/Tests/Functional/app/console.php config:dump-reference enqueue bin/phpunit "$@" diff --git a/docker/thruway/Dockerfile b/docker/thruway/Dockerfile new file mode 100644 index 000000000..042a49d64 --- /dev/null +++ b/docker/thruway/Dockerfile @@ -0,0 +1,13 @@ +FROM makasim/nginx-php-fpm:7.4-all-exts + +RUN mkdir -p /thruway +WORKDIR /thruway + +# Thruway router +COPY --from=composer /usr/bin/composer /usr/bin/composer +RUN COMPOSER_HOME=/thruway composer global require --prefer-dist --no-scripts voryx/thruway + +COPY WsRouter.php . + +CMD ["/usr/bin/php", "WsRouter.php"] + diff --git a/docker/thruway/WsRouter.php b/docker/thruway/WsRouter.php new file mode 100644 index 000000000..ee5bb948a --- /dev/null +++ b/docker/thruway/WsRouter.php @@ -0,0 +1,14 @@ +addTransportProvider($transportProvider); + +$router->start(); diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..29949d465 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +/_site +/Gemfile.lock diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 000000000..5b4694d54 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,28 @@ +source "https://rubygems.org/" + +# Hello! This is where you manage which Jekyll version is used to run. +# When you want to use a different version, change it below, save the +# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: +# +# bundle exec jekyll serve +# +# This will help ensure the proper Jekyll version is running. +# Happy Jekylling! +gem "jekyll", "~> 3.8.5" + +# This is the default theme for new Jekyll sites. You may change this to anything you like. +# gem "minima", "~> 2.0" +gem "just-the-docs" + +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "github-pages" +# gem "jekyll-feed", "~> 0.6" +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +# Performance-booster for watching directories on Windows +gem "wdm", "~> 0.1.0" if Gem.win_platform? + diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..0e457d2e6 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,38 @@ +title: enqueue-dev +description: A Jekyll theme for documentation +baseurl: "" # the subpath of your site, e.g. /blog +url: "" # the base hostname & protocol for your site, e.g. http://example.com + +permalink: pretty +exclude: + - "node_modules/" + - "*.gemspec" + - "*.gem" + - "Gemfile" + - "Gemfile.lock" + - "package.json" + - "package-lock.json" + - "script/" + - "LICENSE.txt" + - "lib/" + - "bin/" + - "README.md" + - "Rakefile" + +markdown: kramdown + +# Enable or disable the site search +search_enabled: true + +# Aux links for the upper right navigation +aux_links: + "enqueue-dev on GitHub": + - "//github.com/php-enqueue/enqueue-dev" + +# Color scheme currently only supports "dark" or nil (default) +color_scheme: nil + +remote_theme: pmarsceill/just-the-docs + +plugins: + - jekyll-seo-tag diff --git a/docs/_includes/nav.html b/docs/_includes/nav.html new file mode 100644 index 000000000..fa2492acc --- /dev/null +++ b/docs/_includes/nav.html @@ -0,0 +1,62 @@ + diff --git a/docs/_includes/support.md b/docs/_includes/support.md new file mode 100644 index 000000000..3f8cf322a --- /dev/null +++ b/docs/_includes/support.md @@ -0,0 +1,7 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become our client](http://forma-pro.com/) + +--- diff --git a/docs/async_event_dispatcher/quick_tour.md b/docs/async_event_dispatcher/quick_tour.md index 1ae166025..c9eb0e0a9 100644 --- a/docs/async_event_dispatcher/quick_tour.md +++ b/docs/async_event_dispatcher/quick_tour.md @@ -1,8 +1,14 @@ +--- +layout: default +nav_exclude: true +--- +{% include support.md %} + # Async event dispatcher (Symfony) -The doc shows how you can setup async event dispatching in plain PHP. +The doc shows how you can setup async event dispatching in plain PHP. If you are looking for the ways to use it in Symfony application [read this post instead](../bundle/async_events.md) - + * [Installation](#installation) * [Configuration](#configuration) * [Dispatch event](#dispatch-event) @@ -38,8 +44,8 @@ $context = (new FsConnectionFactory('file://'.__DIR__.'/queues'))->createContext $eventQueue = $context->createQueue('symfony_events'); $registry = new SimpleRegistry( - ['the_event' => 'default'], - ['default' => new PhpSerializerEventTransformer($context, true)] + ['the_event' => 'default'], + ['default' => new PhpSerializerEventTransformer($context)] ); $asyncListener = new AsyncListener($context, $registry, $eventQueue); @@ -81,7 +87,7 @@ $dispatcher->dispatch('the_event', new GenericEvent('theSubject')); // consume.php -use Interop\Queue\PsrProcessor; +use Interop\Queue\Processor; require_once __DIR__.'/vendor/autoload.php'; include __DIR__.'/config.php'; @@ -93,13 +99,13 @@ while (true) { $result = $asyncProcessor->process($message, $context); switch ((string) $result) { - case PsrProcessor::ACK: + case Processor::ACK: $consumer->acknowledge($message); break; - case PsrProcessor::REJECT: + case Processor::REJECT: $consumer->reject($message); break; - case PsrProcessor::REQUEUE: + case Processor::REQUEUE: $consumer->reject($message, true); break; default: diff --git a/docs/bundle/async_commands.md b/docs/bundle/async_commands.md new file mode 100644 index 000000000..b099c0b9c --- /dev/null +++ b/docs/bundle/async_commands.md @@ -0,0 +1,77 @@ +--- +layout: default +parent: "Symfony bundle" +title: Async commands +nav_order: 7 +--- +{% include support.md %} + +# Async commands + +## Installation + +```bash +$ composer require enqueue/async-command:0.9.x-dev +``` + +## Configuration + +```yaml +# config/packages/enqueue_async_commands.yaml + +enqueue: + default: + async_commands: + enabled: true + timeout: 60 + command_name: ~ + queue_name: ~ +``` + +## Usage + +```php +get(ProducerInterface::class); + +$cmd = new RunCommand('debug:container', ['--tag=form.type']); +$producer->sendCommand(Commands::RUN_COMMAND, $cmd); +``` + +optionally you can get a command execution result: + +```php +get(ProducerInterface::class); + +$promise = $producer->sendCommand(Commands::RUN_COMMAND, new RunCommand('debug:container'), true); + +// do other stuff. + +if ($replyMessage = $promise->receive(5000)) { + $result = CommandResult::jsonUnserialize($replyMessage->getBody()); + + echo $result->getOutput(); +} +``` + +[back to index](index.md) diff --git a/docs/bundle/async_events.md b/docs/bundle/async_events.md index 84c347e85..d2d256146 100644 --- a/docs/bundle/async_events.md +++ b/docs/bundle/async_events.md @@ -1,8 +1,16 @@ +--- +layout: default +parent: "Symfony bundle" +title: Async events +nav_order: 6 +--- +{% include support.md %} + # Async events -The EnqueueBundle allows you to dispatch events asynchronously. -Behind the scene it replaces your listener with one that sends a message to MQ. -The message contains the event object. +The EnqueueBundle allows you to dispatch events asynchronously. +Behind the scene it replaces your listener with one that sends a message to MQ. +The message contains the event object. The consumer, once it receives the message, restores the event and dispatches it to only async listeners. Async listeners benefits: @@ -22,15 +30,16 @@ If you already [installed the bundle](quick_tour.md#install), then enable `async # app/config/config.yml enqueue: - async_events: - enabled: true - # if you'd like to send send messages onTerminate use spool_producer (it further reduces response time): - # spool_producer: true + default: + async_events: + enabled: true + # if you'd like to send send messages onTerminate use spool_producer (it further reduces response time): + # spool_producer: true ``` ## Usage -To make your listener async you have add `async: true` attribute to the tag `kernel.event_listener`, like this: +To make your listener async you have add `async: true` and `dispatcher: 'enqueue.events.event_dispatcher'` attributes to the tag `kernel.event_listener`, like this: ```yaml # app/config/config.yml @@ -39,7 +48,7 @@ services: acme.foo_listener: class: 'AcmeBundle\Listener\FooListener' tags: - - { name: 'kernel.event_listener', async: true, event: 'foo', method: 'onEvent' } + - { name: 'kernel.event_listener', async: true, event: 'foo', method: 'onEvent', dispatcher: 'enqueue.events.event_dispatcher' } ``` or to `kernel.event_subscriber`: @@ -47,14 +56,14 @@ or to `kernel.event_subscriber`: ```yaml # app/config/config.yml -services: +services: test_async_subscriber: class: 'AcmeBundle\Listener\TestAsyncSubscriber' tags: - - { name: 'kernel.event_subscriber', async: true } + - { name: 'kernel.event_subscriber', async: true, dispatcher: 'enqueue.events.event_dispatcher' } ``` -That's basically it. The rest of the doc describes advanced features. +That's basically it. The rest of the doc describes advanced features. ## Advanced Usage. @@ -69,7 +78,7 @@ services: public: false arguments: ['@enqueue.transport.default.context', '@enqueue.events.registry', 'a_queue_name'] tags: - - { name: 'kernel.event_listener', event: 'foo', method: 'onEvent' } + - { name: 'kernel.event_listener', event: 'foo', method: 'onEvent', dispatcher: 'enqueue.events.event_dispatcher' } ``` @@ -77,8 +86,8 @@ services: The bundle uses [php serializer](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue-bundle/Events/PhpSerializerEventTransformer.php) transformer by default to pass events through MQ. You can write a transformer for each event type by implementing the `Enqueue\AsyncEventDispatcher\EventTransformer` interface. -Consider the next example. It shows how to send an event that contains Doctrine entity as a subject - +Consider the next example. It shows how to send an event that contains Doctrine entity as a subject + ```php getSubject(); $entityClass = get_class($entity); - + $manager = $this->doctrine->getManagerForClass($entityClass); $meta = $manager->getClassMetadata($entityClass); $id = $meta->getIdentifierValues($entity); - + $message = new Message(); $message->setBody([ - 'entityClass' => $entityClass, + 'entityClass' => $entityClass, 'entityId' => $id, 'arguments' => $event->getArguments() ]); @@ -132,17 +141,17 @@ class FooEventTransformer implements EventTransformer /** * {@inheritdoc} */ - public function toEvent($eventName, PsrMessage $message) + public function toEvent($eventName, QueueMessage $message) { $data = JSON::decode($message->getBody()); - + $entityClass = $data['entityClass']; - + $manager = $this->doctrine->getManagerForClass($entityClass); if (false == $entity = $manager->find($entityClass, $data['entityId'])) { return Result::reject('The entity could not be found.'); } - + return new GenericEvent($entity, $data['arguments']); } } @@ -161,7 +170,7 @@ services: - {name: 'enqueue.event_transformer', eventName: 'foo' } ``` -The `eventName` attribute accepts a regexp. You can do next `eventName: '/foo\..*?/'`. +The `eventName` attribute accepts a regexp. You can do next `eventName: '/foo\..*?/'`. It uses this transformer for all event with the name beginning with `foo.` -[back to index](../index.md) +[back to index](index.md) diff --git a/docs/bundle/cli_commands.md b/docs/bundle/cli_commands.md index 02225a105..6c383f8c6 100644 --- a/docs/bundle/cli_commands.md +++ b/docs/bundle/cli_commands.md @@ -1,14 +1,21 @@ +--- +layout: default +parent: "Symfony bundle" +title: CLI commands +nav_order: 3 +--- +{% include support.md %} + # Cli commands -The EnqueueBundle provides several commands. +The EnqueueBundle provides several commands. The most useful one `enqueue:consume` connects to the broker and process the messages. Other commands could be useful during debugging (like `enqueue:topics`) or deployment (like `enqueue:setup-broker`). * [enqueue:consume](#enqueueconsume) * [enqueue:produce](#enqueueproduce) * [enqueue:setup-broker](#enqueuesetup-broker) -* [enqueue:queues](#enqueuequeues) -* [enqueue:topics](#enqueuetopics) +* [enqueue:routes](#enqueueroutes) * [enqueue:transport:consume](#enqueuetransportconsume) ## enqueue:consume @@ -26,18 +33,20 @@ Options: --message-limit=MESSAGE-LIMIT Consume n messages and exit --time-limit=TIME-LIMIT Consume messages during this time --memory-limit=MEMORY-LIMIT Consume messages until process reaches this memory limit in MB + --niceness=NICENESS Set process niceness --setup-broker Creates queues, topics, exchanges, binding etc on broker side. - --idle-timeout=IDLE-TIMEOUT The time in milliseconds queue consumer idle if no message has been received. --receive-timeout=RECEIVE-TIMEOUT The time in milliseconds queue consumer waits for a message. + --logger[=LOGGER] A logger to be used. Could be "default", "null", "stdout". [default: "default"] --skip[=SKIP] Queues to skip consumption of messages from (multiple values allowed) + -c, --client[=CLIENT] The client to consume messages from. [default: "default"] -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n, --no-interaction Do not ask any interactive question - -e, --env=ENV The environment name [default: "test"] - --no-debug Switches off debug mode + -e, --env=ENV The Environment name. [default: "test"] + --no-debug Switches off debug mode. -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Help: @@ -49,26 +58,28 @@ Help: ``` ./bin/console enqueue:produce --help Usage: - enqueue:produce - enq:p + enqueue:produce [options] [--] Arguments: - topic A topic to send message to - message A message to send + message A message Options: - -h, --help Display this help message - -q, --quiet Do not output any message - -V, --version Display this application version - --ansi Force ANSI output - --no-ansi Disable ANSI output - -n, --no-interaction Do not ask any interactive question - -e, --env=ENV The environment name [default: "dev"] - --no-debug Switches off debug mode - -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + -c, --client[=CLIENT] The client to send messages to. [default: "default"] + --topic[=TOPIC] The topic to send a message to + --command[=COMMAND] The command to send a message to + -h, --help Display this help message + -q, --quiet Do not output any message + -V, --version Display this application version + --ansi Force ANSI output + --no-ansi Disable ANSI output + -n, --no-interaction Do not ask any interactive question + -e, --env=ENV The Environment name. [default: "test"] + --no-debug Switches off debug mode. + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Help: - A command to send a message to topic + Sends an event to the topic + ``` ## enqueue:setup-broker @@ -76,101 +87,81 @@ Help: ``` ./bin/console enqueue:setup-broker --help Usage: - enqueue:setup-broker + enqueue:setup-broker [options] enq:sb Options: - -h, --help Display this help message - -q, --quiet Do not output any message - -V, --version Display this application version - --ansi Force ANSI output - --no-ansi Disable ANSI output - -n, --no-interaction Do not ask any interactive question - -e, --env=ENV The environment name [default: "dev"] - --no-debug Switches off debug mode - -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + -c, --client[=CLIENT] The client to consume messages from. [default: "default"] + -h, --help Display this help message + -q, --quiet Do not output any message + -V, --version Display this application version + --ansi Force ANSI output + --no-ansi Disable ANSI output + -n, --no-interaction Do not ask any interactive question + -e, --env=ENV The Environment name. [default: "test"] + --no-debug Switches off debug mode. + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Help: - Creates all required queues + Setup broker. Configure the broker, creates queues, topics and so on. ``` -## enqueue:queues +## enqueue:routes ``` -./bin/console enqueue:queues --help +./bin/console enqueue:routes --help Usage: - enqueue:queues - enq:m:q - debug:enqueue:queues + enqueue:routes [options] + debug:enqueue:routes Options: - -h, --help Display this help message - -q, --quiet Do not output any message - -V, --version Display this application version - --ansi Force ANSI output - --no-ansi Disable ANSI output - -n, --no-interaction Do not ask any interactive question - -e, --env=ENV The environment name [default: "dev"] - --no-debug Switches off debug mode - -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + --show-route-options Adds ability to hide options. + -c, --client[=CLIENT] The client to consume messages from. [default: "default"] + -h, --help Display this help message + -q, --quiet Do not output any message + -V, --version Display this application version + --ansi Force ANSI output + --no-ansi Disable ANSI output + -n, --no-interaction Do not ask any interactive question + -e, --env=ENV The Environment name. [default: "test"] + --no-debug Switches off debug mode. + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Help: - A command shows all available queues and some information about them. -``` - -## enqueue:topics - -``` -./bin/console enqueue:topics --help -Usage: - enqueue:topics - enq:m:t - debug:enqueue:topics - -Options: - -h, --help Display this help message - -q, --quiet Do not output any message - -V, --version Display this application version - --ansi Force ANSI output - --no-ansi Disable ANSI output - -n, --no-interaction Do not ask any interactive question - -e, --env=ENV The environment name [default: "dev"] - --no-debug Switches off debug mode - -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug - -Help: - A command shows all available topics and some information about them. + A command lists all registered routes. ``` ## enqueue:transport:consume - + ``` ./bin/console enqueue:transport:consume --help -Usage:ng mqdev_gearmand_1 ... done - enqueue:transport:consume [options] [--] +Usage: + enqueue:transport:consume [options] [--] []... Arguments: - processor-service A message processor service + processor A message processor. + queues A queue to consume from Options: --message-limit=MESSAGE-LIMIT Consume n messages and exit --time-limit=TIME-LIMIT Consume messages during this time --memory-limit=MEMORY-LIMIT Consume messages until process reaches this memory limit in MB - --idle-timeout=IDLE-TIMEOUT The time in milliseconds queue consumer idle if no message has been received. + --niceness=NICENESS Set process niceness --receive-timeout=RECEIVE-TIMEOUT The time in milliseconds queue consumer waits for a message. - --queue[=QUEUE] Queues to consume from (multiple values allowed) + --logger[=LOGGER] A logger to be used. Could be "default", "null", "stdout". [default: "default"] + -t, --transport[=TRANSPORT] The transport to consume messages from. [default: "default"] -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n, --no-interaction Do not ask any interactive question - -e, --env=ENV The environment name [default: "test"] - --no-debug Switches off debug mode + -e, --env=ENV The Environment name. [default: "test"] + --no-debug Switches off debug mode. -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Help: A worker that consumes message from a broker. To use this broker you have to explicitly set a queue to consume from and a message processor service ``` -[back to index](../index.md) +[back to index](index.md) diff --git a/docs/bundle/config_reference.md b/docs/bundle/config_reference.md index 03290a602..042b93cfa 100644 --- a/docs/bundle/config_reference.md +++ b/docs/bundle/config_reference.md @@ -1,3 +1,11 @@ +--- +layout: default +parent: "Symfony bundle" +title: Config reference +nav_order: 2 +--- +{% include support.md %} + # Config reference You can get this info by running `./bin/console config:dump-reference enqueue` command. @@ -5,311 +13,71 @@ You can get this info by running `./bin/console config:dump-reference enqueue` c ```yaml # Default configuration for extension with alias: "enqueue" enqueue: - transport: # Required - default: - alias: ~ - dsn: ~ - null: - dsn: ~ - stomp: - host: localhost - port: 61613 - login: guest - password: guest - vhost: / - sync: true - connection_timeout: 1 - buffer_size: 1000 - lazy: true - ssl_on: false - rabbitmq_stomp: - host: localhost - port: 61613 - login: guest - password: guest - vhost: / - sync: true - connection_timeout: 1 - buffer_size: 1000 - lazy: true - ssl_on: false - - # The option tells whether RabbitMQ broker has management plugin installed or not - management_plugin_installed: false - management_plugin_port: 15672 - - # The option tells whether RabbitMQ broker has delay plugin installed or not - delay_plugin_installed: false - amqp: - driver: ~ - - # The connection to AMQP broker set as a string. Other parameters could be used as defaults - dsn: ~ - - # The host to connect too. Note: Max 1024 characters - host: ~ - - # Port on the host. - port: ~ - - # The user name to use. Note: Max 128 characters. - user: ~ - - # Password. Note: Max 128 characters. - pass: ~ - - # The virtual host on the host. Note: Max 128 characters. - vhost: ~ - - # Connection timeout. Note: 0 or greater seconds. May be fractional. - connection_timeout: ~ - - # Timeout in for income activity. Note: 0 or greater seconds. May be fractional. - read_timeout: ~ - - # Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional. - write_timeout: ~ - - # How often to send heartbeat. 0 means off. - heartbeat: ~ - persisted: ~ - lazy: ~ - - # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher - receive_method: ~ # One of "basic_get"; "basic_consume" - - # The server will send a message in advance if it is equal to or smaller in size than the available prefetch size. May be set to zero, meaning "no specific limit" - qos_prefetch_size: ~ - - # Specifies a prefetch window in terms of whole messages - qos_prefetch_count: ~ - - # If "false" the QoS settings apply to the current channel only. If this field is "true", they are applied to the entire connection. - qos_global: ~ - - # The options that are specific to the amqp transport you chose. For example amqp+lib have insist, keepalive, stream options. amqp+bunny has tcp_nodelay extra option. - driver_options: ~ - - # Should be true if you want to use secure connections. False by default - ssl_on: ~ - - # This option determines whether ssl client verifies that the server cert is for the server it is known as. True by default. - ssl_verify: ~ - - # Location of Certificate Authority file on local filesystem which should be used with the verify_peer context option to authenticate the identity of the remote peer. A string. - ssl_cacert: ~ - - # Path to local certificate file on filesystem. It must be a PEM encoded file which contains your certificate and private key. A string - ssl_cert: ~ - - # Path to local private key file on filesystem in case of separate files for certificate (local_cert) and private key. A string. - ssl_key: ~ - rabbitmq_amqp: - driver: ~ - - # The connection to AMQP broker set as a string. Other parameters could be used as defaults - dsn: ~ - - # The host to connect too. Note: Max 1024 characters - host: ~ - - # Port on the host. - port: ~ - - # The user name to use. Note: Max 128 characters. - user: ~ - - # Password. Note: Max 128 characters. - pass: ~ - - # The virtual host on the host. Note: Max 128 characters. - vhost: ~ - - # Connection timeout. Note: 0 or greater seconds. May be fractional. - connection_timeout: ~ - - # Timeout in for income activity. Note: 0 or greater seconds. May be fractional. - read_timeout: ~ - - # Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional. - write_timeout: ~ - - # How often to send heartbeat. 0 means off. - heartbeat: ~ - persisted: ~ - lazy: ~ - - # The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher - receive_method: ~ # One of "basic_get"; "basic_consume" - - # The server will send a message in advance if it is equal to or smaller in size than the available prefetch size. May be set to zero, meaning "no specific limit" - qos_prefetch_size: ~ - - # Specifies a prefetch window in terms of whole messages - qos_prefetch_count: ~ - - # If "false" the QoS settings apply to the current channel only. If this field is "true", they are applied to the entire connection. - qos_global: ~ - - # The options that are specific to the amqp transport you chose. For example amqp+lib have insist, keepalive, stream options. amqp+bunny has tcp_nodelay extra option. - driver_options: ~ - - # Should be true if you want to use secure connections. False by default - ssl_on: ~ - - # This option determines whether ssl client verifies that the server cert is for the server it is known as. True by default. - ssl_verify: ~ - - # Location of Certificate Authority file on local filesystem which should be used with the verify_peer context option to authenticate the identity of the remote peer. A string. - ssl_cacert: ~ - - # Path to local certificate file on filesystem. It must be a PEM encoded file which contains your certificate and private key. A string - ssl_cert: ~ - - # Path to local private key file on filesystem in case of separate files for certificate (local_cert) and private key. A string. - ssl_key: ~ - - # The delay strategy to be used. Possible values are "dlx", "delayed_message_plugin" or service id - delay_strategy: dlx - fs: - - # The path to a directory where to store messages given as DSN. For example file://tmp/foo - dsn: ~ - - # The store directory where all queue\topics files will be created and messages are stored - path: ~ - - # The option tells how many messages should be read from file at once. The feature save resources but could lead to bigger messages lose. - pre_fetch_count: 1 - - # The queue files are created with this given permissions if not exist. - chmod: 384 - - # How often query for new messages. - polling_interval: 100 - redis: - - # The redis connection given as DSN. For example redis://host:port?vendor=predis - dsn: ~ - - # can be a host, or the path to a unix domain socket - host: ~ - port: ~ - - # The library used internally to interact with Redis server - vendor: ~ # One of "phpredis"; "predis"; "custom" - - # A custom redis service id, used with vendor true only - redis: ~ - - # bool, Whether it use single persisted connection or open a new one for every context - persisted: false - - # the connection will be performed as later as possible, if the option set to true - lazy: true - - # Database index to select when connected. - database: 0 - dbal: - - # The Doctrine DBAL DSN. Other parameters are ignored if set - dsn: ~ - - # Doctrine DBAL connection options. See http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html - connection: ~ - - # Doctrine dbal connection name. - dbal_connection_name: null - - # Database table name. - table_name: enqueue - - # How often query for new messages. - polling_interval: 1000 - lazy: true - sqs: - client: null - key: null - secret: null - token: null - region: ~ # Required - retries: 3 - version: '2012-11-05' - - # the connection will be performed as later as possible, if the option set to true - lazy: true - endpoint: null - gps: - - # The connection to Google Pub/Sub broker set as a string. Other parameters are ignored if set - dsn: ~ - - # The project ID from the Google Developer's Console. - projectId: ~ - - # The full path to your service account credentials.json file retrieved from the Google Developers Console. - keyFilePath: ~ - - # Number of retries for a failed request. - retries: 3 - - # Scopes to be used for the request. - scopes: [] - - # The connection will be performed as later as possible, if the option set to true - lazy: true - rdkafka: - - # The kafka DSN. Other parameters are ignored if set - dsn: ~ - - # The kafka global configuration properties - global: [] - - # The kafka topic configuration properties - topic: [] - - # Delivery report callback - dr_msg_cb: ~ - - # Error callback - error_cb: ~ - - # Called after consumer group has been rebalanced - rebalance_cb: ~ - - # Which partitioner to use - partitioner: ~ # One of "RD_KAFKA_MSG_PARTITIONER_RANDOM"; "RD_KAFKA_MSG_PARTITIONER_CONSISTENT" - - # Logging level (syslog(3) levels) - log_level: ~ - - # Commit asynchronous - commit_async: false - client: - traceable_producer: true - prefix: enqueue - app_name: app - router_topic: default - router_queue: default - router_processor: Enqueue\Client\RouterProcessor - default_processor_queue: default - redelivered_delay_time: 0 - consumption: - - # the time in milliseconds queue consumer waits if no message received - idle_timeout: 0 - # the time in milliseconds queue consumer waits for a message (100 ms by default) - receive_timeout: 100 - job: false - async_events: - enabled: false - extensions: - doctrine_ping_connection_extension: false - doctrine_clear_identity_map_extension: false - signal_extension: true - reply_extension: true + # Prototype + key: + + # The transport option could accept a string DSN, an array with DSN key, or null. It accept extra options. To find out what option you can set, look at connection factory constructor docblock. + transport: # Required + + # The MQ broker DSN. These schemes are supported: "file", "amqp", "amqps", "db2", "ibm-db2", "mssql", "sqlsrv", "mysql", "mysql2", "pgsql", "postgres", "sqlite", "sqlite3", "null", "gearman", "beanstalk", "kafka", "rdkafka", "redis", "rediss", "stomp", "sqs", "gps", "mongodb", "wamp", "ws", to use these "file", "amqp", "amqps", "db2", "ibm-db2", "mssql", "sqlsrv", "mysql", "mysql2", "pgsql", "postgres", "sqlite", "sqlite3", "null", "gearman", "beanstalk", "kafka", "rdkafka", "redis", "rediss", "stomp", "sqs", "gps", "mongodb", "wamp", "ws" you have to install a package. + dsn: ~ # Required + + # The connection factory class should implement "Interop\Queue\ConnectionFactory" interface + connection_factory_class: ~ + + # The factory class should implement "Enqueue\ConnectionFactoryFactoryInterface" interface + factory_service: ~ + + # The factory service should be a class that implements "Enqueue\ConnectionFactoryFactoryInterface" interface + factory_class: ~ + consumption: + + # the time in milliseconds queue consumer waits for a message (100 ms by default) + receive_timeout: 10000 + client: + traceable_producer: true + prefix: enqueue + separator: . + app_name: app + router_topic: default + router_queue: default + router_processor: null + redelivered_delay_time: 0 + default_queue: default + + # The array contains driver specific options + driver_options: [] + + # The "monitoring" option could accept a string DSN, an array with DSN key, or null. It accept extra options. To find out what option you can set, look at stats storage constructor doc block. + monitoring: + + # The stats storage DSN. These schemes are supported: "wamp", "ws", "influxdb". + dsn: ~ # Required + + # The factory class should implement "Enqueue\Monitoring\StatsStorageFactory" interface + storage_factory_service: ~ + + # The factory service should be a class that implements "Enqueue\Monitoring\StatsStorageFactory" interface + storage_factory_class: ~ + async_commands: + enabled: false + timeout: 60 + command_name: ~ + queue_name: ~ + job: + enabled: false + default_mapping: true + async_events: + enabled: false + extensions: + doctrine_ping_connection_extension: false + doctrine_clear_identity_map_extension: false + doctrine_odm_clear_identity_map_extension: false + doctrine_closed_entity_manager_extension: false + reset_services_extension: false + signal_extension: true + reply_extension: true ``` -[back to index](../index.md) +[back to index](index.md) diff --git a/docs/bundle/consumption_extension.md b/docs/bundle/consumption_extension.md index 81b49e52f..b05c9f89e 100644 --- a/docs/bundle/consumption_extension.md +++ b/docs/bundle/consumption_extension.md @@ -1,3 +1,11 @@ +--- +layout: default +parent: "Symfony bundle" +title: Consumption extension +nav_order: 9 +--- +{% include support.md %} + # Consumption extension Here, I show how you can create a custom extension and register it. @@ -8,20 +16,14 @@ Let's first create an extension itself: // src/AppBundle/Enqueue; namespace AppBundle\Enqueue; -use Enqueue\Consumption\ExtensionInterface; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; +use Enqueue\Consumption\Context\PostMessageReceived; -class CountProcessedMessagesExtension implements ExtensionInterface +class CountProcessedMessagesExtension implements PostMessageReceivedExtensionInterface { - use EmptyExtensionTrait; - private $processedMessages = 0; - - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + + public function onPostMessageReceived(PostMessageReceived $context): void { $this->processedMessages += 1; } @@ -38,4 +40,15 @@ services: - { name: 'enqueue.consumption.extension', priority: 10 } ``` -[back to index](../index.md) +When using multiple enqueue instances, you can apply extension to +specific or all instances by providing an additional tag attribute: + +``` +services: + app.enqueue.count_processed_messages_extension: + class: 'AppBundle\Enqueue\CountProcessedMessagesExtension' + tags: + - { name: 'enqueue.consumption.extension', priority: 10, client: 'all' } +``` + +[back to index](index.md) diff --git a/docs/bundle/debugging.md b/docs/bundle/debugging.md index 59fc8e5e2..6ae79b3a0 100644 --- a/docs/bundle/debugging.md +++ b/docs/bundle/debugging.md @@ -1,8 +1,16 @@ +--- +layout: default +parent: "Symfony bundle" +title: Debugging +nav_order: 11 +--- +{% include support.md %} + # Debugging ## Profiler -It may be useful to see what messages were sent during a http request. +It may be useful to see what messages were sent during a http request. The bundle provides a collector for Symfony [profiler](http://symfony.com/doc/current/profiler.html). The extension collects all sent messages @@ -12,8 +20,9 @@ To enable profiler # app/config/config_dev.yml enqueue: - client: - traceable_producer: true + default: + client: + traceable_producer: true ``` Now suppose you have this code in an action: @@ -26,17 +35,17 @@ use Symfony\Component\HttpFoundation\Request; use Enqueue\Client\Message; use Enqueue\Client\ProducerInterface; -class DefaultController extends Controller +class DefaultController extends Controller /** * @Route("/", name="homepage") */ public function indexAction(Request $request) { /** @var ProducerInterface $producer */ - $producer = $this->get('enqueue.producer'); - + $producer = $this->get('enqueue.producer'); + $producer->sendEvent('foo_topic', 'Hello world'); - + $producer->sendEvent('bar_topic', ['bar' => 'val']); $message = new Message(); @@ -49,10 +58,10 @@ class DefaultController extends Controller ``` For this action you may see something like this in the profiler: - + ![Symfony profiler](../images/symfony_profiler.png) - -## Queues and topics available + +## Queues and topics available There are two console commands `./bin/console enqueue:queues` and `./bin/console enqueue:topics`. They are here to help you to learn more about existing topics and queues. @@ -61,11 +70,11 @@ Here's the result: ![Cli debug commands](../images/cli_debug_commands.png) -## Consume command verbosity +## Consume command verbosity -By default the commands `enqueue:consume` or `enqueue:transport:consume` does not output anything. +By default the commands `enqueue:consume` or `enqueue:transport:consume` does not output anything. You can add `-vvv` to see more information. - + ![Consume command verbosity](../images/consume_command_verbosity.png) -[back to index](../index.md) +[back to index](index.md) diff --git a/docs/bundle/functional_testing.md b/docs/bundle/functional_testing.md index 9950d691e..ca475a2e4 100644 --- a/docs/bundle/functional_testing.md +++ b/docs/bundle/functional_testing.md @@ -1,40 +1,48 @@ +--- +layout: default +parent: "Symfony bundle" +title: Functional testing +nav_order: 12 +--- +{% include support.md %} + # Functional testing In this chapter we give some advices on how to test message queue related logic. - + * [NULL transport](#null-transport) * [Traceable message producer](#traceable-message-producer) ## NULL transport -While testing the application you don't usually need to send real message to real broker. -Or even have a dependency on a MQ broker. -Here's the purpose of the NULL transport. -It simple do nothing when you ask it to send a message. -Pretty useful in tests. +While testing the application you don't usually need to send real message to real broker. +Or even have a dependency on a MQ broker. +Here's the purpose of the NULL transport. +It simple do nothing when you ask it to send a message. +Pretty useful in tests. Here's how you can configure it. ```yaml # app/config/config_test.yml enqueue: - transport: - default: 'null' - 'null': ~ - client: ~ + default: + transport: 'null:' + client: ~ ``` ## Traceable message producer -Imagine you have a service that internally sends a message and you have to find out was the message sent or not. -There is a solution for that. You have to enable traceable message producer in test environment. +Imagine you have a service `my_service` with a method `someMethod()` that internally sends a message and you have to find out was the message sent or not. +There is a solution for that. You have to enable traceable message producer in test environment. ```yaml # app/config/config_test.yml enqueue: - client: - traceable_producer: true + default: + client: + traceable_producer: true ``` If you did so, you can use its methods `getTraces`, `getTopicTraces` or `clearTraces`. Here's an example: @@ -48,27 +56,28 @@ class FooTest extends WebTestCase { /** @var \Symfony\Bundle\FrameworkBundle\Client */ private $client; - - public function setUp() + + public function setUp(): void { - $this->client = static::createClient(); + $this->client = static::createClient(); } - + public function testMessageSentToFooTopic() { - $service = $this->client->getContainer()->get('a_service'); - - // the method calls inside $producer->send('fooTopic', 'messageBody'); - $service->do(); - + // Use your own business logic here: + $service = $this->client->getContainer()->get('my_service'); + + // someMethod() is part of your business logic and is calling somewhere $producer->send('fooTopic', 'messageBody'); + $service->someMethod(); + $traces = $this->getProducer()->getTopicTraces('fooTopic'); - + $this->assertCount(1, $traces); $this->assertEquals('messageBody', $traces[0]['message']); } - + /** - * @return TraceableProducer + * @return TraceableProducer */ private function getProducer() { @@ -77,4 +86,4 @@ class FooTest extends WebTestCase } ``` -[back to index](../index.md) \ No newline at end of file +[back to index](index.md) diff --git a/docs/bundle/index.md b/docs/bundle/index.md new file mode 100644 index 000000000..abd08c663 --- /dev/null +++ b/docs/bundle/index.md @@ -0,0 +1,11 @@ +--- +layout: default +title: "Symfony bundle" +nav_order: 6 +has_children: true +permalink: /symfony +--- + +{:toc} + +[back to index](../index.md) diff --git a/docs/bundle/job_queue.md b/docs/bundle/job_queue.md index 67ebd8367..cd13ca3cd 100644 --- a/docs/bundle/job_queue.md +++ b/docs/bundle/job_queue.md @@ -1,3 +1,11 @@ +--- +layout: default +parent: "Symfony bundle" +title: Job queue +nav_order: 8 +--- +{% include support.md %} + # Jobs Use jobs when your message flow has several steps(tasks) which run one after another. @@ -11,9 +19,9 @@ until previous job has finished. ## Installation -The easiest way to install Enqueue's job queues is to by requiring a `enqueue/job-queue-pack` pack. +The easiest way to install Enqueue's job queues is to by requiring a `enqueue/job-queue-pack` pack. It installs installs everything you need. It also configures everything for you If you are on Symfony Flex. - + ```bash $ composer require enqueue/job-queue-pack=^0.8 ``` @@ -36,7 +44,7 @@ class AppKernel extends Kernel new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), new Enqueue\Bundle\EnqueueBundle(), ]; - + return $bundles; } } @@ -48,9 +56,14 @@ class AppKernel extends Kernel # app/config/config.yml enqueue: - # plus basic bundle configuration - - job: true + default: + # plus basic bundle configuration + + job: true + + # adds bundle's default Job entity mapping to application's entity manager. + # set it to false when using your own mapped entities for jobs. + default_mapping: true doctrine: # plus basic bundle configuration @@ -74,34 +87,34 @@ $ bin/console doctrine:schema:update ## Unique job Guarantee that there is only one job with such name running at a time. -For example you have a task that builds a search index. +For example you have a task that builds a search index. It takes quite a lot of time and you don't want another instance of same task working at the same time. -Here's how to do it: +Here's how to do it: * Write a job processor class: ```php -jobRunner = $jobRunner; } - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $data = JSON::decode($message->getBody()); @@ -117,8 +130,8 @@ class SearchReindexProcessor implements PsrProcessor, CommandSubscriberInterface return $result ? self::ACK : self::REJECT; } - - public static function getSubscribedCommand() + + public static function getSubscribedCommand() { return 'search_reindex'; } @@ -133,7 +146,7 @@ services: class: 'App\Queue\SearchReindexProcessor' arguments: ['@Enqueue\JobQueue\JobRunner'] tags: - - { name: 'enqueue.client.processor' } + - { name: 'enqueue.command_subscriber' } ``` * Schedule command @@ -160,11 +173,11 @@ use Enqueue\JobQueue\JobRunner; use Enqueue\JobQueue\Job; use Enqueue\Client\ProducerInterface; use Enqueue\Util\JSON; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Message; +use Interop\Queue\Context; +use Interop\Queue\Processor; -class Step1Processor implements PsrProcessor +class Step1Processor implements Processor { /** * @var JobRunner @@ -176,7 +189,7 @@ class Step1Processor implements PsrProcessor */ private $producer; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $data = JSON::decode($message->getBody()); @@ -207,14 +220,14 @@ class Step1Processor implements PsrProcessor } } -class Step2Processor implements PsrProcessor +class Step2Processor implements Processor { /** * @var JobRunner */ private $jobRunner; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $data = JSON::decode($message->getBody()); @@ -244,11 +257,11 @@ use Enqueue\JobQueue\JobRunner; use Enqueue\JobQueue\Job; use Enqueue\JobQueue\DependentJobService; use Enqueue\Util\JSON; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Message; +use Interop\Queue\Context; +use Interop\Queue\Processor; -class ReindexProcessor implements PsrProcessor +class ReindexProcessor implements Processor { /** * @var JobRunner @@ -260,7 +273,7 @@ class ReindexProcessor implements PsrProcessor */ private $dependentJob; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $data = JSON::decode($message->getBody()); @@ -287,4 +300,4 @@ class ReindexProcessor implements PsrProcessor } ``` -[back to index](../index.md) +[back to index](index.md) diff --git a/docs/bundle/message_processor.md b/docs/bundle/message_processor.md index 674bed2d3..7d10c837d 100644 --- a/docs/bundle/message_processor.md +++ b/docs/bundle/message_processor.md @@ -1,44 +1,42 @@ +--- +layout: default +parent: "Symfony bundle" +title: Message processor +nav_order: 5 +--- +{% include support.md %} + # Message processor +A processor is responsible for processing consumed messages. Message processors and usage examples described in [consumption/message_processor](../consumption/message_processor.md) -Here we just show how to register a message processor service to enqueue. Let's say we have app bundle and a message processor there - -* [Container tag](#container-tag) -* [Topic subscriber](#topic-subscriber) -* [Command subscriber](#command-subscriber) +Here we just show how to register a message processor service to enqueue. -# Container tag +* Transport: -```yaml -# src/AppBundle/Resources/services.yml - -services: - app.async.say_hello_processor: - class: 'AppBundle\Async\SayHelloProcessor' - tags: - - { name: 'enqueue.client.processor', topicName: 'aTopic' } - -``` + * [Register a transport processor](#register-a-transport-processor) -The tag has some additional options: +* Client: -* topicName [Req]: Tells what topic to consume messages from. -* queueName: By default message processor does not require an extra queue on broker side. It reuse a default one. Setting the option you can define a custom queue to be used. -* processorName: By default the service id is used as message processor name. Using the option you can define a custom name. + * [Register a topic subscriber processor](#register-a-topic-subscriber-processor) + * [Register a command subscriber processor](#register-a-command-subscriber-processor) + * [Register a custom processor](#register-a-custom-processor) -# Topic subscriber +## Register a topic subscriber processor -There is a `TopicSubscriberInterface` interface (like [EventSubscriberInterface](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php)). -It is handy to subscribe on event messages. It allows to keep subscription login and process logic closer to each other. +There is a `TopicSubscriberInterface` interface (like [EventSubscriberInterface](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/EventDispatcher/EventSubscriberInterface.php)). +It is handy to subscribe on event messages. +Check interface description for more possible ways to configure it. +It allows to keep subscription and processing logic in one place. ```php ['queueName' => 'fooQueue', 'processorName' => 'foo'], - 'anotherTopic' => ['queueName' => 'barQueue', 'processorName' => 'bar'], - ]; - } -} -``` - -In the container you can just add the tag `enqueue.client.message_processor` and omit any other options: +Tag the service in the container with `enqueue.topic_subscriber` tag: ```yaml -# src/AppBundle/Resources/services.yml +# config/services.yml services: - app.async.say_hello_processor: - class: 'AppBundle\Async\SayHelloProcessor' + App\Queue\SayHelloProcessor: tags: - - { name: 'enqueue.client.processor'} + - { name: 'enqueue.topic_subscriber' } + # registers to no default client + - { name: 'enqueue.topic_subscriber', client: 'foo' } ``` -# Command subscriber +## Register a command subscriber processor -There is a `CommandSubscriberInterface` interface which allows to register a command handlers. -If you send a message using ProducerV2::sendCommand('aCommandName') method it will come to this processor. +There is a `CommandSubscriberInterface` interface. +It is handy to register a command processor. +Check interface description for more possible ways to configure it. +It allows to keep subscription and processing logic in one place. ```php 'fooQueue', 'processorName' => 'aCommandName']; - } -} +services: + App\Queue\SendEmailProcessor: + tags: + - { name: 'enqueue.command_subscriber' } + + # registers to no default client + - { name: 'enqueue.command_subscriber', client: 'foo' } ``` There is a possibility to register a command processor which works exclusively on the queue (no other processors bound to it). -In this case you can send messages without setting any message properties at all. Here's an example of such a processor: +In this case you can send messages without setting any message properties at all. +It might be handy if you want to process messages that are sent by another application. -In the container you can just add the tag `enqueue.client.message_processor` and omit any other options: +Here's a configuration example: ```php 'the-exclusive-command-name', - 'queueName' => 'the-queue-name', - 'queueNameHardcoded' => true, + 'command' => 'aCommand', + 'queue' => 'the-queue-name', + 'prefix_queue' => false, 'exclusive' => true, ]; } } ``` -The same as a topic subscriber you have to tag a processor service (no need to add any options there): +The service has to be tagged with `enqueue.command_subscriber` tag. +# Register a custom processor + +You could register a processor that does not implement neither `CommandSubscriberInterface` not `TopicSubscriberInterface`. +There is a tag `enqueue.processor` for it. You must define either `topic` or `command` tag attribute. +It is possible to define a client you would like to register the processor to. By default, it is registered to default client (first configured or named `default` one ). ```yaml # src/AppBundle/Resources/services.yml services: - app.async.say_hello_processor: - class: 'AppBundle\Async\SayHelloProcessor' + AppBundle\Async\SayHelloProcessor: tags: - - { name: 'enqueue.client.processor'} + # registers as topic processor + - { name: 'enqueue.processor', topic: 'aTopic' } + # registers as command processor + - { name: 'enqueue.processor', command: 'aCommand' } + + # registers to no default client + - { name: 'enqueue.processor', command: 'aCommand', client: 'foo' } +``` + +The tag has some additional options: + +* queue +* prefix_queue +* processor +* exclusive + +You could add your own attributes. They will be accessible through `Route::getOption` later. + +# Register a transport processor + +If you want to use a processor with `enqueue:transport:consume` it should be tagged `enqueue.transport.processor`. +It is possible to define a transport you would like to register the processor to. By default, it is registered to default transport (first configured or named `default` one ). + +```yaml +# config/services.yml + +services: + App\Queue\SayHelloProcessor: + tags: + - { name: 'enqueue.transport.processor', processor: 'say_hello' } + + # registers to no default transport + - { name: 'enqueue.processor', transport: 'foo' } +``` + +The tag has some additional options: + +* processor + +Now you can run a command and tell it to consume from a given queue and process messages with given processor: +```bash +$ ./bin/console enqueue:transport:consume say_hello foo_queue -vvv ``` -[back to index](../index.md) +[back to index](index.md) diff --git a/docs/bundle/message_producer.md b/docs/bundle/message_producer.md index 95712ff1d..a4448bd7e 100644 --- a/docs/bundle/message_producer.md +++ b/docs/bundle/message_producer.md @@ -1,17 +1,25 @@ +--- +layout: default +parent: "Symfony bundle" +title: Message producer +nav_order: 4 +--- +{% include support.md %} + # Message producer -You can choose how to send messages either using a transport directly or with the client. +You can choose how to send messages either using a transport directly or with the client. Transport gives you the access to all transport specific features so you can tune things where the client provides you with easy to use abstraction. - + ## Transport - + ```php get('enqueue.transport.context'); +/** @var Interop\Queue\Context $context */ +$context = $container->get('enqueue.transport.[transport_name].context'); $context->createProducer()->send( $context->createQueue('a_queue'), @@ -21,15 +29,15 @@ $context->createProducer()->send( ## Client -The client is shipped with two types of producers. The first one sends messages immediately +The client is shipped with two types of producers. The first one sends messages immediately where another one (it is called spool producer) collects them in memory and sends them `onTerminate` event (the response is already sent). -The producer has two types on send methods: +The producer has two types on send methods: * `sendEvent` - Message is sent to topic and many consumers can subscribe to it. It is "fire and forget" strategy. The event could be sent to "message bus" to other applications. * `sendCommand` - Message is to ONE exact consumer. It could be used as "fire and forget" or as RPC. The command message is always sent in scope of current application. - -### Send event + +### Send event ```php get(SpoolProducer::class); // message is being sent on console.terminate or kernel.terminate event $spoolProducer->sendEvent('a_topic', 'Hello there!'); -// you could send queued messages manually by calling flush method +// you could send queued messages manually by calling flush method $spoolProducer->flush(); ``` -### Send command +### Send command ```php get(SpoolProducer::class); // message is being sent on console.terminate or kernel.terminate event $spoolProducer->sendCommand('a_processor_name', 'Hello there!'); -// you could send queued messages manually by calling flush method +// you could send queued messages manually by calling flush method $spoolProducer->flush(); ``` -[back to index](../index.md) +[back to index](index.md) diff --git a/docs/bundle/production_settings.md b/docs/bundle/production_settings.md index 4a5ae2850..7f07abed9 100644 --- a/docs/bundle/production_settings.md +++ b/docs/bundle/production_settings.md @@ -1,13 +1,21 @@ +--- +layout: default +parent: "Symfony bundle" +title: Production settings +nav_order: 10 +--- +{% include support.md %} + # Production settings ## Supervisord -As you may read in [quick tour](quick_tour.md) you have to run `enqueue:consume` in order to process messages +As you may read in [quick tour](quick_tour.md) you have to run `enqueue:consume` in order to process messages The php process is not designed to work for a long time. So it has to quit periodically. -Or, the command may exit because of error or exception. +Or, the command may exit because of error or exception. Something has to bring it back and continue message consumption. -We advise you to use [Supervisord](http://supervisord.org/) for that. -It starts processes and keep an eye on them while they are working. +We advise you to use [Supervisord](http://supervisord.org/) for that. +It starts processes and keep an eye on them while they are working. Here an example of supervisord configuration. @@ -27,4 +35,4 @@ redirect_stderr=true _**Note**: Pay attention to `--time-limit` it tells the command to exit after 5 minutes._ -[back to index](../index.md) \ No newline at end of file +[back to index](index.md) diff --git a/docs/bundle/quick_tour.md b/docs/bundle/quick_tour.md index 9380100c2..790c570cb 100644 --- a/docs/bundle/quick_tour.md +++ b/docs/bundle/quick_tour.md @@ -1,3 +1,11 @@ +--- +layout: default +parent: "Symfony bundle" +title: Quick tour +nav_order: 1 +--- +{% include support.md %} + # EnqueueBundle. Quick tour. The [EnqueueBundle](https://github.com/php-enqueue/enqueue-bundle) integrates enqueue library. @@ -6,10 +14,10 @@ It adds easy to use [configuration layer](config_reference.md), register service ## Install ```bash -$ composer require enqueue/enqueue-bundle enqueue/amqp-ext # or enqueue/amqp-bunny, enqueue/amqp-lib +$ composer require enqueue/enqueue-bundle enqueue/fs ``` -_**Note**: You could use not only AMQP transport but any other [available](https://github.com/php-enqueue/enqueue-dev/tree/master/docs/transport)._ +_**Note**: You could various other [transports](https://github.com/php-enqueue/enqueue-dev/tree/master/docs/transport)._ _**Note**: If you are looking for a way to migrate from `php-amqplib/rabbitmq-bundle` read this [article](https://blog.forma-pro.com/the-how-and-why-of-the-migration-from-rabbitmqbundle-to-enqueuebundle-6c4054135e2b)._ @@ -19,11 +27,12 @@ Then, enable the bundle by adding `new Enqueue\Bundle\EnqueueBundle()` to the bu ```php get(ProducerInterface::class); +// If you want a different producer than default (for example the other specified in sample above) then use +// $producer = $container->get('enqueue.client.some_other_transport.producer'); // send event to many consumers $producer->sendEvent('aFooTopic', 'Something has happened'); +// You can also pass an instance of Enqueue\Client\Message as second argument if you need more flexibility. +$properties = []; +$headers = []; +$message = new Message('Message body', $properties, $headers); +$producer->sendEvent('aBarTopic', $message); // send command to ONE consumer $producer->sendCommand('aProcessorName', 'Something has happened'); ``` -To consume messages you have to first create a message processor: +To consume messages you have to first create a message processor. + +Example below shows how to create a Processor that will receive messages from `aFooTopic` topic (and only that one). +It assumes that you're using default Symfony services configuration and this class is +[autoconfigured](https://symfony.com/doc/current/service_container.html#the-autoconfigure-option). Otherwise you'll +have to tag it manually. This is especially true if you're using multiple transports: if left autoconfigured, processor +will be attached to the default transport only. + +Note: Topic in enqueue and topic on some transports (for example Kafka) are two different things. ```php getBody(); @@ -97,13 +134,16 @@ class FooProcessor implements PsrProcessor, TopicSubscriberInterface } ``` -Register it as a container service and subscribe to the topic: +Register it as a container service. Subscribe it to the topic if you are not using autowiring. ```yaml foo_message_processor: class: 'FooProcessor' tags: - - { name: 'enqueue.client.processor' } + - { name: 'enqueue.topic_subscriber' } + # Use the variant below to attach to a specific client + # Also note that if you don't disable autoconfigure, above tag will be applied automatically for default client + # - { name: 'enqueue.topic_subsciber', client: 'some_other_transport' } ``` Now you can start consuming messages: @@ -112,7 +152,14 @@ Now you can start consuming messages: $ ./bin/console enqueue:consume --setup-broker -vvv ``` +You can select a specific client for consumption: + +```bash +$ ./bin/console enqueue:consume --setup-broker --client="some_other_transport" -vvv +``` + + _**Note**: Add -vvv to find out what is going while you are consuming messages. There is a lot of valuable debug info there._ -[back to index](../index.md) +[back to index](index.md) diff --git a/docs/client/extensions.md b/docs/client/extensions.md index 0e121b5c3..d70921cc1 100644 --- a/docs/client/extensions.md +++ b/docs/client/extensions.md @@ -1,7 +1,15 @@ +--- +layout: default +parent: Client +title: Extensions +nav_order: 6 +--- +{% include support.md %} + # Client extensions. There is an ability to hook into sending process. You have to create an extension class that implements `Enqueue\Client\ExtensionInterface` interface. -For example, `TimestampMessageExtension` extension adds timestamps every message before sending it to MQ. +For example, `TimestampMessageExtension` extension adds timestamps every message before sending it to MQ. ```php setTimestamp(time()); } } - + public function onPostSend($topic, Message $message) { - + } -} +} ``` ## Symfony -To use the extension in Symfony, you have to register it as a container service with a special tag. +To use the extension in Symfony, you have to register it as a container service with a special tag. ```yaml # config/services.yaml @@ -37,9 +45,9 @@ services: timestamp_message_extension: class: Acme\TimestampMessageExtension tags: - - { name: 'enqueue.client.extensions' } + - { name: 'enqueue.client.extension' } ``` -You can add `priority` attribute with a number. The higher value you set the earlier the extension is called. +You can add `priority` attribute with a number. The higher value you set the earlier the extension is called. [back to index](../index.md) diff --git a/docs/client/index.md b/docs/client/index.md new file mode 100644 index 000000000..aa222138f --- /dev/null +++ b/docs/client/index.md @@ -0,0 +1,9 @@ +--- +layout: default +title: Client +nav_order: 4 +has_children: true +permalink: /client +--- + +{:toc} diff --git a/docs/client/message_bus.md b/docs/client/message_bus.md index 24d29ccd0..0a5a3aa1c 100644 --- a/docs/client/message_bus.md +++ b/docs/client/message_bus.md @@ -1,16 +1,24 @@ +--- +layout: default +parent: Client +title: Message bus +nav_order: 4 +--- +{% include support.md %} + # Client. Message bus - + Here's a description of message bus from [Enterprise Integration Patterns](http://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageBus.html) > A Message Bus is a combination of a common data model, a common command set, and a messaging infrastructure to allow different systems to communicate through a shared set of interfaces. -If all your applications built on top of Enqueue Client you have to only make sure they send message to a shared topic. +If all your applications built on top of Enqueue Client you have to only make sure they send message to a shared topic. The rest is done under the hood. If you'd like to connect another application (written on Python for example ) you have to follow these rules: -* An application defines its own queue that is connected to the topic as fanout. -* A message sent to message bus topic must have a header `enqueue.topic_name`. +* An application defines its own queue that is connected to the topic as fanout. +* A message sent to message bus topic must have a header `enqueue.topic_name`. * Once a message is received it could be routed internally. `enqueue.topic_name` header could be used for that. [back to index](../index.md) diff --git a/docs/client/message_examples.md b/docs/client/message_examples.md index dfee533af..f43ff6d46 100644 --- a/docs/client/message_examples.md +++ b/docs/client/message_examples.md @@ -1,3 +1,11 @@ +--- +layout: default +parent: Client +title: Message examples +nav_order: 2 +--- +{% include support.md %} + # Client. Message examples * [Scope](#scope) @@ -6,11 +14,11 @@ * [Priority](#priority) * [Timestamp, Content type, Message id](#timestamp-content-type-message-id) -## Scope +## Scope -There are two two types possible scopes: `Message:SCOPE_MESSAGE_BUS` and `Message::SCOPE_APP`. +There are two types possible scopes: `Message:SCOPE_MESSAGE_BUS` and `Message::SCOPE_APP`. The first one instructs the client send messages (if driver supports) to the message bus so other apps can consume those messages. -The second in turns limits the message to the application that sent it. No other apps could receive it. +The second in turns limits the message to the application that sent it. No other apps could receive it. ```php setScope(Message::SCOPE_MESSAGE_BUS); /** @var \Enqueue\Client\ProducerInterface $producer */ $producer->sendEvent('aTopic', $message); ``` - -## Delay + +## Delay Message sent with a delay set is processed after the delay time exceed. -Some brokers may not support it from scratch. +Some brokers may not support it from scratch. In order to use delay feature with [RabbitMQ](https://www.rabbitmq.com/) you have to install a [delay plugin](https://github.com/rabbitmq/rabbitmq-delayed-message-exchange). ```php @@ -44,7 +52,7 @@ $producer->sendEvent('aTopic', $message); ## Expiration (TTL) -The message may have an expiration or TTL (time to live). +The message may have an expiration or TTL (time to live). The message is removed from the queue if the expiration exceeded but the message has not been consumed. For example it make sense to send a forgot password email within first few minutes, nobody needs it in an hour. @@ -60,11 +68,16 @@ $message->setExpire(60); // seconds $producer->sendEvent('aTopic', $message); ``` -## Priority +## Priority You can set a priority If you want a message to be processed quicker than other messages in the queue. -Client defines five priority constants: `MessagePriority::VERY_LOW`, `MessagePriority::LOW`, `MessagePriority::NORMAL`, `MessagePriority::HIGH`, `MessagePriority::VERY_HIGH`. -The `MessagePriority::NORMAL` is default priority. +Client defines five priority constants: + +* `MessagePriority::VERY_LOW` +* `MessagePriority::LOW` +* `MessagePriority::NORMAL` (**default**) +* `MessagePriority::HIGH` +* `MessagePriority::VERY_HIGH` ```php sendEvent('aTopic', $message); ## Timestamp, Content type, Message id -Those are self describing things. -Usually they are set by Client so you don't have to worry about them. +Those are self describing things. +Usually they are set by Client so you don't have to worry about them. If you do not like what Client set you can always set custom values: - + ```php sendEvent('user_activated', new class() implements \JsonSerializable { ``` Send command examples: - + ```php getBody(); ```php bind('a_bar_topic', 'a_processor_name', function(PsrMessage $psrMessage) { +$client->bindTopic('a_bar_topic', function(Message $psrMessage) { // processing logic here - - return PsrProcessor::ACK; + + return Processor::ACK; }); $client->consume(); @@ -106,25 +114,23 @@ $client->consume(); // bin/enqueue.php -use Enqueue\Symfony\Client\ConsumeMessagesCommand; -use Enqueue\Symfony\Client\Meta\QueuesCommand; -use Enqueue\Symfony\Client\Meta\TopicsCommand; -use Enqueue\Symfony\Client\ProduceMessageCommand; +use Enqueue\Symfony\Client\SimpleConsumeCommand; +use Enqueue\Symfony\Client\SimpleProduceCommand; +use Enqueue\Symfony\Client\SimpleRoutesCommand; +use Enqueue\Symfony\Client\SimpleSetupBrokerCommand; use Enqueue\Symfony\Client\SetupBrokerCommand; use Symfony\Component\Console\Application; /** @var \Enqueue\SimpleClient\SimpleClient $client */ $application = new Application(); -$application->add(new SetupBrokerCommand($client->getDriver())); -$application->add(new ProduceMessageCommand($client->getProducer())); -$application->add(new QueuesCommand($client->getQueueMetaRegistry())); -$application->add(new TopicsCommand($client->getTopicMetaRegistry())); -$application->add(new ConsumeMessagesCommand( +$application->add(new SimpleSetupBrokerCommand($client->getDriver())); +$application->add(new SimpleRoutesCommand($client->getDriver())); +$application->add(new SimpleProduceCommand($client->getProducer())); +$application->add(new SimpleConsumeCommand( $client->getQueueConsumer(), - $client->getDelegateProcessor(), - $client->getQueueMetaRegistry(), - $client->getDriver() + $client->getDriver(), + $client->getDelegateProcessor() )); $application->run(); @@ -133,13 +139,13 @@ $application->run(); and run to see what is there: ```bash -$ php bin/enqueue.php +$ php bin/enqueue.php ``` or consume messages ```bash -$ php bin/enqueue.php enqueue:consume -vvv --setup-broker +$ php bin/enqueue.php enqueue:consume -vvv --setup-broker ``` [back to index](../index.md) diff --git a/docs/client/rpc_call.md b/docs/client/rpc_call.md index 4afb1e722..cfdd6ce46 100644 --- a/docs/client/rpc_call.md +++ b/docs/client/rpc_call.md @@ -1,35 +1,42 @@ +--- +layout: default +parent: Client +title: RPC call +nav_order: 5 +--- +{% include support.md %} + # Client. RPC call -The client's [quick tour](quick_tour.md) describes how to get the client object. +The client's [quick tour](quick_tour.md) describes how to get the client object. Here we'll show you how to use Enqueue Client to perform a [RPC call](https://en.wikipedia.org/wiki/Remote_procedure_call). You can do it by defining a command which returns something. ## The consumer side On the consumer side we have to register a command processor which computes the result and send it back to the sender. -Pay attention that you have to add reply extension. It wont work without it. +Pay attention that you have to add reply extension. It won't work without it. -Of course it is possible to implement rpc server side based on transport classes only. That would require a bit more work to do. +Of course it is possible to implement rpc server side based on transport classes only. That would require a bit more work to do. ```php bind(Config::COMMAND_TOPIC, 'square', function (PsrMessage $message, PsrContext $context) use (&$requestMessage) { +$client->bindCommand('square', function (Message $message, Context $context) use (&$requestMessage) { $number = (int) $message->getBody(); - + return Result::reply($context->createMessage($number ^ 2)); }); @@ -40,8 +47,8 @@ $client->consume(new ChainExtension([new ReplyExtension()])); ## The sender side -On the sender's side we need a client which send a command and wait for reply messages. - +On the sender's side we need a client which send a command and wait for reply messages. + ```php sendCommand('square', 5, true)->receive(5000 /* 5 sec */)->getBody ``` You can perform several requests asynchronously with `sendCommand` and ask for replays later. - + ```php $promise) { if ($replyMessage = $promise->receiveNoWait()) { $replyMessages[$index] = $replyMessage; - + unset($promises[$index]); } } } -``` \ No newline at end of file +``` diff --git a/docs/client/supported_brokers.md b/docs/client/supported_brokers.md index f734b660c..076e3b013 100644 --- a/docs/client/supported_brokers.md +++ b/docs/client/supported_brokers.md @@ -1,35 +1,51 @@ -# Client. Supported brokers +--- +layout: default +parent: Client +title: Supported brokers +nav_order: 3 +--- +{% include support.md %} + +# Client Supported brokers Here's the list of transports supported by Enqueue Client: -| Transport | Package | DSN | -|:-------------------:|:----------------------------------------------------------:|:-------------------------------:| -| AMQP, RabbitMQ | [enqueue/amqp-bunny](../transport/amqp_bunny.md) | amqp: amqp+bunny: | -| AMQP, RabbitMQ | [enqueue/amqp-lib](../transport/amqp_lib.md) | amqp: amqp+lib: | -| AMQP, RabbitMQ | [enqueue/amqp-ext](../transport/amqp.md) | amqp: amqp+ext: | -| Doctrine DBAL | [enqueue/dbal](../transport/dbal.md) | mysql: pgsql: pdo_pgsql etc | -| Filesystem | [enqueue/fs](../transport/fs.md) | file:///foo/bar | -| Google PubSub | [enqueue/gps](../transport/gps.md) | gps: | -| Redis | [enqueue/gps](../transport/redis.md) | redis: | -| Amazon SQS | [enqueue/sqs](../transport/sqs.md) | sqs: | -| STOMP, RabbitMQ | [enqueue/stomp](../transport/stomp.md) | stomp: | -| Kafka | [enqueue/stomp](../transport/kafka.md) | kafka: | -| Null | [enqueue/null](../transport/null.md) | null: | +| Transport | Package | DSN | +|:---------------------:|:----------------------------------------------------------:|:-------------------------------:| +| AMQP, RabbitMQ | [enqueue/amqp-ext](../transport/amqp.md) | amqp: amqp+ext: | +| AMQP, RabbitMQ | [enqueue/amqp-bunny](../transport/amqp_bunny.md) | amqp: amqp+bunny: | +| AMQP, RabbitMQ | [enqueue/amqp-lib](../transport/amqp_lib.md) | amqp: amqp+lib: amqp+rabbitmq: | +| Doctrine DBAL | [enqueue/dbal](../transport/dbal.md) | mysql: pgsql: pdo_pgsql etc | +| Filesystem | [enqueue/fs](../transport/fs.md) | file:///foo/bar | +| Gearman | [enqueue/gearman](../transport/gearman.md) | gearman: | +| GPS, Google PubSub | [enqueue/gps](../transport/gps.md) | gps: | +| Kafka | [enqueue/rdkafka](../transport/kafka.md) | kafka: | +| MongoDB | [enqueue/mongodb](../transport/mongodb.md) | mongodb: | +| Null | [enqueue/null](../transport/null.md) | null: | +| Pheanstalk, Beanstalk | [enqueue/pheanstalk](../transport/pheanstalk.md) | beanstalk: | +| Redis | [enqueue/redis](../transport/redis.md) | redis: | +| Amazon SQS | [enqueue/sqs](../transport/sqs.md) | sqs: | +| STOMP, RabbitMQ | [enqueue/stomp](../transport/stomp.md) | stomp: | +| WAMP | [enqueue/wamp](../transport/wamp.md) | wamp: | -Here's the list of protocols and Client features supported by them +## Transport Features -| Protocol | Priority | Delay | Expiration | Setup broker | Message bus | -|:--------------:|:--------:|:--------:|:----------:|:------------:|:-----------:| -| AMQP | No | No | Yes | Yes | Yes | -| RabbitMQ AMQP | Yes | Yes | Yes | Yes | Yes | -| STOMP | No | No | Yes | No | Yes** | -| RabbitMQ STOMP | Yes | Yes | Yes | Yes*** | Yes** | -| Filesystem | No | No | No | Yes | No | -| Redis | No | No | No | Not needed | No | -| Doctrine DBAL | Yes | Yes | No | Yes | No | -| Amazon SQS | No | Yes | No | Yes | Not impl | -| Kafka | No | No | No | Yes | No | -| Google PubSub | Not impl | Not impl | Not impl | Yes | Not impl | +| Protocol | Priority | Delay | Expiration | Setup broker | Message bus | Heartbeat | +|:--------------:|:--------:|:--------:|:----------:|:------------:|:-----------:|:---------:| +| AMQP | No | No | Yes | Yes | Yes | No | +| RabbitMQ AMQP | Yes | Yes | Yes | Yes | Yes | Yes | +| Doctrine DBAL | Yes | Yes | No | Yes | No | No | +| Filesystem | No | No | Yes | Yes | No | No | +| Gearman | No | No | No | No | No | No | +| Google PubSub | Not impl | Not impl | Not impl | Yes | Not impl | No | +| Kafka | No | No | No | Yes | No | No | +| MongoDB | Yes | Yes | Yes | Yes | No | No | +| Pheanstalk | Yes | Yes | Yes | No | No | No | +| Redis | No | Yes | Yes | Not needed | No | No | +| Amazon SQS | No | Yes | No | Yes | Not impl | No | +| STOMP | No | No | Yes | No | Yes** | No | +| RabbitMQ STOMP | Yes | Yes | Yes | Yes*** | Yes** | Yes | +| WAMP | No | No | No | No | No | No | * \*\* Possible if topics (exchanges) are configured on broker side manually. * \*\*\* Possible if RabbitMQ Management Plugin is installed. diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 000000000..5ea14d244 --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,88 @@ +--- +layout: default +title: Key concepts +nav_order: 1 +--- + +# Key concepts + +If you are new to queuing system, there are some key concepts to understand to make the most of this lib. + +The library consist of several components. The components could be used independently or as integral part. + +## Components + +### Transport + +The transport is the underlying vendor-specific library that provides the queuing features: a way for programs to create, send, read messages. +Based on [queue interop](https://github.com/queue-interop/queue-interop) interfaces. Use transport directly if you need full control or access to vendor specific features. + +The most famous transports are [RabbitMQ](transport/amqp_lib.md), [Amazon SQS](transport/sqs.md), [Redis](transport/redis.md), [Filesystem](transport/filesystem.md). + +- *connection factory* creates a connection to the vendor service with vendor-specific config. +- *context* provides the Producer, the Consumer and helps create Messages. It is the most commonly used object and an implementation of [abstract factory](https://en.wikipedia.org/wiki/Abstract_factory_pattern) pattern. +- *destination* is a concept of a destination to which messages can be sent. Choose queue or topic. Destination represents broker state so expect to see same names at broker side. +- *queue* is a named destination to which messages can be sent to. Messages accumulate on queues until they are retrieved by programs (called consumers) that service those queues. +- *topic* implements [publish and subscribe](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) semantics. When you publish a message it goes to all the subscribers that are interested - so zero to many subscribers will receive a copy of the message. Some brokers do not support Pub\Sub. +- *message* describes data sent to (or received from) a destination. It has a body, headers and properties. +- *producer* sends a message to the destination. The producer implements vendor-specific logic and is in charge of converting messages between Enqueue and vendor-specific message format. +- *consumer* fetches a message from a destination. The consumer implements vendor-specific logic and is in charge of converting messages between vendor-specific message format and Enqueue. +- *subscription consumer* provides a way to consume messages from several destinations simultaneously. Some brokers do not support this feature. +- *processor* is an optional concept useful for sharing message processing logic. Vendor independent. Implements your business logic. + +Additional terms we might refer to: +- *receive and delete delivery*: the queue deletes the message when it's fetched by consumer. If processing fails, then the message is lost and won't be processed again. This is called _at most once_ processing. +- *peek and lock delivery*: the queue locks for a short amount of time a message when it's fetched by consumer, making it invisible to other consumers, in order to prevent duplicate processing and message lost. If there is no acknowledgment before the lock times out, failure is assumed and then the message is made visible again in the queue for another try. This is called _at least once_ processing. +- *an explicit acknowledgement*: the queue locks a message when it's fetched by consumer, making it invisible to other consumers, in order to prevent duplicate processing and message lost. If there is no explicit acknowledgment received before the connection is closed, failure is assumed and then the message is made visible again in the queue for another try. This is called _at least once_ processing. +- *message delivery delay*: messages are sent to the queue but won't be visible right away to consumers to fetch them. You may need it to plan an action at a specific time. +- *message expiration*: messages could be dropped of a queue within some period of time without processing. You may need it to not process stale messages. Some transports do not support the feature. +- *message priority*: message could be sent with higher priority, therefor being consumed faster. It violates first in first out concept and should be used with precautions. Some transports do not support the feature. +- *first in first out*: messages are processed in the same order than they have entered the queue. + +Lifecycle + +A queuing system is divided in two main parts: producing and consuming. +The [transport section of the Quick Start](quick_tour.md#transport) shows some code example for both parts. + +Producing part +1. The application creates a Context with a Connection factory +2. The Context helps the application to create a Message +3. The application gets a Producer from the Context +4. The application uses the Producer to send the Message to the queue + +Consuming part +1. The application gets a Consumer from the Context +2. The Consumer receives Messages from the queue +3. The Consumer uses a Processor to process a Message +4. The Processor returns a status (like `Interop\Queue\Processor::ACK`) to the Consumer +5. The Consumer requeues or removes the Message from the queue depending on the Processor returned status + +### Consumption + +The consumption component is based on top of transport. +The most important class is [QueueConsumer](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Consumption/QueueConsumer.php). +Could be used with any queue interop compatible transport. +It provides extension points which could be ad-hoc into processing flow. You can register [existing extensions](consumption/extensions.md) or write a custom one. + +### Client + +Enqueue Client is designed for as simple as possible developer experience. +It provides high-level, very opinionated API. +It manages all transport differences internally and even emulate missing features (like publish-subscribe). +Please note: Client has own logic for naming transport destinations. Expect a different transport queue\topic name from the Client topic, command name. The prefix behavior could be disabled. + +- *Topic:* Send a message to the topic when you want to notify several subscribers that something has happened. There is no way to get subscriber results. Uses the router internally to deliver messages. +- *Command:* guarantees that there is exactly one command processor\subscriber. Optionally, you can get a result. If there is no command subscriber an exception is thrown. +- *Router:* copy a message sent to the topic and duplicate it for every subscriber and send. +- *Driver* contains vendor specific logic. +- *Producer* is responsible for sending messages to the topic or command. It has nothing to do with transport's producer. +- *Message* contains data to be sent. Please note that on consumer side you have to deal with transport message. +- *Consumption:* rely on consumption component. + +## How to use Enqueue? + +There are different ways to use Enqueue: both reduce the boiler plate code you have to write to start using the Enqueue feature. +- as a [Client](client/quick_tour.md): relies on a [DSN](client/supported_brokers.md) to connect +- as a [Symfony Bundle](bundle/index.md): recommended if you are using the Symfony framework + +[back to index](index.md) diff --git a/docs/consumption/extensions.md b/docs/consumption/extensions.md index 31a47e29e..8afd61e8b 100644 --- a/docs/consumption/extensions.md +++ b/docs/consumption/extensions.md @@ -1,13 +1,20 @@ +--- +layout: default +parent: Consumption +title: Extensions +--- +{% include support.md %} + # Consumption extensions. You can learn how to register extensions in [quick tour](../quick_tour.md#consumption). There's dedicated [chapter](../bundle/consumption_extension.md) for how to add extension in Symfony app. -## [LoggerExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Consumption/Extension/LoggerExtension.php) +## [LoggerExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Consumption/Extension/LoggerExtension.php) It sets logger to queue consumer context. All log messages will go to it. -## [DoctrineClearIdentityMapExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue-bundle/Consumption/Extension/DoctrineClearIdentityMapExtension.php) +## [DoctrineClearIdentityMapExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue-bundle/Consumption/Extension/DoctrineClearIdentityMapExtension.php) It clears Doctrine's identity map after a message is processed. It reduce memory usage. @@ -15,14 +22,24 @@ It clears Doctrine's identity map after a message is processed. It reduce memory It test a database connection and if it is lost it does reconnect. Fixes "MySQL has gone away" errors. +## [DoctrineClosedEntityManagerExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue-bundle/Consumption/Extension/DoctrineClosedEntityManagerExtension.php) + +The extension interrupts consumption if an entity manager has been closed. + +## [ResetServicesExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue-bundle/Consumption/Extension/ResetServicesExtension.php) + +It resets all services with tag "kernel.reset". +For example, this includes all monolog loggers if installed and will flush/clean all buffers, +reset internal state, and get them back to a state in which they can receive log records again. + ## [ReplyExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Consumption/Extension/ReplyExtension.php) -It comes with RPC code and simplifies reply logic. +It comes with RPC code and simplifies reply logic. It takes care of sending a reply message to reply queue. ## [SetupBrokerExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Client/ConsumptionExtension/SetupBrokerExtension.php) -It responsible for configuring everything at a broker side. queues, topics, bindings and so on. +It responsible for configuring everything at a broker side. queues, topics, bindings and so on. The extension is added at runtime when `--setup-broker` option is used. ## [LimitConsumedMessagesExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Consumption/Extension/LimitConsumedMessagesExtension.php) @@ -33,8 +50,8 @@ The extension is added at runtime when `--message-limit=10` option is used. ## [LimitConsumerMemoryExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Consumption/Extension/LimitConsumerMemoryExtension.php) The extension interrupts consumption once a memory limit is reached. -The extension is added at runtime when `--memory-limit=512` option is used. -The value is Mb. +The extension is added at runtime when `--memory-limit=512` option is used. +The value is Mb. ## [LimitConsumptionTimeExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Consumption/Extension/LimitConsumptionTimeExtension.php) @@ -44,10 +61,14 @@ The extension is added at runtime when `--time-limit="now + 2 minutes"` option i ## [SignalExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Consumption/Extension/SignalExtension.php) The extension catch process signals and gracefully stops consumption. Works only on NIX platforms. - + ## [DelayRedeliveredMessageExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/pkg/enqueue/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php) -The extension checks whether the received message is redelivered (There was attempt to process message but it failed). -If so the extension reject the origin message and creates a copy message with a delay. +The extension checks whether the received message is redelivered (There was attempt to process message but it failed). +If so the extension reject the origin message and creates a copy message with a delay. + +## [ConsumerMonitoringExtension](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/monitoring.md#consumption-extension) + +There is an extension ConsumerMonitoringExtension for Enqueue QueueConsumer. It could collect consumed messages and consumer stats for you and send them to Grafana, InfluxDB or Datadog. [back to index](../index.md) diff --git a/docs/consumption/index.md b/docs/consumption/index.md new file mode 100644 index 000000000..4ecf99d9c --- /dev/null +++ b/docs/consumption/index.md @@ -0,0 +1,9 @@ +--- +layout: default +title: Consumption +nav_order: 3 +has_children: true +permalink: /consumption +--- + +{:toc} diff --git a/docs/consumption/message_processor.md b/docs/consumption/message_processor.md index 9492a4c8d..07d01cdae 100644 --- a/docs/consumption/message_processor.md +++ b/docs/consumption/message_processor.md @@ -1,3 +1,10 @@ +--- +layout: default +parent: Consumption +title: Message processors +--- +{% include support.md %} + # Message processor * [Basics](#basics) @@ -13,133 +20,133 @@ Here's example: ```php mailer->send('foo@example.com', $message->getBody()); - + return self::ACK; } } ``` -By returning `self::ACK` a processor tells a broker that the message has been processed correctly. +By returning `self::ACK` a processor tells a broker that the message has been processed correctly. There are other statuses: * `self::ACK` - Use this constant when the message is processed successfully and the message could be removed from the queue. * `self::REJECT` - Use this constant when the message is not valid or could not be processed. The message is removed from the queue. -* `self::REQUEUE` - Use this constant when the message is not valid or could not be processed right now but we can try again later +* `self::REQUEUE` - Use this constant when the message is not valid or could not be processed right now but we can try again later -Look at the next example that shows the message validation before sending a mail. If the message is not valid a processor rejects it. +Look at the next example that shows the message validation before sending a mail. If the message is not valid a processor rejects it. ```php getBody()); if ($user = $this->userRepository->find($data['userId'])) { return self::REJECT; } - + $this->mailer->send($user->getEmail(), $data['text']); - + return self::ACK; } } ``` -It is possible to find out whether the message failed previously or not. -There is `isRedelivered` method for that. -If it returns true than there was attempt to process message. - +It is possible to find out whether the message failed previously or not. +There is `isRedelivered` method for that. +If it returns true than there was attempt to process message. + ```php isRedelivered()) { return self::REQUEUE; } - + $this->mailer->send('foo@example.com', $message->getBody()); - + return self::ACK; } } ``` The second argument is your context. You can use it to send messages to other queues\topics. - + ```php mailer->send('foo@example.com', $message->getBody()); - + $queue = $context->createQueue('anotherQueue'); $message = $context->createMessage('Message has been sent'); $context->createProducer()->send($queue, $message); - + return self::ACK; } } ``` -## Reply result +## Reply result The consumption component provide some useful extensions, for example there is an extension that makes RPC processing simpler. The producer might wait for a reply from a consumer and in order to send it a processor has to return a reply result. Don't forget to add `ReplyExtension`. - + ```php mailer->send('foo@example.com', $message->getBody()); - + $replyMessage = $context->createMessage('Message has been sent'); - + return Result::reply($replyMessage); } } -/** @var \Interop\Queue\PsrContext $psrContext */ +/** @var \Interop\Queue\Context $context */ -$queueConsumer = new QueueConsumer($psrContext, new ChainExtension([ +$queueConsumer = new QueueConsumer($context, new ChainExtension([ new ReplyExtension() ])); @@ -151,14 +158,14 @@ $queueConsumer->consume(); ## On exceptions -It is advised to not catch exceptions and [fail fast](https://en.wikipedia.org/wiki/Fail-fast). -Also consider using [supervisord](supervisord.org) or similar process manager to restart exited consumers. +It is advised to not catch exceptions and [fail fast](https://en.wikipedia.org/wiki/Fail-fast). +Also consider using [supervisord](supervisord.org) or similar process manager to restart exited consumers. Despite advising to fail there are some cases where you might want to catch exceptions. * A message validator throws an exception on invalid message. It is better to catch it and return `REJECT`. -* Some transports ([Doctrine DBAL](../transport/dbal.md), [Filesystem](../transport/filesystem.md), [Redis](../transport/redis.md)) does notice an error, -and therefor won't be able to redeliver the message. The message is completely lost. You might want to catch an exception to properly redelivery\requeue the message. +* Some transports ([Doctrine DBAL](../transport/dbal.md), [Filesystem](../transport/filesystem.md), [Redis](../transport/redis.md)) does notice an error, +and therefor won't be able to redeliver the message. The message is completely lost. You might want to catch an exception to properly redelivery\requeue the message. # Examples diff --git a/docs/contribution.md b/docs/contribution.md index 39fc89a75..68d051fc5 100644 --- a/docs/contribution.md +++ b/docs/contribution.md @@ -1,7 +1,15 @@ +--- +layout: default +title: Contribution +nav_order: 99 +--- + +{% include support.md %} + # Contribution -To contribute you have to send a pull request to [enqueue-dev](https://github.com/php-enqueue/enqueue-dev) repository. -The pull requests to read only subtree split [repositories](https://github.com/php-enqueue/enqueue-dev/blob/master/bin/subtree-split#L46) will be closed. +To contribute you have to send a pull request to [enqueue-dev](https://github.com/php-enqueue/enqueue-dev) repository. +The pull requests to read only subtree split [repositories](https://github.com/php-enqueue/enqueue-dev/blob/master/bin/subtree-split#L46) will be closed. ## Setup environment @@ -13,24 +21,34 @@ composer install Once you did it you can work on a feature or bug fix. +If you need, you can also use composer scripts to run code linting and static analysis: +* For code style linting, run `composer run cs-lint`. Optionally add file names: +`composer run cs-lint pkg/null/NullTopic.php` for example. +* You can also fix your code style with `composer run cs-fix`. +* Static code analysis can be run using `composer run phpstan`. As above, you can pass specific files. + ## Testing To run tests ``` -./bin/dev -t +./bin/test.sh ``` or for a package only: ``` -./bin/dev -t pkg/enqueue +./bin/test.sh pkg/enqueue ``` -## Commit +## Commit When you try to commit changes `php-cs-fixer` is run. It fixes all coding style issues. Don't forget to stage them and commit everything. -Once everything is done open a pull request on official repository. +Once everything is done open a pull request on official repository. + +## WTF?! + +* If you get `rabbitmqssl: forward host lookup failed: Unknown host, wait for service rabbitmqssl:5671` do `docker compose down`. [back to index](index.md) diff --git a/docs/cookbook/symfony/how-to-change-consume-command-logger.md b/docs/cookbook/symfony/how-to-change-consume-command-logger.md index c5cd95ac2..c2c281753 100644 --- a/docs/cookbook/symfony/how-to-change-consume-command-logger.md +++ b/docs/cookbook/symfony/how-to-change-consume-command-logger.md @@ -1,11 +1,17 @@ +--- +layout: default +nav_exclude: true +--- +{% include support.md %} + # How to change consume command logger By default `bin/console enqueue:consume` (or `bin/console enqueue:transport:consume`) command prints messages to output. The amount of info could be controlled by verbosity option (-v, -vv, -vvv). In order to change the default logger used by a command you have to register a `LoggerExtension` just before the default one. -The extension asks you for a logger service, so just pass the one you want to use. -Here's how you can do it. +The extension asks you for a logger service, so just pass the one you want to use. +Here's how you can do it. ```yaml // config/services.yaml @@ -22,7 +28,7 @@ services: The logger extension with the highest priority will set its logger. -[back to index](../../index.md) +[back to index](../../index.md) diff --git a/docs/dsn.md b/docs/dsn.md new file mode 100644 index 000000000..bd6cf2c0c --- /dev/null +++ b/docs/dsn.md @@ -0,0 +1,123 @@ +--- +layout: default +title: DSN Parser +nav_order: 92 +--- +{% include support.md %} + +## DSN Parser. + +The [enqueue/dsn](https://github.com/php-enqueue/dsn) tool helps to parse DSN\URI string. +The tool is used by Enqueue transports to parse DSNs. + +## Installation + +```bash +composer req enqueue/dsn 0.9.x +``` + +### Examples + +Basic usage: + +```php +getSchemeProtocol(); // 'mysql' +$dsn->getScheme(); // 'mysql+pdo' +$dsn->getSchemeExtensions(); // ['pdo'] +$dsn->getUser(); // 'user' +$dsn->getPassword(); // 'password' +$dsn->getHost(); // 'localhost' +$dsn->getPort(); // 3306 + +$dsn->getQueryString(); // 'connection_timeout=123' +$dsn->getQuery(); // ['connection_timeout' => '123'] +$dsn->getString('connection_timeout'); // '123' +$dsn->getDecimal('connection_timeout'); // 123 +``` + +Parse Cluster DSN: + +```php +getUser(); // 'user' +$dsns[0]->getPassword(); // 'password' +$dsns[0]->getHost(); // 'foo' +$dsns[0]->getPort(); // 3306 + +$dsns[1]->getUser(); // 'user' +$dsns[1]->getPassword(); // 'password' +$dsns[1]->getHost(); // 'bar' +$dsns[1]->getPort(); // 5678 +``` + +Some parts could be omitted: + +```php +getSchemeProtocol(); // 'sqs' +$dsn->getScheme(); // 'sqs' +$dsn->getSchemeExtensions(); // [] +$dsn->getUser(); // null +$dsn->getPassword(); // null +$dsn->getHost(); // null +$dsn->getPort(); // null + +$dsn->getString('key'); // 'aKey' +$dsn->getString('secret'); // 'aSecret' +``` + +Get typed query params: + +```php +getDecimal('decimal'); // 12 +$dsn->getOctal('decimal'); // 0666 +$dsn->getFloat('float'); // 1.2 +$dsn->getBool('bool'); // true +$dsn->getArray('array')->getString(0); // val +$dsn->getArray('array')->getDecimal(1); // 123 +$dsn->getArray('array')->toArray(); // [val] +``` + +Throws exception if DSN not valid: + +```php +getDecimal('connection_timeout'); // throws exception here +``` + +[back to index](index.md) diff --git a/docs/elastica-bundle/overview.md b/docs/elastica-bundle/overview.md index ee82b0afc..22702a813 100644 --- a/docs/elastica-bundle/overview.md +++ b/docs/elastica-bundle/overview.md @@ -1,3 +1,10 @@ +--- +layout: default +title: Elastica bundle +nav_order: 4 +--- +{% include support.md %} + # Enqueue Elastica Bundle `EnqueueElasticaBundle` provides extra features for `FOSElasticaBundle` such as: diff --git a/docs/images/datadog_monitoring.png b/docs/images/datadog_monitoring.png new file mode 100644 index 000000000..731aff3b3 Binary files /dev/null and b/docs/images/datadog_monitoring.png differ diff --git a/docs/images/grafana_monitoring.jpg b/docs/images/grafana_monitoring.jpg new file mode 100644 index 000000000..5d845e351 Binary files /dev/null and b/docs/images/grafana_monitoring.jpg differ diff --git a/docs/index.md b/docs/index.md index cc8577ffa..d38cb873a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,21 @@ +--- +# Feel free to add content and custom Front Matter to this file. +# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults + +layout: default +title: Index +nav_order: 0 +--- + +{% include support.md %} + ## Documentation. * [Quick tour](quick_tour.md) +* [Key concepts](concepts.md) * [Transports](#transports) - Amqp based on [the ext](transport/amqp.md), [bunny](transport/amqp_bunny.md), [the lib](transport/amqp_lib.md) + - [Amazon SNS-SQS](transport/snsqs.md) - [Amazon SQS](transport/sqs.md) - [Google PubSub](transport/gps.md) - [Beanstalk (Pheanstalk)](transport/pheanstalk.md) @@ -10,6 +23,7 @@ - [Kafka](transport/kafka.md) - [Stomp](transport/stomp.md) - [Redis](transport/redis.md) + - [Wamp](transport/wamp.md) - [Doctrine DBAL](transport/dbal.md) - [Filesystem](transport/filesystem.md) - [Null](transport/null.md) @@ -26,18 +40,19 @@ * [Job queue](#job-queue) - [Run unique job](job_queue/run_unique_job.md) - [Run sub job(s)](job_queue/run_sub_job.md) -* [EnqueueBundle (Symfony)](#enqueue-bundle-symfony). +* [EnqueueBundle (Symfony)](bundle/index.md) - [Quick tour](bundle/quick_tour.md) - [Config reference](bundle/config_reference.md) - [Cli commands](bundle/cli_commands.md) - [Message producer](bundle/message_producer.md) - [Message processor](bundle/message_processor.md) - [Async events](bundle/async_events.md) + - [Async commands](bundle/async_commands.md) - [Job queue](bundle/job_queue.md) - [Consumption extension](bundle/consumption_extension.md) - [Production settings](bundle/production_settings.md) - [Debugging](bundle/debugging.md) - - [Functional testing](bundle/functional_testing.md) + - [Functional testing](bundle/functional_testing.md) * [Laravel](#laravel) - [Quick tour](laravel/quick_tour.md) - [Queues](laravel/queues.md) @@ -50,6 +65,8 @@ * [Yii](#yii) - [AMQP Interop driver](yii/amqp_driver.md) * [EnqueueElasticaBundle. Overview](elastica-bundle/overview.md) +* [DSN Parser](dsn.md) +* [Monitoring](monitoring.md) * [Use cases](#use-cases) - [Symfony. Async event dispatcher](async_event_dispatcher/quick_tour.md) - [Monolog. Send messages to message queue](monolog/send-messages-to-mq.md) @@ -76,3 +93,13 @@ * [Spool Swiftmailer emails to real message queue.](https://blog.forma-pro.com/spool-swiftmailer-emails-to-real-message-queue-9ecb8b53b5de) * [Yii PHP Framework has adopted AMQP Interop.](https://blog.forma-pro.com/yii-php-framework-has-adopted-amqp-interop-85ab47c9869f) * [(En)queue Symfony console commands](http://tech.yappa.be/enqueue-symfony-console-commands) +* [From RabbitMq to PhpEnqueue via Symfony Messenger](https://medium.com/@stefanoalletti_40357/from-rabbitmq-to-phpenqueue-via-symfony-messenger-b8260d0e506c) + +## Contributing to this documentation + +To run this documentation locally, you can either create Jekyll environment on your local computer or use docker container. +To run docker container you can use a command from repository root directory: +```shell +docker run -p 4000:4000 --rm --volume="${PWD}/docs:/srv/jekyll" -it jekyll/jekyll jekyll serve --watch +``` +Documentation will then be available for you on http://localhost:4000/ once build completes and rebuild automatically on changes. diff --git a/docs/job_queue/index.md b/docs/job_queue/index.md new file mode 100644 index 000000000..f29a8a97a --- /dev/null +++ b/docs/job_queue/index.md @@ -0,0 +1,9 @@ +--- +layout: default +title: Job Queue +nav_order: 5 +has_children: true +permalink: /job-queue +--- + +{:toc} diff --git a/docs/job_queue/run_sub_job.md b/docs/job_queue/run_sub_job.md index b5f6cb65d..98f2938b3 100644 --- a/docs/job_queue/run_sub_job.md +++ b/docs/job_queue/run_sub_job.md @@ -1,28 +1,36 @@ +--- +layout: default +parent: Job Queue +title: Run sub job +nav_order: 2 +--- +{% include support.md %} + ## Job queue. Run sub job -It shows how you can create and run a sub job, which it is executed separately. -You can create as many sub jobs as you like. -They will be executed in parallel. +It shows how you can create and run a sub job, which it is executed separately. +You can create as many sub jobs as you like. +They will be executed in parallel. ```php jobRunner->runUnique($message->getMessageId(), 'aJobName', function (JobRunner $runner) { $runner->createDelayed('aSubJobName1', function (JobRunner $runner, Job $childJob) { @@ -39,12 +47,12 @@ class RootJobProcessor implements PsrProcessor } } -class SubJobProcessor implements PsrProcessor +class SubJobProcessor implements Processor { /** @var JobRunner */ private $jobRunner; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $data = JSON::decode($message->getBody()); @@ -59,4 +67,4 @@ class SubJobProcessor implements PsrProcessor } ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/job_queue/run_unique_job.md b/docs/job_queue/run_unique_job.md index 84078c40c..c6869ece4 100644 --- a/docs/job_queue/run_unique_job.md +++ b/docs/job_queue/run_unique_job.md @@ -1,3 +1,11 @@ +--- +layout: default +parent: Job Queue +title: Run unique job +nav_order: 1 +--- +{% include support.md %} + ## Job queue. Run unique job There is job queue component build on top of a transport. It provides some additional features: @@ -6,23 +14,23 @@ There is job queue component build on top of a transport. It provides some addit * Run unique job feature. If used guarantee that there is not any job with the same name running same time. * Sub jobs. If used allow split a big job into smaller pieces and process them asynchronously and in parallel. * Depended job. If used allow send a message when the whole job is finished (including sub jobs). - + Here's some examples. -It shows how you can run unique job using job queue (The configuration is described in a dedicated chapter). +It shows how you can run unique job using job queue (The configuration is described in a dedicated chapter). ```php -jobRunner->runUnique($message->getMessageId(), 'aJobName', function () { // do your job, there is no any other processes executing same job, @@ -35,4 +43,4 @@ class UniqueJobProcessor implements PsrProcessor } ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/laravel/index.md b/docs/laravel/index.md new file mode 100644 index 000000000..74f2a9fd4 --- /dev/null +++ b/docs/laravel/index.md @@ -0,0 +1,9 @@ +--- +layout: default +title: Laravel +has_children: true +nav_order: 6 +permalink: /laravel +--- + +{:toc} diff --git a/docs/laravel/queues.md b/docs/laravel/queues.md index 1a6d0bcf7..8a6df4566 100644 --- a/docs/laravel/queues.md +++ b/docs/laravel/queues.md @@ -1,7 +1,15 @@ +--- +layout: default +parent: Laravel +title: Queues +nav_order: 2 +--- +{% include support.md %} + # Laravel Queue. Quick tour. The [LaravelQueue](https://github.com/php-enqueue/laravel-queue) package allows to use [queue-interop](https://github.com/queue-interop/queue-interop) compatible transports [the Laravel way](https://laravel.com/docs/5.4/queues). -I suppose you already [installed and configured](quick_tour.md) the package so let's look what you have to do to make queue work. +I suppose you already [installed and configured](quick_tour.md) the package so let's look what you have to do to make queue work. ## Configure @@ -13,21 +21,18 @@ You have to add a connector to `config/queues.php` file. The driver must be `int // config/queue.php return [ - // uncomment to set it as default - // 'default' => env('QUEUE_DRIVER', 'interop'), - + 'default' => 'interop', 'connections' => [ 'interop' => [ 'driver' => 'interop', - 'connection_factory_class' => \Enqueue\Fs\FsConnectionFactory::class, - - // the factory specific options - 'dsn' => 'file://'.storage_path('enqueue'), + 'dsn' => 'amqp+rabbitmq://guest:guest@localhost:5672/%2f', ], ], ]; ``` +Here's a [full list](../transport) of supported transports. + ## Usage Same as standard [Laravel Queues](https://laravel.com/docs/5.4/queues) @@ -50,11 +55,6 @@ $ php artisan queue:work interop ## Amqp interop -While interop connector can send\consume messages from any queue interop compatible transports. -But it does not support some AMQP specific features, such as queue declaration and delays. -To cover those cases the package provides a AmqpQueue. It can work with any amqp interop [compatible transport](https://github.com/queue-interop/queue-interop#compatible-projects-1), for example `enqueue/amqp-bunny`. -Here's how it could be configured: - ```php env('QUEUE_DRIVER', 'interop'), - + 'connections' => [ 'interop' => [ - 'driver' => 'amqp_interop', - 'connection_factory_class' => \Enqueue\AmqpBunny\AmqpConnectionFactory::class, - + 'driver' => 'interop', + // connects to localhost - 'dsn' => 'amqp:', - - // could be "rabbitmq_dlx", "rabbitmq_delay_plugin", instance of DelayStrategy interface or null - // 'delay_strategy' => 'rabbitmq_dlx' + 'dsn' => 'amqp:', // + + // could be "rabbitmq_dlx", "rabbitmq_delay_plugin", instance of DelayStrategy interface or null + // 'delay_strategy' => 'rabbitmq_dlx' ], ], ]; diff --git a/docs/laravel/quick_tour.md b/docs/laravel/quick_tour.md index e14f4604c..c57ad9e7d 100644 --- a/docs/laravel/quick_tour.md +++ b/docs/laravel/quick_tour.md @@ -1,10 +1,18 @@ +--- +layout: default +parent: Laravel +title: Quick tour +nav_order: 1 +--- +{% include support.md %} + # Laravel Queue. Quick tour. -The [enqueue/laravel-queue](https://github.com/php-enqueue/laravel-queue) is message queue bridge for Enqueue. You can use all transports built on top of [queue-interop](https://github.com/queue-interop/queue-interop) including [all supported](https://github.com/php-enqueue/enqueue-dev/tree/master/docs/transport) by Enqueue. +The [enqueue/laravel-queue](https://github.com/php-enqueue/laravel-queue) is message queue bridge for Enqueue. You can use all transports built on top of [queue-interop](https://github.com/queue-interop/queue-interop) including [all supported](https://github.com/php-enqueue/enqueue-dev/tree/master/docs/transport) by Enqueue. The package allows you to use queue interop transport the [laravel way](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/laravel/queues.md) as well as integrates the [enqueue simple client](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/laravel/quick_tour.md#enqueue-simple-client). -**NOTE:** The part of this code was originally proposed as a PR to [laravel/framework#20148](https://github.com/laravel/framework/pull/20148). It was closed without much explanations, so I decided to open source it as a stand alone package. +**NOTE:** The part of this code was originally proposed as a PR to [laravel/framework#20148](https://github.com/laravel/framework/pull/20148). It was closed without much explanations, so I decided to open source it as a stand alone package. ## Install @@ -31,7 +39,7 @@ return [ ## Laravel queues At this stage you are already able to use [laravel queues](queues.md). - + ## Enqueue Simple client If you want to use [enqueue/simple-client](https://github.com/php-enqueue/simple-client) in your Laravel application you have perform additional steps . @@ -57,7 +65,7 @@ return [ 'client' => [ 'router_topic' => 'default', 'router_queue' => 'default', - 'default_processor_queue' => 'default', + 'default_queue' => 'default', ], ], ]; @@ -68,14 +76,14 @@ Register processor: ```php resolving(SimpleClient::class, function (SimpleClient $client, $app) { - $client->bind('enqueue_test', 'a_processor', function(PsrMessage $message) { + $client->bindTopic('enqueue_test', function(Message $message) { // do stuff here - return PsrProcessor::ACK; + return Processor::ACK; }); return $client; @@ -83,7 +91,7 @@ $app->resolving(SimpleClient::class, function (SimpleClient $client, $app) { ``` -Send message: +Send message: ```php Configuration -> Enqueue Message Queue`. Here's the example of Amqp transport that connects to RabbitMQ broker on localhost: - + ![Сonfiguration](../images/magento_enqueue_configuration.jpeg) @@ -37,20 +45,20 @@ Mage::helper('enqueue')->send('a_topic', 'aMessage'); ## Message Consumption -I assume you have `acme` Magento module properly created, configured and registered. -To consume messages you have to define a processor class first: +I assume you have `acme` Magento module properly created, configured and registered. +To consume messages you have to define a processor class first: ```php getBody() -> 'payload' diff --git a/docs/magento2/cli_commands.md b/docs/magento2/cli_commands.md index b25939098..3f72093b9 100644 --- a/docs/magento2/cli_commands.md +++ b/docs/magento2/cli_commands.md @@ -1,6 +1,14 @@ +--- +layout: default +parent: Magento 2 +title: CLI commands +nav_order: 2 +--- +{% include support.md %} + # Magento2. Cli commands -The enqueue Magento extension provides several commands. +The enqueue Magento extension provides several commands. The most useful one `enqueue:consume` connects to the broker and process the messages. Other commands could be useful during debugging (like `enqueue:topics`) or deployment (like `enqueue:setup-broker`). diff --git a/docs/magento2/index.md b/docs/magento2/index.md new file mode 100644 index 000000000..9ae85803a --- /dev/null +++ b/docs/magento2/index.md @@ -0,0 +1,9 @@ +--- +layout: default +title: Magento 2 +has_children: true +nav_order: 8 +permalink: /magento2 +--- + +{:toc} diff --git a/docs/magento2/quick_tour.md b/docs/magento2/quick_tour.md index c6ced2be2..5063ab56c 100644 --- a/docs/magento2/quick_tour.md +++ b/docs/magento2/quick_tour.md @@ -1,3 +1,11 @@ +--- +layout: default +parent: Magento 2 +title: Quick tour +nav_order: 1 +--- +{% include support.md %} + # Magento2 EnqueueModule The module integrates [Enqueue Client](../client/quick_tour.md) with Magento2. You can send and consume messages to different message queues such as RabbitMQ, AMQP, STOMP, Amazon SQS, Kafka, Redis, Google PubSub, Gearman, Beanstalk, Google PubSub and others. Or integrate Magento2 app with other applications or service via [Message Bus](../client/message_bus.md). @@ -14,12 +22,12 @@ composer require "enqueue/magento2-enqueue:*@dev" "enqueue/amqp-ext" Run setup:upgrade so Magento2 picks up the installed module. ```bash -php bin/magento setup:upgrade +php bin/magento setup:upgrade ``` ## Configuration -At this stage we have configure the Enqueue extension in Magento backend. +At this stage we have configure the Enqueue extension in Magento backend. The config is here: `Stores -> Configuration -> General -> Enqueue Message Queue`. Here's the example of Amqp transport that connects to RabbitMQ broker on localhost: @@ -44,8 +52,8 @@ $replyMessage = $reply->receive(5000); // wait for 5 sec ## Message Consumption -I assume you have `acme` Magento module properly created, configured and registered. -To consume messages you have to define a processor class first: +I assume you have `acme` Magento module properly created, configured and registered. +To consume messages you have to define a processor class first: ```php + @@ -83,7 +91,7 @@ than subscribe it to a topic or several topics: a_topic - acme/async_foo + Acme\Module\Helper\Async\foo diff --git a/docs/messages.md b/docs/messages.md new file mode 100644 index 000000000..362991ba8 --- /dev/null +++ b/docs/messages.md @@ -0,0 +1,22 @@ +--- +layout: default +title: Messages +nav_order: 90 +--- +{% include support.md %} + +## Pull request to readonly repo. + +Thanks for your pull request! We love contributions. + +However, this repository is what we call a "subtree split": a read-only copy of one directory of the main enqueue-dev repository. It is used by Composer to allow developers to depend on specific Enqueue package. + +If you want to contribute, you should instead open a pull request on the main repository: + +https://github.com/php-enqueue/enqueue-dev + +Read the contribution guide + +https://github.com/php-enqueue/enqueue-dev/blob/master/docs/contribution.md + +Thank you for your contribution! diff --git a/docs/monitoring.md b/docs/monitoring.md new file mode 100644 index 000000000..0643b425e --- /dev/null +++ b/docs/monitoring.md @@ -0,0 +1,342 @@ +--- +layout: default +title: Monitoring +nav_order: 95 +--- + +{% include support.md %} + +# Monitoring. + +Enqueue provides a tool for monitoring message queues. +With it, you can control how many messages were sent, how many processed successfully or failed. +How many consumers are working, their up time, processed messages stats, memory usage and system load. +The tool could be integrated with virtually any analytics and monitoring platform. +There are several integration: + * [Datadog StatsD](https://datadoghq.com) + * [InfluxDB](https://www.influxdata.com/) and [Grafana](https://grafana.com/) + * [WAMP (Web Application Messaging Protocol)](https://wamp-proto.org/) +We are working on a JS\WAMP based real-time UI tool, for more information please [contact us](opensource@forma-pro.com). + +![Grafana Monitoring](images/grafana_monitoring.jpg) + +[contact us](mailto:opensource@forma-pro.com) if need a Grafana template such as on the picture. + +* [Installation](#installation) +* [Track sent messages](#track-sent-messages) +* [Track consumed message](#track-consumed-message) +* [Track consumer metrics](#track-consumer-metrics) +* [Consumption extension](#consumption-extension) +* [Enqueue Client Extension](#enqueue-client-extension) +* [InfluxDB Storage](#influxdb-storage) +* [Datadog Storage](#datadog-storage) +* [WAMP (Web Socket Messaging Protocol) Storage](#wamp-(web-socket-messaging-protocol)-storage) +* [Symfony App](#symfony-app) + +## Installation + +```bash +composer req enqueue/monitoring:0.9.x-dev +``` + +## Track sent messages + +```php +create('influxdb://127.0.0.1:8086?db=foo'); +$statsStorage->pushSentMessageStats(new SentMessageStats( + (int) (microtime(true) * 1000), // timestamp + 'queue_name', // queue + 'aMessageId', + 'aCorrelationId', + [], // headers + [] // properties +)); +``` + +or, if you work with [Queue Interop](https://github.com/queue-interop/queue-interop) transport here's how you can track a message sent + +```php +createQueue('foo'); +$message = $context->createMessage('body'); + +$context->createProducer()->send($queue, $message); + +$statsStorage = (new GenericStatsStorageFactory())->create('influxdb://127.0.0.1:8086?db=foo'); +$statsStorage->pushSentMessageStats(new SentMessageStats( + (int) (microtime(true) * 1000), + $queue->getQueueName(), + $message->getMessageId(), + $message->getCorrelationId(), + $message->getHeaders()[], + $message->getProperties() +)); +``` + +## Track consumed message + +```php +create('influxdb://127.0.0.1:8086?db=foo'); +$statsStorage->pushConsumedMessageStats(new ConsumedMessageStats( + 'consumerId', + (int) (microtime(true) * 1000), // now + $receivedAt, + 'aQueue', + 'aMessageId', + 'aCorrelationId', + [], // headers + [], // properties + false, // redelivered or not + ConsumedMessageStats::STATUS_ACK +)); +``` + +or, if you work with [Queue Interop](https://github.com/queue-interop/queue-interop) transport here's how you can track a message sent + +```php +createQueue('foo'); + +$consumer = $context->createConsumer($queue); + +$consumerId = uniqid('consumer-id', true); // we suggest using UUID here +if ($message = $consumer->receiveNoWait()) { + $receivedAt = (int) (microtime(true) * 1000); + + // heavy processing here. + + $consumer->acknowledge($message); + + $statsStorage = (new GenericStatsStorageFactory())->create('influxdb://127.0.0.1:8086?db=foo'); + $statsStorage->pushConsumedMessageStats(new ConsumedMessageStats( + $consumerId, + (int) (microtime(true) * 1000), // now + $receivedAt, + $queue->getQueueName(), + $message->getMessageId(), + $message->getCorrelationId(), + $message->getHeaders(), + $message->getProperties(), + $message->isRedelivered(), + ConsumedMessageStats::STATUS_ACK + )); +} +``` + +## Track consumer metrics + +Consumers are long running processes. It vital to know how many of them are running right now, how they perform, how much memory do they use and so. +This example shows how you can send such metrics. +Call this code from time to time between processing messages. + +```php +create('influxdb://127.0.0.1:8086?db=foo'); +$statsStorage->pushConsumerStats(new ConsumerStats( + 'consumerId', + (int) (microtime(true) * 1000), // now + $startedAt, + null, // finished at + true, // is started? + false, // is finished? + false, // is failed + ['foo'], // consume from queues + 123, // received messages + 120, // acknowledged messages + 1, // rejected messages + 1, // requeued messages + memory_get_usage(true), + sys_getloadavg()[0] +)); +``` + +## Consumption extension + +There is an extension `ConsumerMonitoringExtension` for Enqueue [QueueConsumer](quick_tour.md#consumption). +It could collect consumed messages and consumer stats for you. + +```php +create('influxdb://127.0.0.1:8086?db=foo'); + +$queueConsumer = new QueueConsumer($context, new ChainExtension([ + new ConsumerMonitoringExtension($statsStorage) +])); + +// bind + +// consume +``` + +## Enqueue Client Extension + +There is an extension ClientMonitoringExtension for Enqueue [Client](quick_tour.md#client) too. It could collect sent messages stats for you. + +## InfluxDB Storage + +Install additional packages: + +``` +composer req influxdb/influxdb-php:^1.14 +``` + +```php +create('influxdb://127.0.0.1:8086?db=foo'); +``` + +There are available options: + +``` +* 'host' => '127.0.0.1', +* 'port' => '8086', +* 'user' => '', +* 'password' => '', +* 'db' => 'enqueue', +* 'measurementSentMessages' => 'sent-messages', +* 'measurementConsumedMessages' => 'consumed-messages', +* 'measurementConsumers' => 'consumers', +* 'client' => null, +* 'retentionPolicy' => null, +``` + +You can pass InfluxDB\Client instance in `client` option. Otherwise, it will be created on first use according to other +options. + +If your InfluxDB\Client uses driver that implements InfluxDB\Driver\QueryDriverInterface, then database will be +automatically created for you if it doesn't exist. Default InfluxDB\Client will also do that. + +## Datadog storage + +Install additional packages: + +``` +composer req datadog/php-datadogstatsd:^1.3 +``` + +```php +create('datadog://127.0.0.1:8125'); +``` + +For best experience please adjust units and types in metric summary. + +Example dashboard: + +![Datadog monitoring](images/datadog_monitoring.png) + + +There are available options (and all available metrics): + +``` +* 'host' => '127.0.0.1', +* 'port' => '8125', +* 'batched' => true, // performance boost +* 'global_tags' => '', // should contain keys and values +* 'metric.messages.sent' => 'enqueue.messages.sent', +* 'metric.messages.consumed' => 'enqueue.messages.consumed', +* 'metric.messages.redelivered' => 'enqueue.messages.redelivered', +* 'metric.messages.failed' => 'enqueue.messages.failed', +* 'metric.consumers.started' => 'enqueue.consumers.started', +* 'metric.consumers.finished' => 'enqueue.consumers.finished', +* 'metric.consumers.failed' => 'enqueue.consumers.failed', +* 'metric.consumers.received' => 'enqueue.consumers.received', +* 'metric.consumers.acknowledged' => 'enqueue.consumers.acknowledged', +* 'metric.consumers.rejected' => 'enqueue.consumers.rejected', +* 'metric.consumers.requeued' => 'enqueue.consumers.requeued', +* 'metric.consumers.memoryUsage' => 'enqueue.consumers.memoryUsage', +``` + + +## WAMP (Web Socket Messaging Protocol) Storage + +Install additional packages: + +``` +composer req thruway/pawl-transport:^0.5.0 thruway/client:^0.5.0 +``` + +```php +create('wamp://127.0.0.1:9090?topic=stats'); +``` + +There are available options: + +``` +* 'host' => '127.0.0.1', +* 'port' => '9090', +* 'topic' => 'stats', +* 'max_retries' => 15, +* 'initial_retry_delay' => 1.5, +* 'max_retry_delay' => 300, +* 'retry_delay_growth' => 1.5, +``` + +## Symfony App + +You have to register some services in order to incorporate monitoring facilities into your Symfony application. + +```yaml +# config/packages/enqueue.yaml + +enqueue: + default: + transport: 'amqp://guest:guest@bar:5672/%2f' + monitoring: 'influxdb://127.0.0.1:8086?db=foo' + + another: + transport: 'amqp://guest:guest@foo:5672/%2f' + monitoring: 'wamp://127.0.0.1:9090?topic=stats' + client: ~ + + datadog: + transport: 'amqp://guest:guest@foo:5672/%2f' + monitoring: 'datadog://127.0.0.1:8125?batched=false' + client: ~ +``` + +[back to index](index.md) diff --git a/docs/monolog/send-messages-to-mq.md b/docs/monolog/send-messages-to-mq.md index caead3723..dbcb90eb5 100644 --- a/docs/monolog/send-messages-to-mq.md +++ b/docs/monolog/send-messages-to-mq.md @@ -1,15 +1,21 @@ -# Enqueue Monolog Handlers +--- +layout: default +nav_exclude: true +--- +{% include support.md %} -The package provides handlers for [Monolog](https://github.com/Seldaek/monolog). -These handler allows to send logs to MQ using any [queue-interop](https://github.com/queue-interop/queue-interop) compatible transports. +# Enqueue Monolog Handlers + +The package provides handlers for [Monolog](https://github.com/Seldaek/monolog). +These handler allows to send logs to MQ using any [queue-interop](https://github.com/queue-interop/queue-interop) compatible transports. ## Installation You have to install monolog itself, queue interop handlers and one of [the transports](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md#transports). -For the simplicity we are going to install the filesystem based MQ. +For the simplicity we are going to install the filesystem based MQ. ``` -composer require enqueue/monolog-queue-handler monolog/monlog enqueue/fs +composer require enqueue/monolog-queue-handler monolog/monolog enqueue/fs ``` ## Usage @@ -33,28 +39,28 @@ $log->warning('Foo'); $log->error('Bar'); ``` -the consumer may look like this: +the consumer may look like this: ```php createContext(); $consumer = new QueueConsumer($context); -$consumer->bind('log', function(PsrMessage $message) { +$consumer->bindCallback('log', function(Message $message) { echo $message->getBody().PHP_EOL; - return PsrProcessor::ACK; + return Processor::ACK; }); $consumer->consume(); ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/quick_tour.md b/docs/quick_tour.md index 51fffaff6..4e6bfccec 100644 --- a/docs/quick_tour.md +++ b/docs/quick_tour.md @@ -1,51 +1,60 @@ +--- +layout: default +title: Quick tour +nav_order: 2 +--- +{% include support.md %} + # Quick tour - + * [Transport](#transport) * [Consumption](#consumption) * [Remote Procedure Call (RPC)](#remote-procedure-call-rpc) * [Client](#client) * [Cli commands](#cli-commands) +* [Monitoring](#monitoring) +* [Symfony bundle](#symfony) ## Transport -The transport layer or PSR (Enqueue message service) is a Message Oriented Middleware for sending messages between two or more clients. -It is a messaging component that allows applications to create, send, receive, and read messages. +The transport layer or PSR (Enqueue message service) is a Message Oriented Middleware for sending messages between two or more clients. +It is a messaging component that allows applications to create, send, receive, and read messages. It allows the communication between different components of a distributed application to be loosely coupled, reliable, and asynchronous. -PSR is inspired by JMS (Java Message Service). We tried to be as close as possible to [JSR 914](https://docs.oracle.com/javaee/7/api/javax/jms/package-summary.html) specification. +PSR is inspired by JMS (Java Message Service). We tried to stay as close as possible to the [JSR 914](https://docs.oracle.com/javaee/7/api/javax/jms/package-summary.html) specification. For now it supports [AMQP](https://www.rabbitmq.com/tutorials/amqp-concepts.html) and [STOMP](https://stomp.github.io/) message queue protocols. -You can connect to many modern brokers such as [RabbitMQ](https://www.rabbitmq.com/), [ActiveMQ](http://activemq.apache.org/) and others. +You can connect to many modern brokers such as [RabbitMQ](https://www.rabbitmq.com/), [ActiveMQ](http://activemq.apache.org/) and others. Produce a message: ```php createContext(); +/** @var ConnectionFactory $connectionFactory **/ +$context = $connectionFactory->createContext(); -$destination = $psrContext->createQueue('foo'); +$destination = $context->createQueue('foo'); //$destination = $context->createTopic('foo'); -$message = $psrContext->createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($destination, $message); +$context->createProducer()->send($destination, $message); ``` Consume a message: ```php createContext(); +/** @var ConnectionFactory $connectionFactory **/ +$context = $connectionFactory->createContext(); -$destination = $psrContext->createQueue('foo'); +$destination = $context->createQueue('foo'); //$destination = $context->createTopic('foo'); -$consumer = $psrContext->createConsumer($destination); +$consumer = $context->createConsumer($destination); $message = $consumer->receive(); @@ -55,42 +64,42 @@ $consumer->acknowledge($message); // $consumer->reject($message); ``` -## Consumption +## Consumption -Consumption is a layer built on top of a transport functionality. -The goal of the component is to simply consume messages. -The `QueueConsumer` is main piece of the component it allows binding of message processors (or callbacks) to queues. +Consumption is a layer built on top of a transport functionality. +The goal of the component is to simply consume messages. +The `QueueConsumer` is main piece of the component it allows binding of message processors (or callbacks) to queues. The `consume` method starts the consumption process which last as long as it is not interrupted. ```php bind('foo_queue', function(PsrMessage $message) { +$queueConsumer->bindCallback('foo_queue', function(Message $message) { // process message - - return PsrProcessor::ACK; + + return Processor::ACK; }); -$queueConsumer->bind('bar_queue', function(PsrMessage $message) { +$queueConsumer->bindCallback('bar_queue', function(Message $message) { // process message - - return PsrProcessor::ACK; + + return Processor::ACK; }); $queueConsumer->consume(); ``` -There are bunch of [extensions](consumption/extensions.md) available. -This is an example of how you can add them. +There are bunch of [extensions](consumption/extensions.md) available. +This is an example of how you can add them. The `SignalExtension` provides support of process signals, whenever you send SIGTERM for example it will correctly managed. -The `LimitConsumptionTimeExtension` interrupts the consumption after given time. +The `LimitConsumptionTimeExtension` interrupts the consumption after given time. ```php createQueue('foo'); -$message = $psrContext->createMessage('Hi there!'); +$queue = $context->createQueue('foo'); +$message = $context->createMessage('Hi there!'); -$rpcClient = new RpcClient($psrContext); +$rpcClient = new RpcClient($context); $promise = $rpcClient->callAsync($queue, $message, 1); $replyMessage = $promise->receive(); ``` -There is also extensions for the consumption component. +There is also extensions for the consumption component. It simplifies a server side of RPC. ```php bind('foo', function(PsrMessage $message, PsrContext $context) { +$queueConsumer->bindCallback('foo', function(Message $message, Context $context) { $replyMessage = $context->createMessage('Hello'); - + return Result::reply($replyMessage); }); @@ -157,69 +166,68 @@ $queueConsumer->consume(); ## Client It provides an easy to use high level abstraction. -The goal of the component is hide as much as possible low level details so you can concentrate on things that really matters. -For example, It configure a broker for you by creating queues, exchanges and bind them. -It provides easy to use services for producing and processing messages. +The goal of the component is to hide as much as possible low level details so you can concentrate on things that really matter. +For example, it configures a broker for you by creating queues, exchanges and bind them. +It provides easy to use services for producing and processing messages. It supports unified format for setting message expiration, delay, timestamp, correlation id. It supports [message bus](http://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageBus.html) so different applications can talk to each other. - + Here's an example of how you can send and consume **event messages**. - + ```php bindTopic('a_foo_topic', function(Message $message) { + echo $message->getBody().PHP_EOL; + + // your event processor logic here +}); $client->setupBroker(); $client->sendEvent('a_foo_topic', 'message'); -$client->bind('a_foo_topic', 'fooProcessor', function(PsrMessage $message) { - echo $message->getBody().PHP_EOL; - - // your event processor logic here -}); - -// this is a blocking call, it'll consume message until it is interrupted +// this is a blocking call, it'll consume message until it is interrupted $client->consume(); ``` -and **command messages**: +and **command messages**: ```php setupBroker(); - -$client->bind(Config::COMMAND_TOPIC, 'bar_command', function(PsrMessage $message) { +$client->bindCommand('bar_command', function(Message $message) { // your bar command processor logic here }); -$client->bind(Config::COMMAND_TOPIC, 'baz_reply_command', function(PsrMessage $message, PsrContext $context) { +$client->bindCommand('baz_reply_command', function(Message $message, Context $context) { // your baz reply command processor logic here - + return Result::reply($context->createMessage('theReplyBody')); }); -// It is sent to one consumer. +$client->setupBroker(); + +// It is sent to one consumer. $client->sendCommand('bar_command', 'aMessageData'); // It is possible to get reply @@ -229,16 +237,16 @@ $promise = $client->sendCommand('bar_command', 'aMessageData', true); $replyMessage = $promise->receive(2000); // 2 sec -// this is a blocking call, it'll consume message until it is interrupted +// this is a blocking call, it'll consume message until it is interrupted $client->consume([new ReplyExtension()]); ``` -Read more about events and commands [here](client/quick_tour.md#produce-message). +Read more about events and commands [here](client/quick_tour.md#produce-message). ## Cli commands -The library provides handy commands out of the box. -They all build on top of [Symfony Console component](http://symfony.com/doc/current/components/console.html). +The library provides handy commands out of the box. +They all build on top of [Symfony Console component](http://symfony.com/doc/current/components/console.html). The most useful is a consume command. There are two of them one from consumption component and the other from client one. Let's see how you can use consumption one: @@ -249,17 +257,17 @@ Let's see how you can use consumption one: // app.php use Symfony\Component\Console\Application; -use Interop\Queue\PsrMessage; +use Interop\Queue\Message; use Enqueue\Consumption\QueueConsumer; -use Enqueue\Symfony\Consumption\ConsumeMessagesCommand; +use Enqueue\Symfony\Consumption\SimpleConsumeCommand; /** @var QueueConsumer $queueConsumer */ -$queueConsumer->bind('a_queue', function(PsrMessage $message) { - // process message +$queueConsumer->bindCallback('a_queue', function(Message $message) { + // process message }); -$consumeCommand = new ConsumeMessagesCommand($queueConsumer); +$consumeCommand = new SimpleConsumeCommand($queueConsumer); $consumeCommand->setName('consume'); $app = new Application(); @@ -268,9 +276,17 @@ $app->run(); ``` and starts the consumption from the console: - + ```bash $ app.php consume ``` +## Monitoring + +There is a tool that can track sent\consumed messages as well as consumer performance. Read more [here](monitoring.md) + [back to index](index.md) + +## Symfony + +Read more [here](bundle/quick_tour.md) about using Enqueue as a Symfony Bundle. diff --git a/docs/transport/amqp.md b/docs/transport/amqp.md index 68a8e8bfb..f398f0043 100644 --- a/docs/transport/amqp.md +++ b/docs/transport/amqp.md @@ -1,12 +1,25 @@ +--- +layout: default +title: AMQP +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # AMQP transport Implements [AMQP specifications](https://www.rabbitmq.com/specification.html) and implements [amqp interop](https://github.com/queue-interop/amqp-interop) interfaces. Build on top of [php amqp extension](https://github.com/pdezwart/php-amqp). +Drawbacks: +* [heartbeats will not work properly](https://github.com/pdezwart/php-amqp#persistent-connection) +* [signals will not be properly handled](https://github.com/pdezwart/php-amqp#keeping-track-of-the-workers) + +Parts: * [Installation](#installation) * [Create context](#create-context) * [Declare topic](#declare-topic) -* [Declare queue](#decalre-queue) +* [Declare queue](#declare-queue) * [Bind queue to topic](#bind-queue-to-topic) * [Send message to topic](#send-message-to-topic) * [Send message to queue](#send-message-to-queue) @@ -14,6 +27,7 @@ Build on top of [php amqp extension](https://github.com/pdezwart/php-amqp). * [Send expiration message](#send-expiration-message) * [Send delayed message](#send-delayed-message) * [Consume message](#consume-message) +* [Subscription consumer](#subscription-consumer) * [Purge queue messages](#purge-queue-messages) ## Installation @@ -52,7 +66,7 @@ $factory = new AmqpConnectionFactory([ // same as above but given as DSN string $factory = new AmqpConnectionFactory('amqp://user:pass@example.com:10000/%2f'); -// SSL or secure connection +// SSL or secure connection $factory = new AmqpConnectionFactory([ 'dsn' => 'amqps:', 'ssl_cacert' => '/path/to/cacert.pem', @@ -60,104 +74,104 @@ $factory = new AmqpConnectionFactory([ 'ssl_key' => '/path/to/key.pem', ]); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('amqp:'); -$psrContext = \Enqueue\dsn_to_context('amqp+ext:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('amqp:')->createContext(); +$context = (new \Enqueue\ConnectionFactoryFactory())->create('amqp+ext:')->createContext(); ``` ## Declare topic. -Declare topic operation creates a topic on a broker side. - +Declare topic operation creates a topic on a broker side. + ```php createTopic('foo'); +$fooTopic = $context->createTopic('foo'); $fooTopic->setType(AmqpTopic::TYPE_FANOUT); -$psrContext->declareTopic($fooTopic); +$context->declareTopic($fooTopic); // to remove topic use delete topic method -//$psrContext->deleteTopic($fooTopic); +//$context->deleteTopic($fooTopic); ``` ## Declare queue. -Declare queue operation creates a queue on a broker side. - +Declare queue operation creates a queue on a broker side. + ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); $fooQueue->addFlag(AmqpQueue::FLAG_DURABLE); -$psrContext->declareQueue($fooQueue); +$context->declareQueue($fooQueue); -// to remove topic use delete queue method -//$psrContext->deleteQueue($fooQueue); +// to remove queue use delete queue method +//$context->deleteQueue($fooQueue); ``` ## Bind queue to topic -Connects a queue to the topic. So messages from that topic comes to the queue and could be processed. +Connects a queue to the topic. So messages from that topic comes to the queue and could be processed. ```php bind(new AmqpBind($fooTopic, $fooQueue)); +$context->bind(new AmqpBind($fooTopic, $fooQueue)); ``` -## Send message to topic +## Send message to topic ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Send priority message ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); $fooQueue->addFlag(AmqpQueue::FLAG_DURABLE); $fooQueue->setArguments(['x-max-priority' => 10]); -$psrContext->declareQueue($fooQueue); +$context->declareQueue($fooQueue); -$message = $psrContext->createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setPriority(5) // the higher priority the sooner a message gets to a consumer - // + // ->send($fooQueue, $message) ; ``` @@ -166,21 +180,21 @@ $psrContext->createProducer() ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setTimeToLive(60000) // 60 sec - // + // ->send($fooQueue, $message) ; ``` ## Send delayed message -AMQP specification says nothing about message delaying hence the producer throws `DeliveryDelayNotSupportedException`. +AMQP specification says nothing about message delaying hence the producer throws `DeliveryDelayNotSupportedException`. Though the producer (and the context) accepts a delivery delay strategy and if it is set it uses it to send delayed message. The `enqueue/amqp-tools` package provides two RabbitMQ delay strategies, to use them you have to install that package @@ -188,29 +202,29 @@ The `enqueue/amqp-tools` package provides two RabbitMQ delay strategies, to use createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setDelayStrategy(new RabbitMqDlxDelayStrategy()) ->setDeliveryDelay(5000) // 5 sec - + ->send($fooQueue, $message) ; -```` +```` ## Consume message: ```php createConsumer($fooQueue); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); @@ -220,16 +234,49 @@ $consumer->acknowledge($message); // $consumer->reject($message); ``` +## Subscription consumer + +```php +createConsumer($fooQueue); +$barConsumer = $context->createConsumer($barQueue); + +$subscriptionConsumer = $context->createSubscriptionConsumer(); +$subscriptionConsumer->subscribe($fooConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); +$subscriptionConsumer->subscribe($barConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); + +$subscriptionConsumer->consume(2000); // 2 sec +``` + ## Purge queue messages: ```php createQueue('aQueue'); +$queue = $context->createQueue('aQueue'); -$psrContext->purgeQueue($queue); +$context->purgeQueue($queue); ``` [back to index](../index.md) diff --git a/docs/transport/amqp_bunny.md b/docs/transport/amqp_bunny.md index ce064a9ce..066a023cd 100644 --- a/docs/transport/amqp_bunny.md +++ b/docs/transport/amqp_bunny.md @@ -1,3 +1,11 @@ +--- +layout: default +title: AMQP Bunny +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # AMQP transport Implements [AMQP specifications](https://www.rabbitmq.com/specification.html) and implements [amqp interop](https://github.com/queue-interop/amqp-interop) interfaces. @@ -6,7 +14,7 @@ Build on top of [bunny lib](https://github.com/jakubkulhan/bunny). * [Installation](#installation) * [Create context](#create-context) * [Declare topic](#declare-topic) -* [Declare queue](#decalre-queue) +* [Declare queue](#declare-queue) * [Bind queue to topic](#bind-queue-to-topic) * [Send message to topic](#send-message-to-topic) * [Send message to queue](#send-message-to-queue) @@ -14,6 +22,7 @@ Build on top of [bunny lib](https://github.com/jakubkulhan/bunny). * [Send expiration message](#send-expiration-message) * [Send delayed message](#send-delayed-message) * [Consume message](#consume-message) +* [Subscription consumer](#subscription-consumer) * [Purge queue messages](#purge-queue-messages) ## Installation @@ -50,104 +59,106 @@ $factory = new AmqpConnectionFactory([ // same as above but given as DSN string $factory = new AmqpConnectionFactory('amqp://user:pass@example.com:10000/%2f'); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('amqp:'); -$psrContext = \Enqueue\dsn_to_context('amqp+bunny:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('amqp:')->createContext(); +$context = (new \Enqueue\ConnectionFactoryFactory())->create('amqp+bunny:')->createContext(); ``` ## Declare topic. -Declare topic operation creates a topic on a broker side. - +Declare topic operation creates a topic on a broker side. + ```php createTopic('foo'); +$fooTopic = $context->createTopic('foo'); $fooTopic->setType(AmqpTopic::TYPE_FANOUT); -$psrContext->declareTopic($fooTopic); +$context->declareTopic($fooTopic); // to remove topic use delete topic method -//$psrContext->deleteTopic($fooTopic); +//$context->deleteTopic($fooTopic); ``` ## Declare queue. -Declare queue operation creates a queue on a broker side. - +Declare queue operation creates a queue on a broker side. + ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); $fooQueue->addFlag(AmqpQueue::FLAG_DURABLE); -$psrContext->declareQueue($fooQueue); +$context->declareQueue($fooQueue); // to remove topic use delete queue method -//$psrContext->deleteQueue($fooQueue); +//$context->deleteQueue($fooQueue); ``` ## Bind queue to topic -Connects a queue to the topic. So messages from that topic comes to the queue and could be processed. +Connects a queue to the topic. So messages from that topic comes to the queue and could be processed. ```php bind(new AmqpBind($fooTopic, $fooQueue)); +$context->bind(new AmqpBind($fooTopic, $fooQueue)); ``` -## Send message to topic +## Send message to topic ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Send priority message ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); $fooQueue->addFlag(AmqpQueue::FLAG_DURABLE); $fooQueue->setArguments(['x-max-priority' => 10]); -$psrContext->declareQueue($fooQueue); +$context->declareQueue($fooQueue); -$message = $psrContext->createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setPriority(5) // the higher priority the sooner a message gets to a consumer - // + // ->send($fooQueue, $message) ; ``` @@ -156,21 +167,21 @@ $psrContext->createProducer() ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setTimeToLive(60000) // 60 sec - // + // ->send($fooQueue, $message) ; ``` ## Send delayed message -AMQP specification says nothing about message delaying hence the producer throws `DeliveryDelayNotSupportedException`. +AMQP specification says nothing about message delaying hence the producer throws `DeliveryDelayNotSupportedException`. Though the producer (and the context) accepts a delivery delay strategy and if it is set it uses it to send delayed message. The `enqueue/amqp-tools` package provides two RabbitMQ delay strategies, to use them you have to install that package @@ -178,17 +189,17 @@ The `enqueue/amqp-tools` package provides two RabbitMQ delay strategies, to use createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setDelayStrategy(new RabbitMqDlxDelayStrategy()) ->setDeliveryDelay(5000) // 5 sec - + ->send($fooQueue, $message) ; ```` @@ -197,10 +208,10 @@ $psrContext->createProducer() ```php createConsumer($fooQueue); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); @@ -210,16 +221,49 @@ $consumer->acknowledge($message); // $consumer->reject($message); ``` +## Subscription consumer + +```php +createConsumer($fooQueue); +$barConsumer = $context->createConsumer($barQueue); + +$subscriptionConsumer = $context->createSubscriptionConsumer(); +$subscriptionConsumer->subscribe($fooConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); +$subscriptionConsumer->subscribe($barConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); + +$subscriptionConsumer->consume(2000); // 2 sec +``` + ## Purge queue messages: ```php createQueue('aQueue'); +$queue = $context->createQueue('aQueue'); -$psrContext->purgeQueue($queue); +$context->purgeQueue($queue); ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/transport/amqp_lib.md b/docs/transport/amqp_lib.md index f7dbcea09..288779a55 100644 --- a/docs/transport/amqp_lib.md +++ b/docs/transport/amqp_lib.md @@ -1,8 +1,25 @@ +--- +layout: default +title: AMQP Lib +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # AMQP transport Implements [AMQP specifications](https://www.rabbitmq.com/specification.html) and implements [amqp interop](https://github.com/queue-interop/amqp-interop) interfaces. Build on top of [php amqp lib](https://github.com/php-amqplib/php-amqplib). +Features: +* Configure with DSN string +* Delay strategies out of the box +* Interchangeable with other AMQP Interop implementations +* Fixes AMQPIOWaitException when signal is sent. +* More reliable heartbeat implementations. +* Supports Subscription consumer + +Parts: * [Installation](#installation) * [Create context](#create-context) * [Declare topic](#declare-topic) @@ -14,7 +31,9 @@ Build on top of [php amqp lib](https://github.com/php-amqplib/php-amqplib). * [Send expiration message](#send-expiration-message) * [Send delayed message](#send-delayed-message) * [Consume message](#consume-message) +* [Subscription consumer](#subscription-consumer) * [Purge queue messages](#purge-queue-messages) +* [Long running task and heartbeat and timeouts](#long-running-task-and-heartbeat-and-timeouts) ## Installation @@ -50,7 +69,7 @@ $factory = new AmqpConnectionFactory([ // same as above but given as DSN string $factory = new AmqpConnectionFactory('amqp://user:pass@example.com:10000/%2f'); -// SSL or secure connection +// SSL or secure connection $factory = new AmqpConnectionFactory([ 'dsn' => 'amqps:', 'ssl_cacert' => '/path/to/cacert.pem', @@ -58,104 +77,106 @@ $factory = new AmqpConnectionFactory([ 'ssl_key' => '/path/to/key.pem', ]); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('amqp:'); -$psrContext = \Enqueue\dsn_to_context('amqp+lib:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('amqp:')->createContext(); +$context = (new \Enqueue\ConnectionFactoryFactory())->create('amqp+lib:')->createContext(); ``` ## Declare topic. -Declare topic operation creates a topic on a broker side. - +Declare topic operation creates a topic on a broker side. + ```php createTopic('foo'); +$fooTopic = $context->createTopic('foo'); $fooTopic->setType(AmqpTopic::TYPE_FANOUT); -$psrContext->declareTopic($fooTopic); +$context->declareTopic($fooTopic); // to remove topic use delete topic method -//$psrContext->deleteTopic($fooTopic); +//$context->deleteTopic($fooTopic); ``` ## Declare queue. -Declare queue operation creates a queue on a broker side. - +Declare queue operation creates a queue on a broker side. + ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); $fooQueue->addFlag(AmqpQueue::FLAG_DURABLE); -$psrContext->declareQueue($fooQueue); +$context->declareQueue($fooQueue); // to remove topic use delete queue method -//$psrContext->deleteQueue($fooQueue); +//$context->deleteQueue($fooQueue); ``` ## Bind queue to topic -Connects a queue to the topic. So messages from that topic comes to the queue and could be processed. +Connects a queue to the topic. So messages from that topic comes to the queue and could be processed. ```php bind(new AmqpBind($fooTopic, $fooQueue)); +$context->bind(new AmqpBind($fooTopic, $fooQueue)); ``` -## Send message to topic +## Send message to topic ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Send priority message ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); $fooQueue->addFlag(AmqpQueue::FLAG_DURABLE); $fooQueue->setArguments(['x-max-priority' => 10]); -$psrContext->declareQueue($fooQueue); +$context->declareQueue($fooQueue); -$message = $psrContext->createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setPriority(5) // the higher priority the sooner a message gets to a consumer - // + // ->send($fooQueue, $message) ; ``` @@ -164,21 +185,21 @@ $psrContext->createProducer() ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setTimeToLive(60000) // 60 sec - // + // ->send($fooQueue, $message) ; ``` ## Send delayed message -AMQP specification says nothing about message delaying hence the producer throws `DeliveryDelayNotSupportedException`. +AMQP specification says nothing about message delaying hence the producer throws `DeliveryDelayNotSupportedException`. Though the producer (and the context) accepts a delivery delay strategy and if it is set it uses it to send delayed message. The `enqueue/amqp-tools` package provides two RabbitMQ delay strategies, to use them you have to install that package @@ -186,17 +207,17 @@ The `enqueue/amqp-tools` package provides two RabbitMQ delay strategies, to use createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setDelayStrategy(new RabbitMqDlxDelayStrategy()) ->setDeliveryDelay(5000) // 5 sec - + ->send($fooQueue, $message) ; ```` @@ -205,10 +226,10 @@ $psrContext->createProducer() ```php createConsumer($fooQueue); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); @@ -218,16 +239,109 @@ $consumer->acknowledge($message); // $consumer->reject($message); ``` +## Subscription consumer + +```php +createConsumer($fooQueue); +$barConsumer = $context->createConsumer($barQueue); + +$subscriptionConsumer = $context->createSubscriptionConsumer(); +$subscriptionConsumer->subscribe($fooConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); +$subscriptionConsumer->subscribe($barConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); + +$subscriptionConsumer->consume(2000); // 2 sec +``` + ## Purge queue messages: ```php createQueue('aQueue'); +$queue = $context->createQueue('aQueue'); + +$context->purgeQueue($queue); +``` + +## Long running task and heartbeat and timeouts -$psrContext->purgeQueue($queue); +AMQP relies on heartbeat feature to make sure consumer is still there. +Basically consumer is expected to send heartbeat frames from time to time to RabbitMQ broker so the broker does not close the connection. +It is not possible to implement heartbeat feature in PHP, due to its synchronous nature. +You could read more about the issues in post: [Keeping RabbitMQ connections alive in PHP](https://blog.mollie.com/keeping-rabbitmq-connections-alive-in-php-b11cb657d5fb). + +`enqueue/amqp-lib` address the issue by registering heartbeat call as a [tick callbacks](http://php.net/manual/en/function.register-tick-function.php). +To make it work you have to wrapp your long running task by `declare(ticks=1) {}`. +The number of ticks could be adjusted to your needs. +Calling it at every tick is not good. + +Please note that it does not fix heartbeat issue if you spent most of the time on IO operation. + +Example: + +```php +createContext(); + +$queue = $context->createQueue('a_queue'); +$consumer = $context->createConsumer($queue); + +$subscriptionConsumer = $context->createSubscriptionConsumer(); +$subscriptionConsumer->subscribe($consumer, function(AmqpMessage $message, AmqpConsumer $consumer) { + // ticks number should be adjusted. + declare(ticks=1) { + foreach (fetchHugeSet() as $item) { + // cycle does something for a long time, much longer than amqp heartbeat. + } + } + + $consumer->acknowledge($message); + + return true; +}); + +$subscriptionConsumer->consume(10000); + + +function fetchHugeSet(): array {}; +``` + +Fixes partly `Invalid frame type 65` issue. + +``` +Error: Uncaught PhpAmqpLib\Exception\AMQPRuntimeException: Invalid frame type 65 in /some/path/vendor/php-amqplib/php-amqplib/PhpAmqpLib/Connection/AbstractConnection.php:528 +``` + +Fixes partly `Broken pipe or closed connection` issue. + +``` +PHP Fatal error: Uncaught exception 'PhpAmqpLib\Exception\AMQPRuntimeException' with message 'Broken pipe or closed connection' in /some/path/vendor/php-amqplib/php-amqplib/PhpAmqpLib/Wire/IO/StreamIO.php:190 ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/transport/dbal.md b/docs/transport/dbal.md index 2d84782e1..559414a74 100644 --- a/docs/transport/dbal.md +++ b/docs/transport/dbal.md @@ -1,16 +1,25 @@ +--- +layout: default +title: DBAL +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # Doctrine DBAL transport -The transport uses [Doctrine DBAL](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/) library and SQL like server as a broker. -It creates a table there. Pushes and pops messages to\from that table. - -**Limitations** It works only in auto ack mode hence If consumer crashes the message is lost. +The transport uses [Doctrine DBAL](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/) library and SQL like server as a broker. +It creates a table there. Pushes and pops messages to\from that table. * [Installation](#installation) * [Init database](#init-database) * [Create context](#create-context) * [Send message to topic](#send-message-to-topic) * [Send message to queue](#send-message-to-queue) +* [Send expiration message](#send-expiration-message) +* [Send delayed message](#send-delayed-message) * [Consume message](#consume-message) +* [Subscription consumer](#subscription-consumer) ## Installation @@ -31,7 +40,7 @@ $factory = new DbalConnectionFactory('mysql://user:pass@localhost:3306/mqdev'); // connects to localhost $factory = new DbalConnectionFactory('mysql:'); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); ``` * With existing connection: @@ -39,7 +48,7 @@ $psrContext = $factory->createContext(); ```php 'default', ]); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('mysql:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('mysql:')->createContext(); ``` ## Init database @@ -60,47 +69,115 @@ Please pay attention to that the database has to be created manually. ```php createDataBaseTable(); +$context->createDataBaseTable(); ``` ## Send message to topic +```php +createTopic('aTopic'); +$message = $context->createMessage('Hello world!'); + +$context->createProducer()->send($fooTopic, $message); +``` + +## Send message to queue + +```php +createQueue('aQueue'); +$message = $context->createMessage('Hello world!'); + +$context->createProducer()->send($fooQueue, $message); +``` + +## Send expiration message + ```php createTopic('aTopic'); $message = $psrContext->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooTopic, $message); +$psrContext->createProducer() + ->setTimeToLive(60000) // 60 sec + // + ->send($fooQueue, $message) +; ``` -## Send message to queue +## Send delayed message ```php createQueue('aQueue'); $message = $psrContext->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); -``` +$psrContext->createProducer() + ->setDeliveryDelay(5000) // 5 sec + // + ->send($fooQueue, $message) +; +```` ## Consume message: ```php createQueue('aQueue'); -$consumer = $psrContext->createConsumer($fooQueue); +$fooQueue = $context->createQueue('aQueue'); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); // process a message + +$consumer->acknowledge($message); +//$consumer->reject($message); +``` + +## Subscription consumer + +```php +createConsumer($fooQueue); +$barConsumer = $context->createConsumer($barQueue); + +$subscriptionConsumer = $context->createSubscriptionConsumer(); +$subscriptionConsumer->subscribe($fooConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); +$subscriptionConsumer->subscribe($barConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); + +$subscriptionConsumer->consume(2000); // 2 sec ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/transport/filesystem.md b/docs/transport/filesystem.md index 825b7b5a2..768ee50b7 100644 --- a/docs/transport/filesystem.md +++ b/docs/transport/filesystem.md @@ -1,9 +1,17 @@ +--- +layout: default +title: Filesystem +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # Filesystem transport -Use files on local filesystem as queues. -It creates a file per queue\topic. +Use files on local filesystem as queues. +It creates a file per queue\topic. A message is a line inside the file. -**Limitations** It works only in auto ack mode hence If consumer crashes the message is lost. Local by nature therefor messages are not visible on other servers. +**Limitations** It works only in auto ack mode hence If consumer crashes the message is lost. Local by nature therefor messages are not visible on other servers. * [Installation](#installation) * [Create context](#create-context) @@ -35,10 +43,10 @@ $connectionFactory = new FsConnectionFactory('file:'); $connectionFactory = new FsConnectionFactory('/path/to/queue/dir'); // same as above -$connectionFactory = new FsConnectionFactory('file://path/to/queue/dir'); +$connectionFactory = new FsConnectionFactory('file:///path/to/queue/dir'); // with options -$connectionFactory = new FsConnectionFactory('file://path/to/queue/dir?pre_fetch_count=1'); +$connectionFactory = new FsConnectionFactory('file:///path/to/queue/dir?pre_fetch_count=1'); // as an array $connectionFactory = new FsConnectionFactory([ @@ -46,48 +54,48 @@ $connectionFactory = new FsConnectionFactory([ 'pre_fetch_count' => 1, ]); -$psrContext = $connectionFactory->createContext(); +$context = $connectionFactory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('file:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('file:')->createContext(); ``` ## Send message to topic ```php createTopic('aTopic'); -$message = $psrContext->createMessage('Hello world!'); +$fooTopic = $context->createTopic('aTopic'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createQueue('aQueue'); -$message = $psrContext->createMessage('Hello world!'); +$fooQueue = $context->createQueue('aQueue'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Send expiration message ```php createQueue('aQueue'); -$message = $psrContext->createMessage('Hello world!'); +$fooQueue = $context->createQueue('aQueue'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setTimeToLive(60000) // 60 sec - // + // ->send($fooQueue, $message) ; ``` @@ -96,10 +104,10 @@ $psrContext->createProducer() ```php createQueue('aQueue'); -$consumer = $psrContext->createConsumer($fooQueue); +$fooQueue = $context->createQueue('aQueue'); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); @@ -113,11 +121,11 @@ $consumer->acknowledge($message); ```php createQueue('aQueue'); +$fooQueue = $context->createQueue('aQueue'); -$psrContext->purge($fooQueue); +$context->purge($fooQueue); ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/transport/gearman.md b/docs/transport/gearman.md index 0161048d5..8ed6da021 100644 --- a/docs/transport/gearman.md +++ b/docs/transport/gearman.md @@ -1,7 +1,15 @@ +--- +layout: default +title: Gearman +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # Gearman transport -The transport uses [Gearman](http://gearman.org/) job manager. -The transport uses [Gearman PHP extension](http://php.net/manual/en/book.gearman.php) internally. +The transport uses [Gearman](http://gearman.org/) job manager. +The transport uses [Gearman PHP extension](http://php.net/manual/en/book.gearman.php) internally. * [Installation](#installation) * [Create context](#create-context) @@ -37,48 +45,48 @@ $factory = new GearmanConnectionFactory([ 'port' => 5555 ]); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('gearman:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('gearman:')->createContext(); ``` ## Send message to topic ```php createTopic('aTopic'); -$message = $psrContext->createMessage('Hello world!'); +$fooTopic = $context->createTopic('aTopic'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createQueue('aQueue'); -$message = $psrContext->createMessage('Hello world!'); +$fooQueue = $context->createQueue('aQueue'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Consume message: ```php createQueue('aQueue'); -$consumer = $psrContext->createConsumer($fooQueue); +$fooQueue = $context->createQueue('aQueue'); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(2000); // wait for 2 seconds -$message = $consumer->receiveNoWait(); // fetch message or return null immediately +$message = $consumer->receiveNoWait(); // fetch message or return null immediately // process a message @@ -86,4 +94,4 @@ $consumer->acknowledge($message); // $consumer->reject($message); ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/transport/gps.md b/docs/transport/gps.md index 7d0197f23..b56f5c949 100644 --- a/docs/transport/gps.md +++ b/docs/transport/gps.md @@ -1,7 +1,15 @@ +--- +layout: default +title: GPS +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # Google Pub Sub transport A transport for [Google Pub Sub](https://cloud.google.com/pubsub/docs/) cloud MQ. -It uses internally official google sdk library [google/cloud-pubsub](https://packagist.org/packages/google/cloud-pubsub) +It uses internally official google sdk library [google/cloud-pubsub](https://packagist.org/packages/google/cloud-pubsub) * [Installation](#installation) * [Create context](#create-context) @@ -16,8 +24,8 @@ $ composer require enqueue/gps ## Create context -To enable the Google Cloud Pub/Sub Emulator, set the `PUBSUB_EMULATOR_HOST` environment variable. -There is a handy docker container [google/cloud-sdk](https://hub.docker.com/r/google/cloud-sdk/). +To enable the Google Cloud Pub/Sub Emulator, set the `PUBSUB_EMULATOR_HOST` environment variable. +There is a handy docker container [google/cloud-sdk](https://hub.docker.com/r/google/cloud-sdk/). ```php createContext(); +$context = $connectionFactory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('gps:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('gps:')->createContext(); ``` ## Send message to topic -Before you can send message you have to declare a topic. -The operation creates a topic on a broker side. -Google allows messages to be sent only to topic. +Before you can send message you have to declare a topic. +The operation creates a topic on a broker side. +Google allows messages to be sent only to topic. ```php createTopic('foo'); -$message = $psrContext->createMessage('Hello world!'); +$fooTopic = $context->createTopic('foo'); +$message = $context->createMessage('Hello world!'); -$psrContext->declareTopic($fooTopic); +$context->declareTopic($fooTopic); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` ## Consume message: -Before you can consume message you have to subscribe a queue to the topic. -Google does not allow consuming message from the topic directly. +Before you can consume message you have to subscribe a queue to the topic. +Google does not allow consuming message from the topic directly. ```php createTopic('foo'); -$fooQueue = $psrContext->createQueue('foo'); +$fooTopic = $context->createTopic('foo'); +$fooQueue = $context->createQueue('foo'); -$psrContext->subscribe($fooTopic, $fooQueue); +$context->subscribe($fooTopic, $fooQueue); -$consumer = $psrContext->createConsumer($fooQueue); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); // process a message @@ -77,4 +85,4 @@ $consumer->acknowledge($message); // $consumer->reject($message); ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/transport/index.md b/docs/transport/index.md new file mode 100644 index 000000000..47da348c4 --- /dev/null +++ b/docs/transport/index.md @@ -0,0 +1,11 @@ +--- +layout: default +title: Transports +nav_order: 3 +has_children: true +permalink: /transport +--- + +{:toc} + +[Feature Comparison Table](../client/supported_brokers.md#transport-features) diff --git a/docs/transport/kafka.md b/docs/transport/kafka.md index 77006f9d6..1009034ba 100644 --- a/docs/transport/kafka.md +++ b/docs/transport/kafka.md @@ -1,3 +1,12 @@ +--- +layout: default +title: Kafka +parent: Transports +nav_order: 3 +--- + +{% include support.md %} + # Kafka transport The transport uses [Kafka](https://kafka.apache.org/) streaming platform as a MQ broker. @@ -32,7 +41,7 @@ $connectionFactory = new RdKafkaConnectionFactory('kafka:'); $connectionFactory = new RdKafkaConnectionFactory([]); // connect to Kafka broker at example.com:1000 plus custom options -$connectionFactory = new RdKafkaConnectionFactory([ +$connectionFactory = new RdKafkaConnectionFactory([ 'global' => [ 'group.id' => uniqid('', true), 'metadata.broker.list' => 'example.com:1000', @@ -43,49 +52,49 @@ $connectionFactory = new RdKafkaConnectionFactory([ ], ]); -$psrContext = $connectionFactory->createContext(); +$context = $connectionFactory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('kafka:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('kafka:')->createContext(); ``` -## Send message to topic +## Send message to topic ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$fooTopic = $psrContext->createTopic('foo'); +$fooTopic = $context->createTopic('foo'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$fooQueue = $psrContext->createQueue('foo'); +$fooQueue = $context->createQueue('foo'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Consume message: ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); -$consumer = $psrContext->createConsumer($fooQueue); +$consumer = $context->createConsumer($fooQueue); -// Enable async commit to gain better performance. +// Enable async commit to gain better performance (true by default since version 0.9.9). //$consumer->setCommitAsync(true); $message = $consumer->receive(); @@ -99,7 +108,7 @@ $consumer->acknowledge($message); ## Serialize message By default the transport serializes messages to json format but you might want to use another format such as [Apache Avro](https://avro.apache.org/docs/1.2.0/). -For that you have to implement Serializer interface and set it to the context, producer or consumer. +For that you have to implement Serializer interface and set it to the context, producer or consumer. If a serializer set to context it will be injected to all consumers and producers created by the context. ```php @@ -110,13 +119,13 @@ use Enqueue\RdKafka\RdKafkaMessage; class FooSerializer implements Serializer { public function toMessage($string) {} - + public function toString(RdKafkaMessage $message) {} } -/** @var \Enqueue\RdKafka\RdKafkaContext $psrContext */ +/** @var \Enqueue\RdKafka\RdKafkaContext $context */ -$psrContext->setSerializer(new FooSerializer()); +$context->setSerializer(new FooSerializer()); ``` ## Change offset @@ -126,14 +135,54 @@ There is an ability to change the current offset. ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); -$consumer = $psrContext->createConsumer($fooQueue); +$consumer = $context->createConsumer($fooQueue); $consumer->setOffset(123); $message = $consumer->receive(2000); ``` -[back to index](index.md) \ No newline at end of file +## Usage with Symfony bundle + +Set your enqueue to use rdkafka as your transport + +```yaml +# app/config/config.yml + +enqueue: + default: + transport: "rdkafka:" + client: ~ +``` + +You can also you extended configuration to pass additional options, if you don't want to pass them via DSN string or +need to pass specific options. Since rdkafka uses librdkafka (being basically a wrapper around it) most configuration +options are identical to those found at https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md. + +```yaml +# app/config/config.yml + +enqueue: + default: + transport: + dsn: "rdkafka://" + global: + ### Make sure this is unique for each application / consumer group and does not change + ### Otherwise, Kafka won't be able to track your last offset and will always start according to + ### `auto.offset.reset` setting. + ### See Kafka documentation regarding `group.id` property if you want to know more + group.id: 'foo-app' + metadata.broker.list: 'example.com:1000' + topic: + auto.offset.reset: beginning + ### Commit async is true by default since version 0.9.9. + ### It is suggested to set it to true in earlier versions since otherwise consumers become extremely slow, + ### waiting for offset to be stored on Kafka before continuing. + commit_async: true + client: ~ +``` + +[back to index](index.md) diff --git a/docs/transport/mongodb.md b/docs/transport/mongodb.md index 315bb9bdb..9e97872aa 100644 --- a/docs/transport/mongodb.md +++ b/docs/transport/mongodb.md @@ -1,6 +1,14 @@ +--- +layout: default +title: MongoDB +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # Enqueue Mongodb message queue transport -Allows to use [MongoDB](https://www.mongodb.com/) as a message queue broker. +Allows to use [MongoDB](https://www.mongodb.com/) as a message queue broker. * [Installation](#installation) * [Create context](#create-context) @@ -10,6 +18,7 @@ Allows to use [MongoDB](https://www.mongodb.com/) as a message queue broker. * [Send expiration message](#send-expiration-message) * [Send delayed message](#send-delayed-message) * [Consume message](#consume-message) +* [Subscription consumer](#subscription-consumer) ## Installation @@ -39,49 +48,49 @@ $factory = new MongodbConnectionFactory([ 'polling_interval' => '1000', ]); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('mongodb:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('mongodb:')->createContext(); ``` -## Send message to topic +## Send message to topic ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Send priority message ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); -$message = $psrContext->createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setPriority(5) // the higher priority the sooner a message gets to a consumer - // + // ->send($fooQueue, $message) ; ``` @@ -90,14 +99,14 @@ $psrContext->createProducer() ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setTimeToLive(60000) // 60 sec - // + // ->send($fooQueue, $message) ; ``` @@ -108,28 +117,28 @@ $psrContext->createProducer() createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setDeliveryDelay(5000) // 5 sec - + ->send($fooQueue, $message) ; -```` +```` ## Consume message: ```php createConsumer($fooQueue); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); @@ -139,4 +148,37 @@ $consumer->acknowledge($message); // $consumer->reject($message); ``` +## Subscription consumer + +```php +createConsumer($fooQueue); +$barConsumer = $context->createConsumer($barQueue); + +$subscriptionConsumer = $context->createSubscriptionConsumer(); +$subscriptionConsumer->subscribe($fooConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); +$subscriptionConsumer->subscribe($barConsumer, function(Message $message, Consumer $consumer) { + // process message + + $consumer->acknowledge($message); + + return true; +}); + +$subscriptionConsumer->consume(2000); // 2 sec +``` + [back to index](../index.md) diff --git a/docs/transport/null.md b/docs/transport/null.md index 0a30244ea..aa77b5e72 100644 --- a/docs/transport/null.md +++ b/docs/transport/null.md @@ -1,6 +1,14 @@ +--- +layout: default +title: "Null" +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # NULL transport -This a special transport implementation, kind of stub. +This a special transport implementation, kind of stub. It does not send nor receive anything. Useful in tests for example. @@ -21,7 +29,7 @@ use Enqueue\Null\NullConnectionFactory; $connectionFactory = new NullConnectionFactory(); -$psrContext = $connectionFactory->createContext(); +$context = $connectionFactory->createContext(); ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/transport/pheanstalk.md b/docs/transport/pheanstalk.md index 4371c2966..9d3af572b 100644 --- a/docs/transport/pheanstalk.md +++ b/docs/transport/pheanstalk.md @@ -1,7 +1,15 @@ +--- +layout: default +title: Pheanstalk +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # Beanstalk (Pheanstalk) transport -The transport uses [Beanstalkd](http://kr.github.io/beanstalkd/) job manager. -The transport uses [Pheanstalk](https://github.com/pda/pheanstalk) library internally. +The transport uses [Beanstalkd](http://kr.github.io/beanstalkd/) job manager. +The transport uses [Pheanstalk](https://github.com/pda/pheanstalk) library internally. * [Installation](#installation) * [Create context](#create-context) @@ -37,48 +45,48 @@ $factory = new PheanstalkConnectionFactory([ 'port' => 5555 ]); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('beanstalk:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('beanstalk:')->createContext(); ``` ## Send message to topic ```php createTopic('aTopic'); -$message = $psrContext->createMessage('Hello world!'); +$fooTopic = $context->createTopic('aTopic'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createQueue('aQueue'); -$message = $psrContext->createMessage('Hello world!'); +$fooQueue = $context->createQueue('aQueue'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Consume message: ```php createQueue('aQueue'); -$consumer = $psrContext->createConsumer($fooQueue); +$fooQueue = $context->createQueue('aQueue'); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(2000); // wait for 2 seconds -$message = $consumer->receiveNoWait(); // fetch message or return null immediately +$message = $consumer->receiveNoWait(); // fetch message or return null immediately // process a message @@ -86,4 +94,4 @@ $consumer->acknowledge($message); // $consumer->reject($message); ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/transport/redis.md b/docs/transport/redis.md index 3a1240bd8..c357fd083 100644 --- a/docs/transport/redis.md +++ b/docs/transport/redis.md @@ -1,19 +1,38 @@ +--- +layout: default +title: Redis +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # Redis transport -The transport uses [Redis](https://redis.io/) as a message broker. +The transport uses [Redis](https://redis.io/) as a message broker. It creates a collection (a queue or topic) there. Pushes messages to the tail of the collection and pops from the head. -The transport works with [phpredis](https://github.com/phpredis/phpredis) php extension or [predis](https://github.com/nrk/predis) library. -Make sure you installed either of them - -**Limitations** It works only in auto ack mode hence If consumer crashes the message is lost. - +The transport works with [phpredis](https://github.com/phpredis/phpredis) php extension or [predis](https://github.com/nrk/predis) library. +Make sure you installed either of them + +Features: +* Configure with DSN string +* Delay strategies out of the box +* Recovery&Redelivery support +* Expiration support +* Delaying support +* Interchangeable with other Queue Interop implementations +* Supports Subscription consumer + +Parts: * [Installation](#installation) * [Create context](#create-context) * [Send message to topic](#send-message-to-topic) * [Send message to queue](#send-message-to-queue) +* [Send expiration message](#send-expiration-message) +* [Send delayed message](#send-delayed-message) * [Consume message](#consume-message) * [Delete queue (purge messages)](#delete-queue-purge-messages) * [Delete topic (purge messages)](#delete-topic-purge-messages) +* [Connect Heroku Redis](#connect-heroku-redis) ## Installation @@ -47,20 +66,29 @@ $factory = new RedisConnectionFactory('redis:'); // same as above $factory = new RedisConnectionFactory([]); -// connect to Redis at example.com port 1000 using phpredis extension +// connect to Redis at example.com port 1000 using phpredis extension $factory = new RedisConnectionFactory([ 'host' => 'example.com', 'port' => 1000, - 'vendor' => 'phpredis', + 'scheme_extensions' => ['phpredis'], ]); // same as above but given as DSN string -$factory = new RedisConnectionFactory('redis://example.com:1000?vendor=phpredis'); +$factory = new RedisConnectionFactory('redis+phpredis://example.com:1000'); + +$context = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('redis:')->createContext(); -$psrContext = $factory->createContext(); +// pass redis instance directly +$redis = new \Enqueue\Redis\PhpRedis([ /** redis connection options */ ]); +$redis->connect(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('redis:'); +// Secure\TLS connection. Works only with predis library. Note second "S" in scheme. +$factory = new RedisConnectionFactory('rediss+predis://user:pass@host/0'); + +$factory = new RedisConnectionFactory($redis); ``` * With predis library: @@ -72,13 +100,13 @@ use Enqueue\Redis\RedisConnectionFactory; $connectionFactory = new RedisConnectionFactory([ 'host' => 'localhost', 'port' => 6379, - 'vendor' => 'predis', + 'scheme_extensions' => ['predis'], ]); -$psrContext = $connectionFactory->createContext(); +$context = $connectionFactory->createContext(); ``` -* With custom redis instance: +* With predis and custom [options](https://github.com/nrk/predis/wiki/Client-Options): It gives you more control over vendor specific features. @@ -86,73 +114,126 @@ It gives you more control over vendor specific features. 'localhost', + 'port' => 6379, + 'predis_options' => [ + 'prefix' => 'ns:' + ] +]; + +$redis = new PRedis($config); -$factory = new RedisConnectionFactory(['vendor' => 'custom', 'redis' => $redis]); +$factory = new RedisConnectionFactory($redis); ``` ## Send message to topic ```php createTopic('aTopic'); -$message = $psrContext->createMessage('Hello world!'); +$fooTopic = $context->createTopic('aTopic'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createQueue('aQueue'); -$message = $psrContext->createMessage('Hello world!'); +$fooQueue = $context->createQueue('aQueue'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` +## Send expiration message + +```php +createMessage('Hello world!'); + +$context->createProducer() + ->setTimeToLive(60000) // 60 sec + // + ->send($fooQueue, $message) +; +``` + +## Send delayed message + +```php +createMessage('Hello world!'); + +$context->createProducer() + ->setDeliveryDelay(5000) // 5 sec + + ->send($fooQueue, $message) +; +```` + ## Consume message: ```php createQueue('aQueue'); -$consumer = $psrContext->createConsumer($fooQueue); +$fooQueue = $context->createQueue('aQueue'); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); // process a message + +$consumer->acknowledge($message); +//$consumer->reject($message); ``` ## Delete queue (purge messages): ```php createQueue('aQueue'); +$fooQueue = $context->createQueue('aQueue'); -$psrContext->deleteQueue($fooQueue); +$context->deleteQueue($fooQueue); ``` ## Delete topic (purge messages): ```php createTopic('aTopic'); + +$context->deleteTopic($fooTopic); +``` + +## Connect Heroku Redis + +[Heroku Redis](https://devcenter.heroku.com/articles/heroku-redis) describes how to setup Redis instance on Heroku. +To use it with Enqueue Redis you have to pass REDIS_URL to RedisConnectionFactory constructor. + +```php +createTopic('aTopic'); +// REDIS_URL: redis://h:asdfqwer1234asdf@ec2-111-1-1-1.compute-1.amazonaws.com:111 -$psrContext->deleteTopic($fooTopic); +$connection = new \Enqueue\Redis\RedisConnectionFactory(getenv('REDIS_URL')); ``` -[back to index](../index.md) \ No newline at end of file +[back to index](../index.md) diff --git a/docs/transport/sns.md b/docs/transport/sns.md new file mode 100644 index 000000000..41e2ad04c --- /dev/null +++ b/docs/transport/sns.md @@ -0,0 +1,14 @@ +--- +layout: default +title: Amazon SNS +parent: Transports +nav_order: 3 +--- +{% include support.md %} + +# Amazon SNS transport + +SNS documentation is under construction. +See: [#769](https://github.com/php-enqueue/enqueue-dev/issues/769) + +[back to index](../index.md) diff --git a/docs/transport/snsqs.md b/docs/transport/snsqs.md new file mode 100644 index 000000000..86d929f4e --- /dev/null +++ b/docs/transport/snsqs.md @@ -0,0 +1,181 @@ +--- +layout: default +title: Amazon SNS-SQS +parent: Transports +nav_order: 3 +--- +{% include support.md %} + +# Amazon SNS-SQS transport + +Utilize two Amazon services [SNS-SQS](https://docs.aws.amazon.com/sns/latest/dg/sns-sqs-as-subscriber.html) to +implement [Publish-Subscribe](https://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html) +enterprise integration pattern. As opposed to single SQS transport this adds ability to use [MessageBus](https://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageBus.html) +with enqueue. + +A transport for [Amazon SQS](https://aws.amazon.com/sqs/) broker. +It uses internally official [aws sdk library](https://packagist.org/packages/aws/aws-sdk-php) + +* [Installation](#installation) +* [Create context](#create-context) +* [Declare topic, queue and bind them together](#declare-topic-queue-and-bind-them-together) +* [Send message to topic](#send-message-to-topic) +* [Send message to queue](#send-message-to-queue) +* [Consume message](#consume-message) +* [Purge queue messages](#purge-queue-messages) +* [Queue from another AWS account](#queue-from-another-aws-account) + +## Installation + +```bash +$ composer require enqueue/snsqs +``` + +## Create context + +```php + 'aKey', + 'secret' => 'aSecret', + 'region' => 'aRegion', + + // or you can segregate options using prefixes "sns_", "sqs_" + 'key' => 'aKey', // common option for both SNS and SQS + 'sns_region' => 'aSnsRegion', // SNS transport option + 'sqs_region' => 'aSqsRegion', // SQS transport option +]); + +// same as above but given as DSN string. You may need to url encode secret if it contains special char (like +) +$factory = new SnsQsConnectionFactory('snsqs:?key=aKey&secret=aSecret®ion=aRegion'); + +$context = $factory->createContext(); + +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('snsqs:')->createContext(); +``` + +## Declare topic, queue and bind them together + +Declare topic, queue operation creates a topic, queue on a broker side. +Bind creates connection between topic and queue. You publish message to +the topic and topic sends message to each queue connected to the topic. + + +```php +createTopic('in'); +$context->declareTopic($inTopic); + +$out1Queue = $context->createQueue('out1'); +$context->declareQueue($out1Queue); + +$out2Queue = $context->createQueue('out2'); +$context->declareQueue($out2Queue); + +$context->bind($inTopic, $out1Queue); +$context->bind($inTopic, $out2Queue); + +// to remove topic/queue use deleteTopic/deleteQueue method +//$context->deleteTopic($inTopic); +//$context->deleteQueue($out1Queue); +//$context->unbind(inTopic, $out1Queue); +``` + +## Send message to topic + +```php +createTopic('in'); +$message = $context->createMessage('Hello world!'); + +$context->createProducer()->send($inTopic, $message); +``` + +## Send message to queue + +You can bypass topic and publish message directly to the queue + +```php +createQueue('foo'); +$message = $context->createMessage('Hello world!'); + +$context->createProducer()->send($fooQueue, $message); +``` + + +## Consume message: + +```php +createQueue('out1'); +$consumer = $context->createConsumer($out1Queue); + +$message = $consumer->receive(); + +// process a message + +$consumer->acknowledge($message); +// $consumer->reject($message); +``` + +## Purge queue messages: + +```php +createQueue('foo'); + +$context->purgeQueue($fooQueue); +``` + +## Queue from another AWS account + +SQS allows to use queues from another account. You could set it globally for all queues via option `queue_owner_aws_account_id` or +per queue using `SnsQsQueue::setQueueOwnerAWSAccountId` method. + +```php +createContext(); + +// per queue. +$queue = $context->createQueue('foo'); +$queue->setQueueOwnerAWSAccountId('awsAccountId'); +``` + +## Multi region examples + +Enqueue SNSQS provides a generic multi-region support. This enables users to specify which AWS Region to send a command to by setting region on SnsQsQueue. +If not specified the default region is used. + +```php +createContext(); + +$queue = $context->createQueue('foo'); +$queue->setRegion('us-west-2'); + +// the request goes to US West (Oregon) Region +$context->declareQueue($queue); +``` + +[back to index](../index.md) diff --git a/docs/transport/sqs.md b/docs/transport/sqs.md index 8f447d40d..3ead089e8 100644 --- a/docs/transport/sqs.md +++ b/docs/transport/sqs.md @@ -1,7 +1,15 @@ +--- +layout: default +title: Amazon SQS +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # Amazon SQS transport A transport for [Amazon SQS](https://aws.amazon.com/sqs/) broker. -It uses internally official [aws sdk library](https://packagist.org/packages/aws/aws-sdk-php) +It uses internally official [aws sdk library](https://packagist.org/packages/aws/aws-sdk-php) * [Installation](#installation) * [Create context](#create-context) @@ -10,6 +18,7 @@ It uses internally official [aws sdk library](https://packagist.org/packages/aws * [Send delay message](#send-delay-message) * [Consume message](#consume-message) * [Purge queue messages](#purge-queue-messages) +* [Queue from another AWS account](#queue-from-another-aws-account) ## Installation @@ -22,7 +31,7 @@ $ composer require enqueue/sqs ```php 'aKey', 'secret' => 'aSecret', @@ -32,55 +41,55 @@ $factory = new SqsConnectionFactory([ // same as above but given as DSN string. You may need to url encode secret if it contains special char (like +) $factory = new SqsConnectionFactory('sqs:?key=aKey&secret=aSecret®ion=aRegion'); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); // using a pre-configured client $client = new Aws\Sqs\SqsClient([ /* ... */ ]); $factory = new SqsConnectionFactory($client); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('sqs:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('sqs:')->createContext(); ``` ## Declare queue. -Declare queue operation creates a queue on a broker side. - +Declare queue operation creates a queue on a broker side. + ```php createQueue('foo'); -$psrContext->declareQueue($fooQueue); +$fooQueue = $context->createQueue('foo'); +$context->declareQueue($fooQueue); // to remove queue use deleteQueue method -//$psrContext->deleteQueue($fooQueue); +//$context->deleteQueue($fooQueue); ``` -## Send message to queue +## Send message to queue ```php createQueue('foo'); -$message = $psrContext->createMessage('Hello world!'); +$fooQueue = $context->createQueue('foo'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Send delay message ```php createQueue('foo'); -$message = $psrContext->createMessage('Hello world!'); +$fooQueue = $context->createQueue('foo'); +$message = $context->createMessage('Hello world!'); -$psrContext->createProducer() +$context->createProducer() ->setDeliveryDelay(60000) // 60 sec - + ->send($fooQueue, $message) ; ``` @@ -89,10 +98,10 @@ $psrContext->createProducer() ```php createQueue('foo'); -$consumer = $psrContext->createConsumer($fooQueue); +$fooQueue = $context->createQueue('foo'); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); @@ -106,11 +115,49 @@ $consumer->acknowledge($message); ```php createQueue('foo'); + +$context->purgeQueue($fooQueue); +``` + +## Queue from another AWS account + +SQS allows to use queues from another account. You could set it globally for all queues via option `queue_owner_aws_account_id` or +per queue using `SqsDestination::setQueueOwnerAWSAccountId` method. + +```php +createContext(); + +// per queue. +$queue = $context->createQueue('foo'); +$queue->setQueueOwnerAWSAccountId('awsAccountId'); +``` + +## Multi region examples + +Enqueue SQS provides a generic multi-region support. This enables users to specify which AWS Region to send a command to by setting region on SqsDestination. +You might need it to access SQS FIFO queue because they are not available for all regions. +If not specified the default region is used. + +```php +createContext(); -$fooQueue = $psrContext->createQueue('foo'); +$queue = $context->createQueue('foo'); +$queue->setRegion('us-west-2'); -$psrContext->purge($fooQueue); +// the request goes to US West (Oregon) Region +$context->declareQueue($queue); ``` [back to index](../index.md) diff --git a/docs/transport/stomp.md b/docs/transport/stomp.md index fb9ea7694..053765d67 100644 --- a/docs/transport/stomp.md +++ b/docs/transport/stomp.md @@ -1,3 +1,11 @@ +--- +layout: default +title: STOMP +parent: Transports +nav_order: 3 +--- +{% include support.md %} + # STOMP transport * [Installation](#installation) @@ -27,7 +35,13 @@ $factory = new StompConnectionFactory('stomp:'); // same as above $factory = new StompConnectionFactory([]); -// connect to stomp broker at example.com port 1000 using +// connect via stomp to RabbitMQ (default) - the topic names are prefixed with /exchange +$factory = new StompConnectionFactory('stomp+rabbitmq:'); + +// connect via stomp to ActiveMQ - the topic names are prefixed with /topic +$factory = new StompConnectionFactory('stomp+activemq:'); + +// connect to stomp broker at example.com port 1000 using $factory = new StompConnectionFactory([ 'host' => 'example.com', 'port' => 1000, @@ -37,47 +51,47 @@ $factory = new StompConnectionFactory([ // same as above but given as DSN string $factory = new StompConnectionFactory('stomp://example.com:1000?login=theLogin'); -$psrContext = $factory->createContext(); +$context = $factory->createContext(); -// if you have enqueue/enqueue library installed you can use a function from there to create the context -$psrContext = \Enqueue\dsn_to_context('stomp:'); +// if you have enqueue/enqueue library installed you can use a factory to build context from DSN +$context = (new \Enqueue\ConnectionFactoryFactory())->create('stomp:')->createContext(); ``` -## Send message to topic +## Send message to topic ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$fooTopic = $psrContext->createTopic('foo'); +$fooTopic = $context->createTopic('foo'); -$psrContext->createProducer()->send($fooTopic, $message); +$context->createProducer()->send($fooTopic, $message); ``` -## Send message to queue +## Send message to queue ```php createMessage('Hello world!'); +$message = $context->createMessage('Hello world!'); -$fooQueue = $psrContext->createQueue('foo'); +$fooQueue = $context->createQueue('foo'); -$psrContext->createProducer()->send($fooQueue, $message); +$context->createProducer()->send($fooQueue, $message); ``` ## Consume message: ```php createQueue('foo'); +$fooQueue = $context->createQueue('foo'); -$consumer = $psrContext->createConsumer($fooQueue); +$consumer = $context->createConsumer($fooQueue); $message = $consumer->receive(); @@ -87,4 +101,4 @@ $consumer->acknowledge($message); // $consumer->reject($message); ``` -[back to index](index.md) \ No newline at end of file +[back to index](index.md) diff --git a/docs/transport/wamp.md b/docs/transport/wamp.md new file mode 100644 index 000000000..9187fc5e8 --- /dev/null +++ b/docs/transport/wamp.md @@ -0,0 +1,116 @@ +--- +layout: default +title: WAMP +parent: Transports +nav_order: 3 +--- +{% include support.md %} + +# Web Application Messaging Protocol (WAMP) Transport + +A transport for [Web Application Messaging Protocol](https://wamp-proto.org/). +WAMP is an open standard WebSocket subprotocol. +It uses internally Thruway PHP library [thruway/client](https://github.com/thruway/client) + +* [Installation](#installation) +* [Start the WAMP router](#start-the-wamp-router) +* [Create context](#create-context) +* [Consume message](#consume-message) +* [Subscription consumer](#subscription-consumer) +* [Send message to topic](#send-message-to-topic) + +## Installation + +```bash +$ composer require enqueue/wamp +``` + +## Start the WAMP router + +You can get a WAMP router with [Thruway](https://github.com/voryx/Thruway): + +```bash +$ composer require voryx/thruway +$ php vendor/voryx/thruway/Examples/SimpleWsRouter.php +``` + +Thruway is now running on 127.0.0.1 port 9090 + + +## Create context + +```php +createContext(); +``` + +## Consume message: + +Start message consumer before send message to the topic + +```php +createTopic('foo'); + +$consumer = $context->createConsumer($fooQueue); + +while (true) { + if ($message = $consumer->receive()) { + // process a message + } +} +``` + +## Subscription consumer + +```php +createConsumer($fooQueue); +$barConsumer = $context->createConsumer($barQueue); + +$subscriptionConsumer = $context->createSubscriptionConsumer(); +$subscriptionConsumer->subscribe($fooConsumer, function(Message $message, Consumer $consumer) { + // process message + + return true; +}); +$subscriptionConsumer->subscribe($barConsumer, function(Message $message, Consumer $consumer) { + // process message + + return true; +}); + +$subscriptionConsumer->consume(2000); // 2 sec +``` + +## Send message to topic + +```php +createTopic('foo'); +$message = $context->createMessage('Hello world!'); + +$context->createProducer()->send($fooTopic, $message); +``` + +[back to index](../index.md) diff --git a/docs/yii/amqp_driver.md b/docs/yii/amqp_driver.md index b4a59baf4..49798b331 100644 --- a/docs/yii/amqp_driver.md +++ b/docs/yii/amqp_driver.md @@ -1,3 +1,11 @@ +--- +layout: default +parent: Yii +title: AMQP Interop driver +nav_order: 1 +--- +{% include support.md %} + # Yii2Queue. AMQP Interop driver _**Note:** This a copy of [AMQP Interop doc](https://github.com/yiisoft/yii2-queue/blob/master/docs/guide/driver-amqp-interop.md) from the yiisoft/yii2-queue [repository](https://github.com/yiisoft/yii2-queue)._ @@ -9,12 +17,12 @@ In order for it to work you should add any [amqp interop](https://github.com/que Advantages: -* It would work with any amqp interop compatible transports, such as +* It would work with any amqp interop compatible transports, such as * [enqueue/amqp-ext](https://github.com/php-enqueue/amqp-ext) based on [PHP amqp extension](https://github.com/pdezwart/php-amqp) * [enqueue/amqp-lib](https://github.com/php-enqueue/amqp-lib) based on [php-amqplib/php-amqplib](https://github.com/php-amqplib/php-amqplib) * [enqueue/amqp-bunny](https://github.com/php-enqueue/amqp-bunny) based on [bunny](https://github.com/jakubkulhan/bunny) - + * Supports priorities * Supports delays * Supports ttr @@ -38,10 +46,10 @@ return [ 'password' => 'guest', 'queueName' => 'queue', 'driver' => yii\queue\amqp_interop\Queue::ENQUEUE_AMQP_LIB, - + // or 'dsn' => 'amqp://guest:guest@localhost:5672/%2F', - + // or, same as above 'dsn' => 'amqp:', ], diff --git a/docs/yii/index.md b/docs/yii/index.md new file mode 100644 index 000000000..943059db5 --- /dev/null +++ b/docs/yii/index.md @@ -0,0 +1,9 @@ +--- +layout: default +title: Yii +has_children: true +nav_order: 9 +permalink: /yii +--- + +{:toc} diff --git a/phpstan.neon b/phpstan.neon index ecb473cfd..b689b6b8a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,11 +1,16 @@ parameters: - excludes_analyse: + excludePaths: + - docs + - bin + - docker + - var + - pkg/amqp-lib/tutorial + - pkg/enqueue-bundle/Tests/Functional/App/AsyncListener.php - pkg/enqueue/Util/UUID.php - - pkg/enqueue-bundle/Tests/Functional/App - pkg/job-queue/Test/JobRunner.php - pkg/job-queue/Tests/Functional/app/AppKernel.php - - pkg/redis/PhpRedis.php - - pkg/redis/RedisConnectionFactory.php - - pkg/gearman - - pkg/amqp-ext/AmqpConsumer.php - - pkg/amqp-ext/AmqpContext.php \ No newline at end of file + - pkg/rdkafka/RdKafkaConsumer.php + - pkg/async-event-dispatcher/DependencyInjection/Configuration.php + - pkg/enqueue-bundle/DependencyInjection/Configuration.php + - pkg/enqueue/Tests/Symfony/DependencyInjection/TransportFactoryTest.php + - pkg/simple-client/SimpleClient.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 61c7430cb..f5ba01d8f 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,16 +1,19 @@ - + @@ -61,6 +64,10 @@ pkg/sqs/Tests + + pkg/sns/Tests + + pkg/pheanstalk/Tests @@ -96,14 +103,38 @@ pkg/async-event-dispatcher/Tests + + + pkg/async-command/Tests + + + + pkg/dsn/Tests + + + + pkg/wamp/Tests + + + + pkg/monitoring/Tests + + + + pkg/snsqs/Tests + - - + + + + + + . - - ./vendor - - - + + + ./vendor + + diff --git a/pkg/amqp-bunny/.github/workflows/ci.yml b/pkg/amqp-bunny/.github/workflows/ci.yml new file mode 100644 index 000000000..5448d7b1a --- /dev/null +++ b/pkg/amqp-bunny/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/amqp-bunny/.travis.yml b/pkg/amqp-bunny/.travis.yml deleted file mode 100644 index 09a25c9e0..000000000 --- a/pkg/amqp-bunny/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/amqp-bunny/AmqpConnectionFactory.php b/pkg/amqp-bunny/AmqpConnectionFactory.php index c5c567c40..749e63be0 100644 --- a/pkg/amqp-bunny/AmqpConnectionFactory.php +++ b/pkg/amqp-bunny/AmqpConnectionFactory.php @@ -1,11 +1,15 @@ config = (new ConnectionConfig($config)) ->addSupportedScheme('amqp+bunny') - ->addDefaultOption('receive_method', 'basic_get') ->addDefaultOption('tcp_nodelay', null) ->parse() ; - $supportedMethods = ['basic_get', 'basic_consume']; - if (false == in_array($this->config->getOption('receive_method'), $supportedMethods, true)) { - throw new \LogicException(sprintf( - 'Invalid "receive_method" option value "%s". It could be only "%s"', - $this->config->getOption('receive_method'), - implode('", "', $supportedMethods) - )); + if (in_array('rabbitmq', $this->config->getSchemeExtensions(), true)) { + $this->setDelayStrategy(new RabbitMqDlxDelayStrategy()); } } /** * @return AmqpContext */ - public function createContext() + public function createContext(): Context { if ($this->config->isLazy()) { $context = new AmqpContext(function () { @@ -72,18 +67,12 @@ public function createContext() return $context; } - /** - * @return ConnectionConfig - */ - public function getConfig() + public function getConfig(): ConnectionConfig { return $this->config; } - /** - * @return BunnyClient - */ - private function establishConnection() + private function establishConnection(): BunnyClient { if ($this->config->isSslOn()) { throw new \LogicException('The bunny library does not support SSL connections'); @@ -100,10 +89,10 @@ private function establishConnection() $bunnyConfig['timeout'] = $this->config->getConnectionTimeout(); // @see https://github.com/php-enqueue/enqueue-dev/issues/229 -// $bunnyConfig['persistent'] = $this->config->isPersisted(); -// if ($this->config->isPersisted()) { -// $bunnyConfig['path'] = 'enqueue';//$this->config->getOption('path', $this->config->getOption('vhost')); -// } + // $bunnyConfig['persistent'] = $this->config->isPersisted(); + // if ($this->config->isPersisted()) { + // $bunnyConfig['path'] = 'enqueue';//$this->config->getOption('path', $this->config->getOption('vhost')); + // } if ($this->config->getHeartbeat()) { $bunnyConfig['heartbeat'] = $this->config->getHeartbeat(); diff --git a/pkg/amqp-bunny/AmqpConsumer.php b/pkg/amqp-bunny/AmqpConsumer.php index 0bfd0b94a..89301c80c 100644 --- a/pkg/amqp-bunny/AmqpConsumer.php +++ b/pkg/amqp-bunny/AmqpConsumer.php @@ -1,14 +1,17 @@ context = $context; $this->channel = $context->getBunnyChannel(); $this->queue = $queue; - $this->buffer = $buffer; - $this->receiveMethod = $receiveMethod; $this->flags = self::FLAG_NOPARAM; } - /** - * {@inheritdoc} - */ - public function setConsumerTag($consumerTag) + public function setConsumerTag(?string $consumerTag = null): void { $this->consumerTag = $consumerTag; } - /** - * {@inheritdoc} - */ - public function getConsumerTag() + public function getConsumerTag(): ?string { return $this->consumerTag; } - /** - * {@inheritdoc} - */ - public function clearFlags() + public function clearFlags(): void { $this->flags = self::FLAG_NOPARAM; } - /** - * {@inheritdoc} - */ - public function addFlag($flag) + public function addFlag(int $flag): void { $this->flags |= $flag; } - /** - * {@inheritdoc} - */ - public function getFlags() + public function getFlags(): int { return $this->flags; } - /** - * {@inheritdoc} - */ - public function setFlags($flags) + public function setFlags(int $flags): void { $this->flags = $flags; } /** - * {@inheritdoc} + * @return InteropAmqpQueue */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } /** - * {@inheritdoc} + * @return InteropAmqpMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { - if ('basic_get' == $this->receiveMethod) { - return $this->receiveBasicGet($timeout); - } + $end = microtime(true) + ($timeout / 1000); - if ('basic_consume' == $this->receiveMethod) { - return $this->receiveBasicConsume($timeout); + while (0 === $timeout || microtime(true) < $end) { + if ($message = $this->receiveNoWait()) { + return $message; + } + + usleep(100000); // 100ms } - throw new \LogicException('The "receiveMethod" is not supported'); + return null; } /** - * {@inheritdoc} + * @return InteropAmqpMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { if ($message = $this->channel->get($this->queue->getQueueName(), (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOACK))) { return $this->context->convertMessage($message); } + + return null; } /** * @param InteropAmqpMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); - $bunnyMessage = new Message('', $message->getDeliveryTag(), '', '', '', [], ''); + $bunnyMessage = new BunnyMessage('', $message->getDeliveryTag(), '', '', '', [], ''); $this->channel->ack($bunnyMessage); } /** * @param InteropAmqpMessage $message - * @param bool $requeue */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); - $bunnyMessage = new Message('', $message->getDeliveryTag(), '', '', '', [], ''); + $bunnyMessage = new BunnyMessage('', $message->getDeliveryTag(), '', '', '', [], ''); $this->channel->reject($bunnyMessage, $requeue); } - - /** - * @param int $timeout - * - * @return InteropAmqpMessage|null - */ - private function receiveBasicGet($timeout) - { - $end = microtime(true) + ($timeout / 1000); - - while (0 === $timeout || microtime(true) < $end) { - if ($message = $this->receiveNoWait()) { - return $message; - } - - usleep(100000); //100ms - } - } - - /** - * @param int $timeout - * - * @return InteropAmqpMessage|null - */ - private function receiveBasicConsume($timeout) - { - if (false == $this->consumerTag) { - $this->context->subscribe($this, function (InteropAmqpMessage $message) { - $this->buffer->push($message->getConsumerTag(), $message); - - return false; - }); - } - - if ($message = $this->buffer->pop($this->consumerTag)) { - return $message; - } - - while (true) { - $start = microtime(true); - - $this->context->consume($timeout); - - if ($message = $this->buffer->pop($this->consumerTag)) { - return $message; - } - - // is here when consumed message is not for this consumer - - // as timeout is infinite have to continue consumption, but it can overflow message buffer - if ($timeout <= 0) { - continue; - } - - // compute remaining timeout and continue until time is up - $stop = microtime(true); - $timeout -= ($stop - $start) * 1000; - - if ($timeout <= 0) { - break; - } - } - } } diff --git a/pkg/amqp-bunny/AmqpContext.php b/pkg/amqp-bunny/AmqpContext.php index d0730d6b5..151cbd842 100644 --- a/pkg/amqp-bunny/AmqpContext.php +++ b/pkg/amqp-bunny/AmqpContext.php @@ -1,18 +1,16 @@ config = array_replace([ - 'receive_method' => 'basic_get', 'qos_prefetch_size' => 0, 'qos_prefetch_count' => 1, 'qos_global' => false, @@ -79,51 +67,40 @@ public function __construct($bunnyChannel, $config = []) } else { throw new \InvalidArgumentException('The bunnyChannel argument must be either \Bunny\Channel or callable that return it.'); } - - $this->buffer = new Buffer(); - $this->subscribers = []; } /** - * @param string|null $body - * @param array $properties - * @param array $headers - * * @return InteropAmqpMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new AmqpMessage($body, $properties, $headers); } /** - * @param string $name - * * @return InteropAmqpQueue */ - public function createQueue($name) + public function createQueue(string $name): Queue { return new AmqpQueue($name); } /** - * @param string $name - * * @return InteropAmqpTopic */ - public function createTopic($name) + public function createTopic(string $name): Topic { return new AmqpTopic($name); } /** - * @param PsrDestination $destination + * @param AmqpDestination $destination * * @return AmqpConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { - $destination instanceof PsrTopic + $destination instanceof Topic ? InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpTopic::class) : InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpQueue::class) ; @@ -132,24 +109,24 @@ public function createConsumer(PsrDestination $destination) $queue = $this->createTemporaryQueue(); $this->bind(new AmqpBind($destination, $queue, $queue->getQueueName())); - return new AmqpConsumer($this, $queue, $this->buffer, $this->config['receive_method']); + return new AmqpConsumer($this, $queue); } - return new AmqpConsumer($this, $destination, $this->buffer, $this->config['receive_method']); + return new AmqpConsumer($this, $destination); } /** - * {@inheritdoc} + * @return AmqpSubscriptionConsumer */ - public function createSubscriptionConsumer() + public function createSubscriptionConsumer(): SubscriptionConsumer { - return new SubscriptionConsumer($this); + return new AmqpSubscriptionConsumer($this); } /** * @return AmqpProducer */ - public function createProducer() + public function createProducer(): Producer { $producer = new AmqpProducer($this->getBunnyChannel(), $this); $producer->setDelayStrategy($this->delayStrategy); @@ -160,7 +137,7 @@ public function createProducer() /** * @return InteropAmqpQueue */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { $frame = $this->getBunnyChannel()->queueDeclare('', false, false, true, false); @@ -170,10 +147,7 @@ public function createTemporaryQueue() return $queue; } - /** - * {@inheritdoc} - */ - public function declareTopic(InteropAmqpTopic $topic) + public function declareTopic(InteropAmqpTopic $topic): void { $this->getBunnyChannel()->exchangeDeclare( $topic->getTopicName(), @@ -187,10 +161,7 @@ public function declareTopic(InteropAmqpTopic $topic) ); } - /** - * {@inheritdoc} - */ - public function deleteTopic(InteropAmqpTopic $topic) + public function deleteTopic(InteropAmqpTopic $topic): void { $this->getBunnyChannel()->exchangeDelete( $topic->getTopicName(), @@ -199,10 +170,7 @@ public function deleteTopic(InteropAmqpTopic $topic) ); } - /** - * {@inheritdoc} - */ - public function declareQueue(InteropAmqpQueue $queue) + public function declareQueue(InteropAmqpQueue $queue): int { $frame = $this->getBunnyChannel()->queueDeclare( $queue->getQueueName(), @@ -217,10 +185,7 @@ public function declareQueue(InteropAmqpQueue $queue) return $frame->messageCount; } - /** - * {@inheritdoc} - */ - public function deleteQueue(InteropAmqpQueue $queue) + public function deleteQueue(InteropAmqpQueue $queue): void { $this->getBunnyChannel()->queueDelete( $queue->getQueueName(), @@ -231,9 +196,9 @@ public function deleteQueue(InteropAmqpQueue $queue) } /** - * {@inheritdoc} + * @param InteropAmqpQueue $queue */ - public function purgeQueue(InteropAmqpQueue $queue) + public function purgeQueue(Queue $queue): void { $this->getBunnyChannel()->queuePurge( $queue->getQueueName(), @@ -241,10 +206,7 @@ public function purgeQueue(InteropAmqpQueue $queue) ); } - /** - * {@inheritdoc} - */ - public function bind(InteropAmqpBind $bind) + public function bind(InteropAmqpBind $bind): void { if ($bind->getSource() instanceof InteropAmqpQueue && $bind->getTarget() instanceof InteropAmqpQueue) { throw new Exception('Is not possible to bind queue to queue. It is possible to bind topic to queue or topic to topic'); @@ -280,10 +242,7 @@ public function bind(InteropAmqpBind $bind) } } - /** - * {@inheritdoc} - */ - public function unbind(InteropAmqpBind $bind) + public function unbind(InteropAmqpBind $bind): void { if ($bind->getSource() instanceof InteropAmqpQueue && $bind->getTarget() instanceof InteropAmqpQueue) { throw new Exception('Is not possible to bind queue to queue. It is possible to bind topic to queue or topic to topic'); @@ -317,123 +276,24 @@ public function unbind(InteropAmqpBind $bind) } } - public function close() + public function close(): void { if ($this->bunnyChannel) { $this->bunnyChannel->close(); } } - /** - * {@inheritdoc} - */ - public function setQos($prefetchSize, $prefetchCount, $global) + public function setQos(int $prefetchSize, int $prefetchCount, bool $global): void { $this->getBunnyChannel()->qos($prefetchSize, $prefetchCount, $global); } - /** - * @deprecated since 0.8.34 will be removed in 0.9 - * - * {@inheritdoc} - */ - public function subscribe(InteropAmqpConsumer $consumer, callable $callback) - { - if ($consumer->getConsumerTag() && array_key_exists($consumer->getConsumerTag(), $this->subscribers)) { - return; - } - - $bunnyCallback = function (Message $message, Channel $channel, Client $bunny) { - $receivedMessage = $this->convertMessage($message); - $receivedMessage->setConsumerTag($message->consumerTag); - - /** - * @var AmqpConsumer - * @var callable $callback - */ - list($consumer, $callback) = $this->subscribers[$message->consumerTag]; - - if (false === call_user_func($callback, $receivedMessage, $consumer)) { - $bunny->stop(); - } - }; - - $frame = $this->getBunnyChannel()->consume( - $bunnyCallback, - $consumer->getQueue()->getQueueName(), - $consumer->getConsumerTag(), - (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOLOCAL), - (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOACK), - (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_EXCLUSIVE), - (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOWAIT) - ); - - if (empty($frame->consumerTag)) { - throw new Exception('Got empty consumer tag'); - } - - $consumer->setConsumerTag($frame->consumerTag); - - $this->subscribers[$frame->consumerTag] = [$consumer, $callback]; - } - - /** - * @deprecated since 0.8.34 will be removed in 0.9 - * - * {@inheritdoc} - */ - public function unsubscribe(InteropAmqpConsumer $consumer) - { - if (false == $consumer->getConsumerTag()) { - return; - } - - $consumerTag = $consumer->getConsumerTag(); - - $this->getBunnyChannel()->cancel($consumerTag); - $consumer->setConsumerTag(null); - unset($this->subscribers[$consumerTag]); - } - - /** - * @deprecated since 0.8.34 will be removed in 0.9 - * - * {@inheritdoc} - */ - public function consume($timeout = 0) - { - if (empty($this->subscribers)) { - throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); - } - - $signalHandler = new SignalSocketHelper(); - $signalHandler->beforeSocket(); - - try { - $this->getBunnyChannel()->getClient()->run(0 !== $timeout ? $timeout / 1000 : null); - } catch (ClientException $e) { - if ('stream_select() failed.' == $e->getMessage() && $signalHandler->wasThereSignal()) { - return; - } - - throw $e; - } finally { - $signalHandler->afterSocket(); - } - } - - /** - * @return Channel - */ - public function getBunnyChannel() + public function getBunnyChannel(): Channel { if (false == $this->bunnyChannel) { $bunnyChannel = call_user_func($this->bunnyChannelFactory); if (false == $bunnyChannel instanceof Channel) { - throw new \LogicException(sprintf( - 'The factory must return instance of \Bunny\Channel. It returned %s', - is_object($bunnyChannel) ? get_class($bunnyChannel) : gettype($bunnyChannel) - )); + throw new \LogicException(sprintf('The factory must return instance of \Bunny\Channel. It returned %s', is_object($bunnyChannel) ? $bunnyChannel::class : gettype($bunnyChannel))); } $this->bunnyChannel = $bunnyChannel; @@ -444,14 +304,11 @@ public function getBunnyChannel() /** * @internal It must be used here and in the consumer only - * - * @param Message $bunnyMessage - * - * @return InteropAmqpMessage */ - public function convertMessage(Message $bunnyMessage) + public function convertMessage(BunnyMessage $bunnyMessage): InteropAmqpMessage { $headers = $bunnyMessage->headers; + $headers = $this->convertHeadersFromBunnyNotation($headers); $properties = []; if (isset($headers['application_headers'])) { @@ -467,10 +324,112 @@ public function convertMessage(Message $bunnyMessage) } $message = new AmqpMessage($bunnyMessage->content, $properties, $headers); - $message->setDeliveryTag($bunnyMessage->deliveryTag); + $message->setDeliveryTag((int) $bunnyMessage->deliveryTag); $message->setRedelivered($bunnyMessage->redelivered); $message->setRoutingKey($bunnyMessage->routingKey); return $message; } + + /** @internal It must be used here and in the producer only */ + public function convertHeadersToBunnyNotation(array $headers): array + { + if (isset($headers['content_type'])) { + $headers['content-type'] = $headers['content_type']; + unset($headers['content_type']); + } + + if (isset($headers['content_encoding'])) { + $headers['content-encoding'] = $headers['content_encoding']; + unset($headers['content_encoding']); + } + + if (isset($headers['delivery_mode'])) { + $headers['delivery-mode'] = $headers['delivery_mode']; + unset($headers['delivery_mode']); + } + + if (isset($headers['correlation_id'])) { + $headers['correlation-id'] = $headers['correlation_id']; + unset($headers['correlation_id']); + } + + if (isset($headers['reply_to'])) { + $headers['reply-to'] = $headers['reply_to']; + unset($headers['reply_to']); + } + + if (isset($headers['message_id'])) { + $headers['message-id'] = $headers['message_id']; + unset($headers['message_id']); + } + + if (isset($headers['user_id'])) { + $headers['user-id'] = $headers['user_id']; + unset($headers['user_id']); + } + + if (isset($headers['app_id'])) { + $headers['app-id'] = $headers['app_id']; + unset($headers['app_id']); + } + + if (isset($headers['cluster_id'])) { + $headers['cluster-id'] = $headers['cluster_id']; + unset($headers['cluster_id']); + } + + return $headers; + } + + /** @internal It must be used here and in the consumer only */ + public function convertHeadersFromBunnyNotation(array $bunnyHeaders): array + { + if (isset($bunnyHeaders['content-type'])) { + $bunnyHeaders['content_type'] = $bunnyHeaders['content-type']; + unset($bunnyHeaders['content-type']); + } + + if (isset($bunnyHeaders['content-encoding'])) { + $bunnyHeaders['content_encoding'] = $bunnyHeaders['content-encoding']; + unset($bunnyHeaders['content-encoding']); + } + + if (isset($bunnyHeaders['delivery-mode'])) { + $bunnyHeaders['delivery_mode'] = $bunnyHeaders['delivery-mode']; + unset($bunnyHeaders['delivery-mode']); + } + + if (isset($bunnyHeaders['correlation-id'])) { + $bunnyHeaders['correlation_id'] = $bunnyHeaders['correlation-id']; + unset($bunnyHeaders['correlation-id']); + } + + if (isset($bunnyHeaders['reply-to'])) { + $bunnyHeaders['reply_to'] = $bunnyHeaders['reply-to']; + unset($bunnyHeaders['reply-to']); + } + + if (isset($bunnyHeaders['message-id'])) { + $bunnyHeaders['message_id'] = $bunnyHeaders['message-id']; + unset($bunnyHeaders['message-id']); + } + + if (isset($bunnyHeaders['user-id'])) { + $bunnyHeaders['user_id'] = $bunnyHeaders['user-id']; + unset($bunnyHeaders['user-id']); + } + + if (isset($bunnyHeaders['app-id'])) { + $bunnyHeaders['app_id'] = $bunnyHeaders['app-id']; + unset($bunnyHeaders['app-id']); + } + + if (isset($bunnyHeaders['cluster-id'])) { + $bunnyHeaders['cluster_id'] = $bunnyHeaders['cluster-id']; + unset($bunnyHeaders['cluster-id']); + } + + return $bunnyHeaders; + } } diff --git a/pkg/amqp-bunny/AmqpProducer.php b/pkg/amqp-bunny/AmqpProducer.php index 8c8dc6463..178ff81a8 100644 --- a/pkg/amqp-bunny/AmqpProducer.php +++ b/pkg/amqp-bunny/AmqpProducer.php @@ -1,5 +1,7 @@ channel = $channel; @@ -58,14 +57,12 @@ public function __construct(Channel $channel, AmqpContext $context) } /** - * {@inheritdoc} - * * @param InteropAmqpTopic|InteropAmqpQueue $destination * @param InteropAmqpMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { - $destination instanceof PsrTopic + $destination instanceof Topic ? InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpTopic::class) : InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpQueue::class) ; @@ -80,9 +77,9 @@ public function send(PsrDestination $destination, PsrMessage $message) } /** - * {@inheritdoc} + * @return self */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { if (null === $this->delayStrategy) { throw DeliveryDelayNotSupportedException::providerDoestNotSupportIt(); @@ -93,51 +90,42 @@ public function setDeliveryDelay($deliveryDelay) return $this; } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return $this->deliveryDelay; } /** - * {@inheritdoc} + * @return self */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { $this->priority = $priority; return $this; } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return $this->priority; } /** - * {@inheritdoc} + * @return self */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { $this->timeToLive = $timeToLive; return $this; } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return $this->timeToLive; } - private function doSend(InteropAmqpDestination $destination, InteropAmqpMessage $message) + private function doSend(InteropAmqpDestination $destination, InteropAmqpMessage $message): void { if (null !== $this->priority && null === $message->getPriority()) { $message->setPriority($this->priority); @@ -148,9 +136,10 @@ private function doSend(InteropAmqpDestination $destination, InteropAmqpMessage } $amqpProperties = $message->getHeaders(); + $amqpProperties = $this->context->convertHeadersToBunnyNotation($amqpProperties); if (array_key_exists('timestamp', $amqpProperties) && null !== $amqpProperties['timestamp']) { - $amqpProperties['timestamp'] = \DateTime::createFromFormat('U', $amqpProperties['timestamp']); + $amqpProperties['timestamp'] = \DateTime::createFromFormat('U', (string) $amqpProperties['timestamp']); } if ($appProperties = $message->getProperties()) { diff --git a/pkg/amqp-bunny/AmqpSubscriptionConsumer.php b/pkg/amqp-bunny/AmqpSubscriptionConsumer.php new file mode 100644 index 000000000..2904c1c19 --- /dev/null +++ b/pkg/amqp-bunny/AmqpSubscriptionConsumer.php @@ -0,0 +1,133 @@ +context = $context; + + $this->subscribers = []; + } + + public function consume(int $timeout = 0): void + { + if (empty($this->subscribers)) { + throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); + } + + $signalHandler = new SignalSocketHelper(); + $signalHandler->beforeSocket(); + + try { + $this->context->getBunnyChannel()->getClient()->run(0 !== $timeout ? $timeout / 1000 : null); + } catch (ClientException $e) { + if (str_starts_with($e->getMessage(), 'stream_select() failed') && $signalHandler->wasThereSignal()) { + return; + } + + throw $e; + } finally { + $signalHandler->afterSocket(); + } + } + + /** + * @param AmqpConsumer $consumer + */ + public function subscribe(Consumer $consumer, callable $callback): void + { + if (false == $consumer instanceof AmqpConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', AmqpConsumer::class, $consumer::class)); + } + + if ($consumer->getConsumerTag() && array_key_exists($consumer->getConsumerTag(), $this->subscribers)) { + return; + } + + $bunnyCallback = function (Message $message, Channel $channel, Client $bunny) { + $receivedMessage = $this->context->convertMessage($message); + $receivedMessage->setConsumerTag($message->consumerTag); + + /** + * @var AmqpConsumer + * @var callable $callback + */ + list($consumer, $callback) = $this->subscribers[$message->consumerTag]; + + if (false === call_user_func($callback, $receivedMessage, $consumer)) { + $bunny->stop(); + } + }; + + $frame = $this->context->getBunnyChannel()->consume( + $bunnyCallback, + $consumer->getQueue()->getQueueName(), + $consumer->getConsumerTag() ?? '', + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOLOCAL), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOACK), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_EXCLUSIVE), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOWAIT) + ); + + if (empty($frame->consumerTag)) { + throw new Exception('Got empty consumer tag'); + } + + $consumer->setConsumerTag($frame->consumerTag); + + $this->subscribers[$frame->consumerTag] = [$consumer, $callback]; + } + + /** + * @param AmqpConsumer $consumer + */ + public function unsubscribe(Consumer $consumer): void + { + if (false == $consumer instanceof AmqpConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', AmqpConsumer::class, $consumer::class)); + } + + if (false == $consumer->getConsumerTag()) { + return; + } + + $consumerTag = $consumer->getConsumerTag(); + + $this->context->getBunnyChannel()->cancel($consumerTag); + $consumer->setConsumerTag(null); + unset($this->subscribers[$consumerTag]); + } + + public function unsubscribeAll(): void + { + foreach ($this->subscribers as list($consumer)) { + $this->unsubscribe($consumer); + } + } +} diff --git a/pkg/amqp-bunny/Buffer.php b/pkg/amqp-bunny/Buffer.php deleted file mode 100644 index 1912274f7..000000000 --- a/pkg/amqp-bunny/Buffer.php +++ /dev/null @@ -1,43 +0,0 @@ - [AmqpMessage, AmqpMessage ...]] - */ - private $messages; - - public function __construct() - { - $this->messages = []; - } - - /** - * @param string $consumerTag - * @param AmqpMessage $message - */ - public function push($consumerTag, AmqpMessage $message) - { - if (false == array_key_exists($consumerTag, $this->messages)) { - $this->messages[$consumerTag] = []; - } - - $this->messages[$consumerTag][] = $message; - } - - /** - * @param string $consumerTag - * - * @return AmqpMessage|null - */ - public function pop($consumerTag) - { - if (false == empty($this->messages[$consumerTag])) { - return array_shift($this->messages[$consumerTag]); - } - } -} diff --git a/pkg/amqp-bunny/BunnyClient.php b/pkg/amqp-bunny/BunnyClient.php index 8697b9de4..19a76013a 100644 --- a/pkg/amqp-bunny/BunnyClient.php +++ b/pkg/amqp-bunny/BunnyClient.php @@ -1,5 +1,7 @@ Supporting Enqueue + +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # AMQP Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/amqp-bunny.png?branch=master)](https://travis-ci.org/php-enqueue/amqp-bunny) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/amqp-bunny/ci.yml?branch=master)](https://github.com/php-enqueue/amqp-bunny/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/amqp-bunny/d/total.png)](https://packagist.org/packages/enqueue/amqp-bunny) [![Latest Stable Version](https://poser.pugx.org/enqueue/amqp-bunny/version.png)](https://packagist.org/packages/enqueue/amqp-bunny) - -This is an implementation of [amqp interop](https://github.com/queue-interop/amqp-interop). It uses [bunny](https://github.com/jakubkulhan/bunny) internally. + +This is an implementation of [amqp interop](https://github.com/queue-interop/amqp-interop). It uses [bunny](https://github.com/jakubkulhan/bunny) internally. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/amqp_bunny/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/amqp-bunny/Tests/AmqpConnectionFactoryTest.php b/pkg/amqp-bunny/Tests/AmqpConnectionFactoryTest.php index 43ae04fdd..da54dfc0d 100644 --- a/pkg/amqp-bunny/Tests/AmqpConnectionFactoryTest.php +++ b/pkg/amqp-bunny/Tests/AmqpConnectionFactoryTest.php @@ -3,26 +3,26 @@ namespace Enqueue\AmqpBunny\Tests; use Enqueue\AmqpBunny\AmqpConnectionFactory; +use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\ConnectionFactory; use PHPUnit\Framework\TestCase; class AmqpConnectionFactoryTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, AmqpConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, AmqpConnectionFactory::class); } - public function testShouldSupportAmqpLibScheme() + public function testShouldSetRabbitMqDlxDelayStrategyIfRabbitMqSchemeExtensionPresent() { - // no exception here - new AmqpConnectionFactory('amqp+bunny:'); + $factory = new AmqpConnectionFactory('amqp+rabbitmq:'); - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN scheme "amqp+foo" is not supported. Could be one of "amqp", "amqps", "amqp+bunny" only.'); - new AmqpConnectionFactory('amqp+foo:'); + $this->assertAttributeInstanceOf(RabbitMqDlxDelayStrategy::class, 'delayStrategy', $factory); } } diff --git a/pkg/amqp-bunny/Tests/AmqpConsumerTest.php b/pkg/amqp-bunny/Tests/AmqpConsumerTest.php index 938dff5d2..c2c694566 100644 --- a/pkg/amqp-bunny/Tests/AmqpConsumerTest.php +++ b/pkg/amqp-bunny/Tests/AmqpConsumerTest.php @@ -7,14 +7,14 @@ use Bunny\Message; use Enqueue\AmqpBunny\AmqpConsumer; use Enqueue\AmqpBunny\AmqpContext; -use Enqueue\AmqpBunny\Buffer; use Enqueue\Null\NullMessage; use Enqueue\Test\ClassExtensionTrait; use Enqueue\Test\WriteAttributeTrait; use Interop\Amqp\Impl\AmqpMessage; use Interop\Amqp\Impl\AmqpQueue; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrConsumer; +use Interop\Queue\Consumer; +use Interop\Queue\Exception\InvalidMessageException; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class AmqpConsumerTest extends TestCase @@ -24,31 +24,21 @@ class AmqpConsumerTest extends TestCase public function testShouldImplementConsumerInterface() { - $this->assertClassImplements(PsrConsumer::class, AmqpConsumer::class); - } - - public function testCouldBeConstructedWithContextAndQueueAndBufferAsArguments() - { - new AmqpConsumer( - $this->createContextMock(), - new AmqpQueue('aName'), - new Buffer(), - 'basic_get' - ); + $this->assertClassImplements(Consumer::class, AmqpConsumer::class); } public function testShouldReturnQueue() { $queue = new AmqpQueue('aName'); - $consumer = new AmqpConsumer($this->createContextMock(), $queue, new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), $queue); $this->assertSame($queue, $consumer->getQueue()); } public function testOnAcknowledgeShouldThrowExceptionIfNotAmqpMessage() { - $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName')); $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of Interop\Amqp\AmqpMessage but'); @@ -58,7 +48,7 @@ public function testOnAcknowledgeShouldThrowExceptionIfNotAmqpMessage() public function testOnRejectShouldThrowExceptionIfNotAmqpMessage() { - $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName')); $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of Interop\Amqp\AmqpMessage but'); @@ -74,7 +64,7 @@ public function testOnAcknowledgeShouldAcknowledgeMessage() ->method('ack') ->with($this->isInstanceOf(Message::class)) ->willReturnCallback(function (Message $message) { - $this->assertSame('theDeliveryTag', $message->deliveryTag); + $this->assertSame(145, $message->deliveryTag); }); $context = $this->createContextMock(); @@ -84,10 +74,10 @@ public function testOnAcknowledgeShouldAcknowledgeMessage() ->willReturn($channel) ; - $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName')); $message = new AmqpMessage(); - $message->setDeliveryTag('theDeliveryTag'); + $message->setDeliveryTag(145); $consumer->acknowledge($message); } @@ -100,7 +90,7 @@ public function testOnRejectShouldRejectMessage() ->method('reject') ->with($this->isInstanceOf(Message::class), false) ->willReturnCallback(function (Message $message) { - $this->assertSame('theDeliveryTag', $message->deliveryTag); + $this->assertSame(167, $message->deliveryTag); }); $context = $this->createContextMock(); @@ -110,10 +100,10 @@ public function testOnRejectShouldRejectMessage() ->willReturn($channel) ; - $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName')); $message = new AmqpMessage(); - $message->setDeliveryTag('theDeliveryTag'); + $message->setDeliveryTag(167); $consumer->reject($message, false); } @@ -126,7 +116,7 @@ public function testOnRejectShouldRequeueMessage() ->method('reject') ->with($this->isInstanceOf(Message::class), true) ->willReturnCallback(function (Message $message) { - $this->assertSame('theDeliveryTag', $message->deliveryTag); + $this->assertSame(178, $message->deliveryTag); }); $context = $this->createContextMock(); @@ -136,10 +126,10 @@ public function testOnRejectShouldRequeueMessage() ->willReturn($channel) ; - $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName')); $message = new AmqpMessage(); - $message->setDeliveryTag('theDeliveryTag'); + $message->setDeliveryTag(178); $consumer->reject($message, true); } @@ -170,7 +160,7 @@ public function testShouldReturnMessageOnReceiveNoWait() ->willReturn($message) ; - $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName')); $receivedMessage = $consumer->receiveNoWait(); @@ -203,7 +193,7 @@ public function testShouldReturnMessageOnReceiveWithReceiveMethodBasicGet() ->willReturn($message) ; - $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName')); $receivedMessage = $consumer->receive(); @@ -211,7 +201,7 @@ public function testShouldReturnMessageOnReceiveWithReceiveMethodBasicGet() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Client + * @return MockObject|Client */ public function createClientMock() { @@ -219,7 +209,7 @@ public function createClientMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext + * @return MockObject|AmqpContext */ public function createContextMock() { @@ -227,7 +217,7 @@ public function createContextMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Channel + * @return MockObject|Channel */ public function createBunnyChannelMock() { diff --git a/pkg/amqp-bunny/Tests/AmqpContextTest.php b/pkg/amqp-bunny/Tests/AmqpContextTest.php index 74f435ea5..69d9b5012 100644 --- a/pkg/amqp-bunny/Tests/AmqpContextTest.php +++ b/pkg/amqp-bunny/Tests/AmqpContextTest.php @@ -5,9 +5,11 @@ use Bunny\Channel; use Bunny\Protocol\MethodQueueDeclareOkFrame; use Enqueue\AmqpBunny\AmqpContext; +use Enqueue\AmqpBunny\AmqpSubscriptionConsumer; use Interop\Amqp\Impl\AmqpBind; use Interop\Amqp\Impl\AmqpQueue; use Interop\Amqp\Impl\AmqpTopic; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class AmqpContextTest extends TestCase @@ -39,7 +41,7 @@ public function testShouldDeclareTopic() $topic->addFlag(AmqpTopic::FLAG_INTERNAL); $topic->addFlag(AmqpTopic::FLAG_AUTODELETE); - $session = new AmqpContext($channel); + $session = new AmqpContext($channel, []); $session->declareTopic($topic); } @@ -62,7 +64,7 @@ public function testShouldDeleteTopic() $topic->addFlag(AmqpTopic::FLAG_IFUNUSED); $topic->addFlag(AmqpTopic::FLAG_NOWAIT); - $session = new AmqpContext($channel); + $session = new AmqpContext($channel, []); $session->deleteTopic($topic); } @@ -97,7 +99,7 @@ public function testShouldDeclareQueue() $queue->addFlag(AmqpQueue::FLAG_EXCLUSIVE); $queue->addFlag(AmqpQueue::FLAG_NOWAIT); - $session = new AmqpContext($channel); + $session = new AmqpContext($channel, []); $this->assertSame(123, $session->declareQueue($queue)); } @@ -122,7 +124,7 @@ public function testShouldDeleteQueue() $queue->addFlag(AmqpQueue::FLAG_IFEMPTY); $queue->addFlag(AmqpQueue::FLAG_NOWAIT); - $session = new AmqpContext($channel); + $session = new AmqpContext($channel, []); $session->deleteQueue($queue); } @@ -138,7 +140,7 @@ public function testBindShouldBindTopicToTopic() ->with($this->identicalTo('target'), $this->identicalTo('source'), $this->identicalTo('routing-key'), $this->isTrue()) ; - $context = new AmqpContext($channel); + $context = new AmqpContext($channel, []); $context->bind(new AmqpBind($target, $source, 'routing-key', 12345)); } @@ -154,7 +156,7 @@ public function testBindShouldBindTopicToQueue() ->with($this->identicalTo('target'), $this->identicalTo('source'), $this->identicalTo('routing-key'), $this->isTrue()) ; - $context = new AmqpContext($channel); + $context = new AmqpContext($channel, []); $context->bind(new AmqpBind($target, $source, 'routing-key', 12345)); $context->bind(new AmqpBind($source, $target, 'routing-key', 12345)); } @@ -171,7 +173,7 @@ public function testShouldUnBindTopicFromTopic() ->with($this->identicalTo('target'), $this->identicalTo('source'), $this->identicalTo('routing-key'), $this->isTrue()) ; - $context = new AmqpContext($channel); + $context = new AmqpContext($channel, []); $context->unbind(new AmqpBind($target, $source, 'routing-key', 12345)); } @@ -187,7 +189,7 @@ public function testShouldUnBindTopicFromQueue() ->with($this->identicalTo('target'), $this->identicalTo('source'), $this->identicalTo('routing-key'), ['key' => 'value']) ; - $context = new AmqpContext($channel); + $context = new AmqpContext($channel, []); $context->unbind(new AmqpBind($target, $source, 'routing-key', 12345, ['key' => 'value'])); $context->unbind(new AmqpBind($source, $target, 'routing-key', 12345, ['key' => 'value'])); } @@ -200,7 +202,7 @@ public function testShouldCloseChannelConnection() ->method('close') ; - $context = new AmqpContext($channel); + $context = new AmqpContext($channel, []); $context->createProducer(); $context->close(); @@ -218,7 +220,7 @@ public function testShouldPurgeQueue() ->with($this->identicalTo('queue'), $this->isTrue()) ; - $context = new AmqpContext($channel); + $context = new AmqpContext($channel, []); $context->purgeQueue($queue); } @@ -231,12 +233,19 @@ public function testShouldSetQos() ->with($this->identicalTo(123), $this->identicalTo(456), $this->isTrue()) ; - $context = new AmqpContext($channel); + $context = new AmqpContext($channel, []); $context->setQos(123, 456, true); } + public function testShouldReturnExpectedSubscriptionConsumerInstance() + { + $context = new AmqpContext($this->createChannelMock(), []); + + $this->assertInstanceOf(AmqpSubscriptionConsumer::class, $context->createSubscriptionConsumer()); + } + /** - * @return \PHPUnit_Framework_MockObject_MockObject|Channel + * @return MockObject|Channel */ public function createChannelMock() { diff --git a/pkg/amqp-bunny/Tests/AmqpProducerTest.php b/pkg/amqp-bunny/Tests/AmqpProducerTest.php index ddb067858..17b390341 100644 --- a/pkg/amqp-bunny/Tests/AmqpProducerTest.php +++ b/pkg/amqp-bunny/Tests/AmqpProducerTest.php @@ -3,7 +3,6 @@ namespace Enqueue\AmqpBunny\Tests; use Bunny\Channel; -use Bunny\Message; use Enqueue\AmqpBunny\AmqpContext; use Enqueue\AmqpBunny\AmqpProducer; use Enqueue\AmqpTools\DelayStrategy; @@ -12,26 +11,22 @@ use Interop\Amqp\Impl\AmqpMessage; use Interop\Amqp\Impl\AmqpQueue; use Interop\Amqp\Impl\AmqpTopic; -use Interop\Queue\DeliveryDelayNotSupportedException; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProducer; +use Interop\Queue\Destination; +use Interop\Queue\Exception\DeliveryDelayNotSupportedException; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Message; +use Interop\Queue\Producer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class AmqpProducerTest extends TestCase { use ClassExtensionTrait; - public function testCouldBeConstructedWithRequiredArguments() + public function testShouldImplementQueueInteropProducerInterface() { - new AmqpProducer($this->createBunnyChannelMock(), $this->createContextMock()); - } - - public function testShouldImplementPsrProducerInterface() - { - $this->assertClassImplements(PsrProducer::class, AmqpProducer::class); + $this->assertClassImplements(Producer::class, AmqpProducer::class); } public function testShouldThrowExceptionWhenDestinationTypeIsInvalid() @@ -135,11 +130,39 @@ public function testShouldSetMessageHeaders() $channel ->expects($this->once()) ->method('publish') - ->with($this->anything(), ['content_type' => 'text/plain']) + ->with($this->anything(), ['misc' => 'text/plain']) ; $producer = new AmqpProducer($channel, $this->createContextMock()); - $producer->send(new AmqpTopic('name'), new AmqpMessage('body', [], ['content_type' => 'text/plain'])); + $producer->send(new AmqpTopic('name'), new AmqpMessage('body', [], ['misc' => 'text/plain'])); + } + + public function testShouldConvertStandartHeadersToBunnyFormat() + { + $channel = $this->createBunnyChannelMock(); + $expectedHeaders = [ + 'content-encoding' => 'utf8', + 'content-type' => 'text/plain', + 'message-id' => 'id', + 'correlation-id' => 'correlation', + 'reply-to' => 'reply', + 'delivery-mode' => 2, + ]; + $channel + ->expects($this->once()) + ->method('publish') + ->with($this->anything(), $expectedHeaders); + + $producer = new AmqpProducer($channel, $this->createContextMock()); + $message = new AmqpMessage('body', []); + $message->setMessageId('id'); + $message->setReplyTo('reply'); + $message->setDeliveryMode(2); + $message->setContentType('text/plain'); + $message->setContentEncoding('utf8'); + $message->setCorrelationId('correlation'); + + $producer->send(new AmqpTopic('name'), $message); } public function testShouldSetMessageProperties() @@ -173,23 +196,23 @@ public function testShouldPropagateFlags() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrMessage + * @return MockObject|Message */ private function createMessageMock() { - return $this->createMock(PsrMessage::class); + return $this->createMock(Message::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrDestination + * @return MockObject|Destination */ private function createDestinationMock() { - return $this->createMock(PsrDestination::class); + return $this->createMock(Destination::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Channel + * @return MockObject|Channel */ private function createBunnyChannelMock() { @@ -197,15 +220,15 @@ private function createBunnyChannelMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext + * @return MockObject|AmqpContext */ private function createContextMock() { - return $this->createMock(AmqpContext::class); + return $this->createPartialMock(AmqpContext::class, []); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DelayStrategy + * @return MockObject|DelayStrategy */ private function createDelayStrategyMock() { diff --git a/pkg/amqp-bunny/Tests/AmqpSubscriptionConsumerTest.php b/pkg/amqp-bunny/Tests/AmqpSubscriptionConsumerTest.php new file mode 100644 index 000000000..4bf08ded5 --- /dev/null +++ b/pkg/amqp-bunny/Tests/AmqpSubscriptionConsumerTest.php @@ -0,0 +1,27 @@ +assertTrue($rc->implementsInterface(SubscriptionConsumer::class)); + } + + /** + * @return AmqpContext|MockObject + */ + private function createAmqpContextMock() + { + return $this->createMock(AmqpContext::class); + } +} diff --git a/pkg/amqp-bunny/Tests/BufferTest.php b/pkg/amqp-bunny/Tests/BufferTest.php deleted file mode 100644 index 6cb1ef0ad..000000000 --- a/pkg/amqp-bunny/Tests/BufferTest.php +++ /dev/null @@ -1,64 +0,0 @@ -assertAttributeSame([], 'messages', $buffer); - } - - public function testShouldReturnNullIfNoMessagesInBuffer() - { - $buffer = new Buffer(); - - $this->assertNull($buffer->pop('aConsumerTag')); - $this->assertNull($buffer->pop('anotherConsumerTag')); - } - - public function testShouldPushMessageToBuffer() - { - $fooMessage = new AmqpMessage(); - $barMessage = new AmqpMessage(); - $bazMessage = new AmqpMessage(); - - $buffer = new Buffer(); - - $buffer->push('aConsumerTag', $fooMessage); - $buffer->push('aConsumerTag', $barMessage); - - $buffer->push('anotherConsumerTag', $bazMessage); - - $this->assertAttributeSame([ - 'aConsumerTag' => [$fooMessage, $barMessage], - 'anotherConsumerTag' => [$bazMessage], - ], 'messages', $buffer); - } - - public function testShouldPopMessageFromBuffer() - { - $fooMessage = new AmqpMessage(); - $barMessage = new AmqpMessage(); - - $buffer = new Buffer(); - - $buffer->push('aConsumerTag', $fooMessage); - $buffer->push('aConsumerTag', $barMessage); - - $this->assertSame($fooMessage, $buffer->pop('aConsumerTag')); - $this->assertSame($barMessage, $buffer->pop('aConsumerTag')); - $this->assertNull($buffer->pop('aConsumerTag')); - } -} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php deleted file mode 100644 index 32820ba6a..000000000 --- a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php deleted file mode 100644 index d2a14f3d9..000000000 --- a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php deleted file mode 100644 index eb1a38b0a..000000000 --- a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php deleted file mode 100644 index 7d9806f79..000000000 --- a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpConnectionFactoryTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpConnectionFactoryTest.php index d8d481527..6fd12b642 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpConnectionFactoryTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpConnectionFactoryTest.php @@ -3,9 +3,9 @@ namespace Enqueue\AmqpBunny\Tests\Spec; use Enqueue\AmqpBunny\AmqpConnectionFactory; -use Interop\Queue\Spec\PsrConnectionFactorySpec; +use Interop\Queue\Spec\ConnectionFactorySpec; -class AmqpConnectionFactoryTest extends PsrConnectionFactorySpec +class AmqpConnectionFactoryTest extends ConnectionFactorySpec { protected function createConnectionFactory() { diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpContextTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpContextTest.php index 52bc2067d..60d4672e0 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpContextTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpContextTest.php @@ -4,14 +4,14 @@ use Bunny\Channel; use Enqueue\AmqpBunny\AmqpContext; -use Interop\Queue\Spec\PsrContextSpec; +use Interop\Queue\Spec\ContextSpec; -class AmqpContextTest extends PsrContextSpec +class AmqpContextTest extends ContextSpec { protected function createContext() { $channel = $this->createMock(Channel::class); - return new AmqpContext($channel); + return new AmqpContext($channel, []); } } diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpPreFetchCountTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpPreFetchCountTest.php deleted file mode 100644 index 53ae33e14..000000000 --- a/pkg/amqp-bunny/Tests/Spec/AmqpPreFetchCountTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php index 1ef23b65b..e5c2d1302 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpBunny\AmqpConnectionFactory; use Enqueue\AmqpBunny\AmqpContext; use Enqueue\AmqpTools\RabbitMqDelayPluginDelayStrategy; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; /** @@ -18,9 +18,6 @@ public function test() $this->markTestIncomplete(); } - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -31,10 +28,8 @@ protected function createContext() /** * @param AmqpContext $context - * - * {@inheritdoc} */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = parent::createQueue($context, $queueName); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php index 531389a02..b7c311cfb 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpBunny\AmqpConnectionFactory; use Enqueue\AmqpBunny\AmqpContext; use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest extends SendAndReceiveDelayedMessageFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -26,10 +23,8 @@ protected function createContext() /** * @param AmqpContext $context - * - * {@inheritdoc} */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = parent::createQueue($context, $queueName); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php index c70f89079..89530e2e6 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpBunny\AmqpConnectionFactory; use Enqueue\AmqpBunny\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceivePriorityMessagesFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendAndReceivePriorityMessagesFromQueueTest extends SendAndReceivePriorityMessagesFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $queue->setArguments(['x-max-priority' => 10]); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php index 5f5e07c0f..793e3fa78 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpBunny\AmqpConnectionFactory; use Enqueue\AmqpBunny\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveTimeToLiveMessagesFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest extends SendAndReceiveTimeToLiveMessagesFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimestampAsIntegerTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimestampAsIntegerTest.php index f253d73c9..37ef1d0bd 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimestampAsIntegerTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendAndReceiveTimestampAsIntegerTest.php @@ -10,9 +10,6 @@ */ class AmqpSendAndReceiveTimestampAsIntegerTest extends SendAndReceiveTimestampAsIntegerSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php index 1111103c1..e3286ae9c 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpBunny\AmqpConnectionFactory; use Enqueue\AmqpBunny\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php index 4647239ef..ce9fc2794 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpBunny\AmqpConnectionFactory; use Enqueue\AmqpBunny\AmqpContext; use Interop\Amqp\AmqpTopic; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromTopicSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendToAndReceiveFromTopicTest extends SendToAndReceiveFromTopicSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -24,11 +21,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php index 461e98680..f1210d03a 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpBunny\AmqpConnectionFactory; use Enqueue\AmqpBunny\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendToAndReceiveNoWaitFromQueueTest extends SendToAndReceiveNoWaitFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php index 8fa295b52..cc47cf44a 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpBunny\AmqpConnectionFactory; use Enqueue\AmqpBunny\AmqpContext; use Interop\Amqp\AmqpTopic; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromTopicSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendToAndReceiveNoWaitFromTopicTest extends SendToAndReceiveNoWaitFromTopicSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -24,11 +21,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicGetMethodTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php similarity index 73% rename from pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicGetMethodTest.php rename to pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php index c06dac310..f13ead179 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicGetMethodTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php @@ -6,17 +6,14 @@ use Enqueue\AmqpBunny\AmqpContext; use Interop\Amqp\AmqpTopic; use Interop\Amqp\Impl\AmqpBind; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveFromQueueSpec; /** * @group functional */ -class AmqpSendToTopicAndReceiveFromQueueWithBasicGetMethodTest extends SendToTopicAndReceiveFromQueueSpec +class AmqpSendToTopicAndReceiveFromQueueTest extends SendToTopicAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -25,11 +22,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); @@ -41,11 +36,9 @@ protected function createQueue(PsrContext $context, $queueName) } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicConsumeMethodTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicConsumeMethodTest.php deleted file mode 100644 index 49f502695..000000000 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicConsumeMethodTest.php +++ /dev/null @@ -1,61 +0,0 @@ -createContext(); - } - - /** - * {@inheritdoc} - * - * @param AmqpContext $context - */ - protected function createQueue(PsrContext $context, $queueName) - { - $queueName .= '_basic_consume'; - - $queue = $context->createQueue($queueName); - $context->declareQueue($queue); - $context->purgeQueue($queue); - - $context->bind(new AmqpBind($context->createTopic($queueName), $queue)); - - return $queue; - } - - /** - * {@inheritdoc} - * - * @param AmqpContext $context - */ - protected function createTopic(PsrContext $context, $topicName) - { - $topicName .= '_basic_consume'; - - $topic = $context->createTopic($topicName); - $topic->setType(AmqpTopic::TYPE_FANOUT); - $topic->addFlag(AmqpTopic::FLAG_DURABLE); - $context->declareTopic($topic); - - return $topic; - } -} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php index 547c3b775..683e0b1ca 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php @@ -6,7 +6,7 @@ use Enqueue\AmqpBunny\AmqpContext; use Interop\Amqp\AmqpTopic; use Interop\Amqp\Impl\AmqpBind; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveNoWaitFromQueueSpec; /** @@ -14,9 +14,6 @@ */ class AmqpSendToTopicAndReceiveNoWaitFromQueueTest extends SendToTopicAndReceiveNoWaitFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -25,11 +22,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); @@ -41,11 +36,9 @@ protected function createQueue(PsrContext $context, $queueName) } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php index 1f53ae398..4a549fcf8 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpBunny\AmqpConnectionFactory; use Enqueue\AmqpBunny\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromQueueSpec; /** @@ -19,9 +19,6 @@ public function test() parent::test(); } - /** - * {@inheritdoc} - */ protected function createContext() { $baseDir = realpath(__DIR__.'/../../../../'); @@ -44,11 +41,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerAddConsumerTagOnSubscribeTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerAddConsumerTagOnSubscribeTest.php new file mode 100644 index 000000000..1814b8c7c --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerAddConsumerTagOnSubscribeTest.php @@ -0,0 +1,20 @@ +createContext(); + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..5d4d8d40e --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,41 @@ +createContext(); + $context->setQos(0, 5, false); + + return $context; + } + + /** + * @param AmqpContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var AmqpQueue $queue */ + $queue = parent::createQueue($context, $queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerConsumeUntilUnsubscribedTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..6cae48148 --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerConsumeUntilUnsubscribedTest.php @@ -0,0 +1,50 @@ +subscriptionConsumer) { + $this->subscriptionConsumer->unsubscribeAll(); + } + + parent::tearDown(); + } + + /** + * @return AmqpContext + */ + protected function createContext() + { + $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); + + $context = $factory->createContext(); + $context->setQos(0, 1, false); + + return $context; + } + + /** + * @param AmqpContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var AmqpQueue $queue */ + $queue = parent::createQueue($context, $queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerPreFetchCountTest.php similarity index 50% rename from pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php rename to pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerPreFetchCountTest.php index 188118f64..b7926e93e 100644 --- a/pkg/amqp-bunny/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerPreFetchCountTest.php @@ -3,17 +3,15 @@ namespace Enqueue\AmqpBunny\Tests\Spec; use Enqueue\AmqpBunny\AmqpConnectionFactory; -use Interop\Queue\Spec\Amqp\BasicConsumeFromAllSubscribedQueuesSpec; +use Interop\Amqp\AmqpContext; +use Interop\Queue\Spec\Amqp\SubscriptionConsumerPreFetchCountSpec; /** * @group functional */ -class AmqpBasicConsumeFromAllSubscribedQueuesTest extends BasicConsumeFromAllSubscribedQueuesSpec +class AmqpSubscriptionConsumerPreFetchCountTest extends SubscriptionConsumerPreFetchCountSpec { - /** - * {@inheritdoc} - */ - protected function createContext() + protected function createContext(): AmqpContext { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest.php new file mode 100644 index 000000000..ddf0ffca5 --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest.php @@ -0,0 +1,20 @@ +createContext(); + } +} diff --git a/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerStopOnFalseTest.php b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerStopOnFalseTest.php new file mode 100644 index 000000000..d9d8527d3 --- /dev/null +++ b/pkg/amqp-bunny/Tests/Spec/AmqpSubscriptionConsumerStopOnFalseTest.php @@ -0,0 +1,41 @@ +createContext(); + $context->setQos(0, 5, false); + + return $context; + } + + /** + * @param AmqpContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var AmqpQueue $queue */ + $queue = parent::createQueue($context, $queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-bunny/composer.json b/pkg/amqp-bunny/composer.json index b261a74da..84d0f4309 100644 --- a/pkg/amqp-bunny/composer.json +++ b/pkg/amqp-bunny/composer.json @@ -6,20 +6,17 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - - "queue-interop/amqp-interop": "^0.7.4@dev", - "bunny/bunny": "^0.2.4|^0.3|^0.4", - "enqueue/amqp-tools": "^0.8.35@dev" + "php": "^8.1", + "queue-interop/amqp-interop": "^0.8.2", + "queue-interop/queue-interop": "^0.8", + "bunny/bunny": "^0.4|^0.5", + "enqueue/amqp-tools": "^0.10" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -34,13 +31,10 @@ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/amqp-bunny/phpunit.xml.dist b/pkg/amqp-bunny/phpunit.xml.dist index 7d6c5ed3e..3ca071a57 100644 --- a/pkg/amqp-bunny/phpunit.xml.dist +++ b/pkg/amqp-bunny/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/amqp-ext/.github/workflows/ci.yml b/pkg/amqp-ext/.github/workflows/ci.yml new file mode 100644 index 000000000..d48deb0af --- /dev/null +++ b/pkg/amqp-ext/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - run: php Tests/fix_composer_json.php + + - uses: "ramsey/composer-install@v1" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/amqp-ext/.travis.yml b/pkg/amqp-ext/.travis.yml deleted file mode 100644 index 658dcabad..000000000 --- a/pkg/amqp-ext/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - php Tests/fix_composer_json.php - - composer self-update - - composer install - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/amqp-ext/AmqpConnectionFactory.php b/pkg/amqp-ext/AmqpConnectionFactory.php index a3561b85a..c3241d72a 100644 --- a/pkg/amqp-ext/AmqpConnectionFactory.php +++ b/pkg/amqp-ext/AmqpConnectionFactory.php @@ -5,7 +5,9 @@ use Enqueue\AmqpTools\ConnectionConfig; use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; +use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; use Interop\Amqp\AmqpConnectionFactory as InteropAmqpConnectionFactory; +use Interop\Queue\Context; class AmqpConnectionFactory implements InteropAmqpConnectionFactory, DelayStrategyAware { @@ -22,10 +24,7 @@ class AmqpConnectionFactory implements InteropAmqpConnectionFactory, DelayStrate private $connection; /** - * @see \Enqueue\AmqpTools\ConnectionConfig for possible config formats and values - * - * In addition this factory accepts next options: - * receive_method - Could be either basic_get or basic_consume + * @see ConnectionConfig for possible config formats and values * * @param array|string|null $config */ @@ -34,33 +33,18 @@ public function __construct($config = 'amqp:') $this->config = (new ConnectionConfig($config)) ->addSupportedScheme('amqp+ext') ->addSupportedScheme('amqps+ext') - ->addDefaultOption('receive_method', 'basic_get') ->parse() ; - $supportedMethods = ['basic_get', 'basic_consume']; - if (false == in_array($this->config->getOption('receive_method'), $supportedMethods, true)) { - throw new \LogicException(sprintf( - 'Invalid "receive_method" option value "%s". It could be only "%s"', - $this->config->getOption('receive_method'), - implode('", "', $supportedMethods) - )); - } - - if ('basic_consume' == $this->config->getOption('receive_method')) { - if (false == (version_compare(phpversion('amqp'), '1.9.1', '>=') || '1.9.1-dev' == phpversion('amqp'))) { - // @see https://github.com/php-enqueue/enqueue-dev/issues/110 and https://github.com/pdezwart/php-amqp/issues/281 - throw new \LogicException('The "basic_consume" method does not work on amqp extension prior 1.9.1 version.'); - } + if (in_array('rabbitmq', $this->config->getSchemeExtensions(), true)) { + $this->setDelayStrategy(new RabbitMqDlxDelayStrategy()); } } /** - * {@inheritdoc} - * * @return AmqpContext */ - public function createContext() + public function createContext(): Context { if ($this->config->isLazy()) { $context = new AmqpContext(function () { @@ -68,41 +52,30 @@ public function createContext() $extContext->qos($this->config->getQosPrefetchSize(), $this->config->getQosPrefetchCount()); return $extContext; - }, $this->config->getOption('receive_method')); + }); $context->setDelayStrategy($this->delayStrategy); return $context; } - $context = new AmqpContext($this->createExtContext($this->establishConnection()), $this->config->getOption('receive_method')); + $context = new AmqpContext($this->createExtContext($this->establishConnection())); $context->setDelayStrategy($this->delayStrategy); $context->setQos($this->config->getQosPrefetchSize(), $this->config->getQosPrefetchCount(), $this->config->isQosGlobal()); return $context; } - /** - * @return ConnectionConfig - */ - public function getConfig() + public function getConfig(): ConnectionConfig { return $this->config; } - /** - * @param \AMQPConnection $extConnection - * - * @return \AMQPChannel - */ - private function createExtContext(\AMQPConnection $extConnection) + private function createExtContext(\AMQPConnection $extConnection): \AMQPChannel { return new \AMQPChannel($extConnection); } - /** - * @return \AMQPConnection - */ - private function establishConnection() + private function establishConnection(): \AMQPConnection { if (false == $this->connection) { $extConfig = []; diff --git a/pkg/amqp-ext/AmqpConsumer.php b/pkg/amqp-ext/AmqpConsumer.php index eb47c0f44..700e8d77f 100644 --- a/pkg/amqp-ext/AmqpConsumer.php +++ b/pkg/amqp-ext/AmqpConsumer.php @@ -4,10 +4,10 @@ use Interop\Amqp\AmqpConsumer as InteropAmqpConsumer; use Interop\Amqp\AmqpMessage as InteropAmqpMessage; -use Interop\Amqp\AmqpQueue; -use Interop\Amqp\Impl\AmqpMessage; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrMessage; +use Interop\Amqp\AmqpQueue as InteropAmqpQueue; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Message; +use Interop\Queue\Queue; class AmqpConsumer implements InteropAmqpConsumer { @@ -17,25 +17,15 @@ class AmqpConsumer implements InteropAmqpConsumer private $context; /** - * @var AmqpQueue + * @var InteropAmqpQueue */ private $queue; - /** - * @var Buffer - */ - private $buffer; - /** * @var \AMQPQueue */ private $extQueue; - /** - * @var string - */ - private $receiveMethod; - /** * @var int */ @@ -46,115 +36,85 @@ class AmqpConsumer implements InteropAmqpConsumer */ private $consumerTag; - /** - * @param AmqpContext $context - * @param AmqpQueue $queue - * @param Buffer $buffer - * @param string $receiveMethod - */ - public function __construct(AmqpContext $context, AmqpQueue $queue, Buffer $buffer, $receiveMethod) + public function __construct(AmqpContext $context, InteropAmqpQueue $queue) { $this->queue = $queue; $this->context = $context; - $this->buffer = $buffer; - $this->receiveMethod = $receiveMethod; $this->flags = self::FLAG_NOPARAM; } - /** - * {@inheritdoc} - */ - public function setConsumerTag($consumerTag) + public function setConsumerTag(?string $consumerTag = null): void { $this->consumerTag = $consumerTag; } - /** - * {@inheritdoc} - */ - public function getConsumerTag() + public function getConsumerTag(): ?string { return $this->consumerTag; } - /** - * {@inheritdoc} - */ - public function clearFlags() + public function clearFlags(): void { $this->flags = self::FLAG_NOPARAM; } - /** - * {@inheritdoc} - */ - public function addFlag($flag) + public function addFlag(int $flag): void { $this->flags |= $flag; } - /** - * {@inheritdoc} - */ - public function getFlags() + public function getFlags(): int { return $this->flags; } - /** - * {@inheritdoc} - */ - public function setFlags($flags) + public function setFlags(int $flags): void { $this->flags = $flags; } /** - * {@inheritdoc} - * - * @return AmqpQueue + * @return InteropAmqpQueue */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } /** - * {@inheritdoc} - * - * @return InteropAmqpMessage|null + * @return InteropAmqpMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { - if ('basic_get' == $this->receiveMethod) { - return $this->receiveBasicGet($timeout); - } + $end = microtime(true) + ($timeout / 1000); + + while (0 === $timeout || microtime(true) < $end) { + if ($message = $this->receiveNoWait()) { + return $message; + } - if ('basic_consume' == $this->receiveMethod) { - return $this->receiveBasicConsume($timeout); + usleep(100000); // 100ms } - throw new \LogicException('The "receiveMethod" is not supported'); + return null; } /** - * {@inheritdoc} - * - * @return AmqpMessage|null + * @return InteropAmqpMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { if ($extMessage = $this->getExtQueue()->get(Flags::convertConsumerFlags($this->flags))) { return $this->context->convertMessage($extMessage); } + + return null; } /** - * {@inheritdoc} - * - * @param AmqpMessage $message + * @param InteropAmqpMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); @@ -162,87 +122,19 @@ public function acknowledge(PsrMessage $message) } /** - * {@inheritdoc} - * - * @param AmqpMessage $message + * @param InteropAmqpMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); $this->getExtQueue()->reject( $message->getDeliveryTag(), - $requeue ? AMQP_REQUEUE : AMQP_NOPARAM + $requeue ? \AMQP_REQUEUE : \AMQP_NOPARAM ); } - /** - * @param int $timeout - * - * @return InteropAmqpMessage|null - */ - private function receiveBasicGet($timeout) - { - $end = microtime(true) + ($timeout / 1000); - - while (0 === $timeout || microtime(true) < $end) { - if ($message = $this->receiveNoWait()) { - return $message; - } - - usleep(100000); //100ms - } - } - - /** - * @param int $timeout - * - * @return InteropAmqpMessage|null - */ - private function receiveBasicConsume($timeout) - { - if (false == $this->consumerTag) { - $this->context->subscribe($this, function (InteropAmqpMessage $message) { - $this->buffer->push($message->getConsumerTag(), $message); - - return false; - }); - } - - if ($message = $this->buffer->pop($this->consumerTag)) { - return $message; - } - - while (true) { - $start = microtime(true); - - $this->context->consume($timeout); - - if ($message = $this->buffer->pop($this->consumerTag)) { - return $message; - } - - // is here when consumed message is not for this consumer - - // as timeout is infinite have to continue consumption, but it can overflow message buffer - if ($timeout <= 0) { - continue; - } - - // compute remaining timeout and continue until time is up - $stop = microtime(true); - $timeout -= ($stop - $start) * 1000; - - if ($timeout <= 0) { - break; - } - } - } - - /** - * @return \AMQPQueue - */ - private function getExtQueue() + private function getExtQueue(): \AMQPQueue { if (false == $this->extQueue) { $extQueue = new \AMQPQueue($this->context->getExtChannel()); diff --git a/pkg/amqp-ext/AmqpContext.php b/pkg/amqp-ext/AmqpContext.php index 256f0b19a..c339dc0a1 100644 --- a/pkg/amqp-ext/AmqpContext.php +++ b/pkg/amqp-ext/AmqpContext.php @@ -4,23 +4,26 @@ use Enqueue\AmqpTools\DelayStrategyAware; use Enqueue\AmqpTools\DelayStrategyAwareTrait; -use Enqueue\AmqpTools\SubscriptionConsumer; use Interop\Amqp\AmqpBind as InteropAmqpBind; -use Interop\Amqp\AmqpConsumer as InteropAmqpConsumer; use Interop\Amqp\AmqpContext as InteropAmqpContext; +use Interop\Amqp\AmqpMessage as InteropAmqpMessage; use Interop\Amqp\AmqpQueue as InteropAmqpQueue; use Interop\Amqp\AmqpTopic as InteropAmqpTopic; use Interop\Amqp\Impl\AmqpBind; use Interop\Amqp\Impl\AmqpMessage; use Interop\Amqp\Impl\AmqpQueue; use Interop\Amqp\Impl\AmqpTopic; -use Interop\Queue\Exception; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrSubscriptionConsumerAwareContext; -use Interop\Queue\PsrTopic; - -class AmqpContext implements InteropAmqpContext, DelayStrategyAware, PsrSubscriptionConsumerAwareContext +use Interop\Queue\Consumer; +use Interop\Queue\Destination; +use Interop\Queue\Exception\Exception; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Message; +use Interop\Queue\Producer; +use Interop\Queue\Queue; +use Interop\Queue\SubscriptionConsumer; +use Interop\Queue\Topic; + +class AmqpContext implements InteropAmqpContext, DelayStrategyAware { use DelayStrategyAwareTrait; @@ -34,33 +37,13 @@ class AmqpContext implements InteropAmqpContext, DelayStrategyAware, PsrSubscrip */ private $extChannelFactory; - /** - * @var Buffer - */ - private $buffer; - - /** - * @var string - */ - private $receiveMethod; - - /** - * an item contains an array: [AmqpConsumerInterop $consumer, callable $callback];. - * - * @var array - */ - private $subscribers; - /** * Callable must return instance of \AMQPChannel once called. * * @param \AMQPChannel|callable $extChannel - * @param string $receiveMethod */ - public function __construct($extChannel, $receiveMethod) + public function __construct($extChannel) { - $this->receiveMethod = $receiveMethod; - if ($extChannel instanceof \AMQPChannel) { $this->extChannel = $extChannel; } elseif (is_callable($extChannel)) { @@ -68,40 +51,31 @@ public function __construct($extChannel, $receiveMethod) } else { throw new \InvalidArgumentException('The extChannel argument must be either AMQPChannel or callable that return AMQPChannel.'); } - - $this->buffer = new Buffer(); - $this->subscribers = []; } /** - * {@inheritdoc} + * @return InteropAmqpMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new AmqpMessage($body, $properties, $headers); } /** - * {@inheritdoc} + * @return InteropAmqpTopic */ - public function createTopic($topicName) + public function createTopic(string $topicName): Topic { return new AmqpTopic($topicName); } - /** - * {@inheritdoc} - */ - public function deleteTopic(InteropAmqpTopic $topic) + public function deleteTopic(InteropAmqpTopic $topic): void { $extExchange = new \AMQPExchange($this->getExtChannel()); $extExchange->delete($topic->getTopicName(), Flags::convertTopicFlags($topic->getFlags())); } - /** - * {@inheritdoc} - */ - public function declareTopic(InteropAmqpTopic $topic) + public function declareTopic(InteropAmqpTopic $topic): void { $extExchange = new \AMQPExchange($this->getExtChannel()); $extExchange->setName($topic->getTopicName()); @@ -113,27 +87,21 @@ public function declareTopic(InteropAmqpTopic $topic) } /** - * {@inheritdoc} + * @return InteropAmqpQueue */ - public function createQueue($queueName) + public function createQueue(string $queueName): Queue { return new AmqpQueue($queueName); } - /** - * {@inheritdoc} - */ - public function deleteQueue(InteropAmqpQueue $queue) + public function deleteQueue(InteropAmqpQueue $queue): void { $extQueue = new \AMQPQueue($this->getExtChannel()); $extQueue->setName($queue->getQueueName()); $extQueue->delete(Flags::convertQueueFlags($queue->getFlags())); } - /** - * {@inheritdoc} - */ - public function declareQueue(InteropAmqpQueue $queue) + public function declareQueue(InteropAmqpQueue $queue): int { $extQueue = new \AMQPQueue($this->getExtChannel()); $extQueue->setName($queue->getQueueName()); @@ -144,19 +112,18 @@ public function declareQueue(InteropAmqpQueue $queue) } /** - * {@inheritdoc} + * @param InteropAmqpQueue $queue */ - public function purgeQueue(InteropAmqpQueue $queue) + public function purgeQueue(Queue $queue): void { + InvalidDestinationException::assertDestinationInstanceOf($queue, InteropAmqpQueue::class); + $amqpQueue = new \AMQPQueue($this->getExtChannel()); $amqpQueue->setName($queue->getQueueName()); $amqpQueue->purge(); } - /** - * {@inheritdoc} - */ - public function bind(InteropAmqpBind $bind) + public function bind(InteropAmqpBind $bind): void { if ($bind->getSource() instanceof InteropAmqpQueue && $bind->getTarget() instanceof InteropAmqpQueue) { throw new Exception('Is not possible to bind queue to queue. It is possible to bind topic to queue or topic to topic'); @@ -180,10 +147,7 @@ public function bind(InteropAmqpBind $bind) } } - /** - * {@inheritdoc} - */ - public function unbind(InteropAmqpBind $bind) + public function unbind(InteropAmqpBind $bind): void { if ($bind->getSource() instanceof InteropAmqpQueue && $bind->getTarget() instanceof InteropAmqpQueue) { throw new Exception('Is not possible to unbind queue to queue. It is possible to unbind topic from queue or topic from topic'); @@ -208,14 +172,12 @@ public function unbind(InteropAmqpBind $bind) } /** - * {@inheritdoc} - * * @return InteropAmqpQueue */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { $extQueue = new \AMQPQueue($this->getExtChannel()); - $extQueue->setFlags(AMQP_EXCLUSIVE); + $extQueue->setFlags(\AMQP_EXCLUSIVE); $extQueue->declareQueue(); @@ -226,11 +188,9 @@ public function createTemporaryQueue() } /** - * {@inheritdoc} - * * @return AmqpProducer */ - public function createProducer() + public function createProducer(): Producer { $producer = new AmqpProducer($this->getExtChannel(), $this); $producer->setDelayStrategy($this->delayStrategy); @@ -239,15 +199,13 @@ public function createProducer() } /** - * {@inheritdoc} - * - * @param PsrDestination|AmqpQueue $destination + * @param InteropAmqpQueue $destination * * @return AmqpConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { - $destination instanceof PsrTopic + $destination instanceof Topic ? InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpTopic::class) : InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpQueue::class) ; @@ -256,24 +214,18 @@ public function createConsumer(PsrDestination $destination) $queue = $this->createTemporaryQueue(); $this->bind(new AmqpBind($destination, $queue, $queue->getQueueName())); - return new AmqpConsumer($this, $queue, $this->buffer, $this->receiveMethod); + return new AmqpConsumer($this, $queue); } - return new AmqpConsumer($this, $destination, $this->buffer, $this->receiveMethod); + return new AmqpConsumer($this, $destination); } - /** - * {@inheritdoc} - */ - public function createSubscriptionConsumer() + public function createSubscriptionConsumer(): SubscriptionConsumer { - return new SubscriptionConsumer($this); + return new AmqpSubscriptionConsumer($this); } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { $extConnection = $this->getExtChannel()->getConnection(); if ($extConnection->isConnected()) { @@ -281,26 +233,17 @@ public function close() } } - /** - * {@inheritdoc} - */ - public function setQos($prefetchSize, $prefetchCount, $global) + public function setQos(int $prefetchSize, int $prefetchCount, bool $global): void { $this->getExtChannel()->qos($prefetchSize, $prefetchCount); } - /** - * @return \AMQPChannel - */ - public function getExtChannel() + public function getExtChannel(): \AMQPChannel { if (false == $this->extChannel) { $extChannel = call_user_func($this->extChannelFactory); if (false == $extChannel instanceof \AMQPChannel) { - throw new \LogicException(sprintf( - 'The factory must return instance of AMQPChannel. It returns %s', - is_object($extChannel) ? get_class($extChannel) : gettype($extChannel) - )); + throw new \LogicException(sprintf('The factory must return instance of AMQPChannel. It returns %s', is_object($extChannel) ? $extChannel::class : gettype($extChannel))); } $this->extChannel = $extChannel; @@ -309,109 +252,10 @@ public function getExtChannel() return $this->extChannel; } - /** - * @deprecated since 0.8.34 will be removed in 0.9 - * - * {@inheritdoc} - */ - public function subscribe(InteropAmqpConsumer $consumer, callable $callback) - { - if ($consumer->getConsumerTag() && array_key_exists($consumer->getConsumerTag(), $this->subscribers)) { - return; - } - - $extQueue = new \AMQPQueue($this->getExtChannel()); - $extQueue->setName($consumer->getQueue()->getQueueName()); - - $extQueue->consume(null, Flags::convertConsumerFlags($consumer->getFlags()), $consumer->getConsumerTag()); - - $consumerTag = $extQueue->getConsumerTag(); - $consumer->setConsumerTag($consumerTag); - $this->subscribers[$consumerTag] = [$consumer, $callback, $extQueue]; - } - - /** - * @deprecated since 0.8.34 will be removed in 0.9 - * - * {@inheritdoc} - */ - public function unsubscribe(InteropAmqpConsumer $consumer) - { - if (false == $consumer->getConsumerTag()) { - return; - } - - $consumerTag = $consumer->getConsumerTag(); - $consumer->setConsumerTag(null); - - list($consumer, $callback, $extQueue) = $this->subscribers[$consumerTag]; - - $extQueue->cancel($consumerTag); - unset($this->subscribers[$consumerTag]); - } - - /** - * @deprecated since 0.8.34 will be removed in 0.9 - * - * {@inheritdoc} - */ - public function consume($timeout = 0) - { - if (empty($this->subscribers)) { - throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); - } - - /** @var \AMQPQueue $extQueue */ - $extConnection = $this->getExtChannel()->getConnection(); - - $originalTimeout = $extConnection->getReadTimeout(); - try { - $extConnection->setReadTimeout($timeout / 1000); - - reset($this->subscribers); - /** @var $consumer AmqpConsumer */ - list($consumer) = current($this->subscribers); - - $extQueue = new \AMQPQueue($this->getExtChannel()); - $extQueue->setName($consumer->getQueue()->getQueueName()); - $extQueue->consume(function (\AMQPEnvelope $extEnvelope, \AMQPQueue $q) use ($originalTimeout, $extConnection) { - $consumeTimeout = $extConnection->getReadTimeout(); - try { - $extConnection->setReadTimeout($originalTimeout); - - $message = $this->convertMessage($extEnvelope); - $message->setConsumerTag($q->getConsumerTag()); - - /** - * @var AmqpConsumer - * @var callable $callback - */ - list($consumer, $callback) = $this->subscribers[$q->getConsumerTag()]; - - return call_user_func($callback, $message, $consumer); - } finally { - $extConnection->setReadTimeout($consumeTimeout); - } - }, AMQP_JUST_CONSUME); - } catch (\AMQPQueueException $e) { - if ('Consumer timeout exceed' == $e->getMessage()) { - return null; - } - - throw $e; - } finally { - $extConnection->setReadTimeout($originalTimeout); - } - } - /** * @internal It must be used here and in the consumer only - * - * @param \AMQPEnvelope $extEnvelope - * - * @return AmqpMessage */ - public function convertMessage(\AMQPEnvelope $extEnvelope) + public function convertMessage(\AMQPEnvelope $extEnvelope): InteropAmqpMessage { $message = new AmqpMessage( $extEnvelope->getBody(), diff --git a/pkg/amqp-ext/AmqpProducer.php b/pkg/amqp-ext/AmqpProducer.php index 8ed9c2fd7..fc55ca29e 100644 --- a/pkg/amqp-ext/AmqpProducer.php +++ b/pkg/amqp-ext/AmqpProducer.php @@ -9,13 +9,14 @@ use Interop\Amqp\AmqpProducer as InteropAmqpProducer; use Interop\Amqp\AmqpQueue; use Interop\Amqp\AmqpTopic; -use Interop\Queue\DeliveryDelayNotSupportedException; -use Interop\Queue\Exception; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrTopic; +use Interop\Queue\Destination; +use Interop\Queue\Exception\DeliveryDelayNotSupportedException; +use Interop\Queue\Exception\Exception; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Message; +use Interop\Queue\Producer; +use Interop\Queue\Topic; class AmqpProducer implements InteropAmqpProducer, DelayStrategyAware { @@ -46,10 +47,6 @@ class AmqpProducer implements InteropAmqpProducer, DelayStrategyAware */ private $deliveryDelay; - /** - * @param \AMQPChannel $ampqChannel - * @param AmqpContext $context - */ public function __construct(\AMQPChannel $ampqChannel, AmqpContext $context) { $this->amqpChannel = $ampqChannel; @@ -57,14 +54,12 @@ public function __construct(\AMQPChannel $ampqChannel, AmqpContext $context) } /** - * {@inheritdoc} - * * @param AmqpTopic|AmqpQueue $destination * @param AmqpMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { - $destination instanceof PsrTopic + $destination instanceof Topic ? InvalidDestinationException::assertDestinationInstanceOf($destination, AmqpTopic::class) : InvalidDestinationException::assertDestinationInstanceOf($destination, AmqpQueue::class); @@ -77,10 +72,7 @@ public function send(PsrDestination $destination, PsrMessage $message) } } - /** - * {@inheritdoc} - */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { if (null === $this->delayStrategy) { throw DeliveryDelayNotSupportedException::providerDoestNotSupportIt(); @@ -91,51 +83,36 @@ public function setDeliveryDelay($deliveryDelay) return $this; } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return $this->deliveryDelay; } - /** - * {@inheritdoc} - */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { $this->priority = $priority; return $this; } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return $this->priority; } - /** - * {@inheritdoc} - */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { $this->timeToLive = $timeToLive; return $this; } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return $this->timeToLive; } - private function doSend(AmqpDestination $destination, AmqpMessage $message) + private function doSend(AmqpDestination $destination, AmqpMessage $message): void { if (null !== $this->priority && null === $message->getPriority()) { $message->setPriority($this->priority); @@ -169,7 +146,7 @@ private function doSend(AmqpDestination $destination, AmqpMessage $message) } else { /** @var AmqpQueue $destination */ $amqpExchange = new \AMQPExchange($this->amqpChannel); - $amqpExchange->setType(AMQP_EX_TYPE_DIRECT); + $amqpExchange->setType(\AMQP_EX_TYPE_DIRECT); $amqpExchange->setName(''); $amqpExchange->publish( diff --git a/pkg/amqp-ext/AmqpSubscriptionConsumer.php b/pkg/amqp-ext/AmqpSubscriptionConsumer.php new file mode 100644 index 000000000..3d0faccb7 --- /dev/null +++ b/pkg/amqp-ext/AmqpSubscriptionConsumer.php @@ -0,0 +1,134 @@ +context = $context; + + $this->subscribers = []; + } + + public function consume(int $timeout = 0): void + { + if (empty($this->subscribers)) { + throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); + } + + if (false == (version_compare(phpversion('amqp'), '1.9.1', '>=') || '1.9.1-dev' == phpversion('amqp'))) { + // @see https://github.com/php-enqueue/enqueue-dev/issues/110 and https://github.com/pdezwart/php-amqp/issues/281 + throw new \LogicException('The AMQP extension "basic_consume" method does not work properly prior 1.9.1 version.'); + } + + /** @var \AMQPQueue $extQueue */ + $extConnection = $this->context->getExtChannel()->getConnection(); + + $originalTimeout = $extConnection->getReadTimeout(); + try { + $extConnection->setReadTimeout($timeout / 1000); + + reset($this->subscribers); + /** @var $consumer AmqpConsumer */ + list($consumer) = current($this->subscribers); + + $extQueue = new \AMQPQueue($this->context->getExtChannel()); + $extQueue->setName($consumer->getQueue()->getQueueName()); + $extQueue->consume(function (\AMQPEnvelope $extEnvelope, \AMQPQueue $q) use ($originalTimeout, $extConnection) { + $consumeTimeout = $extConnection->getReadTimeout(); + try { + $extConnection->setReadTimeout($originalTimeout); + + $message = $this->context->convertMessage($extEnvelope); + $message->setConsumerTag($q->getConsumerTag()); + + /** + * @var AmqpConsumer + * @var callable $callback + */ + list($consumer, $callback) = $this->subscribers[$q->getConsumerTag()]; + + return call_user_func($callback, $message, $consumer); + } finally { + $extConnection->setReadTimeout($consumeTimeout); + } + }, \AMQP_JUST_CONSUME); + } catch (\AMQPQueueException $e) { + if ('Consumer timeout exceed' == $e->getMessage()) { + return; + } + + throw $e; + } finally { + $extConnection->setReadTimeout($originalTimeout); + } + } + + /** + * @param AmqpConsumer $consumer + */ + public function subscribe(Consumer $consumer, callable $callback): void + { + if (false == $consumer instanceof AmqpConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', AmqpConsumer::class, $consumer::class)); + } + + if ($consumer->getConsumerTag() && array_key_exists($consumer->getConsumerTag(), $this->subscribers)) { + return; + } + + $extQueue = new \AMQPQueue($this->context->getExtChannel()); + $extQueue->setName($consumer->getQueue()->getQueueName()); + + $extQueue->consume(null, Flags::convertConsumerFlags($consumer->getFlags()), $consumer->getConsumerTag()); + + $consumerTag = $extQueue->getConsumerTag(); + $consumer->setConsumerTag($consumerTag); + $this->subscribers[$consumerTag] = [$consumer, $callback, $extQueue]; + } + + /** + * @param AmqpConsumer $consumer + */ + public function unsubscribe(Consumer $consumer): void + { + if (false == $consumer instanceof AmqpConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', AmqpConsumer::class, $consumer::class)); + } + + if (false == $consumer->getConsumerTag()) { + return; + } + + $consumerTag = $consumer->getConsumerTag(); + $consumer->setConsumerTag(null); + + list($consumer, $callback, $extQueue) = $this->subscribers[$consumerTag]; + + $extQueue->cancel($consumerTag); + unset($this->subscribers[$consumerTag]); + } + + public function unsubscribeAll(): void + { + foreach ($this->subscribers as list($consumer)) { + $this->unsubscribe($consumer); + } + } +} diff --git a/pkg/amqp-ext/Buffer.php b/pkg/amqp-ext/Buffer.php deleted file mode 100644 index a192e605b..000000000 --- a/pkg/amqp-ext/Buffer.php +++ /dev/null @@ -1,43 +0,0 @@ - [AmqpMessage, AmqpMessage ...]] - */ - private $messages; - - public function __construct() - { - $this->messages = []; - } - - /** - * @param string $consumerTag - * @param AmqpMessage $message - */ - public function push($consumerTag, AmqpMessage $message) - { - if (false == array_key_exists($consumerTag, $this->messages)) { - $this->messages[$consumerTag] = []; - } - - $this->messages[$consumerTag][] = $message; - } - - /** - * @param string $consumerTag - * - * @return AmqpMessage|null - */ - public function pop($consumerTag) - { - if (false == empty($this->messages[$consumerTag])) { - return array_shift($this->messages[$consumerTag]); - } - } -} diff --git a/pkg/amqp-ext/Flags.php b/pkg/amqp-ext/Flags.php index 778d83810..2054f5526 100644 --- a/pkg/amqp-ext/Flags.php +++ b/pkg/amqp-ext/Flags.php @@ -10,113 +10,88 @@ class Flags { - /** - * @param int $interop - * - * @return int - */ - public static function convertMessageFlags($interop) + public static function convertMessageFlags(int $interop): int { - $flags = AMQP_NOPARAM; + $flags = \AMQP_NOPARAM; if ($interop & InteropAmqpMessage::FLAG_MANDATORY) { - $flags |= AMQP_MANDATORY; + $flags |= \AMQP_MANDATORY; } if ($interop & InteropAmqpMessage::FLAG_IMMEDIATE) { - $flags |= AMQP_IMMEDIATE; + $flags |= \AMQP_IMMEDIATE; } return $flags; } - /** - * @param int $interop - * - * @return int - */ - public static function convertTopicFlags($interop) + public static function convertTopicFlags(int $interop): int { - $flags = AMQP_NOPARAM; + $flags = \AMQP_NOPARAM; $flags |= static::convertDestinationFlags($interop); if ($interop & InteropAmqpTopic::FLAG_INTERNAL) { - $flags |= AMQP_INTERNAL; + $flags |= \AMQP_INTERNAL; } return $flags; } - /** - * @param int $interop - * - * @return int - */ - public static function convertQueueFlags($interop) + public static function convertQueueFlags(int $interop): int { - $flags = AMQP_NOPARAM; + $flags = \AMQP_NOPARAM; $flags |= static::convertDestinationFlags($interop); if ($interop & InteropAmqpQueue::FLAG_EXCLUSIVE) { - $flags |= AMQP_EXCLUSIVE; + $flags |= \AMQP_EXCLUSIVE; } return $flags; } - /** - * @param int $interop - * - * @return int - */ - public static function convertDestinationFlags($interop) + public static function convertDestinationFlags(int $interop): int { - $flags = AMQP_NOPARAM; + $flags = \AMQP_NOPARAM; if ($interop & InteropAmqpDestination::FLAG_PASSIVE) { - $flags |= AMQP_PASSIVE; + $flags |= \AMQP_PASSIVE; } if ($interop & InteropAmqpDestination::FLAG_DURABLE) { - $flags |= AMQP_DURABLE; + $flags |= \AMQP_DURABLE; } if ($interop & InteropAmqpDestination::FLAG_AUTODELETE) { - $flags |= AMQP_AUTODELETE; + $flags |= \AMQP_AUTODELETE; } if ($interop & InteropAmqpDestination::FLAG_NOWAIT) { - $flags |= AMQP_NOWAIT; + $flags |= \AMQP_NOWAIT; } return $flags; } - /** - * @param int $interop - * - * @return int - */ - public static function convertConsumerFlags($interop) + public static function convertConsumerFlags(int $interop): int { - $flags = AMQP_NOPARAM; + $flags = \AMQP_NOPARAM; if ($interop & InteropAmqpConsumer::FLAG_NOLOCAL) { - $flags |= AMQP_NOLOCAL; + $flags |= \AMQP_NOLOCAL; } if ($interop & InteropAmqpConsumer::FLAG_NOACK) { - $flags |= AMQP_AUTOACK; + $flags |= \AMQP_AUTOACK; } if ($interop & InteropAmqpConsumer::FLAG_EXCLUSIVE) { - $flags |= AMQP_EXCLUSIVE; + $flags |= \AMQP_EXCLUSIVE; } if ($interop & InteropAmqpConsumer::FLAG_NOWAIT) { - $flags |= AMQP_NOWAIT; + $flags |= \AMQP_NOWAIT; } return $flags; diff --git a/pkg/amqp-ext/README.md b/pkg/amqp-ext/README.md index 52c3d7ac1..1b254a860 100644 --- a/pkg/amqp-ext/README.md +++ b/pkg/amqp-ext/README.md @@ -1,27 +1,34 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + # AMQP Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/amqp-ext.png?branch=master)](https://travis-ci.org/php-enqueue/amqp-ext) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/amqp-ext/ci.yml?branch=master)](https://github.com/php-enqueue/amqp-ext/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/amqp-ext/d/total.png)](https://packagist.org/packages/enqueue/amqp-ext) [![Latest Stable Version](https://poser.pugx.org/enqueue/amqp-ext/version.png)](https://packagist.org/packages/enqueue/amqp-ext) - -This is an implementation of [amqp interop](https://github.com/queue-interop/amqp-interop). It uses PHP [amqp extension](https://github.com/pdezwart/php-amqp) internally. + +This is an implementation of [amqp interop](https://github.com/queue-interop/amqp-interop). It uses PHP [amqp extension](https://github.com/pdezwart/php-amqp) internally. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/amqp/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/amqp-ext/Tests/AmqpConnectionFactoryTest.php b/pkg/amqp-ext/Tests/AmqpConnectionFactoryTest.php index b4e1201ef..7ea58f947 100644 --- a/pkg/amqp-ext/Tests/AmqpConnectionFactoryTest.php +++ b/pkg/amqp-ext/Tests/AmqpConnectionFactoryTest.php @@ -4,28 +4,27 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; +use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\ConnectionFactory; use PHPUnit\Framework\TestCase; class AmqpConnectionFactoryTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, AmqpConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, AmqpConnectionFactory::class); } - public function testShouldSupportAmqpExtScheme() + public function testShouldSetRabbitMqDlxDelayStrategyIfRabbitMqSchemeExtensionPresent() { - // no exception here - new AmqpConnectionFactory('amqp+ext:'); - new AmqpConnectionFactory('amqps+ext:'); + $factory = new AmqpConnectionFactory('amqp+rabbitmq:'); - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN scheme "amqp+foo" is not supported. Could be one of "amqp", "amqps", "amqp+ext", "amqps+ext" only.'); - new AmqpConnectionFactory('amqp+foo:'); + $this->assertAttributeInstanceOf(RabbitMqDlxDelayStrategy::class, 'delayStrategy', $factory); } public function testShouldCreateLazyContext() @@ -37,6 +36,6 @@ public function testShouldCreateLazyContext() $this->assertInstanceOf(AmqpContext::class, $context); $this->assertAttributeEquals(null, 'extChannel', $context); - $this->assertInternalType('callable', $this->readAttribute($context, 'extChannelFactory')); + self::assertIsCallable($this->readAttribute($context, 'extChannelFactory')); } } diff --git a/pkg/amqp-ext/Tests/AmqpConsumerTest.php b/pkg/amqp-ext/Tests/AmqpConsumerTest.php index 5d184c321..1dcc0f349 100644 --- a/pkg/amqp-ext/Tests/AmqpConsumerTest.php +++ b/pkg/amqp-ext/Tests/AmqpConsumerTest.php @@ -4,10 +4,9 @@ use Enqueue\AmqpExt\AmqpConsumer; use Enqueue\AmqpExt\AmqpContext; -use Enqueue\AmqpExt\Buffer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Amqp\Impl\AmqpQueue; -use Interop\Queue\PsrConsumer; +use Interop\Queue\Consumer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class AmqpConsumerTest extends TestCase @@ -16,21 +15,11 @@ class AmqpConsumerTest extends TestCase public function testShouldImplementConsumerInterface() { - $this->assertClassImplements(PsrConsumer::class, AmqpConsumer::class); - } - - public function testCouldBeConstructedWithContextAndQueueAndBufferAsArguments() - { - new AmqpConsumer( - $this->createContext(), - new AmqpQueue('aName'), - new Buffer(), - 'basic_get' - ); + $this->assertClassImplements(Consumer::class, AmqpConsumer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext + * @return MockObject|AmqpContext */ private function createContext() { diff --git a/pkg/amqp-ext/Tests/AmqpContextTest.php b/pkg/amqp-ext/Tests/AmqpContextTest.php index 42f23562c..2b03bb3d2 100644 --- a/pkg/amqp-ext/Tests/AmqpContextTest.php +++ b/pkg/amqp-ext/Tests/AmqpContextTest.php @@ -5,45 +5,27 @@ use Enqueue\AmqpExt\AmqpConsumer; use Enqueue\AmqpExt\AmqpContext; use Enqueue\AmqpExt\AmqpProducer; -use Enqueue\AmqpExt\Buffer; +use Enqueue\AmqpExt\AmqpSubscriptionConsumer; use Enqueue\Null\NullQueue; use Enqueue\Null\NullTopic; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use Interop\Amqp\Impl\AmqpMessage; use Interop\Amqp\Impl\AmqpQueue; use Interop\Amqp\Impl\AmqpTopic; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; +use Interop\Queue\Exception\InvalidDestinationException; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class AmqpContextTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; - public function testShouldImplementPsrContextInterface() + public function testShouldImplementQueueInteropContextInterface() { - $this->assertClassImplements(PsrContext::class, AmqpContext::class); - } - - public function testCouldBeConstructedWithExtChannelAsFirstArgument() - { - new AmqpContext($this->createExtChannelMock(), 'basic_get'); - } - - public function testCouldBeConstructedWithExtChannelCallbackFactoryAsFirstArgument() - { - new AmqpContext(function () { - return $this->createExtChannelMock(); - }, 'basic_get'); - } - - public function testShouldCreateNewBufferOnConstruct() - { - $context = new AmqpContext(function () { - return $this->createExtChannelMock(); - }, 'basic_get'); - - $this->assertAttributeInstanceOf(Buffer::class, 'buffer', $context); + $this->assertClassImplements(Context::class, AmqpContext::class); } public function testThrowIfNeitherCallbackNorExtChannelAsFirstArgument() @@ -51,12 +33,12 @@ public function testThrowIfNeitherCallbackNorExtChannelAsFirstArgument() $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The extChannel argument must be either AMQPChannel or callable that return AMQPChannel.'); - new AmqpContext(new \stdClass(), 'basic_get'); + new AmqpContext(new \stdClass()); } public function testShouldReturnAmqpMessageOnCreateMessageCallWithoutArguments() { - $context = new AmqpContext($this->createExtChannelMock(), 'basic_get'); + $context = new AmqpContext($this->createExtChannelMock()); $message = $context->createMessage(); @@ -68,7 +50,7 @@ public function testShouldReturnAmqpMessageOnCreateMessageCallWithoutArguments() public function testShouldReturnAmqpMessageOnCreateMessageCal() { - $context = new AmqpContext($this->createExtChannelMock(), 'basic_get'); + $context = new AmqpContext($this->createExtChannelMock()); $message = $context->createMessage('theBody', ['foo' => 'fooVal'], ['bar' => 'barVal']); @@ -80,7 +62,7 @@ public function testShouldReturnAmqpMessageOnCreateMessageCal() public function testShouldCreateTopicWithGivenName() { - $context = new AmqpContext($this->createExtChannelMock(), 'basic_get'); + $context = new AmqpContext($this->createExtChannelMock()); $topic = $context->createTopic('theName'); @@ -92,7 +74,7 @@ public function testShouldCreateTopicWithGivenName() public function testShouldCreateQueueWithGivenName() { - $context = new AmqpContext($this->createExtChannelMock(), 'basic_get'); + $context = new AmqpContext($this->createExtChannelMock()); $queue = $context->createQueue('theName'); @@ -105,7 +87,7 @@ public function testShouldCreateQueueWithGivenName() public function testShouldReturnAmqpProducer() { - $context = new AmqpContext($this->createExtChannelMock(), 'basic_get'); + $context = new AmqpContext($this->createExtChannelMock()); $producer = $context->createProducer(); @@ -114,9 +96,7 @@ public function testShouldReturnAmqpProducer() public function testShouldReturnAmqpConsumerForGivenQueue() { - $context = new AmqpContext($this->createExtChannelMock(), 'basic_get'); - - $buffer = $this->readAttribute($context, 'buffer'); + $context = new AmqpContext($this->createExtChannelMock()); $queue = new AmqpQueue('aName'); @@ -125,13 +105,11 @@ public function testShouldReturnAmqpConsumerForGivenQueue() $this->assertInstanceOf(AmqpConsumer::class, $consumer); $this->assertAttributeSame($context, 'context', $consumer); $this->assertAttributeSame($queue, 'queue', $consumer); - $this->assertAttributeSame($queue, 'queue', $consumer); - $this->assertAttributeSame($buffer, 'buffer', $consumer); } public function testShouldThrowIfNotAmqpQueueGivenOnCreateConsumerCall() { - $context = new AmqpContext($this->createExtChannelMock(), 'basic_get'); + $context = new AmqpContext($this->createExtChannelMock()); $this->expectException(InvalidDestinationException::class); $this->expectExceptionMessage('The destination must be an instance of Interop\Amqp\AmqpQueue but got Enqueue\Null\NullQueue.'); @@ -140,7 +118,7 @@ public function testShouldThrowIfNotAmqpQueueGivenOnCreateConsumerCall() public function testShouldThrowIfNotAmqpTopicGivenOnCreateConsumerCall() { - $context = new AmqpContext($this->createExtChannelMock(), 'basic_get'); + $context = new AmqpContext($this->createExtChannelMock()); $this->expectException(InvalidDestinationException::class); $this->expectExceptionMessage('The destination must be an instance of Interop\Amqp\AmqpTopic but got Enqueue\Null\NullTopic.'); @@ -175,7 +153,7 @@ public function testShouldDoNothingIfConnectionAlreadyClosed() ->willReturn($extConnectionMock) ; - $context = new AmqpContext($extChannelMock, 'basic_get'); + $context = new AmqpContext($extChannelMock); $context->close(); } @@ -209,7 +187,7 @@ public function testShouldCloseNotPersistedConnection() ->willReturn($extConnectionMock) ; - $context = new AmqpContext($extChannelMock, 'basic_get'); + $context = new AmqpContext($extChannelMock); $context->close(); } @@ -243,13 +221,20 @@ public function testShouldClosePersistedConnection() ->willReturn($extConnectionMock) ; - $context = new AmqpContext($extChannelMock, 'basic_get'); + $context = new AmqpContext($extChannelMock); $context->close(); } + public function testShouldReturnExpectedSubscriptionConsumerInstance() + { + $context = new AmqpContext($this->createExtChannelMock()); + + $this->assertInstanceOf(AmqpSubscriptionConsumer::class, $context->createSubscriptionConsumer()); + } + /** - * @return \PHPUnit_Framework_MockObject_MockObject|\AMQPChannel + * @return MockObject|\AMQPChannel */ private function createExtChannelMock() { @@ -257,7 +242,7 @@ private function createExtChannelMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|\AMQPChannel + * @return MockObject|\AMQPChannel */ private function createExtConnectionMock() { diff --git a/pkg/amqp-ext/Tests/AmqpProducerTest.php b/pkg/amqp-ext/Tests/AmqpProducerTest.php index 9d3278123..30c2e99ef 100644 --- a/pkg/amqp-ext/Tests/AmqpProducerTest.php +++ b/pkg/amqp-ext/Tests/AmqpProducerTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpExt\AmqpProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrProducer; +use Interop\Queue\Producer; use PHPUnit\Framework\TestCase; class AmqpProducerTest extends TestCase @@ -13,6 +13,6 @@ class AmqpProducerTest extends TestCase public function testShouldImplementProducerInterface() { - $this->assertClassImplements(PsrProducer::class, AmqpProducer::class); + $this->assertClassImplements(Producer::class, AmqpProducer::class); } } diff --git a/pkg/amqp-ext/Tests/AmqpSubscriptionConsumerTest.php b/pkg/amqp-ext/Tests/AmqpSubscriptionConsumerTest.php new file mode 100644 index 000000000..d71ddd776 --- /dev/null +++ b/pkg/amqp-ext/Tests/AmqpSubscriptionConsumerTest.php @@ -0,0 +1,27 @@ +assertTrue($rc->implementsInterface(SubscriptionConsumer::class)); + } + + /** + * @return AmqpContext|MockObject + */ + private function createAmqpContextMock() + { + return $this->createMock(AmqpContext::class); + } +} diff --git a/pkg/amqp-ext/Tests/BufferTest.php b/pkg/amqp-ext/Tests/BufferTest.php deleted file mode 100644 index 977365ce6..000000000 --- a/pkg/amqp-ext/Tests/BufferTest.php +++ /dev/null @@ -1,64 +0,0 @@ -assertAttributeSame([], 'messages', $buffer); - } - - public function testShouldReturnNullIfNoMessagesInBuffer() - { - $buffer = new Buffer(); - - $this->assertNull($buffer->pop('aConsumerTag')); - $this->assertNull($buffer->pop('anotherConsumerTag')); - } - - public function testShouldPushMessageToBuffer() - { - $fooMessage = new AmqpMessage(); - $barMessage = new AmqpMessage(); - $bazMessage = new AmqpMessage(); - - $buffer = new Buffer(); - - $buffer->push('aConsumerTag', $fooMessage); - $buffer->push('aConsumerTag', $barMessage); - - $buffer->push('anotherConsumerTag', $bazMessage); - - $this->assertAttributeSame([ - 'aConsumerTag' => [$fooMessage, $barMessage], - 'anotherConsumerTag' => [$bazMessage], - ], 'messages', $buffer); - } - - public function testShouldPopMessageFromBuffer() - { - $fooMessage = new AmqpMessage(); - $barMessage = new AmqpMessage(); - - $buffer = new Buffer(); - - $buffer->push('aConsumerTag', $fooMessage); - $buffer->push('aConsumerTag', $barMessage); - - $this->assertSame($fooMessage, $buffer->pop('aConsumerTag')); - $this->assertSame($barMessage, $buffer->pop('aConsumerTag')); - $this->assertNull($buffer->pop('aConsumerTag')); - } -} diff --git a/pkg/amqp-ext/Tests/Functional/AmqpCommonUseCasesTest.php b/pkg/amqp-ext/Tests/Functional/AmqpCommonUseCasesTest.php index 294248e4e..ee53ad90b 100644 --- a/pkg/amqp-ext/Tests/Functional/AmqpCommonUseCasesTest.php +++ b/pkg/amqp-ext/Tests/Functional/AmqpCommonUseCasesTest.php @@ -14,15 +14,15 @@ */ class AmqpCommonUseCasesTest extends TestCase { - use RabbitmqAmqpExtension; use RabbitManagementExtensionTrait; + use RabbitmqAmqpExtension; /** * @var AmqpContext */ private $amqpContext; - public function setUp() + protected function setUp(): void { $this->amqpContext = $this->buildAmqpContext(); @@ -30,7 +30,7 @@ public function setUp() $this->removeExchange('amqp_ext.test_exchange'); } - public function tearDown() + protected function tearDown(): void { $this->amqpContext->close(); } @@ -112,6 +112,7 @@ public function testProduceAndReceiveOneMessageSentDirectlyToTemporaryQueue() $queue = $this->amqpContext->createTemporaryQueue(); $message = $this->amqpContext->createMessage(__METHOD__); + $message->setDeliveryTag(145); $producer = $this->amqpContext->createProducer(); $producer->send($queue, $message); @@ -128,7 +129,7 @@ public function testProduceAndReceiveOneMessageSentDirectlyToTemporaryQueue() public function testProduceAndReceiveOneMessageSentDirectlyToTopic() { $topic = $this->amqpContext->createTopic('amqp_ext.test_exchange'); - $topic->setType(AMQP_EX_TYPE_FANOUT); + $topic->setType(\AMQP_EX_TYPE_FANOUT); $this->amqpContext->declareTopic($topic); $queue = $this->amqpContext->createQueue('amqp_ext.test'); @@ -137,6 +138,7 @@ public function testProduceAndReceiveOneMessageSentDirectlyToTopic() $this->amqpContext->bind(new AmqpBind($topic, $queue)); $message = $this->amqpContext->createMessage(__METHOD__); + $message->setDeliveryTag(145); $producer = $this->amqpContext->createProducer(); $producer->send($topic, $message); @@ -153,15 +155,16 @@ public function testProduceAndReceiveOneMessageSentDirectlyToTopic() public function testConsumerReceiveMessageFromTopicDirectly() { $topic = $this->amqpContext->createTopic('amqp_ext.test_exchange'); - $topic->setType(AMQP_EX_TYPE_FANOUT); + $topic->setType(\AMQP_EX_TYPE_FANOUT); $this->amqpContext->declareTopic($topic); $consumer = $this->amqpContext->createConsumer($topic); - //guard + // guard $this->assertNull($consumer->receive(1000)); $message = $this->amqpContext->createMessage(__METHOD__); + $message->setDeliveryTag(145); $producer = $this->amqpContext->createProducer(); $producer->send($topic, $message); @@ -176,15 +179,16 @@ public function testConsumerReceiveMessageFromTopicDirectly() public function testConsumerReceiveMessageWithZeroTimeout() { $topic = $this->amqpContext->createTopic('amqp_ext.test_exchange'); - $topic->setType(AMQP_EX_TYPE_FANOUT); + $topic->setType(\AMQP_EX_TYPE_FANOUT); $this->amqpContext->declareTopic($topic); $consumer = $this->amqpContext->createConsumer($topic); - //guard + // guard $this->assertNull($consumer->receive(1000)); $message = $this->amqpContext->createMessage(__METHOD__); + $message->setDeliveryTag(145); $producer = $this->amqpContext->createProducer(); $producer->send($topic, $message); @@ -205,6 +209,7 @@ public function testPurgeMessagesFromQueue() $consumer = $this->amqpContext->createConsumer($queue); $message = $this->amqpContext->createMessage(__METHOD__); + $message->setDeliveryTag(145); $producer = $this->amqpContext->createProducer(); $producer->send($queue, $message); diff --git a/pkg/amqp-ext/Tests/Functional/AmqpConsumptionUseCasesTest.php b/pkg/amqp-ext/Tests/Functional/AmqpConsumptionUseCasesTest.php index 54fccb719..51d5a7c54 100644 --- a/pkg/amqp-ext/Tests/Functional/AmqpConsumptionUseCasesTest.php +++ b/pkg/amqp-ext/Tests/Functional/AmqpConsumptionUseCasesTest.php @@ -9,11 +9,11 @@ use Enqueue\Consumption\Extension\ReplyExtension; use Enqueue\Consumption\QueueConsumer; use Enqueue\Consumption\Result; -use Enqueue\Test\RabbitmqAmqpExtension; use Enqueue\Test\RabbitManagementExtensionTrait; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Enqueue\Test\RabbitmqAmqpExtension; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; use PHPUnit\Framework\TestCase; /** @@ -21,22 +21,22 @@ */ class AmqpConsumptionUseCasesTest extends TestCase { - use RabbitmqAmqpExtension; use RabbitManagementExtensionTrait; + use RabbitmqAmqpExtension; /** * @var AmqpContext */ private $amqpContext; - public function setUp() + protected function setUp(): void { $this->amqpContext = $this->buildAmqpContext(); $this->removeQueue('amqp_ext.test'); } - public function tearDown() + protected function tearDown(): void { $this->amqpContext->close(); } @@ -59,7 +59,7 @@ public function testConsumeOneMessageAndExit() $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); } @@ -92,22 +92,22 @@ public function testConsumeOneMessageAndSendReplyExit() $queueConsumer->bind($replyQueue, $replyProcessor); $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); - $this->assertInstanceOf(PsrMessage::class, $replyProcessor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $replyProcessor->lastProcessedMessage); $this->assertEquals(__METHOD__.'.reply', $replyProcessor->lastProcessedMessage->getBody()); } } -class StubProcessor implements PsrProcessor +class StubProcessor implements Processor { public $result = self::ACK; - /** @var PsrMessage */ + /** @var Message */ public $lastProcessedMessage; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $this->lastProcessedMessage = $message; diff --git a/pkg/amqp-ext/Tests/Functional/AmqpRpcUseCasesTest.php b/pkg/amqp-ext/Tests/Functional/AmqpRpcUseCasesTest.php index bbc94423a..66ae69241 100644 --- a/pkg/amqp-ext/Tests/Functional/AmqpRpcUseCasesTest.php +++ b/pkg/amqp-ext/Tests/Functional/AmqpRpcUseCasesTest.php @@ -15,15 +15,15 @@ */ class AmqpRpcUseCasesTest extends TestCase { - use RabbitmqAmqpExtension; use RabbitManagementExtensionTrait; + use RabbitmqAmqpExtension; /** * @var AmqpContext */ private $amqpContext; - public function setUp() + protected function setUp(): void { $this->amqpContext = $this->buildAmqpContext(); @@ -31,7 +31,7 @@ public function setUp() $this->removeQueue('rpc.reply_test'); } - public function tearDown() + protected function tearDown(): void { $this->amqpContext->close(); } diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php deleted file mode 100644 index 18a4265ee..000000000 --- a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php deleted file mode 100644 index 9b2112b58..000000000 --- a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php b/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php deleted file mode 100644 index 3d9dcc449..000000000 --- a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php +++ /dev/null @@ -1,27 +0,0 @@ -markTestIncomplete('Seg fault'); - } - - /** - * {@inheritdoc} - */ - protected function createContext() - { - $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); - - return $factory->createContext(); - } -} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpMessageTest.php b/pkg/amqp-ext/Tests/Spec/AmqpMessageTest.php index ea36b648c..73fc4ad14 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpMessageTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpMessageTest.php @@ -3,13 +3,10 @@ namespace Enqueue\AmqpExt\Tests\Spec; use Interop\Amqp\Impl\AmqpMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class AmqpMessageTest extends PsrMessageSpec +class AmqpMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new AmqpMessage(); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpPreFetchCountTest.php b/pkg/amqp-ext/Tests/Spec/AmqpPreFetchCountTest.php deleted file mode 100644 index 265c50e04..000000000 --- a/pkg/amqp-ext/Tests/Spec/AmqpPreFetchCountTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpProducerTest.php b/pkg/amqp-ext/Tests/Spec/AmqpProducerTest.php index 5c988c101..1183e3a8e 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpProducerTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpProducerTest.php @@ -3,16 +3,13 @@ namespace Enqueue\AmqpExt\Tests\Spec; use Enqueue\AmqpExt\AmqpConnectionFactory; -use Interop\Queue\Spec\PsrProducerSpec; +use Interop\Queue\Spec\ProducerSpec; /** * @group functional */ -class AmqpProducerTest extends PsrProducerSpec +class AmqpProducerTest extends ProducerSpec { - /** - * {@inheritdoc} - */ protected function createProducer() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php index b3916896b..f79f7e635 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; use Enqueue\AmqpTools\RabbitMqDelayPluginDelayStrategy; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest extends SendAndReceiveDelayedMessageFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -26,10 +23,8 @@ protected function createContext() /** * @param AmqpContext $context - * - * {@inheritdoc} */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = parent::createQueue($context, $queueName); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php index 251ea07db..71da671f4 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest extends SendAndReceiveDelayedMessageFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -26,10 +23,8 @@ protected function createContext() /** * @param AmqpContext $context - * - * {@inheritdoc} */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = parent::createQueue($context, $queueName); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php index e8251eea0..40edcd865 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceivePriorityMessagesFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendAndReceivePriorityMessagesFromQueueTest extends SendAndReceivePriorityMessagesFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $queue->setArguments(['x-max-priority' => 10]); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php index 16a67127c..6654107c9 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveTimeToLiveMessagesFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest extends SendAndReceiveTimeToLiveMessagesFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php index 00d7e3840..5ecc00046 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php @@ -10,9 +10,6 @@ */ class AmqpSendAndReceiveTimestampAsIntengerTest extends SendAndReceiveTimestampAsIntegerSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php index 92436483b..cb45dc5a5 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php index c13da615e..fb8e62750 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; use Interop\Amqp\AmqpTopic; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromTopicSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendToAndReceiveFromTopicTest extends SendToAndReceiveFromTopicSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -24,11 +21,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php index dd5035efa..a0d93c38b 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendToAndReceiveNoWaitFromQueueTest extends SendToAndReceiveNoWaitFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php index 2db18a6ce..f9867d1b1 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; use Interop\Amqp\AmqpTopic; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromTopicSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendToAndReceiveNoWaitFromTopicTest extends SendToAndReceiveNoWaitFromTopicSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -24,11 +21,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php index be29f1a4f..058606b51 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php @@ -6,7 +6,7 @@ use Enqueue\AmqpExt\AmqpContext; use Interop\Amqp\AmqpTopic; use Interop\Amqp\Impl\AmqpBind; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveFromQueueSpec; /** @@ -14,9 +14,6 @@ */ class AmqpSendToTopicAndReceiveFromQueueTest extends SendToTopicAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -25,11 +22,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); @@ -41,11 +36,9 @@ protected function createQueue(PsrContext $context, $queueName) } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php index fa6633052..b8ac82403 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php @@ -6,7 +6,7 @@ use Enqueue\AmqpExt\AmqpContext; use Interop\Amqp\AmqpTopic; use Interop\Amqp\Impl\AmqpBind; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveNoWaitFromQueueSpec; /** @@ -14,9 +14,6 @@ */ class AmqpSendToTopicAndReceiveNoWaitFromQueueTest extends SendToTopicAndReceiveNoWaitFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -25,11 +22,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); @@ -41,11 +36,9 @@ protected function createQueue(PsrContext $context, $queueName) } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php index 4313b9b5e..0aa03cbd0 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSslSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $baseDir = realpath(__DIR__.'/../../../../'); @@ -37,11 +34,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerAddConsumerTagOnSubscribeTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerAddConsumerTagOnSubscribeTest.php new file mode 100644 index 000000000..173247262 --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerAddConsumerTagOnSubscribeTest.php @@ -0,0 +1,20 @@ +createContext(); + } +} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..c069acefd --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,41 @@ +createContext(); + $context->setQos(0, 5, false); + + return $context; + } + + /** + * @param AmqpContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var AmqpQueue $queue */ + $queue = parent::createQueue($context, $queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerConsumeUntilUnsubscribedTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..c3341c937 --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerConsumeUntilUnsubscribedTest.php @@ -0,0 +1,50 @@ +subscriptionConsumer) { + $this->subscriptionConsumer->unsubscribeAll(); + } + + parent::tearDown(); + } + + /** + * @return AmqpContext + */ + protected function createContext() + { + $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); + + $context = $factory->createContext(); + $context->setQos(0, 1, false); + + return $context; + } + + /** + * @param AmqpContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var AmqpQueue $queue */ + $queue = parent::createQueue($context, $queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerPreFetchCountTest.php similarity index 50% rename from pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php rename to pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerPreFetchCountTest.php index f467f7b66..58182946a 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerPreFetchCountTest.php @@ -3,17 +3,15 @@ namespace Enqueue\AmqpExt\Tests\Spec; use Enqueue\AmqpExt\AmqpConnectionFactory; -use Interop\Queue\Spec\Amqp\BasicConsumeBreakOnFalseSpec; +use Interop\Amqp\AmqpContext; +use Interop\Queue\Spec\Amqp\SubscriptionConsumerPreFetchCountSpec; /** * @group functional */ -class AmqpBasicConsumeBreakOnFalseTest extends BasicConsumeBreakOnFalseSpec +class AmqpSubscriptionConsumerPreFetchCountTest extends SubscriptionConsumerPreFetchCountSpec { - /** - * {@inheritdoc} - */ - protected function createContext() + protected function createContext(): AmqpContext { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest.php similarity index 53% rename from pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php rename to pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest.php index c5e139f37..f528dbc13 100644 --- a/pkg/amqp-ext/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php +++ b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest.php @@ -3,22 +3,20 @@ namespace Enqueue\AmqpExt\Tests\Spec; use Enqueue\AmqpExt\AmqpConnectionFactory; -use Interop\Queue\Spec\Amqp\BasicConsumeShouldRemoveConsumerTagOnUnsubscribeSpec; +use Interop\Amqp\AmqpContext; +use Interop\Queue\Spec\Amqp\SubscriptionConsumerRemoveConsumerTagOnUnsubscribeSpec; /** * @group functional */ -class AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest extends BasicConsumeShouldRemoveConsumerTagOnUnsubscribeSpec +class AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest extends SubscriptionConsumerRemoveConsumerTagOnUnsubscribeSpec { public function test() { $this->markTestIncomplete('Seg fault.'); } - /** - * {@inheritdoc} - */ - protected function createContext() + protected function createContext(): AmqpContext { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); diff --git a/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerStopOnFalseTest.php b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerStopOnFalseTest.php new file mode 100644 index 000000000..e017bb603 --- /dev/null +++ b/pkg/amqp-ext/Tests/Spec/AmqpSubscriptionConsumerStopOnFalseTest.php @@ -0,0 +1,41 @@ +createContext(); + $context->setQos(0, 5, false); + + return $context; + } + + /** + * @param AmqpContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var AmqpQueue $queue */ + $queue = parent::createQueue($context, $queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-ext/Tests/fix_composer_json.php b/pkg/amqp-ext/Tests/fix_composer_json.php index f025f6081..01f73c95e 100644 --- a/pkg/amqp-ext/Tests/fix_composer_json.php +++ b/pkg/amqp-ext/Tests/fix_composer_json.php @@ -6,4 +6,4 @@ $composerJson['config']['platform']['ext-amqp'] = '1.9.3'; -file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, JSON_PRETTY_PRINT)); +file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, \JSON_PRETTY_PRINT)); diff --git a/pkg/amqp-ext/composer.json b/pkg/amqp-ext/composer.json index cdbaaf1b4..99f88c507 100644 --- a/pkg/amqp-ext/composer.json +++ b/pkg/amqp-ext/composer.json @@ -6,21 +6,18 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "ext-amqp": "^1.9.3", - - "queue-interop/amqp-interop": "^0.7.4@dev", - "enqueue/amqp-tools": "^0.8.35@dev" + "php": "^8.1", + "ext-amqp": "^1.9.3|^2.0.0", + "queue-interop/amqp-interop": "^0.8.2", + "queue-interop/queue-interop": "^0.8", + "enqueue/amqp-tools": "^0.10" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "empi89/php-amqp-stubs": "*@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2", + "empi89/php-amqp-stubs": "*@dev" }, "support": { "email": "opensource@forma-pro.com", @@ -35,13 +32,10 @@ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/amqp-ext/examples/consume.php b/pkg/amqp-ext/examples/consume.php index ab13cece9..d510bf077 100644 --- a/pkg/amqp-ext/examples/consume.php +++ b/pkg/amqp-ext/examples/consume.php @@ -12,20 +12,12 @@ if ($autoload) { require_once $autoload; } else { - throw new \LogicException('Composer autoload was not found'); + throw new LogicException('Composer autoload was not found'); } use Enqueue\AmqpExt\AmqpConnectionFactory; -$config = [ - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), -]; - -$factory = new AmqpConnectionFactory($config); +$factory = new AmqpConnectionFactory(getenv('RABBITMQ_AMQP_DSN')); $context = $factory->createContext(); $queue = $context->createQueue('foo'); @@ -40,7 +32,7 @@ while (true) { if ($m = $consumer->receive(1)) { - echo $m->getBody(), PHP_EOL; + echo $m->getBody(), \PHP_EOL; $consumer->acknowledge($m); } diff --git a/pkg/amqp-ext/examples/produce.php b/pkg/amqp-ext/examples/produce.php index d529a3431..dfc4374da 100644 --- a/pkg/amqp-ext/examples/produce.php +++ b/pkg/amqp-ext/examples/produce.php @@ -12,7 +12,7 @@ if ($autoload) { require_once $autoload; } else { - throw new \LogicException('Composer autoload was not found'); + throw new LogicException('Composer autoload was not found'); } use Enqueue\AmqpExt\AmqpConnectionFactory; @@ -20,15 +20,7 @@ use Interop\Amqp\AmqpTopic; use Interop\Amqp\Impl\AmqpBind; -$config = [ - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), -]; - -$factory = new AmqpConnectionFactory($config); +$factory = new AmqpConnectionFactory(getenv('RABBITMQ_AMQP_DSN')); $context = $factory->createContext(); $topic = $context->createTopic('test.amqp.ext'); diff --git a/pkg/amqp-ext/phpunit.xml.dist b/pkg/amqp-ext/phpunit.xml.dist index 4dca142e1..1e72c01a2 100644 --- a/pkg/amqp-ext/phpunit.xml.dist +++ b/pkg/amqp-ext/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/amqp-lib/.github/workflows/ci.yml b/pkg/amqp-lib/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/amqp-lib/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/amqp-lib/.travis.yml b/pkg/amqp-lib/.travis.yml deleted file mode 100644 index b9cf57fc9..000000000 --- a/pkg/amqp-lib/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/amqp-lib/AmqpConnectionFactory.php b/pkg/amqp-lib/AmqpConnectionFactory.php index bede43086..198e6874d 100644 --- a/pkg/amqp-lib/AmqpConnectionFactory.php +++ b/pkg/amqp-lib/AmqpConnectionFactory.php @@ -1,11 +1,15 @@ addDefaultOption('login_response', null) ->addDefaultOption('locale', 'en_US') ->addDefaultOption('keepalive', false) - ->addDefaultOption('receive_method', 'basic_get') + ->addDefaultOption('channel_rpc_timeout', 0.) + ->addDefaultOption('heartbeat_on_tick', true) ->parse() ; - $supportedMethods = ['basic_get', 'basic_consume']; - if (false == in_array($this->config->getOption('receive_method'), $supportedMethods, true)) { - throw new \LogicException(sprintf( - 'Invalid "receive_method" option value "%s". It could be only "%s"', - $this->config->getOption('receive_method'), - implode('", "', $supportedMethods) - )); + if (in_array('rabbitmq', $this->config->getSchemeExtensions(), true)) { + $this->setDelayStrategy(new RabbitMqDlxDelayStrategy()); } } /** * @return AmqpContext */ - public function createContext() + public function createContext(): Context { $context = new AmqpContext($this->establishConnection(), $this->config->getConfig()); $context->setDelayStrategy($this->delayStrategy); @@ -71,18 +68,12 @@ public function createContext() return $context; } - /** - * @return ConnectionConfig - */ - public function getConfig() + public function getConfig(): ConnectionConfig { return $this->config; } - /** - * @return AbstractConnection - */ - private function establishConnection() + private function establishConnection(): AbstractConnection { if (false == $this->connection) { if ($this->config->getOption('stream')) { @@ -130,7 +121,8 @@ private function establishConnection() (int) round(min($this->config->getReadTimeout(), $this->config->getWriteTimeout())), null, $this->config->getOption('keepalive'), - (int) round($this->config->getHeartbeat()) + (int) round($this->config->getHeartbeat()), + $this->config->getOption('channel_rpc_timeout') ); } else { $con = new AMQPStreamConnection( @@ -147,7 +139,8 @@ private function establishConnection() (int) round(min($this->config->getReadTimeout(), $this->config->getWriteTimeout())), null, $this->config->getOption('keepalive'), - (int) round($this->config->getHeartbeat()) + (int) round($this->config->getHeartbeat()), + $this->config->getOption('channel_rpc_timeout') ); } } else { @@ -169,7 +162,8 @@ private function establishConnection() (int) round($this->config->getReadTimeout()), $this->config->getOption('keepalive'), (int) round($this->config->getWriteTimeout()), - (int) round($this->config->getHeartbeat()) + (int) round($this->config->getHeartbeat()), + $this->config->getOption('channel_rpc_timeout') ); } else { $con = new AMQPSocketConnection( @@ -185,7 +179,8 @@ private function establishConnection() (int) round($this->config->getReadTimeout()), $this->config->getOption('keepalive'), (int) round($this->config->getWriteTimeout()), - (int) round($this->config->getHeartbeat()) + (int) round($this->config->getHeartbeat()), + $this->config->getOption('channel_rpc_timeout') ); } } diff --git a/pkg/amqp-lib/AmqpConsumer.php b/pkg/amqp-lib/AmqpConsumer.php index cd10f39df..0534a9371 100644 --- a/pkg/amqp-lib/AmqpConsumer.php +++ b/pkg/amqp-lib/AmqpConsumer.php @@ -1,12 +1,15 @@ context = $context; $this->channel = $context->getLibChannel(); $this->queue = $queue; - $this->buffer = $buffer; - $this->receiveMethod = $receiveMethod; $this->flags = self::FLAG_NOPARAM; } - /** - * {@inheritdoc} - */ - public function setConsumerTag($consumerTag) + public function setConsumerTag(?string $consumerTag = null): void { $this->consumerTag = $consumerTag; } - /** - * {@inheritdoc} - */ - public function getConsumerTag() + public function getConsumerTag(): ?string { return $this->consumerTag; } - /** - * {@inheritdoc} - */ - public function clearFlags() + public function clearFlags(): void { $this->flags = self::FLAG_NOPARAM; } - /** - * {@inheritdoc} - */ - public function addFlag($flag) + public function addFlag(int $flag): void { $this->flags |= $flag; } - /** - * {@inheritdoc} - */ - public function getFlags() + public function getFlags(): int { return $this->flags; } - /** - * {@inheritdoc} - */ - public function setFlags($flags) + public function setFlags(int $flags): void { $this->flags = $flags; } @@ -113,43 +80,45 @@ public function setFlags($flags) /** * @return InteropAmqpQueue */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } /** - * {@inheritdoc} - * - * @return InteropAmqpMessage|null + * @return InteropAmqpMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { - if ('basic_get' == $this->receiveMethod) { - return $this->receiveBasicGet($timeout); - } + $end = microtime(true) + ($timeout / 1000); - if ('basic_consume' == $this->receiveMethod) { - return $this->receiveBasicConsume($timeout); + while (0 === $timeout || microtime(true) < $end) { + if ($message = $this->receiveNoWait()) { + return $message; + } + + usleep(100000); // 100ms } - throw new \LogicException('The "receiveMethod" is not supported'); + return null; } /** - * @return InteropAmqpMessage|null + * @return InteropAmqpMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { if ($message = $this->channel->basic_get($this->queue->getQueueName(), (bool) ($this->getFlags() & InteropAmqpConsumer::FLAG_NOACK))) { return $this->context->convertMessage($message); } + + return null; } /** * @param InteropAmqpMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); @@ -158,75 +127,11 @@ public function acknowledge(PsrMessage $message) /** * @param InteropAmqpMessage $message - * @param bool $requeue */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { InvalidMessageException::assertMessageInstanceOf($message, InteropAmqpMessage::class); $this->channel->basic_reject($message->getDeliveryTag(), $requeue); } - - /** - * @param int $timeout - * - * @return InteropAmqpMessage|null - */ - private function receiveBasicGet($timeout) - { - $end = microtime(true) + ($timeout / 1000); - - while (0 === $timeout || microtime(true) < $end) { - if ($message = $this->receiveNoWait()) { - return $message; - } - - usleep(100000); //100ms - } - } - - /** - * @param int $timeout - * - * @return InteropAmqpMessage|null - */ - private function receiveBasicConsume($timeout) - { - if (false == $this->consumerTag) { - $this->context->subscribe($this, function (InteropAmqpMessage $message) { - $this->buffer->push($message->getConsumerTag(), $message); - - return false; - }); - } - - if ($message = $this->buffer->pop($this->consumerTag)) { - return $message; - } - - while (true) { - $start = microtime(true); - - $this->context->consume($timeout); - - if ($message = $this->buffer->pop($this->consumerTag)) { - return $message; - } - - // is here when consumed message is not for this consumer - - // as timeout is infinite have to continue consumption, but it can overflow message buffer - if ($timeout <= 0) { - continue; - } - - // compute remaining timeout and continue until time is up - $stop = microtime(true); - $timeout -= ($stop - $start) * 1000; - - if ($timeout <= 0) { - break; - } - } - } } diff --git a/pkg/amqp-lib/AmqpContext.php b/pkg/amqp-lib/AmqpContext.php index 02c9bcb85..34569659d 100644 --- a/pkg/amqp-lib/AmqpContext.php +++ b/pkg/amqp-lib/AmqpContext.php @@ -1,13 +1,12 @@ config = array_replace([ - 'receive_method' => 'basic_get', 'qos_prefetch_size' => 0, 'qos_prefetch_count' => 1, 'qos_global' => false, ], $config); $this->connection = $connection; - $this->buffer = new Buffer(); - $this->subscribers = []; } /** - * @param string|null $body - * @param array $properties - * @param array $headers - * * @return InteropAmqpMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new AmqpMessage($body, $properties, $headers); } /** - * @param string $name - * * @return InteropAmqpQueue */ - public function createQueue($name) + public function createQueue(string $name): Queue { return new AmqpQueue($name); } /** - * @param string $name - * * @return InteropAmqpTopic */ - public function createTopic($name) + public function createTopic(string $name): Topic { return new AmqpTopic($name); } /** - * @param PsrDestination $destination + * @param InteropAmqpTopic|InteropAmqpQueue $destination * * @return AmqpConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { - $destination instanceof PsrTopic + $destination instanceof Topic ? InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpTopic::class) : InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpQueue::class) ; @@ -125,24 +99,24 @@ public function createConsumer(PsrDestination $destination) $queue = $this->createTemporaryQueue(); $this->bind(new AmqpBind($destination, $queue, $queue->getQueueName())); - return new AmqpConsumer($this, $queue, $this->buffer, $this->config['receive_method']); + return new AmqpConsumer($this, $queue); } - return new AmqpConsumer($this, $destination, $this->buffer, $this->config['receive_method']); + return new AmqpConsumer($this, $destination); } /** - * {@inheritdoc} + * @return AmqpSubscriptionConsumer */ - public function createSubscriptionConsumer() + public function createSubscriptionConsumer(): SubscriptionConsumer { - return new SubscriptionConsumer($this); + return new AmqpSubscriptionConsumer($this, (bool) $this->config['heartbeat_on_tick']); } /** * @return AmqpProducer */ - public function createProducer() + public function createProducer(): Producer { $producer = new AmqpProducer($this->getLibChannel(), $this); $producer->setDelayStrategy($this->delayStrategy); @@ -153,7 +127,7 @@ public function createProducer() /** * @return InteropAmqpQueue */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { list($name) = $this->getLibChannel()->queue_declare('', false, false, true, false); @@ -163,10 +137,7 @@ public function createTemporaryQueue() return $queue; } - /** - * {@inheritdoc} - */ - public function declareTopic(InteropAmqpTopic $topic) + public function declareTopic(InteropAmqpTopic $topic): void { $this->getLibChannel()->exchange_declare( $topic->getTopicName(), @@ -180,10 +151,7 @@ public function declareTopic(InteropAmqpTopic $topic) ); } - /** - * {@inheritdoc} - */ - public function deleteTopic(InteropAmqpTopic $topic) + public function deleteTopic(InteropAmqpTopic $topic): void { $this->getLibChannel()->exchange_delete( $topic->getTopicName(), @@ -192,10 +160,7 @@ public function deleteTopic(InteropAmqpTopic $topic) ); } - /** - * {@inheritdoc} - */ - public function declareQueue(InteropAmqpQueue $queue) + public function declareQueue(InteropAmqpQueue $queue): int { list(, $messageCount) = $this->getLibChannel()->queue_declare( $queue->getQueueName(), @@ -207,13 +172,10 @@ public function declareQueue(InteropAmqpQueue $queue) $queue->getArguments() ? new AMQPTable($queue->getArguments()) : null ); - return $messageCount; + return $messageCount ?? 0; } - /** - * {@inheritdoc} - */ - public function deleteQueue(InteropAmqpQueue $queue) + public function deleteQueue(InteropAmqpQueue $queue): void { $this->getLibChannel()->queue_delete( $queue->getQueueName(), @@ -224,20 +186,19 @@ public function deleteQueue(InteropAmqpQueue $queue) } /** - * {@inheritdoc} + * @param AmqpQueue $queue */ - public function purgeQueue(InteropAmqpQueue $queue) + public function purgeQueue(Queue $queue): void { + InvalidDestinationException::assertDestinationInstanceOf($queue, InteropAmqpQueue::class); + $this->getLibChannel()->queue_purge( $queue->getQueueName(), (bool) ($queue->getFlags() & InteropAmqpQueue::FLAG_NOWAIT) ); } - /** - * {@inheritdoc} - */ - public function bind(InteropAmqpBind $bind) + public function bind(InteropAmqpBind $bind): void { if ($bind->getSource() instanceof InteropAmqpQueue && $bind->getTarget() instanceof InteropAmqpQueue) { throw new Exception('Is not possible to bind queue to queue. It is possible to bind topic to queue or topic to topic'); @@ -259,7 +220,7 @@ public function bind(InteropAmqpBind $bind) $bind->getTarget()->getTopicName(), $bind->getRoutingKey(), (bool) ($bind->getFlags() & InteropAmqpBind::FLAG_NOWAIT), - $bind->getArguments() + new AMQPTable($bind->getArguments()) ); // bind exchange to queue } else { @@ -268,15 +229,12 @@ public function bind(InteropAmqpBind $bind) $bind->getSource()->getTopicName(), $bind->getRoutingKey(), (bool) ($bind->getFlags() & InteropAmqpBind::FLAG_NOWAIT), - $bind->getArguments() + new AMQPTable($bind->getArguments()) ); } } - /** - * {@inheritdoc} - */ - public function unbind(InteropAmqpBind $bind) + public function unbind(InteropAmqpBind $bind): void { if ($bind->getSource() instanceof InteropAmqpQueue && $bind->getTarget() instanceof InteropAmqpQueue) { throw new Exception('Is not possible to bind queue to queue. It is possible to bind topic to queue or topic to topic'); @@ -310,134 +268,19 @@ public function unbind(InteropAmqpBind $bind) } } - public function close() + public function close(): void { if ($this->channel) { $this->channel->close(); } } - /** - * {@inheritdoc} - */ - public function setQos($prefetchSize, $prefetchCount, $global) + public function setQos(int $prefetchSize, int $prefetchCount, bool $global): void { $this->getLibChannel()->basic_qos($prefetchSize, $prefetchCount, $global); } - /** - * @deprecated since 0.8.34 will be removed in 0.9 - * - * {@inheritdoc} - */ - public function subscribe(InteropAmqpConsumer $consumer, callable $callback) - { - if ($consumer->getConsumerTag() && array_key_exists($consumer->getConsumerTag(), $this->subscribers)) { - return; - } - - $libCallback = function (LibAMQPMessage $message) { - $receivedMessage = $this->convertMessage($message); - $receivedMessage->setConsumerTag($message->delivery_info['consumer_tag']); - - /** - * @var AmqpConsumer - * @var callable $callback - */ - list($consumer, $callback) = $this->subscribers[$message->delivery_info['consumer_tag']]; - - if (false === call_user_func($callback, $receivedMessage, $consumer)) { - throw new StopBasicConsumptionException(); - } - }; - - $consumerTag = $this->getLibChannel()->basic_consume( - $consumer->getQueue()->getQueueName(), - $consumer->getConsumerTag(), - (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOLOCAL), - (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOACK), - (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_EXCLUSIVE), - (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOWAIT), - $libCallback - ); - - if (empty($consumerTag)) { - throw new Exception('Got empty consumer tag'); - } - - $consumer->setConsumerTag($consumerTag); - - $this->subscribers[$consumerTag] = [$consumer, $callback]; - } - - /** - * @deprecated since 0.8.34 will be removed in 0.9 - * - * {@inheritdoc} - */ - public function unsubscribe(InteropAmqpConsumer $consumer) - { - if (false == $consumer->getConsumerTag()) { - return; - } - - $consumerTag = $consumer->getConsumerTag(); - - $this->getLibChannel()->basic_cancel($consumerTag); - - $consumer->setConsumerTag(null); - unset($this->subscribers[$consumerTag], $this->getLibChannel()->callbacks[$consumerTag]); - } - - /** - * @deprecated since 0.8.34 will be removed in 0.9 - * - * {@inheritdoc} - */ - public function consume($timeout = 0) - { - if (empty($this->subscribers)) { - throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); - } - - $signalHandler = new SignalSocketHelper(); - $signalHandler->beforeSocket(); - - try { - while (true) { - $start = microtime(true); - - $this->channel->wait(null, false, $timeout / 1000); - - if ($timeout <= 0) { - continue; - } - - // compute remaining timeout and continue until time is up - $stop = microtime(true); - $timeout -= ($stop - $start) * 1000; - - if ($timeout <= 0) { - break; - } - } - } catch (AMQPTimeoutException $e) { - } catch (StopBasicConsumptionException $e) { - } catch (AMQPIOWaitException $e) { - if ($signalHandler->wasThereSignal()) { - return; - } - - throw $e; - } finally { - $signalHandler->afterSocket(); - } - } - - /** - * @return AMQPChannel - */ - public function getLibChannel() + public function getLibChannel(): AMQPChannel { if (null === $this->channel) { $this->channel = $this->connection->channel(); @@ -453,12 +296,8 @@ public function getLibChannel() /** * @internal It must be used here and in the consumer only - * - * @param LibAMQPMessage $amqpMessage - * - * @return InteropAmqpMessage */ - public function convertMessage(LibAMQPMessage $amqpMessage) + public function convertMessage(LibAMQPMessage $amqpMessage): InteropAmqpMessage { $headers = new AMQPTable($amqpMessage->get_properties()); $headers = $headers->getNativeData(); @@ -470,9 +309,9 @@ public function convertMessage(LibAMQPMessage $amqpMessage) unset($headers['application_headers']); $message = new AmqpMessage($amqpMessage->getBody(), $properties, $headers); - $message->setDeliveryTag($amqpMessage->delivery_info['delivery_tag']); - $message->setRedelivered($amqpMessage->delivery_info['redelivered']); - $message->setRoutingKey($amqpMessage->delivery_info['routing_key']); + $message->setDeliveryTag((int) $amqpMessage->getDeliveryTag()); + $message->setRedelivered($amqpMessage->isRedelivered()); + $message->setRoutingKey($amqpMessage->getRoutingKey()); return $message; } diff --git a/pkg/amqp-lib/AmqpProducer.php b/pkg/amqp-lib/AmqpProducer.php index e5537b137..928597298 100644 --- a/pkg/amqp-lib/AmqpProducer.php +++ b/pkg/amqp-lib/AmqpProducer.php @@ -1,5 +1,7 @@ channel = $channel; @@ -60,14 +59,12 @@ public function __construct(AMQPChannel $channel, AmqpContext $context) } /** - * {@inheritdoc} - * * @param InteropAmqpTopic|InteropAmqpQueue $destination * @param InteropAmqpMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { - $destination instanceof PsrTopic + $destination instanceof Topic ? InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpTopic::class) : InvalidDestinationException::assertDestinationInstanceOf($destination, InteropAmqpQueue::class) ; @@ -82,9 +79,9 @@ public function send(PsrDestination $destination, PsrMessage $message) } /** - * {@inheritdoc} + * @return self */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { if (null === $this->delayStrategy) { throw DeliveryDelayNotSupportedException::providerDoestNotSupportIt(); @@ -95,51 +92,42 @@ public function setDeliveryDelay($deliveryDelay) return $this; } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return $this->deliveryDelay; } /** - * {@inheritdoc} + * @return self */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { $this->priority = $priority; return $this; } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return $this->priority; } /** - * {@inheritdoc} + * @return self */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { $this->timeToLive = $timeToLive; return $this; } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return $this->timeToLive; } - private function doSend(InteropAmqpDestination $destination, InteropAmqpMessage $message) + private function doSend(InteropAmqpDestination $destination, InteropAmqpMessage $message): void { if (null !== $this->priority && null === $message->getPriority()) { $message->setPriority($this->priority); diff --git a/pkg/amqp-lib/AmqpSubscriptionConsumer.php b/pkg/amqp-lib/AmqpSubscriptionConsumer.php new file mode 100644 index 000000000..f96c4e49a --- /dev/null +++ b/pkg/amqp-lib/AmqpSubscriptionConsumer.php @@ -0,0 +1,164 @@ +subscribers = []; + $this->context = $context; + $this->heartbeatOnTick = $heartbeatOnTick; + } + + public function consume(int $timeout = 0): void + { + if (empty($this->subscribers)) { + throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); + } + + $signalHandler = new SignalSocketHelper(); + $signalHandler->beforeSocket(); + + $heartbeatOnTick = function (AmqpContext $context) { + $context->getLibChannel()->getConnection()->checkHeartBeat(); + }; + + $this->heartbeatOnTick && register_tick_function($heartbeatOnTick, $this->context); + + try { + while (true) { + $start = microtime(true); + + $this->context->getLibChannel()->wait(null, false, $timeout / 1000); + + if ($timeout <= 0) { + continue; + } + + // compute remaining timeout and continue until time is up + $stop = microtime(true); + $timeout -= ($stop - $start) * 1000; + + if ($timeout <= 0) { + break; + } + } + } catch (AMQPTimeoutException $e) { + } catch (StopBasicConsumptionException $e) { + } catch (AMQPIOWaitException $e) { + if ($signalHandler->wasThereSignal()) { + return; + } + + throw $e; + } finally { + $signalHandler->afterSocket(); + + $this->heartbeatOnTick && unregister_tick_function($heartbeatOnTick); + } + } + + /** + * @param AmqpConsumer $consumer + */ + public function subscribe(Consumer $consumer, callable $callback): void + { + if (false == $consumer instanceof AmqpConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', AmqpConsumer::class, $consumer::class)); + } + + if ($consumer->getConsumerTag() && array_key_exists($consumer->getConsumerTag(), $this->subscribers)) { + return; + } + + $libCallback = function (LibAMQPMessage $message) { + $receivedMessage = $this->context->convertMessage($message); + $receivedMessage->setConsumerTag($message->getConsumerTag()); + + /** + * @var AmqpConsumer + * @var callable $callback + */ + list($consumer, $callback) = $this->subscribers[$message->getConsumerTag()]; + + if (false === call_user_func($callback, $receivedMessage, $consumer)) { + throw new StopBasicConsumptionException(); + } + }; + + $consumerTag = $this->context->getLibChannel()->basic_consume( + $consumer->getQueue()->getQueueName(), + $consumer->getConsumerTag(), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOLOCAL), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOACK), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_EXCLUSIVE), + (bool) ($consumer->getFlags() & InteropAmqpConsumer::FLAG_NOWAIT), + $libCallback + ); + + if (empty($consumerTag)) { + throw new Exception('Got empty consumer tag'); + } + + $consumer->setConsumerTag($consumerTag); + + $this->subscribers[$consumerTag] = [$consumer, $callback]; + } + + /** + * @param AmqpConsumer $consumer + */ + public function unsubscribe(Consumer $consumer): void + { + if (false == $consumer instanceof AmqpConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', AmqpConsumer::class, $consumer::class)); + } + + if (false == $consumer->getConsumerTag()) { + return; + } + + $consumerTag = $consumer->getConsumerTag(); + + $this->context->getLibChannel()->basic_cancel($consumerTag); + + $consumer->setConsumerTag(null); + unset($this->subscribers[$consumerTag], $this->context->getLibChannel()->callbacks[$consumerTag]); + } + + public function unsubscribeAll(): void + { + foreach ($this->subscribers as list($consumer)) { + $this->unsubscribe($consumer); + } + } +} diff --git a/pkg/amqp-lib/Buffer.php b/pkg/amqp-lib/Buffer.php deleted file mode 100644 index 27732806e..000000000 --- a/pkg/amqp-lib/Buffer.php +++ /dev/null @@ -1,43 +0,0 @@ - [AmqpMessage, AmqpMessage ...]] - */ - private $messages; - - public function __construct() - { - $this->messages = []; - } - - /** - * @param string $consumerTag - * @param AmqpMessage $message - */ - public function push($consumerTag, AmqpMessage $message) - { - if (false == array_key_exists($consumerTag, $this->messages)) { - $this->messages[$consumerTag] = []; - } - - $this->messages[$consumerTag][] = $message; - } - - /** - * @param string $consumerTag - * - * @return AmqpMessage|null - */ - public function pop($consumerTag) - { - if (false == empty($this->messages[$consumerTag])) { - return array_shift($this->messages[$consumerTag]); - } - } -} diff --git a/pkg/amqp-lib/README.md b/pkg/amqp-lib/README.md index cadc564a3..f85ce7c5f 100644 --- a/pkg/amqp-lib/README.md +++ b/pkg/amqp-lib/README.md @@ -1,27 +1,36 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # AMQP Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/amqp-lib.png?branch=master)](https://travis-ci.org/php-enqueue/amqp-lib) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/amqp-lib/ci.yml?branch=master)](https://github.com/php-enqueue/amqp-lib/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/amqp-lib/d/total.png)](https://packagist.org/packages/enqueue/amqp-lib) [![Latest Stable Version](https://poser.pugx.org/enqueue/amqp-lib/version.png)](https://packagist.org/packages/enqueue/amqp-lib) - -This is an implementation of [amqp interop](https://github.com/queue-interop/amqp-interop). It uses [php-amqplib](https://github.com/php-amqplib/php-amqplib) internally. + +This is an implementation of [amqp interop](https://github.com/queue-interop/amqp-interop). It uses [php-amqplib](https://github.com/php-amqplib/php-amqplib) internally. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/amqp_lib/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/amqp-lib/StopBasicConsumptionException.php b/pkg/amqp-lib/StopBasicConsumptionException.php index 14d6848e0..99c9ea162 100644 --- a/pkg/amqp-lib/StopBasicConsumptionException.php +++ b/pkg/amqp-lib/StopBasicConsumptionException.php @@ -1,5 +1,7 @@ assertClassImplements(PsrConnectionFactory::class, AmqpConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, AmqpConnectionFactory::class); } - public function testShouldSupportAmqpLibScheme() + public function testShouldSetRabbitMqDlxDelayStrategyIfRabbitMqSchemeExtensionPresent() { - // no exception here - new AmqpConnectionFactory('amqp+lib:'); - new AmqpConnectionFactory('amqps+lib:'); + $factory = new AmqpConnectionFactory('amqp+rabbitmq:'); - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN scheme "amqp+foo" is not supported. Could be one of "amqp", "amqps", "amqp+lib'); - new AmqpConnectionFactory('amqp+foo:'); + $this->assertAttributeInstanceOf(RabbitMqDlxDelayStrategy::class, 'delayStrategy', $factory); } } diff --git a/pkg/amqp-lib/Tests/AmqpConsumerTest.php b/pkg/amqp-lib/Tests/AmqpConsumerTest.php index 52d083ef1..3961e1ab9 100644 --- a/pkg/amqp-lib/Tests/AmqpConsumerTest.php +++ b/pkg/amqp-lib/Tests/AmqpConsumerTest.php @@ -4,15 +4,15 @@ use Enqueue\AmqpLib\AmqpConsumer; use Enqueue\AmqpLib\AmqpContext; -use Enqueue\AmqpLib\Buffer; use Enqueue\Null\NullMessage; use Enqueue\Test\ClassExtensionTrait; use Enqueue\Test\WriteAttributeTrait; use Interop\Amqp\Impl\AmqpMessage; use Interop\Amqp\Impl\AmqpQueue; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrConsumer; +use Interop\Queue\Consumer; +use Interop\Queue\Exception\InvalidMessageException; use PhpAmqpLib\Channel\AMQPChannel; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class AmqpConsumerTest extends TestCase @@ -22,16 +22,16 @@ class AmqpConsumerTest extends TestCase public function testShouldImplementConsumerInterface() { - $this->assertClassImplements(PsrConsumer::class, AmqpConsumer::class); + $this->assertClassImplements(Consumer::class, AmqpConsumer::class); } - public function testCouldBeConstructedWithContextAndQueueAndBufferAsArguments() + public function testCouldBeConstructedWithContextAndQueueAsArguments() { - new AmqpConsumer( - $this->createContextMock(), - new AmqpQueue('aName'), - new Buffer(), - 'basic_get' + self::assertInstanceOf(AmqpConsumer::class, + new AmqpConsumer( + $this->createContextMock(), + new AmqpQueue('aName') + ) ); } @@ -39,14 +39,14 @@ public function testShouldReturnQueue() { $queue = new AmqpQueue('aName'); - $consumer = new AmqpConsumer($this->createContextMock(), $queue, new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), $queue); $this->assertSame($queue, $consumer->getQueue()); } public function testOnAcknowledgeShouldThrowExceptionIfNotAmqpMessage() { - $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName')); $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of Interop\Amqp\AmqpMessage but'); @@ -56,7 +56,7 @@ public function testOnAcknowledgeShouldThrowExceptionIfNotAmqpMessage() public function testOnRejectShouldThrowExceptionIfNotAmqpMessage() { - $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($this->createContextMock(), new AmqpQueue('aName')); $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of Interop\Amqp\AmqpMessage but'); @@ -70,7 +70,7 @@ public function testOnAcknowledgeShouldAcknowledgeMessage() $channel ->expects($this->once()) ->method('basic_ack') - ->with('delivery-tag') + ->with(167) ; $context = $this->createContextMock(); @@ -80,10 +80,10 @@ public function testOnAcknowledgeShouldAcknowledgeMessage() ->willReturn($channel) ; - $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName')); $message = new AmqpMessage(); - $message->setDeliveryTag('delivery-tag'); + $message->setDeliveryTag(167); $consumer->acknowledge($message); } @@ -94,7 +94,7 @@ public function testOnRejectShouldRejectMessage() $channel ->expects($this->once()) ->method('basic_reject') - ->with('delivery-tag', $this->isTrue()) + ->with(125, $this->isTrue()) ; $context = $this->createContextMock(); @@ -104,10 +104,10 @@ public function testOnRejectShouldRejectMessage() ->willReturn($channel) ; - $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName')); $message = new AmqpMessage(); - $message->setDeliveryTag('delivery-tag'); + $message->setDeliveryTag(125); $consumer->reject($message, true); } @@ -115,10 +115,7 @@ public function testOnRejectShouldRejectMessage() public function testShouldReturnMessageOnReceiveNoWait() { $libMessage = new \PhpAmqpLib\Message\AMQPMessage('body'); - $libMessage->delivery_info['delivery_tag'] = 'delivery-tag'; - $libMessage->delivery_info['routing_key'] = 'routing-key'; - $libMessage->delivery_info['redelivered'] = true; - $libMessage->delivery_info['routing_key'] = 'routing-key'; + $libMessage->setDeliveryInfo('delivery-tag', true, '', 'routing-key'); $message = new AmqpMessage(); @@ -142,7 +139,7 @@ public function testShouldReturnMessageOnReceiveNoWait() ->willReturn($message) ; - $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName')); $receivedMessage = $consumer->receiveNoWait(); @@ -152,9 +149,7 @@ public function testShouldReturnMessageOnReceiveNoWait() public function testShouldReturnMessageOnReceiveWithReceiveMethodBasicGet() { $libMessage = new \PhpAmqpLib\Message\AMQPMessage('body'); - $libMessage->delivery_info['delivery_tag'] = 'delivery-tag'; - $libMessage->delivery_info['routing_key'] = 'routing-key'; - $libMessage->delivery_info['redelivered'] = true; + $libMessage->setDeliveryInfo('delivery-tag', true, '', 'routing-key'); $message = new AmqpMessage(); @@ -178,7 +173,7 @@ public function testShouldReturnMessageOnReceiveWithReceiveMethodBasicGet() ->willReturn($message) ; - $consumer = new AmqpConsumer($context, new AmqpQueue('aName'), new Buffer(), 'basic_get'); + $consumer = new AmqpConsumer($context, new AmqpQueue('aName')); $receivedMessage = $consumer->receive(); @@ -186,7 +181,7 @@ public function testShouldReturnMessageOnReceiveWithReceiveMethodBasicGet() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext + * @return MockObject|AmqpContext */ public function createContextMock() { @@ -194,7 +189,7 @@ public function createContextMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AMQPChannel + * @return MockObject|AMQPChannel */ public function createLibChannelMock() { diff --git a/pkg/amqp-lib/Tests/AmqpContextTest.php b/pkg/amqp-lib/Tests/AmqpContextTest.php index be52235e7..4cfde3c14 100644 --- a/pkg/amqp-lib/Tests/AmqpContextTest.php +++ b/pkg/amqp-lib/Tests/AmqpContextTest.php @@ -3,12 +3,14 @@ namespace Enqueue\AmqpLib\Tests; use Enqueue\AmqpLib\AmqpContext; +use Enqueue\AmqpLib\AmqpSubscriptionConsumer; use Interop\Amqp\Impl\AmqpBind; use Interop\Amqp\Impl\AmqpQueue; use Interop\Amqp\Impl\AmqpTopic; use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Connection\AbstractConnection; use PhpAmqpLib\Wire\AMQPTable; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class AmqpContextTest extends TestCase @@ -48,7 +50,7 @@ public function testShouldDeclareTopic() $topic->addFlag(AmqpTopic::FLAG_INTERNAL); $topic->addFlag(AmqpTopic::FLAG_AUTODELETE); - $session = new AmqpContext($connection); + $session = new AmqpContext($connection, []); $session->declareTopic($topic); } @@ -78,7 +80,7 @@ public function testShouldDeleteTopic() $topic->addFlag(AmqpTopic::FLAG_IFUNUSED); $topic->addFlag(AmqpTopic::FLAG_NOWAIT); - $session = new AmqpContext($connection); + $session = new AmqpContext($connection, []); $session->deleteTopic($topic); } @@ -98,6 +100,7 @@ public function testShouldDeclareQueue() $this->isInstanceOf(AMQPTable::class), $this->isNull() ) + ->willReturn([null, 123]) ; $connection = $this->createConnectionMock(); @@ -116,10 +119,36 @@ public function testShouldDeclareQueue() $queue->addFlag(AmqpQueue::FLAG_EXCLUSIVE); $queue->addFlag(AmqpQueue::FLAG_NOWAIT); - $session = new AmqpContext($connection); + $session = new AmqpContext($connection, []); $session->declareQueue($queue); } + public function testShouldReturnCurrentMessageCountOnDeclareQueue() + { + $expectedCount = 1256; + + $channel = $this->createChannelMock(); + $channel + ->expects($this->once()) + ->method('queue_declare') + ->willReturn([null, $expectedCount]) + ; + + $connection = $this->createConnectionMock(); + $connection + ->expects($this->once()) + ->method('channel') + ->willReturn($channel) + ; + + $queue = new AmqpQueue('name'); + + $session = new AmqpContext($connection, []); + $actualCount = $session->declareQueue($queue); + + $this->assertSame($expectedCount, $actualCount); + } + public function testShouldDeleteQueue() { $channel = $this->createChannelMock(); @@ -147,7 +176,7 @@ public function testShouldDeleteQueue() $queue->addFlag(AmqpQueue::FLAG_IFEMPTY); $queue->addFlag(AmqpQueue::FLAG_NOWAIT); - $session = new AmqpContext($connection); + $session = new AmqpContext($connection, []); $session->deleteQueue($queue); } @@ -170,7 +199,7 @@ public function testBindShouldBindTopicToTopic() ->willReturn($channel) ; - $context = new AmqpContext($connection); + $context = new AmqpContext($connection, []); $context->bind(new AmqpBind($target, $source, 'routing-key', 12345)); } @@ -193,7 +222,7 @@ public function testBindShouldBindTopicToQueue() ->willReturn($channel) ; - $context = new AmqpContext($connection); + $context = new AmqpContext($connection, []); $context->bind(new AmqpBind($target, $source, 'routing-key', 12345)); $context->bind(new AmqpBind($source, $target, 'routing-key', 12345)); } @@ -217,7 +246,7 @@ public function testShouldUnBindTopicFromTopic() ->willReturn($channel) ; - $context = new AmqpContext($connection); + $context = new AmqpContext($connection, []); $context->unbind(new AmqpBind($target, $source, 'routing-key', 12345)); } @@ -240,7 +269,7 @@ public function testShouldUnBindTopicFromQueue() ->willReturn($channel) ; - $context = new AmqpContext($connection); + $context = new AmqpContext($connection, []); $context->unbind(new AmqpBind($target, $source, 'routing-key', 12345, ['key' => 'value'])); $context->unbind(new AmqpBind($source, $target, 'routing-key', 12345, ['key' => 'value'])); } @@ -260,7 +289,7 @@ public function testShouldCloseChannelConnection() ->willReturn($channel) ; - $context = new AmqpContext($connection); + $context = new AmqpContext($connection, []); $context->createProducer(); $context->close(); @@ -285,7 +314,7 @@ public function testShouldPurgeQueue() ->willReturn($channel) ; - $context = new AmqpContext($connection); + $context = new AmqpContext($connection, []); $context->purgeQueue($queue); } @@ -310,12 +339,19 @@ public function testShouldSetQos() ->willReturn($channel) ; - $context = new AmqpContext($connection); + $context = new AmqpContext($connection, []); $context->setQos(123, 456, true); } + public function testShouldReturnExpectedSubscriptionConsumerInstance() + { + $context = new AmqpContext($this->createConnectionMock(), ['heartbeat_on_tick' => true]); + + $this->assertInstanceOf(AmqpSubscriptionConsumer::class, $context->createSubscriptionConsumer()); + } + /** - * @return \PHPUnit_Framework_MockObject_MockObject|AbstractConnection + * @return MockObject|AbstractConnection */ public function createConnectionMock() { @@ -323,7 +359,7 @@ public function createConnectionMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AMQPChannel + * @return MockObject|AMQPChannel */ public function createChannelMock() { diff --git a/pkg/amqp-lib/Tests/AmqpProducerTest.php b/pkg/amqp-lib/Tests/AmqpProducerTest.php index 8ccf419ac..5746d911a 100644 --- a/pkg/amqp-lib/Tests/AmqpProducerTest.php +++ b/pkg/amqp-lib/Tests/AmqpProducerTest.php @@ -8,14 +8,15 @@ use Interop\Amqp\Impl\AmqpMessage; use Interop\Amqp\Impl\AmqpQueue; use Interop\Amqp\Impl\AmqpTopic; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProducer; +use Interop\Queue\Destination; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Message; +use Interop\Queue\Producer; use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Message\AMQPMessage as LibAMQPMessage; use PhpAmqpLib\Wire\AMQPTable; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class AmqpProducerTest extends TestCase @@ -24,12 +25,15 @@ class AmqpProducerTest extends TestCase public function testCouldBeConstructedWithRequiredArguments() { - new AmqpProducer($this->createAmqpChannelMock(), $this->createContextMock()); + self::assertInstanceOf( + AmqpProducer::class, + new AmqpProducer($this->createAmqpChannelMock(), $this->createContextMock()) + ); } - public function testShouldImplementPsrProducerInterface() + public function testShouldImplementProducerInterface() { - $this->assertClassImplements(PsrProducer::class, AmqpProducer::class); + $this->assertClassImplements(Producer::class, AmqpProducer::class); } public function testShouldThrowExceptionWhenDestinationTypeIsInvalid() @@ -61,9 +65,9 @@ public function testShouldPublishMessageToTopic() ->expects($this->once()) ->method('basic_publish') ->with($this->isInstanceOf(LibAMQPMessage::class), 'topic', 'routing-key') - ->will($this->returnCallback(function (LibAMQPMessage $message) use (&$amqpMessage) { + ->willReturnCallback(function (LibAMQPMessage $message) use (&$amqpMessage) { $amqpMessage = $message; - })) + }) ; $topic = new AmqpTopic('topic'); @@ -86,9 +90,9 @@ public function testShouldPublishMessageToQueue() ->expects($this->once()) ->method('basic_publish') ->with($this->isInstanceOf(LibAMQPMessage::class), $this->isEmpty(), 'queue') - ->will($this->returnCallback(function (LibAMQPMessage $message) use (&$amqpMessage) { + ->willReturnCallback(function (LibAMQPMessage $message) use (&$amqpMessage) { $amqpMessage = $message; - })) + }) ; $queue = new AmqpQueue('queue'); @@ -107,9 +111,9 @@ public function testShouldSetMessageHeaders() $channel ->expects($this->once()) ->method('basic_publish') - ->will($this->returnCallback(function (LibAMQPMessage $message) use (&$amqpMessage) { + ->willReturnCallback(function (LibAMQPMessage $message) use (&$amqpMessage) { $amqpMessage = $message; - })) + }) ; $producer = new AmqpProducer($channel, $this->createContextMock()); @@ -126,9 +130,9 @@ public function testShouldSetMessageProperties() $channel ->expects($this->once()) ->method('basic_publish') - ->will($this->returnCallback(function (LibAMQPMessage $message) use (&$amqpMessage) { + ->willReturnCallback(function (LibAMQPMessage $message) use (&$amqpMessage) { $amqpMessage = $message; - })) + }) ; $producer = new AmqpProducer($channel, $this->createContextMock()); @@ -142,23 +146,23 @@ public function testShouldSetMessageProperties() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrMessage + * @return MockObject|Message */ private function createMessageMock() { - return $this->createMock(PsrMessage::class); + return $this->createMock(Message::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrDestination + * @return MockObject|Destination */ private function createDestinationMock() { - return $this->createMock(PsrDestination::class); + return $this->createMock(Destination::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AMQPChannel + * @return MockObject|AMQPChannel */ private function createAmqpChannelMock() { @@ -166,7 +170,7 @@ private function createAmqpChannelMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext + * @return MockObject|AmqpContext */ private function createContextMock() { diff --git a/pkg/amqp-lib/Tests/AmqpSubscriptionConsumerTest.php b/pkg/amqp-lib/Tests/AmqpSubscriptionConsumerTest.php new file mode 100644 index 000000000..a375657b2 --- /dev/null +++ b/pkg/amqp-lib/Tests/AmqpSubscriptionConsumerTest.php @@ -0,0 +1,35 @@ +assertTrue($rc->implementsInterface(SubscriptionConsumer::class)); + } + + public function testCouldBeConstructedWithAmqpContextAndHeartbeatOnTickAsArguments() + { + self::assertInstanceOf( + AmqpSubscriptionConsumer::class, + new AmqpSubscriptionConsumer($this->createAmqpContextMock(), $heartbeatOnTick = true) + ); + } + + /** + * @return AmqpContext|MockObject + */ + private function createAmqpContextMock() + { + return $this->createMock(AmqpContext::class); + } +} diff --git a/pkg/amqp-lib/Tests/BufferTest.php b/pkg/amqp-lib/Tests/BufferTest.php deleted file mode 100644 index bebe435cb..000000000 --- a/pkg/amqp-lib/Tests/BufferTest.php +++ /dev/null @@ -1,64 +0,0 @@ -assertAttributeSame([], 'messages', $buffer); - } - - public function testShouldReturnNullIfNoMessagesInBuffer() - { - $buffer = new Buffer(); - - $this->assertNull($buffer->pop('aConsumerTag')); - $this->assertNull($buffer->pop('anotherConsumerTag')); - } - - public function testShouldPushMessageToBuffer() - { - $fooMessage = new AmqpMessage(); - $barMessage = new AmqpMessage(); - $bazMessage = new AmqpMessage(); - - $buffer = new Buffer(); - - $buffer->push('aConsumerTag', $fooMessage); - $buffer->push('aConsumerTag', $barMessage); - - $buffer->push('anotherConsumerTag', $bazMessage); - - $this->assertAttributeSame([ - 'aConsumerTag' => [$fooMessage, $barMessage], - 'anotherConsumerTag' => [$bazMessage], - ], 'messages', $buffer); - } - - public function testShouldPopMessageFromBuffer() - { - $fooMessage = new AmqpMessage(); - $barMessage = new AmqpMessage(); - - $buffer = new Buffer(); - - $buffer->push('aConsumerTag', $fooMessage); - $buffer->push('aConsumerTag', $barMessage); - - $this->assertSame($fooMessage, $buffer->pop('aConsumerTag')); - $this->assertSame($barMessage, $buffer->pop('aConsumerTag')); - $this->assertNull($buffer->pop('aConsumerTag')); - } -} diff --git a/pkg/amqp-lib/Tests/Functional/AmqpSubscriptionConsumerWithHeartbeatOnTickTest.php b/pkg/amqp-lib/Tests/Functional/AmqpSubscriptionConsumerWithHeartbeatOnTickTest.php new file mode 100644 index 000000000..4d5b695e5 --- /dev/null +++ b/pkg/amqp-lib/Tests/Functional/AmqpSubscriptionConsumerWithHeartbeatOnTickTest.php @@ -0,0 +1,83 @@ +context) { + $this->context->close(); + } + + parent::tearDown(); + } + + public function test() + { + $this->context = $context = $this->createContext(); + + $fooQueue = $this->createQueue($context, 'foo_subscription_consumer_consume_from_all_subscribed_queues_spec'); + + $expectedFooBody = 'fooBody'; + + $context->createProducer()->send($fooQueue, $context->createMessage($expectedFooBody)); + + $fooConsumer = $context->createConsumer($fooQueue); + + $actualBodies = []; + $actualQueues = []; + $callback = function (Message $message, Consumer $consumer) use (&$actualBodies, &$actualQueues) { + declare(ticks=1) { + $actualBodies[] = $message->getBody(); + $actualQueues[] = $consumer->getQueue()->getQueueName(); + + $consumer->acknowledge($message); + + return true; + } + }; + + $subscriptionConsumer = $context->createSubscriptionConsumer(); + $subscriptionConsumer->subscribe($fooConsumer, $callback); + + $subscriptionConsumer->consume(1000); + + $this->assertCount(1, $actualBodies); + } + + protected function createContext(): AmqpContext + { + $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); + + $context = $factory->createContext(); + $context->setQos(0, 5, false); + + return $context; + } + + protected function createQueue(AmqpContext $context, string $queueName): AmqpQueue + { + $queue = $context->createQueue($queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php deleted file mode 100644 index 5eb07345a..000000000 --- a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeFromAllSubscribedQueuesTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php deleted file mode 100644 index 1a5c939d0..000000000 --- a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldAddConsumerTagOnSubscribeTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php deleted file mode 100644 index caeac1f61..000000000 --- a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeShouldRemoveConsumerTagOnUnsubscribeTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php b/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php deleted file mode 100644 index c751b2c3e..000000000 --- a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeUntilUnsubscribedTest.php +++ /dev/null @@ -1,22 +0,0 @@ -createContext(); - } -} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpConnectionFactoryTest.php b/pkg/amqp-lib/Tests/Spec/AmqpConnectionFactoryTest.php index ebc3b8a7f..6fd1636e4 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpConnectionFactoryTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpConnectionFactoryTest.php @@ -3,9 +3,9 @@ namespace Enqueue\AmqpLib\Tests\Spec; use Enqueue\AmqpLib\AmqpConnectionFactory; -use Interop\Queue\Spec\PsrConnectionFactorySpec; +use Interop\Queue\Spec\ConnectionFactorySpec; -class AmqpConnectionFactoryTest extends PsrConnectionFactorySpec +class AmqpConnectionFactoryTest extends ConnectionFactorySpec { protected function createConnectionFactory() { diff --git a/pkg/amqp-lib/Tests/Spec/AmqpContextTest.php b/pkg/amqp-lib/Tests/Spec/AmqpContextTest.php index 5e3d8bb8e..eb4e4c710 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpContextTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpContextTest.php @@ -3,11 +3,11 @@ namespace Enqueue\AmqpLib\Tests\Spec; use Enqueue\AmqpLib\AmqpContext; -use Interop\Queue\Spec\PsrContextSpec; +use Interop\Queue\Spec\ContextSpec; use PhpAmqpLib\Channel\AMQPChannel; use PhpAmqpLib\Connection\AbstractConnection; -class AmqpContextTest extends PsrContextSpec +class AmqpContextTest extends ContextSpec { protected function createContext() { @@ -20,6 +20,6 @@ protected function createContext() ->willReturn($channel) ; - return new AmqpContext($con); + return new AmqpContext($con, []); } } diff --git a/pkg/amqp-lib/Tests/Spec/AmqpProducerTest.php b/pkg/amqp-lib/Tests/Spec/AmqpProducerTest.php index 9285d598f..f72296a66 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpProducerTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpProducerTest.php @@ -3,16 +3,13 @@ namespace Enqueue\AmqpLib\Tests\Spec; use Enqueue\AmqpLib\AmqpConnectionFactory; -use Interop\Queue\Spec\PsrProducerSpec; +use Interop\Queue\Spec\ProducerSpec; /** * @group functional */ -class AmqpProducerTest extends PsrProducerSpec +class AmqpProducerTest extends ProducerSpec { - /** - * {@inheritdoc} - */ protected function createProducer() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php index 42bc523ce..1a5fb70b3 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpLib\AmqpConnectionFactory; use Enqueue\AmqpLib\AmqpContext; use Enqueue\AmqpTools\RabbitMqDelayPluginDelayStrategy; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendAndReceiveDelayedMessageWithDelayPluginStrategyTest extends SendAndReceiveDelayedMessageFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -26,10 +23,8 @@ protected function createContext() /** * @param AmqpContext $context - * - * {@inheritdoc} */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = parent::createQueue($context, $queueName); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php index e27166695..0e00b10e9 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpLib\AmqpConnectionFactory; use Enqueue\AmqpLib\AmqpContext; use Enqueue\AmqpTools\RabbitMqDlxDelayStrategy; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendAndReceiveDelayedMessageWithDlxStrategyTest extends SendAndReceiveDelayedMessageFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -26,10 +23,8 @@ protected function createContext() /** * @param AmqpContext $context - * - * {@inheritdoc} */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = parent::createQueue($context, $queueName); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php index 881c8dd8c..83c4c948f 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceivePriorityMessagesFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpLib\AmqpConnectionFactory; use Enqueue\AmqpLib\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceivePriorityMessagesFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendAndReceivePriorityMessagesFromQueueTest extends SendAndReceivePriorityMessagesFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $queue->setArguments(['x-max-priority' => 10]); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php index b62882fad..d5f35ed65 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpLib\AmqpConnectionFactory; use Enqueue\AmqpLib\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveTimeToLiveMessagesFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendAndReceiveTimeToLiveMessagesFromQueueTest extends SendAndReceiveTimeToLiveMessagesFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php index 2574f5ab2..ade42b346 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendAndReceiveTimestampAsIntengerTest.php @@ -10,9 +10,6 @@ */ class AmqpSendAndReceiveTimestampAsIntengerTest extends SendAndReceiveTimestampAsIntegerSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php index 3a31b46cf..6d66532c6 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpLib\AmqpConnectionFactory; use Enqueue\AmqpLib\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php index d1e78e900..621608020 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveFromTopicTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpLib\AmqpConnectionFactory; use Enqueue\AmqpLib\AmqpContext; use Interop\Amqp\AmqpTopic; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromTopicSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendToAndReceiveFromTopicTest extends SendToAndReceiveFromTopicSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -24,11 +21,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php index d36b168fc..db536948a 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveNoWaitFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpLib\AmqpConnectionFactory; use Enqueue\AmqpLib\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSendToAndReceiveNoWaitFromQueueTest extends SendToAndReceiveNoWaitFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -23,11 +20,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php index d237895e0..c2b184209 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendToAndReceiveNoWaitFromTopicTest.php @@ -5,7 +5,7 @@ use Enqueue\AmqpLib\AmqpConnectionFactory; use Enqueue\AmqpLib\AmqpContext; use Interop\Amqp\AmqpTopic; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromTopicSpec; /** @@ -13,9 +13,6 @@ */ class AmqpSendToAndReceiveNoWaitFromTopicTest extends SendToAndReceiveNoWaitFromTopicSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -24,11 +21,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicGetMethodTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php similarity index 73% rename from pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicGetMethodTest.php rename to pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php index c5512ef35..ec404b59e 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicGetMethodTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueTest.php @@ -6,17 +6,14 @@ use Enqueue\AmqpLib\AmqpContext; use Interop\Amqp\AmqpTopic; use Interop\Amqp\Impl\AmqpBind; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveFromQueueSpec; /** * @group functional */ -class AmqpSendToTopicAndReceiveFromQueueWithBasicGetMethodTest extends SendToTopicAndReceiveFromQueueSpec +class AmqpSendToTopicAndReceiveFromQueueTest extends SendToTopicAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -25,11 +22,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); @@ -41,11 +36,9 @@ protected function createQueue(PsrContext $context, $queueName) } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicConsumeMethodTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicConsumeMethodTest.php deleted file mode 100644 index 0c6eb8cd2..000000000 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveFromQueueWithBasicConsumeMethodTest.php +++ /dev/null @@ -1,71 +0,0 @@ -createContext(); - } - - /** - * {@inheritdoc} - * - * @param AmqpContext $context - */ - protected function createQueue(PsrContext $context, $queueName) - { - $queue = $context->createQueue('send_to_topic_and_receive_from_queue_spec_basic_consume'); - - try { - $context->deleteQueue($queue); - } catch (\Exception $e) { - } - - $context->declareQueue($queue); - $context->purgeQueue($queue); - - $context->bind(new AmqpBind($this->topic, $queue)); - - return $queue; - } - - /** - * {@inheritdoc} - * - * @param AmqpContext $context - */ - protected function createTopic(PsrContext $context, $topicName) - { - $topic = $context->createTopic('send_to_topic_and_receive_from_queue_spec_basic_consume'); - $topic->setType(AmqpTopic::TYPE_FANOUT); - $topic->addFlag(AmqpTopic::FLAG_DURABLE); - - try { - $context->deleteTopic($topic); - } catch (\Exception $e) { - } - - $context->declareTopic($topic); - - return $this->topic = $topic; - } -} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php index 783496ffa..665382fe4 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSendToTopicAndReceiveNoWaitFromQueueTest.php @@ -6,7 +6,7 @@ use Enqueue\AmqpLib\AmqpContext; use Interop\Amqp\AmqpTopic; use Interop\Amqp\Impl\AmqpBind; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveNoWaitFromQueueSpec; /** @@ -14,9 +14,6 @@ */ class AmqpSendToTopicAndReceiveNoWaitFromQueueTest extends SendToTopicAndReceiveNoWaitFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); @@ -25,11 +22,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); @@ -41,11 +36,9 @@ protected function createQueue(PsrContext $context, $queueName) } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { $topic = $context->createTopic($topicName); $topic->setType(AmqpTopic::TYPE_FANOUT); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php index 87fef80a0..7bf142e5d 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSslSendToAndReceiveFromQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\AmqpLib\AmqpConnectionFactory; use Enqueue\AmqpLib\AmqpContext; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class AmqpSslSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $baseDir = realpath(__DIR__.'/../../../../'); @@ -37,11 +34,9 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param AmqpContext $context */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = $context->createQueue($queueName); $context->declareQueue($queue); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerAddConsumerTagOnSubscribeTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerAddConsumerTagOnSubscribeTest.php new file mode 100644 index 000000000..8017c9ac7 --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerAddConsumerTagOnSubscribeTest.php @@ -0,0 +1,20 @@ +createContext(); + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..b81b139e8 --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,41 @@ +createContext(); + $context->setQos(0, 5, false); + + return $context; + } + + /** + * @param AmqpContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var AmqpQueue $queue */ + $queue = parent::createQueue($context, $queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerConsumeUntilUnsubscribedTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..288ab25f4 --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerConsumeUntilUnsubscribedTest.php @@ -0,0 +1,50 @@ +subscriptionConsumer) { + $this->subscriptionConsumer->unsubscribeAll(); + } + + parent::tearDown(); + } + + /** + * @return AmqpContext + */ + protected function createContext() + { + $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); + + $context = $factory->createContext(); + $context->setQos(0, 1, false); + + return $context; + } + + /** + * @param AmqpContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var AmqpQueue $queue */ + $queue = parent::createQueue($context, $queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerPreFetchCountTest.php similarity index 50% rename from pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php rename to pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerPreFetchCountTest.php index 2d77ff55d..5f06a2ad2 100644 --- a/pkg/amqp-lib/Tests/Spec/AmqpBasicConsumeBreakOnFalseTest.php +++ b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerPreFetchCountTest.php @@ -3,17 +3,15 @@ namespace Enqueue\AmqpLib\Tests\Spec; use Enqueue\AmqpLib\AmqpConnectionFactory; -use Interop\Queue\Spec\Amqp\BasicConsumeBreakOnFalseSpec; +use Interop\Amqp\AmqpContext; +use Interop\Queue\Spec\Amqp\SubscriptionConsumerPreFetchCountSpec; /** * @group functional */ -class AmqpBasicConsumeBreakOnFalseTest extends BasicConsumeBreakOnFalseSpec +class AmqpSubscriptionConsumerPreFetchCountTest extends SubscriptionConsumerPreFetchCountSpec { - /** - * {@inheritdoc} - */ - protected function createContext() + protected function createContext(): AmqpContext { $factory = new AmqpConnectionFactory(getenv('AMQP_DSN')); diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest.php new file mode 100644 index 000000000..196b7a962 --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerRemoveConsumerTagOnUnsubscribeTest.php @@ -0,0 +1,20 @@ +createContext(); + } +} diff --git a/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerStopOnFalseTest.php b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerStopOnFalseTest.php new file mode 100644 index 000000000..345007135 --- /dev/null +++ b/pkg/amqp-lib/Tests/Spec/AmqpSubscriptionConsumerStopOnFalseTest.php @@ -0,0 +1,41 @@ +createContext(); + $context->setQos(0, 5, false); + + return $context; + } + + /** + * @param AmqpContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var AmqpQueue $queue */ + $queue = parent::createQueue($context, $queueName); + $context->declareQueue($queue); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/amqp-lib/composer.json b/pkg/amqp-lib/composer.json index 97ac91567..62f906c66 100644 --- a/pkg/amqp-lib/composer.json +++ b/pkg/amqp-lib/composer.json @@ -6,19 +6,17 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "php-amqplib/php-amqplib": "^2.7", - "queue-interop/amqp-interop": "^0.7.4@dev", - "enqueue/amqp-tools": "^0.8.35@dev" + "php": "^8.1", + "php-amqplib/php-amqplib": "^3.2", + "queue-interop/amqp-interop": "^0.8.2", + "queue-interop/queue-interop": "^0.8", + "enqueue/amqp-tools": "^0.10" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -33,13 +31,10 @@ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/amqp-lib/examples/consume.php b/pkg/amqp-lib/examples/consume.php index 4acca541d..03f609c71 100644 --- a/pkg/amqp-lib/examples/consume.php +++ b/pkg/amqp-lib/examples/consume.php @@ -12,21 +12,12 @@ if ($autoload) { require_once $autoload; } else { - throw new \LogicException('Composer autoload was not found'); + throw new LogicException('Composer autoload was not found'); } use Enqueue\AmqpLib\AmqpConnectionFactory; -$config = [ - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - 'receive_method' => 'basic_consume', -]; - -$factory = new AmqpConnectionFactory($config); +$factory = new AmqpConnectionFactory(getenv('RABBITMQ_AMQP_DSN')); $context = $factory->createContext(); $queue = $context->createQueue('foo'); @@ -41,7 +32,7 @@ while (true) { if ($m = $consumer->receive(100)) { - echo $m->getBody(), PHP_EOL; + echo $m->getBody(), \PHP_EOL; $consumer->acknowledge($m); } diff --git a/pkg/amqp-lib/examples/produce.php b/pkg/amqp-lib/examples/produce.php index 4b3ff025a..7527b2620 100644 --- a/pkg/amqp-lib/examples/produce.php +++ b/pkg/amqp-lib/examples/produce.php @@ -12,7 +12,7 @@ if ($autoload) { require_once $autoload; } else { - throw new \LogicException('Composer autoload was not found'); + throw new LogicException('Composer autoload was not found'); } use Enqueue\AmqpLib\AmqpConnectionFactory; @@ -20,21 +20,13 @@ use Interop\Amqp\AmqpTopic; use Interop\Amqp\Impl\AmqpBind; -$config = [ - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), -]; - -$factory = new AmqpConnectionFactory($config); +$factory = new AmqpConnectionFactory(getenv('RABBITMQ_AMQP_DSN')); $context = $factory->createContext(); $topic = $context->createTopic('test.amqp.ext'); $topic->addFlag(AmqpTopic::FLAG_DURABLE); $topic->setType(AmqpTopic::TYPE_FANOUT); -//$topic->setArguments(['alternate-exchange' => 'foo']); +// $topic->setArguments(['alternate-exchange' => 'foo']); $context->deleteTopic($topic); $context->declareTopic($topic); diff --git a/pkg/amqp-lib/phpunit.xml.dist b/pkg/amqp-lib/phpunit.xml.dist index f6b8b173a..2c5fe1f6a 100644 --- a/pkg/amqp-lib/phpunit.xml.dist +++ b/pkg/amqp-lib/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/amqp-lib/tutorial/receive.php b/pkg/amqp-lib/tutorial/receive.php index 337421020..1dba8353f 100644 --- a/pkg/amqp-lib/tutorial/receive.php +++ b/pkg/amqp-lib/tutorial/receive.php @@ -10,7 +10,6 @@ 'port' => 5672, 'user' => 'guest', 'pass' => 'guest', - 'receive_method' => 'basic_consume', ]; $connection = new AmqpConnectionFactory($config); diff --git a/pkg/amqp-lib/tutorial/receive_logs.php b/pkg/amqp-lib/tutorial/receive_logs.php index bf68bf1fb..d28395f17 100644 --- a/pkg/amqp-lib/tutorial/receive_logs.php +++ b/pkg/amqp-lib/tutorial/receive_logs.php @@ -12,7 +12,6 @@ 'port' => 5672, 'user' => 'guest', 'pass' => 'guest', - 'receive_method' => 'basic_consume', ]; $connection = new AmqpConnectionFactory($config); diff --git a/pkg/amqp-lib/tutorial/receive_logs_direct.php b/pkg/amqp-lib/tutorial/receive_logs_direct.php index 699d5108f..2962aa480 100644 --- a/pkg/amqp-lib/tutorial/receive_logs_direct.php +++ b/pkg/amqp-lib/tutorial/receive_logs_direct.php @@ -12,7 +12,6 @@ 'port' => 5672, 'user' => 'guest', 'pass' => 'guest', - 'receive_method' => 'basic_consume', ]; $connection = new AmqpConnectionFactory($config); diff --git a/pkg/amqp-lib/tutorial/receive_logs_topic.php b/pkg/amqp-lib/tutorial/receive_logs_topic.php index a149be84c..89bf9c0ff 100644 --- a/pkg/amqp-lib/tutorial/receive_logs_topic.php +++ b/pkg/amqp-lib/tutorial/receive_logs_topic.php @@ -12,7 +12,6 @@ 'port' => 5672, 'user' => 'guest', 'pass' => 'guest', - 'receive_method' => 'basic_consume', ]; $connection = new AmqpConnectionFactory($config); diff --git a/pkg/amqp-lib/tutorial/rpc_client.php b/pkg/amqp-lib/tutorial/rpc_client.php index 9c34510dd..6ad091bc0 100644 --- a/pkg/amqp-lib/tutorial/rpc_client.php +++ b/pkg/amqp-lib/tutorial/rpc_client.php @@ -9,15 +9,14 @@ 'port' => 5672, 'user' => 'guest', 'pass' => 'guest', - 'receive_method' => 'basic_consume', ]; -class FibonacciRpcClient +class rpc_client { - /** @var \Interop\Amqp\AmqpContext */ + /** @var Interop\Amqp\AmqpContext */ private $context; - /** @var \Interop\Amqp\AmqpQueue */ + /** @var Interop\Amqp\AmqpQueue */ private $callback_queue; public function __construct(array $config) @@ -44,7 +43,7 @@ public function call($n) while (true) { if ($message = $consumer->receive()) { if ($message->getCorrelationId() == $corr_id) { - return (int) ($message->getBody()); + return (int) $message->getBody(); } } } diff --git a/pkg/amqp-lib/tutorial/rpc_server.php b/pkg/amqp-lib/tutorial/rpc_server.php index ec94f89c8..241471684 100644 --- a/pkg/amqp-lib/tutorial/rpc_server.php +++ b/pkg/amqp-lib/tutorial/rpc_server.php @@ -9,7 +9,6 @@ 'port' => 5672, 'user' => 'guest', 'pass' => 'guest', - 'receive_method' => 'basic_consume', ]; function fib($n) @@ -38,7 +37,7 @@ function fib($n) while (true) { if ($req = $consumer->receive()) { - $n = (int) ($req->getBody()); + $n = (int) $req->getBody(); echo ' [.] fib(', $n, ")\n"; $msg = $context->createMessage((string) fib($n)); diff --git a/pkg/amqp-lib/tutorial/worker.php b/pkg/amqp-lib/tutorial/worker.php index 3f908b6a6..b9afe4d4e 100644 --- a/pkg/amqp-lib/tutorial/worker.php +++ b/pkg/amqp-lib/tutorial/worker.php @@ -10,7 +10,6 @@ 'port' => 5672, 'user' => 'guest', 'pass' => 'guest', - 'receive_method' => 'basic_consume', ]; $connection = new AmqpConnectionFactory($config); diff --git a/pkg/amqp-tools/.github/workflows/ci.yml b/pkg/amqp-tools/.github/workflows/ci.yml new file mode 100644 index 000000000..5448d7b1a --- /dev/null +++ b/pkg/amqp-tools/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/amqp-tools/ConnectionConfig.php b/pkg/amqp-tools/ConnectionConfig.php index d4deb1620..e1356c7cb 100644 --- a/pkg/amqp-tools/ConnectionConfig.php +++ b/pkg/amqp-tools/ConnectionConfig.php @@ -1,7 +1,11 @@ '', 'ssl_passphrase' => '', ]; + $this->schemeExtensions = []; $this->addSupportedScheme('amqp'); $this->addSupportedScheme('amqps'); } - /** - * @param string $schema - * - * @return self - */ - public function addSupportedScheme($schema) + public function addSupportedScheme(string $schema): self { $this->supportedSchemes[] = $schema; $this->supportedSchemes = array_unique($this->supportedSchemes); @@ -107,7 +114,6 @@ public function addSupportedScheme($schema) /** * @param string $name - * @param mixed $value * * @return self */ @@ -143,18 +149,18 @@ public function parse() $config = array_replace($this->defaultConfig, $config); $config['host'] = (string) $config['host']; - $config['port'] = (int) ($config['port']); + $config['port'] = (int) $config['port']; $config['user'] = (string) $config['user']; $config['pass'] = (string) $config['pass']; - $config['read_timeout'] = max((float) ($config['read_timeout']), 0); - $config['write_timeout'] = max((float) ($config['write_timeout']), 0); - $config['connection_timeout'] = max((float) ($config['connection_timeout']), 0); - $config['heartbeat'] = max((float) ($config['heartbeat']), 0); + $config['read_timeout'] = max((float) $config['read_timeout'], 0); + $config['write_timeout'] = max((float) $config['write_timeout'], 0); + $config['connection_timeout'] = max((float) $config['connection_timeout'], 0); + $config['heartbeat'] = max((float) $config['heartbeat'], 0); $config['persisted'] = !empty($config['persisted']); $config['lazy'] = !empty($config['lazy']); $config['qos_global'] = !empty($config['qos_global']); - $config['qos_prefetch_count'] = max((int) ($config['qos_prefetch_count']), 0); - $config['qos_prefetch_size'] = max((int) ($config['qos_prefetch_size']), 0); + $config['qos_prefetch_count'] = max((int) $config['qos_prefetch_count'], 0); + $config['qos_prefetch_size'] = max((int) $config['qos_prefetch_size'], 0); $config['ssl_on'] = !empty($config['ssl_on']); $config['ssl_verify'] = !empty($config['ssl_verify']); $config['ssl_cacert'] = (string) $config['ssl_cacert']; @@ -167,6 +173,14 @@ public function parse() return $this; } + /** + * @return string[] + */ + public function getSchemeExtensions(): array + { + return $this->schemeExtensions; + } + /** * @return string */ @@ -328,10 +342,8 @@ public function getSslPassPhrase() } /** - * @param string $name - * @param mixed $default - * - * @return mixed + * @param string $name + * @param mixed|null $default */ public function getOption($name, $default = null) { @@ -361,45 +373,50 @@ public function getConfig() */ private function parseDsn($dsn) { - if (false === parse_url(http://github.com/$dsn)) { - throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); - } - - $config = []; - - $scheme = parse_url(http://github.com/$dsn,%20PHP_URL_SCHEME); - if (false == in_array($scheme, $this->supportedSchemes, true)) { - throw new \LogicException(sprintf('The given DSN scheme "%s" is not supported. Could be one of "%s" only.', $scheme, implode('", "', $this->supportedSchemes))); - } - - if ($host = parse_url(http://github.com/$dsn,%20PHP_URL_HOST)) { - $config['host'] = $host; - } - if ($port = parse_url(http://github.com/$dsn,%20PHP_URL_PORT)) { - $config['port'] = $port; - } - if ($user = parse_url(http://github.com/$dsn,%20PHP_URL_USER)) { - $config['user'] = $user; - } - if ($pass = parse_url(http://github.com/$dsn,%20PHP_URL_PASS)) { - $config['pass'] = $pass; - } - - if ($query = parse_url(http://github.com/$dsn,%20PHP_URL_QUERY)) { - $queryConfig = []; - parse_str($query, $queryConfig); - - $config = array_replace($queryConfig, $config); - } + $dsn = Dsn::parseFirst($dsn); - if ($path = parse_url(http://github.com/$dsn,%20PHP_URL_PATH)) { - $config['vhost'] = ltrim($path, '/'); + $supportedSchemes = $this->supportedSchemes; + if (false == in_array($dsn->getSchemeProtocol(), $supportedSchemes, true)) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be one of "%s".', $dsn->getSchemeProtocol(), implode('", "', $supportedSchemes))); } - if (0 === strpos($scheme, 'amqps')) { - $config['ssl_on'] = true; + $sslOn = false; + $isAmqps = 'amqps' === $dsn->getSchemeProtocol(); + $isTls = in_array('tls', $dsn->getSchemeExtensions(), true); + $isSsl = in_array('ssl', $dsn->getSchemeExtensions(), true); + if ($isAmqps || $isTls || $isSsl) { + $sslOn = true; } - return array_map('urldecode', $config); + $this->schemeExtensions = $dsn->getSchemeExtensions(); + + $config = array_filter(array_replace($dsn->getQuery(), [ + 'host' => $dsn->getHost(), + 'port' => $dsn->getPort(), + 'user' => $dsn->getUser(), + 'pass' => $dsn->getPassword(), + 'vhost' => null !== ($path = $dsn->getPath()) ? + (str_starts_with($path, '/') ? substr($path, 1) : $path) + : null, + 'read_timeout' => $dsn->getFloat('read_timeout'), + 'write_timeout' => $dsn->getFloat('write_timeout'), + 'connection_timeout' => $dsn->getFloat('connection_timeout'), + 'heartbeat' => $dsn->getFloat('heartbeat'), + 'persisted' => $dsn->getBool('persisted'), + 'lazy' => $dsn->getBool('lazy'), + 'qos_global' => $dsn->getBool('qos_global'), + 'qos_prefetch_size' => $dsn->getDecimal('qos_prefetch_size'), + 'qos_prefetch_count' => $dsn->getDecimal('qos_prefetch_count'), + 'ssl_on' => $sslOn, + 'ssl_verify' => $dsn->getBool('ssl_verify'), + 'ssl_cacert' => $dsn->getString('ssl_cacert'), + 'ssl_cert' => $dsn->getString('ssl_cert'), + 'ssl_key' => $dsn->getString('ssl_key'), + 'ssl_passphrase' => $dsn->getString('ssl_passphrase'), + ]), function ($value) { return null !== $value; }); + + return array_map(function ($value) { + return is_string($value) ? rawurldecode($value) : $value; + }, $config); } } diff --git a/pkg/amqp-tools/DelayStrategy.php b/pkg/amqp-tools/DelayStrategy.php index ba1bcc3d2..791bc5519 100644 --- a/pkg/amqp-tools/DelayStrategy.php +++ b/pkg/amqp-tools/DelayStrategy.php @@ -1,5 +1,7 @@ delayStrategy = $delayStrategy; diff --git a/pkg/amqp-tools/DelayStrategyTransportFactoryTrait.php b/pkg/amqp-tools/DelayStrategyTransportFactoryTrait.php index 039d3db3b..0c6702228 100644 --- a/pkg/amqp-tools/DelayStrategyTransportFactoryTrait.php +++ b/pkg/amqp-tools/DelayStrategyTransportFactoryTrait.php @@ -1,5 +1,7 @@ getDefinition($factoryId); diff --git a/pkg/amqp-tools/README.md b/pkg/amqp-tools/README.md index e6e7c9a10..16cb1667f 100644 --- a/pkg/amqp-tools/README.md +++ b/pkg/amqp-tools/README.md @@ -1,28 +1,37 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # AMQP tools [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/amqp-tools.png?branch=master)](https://travis-ci.org/php-enqueue/amqp-tools) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/amqp-tools/ci.yml?branch=master)](https://github.com/php-enqueue/amqp-tools/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/amqp-tools/d/total.png)](https://packagist.org/packages/enqueue/amqp-tools) [![Latest Stable Version](https://poser.pugx.org/enqueue/amqp-tools/version.png)](https://packagist.org/packages/enqueue/amqp-tools) - -Provides features that are not part of the AMQP spec but could be built on top of. -The tools could be used with any [amqp interop](https://github.com/queue-interop/queue-interop#amqp-interop) compatible transport. + +Provides features that are not part of the AMQP spec but could be built on top of. +The tools could be used with any [amqp interop](https://github.com/queue-interop/queue-interop#amqp-interop) compatible transport. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/amqp-tools/RabbitMqDelayPluginDelayStrategy.php b/pkg/amqp-tools/RabbitMqDelayPluginDelayStrategy.php index fc4103789..180d43bd9 100644 --- a/pkg/amqp-tools/RabbitMqDelayPluginDelayStrategy.php +++ b/pkg/amqp-tools/RabbitMqDelayPluginDelayStrategy.php @@ -1,5 +1,7 @@ createMessage($message->getBody(), $message->getProperties(), $message->getHeaders()); - $delayMessage->setProperty('x-delay', (int) $delayMsec); + $delayMessage->setProperty('x-delay', (int) $delay); $delayMessage->setRoutingKey($message->getRoutingKey()); if ($dest instanceof AmqpTopic) { @@ -40,10 +39,7 @@ public function delayMessage(AmqpContext $context, AmqpDestination $dest, AmqpMe $context->declareTopic($delayTopic); $context->bind(new AmqpBind($dest, $delayTopic, $delayMessage->getRoutingKey())); } else { - throw new InvalidDestinationException(sprintf('The destination must be an instance of %s but got %s.', - AmqpTopic::class.'|'.AmqpQueue::class, - get_class($dest) - )); + throw new InvalidDestinationException(sprintf('The destination must be an instance of %s but got %s.', AmqpTopic::class.'|'.AmqpQueue::class, $dest::class)); } $producer = $context->createProducer(); diff --git a/pkg/amqp-tools/RabbitMqDlxDelayStrategy.php b/pkg/amqp-tools/RabbitMqDlxDelayStrategy.php index 6211885e3..35d9b59fe 100644 --- a/pkg/amqp-tools/RabbitMqDlxDelayStrategy.php +++ b/pkg/amqp-tools/RabbitMqDlxDelayStrategy.php @@ -1,5 +1,7 @@ getProperties(); @@ -28,24 +27,21 @@ public function delayMessage(AmqpContext $context, AmqpDestination $dest, AmqpMe if ($dest instanceof AmqpTopic) { $routingKey = $message->getRoutingKey() ? '.'.$message->getRoutingKey() : ''; - $name = sprintf('enqueue.%s%s.%s.x.delay', $dest->getTopicName(), $routingKey, $delayMsec); + $name = sprintf('enqueue.%s%s.%s.x.delay', $dest->getTopicName(), $routingKey, $delay); $delayQueue = $context->createQueue($name); $delayQueue->addFlag(AmqpTopic::FLAG_DURABLE); - $delayQueue->setArgument('x-message-ttl', $delayMsec); + $delayQueue->setArgument('x-message-ttl', $delay); $delayQueue->setArgument('x-dead-letter-exchange', $dest->getTopicName()); $delayQueue->setArgument('x-dead-letter-routing-key', (string) $delayMessage->getRoutingKey()); } elseif ($dest instanceof AmqpQueue) { - $delayQueue = $context->createQueue('enqueue.'.$dest->getQueueName().'.'.$delayMsec.'.delayed'); + $delayQueue = $context->createQueue('enqueue.'.$dest->getQueueName().'.'.$delay.'.delayed'); $delayQueue->addFlag(AmqpTopic::FLAG_DURABLE); - $delayQueue->setArgument('x-message-ttl', $delayMsec); + $delayQueue->setArgument('x-message-ttl', $delay); $delayQueue->setArgument('x-dead-letter-exchange', ''); $delayQueue->setArgument('x-dead-letter-routing-key', $dest->getQueueName()); } else { - throw new InvalidDestinationException(sprintf('The destination must be an instance of %s but got %s.', - AmqpTopic::class.'|'.AmqpQueue::class, - get_class($dest) - )); + throw new InvalidDestinationException(sprintf('The destination must be an instance of %s but got %s.', AmqpTopic::class.'|'.AmqpQueue::class, $dest::class)); } $context->declareQueue($delayQueue); diff --git a/pkg/amqp-tools/SignalSocketHelper.php b/pkg/amqp-tools/SignalSocketHelper.php index d4d8bda99..623a5e3e2 100644 --- a/pkg/amqp-tools/SignalSocketHelper.php +++ b/pkg/amqp-tools/SignalSocketHelper.php @@ -1,5 +1,7 @@ handlers = []; } - public function beforeSocket() + public function beforeSocket(): void { - // PHP 7.1 and higher + // PHP 7.1 and pcntl ext installed higher if (false == function_exists('pcntl_signal_get_handler')) { return; } - $signals = [SIGTERM, SIGQUIT, SIGINT]; + $signals = [\SIGTERM, \SIGQUIT, \SIGINT]; if ($this->handlers) { throw new \LogicException('The handlers property should be empty but it is not. The afterSocket method might not have been called.'); @@ -51,19 +53,19 @@ public function beforeSocket() } } - public function afterSocket() + public function afterSocket(): void { // PHP 7.1 and higher if (false == function_exists('pcntl_signal_get_handler')) { return; } - $signals = [SIGTERM, SIGQUIT, SIGINT]; + $signals = [\SIGTERM, \SIGQUIT, \SIGINT]; $this->wasThereSignal = null; foreach ($signals as $signal) { - $handler = isset($this->handlers[$signal]) ? $this->handlers[$signal] : SIG_DFL; + $handler = isset($this->handlers[$signal]) ? $this->handlers[$signal] : \SIG_DFL; pcntl_signal($signal, $handler); } @@ -71,10 +73,7 @@ public function afterSocket() $this->handlers = []; } - /** - * @return bool - */ - public function wasThereSignal() + public function wasThereSignal(): bool { return (bool) $this->wasThereSignal; } diff --git a/pkg/amqp-tools/SubscriptionConsumer.php b/pkg/amqp-tools/SubscriptionConsumer.php deleted file mode 100644 index e992a6b06..000000000 --- a/pkg/amqp-tools/SubscriptionConsumer.php +++ /dev/null @@ -1,57 +0,0 @@ -context = $context; - } - - /** - * {@inheritdoc} - */ - public function consume($timeout = 0) - { - $this->context->consume($timeout); - } - - /** - * {@inheritdoc} - */ - public function subscribe(PsrConsumer $consumer, callable $callback) - { - $this->context->subscribe($consumer, $callback); - } - - /** - * {@inheritdoc} - */ - public function unsubscribe(PsrConsumer $consumer) - { - $this->context->unsubscribe($consumer); - } - - /** - * TODO. - * - * {@inheritdoc} - */ - public function unsubscribeAll() - { - throw new \LogicException('Not implemented'); - } -} diff --git a/pkg/amqp-tools/Tests/ConnectionConfigTest.php b/pkg/amqp-tools/Tests/ConnectionConfigTest.php index 6fba106ed..1a1dc477d 100644 --- a/pkg/amqp-tools/Tests/ConnectionConfigTest.php +++ b/pkg/amqp-tools/Tests/ConnectionConfigTest.php @@ -24,7 +24,7 @@ public function testThrowNeitherArrayStringNorNullGivenAsConfig() public function testThrowIfSchemeIsNotSupported() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN scheme "http" is not supported. Could be one of "amqp", "amqps" only.'); + $this->expectExceptionMessage('The given scheme protocol "http" is not supported. It must be one of "amqp", "amqps".'); (new ConnectionConfig('http://example.com/'))->parse(); } @@ -32,7 +32,7 @@ public function testThrowIfSchemeIsNotSupported() public function testThrowIfSchemeIsNotSupportedIncludingAdditionalSupportedSchemes() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN scheme "http" is not supported. Could be one of "amqp", "amqps", "amqp+foo" only.'); + $this->expectExceptionMessage('The given scheme protocol "http" is not supported. It must be one of "amqp", "amqps", "amqp+foo".'); (new ConnectionConfig('http://example.com/')) ->addSupportedScheme('amqp+foo') @@ -43,9 +43,9 @@ public function testThrowIfSchemeIsNotSupportedIncludingAdditionalSupportedSchem public function testThrowIfDsnCouldNotBeParsed() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Failed to parse DSN "amqp://:@/"'); + $this->expectExceptionMessage('The DSN is invalid.'); - (new ConnectionConfig('amqp://:@/'))->parse(); + (new ConnectionConfig('foo'))->parse(); } public function testShouldParseEmptyDsnWithDriverSet() @@ -110,11 +110,18 @@ public function testShouldParseCustomDsnWithDriverSet() ], $config->getConfig()); } + public function testShouldGetSchemeExtensions() + { + $config = (new ConnectionConfig('amqp+foo+bar:')) + ->addSupportedScheme('amqp') + ->parse() + ; + + $this->assertSame(['foo', 'bar'], $config->getSchemeExtensions()); + } + /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { @@ -230,6 +237,58 @@ public static function provideConfigs() ], ]; + yield [ + 'amqp+tls:', + [ + 'host' => 'localhost', + 'port' => 5672, + 'vhost' => '/', + 'user' => 'guest', + 'pass' => 'guest', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + 'ssl_on' => true, + 'ssl_verify' => true, + 'ssl_cacert' => '', + 'ssl_cert' => '', + 'ssl_key' => '', + 'ssl_passphrase' => '', + ], + ]; + + yield [ + 'amqp+ssl:', + [ + 'host' => 'localhost', + 'port' => 5672, + 'vhost' => '/', + 'user' => 'guest', + 'pass' => 'guest', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + 'ssl_on' => true, + 'ssl_verify' => true, + 'ssl_cacert' => '', + 'ssl_cert' => '', + 'ssl_key' => '', + 'ssl_passphrase' => '', + ], + ]; + yield [ 'amqp://user:pass@host:10000/vhost', [ @@ -498,5 +557,31 @@ public static function provideConfigs() 'ssl_passphrase' => '', ], ]; + + yield [ + 'amqp://guest:guest@localhost:5672/%2f', + [ + 'host' => 'localhost', + 'port' => 5672, + 'vhost' => '/', + 'user' => 'guest', + 'pass' => 'guest', + 'read_timeout' => 3., + 'write_timeout' => 3., + 'connection_timeout' => 3., + 'persisted' => false, + 'lazy' => true, + 'qos_prefetch_size' => 0, + 'qos_prefetch_count' => 1, + 'qos_global' => false, + 'heartbeat' => 0.0, + 'ssl_on' => false, + 'ssl_verify' => true, + 'ssl_cacert' => '', + 'ssl_cert' => '', + 'ssl_key' => '', + 'ssl_passphrase' => '', + ], + ]; } } diff --git a/pkg/amqp-tools/Tests/RabbitMqDelayPluginDelayStrategyTest.php b/pkg/amqp-tools/Tests/RabbitMqDelayPluginDelayStrategyTest.php index 9f19e59d5..d20506919 100644 --- a/pkg/amqp-tools/Tests/RabbitMqDelayPluginDelayStrategyTest.php +++ b/pkg/amqp-tools/Tests/RabbitMqDelayPluginDelayStrategyTest.php @@ -12,9 +12,10 @@ use Interop\Amqp\Impl\AmqpMessage; use Interop\Amqp\Impl\AmqpQueue; use Interop\Amqp\Impl\AmqpTopic; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; +use Interop\Queue\Destination; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Message; +use Interop\Queue\Producer; use PHPUnit\Framework\TestCase; class RabbitMqDelayPluginDelayStrategyTest extends TestCase @@ -165,7 +166,7 @@ public function testShouldThrowExceptionIfInvalidDestination() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext + * @return \PHPUnit\Framework\MockObject\MockObject|AmqpContext */ private function createContextMock() { @@ -173,7 +174,7 @@ private function createContextMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|TestProducer + * @return \PHPUnit\Framework\MockObject\MockObject|TestProducer */ private function createProducerMock() { @@ -183,35 +184,41 @@ private function createProducerMock() class TestProducer implements AmqpProducer, DelayStrategy { - public function delayMessage(AmqpContext $context, AmqpDestination $dest, \Interop\Amqp\AmqpMessage $message, $delayMsec) + public function delayMessage(AmqpContext $context, AmqpDestination $dest, \Interop\Amqp\AmqpMessage $message, int $delay): void { } - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { } - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { + throw new \BadMethodCallException('This should not be called directly'); } - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { + throw new \BadMethodCallException('This should not be called directly'); } - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { + throw new \BadMethodCallException('This should not be called directly'); } - public function getPriority() + public function getPriority(): ?int { + throw new \BadMethodCallException('This should not be called directly'); } - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { + throw new \BadMethodCallException('This should not be called directly'); } - public function getTimeToLive() + public function getTimeToLive(): ?int { + throw new \BadMethodCallException('This should not be called directly'); } } diff --git a/pkg/amqp-tools/Tests/RabbitMqDlxDelayStrategyTest.php b/pkg/amqp-tools/Tests/RabbitMqDlxDelayStrategyTest.php index 9531fe4ce..f519f8da3 100644 --- a/pkg/amqp-tools/Tests/RabbitMqDlxDelayStrategyTest.php +++ b/pkg/amqp-tools/Tests/RabbitMqDlxDelayStrategyTest.php @@ -11,7 +11,8 @@ use Interop\Amqp\Impl\AmqpMessage; use Interop\Amqp\Impl\AmqpQueue; use Interop\Amqp\Impl\AmqpTopic; -use Interop\Queue\InvalidDestinationException; +use Interop\Queue\Exception\InvalidDestinationException; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class RabbitMqDlxDelayStrategyTest extends TestCase @@ -181,7 +182,7 @@ public function testShouldThrowExceptionIfInvalidDestination() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext + * @return MockObject|AmqpContext */ private function createContextMock() { @@ -189,7 +190,7 @@ private function createContextMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpProducer + * @return MockObject|AmqpProducer */ private function createProducerMock() { diff --git a/pkg/amqp-tools/Tests/SignalSocketHelperTest.php b/pkg/amqp-tools/Tests/SignalSocketHelperTest.php index be064a7b8..a44e42a70 100644 --- a/pkg/amqp-tools/Tests/SignalSocketHelperTest.php +++ b/pkg/amqp-tools/Tests/SignalSocketHelperTest.php @@ -3,10 +3,13 @@ namespace Enqueue\AmqpTools\Tests; use Enqueue\AmqpTools\SignalSocketHelper; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; class SignalSocketHelperTest extends TestCase { + use ReadAttributeTrait; + /** * @var SignalSocketHelper */ @@ -16,24 +19,25 @@ class SignalSocketHelperTest extends TestCase private $backupSigIntHandler; - public function setUp() + protected function setUp(): void { parent::setUp(); + // PHP 7.1 and pcntl ext installed higher if (false == function_exists('pcntl_signal_get_handler')) { - $this->markTestSkipped('PHP 7.1 and higher'); + $this->markTestSkipped('PHP 7.1+ needed'); } - $this->backupSigTermHandler = pcntl_signal_get_handler(SIGTERM); - $this->backupSigIntHandler = pcntl_signal_get_handler(SIGINT); + $this->backupSigTermHandler = pcntl_signal_get_handler(\SIGTERM); + $this->backupSigIntHandler = pcntl_signal_get_handler(\SIGINT); - pcntl_signal(SIGTERM, SIG_DFL); - pcntl_signal(SIGINT, SIG_DFL); + pcntl_signal(\SIGTERM, \SIG_DFL); + pcntl_signal(\SIGINT, \SIG_DFL); $this->signalHelper = new SignalSocketHelper(); } - public function tearDown() + protected function tearDown(): void { parent::tearDown(); @@ -42,11 +46,11 @@ public function tearDown() } if ($this->backupSigTermHandler) { - pcntl_signal(SIGTERM, $this->backupSigTermHandler); + pcntl_signal(\SIGTERM, $this->backupSigTermHandler); } if ($this->backupSigIntHandler) { - pcntl_signal(SIGINT, $this->backupSigIntHandler); + pcntl_signal(\SIGINT, $this->backupSigIntHandler); } } @@ -67,7 +71,7 @@ public function testShouldRegisterHandlerOnBeforeSocketAndBackupCurrentOne() { $handler = function () {}; - pcntl_signal(SIGTERM, $handler); + pcntl_signal(\SIGTERM, $handler); $this->signalHelper->beforeSocket(); @@ -75,9 +79,9 @@ public function testShouldRegisterHandlerOnBeforeSocketAndBackupCurrentOne() $handlers = $this->readAttribute($this->signalHelper, 'handlers'); - $this->assertInternalType('array', $handlers); - $this->assertArrayHasKey(SIGTERM, $handlers); - $this->assertSame($handler, $handlers[SIGTERM]); + self::assertIsArray($handlers); + $this->assertArrayHasKey(\SIGTERM, $handlers); + $this->assertSame($handler, $handlers[\SIGTERM]); } public function testRestoreDefaultPropertiesOnAfterSocket() @@ -93,12 +97,12 @@ public function testRestorePreviousHandlerOnAfterSocket() { $handler = function () {}; - pcntl_signal(SIGTERM, $handler); + pcntl_signal(\SIGTERM, $handler); $this->signalHelper->beforeSocket(); $this->signalHelper->afterSocket(); - $this->assertSame($handler, pcntl_signal_get_handler(SIGTERM)); + $this->assertSame($handler, pcntl_signal_get_handler(\SIGTERM)); } public function testThrowsIfBeforeSocketCalledSecondTime() @@ -114,7 +118,7 @@ public function testShouldReturnTrueOnWasThereSignal() { $this->signalHelper->beforeSocket(); - posix_kill(getmypid(), SIGINT); + posix_kill(getmypid(), \SIGINT); pcntl_signal_dispatch(); $this->assertTrue($this->signalHelper->wasThereSignal()); diff --git a/pkg/amqp-tools/Tests/SubscriptionConsumerTest.php b/pkg/amqp-tools/Tests/SubscriptionConsumerTest.php deleted file mode 100644 index 84b380070..000000000 --- a/pkg/amqp-tools/Tests/SubscriptionConsumerTest.php +++ /dev/null @@ -1,96 +0,0 @@ -assertTrue($rc->implementsInterface(PsrSubscriptionConsumer::class)); - } - - public function testCouldBeConstructedWithAmqpContextAsFirstArgument() - { - new SubscriptionConsumer($this->createContext()); - } - - public function testShouldProxySubscribeCallToContextMethod() - { - $consumer = $this->createConsumer(); - $callback = function () {}; - - $context = $this->createContext(); - $context - ->expects($this->once()) - ->method('subscribe') - ->with($this->identicalTo($consumer), $this->identicalTo($callback)) - ; - - $subscriptionConsumer = new SubscriptionConsumer($context); - $subscriptionConsumer->subscribe($consumer, $callback); - } - - public function testShouldProxyUnsubscribeCallToContextMethod() - { - $consumer = $this->createConsumer(); - - $context = $this->createContext(); - $context - ->expects($this->once()) - ->method('unsubscribe') - ->with($this->identicalTo($consumer)) - ; - - $subscriptionConsumer = new SubscriptionConsumer($context); - $subscriptionConsumer->unsubscribe($consumer); - } - - public function testShouldProxyConsumeCallToContextMethod() - { - $timeout = 123.456; - - $context = $this->createContext(); - $context - ->expects($this->once()) - ->method('consume') - ->with($this->identicalTo($timeout)) - ; - - $subscriptionConsumer = new SubscriptionConsumer($context); - $subscriptionConsumer->consume($timeout); - } - - public function testThrowsNotImplementedOnUnsubscribeAllCall() - { - $context = $this->createContext(); - - $subscriptionConsumer = new SubscriptionConsumer($context); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Not implemented'); - $subscriptionConsumer->unsubscribeAll(); - } - - /** - * @return AmqpConsumer|\PHPUnit_Framework_MockObject_MockObject - */ - private function createConsumer() - { - return $this->createMock(AmqpConsumer::class); - } - - /** - * @return AmqpContext|\PHPUnit_Framework_MockObject_MockObject - */ - private function createContext() - { - return $this->createMock(AmqpContext::class); - } -} diff --git a/pkg/amqp-tools/composer.json b/pkg/amqp-tools/composer.json index 4d03bda67..966e065e8 100644 --- a/pkg/amqp-tools/composer.json +++ b/pkg/amqp-tools/composer.json @@ -6,14 +6,15 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1", - "queue-interop/amqp-interop": "^0.7@dev" + "php": "^8.1", + "queue-interop/amqp-interop": "^0.8.2", + "queue-interop/queue-interop": "^0.8", + "enqueue/dsn": "^0.10" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/null": "^0.8@dev" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev" }, "support": { "email": "opensource@forma-pro.com", @@ -31,7 +32,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/async-command/.gitattributes b/pkg/async-command/.gitattributes new file mode 100644 index 000000000..f13d4d91b --- /dev/null +++ b/pkg/async-command/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore diff --git a/pkg/async-command/.github/workflows/ci.yml b/pkg/async-command/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/async-command/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/async-command/.gitignore b/pkg/async-command/.gitignore new file mode 100644 index 000000000..c19bea911 --- /dev/null +++ b/pkg/async-command/.gitignore @@ -0,0 +1,6 @@ +*~ +/composer.lock +/composer.phar +/phpunit.xml +/vendor/ +/.idea/ \ No newline at end of file diff --git a/pkg/async-command/CommandResult.php b/pkg/async-command/CommandResult.php new file mode 100644 index 000000000..10080d587 --- /dev/null +++ b/pkg/async-command/CommandResult.php @@ -0,0 +1,62 @@ +exitCode = $exitCode; + $this->output = $output; + $this->errorOutput = $errorOutput; + } + + public function getExitCode(): int + { + return $this->exitCode; + } + + public function getOutput(): string + { + return $this->output; + } + + public function getErrorOutput(): string + { + return $this->errorOutput; + } + + public function jsonSerialize(): array + { + return [ + 'exitCode' => $this->exitCode, + 'output' => $this->output, + 'errorOutput' => $this->errorOutput, + ]; + } + + public static function jsonUnserialize(string $json): self + { + $data = json_decode($json, true); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); + } + + return new self($data['exitCode'], $data['output'], $data['errorOutput']); + } +} diff --git a/pkg/async-command/Commands.php b/pkg/async-command/Commands.php new file mode 100644 index 000000000..abc015cf8 --- /dev/null +++ b/pkg/async-command/Commands.php @@ -0,0 +1,8 @@ + $client, + 'command_name' => Commands::RUN_COMMAND, + 'queue_name' => Commands::RUN_COMMAND, + 'timeout' => 60, + ]; + } + + $id = sprintf('enqueue.async_command.%s.run_command_processor', $client['name']); + $container->register($id, RunCommandProcessor::class) + ->addArgument('%kernel.project_dir%') + ->addArgument($client['timeout']) + ->addTag('enqueue.processor', [ + 'client' => $client['name'], + 'command' => $client['command_name'] ?? Commands::RUN_COMMAND, + 'queue' => $client['queue_name'] ?? Commands::RUN_COMMAND, + 'prefix_queue' => false, + 'exclusive' => true, + ]) + ->addTag('enqueue.transport.processor') + ; + } + } +} diff --git a/pkg/async-command/LICENSE b/pkg/async-command/LICENSE new file mode 100644 index 000000000..bd25f8e13 --- /dev/null +++ b/pkg/async-command/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 Max Kotliar + +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. \ No newline at end of file diff --git a/pkg/async-command/README.md b/pkg/async-command/README.md new file mode 100644 index 000000000..711e97163 --- /dev/null +++ b/pkg/async-command/README.md @@ -0,0 +1,37 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + +# Symfony Async Command. + +[![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/async-command/ci.yml?branch=master)](https://github.com/php-enqueue/async-command/actions?query=workflow%3ACI) +[![Total Downloads](https://poser.pugx.org/enqueue/async-command/d/total.png)](https://packagist.org/packages/enqueue/async-command) +[![Latest Stable Version](https://poser.pugx.org/enqueue/async-command/version.png)](https://packagist.org/packages/enqueue/async-command) + +It contains an extension to Symfony's [Console](https://symfony.com/doc/current/components/console.html) component. +It allows to execute Symfony's command async by sending the request to message queue. + +## Resources + +* [Site](https://enqueue.forma-pro.com/) +* [Documentation](https://php-enqueue.github.io/) +* [Questions](https://gitter.im/php-enqueue/Lobby) +* [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) + +## Developed by Forma-Pro + +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. + +If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com + +## License + +It is released under the [MIT License](LICENSE). diff --git a/pkg/async-command/RunCommand.php b/pkg/async-command/RunCommand.php new file mode 100644 index 000000000..573a6200b --- /dev/null +++ b/pkg/async-command/RunCommand.php @@ -0,0 +1,72 @@ +command = $command; + $this->arguments = $arguments; + $this->options = $options; + } + + public function getCommand(): string + { + return $this->command; + } + + /** + * @return string[] + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * @return string[] + */ + public function getOptions(): array + { + return $this->options; + } + + public function jsonSerialize(): array + { + return [ + 'command' => $this->command, + 'arguments' => $this->arguments, + 'options' => $this->options, + ]; + } + + public static function jsonUnserialize(string $json): self + { + $data = json_decode($json, true); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); + } + + return new self($data['command'], $data['arguments'], $data['options']); + } +} diff --git a/pkg/async-command/RunCommandProcessor.php b/pkg/async-command/RunCommandProcessor.php new file mode 100644 index 000000000..2c4462f90 --- /dev/null +++ b/pkg/async-command/RunCommandProcessor.php @@ -0,0 +1,66 @@ +projectDir = $projectDir; + $this->timeout = $timeout; + } + + public function process(Message $message, Context $context): Result + { + $command = RunCommand::jsonUnserialize($message->getBody()); + + $phpBin = (new PhpExecutableFinder())->find(); + $consoleBin = file_exists($this->projectDir.'/bin/console') ? './bin/console' : './app/console'; + + $process = new Process(array_merge( + [$phpBin, $consoleBin, $command->getCommand()], + $command->getArguments(), + $this->getCommandLineOptions($command) + ), $this->projectDir); + $process->setTimeout($this->timeout); + $process->run(); + + if ($message->getReplyTo()) { + $result = new CommandResult($process->getExitCode(), $process->getOutput(), $process->getErrorOutput()); + + return Result::reply($context->createMessage(json_encode($result))); + } + + return Result::ack(); + } + + /** + * @return string[] + */ + private function getCommandLineOptions(RunCommand $command): array + { + $options = []; + foreach ($command->getOptions() as $name => $value) { + $options[] = "$name=$value"; + } + + return $options; + } +} diff --git a/pkg/async-command/Tests/CommandResultTest.php b/pkg/async-command/Tests/CommandResultTest.php new file mode 100644 index 000000000..03a615502 --- /dev/null +++ b/pkg/async-command/Tests/CommandResultTest.php @@ -0,0 +1,69 @@ +assertTrue($rc->implementsInterface(\JsonSerializable::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(CommandResult::class); + + $this->assertTrue($rc->isFinal()); + } + + public function testShouldAllowGetExitCodeSetInConstructor() + { + $result = new CommandResult(123, '', ''); + + $this->assertSame(123, $result->getExitCode()); + } + + public function testShouldAllowGetOutputSetInConstructor() + { + $result = new CommandResult(0, 'theOutput', ''); + + $this->assertSame('theOutput', $result->getOutput()); + } + + public function testShouldAllowGetErrorOutputSetInConstructor() + { + $result = new CommandResult(0, '', 'theErrorOutput'); + + $this->assertSame('theErrorOutput', $result->getErrorOutput()); + } + + public function testShouldSerializeAndUnserialzeCommand() + { + $result = new CommandResult(123, 'theOutput', 'theErrorOutput'); + + $jsonCommand = json_encode($result); + + // guard + $this->assertNotEmpty($jsonCommand); + + $unserializedResult = CommandResult::jsonUnserialize($jsonCommand); + + $this->assertInstanceOf(CommandResult::class, $unserializedResult); + $this->assertSame(123, $unserializedResult->getExitCode()); + $this->assertSame('theOutput', $unserializedResult->getOutput()); + $this->assertSame('theErrorOutput', $unserializedResult->getErrorOutput()); + } + + public function testThrowExceptionIfInvalidJsonGiven() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The malformed json given.'); + + CommandResult::jsonUnserialize('{]'); + } +} diff --git a/pkg/async-command/Tests/Functional/UseCasesTest.php b/pkg/async-command/Tests/Functional/UseCasesTest.php new file mode 100644 index 000000000..03eb52543 --- /dev/null +++ b/pkg/async-command/Tests/Functional/UseCasesTest.php @@ -0,0 +1,41 @@ +setReplyTo('aReplyToQueue'); + + $processor = new RunCommandProcessor(__DIR__); + + $result = $processor->process($Message, new NullContext()); + + $this->assertInstanceOf(Result::class, $result); + $this->assertInstanceOf(Message::class, $result->getReply()); + + $replyMessage = $result->getReply(); + + $commandResult = CommandResult::jsonUnserialize($replyMessage->getBody()); + + $this->assertSame(123, $commandResult->getExitCode()); + $this->assertSame('Command Output', $commandResult->getOutput()); + $this->assertSame('Command Error Output', $commandResult->getErrorOutput()); + } +} diff --git a/pkg/async-command/Tests/Functional/bin/console b/pkg/async-command/Tests/Functional/bin/console new file mode 100644 index 000000000..40d7e3583 --- /dev/null +++ b/pkg/async-command/Tests/Functional/bin/console @@ -0,0 +1,6 @@ +assertTrue($rc->implementsInterface(Processor::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(RunCommandProcessor::class); + + $this->assertTrue($rc->isFinal()); + } + + public function testCouldBeConstructedWithProjectDirAsFirstArgument() + { + $processor = new RunCommandProcessor('aProjectDir'); + + $this->assertAttributeSame('aProjectDir', 'projectDir', $processor); + } + + public function testCouldBeConstructedWithTimeoutAsSecondArgument() + { + $processor = new RunCommandProcessor('aProjectDir', 60); + + $this->assertAttributeSame(60, 'timeout', $processor); + } +} diff --git a/pkg/async-command/Tests/RunCommandTest.php b/pkg/async-command/Tests/RunCommandTest.php new file mode 100644 index 000000000..a673e06f3 --- /dev/null +++ b/pkg/async-command/Tests/RunCommandTest.php @@ -0,0 +1,87 @@ +assertTrue($rc->implementsInterface(\JsonSerializable::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(RunCommand::class); + + $this->assertTrue($rc->isFinal()); + } + + public function testShouldAllowGetCommandSetInConstructor() + { + $command = new RunCommand('theCommand'); + + $this->assertSame('theCommand', $command->getCommand()); + } + + public function testShouldReturnEmptyArrayByDefaultOnGetArguments() + { + $command = new RunCommand('aCommand'); + + $this->assertSame([], $command->getArguments()); + } + + public function testShouldReturnEmptyArrayByDefaultOnGetOptions() + { + $command = new RunCommand('aCommand'); + + $this->assertSame([], $command->getOptions()); + } + + public function testShouldReturnArgumentsSetInConstructor() + { + $command = new RunCommand('aCommand', ['theArgument' => 'theValue']); + + $this->assertSame(['theArgument' => 'theValue'], $command->getArguments()); + } + + public function testShouldReturnOptionsSetInConstructor() + { + $command = new RunCommand('aCommand', [], ['theOption' => 'theValue']); + + $this->assertSame(['theOption' => 'theValue'], $command->getOptions()); + } + + public function testShouldSerializeAndUnserialzeCommand() + { + $command = new RunCommand( + 'theCommand', + ['theArgument' => 'theValue'], + ['theOption' => 'theValue'] + ); + + $jsonCommand = json_encode($command); + + // guard + $this->assertNotEmpty($jsonCommand); + + $unserializedCommand = RunCommand::jsonUnserialize($jsonCommand); + + $this->assertInstanceOf(RunCommand::class, $unserializedCommand); + $this->assertSame('theCommand', $unserializedCommand->getCommand()); + $this->assertSame(['theArgument' => 'theValue'], $unserializedCommand->getArguments()); + $this->assertSame(['theOption' => 'theValue'], $unserializedCommand->getOptions()); + } + + public function testThrowExceptionIfInvalidJsonGiven() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The malformed json given.'); + + RunCommand::jsonUnserialize('{]'); + } +} diff --git a/pkg/async-command/composer.json b/pkg/async-command/composer.json new file mode 100644 index 000000000..95d57ce3a --- /dev/null +++ b/pkg/async-command/composer.json @@ -0,0 +1,47 @@ +{ + "name": "enqueue/async-command", + "type": "library", + "description": "Symfony async command", + "keywords": ["messaging", "queue", "async command", "console", "cli"], + "homepage": "https://enqueue.forma-pro.com/", + "license": "MIT", + "require": { + "php": "^8.1", + "enqueue/enqueue": "^0.10", + "queue-interop/queue-interop": "^0.8", + "symfony/console": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "enqueue/null": "0.10.x-dev", + "enqueue/fs": "0.10.x-dev", + "enqueue/test": "0.10.x-dev" + }, + "support": { + "email": "opensource@forma-pro.com", + "issues": "https://github.com/php-enqueue/enqueue-dev/issues", + "forum": "https://gitter.im/php-enqueue/Lobby", + "source": "https://github.com/php-enqueue/enqueue-dev", + "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" + }, + "suggest": { + "symfony/dependency-injection": "^5.4|^6.0 If you'd like to use async event dispatcher container extension." + }, + "autoload": { + "psr-4": { "Enqueue\\AsyncCommand\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "extra": { + "branch-alias": { + "dev-master": "0.10.x-dev" + } + } +} diff --git a/pkg/async-event-dispatcher/.github/workflows/ci.yml b/pkg/async-event-dispatcher/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/async-event-dispatcher/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/async-event-dispatcher/.travis.yml b/pkg/async-event-dispatcher/.travis.yml deleted file mode 100644 index b9cf57fc9..000000000 --- a/pkg/async-event-dispatcher/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/async-event-dispatcher/AbstractAsyncEventDispatcher.php b/pkg/async-event-dispatcher/AbstractAsyncEventDispatcher.php new file mode 100644 index 000000000..5bc7d270a --- /dev/null +++ b/pkg/async-event-dispatcher/AbstractAsyncEventDispatcher.php @@ -0,0 +1,46 @@ +trueEventDispatcher = $trueEventDispatcher; + $this->asyncListener = $asyncListener; + } + + /** + * This method dispatches only those listeners that were marked as async. + * + * @param string $eventName + * @param ContractEvent|Event|null $event + */ + public function dispatchAsyncListenersOnly($eventName, $event = null) + { + try { + $this->asyncListener->syncMode($eventName); + + $this->parentDispatch($event, $eventName); + } finally { + $this->asyncListener->resetSyncMode(); + } + } + + abstract protected function parentDispatch($event, $eventName); +} diff --git a/pkg/async-event-dispatcher/AbstractAsyncListener.php b/pkg/async-event-dispatcher/AbstractAsyncListener.php new file mode 100644 index 000000000..d4ac19a1f --- /dev/null +++ b/pkg/async-event-dispatcher/AbstractAsyncListener.php @@ -0,0 +1,62 @@ +context = $context; + $this->registry = $registry; + $this->eventQueue = $eventQueue instanceof Queue ? $eventQueue : $context->createQueue($eventQueue); + } + + public function resetSyncMode() + { + $this->syncMode = []; + } + + /** + * @param string $eventName + */ + public function syncMode($eventName) + { + $this->syncMode[$eventName] = true; + } + + /** + * @param string $eventName + * + * @return bool + */ + public function isSyncMode($eventName) + { + return isset($this->syncMode[$eventName]); + } +} diff --git a/pkg/async-event-dispatcher/AbstractPhpSerializerEventTransformer.php b/pkg/async-event-dispatcher/AbstractPhpSerializerEventTransformer.php new file mode 100644 index 000000000..6ac53cbc2 --- /dev/null +++ b/pkg/async-event-dispatcher/AbstractPhpSerializerEventTransformer.php @@ -0,0 +1,24 @@ +context = $context; + } + + public function toEvent($eventName, Message $message) + { + return unserialize($message->getBody()); + } +} diff --git a/pkg/async-event-dispatcher/AsyncEventDispatcher.php b/pkg/async-event-dispatcher/AsyncEventDispatcher.php index c74d9745b..e39136eff 100644 --- a/pkg/async-event-dispatcher/AsyncEventDispatcher.php +++ b/pkg/async-event-dispatcher/AsyncEventDispatcher.php @@ -2,56 +2,17 @@ namespace Enqueue\AsyncEventDispatcher; -use Symfony\Component\EventDispatcher\Event; -use Symfony\Component\EventDispatcher\EventDispatcher; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; - -class AsyncEventDispatcher extends EventDispatcher +class AsyncEventDispatcher extends AbstractAsyncEventDispatcher { - /** - * @var EventDispatcherInterface - */ - private $trueEventDispatcher; - - /** - * @var AsyncListener - */ - private $asyncListener; - - /** - * @param EventDispatcherInterface $trueEventDispatcher - * @param AsyncListener $asyncListener - */ - public function __construct(EventDispatcherInterface $trueEventDispatcher, AsyncListener $asyncListener) + public function dispatch(object $event, ?string $eventName = null): object { - $this->trueEventDispatcher = $trueEventDispatcher; - $this->asyncListener = $asyncListener; - } + $this->parentDispatch($event, $eventName); - /** - * This method dispatches only those listeners that were marked as async. - * - * @param string $eventName - * @param Event|null $event - */ - public function dispatchAsyncListenersOnly($eventName, Event $event = null) - { - try { - $this->asyncListener->syncMode($eventName); - - parent::dispatch($eventName, $event); - } finally { - $this->asyncListener->resetSyncMode(); - } + return $this->trueEventDispatcher->dispatch($event, $eventName); } - /** - * {@inheritdoc} - */ - public function dispatch($eventName, Event $event = null) + protected function parentDispatch($event, $eventName) { - parent::dispatch($eventName, $event); - - $this->trueEventDispatcher->dispatch($eventName, $event); + return parent::dispatch($event, $eventName); } } diff --git a/pkg/async-event-dispatcher/AsyncListener.php b/pkg/async-event-dispatcher/AsyncListener.php index 524fdb2f7..2be4976fb 100644 --- a/pkg/async-event-dispatcher/AsyncListener.php +++ b/pkg/async-event-dispatcher/AsyncListener.php @@ -2,74 +2,16 @@ namespace Enqueue\AsyncEventDispatcher; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrQueue; -use Symfony\Component\EventDispatcher\Event; +use Symfony\Contracts\EventDispatcher\Event; -class AsyncListener +class AsyncListener extends AbstractAsyncListener { - /** - * @var PsrContext - */ - private $context; - - /** - * @var Registry - */ - private $registry; - - /** - * @var PsrQueue - */ - private $eventQueue; - - /** - * @var bool - */ - private $syncMode; - - /** - * @param PsrContext $context - * @param Registry $registry - * @param PsrQueue|string $eventQueue - */ - public function __construct(PsrContext $context, Registry $registry, $eventQueue) - { - $this->context = $context; - $this->registry = $registry; - $this->eventQueue = $eventQueue instanceof PsrQueue ? $eventQueue : $context->createQueue($eventQueue); - } - public function __invoke(Event $event, $eventName) { $this->onEvent($event, $eventName); } - public function resetSyncMode() - { - $this->syncMode = []; - } - - /** - * @param string $eventName - */ - public function syncMode($eventName) - { - $this->syncMode[$eventName] = true; - } - - /** - * @param string $eventName - * - * @return bool - */ - public function isSyncMode($eventName) - { - return isset($this->syncMode[$eventName]); - } - /** - * @param Event $event * @param string $eventName */ public function onEvent(Event $event, $eventName) diff --git a/pkg/async-event-dispatcher/AsyncProcessor.php b/pkg/async-event-dispatcher/AsyncProcessor.php index a472d7676..dc61c5381 100644 --- a/pkg/async-event-dispatcher/AsyncProcessor.php +++ b/pkg/async-event-dispatcher/AsyncProcessor.php @@ -3,12 +3,12 @@ namespace Enqueue\AsyncEventDispatcher; use Enqueue\Consumption\Result; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -class AsyncProcessor implements PsrProcessor +class AsyncProcessor implements Processor { /** * @var Registry @@ -16,34 +16,22 @@ class AsyncProcessor implements PsrProcessor private $registry; /** - * @var AsyncEventDispatcher|OldAsyncEventDispatcher + * @var AsyncEventDispatcher */ private $dispatcher; - /** - * @param Registry $registry - * @param EventDispatcherInterface $dispatcher - */ public function __construct(Registry $registry, EventDispatcherInterface $dispatcher) { $this->registry = $registry; - if (false == ($dispatcher instanceof AsyncEventDispatcher || $dispatcher instanceof OldAsyncEventDispatcher)) { - throw new \InvalidArgumentException(sprintf( - 'The dispatcher argument must be either instance of "%s" or "%s" but got "%s"', - AsyncEventDispatcher::class, - OldAsyncEventDispatcher::class, - get_class($dispatcher) - )); + if (false == $dispatcher instanceof AsyncEventDispatcher) { + throw new \InvalidArgumentException(sprintf('The dispatcher argument must be instance of "%s" but got "%s"', AsyncEventDispatcher::class, $dispatcher::class)); } $this->dispatcher = $dispatcher; } - /** - * {@inheritdoc} - */ - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { if (false == $eventName = $message->getProperty('event_name')) { return Result::reject('The message is missing "event_name" property'); diff --git a/pkg/async-event-dispatcher/Commands.php b/pkg/async-event-dispatcher/Commands.php new file mode 100644 index 000000000..c2263ee38 --- /dev/null +++ b/pkg/async-event-dispatcher/Commands.php @@ -0,0 +1,8 @@ + transformerName] * @param string[] $transformersMap [transformerName => transformerServiceId] */ - public function __construct(array $eventsMap, array $transformersMap) + public function __construct(array $eventsMap, array $transformersMap, ContainerInterface $container) { $this->eventsMap = $eventsMap; $this->transformersMap = $transformersMap; + $this->container = $container; } - /** - * {@inheritdoc} - */ public function getTransformerNameForEvent($eventName) { $transformerName = null; @@ -39,7 +39,7 @@ public function getTransformerNameForEvent($eventName) $transformerName = $this->eventsMap[$eventName]; } else { foreach ($this->eventsMap as $eventNamePattern => $name) { - if ('/' != $eventNamePattern[0]) { + if ('/' !== $eventNamePattern[0]) { continue; } @@ -58,9 +58,6 @@ public function getTransformerNameForEvent($eventName) return $transformerName; } - /** - * {@inheritdoc} - */ public function getTransformer($name) { if (false == array_key_exists($name, $this->transformersMap)) { @@ -69,12 +66,8 @@ public function getTransformer($name) $transformer = $this->container->get($this->transformersMap[$name]); - if (false == $transformer instanceof EventTransformer) { - throw new \LogicException(sprintf( - 'The container must return instance of %s but got %s', - EventTransformer::class, - is_object($transformer) ? get_class($transformer) : gettype($transformer) - )); + if (false == $transformer instanceof EventTransformer) { + throw new \LogicException(sprintf('The container must return instance of %s but got %s', EventTransformer::class, is_object($transformer) ? $transformer::class : gettype($transformer))); } return $transformer; diff --git a/pkg/async-event-dispatcher/DependencyInjection/AsyncEventDispatcherExtension.php b/pkg/async-event-dispatcher/DependencyInjection/AsyncEventDispatcherExtension.php index d417bcedd..0b16ca650 100644 --- a/pkg/async-event-dispatcher/DependencyInjection/AsyncEventDispatcherExtension.php +++ b/pkg/async-event-dispatcher/DependencyInjection/AsyncEventDispatcherExtension.php @@ -2,21 +2,17 @@ namespace Enqueue\AsyncEventDispatcher\DependencyInjection; -use Enqueue\AsyncEventDispatcher\OldAsyncEventDispatcher; +use Enqueue\AsyncEventDispatcher\AsyncProcessor; +use Enqueue\AsyncEventDispatcher\Commands; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpKernel\Kernel; class AsyncEventDispatcherExtension extends Extension { - /** - * {@inheritdoc} - */ public function load(array $configs, ContainerBuilder $container) { $config = $this->processConfiguration(new Configuration(), $configs); @@ -26,12 +22,16 @@ public function load(array $configs, ContainerBuilder $container) $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); - if (version_compare(Kernel::VERSION, '3.3', '<')) { - $container->setDefinition('enqueue.events.event_dispatcher', new Definition(OldAsyncEventDispatcher::class, [ - new Reference('service_container'), - new Reference('event_dispatcher'), - new Reference('enqueue.events.async_listener'), - ])); - } + $container->register('enqueue.events.async_processor', AsyncProcessor::class) + ->addArgument(new Reference('enqueue.events.registry')) + ->addArgument(new Reference('enqueue.events.event_dispatcher')) + ->addTag('enqueue.processor', [ + 'command' => Commands::DISPATCH_ASYNC_EVENTS, + 'queue' => '%enqueue_events_queue%', + 'prefix_queue' => false, + 'exclusive' => true, + ]) + ->addTag('enqueue.transport.processor') + ; } } diff --git a/pkg/async-event-dispatcher/DependencyInjection/AsyncEventsPass.php b/pkg/async-event-dispatcher/DependencyInjection/AsyncEventsPass.php index 1db803539..42774adf7 100644 --- a/pkg/async-event-dispatcher/DependencyInjection/AsyncEventsPass.php +++ b/pkg/async-event-dispatcher/DependencyInjection/AsyncEventsPass.php @@ -4,15 +4,11 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class AsyncEventsPass implements CompilerPassInterface { - /** - * {@inheritdoc} - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { if (false == $container->hasDefinition('enqueue.events.async_listener')) { return; @@ -22,6 +18,8 @@ public function process(ContainerBuilder $container) return; } + $defaultClient = $container->getParameter('enqueue.default_client'); + $registeredToEvent = []; foreach ($container->findTaggedServiceIds('kernel.event_listener') as $serviceId => $tagAttributes) { foreach ($tagAttributes as $tagAttribute) { @@ -31,11 +29,6 @@ public function process(ContainerBuilder $container) $event = $tagAttribute['event']; - $service = $container->getDefinition($serviceId); - - $service->clearTag('kernel.event_listener'); - $service->addTag('enqueue.async_event_listener', $tagAttribute); - if (false == isset($registeredToEvent[$event])) { $container->getDefinition('enqueue.events.async_listener') ->addTag('kernel.event_listener', [ @@ -44,9 +37,18 @@ public function process(ContainerBuilder $container) ]) ; + $container->getDefinition('enqueue.events.async_listener') + ->addTag('kernel.event_listener', [ + 'event' => $event, + 'method' => 'onEvent', + 'dispatcher' => 'enqueue.events.event_dispatcher', + ]) + ; + $container->getDefinition('enqueue.events.async_processor') - ->addTag('enqueue.client.processor', [ - 'topicName' => 'event.'.$event, + ->addTag('enqueue.processor', [ + 'topic' => 'event.'.$event, + 'client' => $defaultClient, ]) ; @@ -62,8 +64,6 @@ public function process(ContainerBuilder $container) } $service = $container->getDefinition($serviceId); - $service->clearTag('kernel.event_subscriber'); - $service->addTag('enqueue.async_event_subscriber', $tagAttribute); /** @var EventSubscriberInterface $serviceClass */ $serviceClass = $service->getClass(); @@ -77,9 +77,18 @@ public function process(ContainerBuilder $container) ]) ; + $container->getDefinition('enqueue.events.async_listener') + ->addTag('kernel.event_listener', [ + 'event' => $event, + 'method' => 'onEvent', + 'dispatcher' => 'enqueue.events.event_dispatcher', + ]) + ; + $container->getDefinition('enqueue.events.async_processor') - ->addTag('enqueue.client.processor', [ + ->addTag('enqueue.processor', [ 'topicName' => 'event.'.$event, + 'client' => $defaultClient, ]) ; @@ -88,12 +97,5 @@ public function process(ContainerBuilder $container) } } } - - $registerListenersPass = new RegisterListenersPass( - 'enqueue.events.event_dispatcher', - 'enqueue.async_event_listener', - 'enqueue.async_event_subscriber' - ); - $registerListenersPass->process($container); } } diff --git a/pkg/async-event-dispatcher/DependencyInjection/AsyncTransformersPass.php b/pkg/async-event-dispatcher/DependencyInjection/AsyncTransformersPass.php index 0adcfcb45..89046dd58 100644 --- a/pkg/async-event-dispatcher/DependencyInjection/AsyncTransformersPass.php +++ b/pkg/async-event-dispatcher/DependencyInjection/AsyncTransformersPass.php @@ -7,9 +7,6 @@ class AsyncTransformersPass implements CompilerPassInterface { - /** - * {@inheritdoc} - */ public function process(ContainerBuilder $container) { if (false == $container->hasDefinition('enqueue.events.registry')) { @@ -18,6 +15,7 @@ public function process(ContainerBuilder $container) $transformerIdsMap = []; $eventNamesMap = []; + $defaultTransformer = null; foreach ($container->findTaggedServiceIds('enqueue.event_transformer') as $serviceId => $tagAttributes) { foreach ($tagAttributes as $tagAttribute) { if (false == isset($tagAttribute['eventName'])) { @@ -28,11 +26,24 @@ public function process(ContainerBuilder $container) $transformerName = isset($tagAttribute['transformerName']) ? $tagAttribute['transformerName'] : $serviceId; - $eventNamesMap[$eventName] = $transformerName; - $transformerIdsMap[$transformerName] = $serviceId; + if (isset($tagAttribute['default']) && $tagAttribute['default']) { + $defaultTransformer = [ + 'id' => $serviceId, + 'transformerName' => $transformerName, + 'eventName' => $eventName, + ]; + } else { + $eventNamesMap[$eventName] = $transformerName; + $transformerIdsMap[$transformerName] = $serviceId; + } } } + if ($defaultTransformer) { + $eventNamesMap[$defaultTransformer['eventName']] = $defaultTransformer['transformerName']; + $transformerIdsMap[$defaultTransformer['transformerName']] = $defaultTransformer['id']; + } + $container->getDefinition('enqueue.events.registry') ->replaceArgument(0, $eventNamesMap) ->replaceArgument(1, $transformerIdsMap) diff --git a/pkg/async-event-dispatcher/DependencyInjection/Configuration.php b/pkg/async-event-dispatcher/DependencyInjection/Configuration.php index 9703e6284..7b85a469d 100644 --- a/pkg/async-event-dispatcher/DependencyInjection/Configuration.php +++ b/pkg/async-event-dispatcher/DependencyInjection/Configuration.php @@ -7,13 +7,15 @@ class Configuration implements ConfigurationInterface { - /** - * {@inheritdoc} - */ - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { - $tb = new TreeBuilder(); - $rootNode = $tb->root('enqueue_async_event_dispatcher'); + if (method_exists(TreeBuilder::class, 'getRootNode')) { + $tb = new TreeBuilder('enqueue_async_event_dispatcher'); + $rootNode = $tb->getRootNode(); + } else { + $tb = new TreeBuilder(); + $rootNode = $tb->root('enqueue_async_event_dispatcher'); + } $rootNode->children() ->scalarNode('context_service')->isRequired()->cannotBeEmpty()->end() diff --git a/pkg/async-event-dispatcher/EventTransformer.php b/pkg/async-event-dispatcher/EventTransformer.php index 4aa2360d4..271dffa08 100644 --- a/pkg/async-event-dispatcher/EventTransformer.php +++ b/pkg/async-event-dispatcher/EventTransformer.php @@ -2,28 +2,26 @@ namespace Enqueue\AsyncEventDispatcher; -use Interop\Queue\PsrMessage; -use Symfony\Component\EventDispatcher\Event; +use Interop\Queue\Message; +use Symfony\Contracts\EventDispatcher\Event; interface EventTransformer { /** - * @param string $eventName - * @param Event|null $event + * @param string $eventName * - * @return PsrMessage + * @return Message */ - public function toMessage($eventName, Event $event); + public function toMessage($eventName, ?Event $event = null); /** * If you able to transform message back to event return it. - * If you failed to transform for some reason you can return a string status (@see PsrProcess constants) or an object that implements __toString method. - * The object must have a __toString method is supposed to be used as PsrProcessor::process return value. - * - * @param string $eventName - * @param PsrMessage $message + * If you failed to transform for some reason you can return a string status. * * @return Event|string|object + * + * @see Process constants) or an object that implements __toString method. + * The object must have a __toString method is supposed to be used as Processor::process return value. */ - public function toEvent($eventName, PsrMessage $message); + public function toEvent($eventName, Message $message); } diff --git a/pkg/async-event-dispatcher/OldAsyncEventDispatcher.php b/pkg/async-event-dispatcher/OldAsyncEventDispatcher.php deleted file mode 100644 index 97720af08..000000000 --- a/pkg/async-event-dispatcher/OldAsyncEventDispatcher.php +++ /dev/null @@ -1,61 +0,0 @@ -trueEventDispatcher = $trueEventDispatcher; - $this->asyncListener = $asyncListener; - } - - /** - * This method dispatches only those listeners that were marked as async. - * - * @param string $eventName - * @param Event|null $event - */ - public function dispatchAsyncListenersOnly($eventName, Event $event = null) - { - try { - $this->asyncListener->syncMode($eventName); - - parent::dispatch($eventName, $event); - } finally { - $this->asyncListener->resetSyncMode(); - } - } - - /** - * {@inheritdoc} - */ - public function dispatch($eventName, Event $event = null) - { - parent::dispatch($eventName, $event); - - $this->trueEventDispatcher->dispatch($eventName, $event); - } -} diff --git a/pkg/async-event-dispatcher/PhpSerializerEventTransformer.php b/pkg/async-event-dispatcher/PhpSerializerEventTransformer.php index d99d0d413..9c23883aa 100644 --- a/pkg/async-event-dispatcher/PhpSerializerEventTransformer.php +++ b/pkg/async-event-dispatcher/PhpSerializerEventTransformer.php @@ -2,66 +2,12 @@ namespace Enqueue\AsyncEventDispatcher; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Symfony\Component\EventDispatcher\Event; -use Symfony\Component\HttpKernel\Kernel; +use Symfony\Contracts\EventDispatcher\Event; -class PhpSerializerEventTransformer implements EventTransformer +class PhpSerializerEventTransformer extends AbstractPhpSerializerEventTransformer implements EventTransformer { - /** - * @var PsrContext - */ - private $context; - - /** - * @var bool - */ - private $skipSymfonyVersionCheck; - - /** - * @param PsrContext $context - * @param bool $skipSymfonyVersionCheck It is useful when async dispatcher is used without Kernel. So there is no way to check the version. - */ - public function __construct(PsrContext $context, $skipSymfonyVersionCheck = false) + public function toMessage($eventName, ?Event $event = null) { - $this->context = $context; - $this->skipSymfonyVersionCheck = $skipSymfonyVersionCheck; - } - - /** - * {@inheritdoc} - */ - public function toMessage($eventName, Event $event = null) - { - $this->assertSymfony30OrHigher(); - return $this->context->createMessage(serialize($event)); } - - /** - * {@inheritdoc} - */ - public function toEvent($eventName, PsrMessage $message) - { - $this->assertSymfony30OrHigher(); - - return unserialize($message->getBody()); - } - - private function assertSymfony30OrHigher() - { - if ($this->skipSymfonyVersionCheck) { - return; - } - - if (version_compare(Kernel::VERSION, '3.0', '<')) { - throw new \LogicException( - 'This transformer does not work on Symfony prior 3.0. '. - 'The event contains eventDispatcher and therefor could not be serialized. '. - 'You have to register a transformer for every async event. '. - 'Read the doc: https://github.com/php-enqueue/enqueue-dev/blob/master/docs/bundle/async_events.md#event-transformer' - ); - } - } } diff --git a/pkg/async-event-dispatcher/README.md b/pkg/async-event-dispatcher/README.md index 4e8f2ceb9..c4804d981 100644 --- a/pkg/async-event-dispatcher/README.md +++ b/pkg/async-event-dispatcher/README.md @@ -1,24 +1,33 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Symfony Async Event Dispatcher. [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/async-event-dispathcer.png?branch=master)](https://travis-ci.org/php-enqueue/async-event-dispathcer) -[![Total Downloads](https://poser.pugx.org/enqueue/async-event-dispathcer/d/total.png)](https://packagist.org/packages/enqueue/async-event-dispathcer) -[![Latest Stable Version](https://poser.pugx.org/enqueue/async-event-dispathcer/version.png)](https://packagist.org/packages/enqueue/async-event-dispathcer) - -It contains an extension to Symfony's [EventDispatcher](https://symfony.com/doc/current/components/event_dispatcher.html) component. -It allows to processes events in background by sending them to MQ. +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/async-event-dispatcher/ci.yml?branch=master)](https://github.com/php-enqueue/async-event-dispathcer/actions?query=workflow%3ACI) +[![Total Downloads](https://poser.pugx.org/enqueue/async-event-dispathcer/d/total.png)](https://packagist.org/packages/enqueue/async-event-dispatcher) +[![Latest Stable Version](https://poser.pugx.org/enqueue/async-event-dispathcer/version.png)](https://packagist.org/packages/enqueue/async-event-dispatcher) + +It contains an extension to Symfony's [EventDispatcher](https://symfony.com/doc/current/components/event_dispatcher.html) component. +It allows to process events in background by sending them to MQ. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com diff --git a/pkg/async-event-dispatcher/Resources/config/services.yml b/pkg/async-event-dispatcher/Resources/config/services.yml index e2916058f..67365dcb5 100644 --- a/pkg/async-event-dispatcher/Resources/config/services.yml +++ b/pkg/async-event-dispatcher/Resources/config/services.yml @@ -8,9 +8,7 @@ services: enqueue.events.registry: class: 'Enqueue\AsyncEventDispatcher\ContainerAwareRegistry' public: false - arguments: [[], []] - calls: - - ['setContainer', ['@service_container']] + arguments: [[], [], '@service_container'] enqueue.events.async_listener: class: 'Enqueue\AsyncEventDispatcher\AsyncListener' @@ -25,25 +23,10 @@ services: - '@event_dispatcher' - '@enqueue.events.async_listener' - enqueue.events.async_processor: - class: 'Enqueue\AsyncEventDispatcher\AsyncProcessor' - public: public - arguments: - - '@enqueue.events.registry' - - '@enqueue.events.event_dispatcher' - tags: - - - name: 'enqueue.client.processor' - topicName: '__command__' - processorName: '%enqueue_events_queue%' - queueName: '%enqueue_events_queue%' - queueNameHardcoded: true - exclusive: true - - enqueue.events.php_serializer_event_transofrmer: + enqueue.events.php_serializer_event_transformer: class: 'Enqueue\AsyncEventDispatcher\PhpSerializerEventTransformer' public: public arguments: - '@enqueue.events.context' tags: - - {name: 'enqueue.event_transformer', eventName: '/.*/', transformerName: 'php_serializer' } + - {name: 'enqueue.event_transformer', eventName: '/.*/', transformerName: 'php_serializer', default: true } diff --git a/pkg/async-event-dispatcher/SimpleRegistry.php b/pkg/async-event-dispatcher/SimpleRegistry.php index 2f39d0cac..e5ba16ef8 100644 --- a/pkg/async-event-dispatcher/SimpleRegistry.php +++ b/pkg/async-event-dispatcher/SimpleRegistry.php @@ -24,9 +24,6 @@ public function __construct(array $eventsMap, array $transformersMap) $this->transformersMap = $transformersMap; } - /** - * {@inheritdoc} - */ public function getTransformerNameForEvent($eventName) { $transformerName = null; @@ -53,9 +50,6 @@ public function getTransformerNameForEvent($eventName) return $transformerName; } - /** - * {@inheritdoc} - */ public function getTransformer($name) { if (false == array_key_exists($name, $this->transformersMap)) { @@ -64,12 +58,8 @@ public function getTransformer($name) $transformer = $this->transformersMap[$name]; - if (false == $transformer instanceof EventTransformer) { - throw new \LogicException(sprintf( - 'The container must return instance of %s but got %s', - EventTransformer::class, - is_object($transformer) ? get_class($transformer) : gettype($transformer) - )); + if (false == $transformer instanceof EventTransformer) { + throw new \LogicException(sprintf('The container must return instance of %s but got %s', EventTransformer::class, is_object($transformer) ? $transformer::class : gettype($transformer))); } return $transformer; diff --git a/pkg/async-event-dispatcher/Tests/AsyncListenerTest.php b/pkg/async-event-dispatcher/Tests/AsyncListenerTest.php index c3e6c0a40..d888c0228 100644 --- a/pkg/async-event-dispatcher/Tests/AsyncListenerTest.php +++ b/pkg/async-event-dispatcher/Tests/AsyncListenerTest.php @@ -8,15 +8,18 @@ use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProducer; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\Context; +use Interop\Queue\Producer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\GenericEvent; +use Symfony\Contracts\EventDispatcher\Event; class AsyncListenerTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testCouldBeConstructedWithContextAndRegistryAndEventQueueAsString() { @@ -35,7 +38,7 @@ public function testCouldBeConstructedWithContextAndRegistryAndEventQueueAsStrin $this->assertAttributeSame($eventQueue, 'eventQueue', $listener); } - public function testCouldBeConstructedWithContextAndRegistryAndPsrQueue() + public function testCouldBeConstructedWithContextAndRegistryAndQueue() { $eventQueue = new NullQueue('symfony_events'); @@ -129,7 +132,7 @@ public function testShouldSendMessageIfSyncModeOff() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|EventTransformer + * @return MockObject|EventTransformer */ private function createEventTransformerMock() { @@ -137,23 +140,23 @@ private function createEventTransformerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer + * @return MockObject|Producer */ private function createProducerMock() { - return $this->createMock(PsrProducer::class); + return $this->createMock(Producer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|Context */ private function createContextMock() { - return $this->createMock(PsrContext::class); + return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Registry + * @return MockObject|Registry */ private function createRegistryMock() { diff --git a/pkg/async-event-dispatcher/Tests/AsyncProcessorTest.php b/pkg/async-event-dispatcher/Tests/AsyncProcessorTest.php index 950290882..019f9bcbe 100644 --- a/pkg/async-event-dispatcher/Tests/AsyncProcessorTest.php +++ b/pkg/async-event-dispatcher/Tests/AsyncProcessorTest.php @@ -10,7 +10,8 @@ use Enqueue\Null\NullContext; use Enqueue\Null\NullMessage; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Processor; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\GenericEvent; @@ -20,12 +21,7 @@ class AsyncProcessorTest extends TestCase public function testShouldImplementProcessorInterface() { - $this->assertClassImplements(PsrProcessor::class, AsyncProcessor::class); - } - - public function testCouldBeConstructedWithRegistryAndProxyEventDispatcher() - { - new AsyncProcessor($this->createRegistryMock(), $this->createProxyEventDispatcherMock()); + $this->assertClassImplements(Processor::class, AsyncProcessor::class); } public function testRejectIfMessageMissingEventNameProperty() @@ -97,7 +93,7 @@ public function testShouldDispatchAsyncListenersOnly() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|EventTransformer + * @return MockObject|EventTransformer */ private function createEventTransformerMock() { @@ -105,7 +101,7 @@ private function createEventTransformerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AsyncEventDispatcher + * @return MockObject|AsyncEventDispatcher */ private function createProxyEventDispatcherMock() { @@ -113,7 +109,7 @@ private function createProxyEventDispatcherMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Registry + * @return MockObject|Registry */ private function createRegistryMock() { diff --git a/pkg/async-event-dispatcher/Tests/ContainerAwareRegistryTest.php b/pkg/async-event-dispatcher/Tests/ContainerAwareRegistryTest.php index 125550268..79762ac17 100644 --- a/pkg/async-event-dispatcher/Tests/ContainerAwareRegistryTest.php +++ b/pkg/async-event-dispatcher/Tests/ContainerAwareRegistryTest.php @@ -6,49 +6,39 @@ use Enqueue\AsyncEventDispatcher\EventTransformer; use Enqueue\AsyncEventDispatcher\Registry; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Container; class ContainerAwareRegistryTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementRegistryInterface() { $this->assertClassImplements(Registry::class, ContainerAwareRegistry::class); } - public function testCouldBeConstructedWithEventsMapAndTransformersMapAsArguments() - { - new ContainerAwareRegistry([], []); - } - - public function testShouldSetContainerToContainerProperty() + public function testShouldAllowGetTransportNameByEventName() { $container = new Container(); - $registry = new ContainerAwareRegistry([], []); - - $registry->setContainer($container); - - $this->assertAttributeSame($container, 'container', $registry); - } - - public function testShouldAllowGetTransportNameByEventName() - { $registry = new ContainerAwareRegistry([ - 'fooEvent' => 'fooTrans', - ], []); + 'fooEvent' => 'fooTrans', + ], [], $container); $this->assertEquals('fooTrans', $registry->getTransformerNameForEvent('fooEvent')); } public function testShouldAllowDefineTransportNameAsRegExpPattern() { + $container = new Container(); + $registry = new ContainerAwareRegistry([ '/.*/' => 'fooRegExpTrans', 'fooEvent' => 'fooTrans', - ], []); + ], [], $container); // guard $this->assertEquals('fooTrans', $registry->getTransformerNameForEvent('fooEvent')); @@ -58,9 +48,11 @@ public function testShouldAllowDefineTransportNameAsRegExpPattern() public function testThrowIfNotSupportedEventGiven() { + $container = new Container(); + $registry = new ContainerAwareRegistry([ 'fooEvent' => 'fooTrans', - ], []); + ], [], $container); $this->expectException(\LogicException::class); $this->expectExceptionMessage('There is no transformer registered for the given event fooNotSupportedEvent'); @@ -69,9 +61,11 @@ public function testThrowIfNotSupportedEventGiven() public function testThrowIfThereIsNoRegisteredTransformerWithSuchName() { + $container = new Container(); + $registry = new ContainerAwareRegistry([], [ 'fooTrans' => 'foo_trans_id', - ]); + ], $container); $this->expectException(\LogicException::class); $this->expectExceptionMessage('There is no transformer named fooNotRegisteredName'); @@ -85,8 +79,7 @@ public function testThrowIfContainerReturnsServiceNotInstanceOfEventTransformer( $registry = new ContainerAwareRegistry([], [ 'fooTrans' => 'foo_trans_id', - ]); - $registry->setContainer($container); + ], $container); $this->expectException(\LogicException::class); $this->expectExceptionMessage('The container must return instance of Enqueue\AsyncEventDispatcher\EventTransformer but got stdClass'); @@ -102,8 +95,7 @@ public function testShouldReturnEventTransformer() $registry = new ContainerAwareRegistry([], [ 'fooTrans' => 'foo_trans_id', - ]); - $registry->setContainer($container); + ], $container); $this->assertSame($eventTransformerMock, $registry->getTransformer('fooTrans')); } diff --git a/pkg/async-event-dispatcher/Tests/Functional/UseCasesTest.php b/pkg/async-event-dispatcher/Tests/Functional/UseCasesTest.php index 1c2768d51..169d8ea5b 100644 --- a/pkg/async-event-dispatcher/Tests/Functional/UseCasesTest.php +++ b/pkg/async-event-dispatcher/Tests/Functional/UseCasesTest.php @@ -8,9 +8,9 @@ use Enqueue\AsyncEventDispatcher\SimpleRegistry; use Enqueue\Bundle\Tests\Functional\App\TestAsyncEventTransformer; use Enqueue\Fs\FsConnectionFactory; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; -use Interop\Queue\PsrQueue; +use Interop\Queue\Context; +use Interop\Queue\Processor; +use Interop\Queue\Queue; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -24,12 +24,12 @@ class UseCasesTest extends TestCase { /** - * @var PsrContext + * @var Context */ protected $context; /** - * @var PsrQueue + * @var Queue */ protected $queue; @@ -53,7 +53,7 @@ class UseCasesTest extends TestCase */ protected $asyncProcessor; - public function setUp() + protected function setUp(): void { (new Filesystem())->remove(__DIR__.'/queues/'); @@ -104,7 +104,7 @@ public function testShouldDispatchBothAsyncEventAndSyncOne() echo "Async event\n"; }); - $this->dispatcher->dispatch('test_async', new GenericEvent()); + $this->dispatch($this->dispatcher, new GenericEvent(), 'test_async'); $this->processMessages(); $this->expectOutputString("Sync event\nSend message for event: test_async\nAsync event\n"); @@ -115,7 +115,7 @@ public function testShouldDispatchBothAsyncEventAndSyncOneFromWhenDispatchedFrom $this->dispatcher->addListener('foo', function ($event, $name, EventDispatcherInterface $dispatcher) { echo "Foo event\n"; - $dispatcher->dispatch('test_async', new GenericEvent()); + $this->dispatch($dispatcher, new GenericEvent(), 'test_async'); }); $this->dispatcher->addListener('test_async', function () { @@ -128,7 +128,8 @@ public function testShouldDispatchBothAsyncEventAndSyncOneFromWhenDispatchedFrom echo "Async event\n"; }); - $this->dispatcher->dispatch('foo'); + $this->dispatch($this->dispatcher, new GenericEvent(), 'foo'); + $this->processMessages(); $this->expectOutputString("Foo event\nSync event\nSend message for event: test_async\nAsync event\n"); @@ -142,14 +143,14 @@ public function testShouldDispatchOtherAsyncEventFromAsyncEvent() $this->asyncDispatcher->addListener('test_async', function ($event, $eventName, EventDispatcherInterface $dispatcher) { echo "Async event\n"; - $dispatcher->dispatch('test_async_from_async'); + $this->dispatch($dispatcher, new GenericEvent(), 'test_async_from_async'); }); $this->dispatcher->addListener('test_async_from_async', function ($event, $eventName, EventDispatcherInterface $dispatcher) { echo "Async event from event\n"; }); - $this->dispatcher->dispatch('test_async'); + $this->dispatch($this->dispatcher, new GenericEvent(), 'test_async'); $this->processMessages(); $this->processMessages(); @@ -168,16 +169,27 @@ public function testShouldDispatchSyncListenerIfDispatchedFromAsycListner() $this->asyncDispatcher->addListener('test_async', function ($event, $eventName, EventDispatcherInterface $dispatcher) { echo "Async event\n"; - $dispatcher->dispatch('sync'); + $this->dispatch($dispatcher, new GenericEvent(), 'sync'); }); - $this->dispatcher->dispatch('test_async'); + $this->dispatch($this->dispatcher, new GenericEvent(), 'test_async'); $this->processMessages(); $this->expectOutputString("Send message for event: test_async\nAsync event\nSync event\n"); } + private function dispatch(EventDispatcherInterface $dispatcher, $event, $eventName): void + { + if (!class_exists(Event::class)) { + // Symfony 5 + $dispatcher->dispatch($event, $eventName); + } else { + // Symfony < 5 + $dispatcher->dispatch($eventName, $event); + } + } + private function processMessages() { $consumer = $this->context->createConsumer($this->queue); @@ -185,13 +197,13 @@ private function processMessages() $result = $this->asyncProcessor->process($message, $this->context); switch ((string) $result) { - case PsrProcessor::ACK: + case Processor::ACK: $consumer->acknowledge($message); break; - case PsrProcessor::REJECT: + case Processor::REJECT: $consumer->reject($message); break; - case PsrProcessor::REQUEUE: + case Processor::REQUEUE: $consumer->reject($message, true); break; default: diff --git a/pkg/async-event-dispatcher/Tests/PhpSerializerEventTransformerTest.php b/pkg/async-event-dispatcher/Tests/PhpSerializerEventTransformerTest.php index 407b44cbc..498ca3ae9 100644 --- a/pkg/async-event-dispatcher/Tests/PhpSerializerEventTransformerTest.php +++ b/pkg/async-event-dispatcher/Tests/PhpSerializerEventTransformerTest.php @@ -6,11 +6,11 @@ use Enqueue\AsyncEventDispatcher\PhpSerializerEventTransformer; use Enqueue\Null\NullMessage; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; +use Interop\Queue\Context; +use Interop\Queue\Message; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\GenericEvent; -use Symfony\Component\HttpKernel\Kernel; class PhpSerializerEventTransformerTest extends TestCase { @@ -21,17 +21,8 @@ public function testShouldImplementEventTransformerInterface() $this->assertClassImplements(EventTransformer::class, PhpSerializerEventTransformer::class); } - public function testCouldBeConstructedWithoutAnyArguments() - { - new PhpSerializerEventTransformer($this->createContextStub()); - } - public function testShouldReturnMessageWithPhpSerializedEventAsBodyOnToMessage() { - if (version_compare(Kernel::VERSION, '3.0', '<')) { - $this->markTestSkipped('This functionality only works on Symfony 3.0 or higher'); - } - $transformer = new PhpSerializerEventTransformer($this->createContextStub()); $event = new GenericEvent('theSubject'); @@ -39,16 +30,12 @@ public function testShouldReturnMessageWithPhpSerializedEventAsBodyOnToMessage() $message = $transformer->toMessage('fooEvent', $event); - $this->assertInstanceOf(PsrMessage::class, $message); + $this->assertInstanceOf(Message::class, $message); $this->assertEquals($expectedBody, $message->getBody()); } public function testShouldReturnEventUnserializedFromMessageBodyOnToEvent() { - if (version_compare(Kernel::VERSION, '3.0', '<')) { - $this->markTestSkipped('This functionality only works on Symfony 3.0 or higher'); - } - $message = new NullMessage(); $message->setBody(serialize(new GenericEvent('theSubject'))); @@ -60,40 +47,12 @@ public function testShouldReturnEventUnserializedFromMessageBodyOnToEvent() $this->assertEquals('theSubject', $event->getSubject()); } - public function testThrowNotSupportedExceptionOnSymfonyPrior30OnToMessage() - { - if (version_compare(Kernel::VERSION, '3.0', '>=')) { - $this->markTestSkipped('This functionality only works on Symfony 3.0 or higher'); - } - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('This transformer does not work on Symfony prior 3.0.'); - - $transformer = new PhpSerializerEventTransformer($this->createContextStub()); - - $transformer->toMessage(new GenericEvent()); - } - - public function testThrowNotSupportedExceptionOnSymfonyPrior30OnToEvent() - { - if (version_compare(Kernel::VERSION, '3.0', '>=')) { - $this->markTestSkipped('This functionality only works on Symfony 3.0 or higher'); - } - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('This transformer does not work on Symfony prior 3.0.'); - - $transformer = new PhpSerializerEventTransformer($this->createContextStub()); - - $transformer->toEvent('anEvent', new NullMessage()); - } - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|Context */ private function createContextStub() { - $context = $this->createMock(PsrContext::class); + $context = $this->createMock(Context::class); $context ->expects($this->any()) ->method('createMessage') diff --git a/pkg/async-event-dispatcher/Tests/ProxyEventDispatcherTest.php b/pkg/async-event-dispatcher/Tests/ProxyEventDispatcherTest.php index 7c8319147..eed680aa6 100644 --- a/pkg/async-event-dispatcher/Tests/ProxyEventDispatcherTest.php +++ b/pkg/async-event-dispatcher/Tests/ProxyEventDispatcherTest.php @@ -5,7 +5,9 @@ use Enqueue\AsyncEventDispatcher\AsyncEventDispatcher; use Enqueue\AsyncEventDispatcher\AsyncListener; use Enqueue\Test\ClassExtensionTrait; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\GenericEvent; @@ -80,7 +82,13 @@ public function testShouldCallOtherEventIfDispatchedFromAsyncEventOnDispatchAsyn $asyncEventWasCalled = true; - func_get_arg(2)->dispatch('theOtherEvent'); + if (!class_exists(Event::class)) { + // Symfony 5 + func_get_arg(2)->dispatch(func_get_arg(0), 'theOtherEvent'); + } else { + // Symfony < 5 + func_get_arg(2)->dispatch('theOtherEvent'); + } }); $event = new GenericEvent(); @@ -113,7 +121,7 @@ public function testShouldNotCallAsyncEventIfDispatchedFromOtherEventOnDispatchA } /** - * @return \PHPUnit_Framework_MockObject_MockObject|AsyncListener + * @return MockObject|AsyncListener */ private function createAsyncListenerMock() { diff --git a/pkg/async-event-dispatcher/Tests/SimpleRegistryTest.php b/pkg/async-event-dispatcher/Tests/SimpleRegistryTest.php index 328ed1780..c144e7466 100644 --- a/pkg/async-event-dispatcher/Tests/SimpleRegistryTest.php +++ b/pkg/async-event-dispatcher/Tests/SimpleRegistryTest.php @@ -18,15 +18,10 @@ public function testShouldImplementRegistryInterface() $this->assertClassImplements(Registry::class, SimpleRegistry::class); } - public function testCouldBeConstructedWithEventsMapAndTransformersMapAsArguments() - { - new SimpleRegistry([], []); - } - public function testShouldAllowGetTransportNameByEventName() { $registry = new SimpleRegistry([ - 'fooEvent' => 'fooTrans', + 'fooEvent' => 'fooTrans', ], []); $this->assertEquals('fooTrans', $registry->getTransformerNameForEvent('fooEvent')); diff --git a/pkg/async-event-dispatcher/composer.json b/pkg/async-event-dispatcher/composer.json index 007e8698d..f78597af4 100644 --- a/pkg/async-event-dispatcher/composer.json +++ b/pkg/async-event-dispatcher/composer.json @@ -6,19 +6,21 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "enqueue/enqueue": "^0.8@dev", - "symfony/event-dispatcher": "^2.8|^3|^4" + "php": "^8.1", + "enqueue/enqueue": "^0.10", + "queue-interop/queue-interop": "^0.8", + "symfony/event-dispatcher": "^5.4|^6.0" }, "require-dev": { - "phpunit/phpunit": "~5.5", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4", - "symfony/http-kernel": "^2.8|^3|^4", - "symfony/filesystem": "^2.8|^3|^4", - "enqueue/null": "^0.8@dev", - "enqueue/fs": "^0.8@dev", - "enqueue/test": "^0.8@dev" + "phpunit/phpunit": "^9.5", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "enqueue/null": "0.10.x-dev", + "enqueue/fs": "0.10.x-dev", + "enqueue/test": "0.10.x-dev" }, "support": { "email": "opensource@forma-pro.com", @@ -28,7 +30,7 @@ "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" }, "suggest": { - "symfony/dependency-injection": "^2.8|^3|^4 If you'd like to use async event dispatcher container extension." + "symfony/dependency-injection": "^5.4|^6.0 If you'd like to use async event dispatcher container extension." }, "autoload": { "psr-4": { "Enqueue\\AsyncEventDispatcher\\": "" }, @@ -38,7 +40,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/async-event-dispatcher/phpunit.xml.dist b/pkg/async-event-dispatcher/phpunit.xml.dist index e64c86d98..e5c3f6d2d 100644 --- a/pkg/async-event-dispatcher/phpunit.xml.dist +++ b/pkg/async-event-dispatcher/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/dbal/.github/workflows/ci.yml b/pkg/dbal/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/dbal/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/dbal/.travis.yml b/pkg/dbal/.travis.yml deleted file mode 100644 index b9cf57fc9..000000000 --- a/pkg/dbal/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/dbal/Client/DbalDriver.php b/pkg/dbal/Client/DbalDriver.php deleted file mode 100644 index d5fd9a5a2..000000000 --- a/pkg/dbal/Client/DbalDriver.php +++ /dev/null @@ -1,186 +0,0 @@ - 0, - MessagePriority::LOW => 1, - MessagePriority::NORMAL => 2, - MessagePriority::HIGH => 3, - MessagePriority::VERY_HIGH => 4, - ]; - - /** - * @param DbalContext $context - * @param Config $config - * @param QueueMetaRegistry $queueMetaRegistry - */ - public function __construct(DbalContext $context, Config $config, QueueMetaRegistry $queueMetaRegistry) - { - $this->context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - * - * @return DbalMessage - */ - public function createTransportMessage(Message $message) - { - $properties = $message->getProperties(); - - $headers = $message->getHeaders(); - $headers['content_type'] = $message->getContentType(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($properties); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setDeliveryDelay($message->getDelay()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - if (array_key_exists($message->getPriority(), self::$priorityMap)) { - $transportMessage->setPriority(self::$priorityMap[$message->getPriority()]); - } - - return $transportMessage; - } - - /** - * @param DbalMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - - $clientMessage->setContentType($message->getHeader('content_type')); - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setDelay($message->getDeliveryDelay()); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - $priorityMap = array_flip(self::$priorityMap); - $priority = array_key_exists($message->getPriority(), $priorityMap) ? - $priorityMap[$message->getPriority()] : - MessagePriority::NORMAL; - $clientMessage->setPriority($priority); - - return $clientMessage; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $queue = $this->createQueue($this->config->getRouterQueueName()); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($queue, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - return $this->context->createQueue($transportName); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $log = function ($text, ...$args) use ($logger) { - $logger->debug(sprintf('[DbalDriver] '.$text, ...$args)); - }; - - $log('Creating database table: "%s"', $this->context->getTableName()); - $this->context->createDataBaseTable(); - } - - /** - * {@inheritdoc} - */ - public function getConfig() - { - return $this->config; - } - - /** - * @return array - */ - public static function getPriorityMap() - { - return self::$priorityMap; - } -} diff --git a/pkg/dbal/DbalConnectionFactory.php b/pkg/dbal/DbalConnectionFactory.php index 21731d3dd..305375a89 100644 --- a/pkg/dbal/DbalConnectionFactory.php +++ b/pkg/dbal/DbalConnectionFactory.php @@ -1,12 +1,16 @@ parseDsn($config); } elseif (is_array($config)) { + if (array_key_exists('dsn', $config)) { + $config = array_replace_recursive($config, $this->parseDsn($config['dsn'], $config)); + unset($config['dsn']); + } } else { throw new \LogicException('The config must be either an array of options, a DSN string or null'); } - $this->config = $config; + $this->config = array_replace_recursive([ + 'connection' => [], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, + ], $config); } /** - * {@inheritdoc} - * * @return DbalContext */ - public function createContext() + public function createContext(): Context { if ($this->config['lazy']) { return new DbalContext(function () { @@ -64,20 +75,14 @@ public function createContext() return new DbalContext($this->establishConnection(), $this->config); } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { if ($this->connection) { $this->connection->close(); } } - /** - * @return Connection - */ - private function establishConnection() + private function establishConnection(): Connection { if (false == $this->connection) { $this->connection = DriverManager::getConnection($this->config['connection']); @@ -87,51 +92,53 @@ private function establishConnection() return $this->connection; } - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) + private function parseDsn(string $dsn, ?array $config = null): array { - if (false === parse_url(http://github.com/$dsn)) { - throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); - } - - if (!preg_match('/^([0-9a-z_]+):(.+)?$/', $dsn, $matches)) { - throw new \LogicException('Schema is empty'); - } - $schema = $matches[1]; + $parsedDsn = Dsn::parseFirst($dsn); $supported = [ - 'db2' => true, - 'ibm_db2' => true, - 'mssql' => true, - 'pdo_sqlsrv' => true, - 'mysql' => true, - 'mysql2' => true, - 'pdo_mysql' => true, - 'pgsql' => true, - 'postgres' => true, - 'postgresql' => true, - 'pdo_pgsql' => true, - 'sqlite' => true, - 'sqlite3' => true, - 'pdo_sqlite' => true, + 'db2' => 'db2', + 'ibm-db2' => 'ibm-db2', + 'mssql' => 'mssql', + 'sqlsrv+pdo' => 'pdo_sqlsrv', + 'mysql' => 'mysql', + 'mysql2' => 'mysql2', + 'mysql+pdo' => 'pdo_mysql', + 'pgsql' => 'pgsql', + 'postgres' => 'postgres', + 'pgsql+pdo' => 'pdo_pgsql', + 'sqlite' => 'sqlite', + 'sqlite3' => 'sqlite3', + 'sqlite+pdo' => 'pdo_sqlite', ]; - if (false == isset($supported[$schema])) { - throw new \LogicException(sprintf( - 'The given DSN schema "%s" is not supported. There are supported schemes: "%s".', - $schema, - implode('", "', array_keys($supported)) - )); + if ($parsedDsn && false == isset($supported[$parsedDsn->getScheme()])) { + throw new \LogicException(sprintf('The given DSN schema "%s" is not supported. There are supported schemes: "%s".', $parsedDsn->getScheme(), implode('", "', array_keys($supported)))); + } + + $doctrineScheme = $supported[$parsedDsn->getScheme()]; + $dsnHasProtocolOnly = $parsedDsn->getScheme().':' === $dsn; + if ($dsnHasProtocolOnly && is_array($config) && array_key_exists('connection', $config)) { + $default = [ + 'driver' => $doctrineScheme, + 'host' => 'localhost', + 'port' => '3306', + 'user' => 'root', + 'password' => '', + ]; + + return [ + 'lazy' => true, + 'connection' => array_replace_recursive($default, $config['connection']), + ]; } return [ 'lazy' => true, 'connection' => [ - 'url' => $schema.':' === $dsn ? $schema.'://root@localhost' : $dsn, + 'url' => $dsnHasProtocolOnly ? + $doctrineScheme.'://root@localhost' : + str_replace($parsedDsn->getScheme(), $doctrineScheme, $dsn), ], ]; } diff --git a/pkg/dbal/DbalConsumer.php b/pkg/dbal/DbalConsumer.php index e3ce55dd2..f1f397441 100644 --- a/pkg/dbal/DbalConsumer.php +++ b/pkg/dbal/DbalConsumer.php @@ -1,15 +1,21 @@ context = $context; $this->queue = $queue; $this->dbal = $this->context->getDbalConnection(); + + $this->redeliveryDelay = 1200000; } /** - * Set polling interval in milliseconds. - * - * @param int $msec + * Get interval between retry failed messages in milliseconds. */ - public function setPollingInterval($msec) + public function getRedeliveryDelay(): int { - $this->pollingInterval = $msec * 1000; + return $this->redeliveryDelay; } /** - * Get polling interval in milliseconds. - * - * @return int + * Get interval between retrying failed messages in milliseconds. */ - public function getPollingInterval() + public function setRedeliveryDelay(int $redeliveryDelay): self { - return (int) $this->pollingInterval / 1000; + $this->redeliveryDelay = $redeliveryDelay; + + return $this; } /** - * {@inheritdoc} - * * @return DbalDestination */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } - /** - * {@inheritdoc} - * - * @return DbalMessage|null - */ - public function receive($timeout = 0) + public function receiveNoWait(): ?Message { - $timeout /= 1000; - $startAt = microtime(true); - - while (true) { - $message = $this->receiveMessage(); - - if ($message) { - return $message; - } - - if ($timeout && (microtime(true) - $startAt) >= $timeout) { - return; - } + $redeliveryDelay = $this->getRedeliveryDelay() / 1000; // milliseconds to seconds - usleep($this->pollingInterval); + $this->removeExpiredMessages(); + $this->redeliverMessages(); - if ($timeout && (microtime(true) - $startAt) >= $timeout) { - return; - } - } - } - - /** - * {@inheritdoc} - * - * @return DbalMessage|null - */ - public function receiveNoWait() - { - return $this->receiveMessage(); + return $this->fetchMessage([$this->queue->getQueueName()], $redeliveryDelay); } /** - * {@inheritdoc} - * * @param DbalMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { - // does nothing + InvalidMessageException::assertMessageInstanceOf($message, DbalMessage::class); + + $this->deleteMessage($message->getDeliveryId()); } /** - * {@inheritdoc} - * * @param DbalMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { InvalidMessageException::assertMessageInstanceOf($message, DbalMessage::class); if ($requeue) { - $this->context->createProducer()->send($this->queue, $message); + $message = clone $message; + $message->setRedelivered(false); - return; + $this->getContext()->createProducer()->send($this->queue, $message); } - } - /** - * @return DbalMessage|null - */ - protected function receiveMessage() - { - $this->dbal->beginTransaction(); - try { - $now = time(); - - $dbalMessage = $this->fetchPrioritizedMessage($now) ?: $dbalMessage = $this->fetchMessage($now); - if (false == $dbalMessage) { - $this->dbal->commit(); - - return; - } - - // remove message - $affectedRows = $this->dbal->delete($this->context->getTableName(), ['id' => $dbalMessage['id']], [ - 'id' => Type::GUID, - ]); - - if (1 !== $affectedRows) { - throw new \LogicException(sprintf('Expected record was removed but it is not. id: "%s"', $dbalMessage['id'])); - } - - $this->dbal->commit(); - - if (empty($dbalMessage['time_to_live']) || $dbalMessage['time_to_live'] > time()) { - return $this->convertMessage($dbalMessage); - } - } catch (\Exception $e) { - $this->dbal->rollBack(); - - throw $e; - } - } - - /** - * @param array $dbalMessage - * - * @return DbalMessage - */ - protected function convertMessage(array $dbalMessage) - { - $message = $this->context->createMessage(); - - $message->setBody($dbalMessage['body']); - $message->setPriority((int) $dbalMessage['priority']); - $message->setRedelivered((bool) $dbalMessage['redelivered']); - $message->setPublishedAt((int) $dbalMessage['published_at']); - - if ($dbalMessage['headers']) { - $message->setHeaders(JSON::decode($dbalMessage['headers'])); - } - - if ($dbalMessage['properties']) { - $message->setProperties(JSON::decode($dbalMessage['properties'])); - } - - return $message; + $this->acknowledge($message); } - /** - * @param int $now - * - * @return array|null - */ - private function fetchPrioritizedMessage($now) + protected function getContext(): DbalContext { - $query = $this->dbal->createQueryBuilder(); - $query - ->select('*') - ->from($this->context->getTableName()) - ->andWhere('queue = :queue') - ->andWhere('priority IS NOT NULL') - ->andWhere('(delayed_until IS NULL OR delayed_until <= :delayedUntil)') - ->addOrderBy('priority', 'desc') - ->addOrderBy('published_at', 'asc') - ->setMaxResults(1) - ; - - $sql = $query->getSQL().' '.$this->dbal->getDatabasePlatform()->getWriteLockSQL(); - - return $this->dbal->executeQuery( - $sql, - [ - 'queue' => $this->queue->getQueueName(), - 'delayedUntil' => $now, - ], - [ - 'queue' => Type::STRING, - 'delayedUntil' => Type::INTEGER, - ] - )->fetch(); + return $this->context; } - /** - * @param int $now - * - * @return array|null - */ - private function fetchMessage($now) + protected function getConnection(): Connection { - $query = $this->dbal->createQueryBuilder(); - $query - ->select('*') - ->from($this->context->getTableName()) - ->andWhere('queue = :queue') - ->andWhere('priority IS NULL') - ->andWhere('(delayed_until IS NULL OR delayed_until <= :delayedUntil)') - ->addOrderBy('published_at', 'asc') - ->setMaxResults(1) - ; - - $sql = $query->getSQL().' '.$this->dbal->getDatabasePlatform()->getWriteLockSQL(); - - return $this->dbal->executeQuery( - $sql, - [ - 'queue' => $this->queue->getQueueName(), - 'delayedUntil' => $now, - ], - [ - 'queue' => Type::STRING, - 'delayedUntil' => Type::INTEGER, - ] - )->fetch(); + return $this->dbal; } } diff --git a/pkg/dbal/DbalConsumerHelperTrait.php b/pkg/dbal/DbalConsumerHelperTrait.php new file mode 100644 index 000000000..4a3d32997 --- /dev/null +++ b/pkg/dbal/DbalConsumerHelperTrait.php @@ -0,0 +1,158 @@ +getConnection()->createQueryBuilder() + ->select('id') + ->from($this->getContext()->getTableName()) + ->andWhere('queue IN (:queues)') + ->andWhere('delayed_until IS NULL OR delayed_until <= :delayedUntil') + ->andWhere('delivery_id IS NULL') + ->addOrderBy('priority', 'asc') + ->addOrderBy('published_at', 'asc') + ->setParameter('queues', $queues, Connection::PARAM_STR_ARRAY) + ->setParameter('delayedUntil', $now, DbalType::INTEGER) + ->setMaxResults(1); + + $update = $this->getConnection()->createQueryBuilder() + ->update($this->getContext()->getTableName()) + ->set('delivery_id', ':deliveryId') + ->set('redeliver_after', ':redeliverAfter') + ->andWhere('id = :messageId') + ->andWhere('delivery_id IS NULL') + ->setParameter('deliveryId', $deliveryId, DbalType::GUID) + ->setParameter('redeliverAfter', $now + $redeliveryDelay, DbalType::BIGINT) + ; + + while (microtime(true) < $endAt) { + try { + $result = $select->execute()->fetch(); + if (empty($result)) { + return null; + } + + $update + ->setParameter('messageId', $result['id'], DbalType::GUID); + + if ($update->execute()) { + $deliveredMessage = $this->getConnection()->createQueryBuilder() + ->select('*') + ->from($this->getContext()->getTableName()) + ->andWhere('delivery_id = :deliveryId') + ->setParameter('deliveryId', $deliveryId, DbalType::GUID) + ->setMaxResults(1) + ->execute() + ->fetch(); + + // the message has been removed by a 3rd party, such as truncate operation. + if (false === $deliveredMessage) { + continue; + } + + if ($deliveredMessage['redelivered'] || empty($deliveredMessage['time_to_live']) || $deliveredMessage['time_to_live'] > time()) { + return $this->getContext()->convertMessage($deliveredMessage); + } + } + } catch (RetryableException $e) { + // maybe next time we'll get more luck + } + } + + return null; + } + + protected function redeliverMessages(): void + { + if (null === $this->redeliverMessagesLastExecutedAt) { + $this->redeliverMessagesLastExecutedAt = microtime(true); + } elseif ((microtime(true) - $this->redeliverMessagesLastExecutedAt) < 1) { + return; + } + + $update = $this->getConnection()->createQueryBuilder() + ->update($this->getContext()->getTableName()) + ->set('delivery_id', ':deliveryId') + ->set('redelivered', ':redelivered') + ->andWhere('redeliver_after < :now') + ->andWhere('delivery_id IS NOT NULL') + ->setParameter('now', time(), DbalType::BIGINT) + ->setParameter('deliveryId', null, DbalType::GUID) + ->setParameter('redelivered', true, DbalType::BOOLEAN) + ; + + try { + $update->execute(); + + $this->redeliverMessagesLastExecutedAt = microtime(true); + } catch (RetryableException $e) { + // maybe next time we'll get more luck + } + } + + protected function removeExpiredMessages(): void + { + if (null === $this->removeExpiredMessagesLastExecutedAt) { + $this->removeExpiredMessagesLastExecutedAt = microtime(true); + } elseif ((microtime(true) - $this->removeExpiredMessagesLastExecutedAt) < 1) { + return; + } + + $delete = $this->getConnection()->createQueryBuilder() + ->delete($this->getContext()->getTableName()) + ->andWhere('(time_to_live IS NOT NULL) AND (time_to_live < :now)') + ->andWhere('delivery_id IS NULL') + ->andWhere('redelivered = :redelivered') + + ->setParameter('now', time(), DbalType::BIGINT) + ->setParameter('redelivered', false, DbalType::BOOLEAN) + ; + + try { + $delete->execute(); + } catch (RetryableException $e) { + // maybe next time we'll get more luck + } + + $this->removeExpiredMessagesLastExecutedAt = microtime(true); + } + + private function deleteMessage(string $deliveryId): void + { + if (empty($deliveryId)) { + throw new \LogicException(sprintf('Expected record was removed but it is not. Delivery id: "%s"', $deliveryId)); + } + + $this->getConnection()->delete( + $this->getContext()->getTableName(), + ['delivery_id' => $deliveryId], + ['delivery_id' => DbalType::GUID] + ); + } +} diff --git a/pkg/dbal/DbalContext.php b/pkg/dbal/DbalContext.php index d7d462aa1..869dd67b8 100644 --- a/pkg/dbal/DbalContext.php +++ b/pkg/dbal/DbalContext.php @@ -1,14 +1,23 @@ config = array_replace([ 'table_name' => 'enqueue', 'polling_interval' => null, + 'subscription_polling_interval' => null, ], $config); if ($connection instanceof Connection) { @@ -43,20 +52,11 @@ public function __construct($connection, array $config = []) } elseif (is_callable($connection)) { $this->connectionFactory = $connection; } else { - throw new \InvalidArgumentException(sprintf( - 'The connection argument must be either %s or callable that returns %s.', - Connection::class, - Connection::class - )); + throw new \InvalidArgumentException(sprintf('The connection argument must be either %s or callable that returns %s.', Connection::class, Connection::class)); } } - /** - * {@inheritdoc} - * - * @return DbalMessage - */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { $message = new DbalMessage(); $message->setBody($body); @@ -67,93 +67,138 @@ public function createMessage($body = '', array $properties = [], array $headers } /** - * {@inheritdoc} - * * @return DbalDestination */ - public function createQueue($name) + public function createQueue(string $name): Queue { return new DbalDestination($name); } /** - * {@inheritdoc} - * * @return DbalDestination */ - public function createTopic($name) + public function createTopic(string $name): Topic { return new DbalDestination($name); } - /** - * {@inheritdoc} - */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { - throw new \BadMethodCallException('Dbal transport does not support temporary queues'); + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); } /** - * {@inheritdoc} - * * @return DbalProducer */ - public function createProducer() + public function createProducer(): Producer { return new DbalProducer($this); } /** - * {@inheritdoc} - * * @return DbalConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, DbalDestination::class); $consumer = new DbalConsumer($this, $destination); if (isset($this->config['polling_interval'])) { - $consumer->setPollingInterval($this->config['polling_interval']); + $consumer->setPollingInterval((int) $this->config['polling_interval']); + } + + if (isset($this->config['redelivery_delay'])) { + $consumer->setRedeliveryDelay((int) $this->config['redelivery_delay']); } return $consumer; } - public function close() + public function close(): void + { + } + + public function createSubscriptionConsumer(): SubscriptionConsumer { + $consumer = new DbalSubscriptionConsumer($this); + + if (isset($this->config['redelivery_delay'])) { + $consumer->setRedeliveryDelay($this->config['redelivery_delay']); + } + + if (isset($this->config['subscription_polling_interval'])) { + $consumer->setPollingInterval($this->config['subscription_polling_interval']); + } + + return $consumer; } /** - * @return string + * @internal It must be used here and in the consumer only */ - public function getTableName() + public function convertMessage(array $arrayMessage): DbalMessage { - return $this->config['table_name']; + /** @var DbalMessage $message */ + $message = $this->createMessage( + $arrayMessage['body'], + $arrayMessage['properties'] ? JSON::decode($arrayMessage['properties']) : [], + $arrayMessage['headers'] ? JSON::decode($arrayMessage['headers']) : [] + ); + + if (isset($arrayMessage['id'])) { + $message->setMessageId($arrayMessage['id']); + } + if (isset($arrayMessage['queue'])) { + $message->setQueue($arrayMessage['queue']); + } + if (isset($arrayMessage['redelivered'])) { + $message->setRedelivered((bool) $arrayMessage['redelivered']); + } + if (isset($arrayMessage['priority'])) { + $message->setPriority((int) (-1 * $arrayMessage['priority'])); + } + if (isset($arrayMessage['published_at'])) { + $message->setPublishedAt((int) $arrayMessage['published_at']); + } + if (isset($arrayMessage['delivery_id'])) { + $message->setDeliveryId($arrayMessage['delivery_id']); + } + if (isset($arrayMessage['redeliver_after'])) { + $message->setRedeliverAfter((int) $arrayMessage['redeliver_after']); + } + + return $message; } /** - * @return array + * @param DbalDestination $queue */ - public function getConfig() + public function purgeQueue(Queue $queue): void + { + $this->getDbalConnection()->delete( + $this->getTableName(), + ['queue' => $queue->getQueueName()], + ['queue' => DbalType::STRING] + ); + } + + public function getTableName(): string + { + return $this->config['table_name']; + } + + public function getConfig(): array { return $this->config; } - /** - * @return Connection - */ - public function getDbalConnection() + public function getDbalConnection(): Connection { if (false == $this->connection) { $connection = call_user_func($this->connectionFactory); if (false == $connection instanceof Connection) { - throw new \LogicException(sprintf( - 'The factory must return instance of Doctrine\DBAL\Connection. It returns %s', - is_object($connection) ? get_class($connection) : gettype($connection) - )); + throw new \LogicException(sprintf('The factory must return instance of Doctrine\DBAL\Connection. It returns %s', is_object($connection) ? $connection::class : gettype($connection))); } $this->connection = $connection; @@ -162,7 +207,7 @@ public function getDbalConnection() return $this->connection; } - public function createDataBaseTable() + public function createDataBaseTable(): void { $sm = $this->getDbalConnection()->getSchemaManager(); @@ -171,22 +216,26 @@ public function createDataBaseTable() } $table = new Table($this->getTableName()); - $table->addColumn('id', 'guid'); - $table->addColumn('published_at', 'bigint'); - $table->addColumn('body', 'text', ['notnull' => false]); - $table->addColumn('headers', 'text', ['notnull' => false]); - $table->addColumn('properties', 'text', ['notnull' => false]); - $table->addColumn('redelivered', 'boolean', ['notnull' => false]); - $table->addColumn('queue', 'string'); - $table->addColumn('priority', 'smallint'); - $table->addColumn('delayed_until', 'integer', ['notnull' => false]); - $table->addColumn('time_to_live', 'integer', ['notnull' => false]); + + $table->addColumn('id', DbalType::GUID, ['length' => 16, 'fixed' => true]); + $table->addColumn('published_at', DbalType::BIGINT); + $table->addColumn('body', DbalType::TEXT, ['notnull' => false]); + $table->addColumn('headers', DbalType::TEXT, ['notnull' => false]); + $table->addColumn('properties', DbalType::TEXT, ['notnull' => false]); + $table->addColumn('redelivered', DbalType::BOOLEAN, ['notnull' => false]); + $table->addColumn('queue', DbalType::STRING); + $table->addColumn('priority', DbalType::SMALLINT, ['notnull' => false]); + $table->addColumn('delayed_until', DbalType::BIGINT, ['notnull' => false]); + $table->addColumn('time_to_live', DbalType::BIGINT, ['notnull' => false]); + $table->addColumn('delivery_id', DbalType::GUID, ['length' => 16, 'fixed' => true, 'notnull' => false]); + $table->addColumn('redeliver_after', DbalType::BIGINT, ['notnull' => false]); $table->setPrimaryKey(['id']); - $table->addIndex(['published_at']); - $table->addIndex(['queue']); - $table->addIndex(['priority']); - $table->addIndex(['delayed_until']); + $table->addIndex(['priority', 'published_at', 'queue', 'delivery_id', 'delayed_until', 'id']); + + $table->addIndex(['redeliver_after', 'delivery_id']); + $table->addIndex(['time_to_live', 'delivery_id']); + $table->addIndex(['delivery_id']); $sm->createTable($table); } diff --git a/pkg/dbal/DbalDestination.php b/pkg/dbal/DbalDestination.php index 1117efd34..793bc40e7 100644 --- a/pkg/dbal/DbalDestination.php +++ b/pkg/dbal/DbalDestination.php @@ -1,37 +1,30 @@ destinationName = $name; } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { return $this->destinationName; } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getTopicName(): string { return $this->destinationName; } diff --git a/pkg/dbal/DbalMessage.php b/pkg/dbal/DbalMessage.php index 28be69033..2485f0691 100644 --- a/pkg/dbal/DbalMessage.php +++ b/pkg/dbal/DbalMessage.php @@ -1,10 +1,12 @@ body = $body; $this->properties = $properties; $this->headers = $headers; $this->redelivered = false; - $this->priority = 0; + $this->priority = null; $this->deliveryDelay = null; + $this->deliveryId = null; + $this->redeliverAfter = null; } - /** - * @param string $body - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * {@inheritdoc} - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * @param array $headers - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * {@inheritdoc} - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * {@inheritdoc} - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = $redelivered; } - /** - * {@inheritdoc} - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * {@inheritdoc} - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } - /** - * @return int - */ - public function getPriority() + public function getPriority(): ?int { return $this->priority; } - /** - * @param int $priority - */ - public function setPriority($priority) + public function setPriority(?int $priority = null): void { $this->priority = $priority; } - /** - * @return int - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return $this->deliveryDelay; } /** * Set delay in milliseconds. - * - * @param int $deliveryDelay */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): void { $this->deliveryDelay = $deliveryDelay; } - /** - * @return int|float|null - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return $this->timeToLive; } /** * Set time to live in milliseconds. - * - * @param int|float|null $timeToLive */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): void { $this->timeToLive = $timeToLive; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id', null); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id', null); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); - return null === $value ? null : (int) $value; + return null === $value ? null : $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } - /** - * @return int - */ - public function getPublishedAt() + public function getDeliveryId(): ?string + { + return $this->deliveryId; + } + + public function setDeliveryId(?string $deliveryId = null): void + { + $this->deliveryId = $deliveryId; + } + + public function getRedeliverAfter(): int + { + return $this->redeliverAfter; + } + + public function setRedeliverAfter(?int $redeliverAfter = null): void + { + $this->redeliverAfter = $redeliverAfter; + } + + public function getPublishedAt(): ?int { return $this->publishedAt; } - /** - * @param int $publishedAt - */ - public function setPublishedAt($publishedAt) + public function setPublishedAt(?int $publishedAt = null): void { $this->publishedAt = $publishedAt; } + + public function getQueue(): ?string + { + return $this->queue; + } + + public function setQueue(?string $queue): void + { + $this->queue = $queue; + } } diff --git a/pkg/dbal/DbalProducer.php b/pkg/dbal/DbalProducer.php index 8fdccbd4c..9e3c203dd 100644 --- a/pkg/dbal/DbalProducer.php +++ b/pkg/dbal/DbalProducer.php @@ -1,16 +1,18 @@ context = $context; } /** - * {@inheritdoc} - * * @param DbalDestination $destination * @param DbalMessage $message - * - * @throws Exception */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, DbalDestination::class); InvalidMessageException::assertMessageInstanceOf($message, DbalMessage::class); - if (null !== $this->priority && 0 === $message->getPriority()) { + if (null !== $this->priority && null === $message->getPriority()) { $message->setPriority($this->priority); } if (null !== $this->deliveryDelay && null === $message->getDeliveryDelay()) { @@ -64,21 +59,6 @@ public function send(PsrDestination $destination, PsrMessage $message) } $body = $message->getBody(); - if (is_scalar($body) || null === $body) { - $body = (string) $body; - } else { - throw new InvalidMessageException(sprintf( - 'The message body must be a scalar or null. Got: %s', - is_object($body) ? get_class($body) : gettype($body) - )); - } - - $sql = 'SELECT '.$this->context->getDbalConnection()->getDatabasePlatform()->getGuidExpression(); - $uuid = $this->context->getDbalConnection()->query($sql)->fetchColumn(0); - - if (empty($uuid)) { - throw new \LogicException('The generated uuid is empty'); - } $publishedAt = null !== $message->getPublishedAt() ? $message->getPublishedAt() : @@ -86,112 +66,100 @@ public function send(PsrDestination $destination, PsrMessage $message) ; $dbalMessage = [ - 'id' => $uuid, + 'id' => Uuid::uuid4(), 'published_at' => $publishedAt, 'body' => $body, 'headers' => JSON::encode($message->getHeaders()), 'properties' => JSON::encode($message->getProperties()), - 'priority' => $message->getPriority(), + 'priority' => -1 * $message->getPriority(), 'queue' => $destination->getQueueName(), + 'redelivered' => false, + 'delivery_id' => null, + 'redeliver_after' => null, ]; $delay = $message->getDeliveryDelay(); if ($delay) { if (!is_int($delay)) { - throw new \LogicException(sprintf( - 'Delay must be integer but got: "%s"', - is_object($delay) ? get_class($delay) : gettype($delay) - )); + throw new \LogicException(sprintf('Delay must be integer but got: "%s"', is_object($delay) ? $delay::class : gettype($delay))); } if ($delay <= 0) { throw new \LogicException(sprintf('Delay must be positive integer but got: "%s"', $delay)); } - $dbalMessage['delayed_until'] = time() + (int) $delay / 1000; + $dbalMessage['delayed_until'] = time() + (int) ($delay / 1000); } $timeToLive = $message->getTimeToLive(); if ($timeToLive) { if (!is_int($timeToLive)) { - throw new \LogicException(sprintf( - 'TimeToLive must be integer but got: "%s"', - is_object($timeToLive) ? get_class($timeToLive) : gettype($timeToLive) - )); + throw new \LogicException(sprintf('TimeToLive must be integer but got: "%s"', is_object($timeToLive) ? $timeToLive::class : gettype($timeToLive))); } if ($timeToLive <= 0) { throw new \LogicException(sprintf('TimeToLive must be positive integer but got: "%s"', $timeToLive)); } - $dbalMessage['time_to_live'] = time() + (int) $timeToLive / 1000; + $dbalMessage['time_to_live'] = time() + (int) ($timeToLive / 1000); } try { - $this->context->getDbalConnection()->insert($this->context->getTableName(), $dbalMessage, [ - 'id' => Type::GUID, - 'published_at' => Type::INTEGER, - 'body' => Type::TEXT, - 'headers' => Type::TEXT, - 'properties' => Type::TEXT, - 'priority' => Type::SMALLINT, - 'queue' => Type::STRING, - 'time_to_live' => Type::INTEGER, - 'delayed_until' => Type::INTEGER, + $rowsAffected = $this->context->getDbalConnection()->insert($this->context->getTableName(), $dbalMessage, [ + 'id' => DbalType::GUID, + 'published_at' => DbalType::INTEGER, + 'body' => DbalType::TEXT, + 'headers' => DbalType::TEXT, + 'properties' => DbalType::TEXT, + 'priority' => DbalType::SMALLINT, + 'queue' => DbalType::STRING, + 'time_to_live' => DbalType::INTEGER, + 'delayed_until' => DbalType::INTEGER, + 'redelivered' => DbalType::SMALLINT, + 'delivery_id' => DbalType::STRING, + 'redeliver_after' => DbalType::BIGINT, ]); + + if (1 !== $rowsAffected) { + throw new Exception('The message was not enqueued. Dbal did not confirm that the record is inserted.'); + } } catch (\Exception $e) { - throw new Exception('The transport fails to send the message due to some internal error.', null, $e); + throw new Exception('The transport fails to send the message due to some internal error.', 0, $e); } } - /** - * {@inheritdoc} - */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { $this->deliveryDelay = $deliveryDelay; return $this; } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return $this->deliveryDelay; } - /** - * {@inheritdoc} - */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { $this->priority = $priority; return $this; } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return $this->priority; } - /** - * {@inheritdoc} - */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { $this->timeToLive = $timeToLive; + + return $this; } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return $this->timeToLive; } diff --git a/pkg/dbal/DbalSubscriptionConsumer.php b/pkg/dbal/DbalSubscriptionConsumer.php new file mode 100644 index 000000000..472fdfcb4 --- /dev/null +++ b/pkg/dbal/DbalSubscriptionConsumer.php @@ -0,0 +1,193 @@ +context = $context; + $this->dbal = $this->context->getDbalConnection(); + $this->subscribers = []; + + $this->redeliveryDelay = 1200000; + } + + /** + * Get interval between retrying failed messages in milliseconds. + */ + public function getRedeliveryDelay(): int + { + return $this->redeliveryDelay; + } + + public function setRedeliveryDelay(int $redeliveryDelay): self + { + $this->redeliveryDelay = $redeliveryDelay; + + return $this; + } + + public function getPollingInterval(): int + { + return $this->pollingInterval; + } + + public function setPollingInterval(int $msec): self + { + $this->pollingInterval = $msec; + + return $this; + } + + public function consume(int $timeout = 0): void + { + if (empty($this->subscribers)) { + throw new \LogicException('No subscribers'); + } + + $queueNames = []; + foreach (array_keys($this->subscribers) as $queueName) { + $queueNames[$queueName] = $queueName; + } + + $timeout /= 1000; + $now = time(); + $redeliveryDelay = $this->getRedeliveryDelay() / 1000; // milliseconds to seconds + + $currentQueueNames = []; + $queueConsumed = false; + while (true) { + if (empty($currentQueueNames)) { + $currentQueueNames = $queueNames; + $queueConsumed = false; + } + + $this->removeExpiredMessages(); + $this->redeliverMessages(); + + if ($message = $this->fetchMessage($currentQueueNames, $redeliveryDelay)) { + $queueConsumed = true; + + /** + * @var DbalConsumer $consumer + * @var callable $callback + */ + [$consumer, $callback] = $this->subscribers[$message->getQueue()]; + + if (false === call_user_func($callback, $message, $consumer)) { + return; + } + + unset($currentQueueNames[$message->getQueue()]); + } else { + $currentQueueNames = []; + + if (!$queueConsumed) { + usleep($this->getPollingInterval() * 1000); + } + } + + if ($timeout && microtime(true) >= $now + $timeout) { + return; + } + } + } + + /** + * @param DbalConsumer $consumer + */ + public function subscribe(Consumer $consumer, callable $callback): void + { + if (false == $consumer instanceof DbalConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', DbalConsumer::class, $consumer::class)); + } + + $queueName = $consumer->getQueue()->getQueueName(); + if (array_key_exists($queueName, $this->subscribers)) { + if ($this->subscribers[$queueName][0] === $consumer && $this->subscribers[$queueName][1] === $callback) { + return; + } + + throw new \InvalidArgumentException(sprintf('There is a consumer subscribed to queue: "%s"', $queueName)); + } + + $this->subscribers[$queueName] = [$consumer, $callback]; + } + + /** + * @param DbalConsumer $consumer + */ + public function unsubscribe(Consumer $consumer): void + { + if (false == $consumer instanceof DbalConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', DbalConsumer::class, $consumer::class)); + } + + $queueName = $consumer->getQueue()->getQueueName(); + + if (false == array_key_exists($queueName, $this->subscribers)) { + return; + } + + if ($this->subscribers[$queueName][0] !== $consumer) { + return; + } + + unset($this->subscribers[$queueName]); + } + + public function unsubscribeAll(): void + { + $this->subscribers = []; + } + + protected function getContext(): DbalContext + { + return $this->context; + } + + protected function getConnection(): Connection + { + return $this->dbal; + } +} diff --git a/pkg/dbal/DbalType.php b/pkg/dbal/DbalType.php new file mode 100644 index 000000000..38a14381f --- /dev/null +++ b/pkg/dbal/DbalType.php @@ -0,0 +1,34 @@ + 1000, - How often query for new messages (milliseconds) * 'lazy' => true, - Use lazy database connection (boolean) * ]. - * - * @param ManagerRegistry $registry - * @param array $config */ public function __construct(ManagerRegistry $registry, array $config = []) { @@ -40,11 +40,9 @@ public function __construct(ManagerRegistry $registry, array $config = []) } /** - * {@inheritdoc} - * * @return DbalContext */ - public function createContext() + public function createContext(): Context { if ($this->config['lazy']) { return new DbalContext(function () { @@ -55,17 +53,11 @@ public function createContext() return new DbalContext($this->establishConnection(), $this->config); } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { } - /** - * @return Connection - */ - private function establishConnection() + private function establishConnection(): Connection { $connection = $this->registry->getConnection($this->config['connection_name']); $connection->connect(); diff --git a/pkg/dbal/README.md b/pkg/dbal/README.md index 480bfc5cc..97ed98367 100644 --- a/pkg/dbal/README.md +++ b/pkg/dbal/README.md @@ -1,27 +1,36 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Doctrine DBAL Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/dbal.png?branch=master)](https://travis-ci.org/php-enqueue/dbal) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/dbal/ci.yml?branch=master)](https://github.com/php-enqueue/dbal/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/dbal/d/total.png)](https://packagist.org/packages/enqueue/dbal) [![Latest Stable Version](https://poser.pugx.org/enqueue/dbal/version.png)](https://packagist.org/packages/enqueue/dbal) - -This is an implementation of PSR specification. It allows you to send and consume message through Doctrine DBAL library and SQL like database as broker. + +This is an implementation of Queue Interop specification. It allows you to send and consume message through Doctrine DBAL library and SQL like database as broker. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/dbal/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/dbal/Symfony/DbalTransportFactory.php b/pkg/dbal/Symfony/DbalTransportFactory.php deleted file mode 100644 index 1deb22416..000000000 --- a/pkg/dbal/Symfony/DbalTransportFactory.php +++ /dev/null @@ -1,138 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifString() - ->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('dsn') - ->info('The Doctrine DBAL DSN. Other parameters are ignored if set') - ->end() - ->variableNode('connection') - ->treatNullLike([]) - ->info('Doctrine DBAL connection options. See http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html') - ->end() - ->scalarNode('dbal_connection_name') - ->defaultNull() - ->info('Doctrine dbal connection name.') - ->end() - ->scalarNode('table_name') - ->defaultValue('enqueue') - ->cannotBeEmpty() - ->info('Database table name.') - ->end() - ->integerNode('polling_interval') - ->defaultValue(1000) - ->min(100) - ->info('How often query for new messages.') - ->end() - ->booleanNode('lazy') - ->defaultTrue() - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - if (false == empty($config['dbal_connection_name'])) { - $factory = new Definition(ManagerRegistryConnectionFactory::class); - $factory->setArguments([new Reference('doctrine'), $config]); - } elseif (false == empty($config['dsn'])) { - $factory = new Definition(DbalConnectionFactory::class); - $factory->setArguments([$config['dsn']]); - } elseif (false == empty($config['connection'])) { - $factory = new Definition(DbalConnectionFactory::class); - $factory->setArguments([$config]); - } else { - throw new \LogicException('Set "dbal_connection_name" options when you want ot use doctrine registry, or use "connection" options to setup direct dbal connection.'); - } - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(DbalContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(DbalDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/dbal/Tests/Client/DbalDriverTest.php b/pkg/dbal/Tests/Client/DbalDriverTest.php deleted file mode 100644 index 6c8de2016..000000000 --- a/pkg/dbal/Tests/Client/DbalDriverTest.php +++ /dev/null @@ -1,354 +0,0 @@ -assertClassImplements(DriverInterface::class, DbalDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new DbalDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = $this->createDummyConfig(); - - $driver = new DbalDriver( - $this->createPsrContextMock(), - $config, - $this->createDummyQueueMetaRegistry() - ); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new DbalDestination('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new DbalDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new DbalDestination('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new DbalDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new DbalMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setPriority(2); - $transportMessage->setDeliveryDelay(12345); - - $driver = new DbalDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame(12345, $clientMessage->getDelay()); - - $this->assertNull($clientMessage->getExpire()); - $this->assertSame(MessagePriority::NORMAL, $clientMessage->getPriority()); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setPriority(MessagePriority::VERY_HIGH); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new DbalMessage()) - ; - - $driver = new DbalDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(DbalMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => null, - 'correlation_id' => null, - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - } - - public function testShouldSendMessageToRouter() - { - $topic = new DbalDestination('queue-name'); - $transportMessage = new DbalMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.default') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new DbalDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new DbalDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new DbalDestination('queue-name'); - $transportMessage = new DbalMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new DbalDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new DbalDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new DbalDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBroker() - { - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('getTableName') - ; - $context - ->expects($this->once()) - ->method('createDataBaseTable') - ; - - $driver = new DbalDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|DbalContext - */ - private function createPsrContextMock() - { - return $this->createMock(DbalContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer - */ - private function createPsrProducerMock() - { - return $this->createMock(PsrProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php b/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php index 5b0bb3b05..5929e1479 100644 --- a/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php +++ b/pkg/dbal/Tests/DbalConnectionFactoryConfigTest.php @@ -4,6 +4,7 @@ use Enqueue\Dbal\DbalConnectionFactory; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; /** @@ -12,6 +13,7 @@ class DbalConnectionFactoryConfigTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testThrowNeitherArrayStringNorNullGivenAsConfig() { @@ -24,7 +26,7 @@ public function testThrowNeitherArrayStringNorNullGivenAsConfig() public function testThrowIfSchemeIsNotSupported() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN schema "http" is not supported. There are supported schemes: "db2", "ibm_db2", "mssql", "pdo_sqlsrv", "mysql", "mysql2", "pdo_mysql", "pgsql", "postgres", "postgresql", "pdo_pgsql", "sqlite", "sqlite3", "pdo_sqlite"'); + $this->expectExceptionMessage('The given DSN schema "http" is not supported. There are supported schemes: "db2", "ibm-db2", "mssql", "sqlsrv+pdo", "mysql", "mysql2", "mysql+pdo", "pgsql", "postgres", "pgsql+pdo", "sqlite", "sqlite3", "sqlite+pdo".'); new DbalConnectionFactory('http://example.com/'); } @@ -32,22 +34,20 @@ public function testThrowIfSchemeIsNotSupported() public function testThrowIfDsnCouldNotBeParsed() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Schema is empty'); + $this->expectExceptionMessage('The DSN is invalid. It does not have scheme separator ":".'); new DbalConnectionFactory('invalidDSN'); } /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { $factory = new DbalConnectionFactory($config); - $this->assertAttributeEquals($expectedConfig, 'config', $factory); + $actualConfig = $this->readAttribute($factory, 'config'); + $this->assertSame($expectedConfig, $actualConfig); } public static function provideConfigs() @@ -55,76 +55,177 @@ public static function provideConfigs() yield [ null, [ - 'lazy' => true, 'connection' => [ 'url' => 'mysql://root@localhost', ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, ], ]; yield [ 'mysql:', [ - 'lazy' => true, 'connection' => [ 'url' => 'mysql://root@localhost', ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, ], ]; yield [ - 'pdo_mysql:', + 'mysql+pdo:', [ - 'lazy' => true, 'connection' => [ 'url' => 'pdo_mysql://root@localhost', ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, ], ]; yield [ 'pgsql:', [ - 'lazy' => true, 'connection' => [ 'url' => 'pgsql://root@localhost', ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, ], ]; yield [ - 'mysql://user:pass@host:10000/db', [ + 'dsn' => 'mysql+pdo:', + 'connection' => [ + 'dbname' => 'customDbName', + ], + ], + [ + 'connection' => [ + 'dbname' => 'customDbName', + 'driver' => 'pdo_mysql', + 'host' => 'localhost', + 'port' => '3306', + 'user' => 'root', + 'password' => '', + ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, 'lazy' => true, + ], + ]; + + yield [ + [ + 'dsn' => 'mysql+pdo:', 'connection' => [ - 'url' => 'mysql://user:pass@host:10000/db', + 'dbname' => 'customDbName', + 'host' => 'host', + 'port' => '10000', + 'user' => 'user', + 'password' => 'pass', + ], + ], + [ + 'connection' => [ + 'dbname' => 'customDbName', + 'host' => 'host', + 'port' => '10000', + 'user' => 'user', + 'password' => 'pass', + 'driver' => 'pdo_mysql', + ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, + ], + ]; + + yield [ + [ + 'dsn' => 'mysql+pdo://user:pass@host:10000/db', + 'connection' => [ + 'foo' => 'fooValue', + ], + ], + [ + 'connection' => [ + 'foo' => 'fooValue', + 'url' => 'pdo_mysql://user:pass@host:10000/db', ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, ], ]; yield [ - 'pdo_mysql://user:pass@host:10001/db', + 'mysql://user:pass@host:10000/db', [ + 'connection' => [ + 'url' => 'mysql://user:pass@host:10000/db', + ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, 'lazy' => true, + ], + ]; + + yield [ + 'mysql+pdo://user:pass@host:10001/db', + [ 'connection' => [ 'url' => 'pdo_mysql://user:pass@host:10001/db', ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, ], ]; yield [ [], [ - 'lazy' => true, 'connection' => [ 'url' => 'mysql://root@localhost', ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, + ], + ]; + + yield [ + [ + 'connection' => ['foo' => 'fooVal', 'bar' => 'barVal'], + 'table_name' => 'a_queue_table', + ], + [ + 'connection' => ['foo' => 'fooVal', 'bar' => 'barVal'], + 'table_name' => 'a_queue_table', + 'polling_interval' => 1000, + 'lazy' => true, ], ]; yield [ - ['table_name' => 'a_queue_table', 'connection' => ['foo' => 'fooVal', 'bar' => 'barVal']], - ['table_name' => 'a_queue_table', 'connection' => ['foo' => 'fooVal', 'bar' => 'barVal']], + ['dsn' => 'mysql+pdo://user:pass@host:10001/db', 'foo' => 'fooVal'], + [ + 'connection' => [ + 'url' => 'pdo_mysql://user:pass@host:10001/db', + ], + 'table_name' => 'enqueue', + 'polling_interval' => 1000, + 'lazy' => true, + 'foo' => 'fooVal', + ], ]; } } diff --git a/pkg/dbal/Tests/DbalConnectionFactoryTest.php b/pkg/dbal/Tests/DbalConnectionFactoryTest.php index 987401287..105466c26 100644 --- a/pkg/dbal/Tests/DbalConnectionFactoryTest.php +++ b/pkg/dbal/Tests/DbalConnectionFactoryTest.php @@ -2,57 +2,60 @@ namespace Enqueue\Dbal\Tests; -use Doctrine\DBAL\Connection; use Enqueue\Dbal\DbalConnectionFactory; use Enqueue\Dbal\DbalContext; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\ConnectionFactory; +use PHPUnit\Framework\TestCase; -class DbalConnectionFactoryTest extends \PHPUnit_Framework_TestCase +class DbalConnectionFactoryTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, DbalConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, DbalConnectionFactory::class); } - public function testCouldBeConstructedWithEmptyConfiguration() + public function testShouldCreateLazyContext() { - $factory = new DbalConnectionFactory(); + $factory = new DbalConnectionFactory(['lazy' => true]); + + $context = $factory->createContext(); - $this->assertAttributeEquals([ - 'lazy' => true, - 'connection' => ['url' => 'mysql://root@localhost'], - ], 'config', $factory); + $this->assertInstanceOf(DbalContext::class, $context); + + $this->assertAttributeEquals(null, 'connection', $context); + $this->assertIsCallable($this->readAttribute($context, 'connectionFactory')); } - public function testCouldBeConstructedWithCustomConfiguration() + public function testShouldParseGenericDSN() { - $factory = new DbalConnectionFactory([ - 'connection' => [ - 'dbname' => 'theDbName', - ], - 'lazy' => false, - ]); - - $this->assertAttributeEquals([ - 'lazy' => false, - 'connection' => [ - 'dbname' => 'theDbName', - ], - ], 'config', $factory); + $factory = new DbalConnectionFactory('pgsql+pdo://foo@bar'); + + $context = $factory->createContext(); + + $this->assertInstanceOf(DbalContext::class, $context); + + $config = $context->getConfig(); + $this->assertArrayHasKey('connection', $config); + $this->assertArrayHasKey('url', $config['connection']); + $this->assertEquals('pdo_pgsql://foo@bar', $config['connection']['url']); } - public function testShouldCreateLazyContext() + public function testShouldParseSqliteAbsolutePathDSN() { - $factory = new DbalConnectionFactory(['lazy' => true]); + $factory = new DbalConnectionFactory('sqlite+pdo:////tmp/some.sq3'); $context = $factory->createContext(); $this->assertInstanceOf(DbalContext::class, $context); - $this->assertAttributeEquals(null, 'connection', $context); - $this->assertAttributeInternalType('callable', 'connectionFactory', $context); + $config = $context->getConfig(); + $this->assertArrayHasKey('connection', $config); + $this->assertArrayHasKey('url', $config['connection']); + $this->assertEquals('pdo_sqlite:////tmp/some.sq3', $config['connection']['url']); } } diff --git a/pkg/dbal/Tests/DbalConsumerTest.php b/pkg/dbal/Tests/DbalConsumerTest.php index 410014cca..0b78eab00 100644 --- a/pkg/dbal/Tests/DbalConsumerTest.php +++ b/pkg/dbal/Tests/DbalConsumerTest.php @@ -1,29 +1,31 @@ assertClassImplements(PsrConsumer::class, DbalConsumer::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new DbalConsumer($this->createContextMock(), new DbalDestination('queue')); + $this->assertClassImplements(Consumer::class, DbalConsumer::class); } public function testShouldReturnInstanceOfDestination() @@ -35,10 +37,55 @@ public function testShouldReturnInstanceOfDestination() $this->assertSame($destination, $consumer->getQueue()); } - public function testCouldCallAcknowledgedMethod() + public function testAcknowledgeShouldThrowIfInstanceOfMessageIsInvalid() { + $this->expectException(InvalidMessageException::class); + $this->expectExceptionMessage( + 'The message must be an instance of '. + 'Enqueue\Dbal\DbalMessage '. + 'but it is Enqueue\Dbal\Tests\InvalidMessage.' + ); + $consumer = new DbalConsumer($this->createContextMock(), new DbalDestination('queue')); - $consumer->acknowledge(new DbalMessage()); + $consumer->acknowledge(new InvalidMessage()); + } + + public function testShouldDeleteMessageOnAcknowledge() + { + $deliveryId = Uuid::uuid4(); + + $queue = new DbalDestination('queue'); + + $message = new DbalMessage(); + $message->setBody('theBody'); + $message->setDeliveryId($deliveryId->toString()); + + $dbal = $this->createConectionMock(); + $dbal + ->expects($this->once()) + ->method('delete') + ->with( + 'some-table-name', + ['delivery_id' => $deliveryId->toString()], + ['delivery_id' => DbalType::GUID] + ) + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getDbalConnection') + ->willReturn($dbal) + ; + $context + ->expects($this->once()) + ->method('getTableName') + ->willReturn('some-table-name') + ; + + $consumer = new DbalConsumer($context, $queue); + + $consumer->acknowledge($message); } public function testCouldSetAndGetPollingInterval() @@ -51,6 +98,16 @@ public function testCouldSetAndGetPollingInterval() $this->assertEquals(123456, $consumer->getPollingInterval()); } + public function testCouldSetAndGetRedeliveryDelay() + { + $destination = new DbalDestination('queue'); + + $consumer = new DbalConsumer($this->createContextMock(), $destination); + $consumer->setRedeliveryDelay(123456); + + $this->assertEquals(123456, $consumer->getRedeliveryDelay()); + } + public function testRejectShouldThrowIfInstanceOfMessageIsInvalid() { $this->expectException(InvalidMessageException::class); @@ -64,17 +121,37 @@ public function testRejectShouldThrowIfInstanceOfMessageIsInvalid() $consumer->reject(new InvalidMessage()); } - public function testShouldDoNothingOnReject() + public function testShouldDeleteMessageFromQueueOnReject() { + $deliveryId = Uuid::uuid4(); + $queue = new DbalDestination('queue'); $message = new DbalMessage(); $message->setBody('theBody'); + $message->setDeliveryId($deliveryId->toString()); + + $dbal = $this->createConectionMock(); + $dbal + ->expects($this->once()) + ->method('delete') + ->with( + 'some-table-name', + ['delivery_id' => $deliveryId->toString()], + ['delivery_id' => DbalType::GUID] + ) + ; $context = $this->createContextMock(); $context - ->expects($this->never()) - ->method('createProducer') + ->expects($this->once()) + ->method('getDbalConnection') + ->willReturn($dbal) + ; + $context + ->expects($this->once()) + ->method('getTableName') + ->willReturn('some-table-name') ; $consumer = new DbalConsumer($context, $queue); @@ -88,19 +165,20 @@ public function testRejectShouldReSendMessageToSameQueueOnRequeue() $message = new DbalMessage(); $message->setBody('theBody'); + $message->setDeliveryId(__METHOD__); $producerMock = $this->createProducerMock(); $producerMock ->expects($this->once()) ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($message)) + ->with($this->identicalTo($queue), $this->isInstanceOf(DbalMessage::class)) ; $context = $this->createContextMock(); $context ->expects($this->once()) ->method('createProducer') - ->will($this->returnValue($producerMock)) + ->willReturn($producerMock) ; $consumer = new DbalConsumer($context, $queue); @@ -109,7 +187,7 @@ public function testRejectShouldReSendMessageToSameQueueOnRequeue() } /** - * @return DbalProducer|\PHPUnit_Framework_MockObject_MockObject + * @return DbalProducer|MockObject */ private function createProducerMock() { @@ -117,93 +195,109 @@ private function createProducerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DbalContext + * @return MockObject|DbalContext */ private function createContextMock() { return $this->createMock(DbalContext::class); } + + /** + * @return MockObject|DbalContext + */ + private function createConectionMock() + { + return $this->createMock(Connection::class); + } } -class InvalidMessage implements PsrMessage +class InvalidMessage implements Message { - public function getBody() + public function getBody(): string { + throw new \BadMethodCallException('This should not be called directly'); } - public function setBody($body) + public function setBody(string $body): void { } - public function setProperties(array $properties) + public function setProperties(array $properties): void { } - public function getProperties() + public function getProperties(): array { + throw new \BadMethodCallException('This should not be called directly'); } - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { } - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { } - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { } - public function getHeaders() + public function getHeaders(): array { + throw new \BadMethodCallException('This should not be called directly'); } - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { } - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { } - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { } - public function isRedelivered() + public function isRedelivered(): bool { + throw new \BadMethodCallException('This should not be called directly'); } - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { } - public function getCorrelationId() + public function getCorrelationId(): ?string { + throw new \BadMethodCallException('This should not be called directly'); } - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { } - public function getMessageId() + public function getMessageId(): ?string { + throw new \BadMethodCallException('This should not be called directly'); } - public function getTimestamp() + public function getTimestamp(): ?int { + throw new \BadMethodCallException('This should not be called directly'); } - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { } - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { } - public function getReplyTo() + public function getReplyTo(): ?string { + throw new \BadMethodCallException('This should not be called directly'); } } diff --git a/pkg/dbal/Tests/DbalContextTest.php b/pkg/dbal/Tests/DbalContextTest.php index 88fb11990..a1900b788 100644 --- a/pkg/dbal/Tests/DbalContextTest.php +++ b/pkg/dbal/Tests/DbalContextTest.php @@ -9,22 +9,22 @@ use Enqueue\Dbal\DbalMessage; use Enqueue\Dbal\DbalProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrDestination; - -class DbalContextTest extends \PHPUnit_Framework_TestCase +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\Context; +use Interop\Queue\Destination; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\TemporaryQueueNotSupportedException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class DbalContextTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementContextInterface() { - $this->assertClassImplements(PsrContext::class, DbalContext::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new DbalContext($this->createConnectionMock()); + $this->assertClassImplements(Context::class, DbalContext::class); } public function testCouldBeConstructedWithEmptyConfiguration() @@ -34,6 +34,7 @@ public function testCouldBeConstructedWithEmptyConfiguration() $this->assertAttributeEquals([ 'table_name' => 'enqueue', 'polling_interval' => null, + 'subscription_polling_interval' => null, ], 'config', $factory); } @@ -42,11 +43,13 @@ public function testCouldBeConstructedWithCustomConfiguration() $factory = new DbalContext($this->createConnectionMock(), [ 'table_name' => 'theTableName', 'polling_interval' => 12345, + 'subscription_polling_interval' => 12345, ]); $this->assertAttributeEquals([ 'table_name' => 'theTableName', 'polling_interval' => 12345, + 'subscription_polling_interval' => 12345, ], 'config', $factory); } @@ -59,10 +62,25 @@ public function testShouldCreateMessage() $this->assertEquals('body', $message->getBody()); $this->assertEquals(['pkey' => 'pval'], $message->getProperties()); $this->assertEquals(['hkey' => 'hval'], $message->getHeaders()); - $this->assertSame(0, $message->getPriority()); + $this->assertNull($message->getPriority()); $this->assertFalse($message->isRedelivered()); } + public function testShouldConvertArrayToDbalMessage() + { + $arrayData = [ + 'body' => 'theBody', + 'properties' => json_encode(['barProp' => 'barPropVal']), + 'headers' => json_encode(['fooHeader' => 'fooHeaderVal']), + ]; + $context = new DbalContext($this->createConnectionMock()); + $message = $context->convertMessage($arrayData); + + $this->assertSame('theBody', $message->getBody()); + $this->assertSame(['barProp' => 'barPropVal'], $message->getProperties()); + $this->assertSame(['fooHeader' => 'fooHeaderVal'], $message->getHeaders()); + } + public function testShouldCreateTopic() { $context = new DbalContext($this->createConnectionMock()); @@ -139,14 +157,13 @@ public function testShouldThrowBadMethodCallExceptionOncreateTemporaryQueueCall( { $context = new DbalContext($connection = $this->createConnectionMock()); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Dbal transport does not support temporary queues'); + $this->expectException(TemporaryQueueNotSupportedException::class); $context->createTemporaryQueue(); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Connection + * @return MockObject|Connection */ private function createConnectionMock() { @@ -154,6 +171,6 @@ private function createConnectionMock() } } -class NotSupportedDestination2 implements PsrDestination +class NotSupportedDestination2 implements Destination { } diff --git a/pkg/dbal/Tests/DbalDestinationTest.php b/pkg/dbal/Tests/DbalDestinationTest.php index 2a7d931ce..65d4fa878 100644 --- a/pkg/dbal/Tests/DbalDestinationTest.php +++ b/pkg/dbal/Tests/DbalDestinationTest.php @@ -4,27 +4,28 @@ use Enqueue\Dbal\DbalDestination; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrQueue; -use Interop\Queue\PsrTopic; +use Interop\Queue\Destination; +use Interop\Queue\Queue; +use Interop\Queue\Topic; +use PHPUnit\Framework\TestCase; -class DbalDestinationTest extends \PHPUnit_Framework_TestCase +class DbalDestinationTest extends TestCase { use ClassExtensionTrait; public function testShouldImplementDestinationInterface() { - $this->assertClassImplements(PsrDestination::class, DbalDestination::class); + $this->assertClassImplements(Destination::class, DbalDestination::class); } public function testShouldImplementTopicInterface() { - $this->assertClassImplements(PsrTopic::class, DbalDestination::class); + $this->assertClassImplements(Topic::class, DbalDestination::class); } public function testShouldImplementQueueInterface() { - $this->assertClassImplements(PsrQueue::class, DbalDestination::class); + $this->assertClassImplements(Queue::class, DbalDestination::class); } public function testShouldReturnTopicAndQueuePreviouslySetInConstructor() diff --git a/pkg/dbal/Tests/DbalMessageTest.php b/pkg/dbal/Tests/DbalMessageTest.php index c1c2115e9..df38b0d65 100644 --- a/pkg/dbal/Tests/DbalMessageTest.php +++ b/pkg/dbal/Tests/DbalMessageTest.php @@ -1,11 +1,14 @@ assertSame(['fooHeader' => 'fooHeaderVal'], $message->getHeaders()); } - public function testShouldSetPriorityToZeroInConstructor() + public function testShouldSetPriorityToNullInConstructor() { $message = new DbalMessage(); - $this->assertSame(0, $message->getPriority()); + $this->assertNull($message->getPriority()); } public function testShouldSetDelayToNullInConstructor() diff --git a/pkg/dbal/Tests/DbalProducerTest.php b/pkg/dbal/Tests/DbalProducerTest.php index 547873f05..ec4d2043c 100644 --- a/pkg/dbal/Tests/DbalProducerTest.php +++ b/pkg/dbal/Tests/DbalProducerTest.php @@ -3,39 +3,22 @@ namespace Enqueue\Dbal\Tests; use Enqueue\Dbal\DbalContext; -use Enqueue\Dbal\DbalDestination; use Enqueue\Dbal\DbalMessage; use Enqueue\Dbal\DbalProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrProducer; +use Interop\Queue\Destination; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Producer; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; -class DbalProducerTest extends \PHPUnit_Framework_TestCase +class DbalProducerTest extends TestCase { use ClassExtensionTrait; public function testShouldImplementProducerInterface() { - $this->assertClassImplements(PsrProducer::class, DbalProducer::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new DbalProducer($this->createContextMock()); - } - - public function testShouldThrowIfBodyOfInvalidType() - { - $this->expectException(InvalidMessageException::class); - $this->expectExceptionMessage('The message body must be a scalar or null. Got: stdClass'); - - $producer = new DbalProducer($this->createContextMock()); - - $message = new DbalMessage(new \stdClass()); - - $producer->send(new DbalDestination(''), $message); + $this->assertClassImplements(Producer::class, DbalProducer::class); } public function testShouldThrowIfDestinationOfInvalidType() @@ -53,7 +36,7 @@ public function testShouldThrowIfDestinationOfInvalidType() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DbalContext + * @return MockObject|DbalContext */ private function createContextMock() { @@ -61,6 +44,6 @@ private function createContextMock() } } -class NotSupportedDestination1 implements PsrDestination +class NotSupportedDestination1 implements Destination { } diff --git a/pkg/dbal/Tests/DbalSubscriptionConsumerTest.php b/pkg/dbal/Tests/DbalSubscriptionConsumerTest.php new file mode 100644 index 000000000..bacbec127 --- /dev/null +++ b/pkg/dbal/Tests/DbalSubscriptionConsumerTest.php @@ -0,0 +1,178 @@ +assertTrue($rc->implementsInterface(SubscriptionConsumer::class)); + } + + public function testShouldAddConsumerAndCallbackToSubscribersPropertyOnSubscribe() + { + $subscriptionConsumer = new DbalSubscriptionConsumer($this->createDbalContextMock()); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $barCallback = function () {}; + $barConsumer = $this->createConsumerStub('bar_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + $subscriptionConsumer->subscribe($barConsumer, $barCallback); + + $this->assertAttributeSame([ + 'foo_queue' => [$fooConsumer, $fooCallback], + 'bar_queue' => [$barConsumer, $barCallback], + ], 'subscribers', $subscriptionConsumer); + } + + public function testThrowsIfTrySubscribeAnotherConsumerToAlreadySubscribedQueue() + { + $subscriptionConsumer = new DbalSubscriptionConsumer($this->createDbalContextMock()); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $barCallback = function () {}; + $barConsumer = $this->createConsumerStub('foo_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('There is a consumer subscribed to queue: "foo_queue"'); + $subscriptionConsumer->subscribe($barConsumer, $barCallback); + } + + /** + * @doesNotPerformAssertions + */ + public function testShouldAllowSubscribeSameConsumerAndCallbackSecondTime() + { + $subscriptionConsumer = new DbalSubscriptionConsumer($this->createDbalContextMock()); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + } + + public function testShouldRemoveSubscribedConsumerOnUnsubscribeCall() + { + $subscriptionConsumer = new DbalSubscriptionConsumer($this->createDbalContextMock()); + + $fooConsumer = $this->createConsumerStub('foo_queue'); + $barConsumer = $this->createConsumerStub('bar_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, function () {}); + $subscriptionConsumer->subscribe($barConsumer, function () {}); + + // guard + $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($fooConsumer); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldDoNothingIfTryUnsubscribeNotSubscribedQueueName() + { + $subscriptionConsumer = new DbalSubscriptionConsumer($this->createDbalContextMock()); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + + // guard + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($this->createConsumerStub('bar_queue')); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldDoNothingIfTryUnsubscribeNotSubscribedConsumer() + { + $subscriptionConsumer = new DbalSubscriptionConsumer($this->createDbalContextMock()); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + + // guard + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($this->createConsumerStub('foo_queue')); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldRemoveAllSubscriberOnUnsubscribeAllCall() + { + $subscriptionConsumer = new DbalSubscriptionConsumer($this->createDbalContextMock()); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + $subscriptionConsumer->subscribe($this->createConsumerStub('bar_queue'), function () {}); + + // guard + $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribeAll(); + + $this->assertAttributeCount(0, 'subscribers', $subscriptionConsumer); + } + + public function testThrowsIfTryConsumeWithoutSubscribers() + { + $subscriptionConsumer = new DbalSubscriptionConsumer($this->createDbalContextMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('No subscribers'); + + $subscriptionConsumer->consume(); + } + + /** + * @return DbalContext|MockObject + */ + private function createDbalContextMock() + { + return $this->createMock(DbalContext::class); + } + + /** + * @param mixed|null $queueName + * + * @return Consumer|MockObject + */ + private function createConsumerStub($queueName = null) + { + $queueMock = $this->createMock(Queue::class); + $queueMock + ->expects($this->any()) + ->method('getQueueName') + ->willReturn($queueName); + + $consumerMock = $this->createMock(DbalConsumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queueMock); + + return $consumerMock; + } +} diff --git a/pkg/dbal/Tests/Functional/DbalConsumerTest.php b/pkg/dbal/Tests/Functional/DbalConsumerTest.php index 92060500f..8042598b9 100644 --- a/pkg/dbal/Tests/Functional/DbalConsumerTest.php +++ b/pkg/dbal/Tests/Functional/DbalConsumerTest.php @@ -1,10 +1,12 @@ context = $this->createDbalContext(); } - protected function tearDown() + protected function tearDown(): void { if ($this->context) { $this->context->close(); @@ -41,7 +43,7 @@ public function testShouldSetPublishedAtDateToReceivedMessage() $consumer = $context->createConsumer($queue); // guard - $this->assertNull($consumer->receiveNoWait()); + $this->assertSame(0, $this->getQuerySize()); $time = (int) (microtime(true) * 10000); @@ -49,11 +51,12 @@ public function testShouldSetPublishedAtDateToReceivedMessage() $producer = $context->createProducer(); + /** @var DbalMessage $message */ $message = $context->createMessage($expectedBody); $message->setPublishedAt($time); $producer->send($queue, $message); - $message = $consumer->receive(8000); // 8 sec + $message = $consumer->receive(100); // 100ms $this->assertInstanceOf(DbalMessage::class, $message); $consumer->acknowledge($message); @@ -69,7 +72,7 @@ public function testShouldOrderMessagesWithSamePriorityByPublishedAtDate() $consumer = $context->createConsumer($queue); // guard - $this->assertNull($consumer->receiveNoWait()); + $this->assertSame(0, $this->getQuerySize()); $time = (int) (microtime(true) * 10000); $olderTime = $time - 10000; @@ -95,10 +98,82 @@ public function testShouldOrderMessagesWithSamePriorityByPublishedAtDate() $consumer->acknowledge($message); $this->assertSame($expectedPriority5BodyOlderTime, $message->getBody()); - $message = $consumer->receive(8000); // 8 sec + $message = $consumer->receive(100); // 8 sec $this->assertInstanceOf(DbalMessage::class, $message); $consumer->acknowledge($message); $this->assertSame($expectedPriority5Body, $message->getBody()); } + + public function testShouldDeleteExpiredMessage() + { + $context = $this->context; + $queue = $context->createQueue(__METHOD__); + + // guard + $this->assertSame(0, $this->getQuerySize()); + + $producer = $context->createProducer(); + + $this->context->getDbalConnection()->insert( + $this->context->getTableName(), [ + 'id' => 'id', + 'published_at' => '123', + 'body' => 'expiredMessage', + 'headers' => json_encode([]), + 'properties' => json_encode([]), + 'queue' => __METHOD__, + 'redelivered' => 0, + 'time_to_live' => time() - 10000, + ]); + + $message = $context->createMessage('notExpiredMessage'); + $message->setRedelivered(false); + $producer->send($queue, $message); + + $this->assertSame(2, $this->getQuerySize()); + + // we need a new consumer to workaround redeliver + $consumer = $context->createConsumer($queue); + $message = $consumer->receive(100); + + $this->assertSame(1, $this->getQuerySize()); + + $consumer->acknowledge($message); + + $this->assertSame(0, $this->getQuerySize()); + } + + public function testShouldRemoveOriginalMessageThatHaveBeenRejectedWithRequeue() + { + $context = $this->context; + $queue = $context->createQueue(__METHOD__); + + $consumer = $context->createConsumer($queue); + + // guard + $this->assertSame(0, $this->getQuerySize()); + + $producer = $context->createProducer(); + + /** @var DbalMessage $message */ + $message = $context->createMessage(__CLASS__); + $producer->send($queue, $message); + + $this->assertSame(1, $this->getQuerySize()); + + $message = $consumer->receive(100); // 100ms + + $this->assertInstanceOf(DbalMessage::class, $message); + $consumer->reject($message, true); + $this->assertSame(1, $this->getQuerySize()); + } + + private function getQuerySize(): int + { + return (int) $this->context->getDbalConnection() + ->executeQuery('SELECT count(*) FROM '.$this->context->getTableName()) + ->fetchOne() + ; + } } diff --git a/pkg/dbal/Tests/ManagerRegistryConnectionFactoryTest.php b/pkg/dbal/Tests/ManagerRegistryConnectionFactoryTest.php index f739a941c..83135c2ed 100644 --- a/pkg/dbal/Tests/ManagerRegistryConnectionFactoryTest.php +++ b/pkg/dbal/Tests/ManagerRegistryConnectionFactoryTest.php @@ -2,20 +2,24 @@ namespace Enqueue\Dbal\Tests; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\DBAL\Connection; +use Doctrine\Persistence\ManagerRegistry; use Enqueue\Dbal\DbalContext; use Enqueue\Dbal\ManagerRegistryConnectionFactory; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\ConnectionFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; -class ManagerRegistryConnectionFactoryTest extends \PHPUnit_Framework_TestCase +class ManagerRegistryConnectionFactoryTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, ManagerRegistryConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, ManagerRegistryConnectionFactory::class); } public function testCouldBeConstructedWithEmptyConfiguration() @@ -69,11 +73,11 @@ public function testShouldCreateLazyContext() $this->assertInstanceOf(DbalContext::class, $context); $this->assertAttributeEquals(null, 'connection', $context); - $this->assertAttributeInternalType('callable', 'connectionFactory', $context); + $this->assertIsCallable($this->readAttribute($context, 'connectionFactory')); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry + * @return MockObject|ManagerRegistry */ private function createManagerRegistryMock() { @@ -81,7 +85,7 @@ private function createManagerRegistryMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Connection + * @return MockObject|Connection */ private function createConnectionMock() { diff --git a/pkg/dbal/Tests/Spec/DbalConnectionFactoryTest.php b/pkg/dbal/Tests/Spec/DbalConnectionFactoryTest.php index 2ed159787..dc39cffe3 100644 --- a/pkg/dbal/Tests/Spec/DbalConnectionFactoryTest.php +++ b/pkg/dbal/Tests/Spec/DbalConnectionFactoryTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Dbal\Tests\Spec; use Enqueue\Dbal\DbalConnectionFactory; -use Interop\Queue\Spec\PsrConnectionFactorySpec; +use Interop\Queue\Spec\ConnectionFactorySpec; -class DbalConnectionFactoryTest extends PsrConnectionFactorySpec +class DbalConnectionFactoryTest extends ConnectionFactorySpec { - /** - * {@inheritdoc} - */ protected function createConnectionFactory() { return new DbalConnectionFactory(); diff --git a/pkg/dbal/Tests/Spec/DbalMessageTest.php b/pkg/dbal/Tests/Spec/DbalMessageTest.php index ca7d4dc69..ee5bdcf6c 100644 --- a/pkg/dbal/Tests/Spec/DbalMessageTest.php +++ b/pkg/dbal/Tests/Spec/DbalMessageTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Dbal\Tests\Spec; use Enqueue\Dbal\DbalMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class DbalMessageTest extends PsrMessageSpec +class DbalMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new DbalMessage(); diff --git a/pkg/dbal/Tests/Spec/DbalQueueTest.php b/pkg/dbal/Tests/Spec/DbalQueueTest.php index 091a48046..690f7e1d6 100644 --- a/pkg/dbal/Tests/Spec/DbalQueueTest.php +++ b/pkg/dbal/Tests/Spec/DbalQueueTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Dbal\Tests\Spec; use Enqueue\Dbal\DbalDestination; -use Interop\Queue\Spec\PsrQueueSpec; +use Interop\Queue\Spec\QueueSpec; -class DbalQueueTest extends PsrQueueSpec +class DbalQueueTest extends QueueSpec { - /** - * {@inheritdoc} - */ protected function createQueue() { return new DbalDestination(self::EXPECTED_QUEUE_NAME); diff --git a/pkg/dbal/Tests/Spec/DbalTopicTest.php b/pkg/dbal/Tests/Spec/DbalTopicTest.php index 91bd52fd0..4bd554681 100644 --- a/pkg/dbal/Tests/Spec/DbalTopicTest.php +++ b/pkg/dbal/Tests/Spec/DbalTopicTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Dbal\Tests\Spec; use Enqueue\Dbal\DbalDestination; -use Interop\Queue\Spec\PsrTopicSpec; +use Interop\Queue\Spec\TopicSpec; -class DbalTopicTest extends PsrTopicSpec +class DbalTopicTest extends TopicSpec { - /** - * {@inheritdoc} - */ protected function createTopic() { return new DbalDestination(self::EXPECTED_TOPIC_NAME); diff --git a/pkg/dbal/Tests/Spec/Mysql/CreateDbalContextTrait.php b/pkg/dbal/Tests/Spec/Mysql/CreateDbalContextTrait.php new file mode 100644 index 000000000..8f76cb2ff --- /dev/null +++ b/pkg/dbal/Tests/Spec/Mysql/CreateDbalContextTrait.php @@ -0,0 +1,27 @@ +markTestSkipped('The MYSQL_DSN env is not available. Skip tests'); + } + + $factory = new DbalConnectionFactory($env); + + $context = $factory->createContext(); + + if ($context->getDbalConnection()->getSchemaManager()->tablesExist([$context->getTableName()])) { + $context->getDbalConnection()->getSchemaManager()->dropTable($context->getTableName()); + } + + $context->createDataBaseTable(); + + return $context; + } +} diff --git a/pkg/dbal/Tests/Spec/DbalContextTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalContextTest.php similarity index 52% rename from pkg/dbal/Tests/Spec/DbalContextTest.php rename to pkg/dbal/Tests/Spec/Mysql/DbalContextTest.php index eb5a5f74f..f235dd50a 100644 --- a/pkg/dbal/Tests/Spec/DbalContextTest.php +++ b/pkg/dbal/Tests/Spec/Mysql/DbalContextTest.php @@ -1,19 +1,16 @@ createDbalContext(); diff --git a/pkg/dbal/Tests/Spec/DbalProducerTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalProducerTest.php similarity index 54% rename from pkg/dbal/Tests/Spec/DbalProducerTest.php rename to pkg/dbal/Tests/Spec/Mysql/DbalProducerTest.php index 2580d3fd3..99cfa2aa6 100644 --- a/pkg/dbal/Tests/Spec/DbalProducerTest.php +++ b/pkg/dbal/Tests/Spec/Mysql/DbalProducerTest.php @@ -1,19 +1,16 @@ createDbalContext()->createProducer(); diff --git a/pkg/dbal/Tests/Spec/DbalRequeueMessageTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalRequeueMessageTest.php similarity index 78% rename from pkg/dbal/Tests/Spec/DbalRequeueMessageTest.php rename to pkg/dbal/Tests/Spec/Mysql/DbalRequeueMessageTest.php index ec22326ad..a642d7288 100644 --- a/pkg/dbal/Tests/Spec/DbalRequeueMessageTest.php +++ b/pkg/dbal/Tests/Spec/Mysql/DbalRequeueMessageTest.php @@ -1,6 +1,6 @@ createDbalContext(); diff --git a/pkg/dbal/Tests/Spec/DbalSendAndReceiveDelayedMessageFromQueueTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalSendAndReceiveDelayedMessageFromQueueTest.php similarity index 82% rename from pkg/dbal/Tests/Spec/DbalSendAndReceiveDelayedMessageFromQueueTest.php rename to pkg/dbal/Tests/Spec/Mysql/DbalSendAndReceiveDelayedMessageFromQueueTest.php index 1ee7abb79..2455217d6 100644 --- a/pkg/dbal/Tests/Spec/DbalSendAndReceiveDelayedMessageFromQueueTest.php +++ b/pkg/dbal/Tests/Spec/Mysql/DbalSendAndReceiveDelayedMessageFromQueueTest.php @@ -1,6 +1,6 @@ createDbalContext(); diff --git a/pkg/dbal/Tests/Spec/DbalSendAndReceivePriorityMessagesFromQueueTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalSendAndReceivePriorityMessagesFromQueueTest.php similarity index 81% rename from pkg/dbal/Tests/Spec/DbalSendAndReceivePriorityMessagesFromQueueTest.php rename to pkg/dbal/Tests/Spec/Mysql/DbalSendAndReceivePriorityMessagesFromQueueTest.php index 7f9441784..6926a3d57 100644 --- a/pkg/dbal/Tests/Spec/DbalSendAndReceivePriorityMessagesFromQueueTest.php +++ b/pkg/dbal/Tests/Spec/Mysql/DbalSendAndReceivePriorityMessagesFromQueueTest.php @@ -1,10 +1,10 @@ createDbalContext(); diff --git a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromQueueTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveFromQueueTest.php similarity index 80% rename from pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromQueueTest.php rename to pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveFromQueueTest.php index 84ae52345..798e4b844 100644 --- a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromQueueTest.php +++ b/pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveFromQueueTest.php @@ -1,6 +1,6 @@ createDbalContext(); diff --git a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromTopicTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveFromTopicTest.php similarity index 80% rename from pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromTopicTest.php rename to pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveFromTopicTest.php index c2b6c085b..1d6f99456 100644 --- a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveFromTopicTest.php +++ b/pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveFromTopicTest.php @@ -1,6 +1,6 @@ createDbalContext(); diff --git a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromQueueTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveNoWaitFromQueueTest.php similarity index 81% rename from pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromQueueTest.php rename to pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveNoWaitFromQueueTest.php index 523673d1c..d96cb85a4 100644 --- a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromQueueTest.php +++ b/pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveNoWaitFromQueueTest.php @@ -1,6 +1,6 @@ createDbalContext(); diff --git a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromTopicTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveNoWaitFromTopicTest.php similarity index 81% rename from pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromTopicTest.php rename to pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveNoWaitFromTopicTest.php index e8f94bb44..b211fc0ab 100644 --- a/pkg/dbal/Tests/Spec/DbalSendToAndReceiveNoWaitFromTopicTest.php +++ b/pkg/dbal/Tests/Spec/Mysql/DbalSendToAndReceiveNoWaitFromTopicTest.php @@ -1,6 +1,6 @@ createDbalContext(); diff --git a/pkg/dbal/Tests/Spec/Mysql/DbalSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..015f1b716 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Mysql/DbalSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,37 @@ +createDbalContext(); + } + + /** + * @param DbalContext $context + */ + protected function createQueue(Context $context, $queueName) + { + $queue = parent::createQueue($context, $queueName); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/dbal/Tests/Spec/Mysql/DbalSubscriptionConsumerConsumeUntilUnsubscribedTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalSubscriptionConsumerConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..37c406804 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Mysql/DbalSubscriptionConsumerConsumeUntilUnsubscribedTest.php @@ -0,0 +1,37 @@ +createDbalContext(); + } + + /** + * @param DbalContext $context + */ + protected function createQueue(Context $context, $queueName) + { + $queue = parent::createQueue($context, $queueName); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/dbal/Tests/Spec/Mysql/DbalSubscriptionConsumerStopOnFalseTest.php b/pkg/dbal/Tests/Spec/Mysql/DbalSubscriptionConsumerStopOnFalseTest.php new file mode 100644 index 000000000..ad59c9e6f --- /dev/null +++ b/pkg/dbal/Tests/Spec/Mysql/DbalSubscriptionConsumerStopOnFalseTest.php @@ -0,0 +1,37 @@ +createDbalContext(); + } + + /** + * @param DbalContext $context + */ + protected function createQueue(Context $context, $queueName) + { + $queue = parent::createQueue($context, $queueName); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/dbal/Tests/Spec/CreateDbalContextTrait.php b/pkg/dbal/Tests/Spec/Postgresql/CreateDbalContextTrait.php similarity index 77% rename from pkg/dbal/Tests/Spec/CreateDbalContextTrait.php rename to pkg/dbal/Tests/Spec/Postgresql/CreateDbalContextTrait.php index 15c1f75e4..fe5a19a0c 100644 --- a/pkg/dbal/Tests/Spec/CreateDbalContextTrait.php +++ b/pkg/dbal/Tests/Spec/Postgresql/CreateDbalContextTrait.php @@ -1,6 +1,6 @@ markTestSkipped('The DOCTRINE_DSN env is not available. Skip tests'); + if (false == $env = getenv('POSTGRES_DSN')) { + $this->markTestSkipped('The POSTGRES_DSN env is not available. Skip tests'); } $factory = new DbalConnectionFactory($env); diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalContextTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalContextTest.php new file mode 100644 index 000000000..b07978cbd --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalContextTest.php @@ -0,0 +1,18 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalProducerTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalProducerTest.php new file mode 100644 index 000000000..aa8894de3 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalProducerTest.php @@ -0,0 +1,18 @@ +createDbalContext()->createProducer(); + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalRequeueMessageTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalRequeueMessageTest.php new file mode 100644 index 000000000..300a572eb --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalRequeueMessageTest.php @@ -0,0 +1,18 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSendAndReceiveDelayedMessageFromQueueTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSendAndReceiveDelayedMessageFromQueueTest.php new file mode 100644 index 000000000..4d915c3b5 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSendAndReceiveDelayedMessageFromQueueTest.php @@ -0,0 +1,18 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSendAndReceivePriorityMessagesFromQueueTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSendAndReceivePriorityMessagesFromQueueTest.php new file mode 100644 index 000000000..556f53b00 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSendAndReceivePriorityMessagesFromQueueTest.php @@ -0,0 +1,49 @@ +publishedAt = (int) (microtime(true) * 10000); + } + + /** + * @return Context + */ + protected function createContext() + { + return $this->createDbalContext(); + } + + /** + * @param DbalContext $context + * + * @return DbalMessage + */ + protected function createMessage(Context $context, $body) + { + /** @var DbalMessage $message */ + $message = parent::createMessage($context, $body); + + // in order to test priorities correctly we have to make sure the messages were sent in the same time. + $message->setPublishedAt($this->publishedAt); + + return $message; + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSendAndReceiveTimeToLiveMessagesFromQueueTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSendAndReceiveTimeToLiveMessagesFromQueueTest.php new file mode 100644 index 000000000..db92febe3 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSendAndReceiveTimeToLiveMessagesFromQueueTest.php @@ -0,0 +1,18 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveFromQueueTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveFromQueueTest.php new file mode 100644 index 000000000..63e4456f0 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveFromQueueTest.php @@ -0,0 +1,18 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveFromTopicTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveFromTopicTest.php new file mode 100644 index 000000000..a2989fd54 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveFromTopicTest.php @@ -0,0 +1,18 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveNoWaitFromQueueTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveNoWaitFromQueueTest.php new file mode 100644 index 000000000..9a08f3676 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveNoWaitFromQueueTest.php @@ -0,0 +1,18 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveNoWaitFromTopicTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveNoWaitFromTopicTest.php new file mode 100644 index 000000000..4383acd36 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSendToAndReceiveNoWaitFromTopicTest.php @@ -0,0 +1,18 @@ +createDbalContext(); + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..d2c8ee22e --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,37 @@ +createDbalContext(); + } + + /** + * @param DbalContext $context + */ + protected function createQueue(Context $context, $queueName) + { + $queue = parent::createQueue($context, $queueName); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSubscriptionConsumerConsumeUntilUnsubscribedTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSubscriptionConsumerConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..892adf372 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSubscriptionConsumerConsumeUntilUnsubscribedTest.php @@ -0,0 +1,37 @@ +createDbalContext(); + } + + /** + * @param DbalContext $context + */ + protected function createQueue(Context $context, $queueName) + { + $queue = parent::createQueue($context, $queueName); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/dbal/Tests/Spec/Postgresql/DbalSubscriptionConsumerStopOnFalseTest.php b/pkg/dbal/Tests/Spec/Postgresql/DbalSubscriptionConsumerStopOnFalseTest.php new file mode 100644 index 000000000..9eeb918b8 --- /dev/null +++ b/pkg/dbal/Tests/Spec/Postgresql/DbalSubscriptionConsumerStopOnFalseTest.php @@ -0,0 +1,37 @@ +createDbalContext(); + } + + /** + * @param DbalContext $context + */ + protected function createQueue(Context $context, $queueName) + { + $queue = parent::createQueue($context, $queueName); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/dbal/Tests/Symfony/DbalTransportFactoryTest.php b/pkg/dbal/Tests/Symfony/DbalTransportFactoryTest.php deleted file mode 100644 index 7f809da43..000000000 --- a/pkg/dbal/Tests/Symfony/DbalTransportFactoryTest.php +++ /dev/null @@ -1,197 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, DbalTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new DbalTransportFactory(); - - $this->assertEquals('dbal', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new DbalTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new DbalTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'connection' => [ - 'key' => 'value', - ], - ]]); - - $this->assertEquals([ - 'connection' => [ - 'key' => 'value', - ], - 'lazy' => true, - 'table_name' => 'enqueue', - 'polling_interval' => 1000, - 'dbal_connection_name' => null, - ], $config); - } - - public function testShouldAllowAddConfigurationAsString() - { - $transport = new DbalTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['mysqlDSN']); - - $this->assertEquals([ - 'dsn' => 'mysqlDSN', - 'dbal_connection_name' => null, - 'table_name' => 'enqueue', - 'polling_interval' => 1000, - 'lazy' => true, - ], $config); - } - - public function testShouldCreateDbalConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new DbalTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'connection' => [ - 'dbname' => 'theDbName', - ], - 'lazy' => true, - 'table_name' => 'enqueue', - 'polling_interval' => 1000, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(DbalConnectionFactory::class, $factory->getClass()); - $this->assertSame([ - 'connection' => [ - 'dbname' => 'theDbName', - ], - 'lazy' => true, - 'table_name' => 'enqueue', - 'polling_interval' => 1000, - ], $factory->getArgument(0)); - } - - public function testShouldCreateConnectionFactoryFromDsnString() - { - $container = new ContainerBuilder(); - - $transport = new DbalTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theDSN', - 'connection' => [], - 'lazy' => true, - 'table_name' => 'enqueue', - 'polling_interval' => 1000, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(DbalConnectionFactory::class, $factory->getClass()); - $this->assertSame('theDSN', $factory->getArgument(0)); - } - - public function testShouldCreateManagerRegistryConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new DbalTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dbal_connection_name' => 'default', - 'lazy' => true, - 'table_name' => 'enqueue', - 'polling_interval' => 1000, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(ManagerRegistryConnectionFactory::class, $factory->getClass()); - $this->assertInstanceOf(Reference::class, $factory->getArgument(0)); - $this->assertSame('doctrine', (string) $factory->getArgument(0)); - $this->assertSame([ - 'dbal_connection_name' => 'default', - 'lazy' => true, - 'table_name' => 'enqueue', - 'polling_interval' => 1000, - ], $factory->getArgument(1)); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new DbalTransportFactory(); - - $serviceId = $transport->createContext($container, []); - - $this->assertEquals('enqueue.transport.dbal.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.dbal.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.dbal.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new DbalTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.dbal.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(DbalDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.dbal.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/dbal/composer.json b/pkg/dbal/composer.json index 4e1929ba3..9499d394d 100644 --- a/pkg/dbal/composer.json +++ b/pkg/dbal/composer.json @@ -6,18 +6,17 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1", - "doctrine/dbal": "~2.5" + "php": "^8.1", + "queue-interop/queue-interop": "^0.8", + "doctrine/dbal": "^2.12|^3.1", + "doctrine/persistence": "^2.0|^3.0", + "ramsey/uuid": "^3.5|^4" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.5@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -32,13 +31,10 @@ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/dbal/examples/consume.php b/pkg/dbal/examples/consume.php index 0fc0eae93..f63cf8a77 100644 --- a/pkg/dbal/examples/consume.php +++ b/pkg/dbal/examples/consume.php @@ -12,19 +12,15 @@ if ($autoload) { require_once $autoload; } else { - throw new \LogicException('Composer autoload was not found'); + throw new LogicException('Composer autoload was not found'); } use Enqueue\Dbal\DbalConnectionFactory; $config = [ 'connection' => [ - 'dbname' => getenv('DOCTRINE_DB_NAME'), - 'user' => getenv('DOCTRINE_USER'), - 'password' => getenv('DOCTRINE_PASSWORD'), - 'host' => getenv('DOCTRINE_HOST'), - 'port' => getenv('DOCTRINE_PORT'), - 'driver' => getenv('DOCTRINE_DRIVER'), + 'url' => getenv('DOCTRINE_DSN'), + 'driver' => 'pdo_mysql', ], ]; @@ -39,7 +35,7 @@ while (true) { if ($m = $consumer->receive(1000)) { $consumer->acknowledge($m); - echo 'Received message: '.$m->getBody().PHP_EOL; + echo 'Received message: '.$m->getBody().\PHP_EOL; } } diff --git a/pkg/dbal/examples/produce.php b/pkg/dbal/examples/produce.php index 31befb24a..9f282bd0b 100644 --- a/pkg/dbal/examples/produce.php +++ b/pkg/dbal/examples/produce.php @@ -12,19 +12,15 @@ if ($autoload) { require_once $autoload; } else { - throw new \LogicException('Composer autoload was not found'); + throw new LogicException('Composer autoload was not found'); } use Enqueue\Dbal\DbalConnectionFactory; $config = [ 'connection' => [ - 'dbname' => getenv('DOCTRINE_DB_NAME'), - 'user' => getenv('DOCTRINE_USER'), - 'password' => getenv('DOCTRINE_PASSWORD'), - 'host' => getenv('DOCTRINE_HOST'), - 'port' => getenv('DOCTRINE_PORT'), - 'driver' => getenv('DOCTRINE_DRIVER'), + 'url' => getenv('DOCTRINE_DSN'), + 'driver' => 'pdo_mysql', ], ]; @@ -38,7 +34,7 @@ while (true) { $context->createProducer()->send($destination, $message); - echo 'Sent message: '.$message->getBody().PHP_EOL; + echo 'Sent message: '.$message->getBody().\PHP_EOL; sleep(1); } diff --git a/pkg/dbal/phpunit.xml.dist b/pkg/dbal/phpunit.xml.dist index 451d24a00..55a8d1f29 100644 --- a/pkg/dbal/phpunit.xml.dist +++ b/pkg/dbal/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/dsn/.gitattributes b/pkg/dsn/.gitattributes new file mode 100644 index 000000000..bdf2dcb14 --- /dev/null +++ b/pkg/dsn/.gitattributes @@ -0,0 +1,5 @@ +/Tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +phpunit.xml.dist export-ignore diff --git a/pkg/dsn/.github/workflows/ci.yml b/pkg/dsn/.github/workflows/ci.yml new file mode 100644 index 000000000..71bcbbd61 --- /dev/null +++ b/pkg/dsn/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit diff --git a/pkg/dsn/.gitignore b/pkg/dsn/.gitignore new file mode 100644 index 000000000..a770439e5 --- /dev/null +++ b/pkg/dsn/.gitignore @@ -0,0 +1,6 @@ +*~ +/composer.lock +/composer.phar +/phpunit.xml +/vendor/ +/.idea/ diff --git a/pkg/dsn/Dsn.php b/pkg/dsn/Dsn.php new file mode 100644 index 000000000..f46d7c056 --- /dev/null +++ b/pkg/dsn/Dsn.php @@ -0,0 +1,354 @@ +scheme = $scheme; + $this->schemeProtocol = $schemeProtocol; + $this->schemeExtensions = $schemeExtensions; + $this->user = $user; + $this->password = $password; + $this->host = $host; + $this->port = $port; + $this->path = $path; + $this->queryString = $queryString; + $this->queryBag = new QueryBag($query); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getSchemeProtocol(): string + { + return $this->schemeProtocol; + } + + public function getSchemeExtensions(): array + { + return $this->schemeExtensions; + } + + public function hasSchemeExtension(string $extension): bool + { + return in_array($extension, $this->schemeExtensions, true); + } + + public function getUser(): ?string + { + return $this->user; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getHost(): ?string + { + return $this->host; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getQueryString(): ?string + { + return $this->queryString; + } + + public function getQueryBag(): QueryBag + { + return $this->queryBag; + } + + public function getQuery(): array + { + return $this->queryBag->toArray(); + } + + public function getString(string $name, ?string $default = null): ?string + { + return $this->queryBag->getString($name, $default); + } + + public function getDecimal(string $name, ?int $default = null): ?int + { + return $this->queryBag->getDecimal($name, $default); + } + + public function getOctal(string $name, ?int $default = null): ?int + { + return $this->queryBag->getOctal($name, $default); + } + + public function getFloat(string $name, ?float $default = null): ?float + { + return $this->queryBag->getFloat($name, $default); + } + + public function getBool(string $name, ?bool $default = null): ?bool + { + return $this->queryBag->getBool($name, $default); + } + + public function getArray(string $name, array $default = []): QueryBag + { + return $this->queryBag->getArray($name, $default); + } + + public function toArray() + { + return [ + 'scheme' => $this->scheme, + 'schemeProtocol' => $this->schemeProtocol, + 'schemeExtensions' => $this->schemeExtensions, + 'user' => $this->user, + 'password' => $this->password, + 'host' => $this->host, + 'port' => $this->port, + 'path' => $this->path, + 'queryString' => $this->queryString, + 'query' => $this->queryBag->toArray(), + ]; + } + + public static function parseFirst(string $dsn): ?self + { + return self::parse($dsn)[0]; + } + + /** + * @return Dsn[] + */ + public static function parse(string $dsn): array + { + if (!str_contains($dsn, ':')) { + throw new \LogicException('The DSN is invalid. It does not have scheme separator ":".'); + } + + list($scheme, $dsnWithoutScheme) = explode(':', $dsn, 2); + + $scheme = strtolower($scheme); + if (false == preg_match('/^[a-z\d+-.]*$/', $scheme)) { + throw new \LogicException('The DSN is invalid. Scheme contains illegal symbols.'); + } + + $schemeParts = explode('+', $scheme); + $schemeProtocol = $schemeParts[0]; + + unset($schemeParts[0]); + $schemeExtensions = array_values($schemeParts); + + $user = parse_url(http://github.com/$dsn,%20/PHP_URL_USER) ?: null; + if (is_string($user)) { + $user = rawurldecode($user); + } + + $password = parse_url(http://github.com/$dsn,%20/PHP_URL_PASS) ?: null; + if (is_string($password)) { + $password = rawurldecode($password); + } + + $path = parse_url(http://github.com/$dsn,%20/PHP_URL_PATH) ?: null; + if ($path) { + $path = rawurldecode($path); + } + + $query = []; + $queryString = parse_url(http://github.com/$dsn,%20/PHP_URL_QUERY) ?: null; + if (is_string($queryString)) { + $query = self::httpParseQuery($queryString, '&', \PHP_QUERY_RFC3986); + } + $hostsPorts = ''; + if (str_starts_with($dsnWithoutScheme, '//')) { + $dsnWithoutScheme = substr($dsnWithoutScheme, 2); + $dsnWithoutUserPassword = explode('@', $dsnWithoutScheme, 2); + $dsnWithoutUserPassword = 2 === count($dsnWithoutUserPassword) ? + $dsnWithoutUserPassword[1] : + $dsnWithoutUserPassword[0] + ; + + list($hostsPorts) = explode('#', $dsnWithoutUserPassword, 2); + list($hostsPorts) = explode('?', $hostsPorts, 2); + list($hostsPorts) = explode('/', $hostsPorts, 2); + } + + if (empty($hostsPorts)) { + return [ + new self( + $scheme, + $schemeProtocol, + $schemeExtensions, + null, + null, + null, + null, + $path, + $queryString, + $query + ), + ]; + } + + $dsns = []; + $hostParts = explode(',', $hostsPorts); + foreach ($hostParts as $key => $hostPart) { + unset($hostParts[$key]); + + $parts = explode(':', $hostPart, 2); + $host = $parts[0]; + + $port = null; + if (isset($parts[1])) { + $port = (int) $parts[1]; + } + + $dsns[] = new self( + $scheme, + $schemeProtocol, + $schemeExtensions, + $user, + $password, + $host, + $port, + $path, + $queryString, + $query + ); + } + + return $dsns; + } + + /** + * based on http://php.net/manual/en/function.parse-str.php#119484 with some slight modifications. + */ + private static function httpParseQuery(string $queryString, string $argSeparator = '&', int $decType = \PHP_QUERY_RFC1738): array + { + $result = []; + $parts = explode($argSeparator, $queryString); + + foreach ($parts as $part) { + list($paramName, $paramValue) = explode('=', $part, 2); + + switch ($decType) { + case \PHP_QUERY_RFC3986: + $paramName = rawurldecode($paramName); + $paramValue = rawurldecode($paramValue); + break; + case \PHP_QUERY_RFC1738: + default: + $paramName = urldecode($paramName); + $paramValue = urldecode($paramValue); + break; + } + + if (preg_match_all('/\[([^\]]*)\]/m', $paramName, $matches)) { + $paramName = substr($paramName, 0, strpos($paramName, '[')); + $keys = array_merge([$paramName], $matches[1]); + } else { + $keys = [$paramName]; + } + + $target = &$result; + + foreach ($keys as $index) { + if ('' === $index) { + if (is_array($target)) { + $intKeys = array_filter(array_keys($target), 'is_int'); + $index = count($intKeys) ? max($intKeys) + 1 : 0; + } else { + $target = [$target]; + $index = 1; + } + } elseif (isset($target[$index]) && !is_array($target[$index])) { + $target[$index] = [$target[$index]]; + } + + $target = &$target[$index]; + } + + if (is_array($target)) { + $target[] = $paramValue; + } else { + $target = $paramValue; + } + } + + return $result; + } +} diff --git a/pkg/dsn/InvalidQueryParameterTypeException.php b/pkg/dsn/InvalidQueryParameterTypeException.php new file mode 100644 index 000000000..e419fa6a2 --- /dev/null +++ b/pkg/dsn/InvalidQueryParameterTypeException.php @@ -0,0 +1,11 @@ +query = $query; + } + + public function toArray(): array + { + return $this->query; + } + + public function getString(string $name, ?string $default = null): ?string + { + return array_key_exists($name, $this->query) ? $this->query[$name] : $default; + } + + public function getDecimal(string $name, ?int $default = null): ?int + { + $value = $this->getString($name); + if (null === $value) { + return $default; + } + + if (false == preg_match('/^[\+\-]?[0-9]*$/', $value)) { + throw InvalidQueryParameterTypeException::create($name, 'decimal'); + } + + return (int) $value; + } + + public function getOctal(string $name, ?int $default = null): ?int + { + $value = $this->getString($name); + if (null === $value) { + return $default; + } + + if (false == preg_match('/^0[\+\-]?[0-7]*$/', $value)) { + throw InvalidQueryParameterTypeException::create($name, 'octal'); + } + + return intval($value, 8); + } + + public function getFloat(string $name, ?float $default = null): ?float + { + $value = $this->getString($name); + if (null === $value) { + return $default; + } + + if (false == is_numeric($value)) { + throw InvalidQueryParameterTypeException::create($name, 'float'); + } + + return (float) $value; + } + + public function getBool(string $name, ?bool $default = null): ?bool + { + $value = $this->getString($name); + if (null === $value) { + return $default; + } + + if (in_array($value, ['', '0', 'false'], true)) { + return false; + } + + if (in_array($value, ['1', 'true'], true)) { + return true; + } + + throw InvalidQueryParameterTypeException::create($name, 'bool'); + } + + public function getArray(string $name, array $default = []): self + { + if (false == array_key_exists($name, $this->query)) { + return new self($default); + } + + $value = $this->query[$name]; + + if (is_array($value)) { + return new self($value); + } + + throw InvalidQueryParameterTypeException::create($name, 'array'); + } +} diff --git a/pkg/dsn/README.md b/pkg/dsn/README.md new file mode 100644 index 000000000..ca0ce7da9 --- /dev/null +++ b/pkg/dsn/README.md @@ -0,0 +1,29 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + +# Enqueue. Parse DSN class + +## Resources + +* [Site](https://enqueue.forma-pro.com/) +* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/dsn.md) +* [Questions](https://gitter.im/php-enqueue/Lobby) +* [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) + +## Developed by Forma-Pro + +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. + +If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com + +## License + +It is released under the [MIT License](LICENSE). diff --git a/pkg/dsn/Tests/DsnTest.php b/pkg/dsn/Tests/DsnTest.php new file mode 100644 index 000000000..8bf4137ed --- /dev/null +++ b/pkg/dsn/Tests/DsnTest.php @@ -0,0 +1,479 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage('The DSN is invalid. It does not have scheme separator ":".'); + Dsn::parseFirst('foobar'); + } + + public function testThrowsIfSchemeContainsIllegalSymbols() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The DSN is invalid. Scheme contains illegal symbols.'); + Dsn::parseFirst('foo_&%&^bar://localhost'); + } + + /** + * @dataProvider provideSchemes + */ + public function testShouldParseSchemeCorrectly(string $dsn, string $expectedScheme, string $expectedSchemeProtocol, array $expectedSchemeExtensions) + { + $dsn = Dsn::parseFirst($dsn); + + $this->assertSame($expectedScheme, $dsn->getScheme()); + $this->assertSame($expectedSchemeProtocol, $dsn->getSchemeProtocol()); + $this->assertSame($expectedSchemeExtensions, $dsn->getSchemeExtensions()); + } + + public function testShouldParseUser() + { + $dsn = Dsn::parseFirst('amqp+ext://theUser:thePass@theHost:1267/thePath'); + + $this->assertSame('theUser', $dsn->getUser()); + } + + public function testShouldParsePassword() + { + $dsn = Dsn::parseFirst('amqp+ext://theUser:thePass@theHost:1267/thePath'); + + $this->assertSame('thePass', $dsn->getPassword()); + } + + public function testShouldParseHost() + { + $dsn = Dsn::parseFirst('amqp+ext://theUser:thePass@theHost:1267/thePath'); + + $this->assertSame('theHost', $dsn->getHost()); + } + + public function testShouldParsePort() + { + $dsn = Dsn::parseFirst('amqp+ext://theUser:thePass@theHost:1267/thePath'); + + $this->assertSame(1267, $dsn->getPort()); + } + + public function testShouldParsePath() + { + $dsn = Dsn::parseFirst('amqp+ext://theUser:thePass@theHost:1267/thePath'); + + $this->assertSame('/thePath', $dsn->getPath()); + } + + public function testShouldUrlDecodedPath() + { + $dsn = Dsn::parseFirst('amqp+ext://theUser:thePass@theHost:1267/%2f'); + + $this->assertSame('//', $dsn->getPath()); + } + + public function testShouldParseQuery() + { + $dsn = Dsn::parseFirst('amqp+ext://theUser:thePass@theHost:1267/thePath?foo=fooVal&bar=bar%2fVal'); + + $this->assertSame('foo=fooVal&bar=bar%2fVal', $dsn->getQueryString()); + $this->assertSame(['foo' => 'fooVal', 'bar' => 'bar/Val'], $dsn->getQuery()); + } + + public function testShouldParseQueryShouldPreservePlusSymbol() + { + $dsn = Dsn::parseFirst('amqp+ext://theUser:thePass@theHost:1267/thePath?foo=fooVal&bar=bar+Val'); + + $this->assertSame('foo=fooVal&bar=bar+Val', $dsn->getQueryString()); + $this->assertSame(['foo' => 'fooVal', 'bar' => 'bar+Val'], $dsn->getQuery()); + } + + /** + * @dataProvider provideIntQueryParameters + */ + public function testShouldParseQueryParameterAsInt(string $parameter, int $expected) + { + $dsn = Dsn::parseFirst('foo:?aName='.$parameter); + + $this->assertSame($expected, $dsn->getDecimal('aName')); + } + + /** + * @dataProvider provideOctalQueryParameters + */ + public function testShouldParseQueryParameterAsOctalInt(string $parameter, int $expected) + { + $dsn = Dsn::parseFirst('foo:?aName='.$parameter); + + $this->assertSame($expected, $dsn->getOctal('aName')); + } + + public function testShouldReturnDefaultIntIfNotSet() + { + $dsn = Dsn::parseFirst('foo:'); + + $this->assertNull($dsn->getDecimal('aName')); + $this->assertSame(123, $dsn->getDecimal('aName', 123)); + } + + public function testThrowIfQueryParameterNotDecimal() + { + $dsn = Dsn::parseFirst('foo:?aName=notInt'); + + $this->expectException(InvalidQueryParameterTypeException::class); + $this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "decimal"'); + $dsn->getDecimal('aName'); + } + + public function testThrowIfQueryParameterNotOctalButString() + { + $dsn = Dsn::parseFirst('foo:?aName=notInt'); + + $this->expectException(InvalidQueryParameterTypeException::class); + $this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "octal"'); + $dsn->getOctal('aName'); + } + + public function testThrowIfQueryParameterNotOctalButDecimal() + { + $dsn = Dsn::parseFirst('foo:?aName=123'); + + $this->expectException(InvalidQueryParameterTypeException::class); + $this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "octal"'); + $dsn->getOctal('aName'); + } + + public function testThrowIfQueryParameterInvalidOctal() + { + $dsn = Dsn::parseFirst('foo:?aName=0128'); + + $this->expectException(InvalidQueryParameterTypeException::class); + $this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "octal"'); + $dsn->getOctal('aName'); + } + + public function testThrowIfQueryParameterInvalidArray() + { + $dsn = Dsn::parseFirst('foo:?aName=foo'); + + $this->expectException(InvalidQueryParameterTypeException::class); + $this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "array"'); + $dsn->getArray('aName'); + } + + /** + * @dataProvider provideFloatQueryParameters + */ + public function testShouldParseQueryParameterAsFloat(string $parameter, float $expected) + { + $dsn = Dsn::parseFirst('foo:?aName='.$parameter); + + $this->assertSame($expected, $dsn->getFloat('aName')); + } + + public function testShouldParseDSNWithoutAuthorityPart() + { + $dsn = Dsn::parseFirst('foo:///foo'); + + $this->assertNull($dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertNull($dsn->getHost()); + $this->assertNull($dsn->getPort()); + } + + public function testShouldReturnDefaultFloatIfNotSet() + { + $dsn = Dsn::parseFirst('foo:'); + + $this->assertNull($dsn->getFloat('aName')); + $this->assertSame(123., $dsn->getFloat('aName', 123.)); + } + + public function testThrowIfQueryParameterNotFloat() + { + $dsn = Dsn::parseFirst('foo:?aName=notFloat'); + + $this->expectException(InvalidQueryParameterTypeException::class); + $this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "float"'); + $dsn->getFloat('aName'); + } + + /** + * @dataProvider provideBooleanQueryParameters + */ + public function testShouldParseQueryParameterAsBoolean(string $parameter, bool $expected) + { + $dsn = Dsn::parseFirst('foo:?aName='.$parameter); + + $this->assertSame($expected, $dsn->getBool('aName')); + } + + /** + * @dataProvider provideArrayQueryParameters + */ + public function testShouldParseQueryParameterAsArray(string $query, array $expected) + { + $dsn = Dsn::parseFirst('foo:?'.$query); + + $this->assertSame($expected, $dsn->getArray('aName')->toArray()); + } + + public function testShouldReturnDefaultBoolIfNotSet() + { + $dsn = Dsn::parseFirst('foo:'); + + $this->assertNull($dsn->getBool('aName')); + $this->assertTrue($dsn->getBool('aName', true)); + } + + public function testThrowIfQueryParameterNotBool() + { + $dsn = Dsn::parseFirst('foo:?aName=notBool'); + + $this->expectException(InvalidQueryParameterTypeException::class); + $this->expectExceptionMessage('The query parameter "aName" has invalid type. It must be "bool"'); + $dsn->getBool('aName'); + } + + public function testShouldParseMultipleDsnsWithUsernameAndPassword() + { + $dsns = Dsn::parse('foo://user:pass@foo,bar'); + + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(2, $dsns); + + $this->assertSame('user', $dsns[0]->getUser()); + $this->assertSame('pass', $dsns[0]->getPassword()); + $this->assertSame('foo', $dsns[0]->getHost()); + + $this->assertSame('user', $dsns[1]->getUser()); + $this->assertSame('pass', $dsns[1]->getPassword()); + $this->assertSame('bar', $dsns[1]->getHost()); + } + + public function testShouldParseMultipleDsnsWithPorts() + { + $dsns = Dsn::parse('foo://foo:123,bar:567'); + + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(2, $dsns); + + $this->assertSame('foo', $dsns[0]->getHost()); + $this->assertSame(123, $dsns[0]->getPort()); + + $this->assertSame('bar', $dsns[1]->getHost()); + $this->assertSame(567, $dsns[1]->getPort()); + } + + public function testShouldParseMultipleDsnsWhenFirstHasPort() + { + $dsns = Dsn::parse('foo://foo:123,bar'); + + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(2, $dsns); + + $this->assertSame('foo', $dsns[0]->getHost()); + $this->assertSame(123, $dsns[0]->getPort()); + + $this->assertSame('bar', $dsns[1]->getHost()); + $this->assertNull($dsns[1]->getPort()); + } + + public function testShouldParseMultipleDsnsWhenLastHasPort() + { + $dsns = Dsn::parse('foo://foo,bar:567'); + + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(2, $dsns); + + $this->assertSame('foo', $dsns[0]->getHost()); + $this->assertNull($dsns[0]->getPort()); + + $this->assertSame('bar', $dsns[1]->getHost()); + $this->assertSame(567, $dsns[1]->getPort()); + } + + public function testShouldParseMultipleDsnsWithPath() + { + $dsns = Dsn::parse('foo://foo:123,bar:567/foo/bar'); + + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(2, $dsns); + + $this->assertSame('foo', $dsns[0]->getHost()); + $this->assertSame(123, $dsns[0]->getPort()); + + $this->assertSame('bar', $dsns[1]->getHost()); + $this->assertSame(567, $dsns[1]->getPort()); + } + + public function testShouldParseMultipleDsnsWithQuery() + { + $dsns = Dsn::parse('foo://foo:123,bar:567?foo=val'); + + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(2, $dsns); + + $this->assertSame('foo', $dsns[0]->getHost()); + $this->assertSame(123, $dsns[0]->getPort()); + + $this->assertSame('bar', $dsns[1]->getHost()); + $this->assertSame(567, $dsns[1]->getPort()); + } + + public function testShouldParseMultipleDsnsWithQueryAndPath() + { + $dsns = Dsn::parse('foo://foo:123,bar:567/foo?foo=val'); + + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(2, $dsns); + + $this->assertSame('foo', $dsns[0]->getHost()); + $this->assertSame(123, $dsns[0]->getPort()); + + $this->assertSame('bar', $dsns[1]->getHost()); + $this->assertSame(567, $dsns[1]->getPort()); + } + + public function testShouldParseMultipleDsnsIfOnlyColonProvided() + { + $dsns = Dsn::parse(':'); + + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(1, $dsns); + + $this->assertNull($dsns[0]->getHost()); + $this->assertNull($dsns[0]->getPort()); + } + + public function testShouldParseMultipleDsnsWithOnlyScheme() + { + $dsns = Dsn::parse('foo:'); + + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(1, $dsns); + + $this->assertSame('foo', $dsns[0]->getScheme()); + $this->assertNull($dsns[0]->getHost()); + $this->assertNull($dsns[0]->getPort()); + } + + public function testShouldParseExpectedNumberOfMultipleDsns() + { + $dsns = Dsn::parse('foo://foo'); + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(1, $dsns); + + $dsns = Dsn::parse('foo://foo,bar'); + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(2, $dsns); + + $dsns = Dsn::parse('foo://foo,bar,baz'); + $this->assertContainsOnly(Dsn::class, $dsns); + $this->assertCount(3, $dsns); + } + + public function testShouldParseDsnWithOnlyUser() + { + $dsn = Dsn::parseFirst('foo://user@host'); + + $this->assertSame('user', $dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertSame('foo', $dsn->getScheme()); + $this->assertSame('host', $dsn->getHost()); + } + + public function testShouldUrlEncodeUser() + { + $dsn = Dsn::parseFirst('foo://us%3Aer@host'); + + $this->assertSame('us:er', $dsn->getUser()); + $this->assertNull($dsn->getPassword()); + $this->assertSame('foo', $dsn->getScheme()); + $this->assertSame('host', $dsn->getHost()); + } + + public function testShouldUrlEncodePassword() + { + $dsn = Dsn::parseFirst('foo://user:pass%3Aword@host'); + + $this->assertSame('user', $dsn->getUser()); + $this->assertSame('pass:word', $dsn->getPassword()); + $this->assertSame('foo', $dsn->getScheme()); + $this->assertSame('host', $dsn->getHost()); + } + + public static function provideSchemes() + { + yield [':', '', '', []]; + + yield ['FOO:', 'foo', 'foo', []]; + + yield ['foo:', 'foo', 'foo', []]; + + yield ['foo+bar:', 'foo+bar', 'foo', ['bar']]; + + yield ['foo+bar+baz:', 'foo+bar+baz', 'foo', ['bar', 'baz']]; + + yield ['foo:?bar=barVal', 'foo', 'foo', []]; + + yield ['amqp+ext://guest:guest@localhost:5672/%2f', 'amqp+ext', 'amqp', ['ext']]; + + yield ['amqp+ext+rabbitmq:', 'amqp+ext+rabbitmq', 'amqp', ['ext', 'rabbitmq']]; + } + + public static function provideIntQueryParameters() + { + yield ['123', 123]; + + yield ['+123', 123]; + + yield ['-123', -123]; + + yield ['010', 10]; + } + + public static function provideOctalQueryParameters() + { + yield ['010', 8]; + } + + public static function provideFloatQueryParameters() + { + yield ['123', 123.]; + + yield ['+123', 123.]; + + yield ['-123', -123.]; + + yield ['0', 0.]; + } + + public static function provideBooleanQueryParameters() + { + yield ['', false]; + + yield ['1', true]; + + yield ['0', false]; + + yield ['true', true]; + + yield ['false', false]; + } + + public static function provideArrayQueryParameters() + { + yield ['aName[0]=val', ['val']]; + + yield ['aName[key]=val', ['key' => 'val']]; + + yield ['aName[0]=fooVal&aName[1]=barVal', ['fooVal', 'barVal']]; + + yield ['aName[foo]=fooVal&aName[bar]=barVal', ['foo' => 'fooVal', 'bar' => 'barVal']]; + } +} diff --git a/pkg/dsn/composer.json b/pkg/dsn/composer.json new file mode 100644 index 000000000..dbd39aed7 --- /dev/null +++ b/pkg/dsn/composer.json @@ -0,0 +1,33 @@ +{ + "name": "enqueue/dsn", + "type": "library", + "description": "Parse DSN", + "keywords": ["dsn", "parse"], + "homepage": "https://enqueue.forma-pro.com/", + "license": "MIT", + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "support": { + "email": "opensource@forma-pro.com", + "issues": "https://github.com/php-enqueue/enqueue-dev/issues", + "forum": "https://gitter.im/php-enqueue/Lobby", + "source": "https://github.com/php-enqueue/enqueue-dev", + "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" + }, + "autoload": { + "psr-4": { "Enqueue\\Dsn\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "0.10.x-dev" + } + } +} diff --git a/pkg/dsn/phpunit.xml.dist b/pkg/dsn/phpunit.xml.dist new file mode 100644 index 000000000..43b743e2a --- /dev/null +++ b/pkg/dsn/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + + ./Tests + + + + + + . + + ./vendor + ./Tests + + + + diff --git a/pkg/enqueue-bundle/.github/workflows/ci.yml b/pkg/enqueue-bundle/.github/workflows/ci.yml new file mode 100644 index 000000000..4c397bef1 --- /dev/null +++ b/pkg/enqueue-bundle/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: mongodb + + - run: php Tests/fix_composer_json.php + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/enqueue-bundle/.travis.yml b/pkg/enqueue-bundle/.travis.yml deleted file mode 100644 index 880a93f5e..000000000 --- a/pkg/enqueue-bundle/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - - -php: - - '7.1' - -services: - - mongodb - -before_install: - - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - -cache: - directories: - - $HOME/.composer/cache - -install: - - php Tests/fix_composer_json.php - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/enqueue-bundle/Consumption/Extension/DoctrineClearIdentityMapExtension.php b/pkg/enqueue-bundle/Consumption/Extension/DoctrineClearIdentityMapExtension.php index 9750fb399..d02b9a274 100644 --- a/pkg/enqueue-bundle/Consumption/Extension/DoctrineClearIdentityMapExtension.php +++ b/pkg/enqueue-bundle/Consumption/Extension/DoctrineClearIdentityMapExtension.php @@ -2,32 +2,23 @@ namespace Enqueue\Bundle\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; -use Symfony\Bridge\Doctrine\RegistryInterface; +use Doctrine\Persistence\ManagerRegistry; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; -class DoctrineClearIdentityMapExtension implements ExtensionInterface +class DoctrineClearIdentityMapExtension implements MessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** - * @var RegistryInterface + * @var ManagerRegistry */ protected $registry; - /** - * @param RegistryInterface $registry - */ - public function __construct(RegistryInterface $registry) + public function __construct(ManagerRegistry $registry) { $this->registry = $registry; } - /** - * {@inheritdoc} - */ - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { foreach ($this->registry->getManagers() as $name => $manager) { $context->getLogger()->debug(sprintf( diff --git a/pkg/enqueue-bundle/Consumption/Extension/DoctrineClosedEntityManagerExtension.php b/pkg/enqueue-bundle/Consumption/Extension/DoctrineClosedEntityManagerExtension.php new file mode 100644 index 000000000..e5ad0c6cf --- /dev/null +++ b/pkg/enqueue-bundle/Consumption/Extension/DoctrineClosedEntityManagerExtension.php @@ -0,0 +1,65 @@ +registry = $registry; + } + + public function onPreConsume(PreConsume $context): void + { + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } + } + + public function onPostConsume(PostConsume $context): void + { + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } + } + + public function onPostMessageReceived(PostMessageReceived $context): void + { + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } + } + + private function shouldBeStopped(LoggerInterface $logger): bool + { + foreach ($this->registry->getManagers() as $name => $manager) { + if (!$manager instanceof EntityManagerInterface || $manager->isOpen()) { + continue; + } + + $logger->debug(sprintf( + '[DoctrineClosedEntityManagerExtension] Interrupt execution as entity manager "%s" has been closed', + $name + )); + + return true; + } + + return false; + } +} diff --git a/pkg/enqueue-bundle/Consumption/Extension/DoctrinePingConnectionExtension.php b/pkg/enqueue-bundle/Consumption/Extension/DoctrinePingConnectionExtension.php index bbbed1239..7fd9527db 100644 --- a/pkg/enqueue-bundle/Consumption/Extension/DoctrinePingConnectionExtension.php +++ b/pkg/enqueue-bundle/Consumption/Extension/DoctrinePingConnectionExtension.php @@ -3,32 +3,23 @@ namespace Enqueue\Bundle\Consumption\Extension; use Doctrine\DBAL\Connection; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; -use Symfony\Bridge\Doctrine\RegistryInterface; +use Doctrine\Persistence\ManagerRegistry; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; -class DoctrinePingConnectionExtension implements ExtensionInterface +class DoctrinePingConnectionExtension implements MessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** - * @var RegistryInterface + * @var ManagerRegistry */ protected $registry; - /** - * @param RegistryInterface $registry - */ - public function __construct(RegistryInterface $registry) + public function __construct(ManagerRegistry $registry) { $this->registry = $registry; } - /** - * {@inheritdoc} - */ - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { /** @var Connection $connection */ foreach ($this->registry->getConnections() as $connection) { @@ -36,7 +27,7 @@ public function onPreReceived(Context $context) continue; } - if ($connection->ping()) { + if ($this->ping($connection)) { continue; } @@ -52,4 +43,23 @@ public function onPreReceived(Context $context) ); } } + + private function ping(Connection $connection): bool + { + set_error_handler(static function (int $severity, string $message, string $file, int $line): bool { + throw new \ErrorException($message, $severity, $severity, $file, $line); + }); + + try { + $dummySelectSQL = $connection->getDatabasePlatform()->getDummySelectSQL(); + + $connection->executeQuery($dummySelectSQL); + + return true; + } catch (\Throwable $exception) { + return false; + } finally { + restore_error_handler(); + } + } } diff --git a/pkg/enqueue-bundle/Consumption/Extension/ResetServicesExtension.php b/pkg/enqueue-bundle/Consumption/Extension/ResetServicesExtension.php new file mode 100644 index 000000000..0bf642197 --- /dev/null +++ b/pkg/enqueue-bundle/Consumption/Extension/ResetServicesExtension.php @@ -0,0 +1,27 @@ +resetter = $resetter; + } + + public function onPostMessageReceived(PostMessageReceived $context): void + { + $context->getLogger()->debug('[ResetServicesExtension] Resetting services.'); + + $this->resetter->reset(); + } +} diff --git a/pkg/enqueue-bundle/DependencyInjection/Compiler/AddTopicMetaPass.php b/pkg/enqueue-bundle/DependencyInjection/Compiler/AddTopicMetaPass.php deleted file mode 100644 index 45becabef..000000000 --- a/pkg/enqueue-bundle/DependencyInjection/Compiler/AddTopicMetaPass.php +++ /dev/null @@ -1,66 +0,0 @@ -topicsMeta = []; - } - - /** - * @param string $topicName - * @param string $topicDescription - * @param array $topicSubscribers - * - * @return $this - */ - public function add($topicName, $topicDescription = '', array $topicSubscribers = []) - { - $this->topicsMeta[$topicName] = []; - - if ($topicDescription) { - $this->topicsMeta[$topicName]['description'] = $topicDescription; - } - - if ($topicSubscribers) { - $this->topicsMeta[$topicName]['processors'] = $topicSubscribers; - } - - return $this; - } - - /** - * {@inheritdoc} - */ - public function process(ContainerBuilder $container) - { - $metaRegistryId = TopicMetaRegistry::class; - - if (false == $container->hasDefinition($metaRegistryId)) { - return; - } - - $metaRegistry = $container->getDefinition($metaRegistryId); - - $metaRegistry->replaceArgument(0, array_merge_recursive($metaRegistry->getArgument(0), $this->topicsMeta)); - } - - /** - * @return static - */ - public static function create() - { - return new static(); - } -} diff --git a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildClientExtensionsPass.php b/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildClientExtensionsPass.php deleted file mode 100644 index 5f83e83e2..000000000 --- a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildClientExtensionsPass.php +++ /dev/null @@ -1,40 +0,0 @@ -hasDefinition('enqueue.client.extensions')) { - return; - } - - $tags = $container->findTaggedServiceIds('enqueue.client.extension'); - - $groupByPriority = []; - foreach ($tags as $serviceId => $tagAttributes) { - foreach ($tagAttributes as $tagAttribute) { - $priority = isset($tagAttribute['priority']) ? (int) $tagAttribute['priority'] : 0; - - $groupByPriority[$priority][] = new Reference($serviceId); - } - } - - krsort($groupByPriority, SORT_NUMERIC); - - $flatExtensions = []; - foreach ($groupByPriority as $extension) { - $flatExtensions = array_merge($flatExtensions, $extension); - } - - $container->getDefinition('enqueue.client.extensions')->replaceArgument(0, $flatExtensions); - } -} diff --git a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildClientRoutingPass.php b/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildClientRoutingPass.php deleted file mode 100644 index 3511a3a96..000000000 --- a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildClientRoutingPass.php +++ /dev/null @@ -1,47 +0,0 @@ -hasDefinition($routerId)) { - return; - } - - $events = []; - $commands = []; - foreach ($container->findTaggedServiceIds($processorTagName) as $serviceId => $tagAttributes) { - $subscriptions = $this->extractSubscriptions($container, $serviceId, $tagAttributes); - - foreach ($subscriptions as $subscription) { - if (Config::COMMAND_TOPIC === $subscription['topicName']) { - $commands[$subscription['processorName']] = $subscription['queueName']; - } else { - $events[$subscription['topicName']][] = [ - $subscription['processorName'], - $subscription['queueName'], - ]; - } - } - } - - $router = $container->getDefinition($routerId); - $router->replaceArgument(1, $events); - $router->replaceArgument(2, $commands); - } -} diff --git a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildConsumptionExtensionsPass.php b/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildConsumptionExtensionsPass.php deleted file mode 100644 index 20f2a3817..000000000 --- a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildConsumptionExtensionsPass.php +++ /dev/null @@ -1,36 +0,0 @@ -findTaggedServiceIds('enqueue.consumption.extension'); - - $groupByPriority = []; - foreach ($tags as $serviceId => $tagAttributes) { - foreach ($tagAttributes as $tagAttribute) { - $priority = isset($tagAttribute['priority']) ? (int) $tagAttribute['priority'] : 0; - - $groupByPriority[$priority][] = new Reference($serviceId); - } - } - - krsort($groupByPriority, SORT_NUMERIC); - - $flatExtensions = []; - foreach ($groupByPriority as $extension) { - $flatExtensions = array_merge($flatExtensions, $extension); - } - - $container->getDefinition('enqueue.consumption.extensions')->replaceArgument(0, $flatExtensions); - } -} diff --git a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildExclusiveCommandsExtensionPass.php b/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildExclusiveCommandsExtensionPass.php deleted file mode 100644 index 3b0d906bd..000000000 --- a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildExclusiveCommandsExtensionPass.php +++ /dev/null @@ -1,49 +0,0 @@ -hasDefinition($extensionId)) { - return; - } - - $queueMetaRegistry = $container->getDefinition($extensionId); - - $queueNameToProcessorNameMap = []; - foreach ($container->findTaggedServiceIds($processorTagName) as $serviceId => $tagAttributes) { - $subscriptions = $this->extractSubscriptions($container, $serviceId, $tagAttributes); - - foreach ($subscriptions as $subscription) { - if (Config::COMMAND_TOPIC != $subscription['topicName']) { - continue; - } - - if (false == isset($subscription['exclusive']) || false === $subscription['exclusive']) { - continue; - } - - if (false == $subscription['queueNameHardcoded']) { - throw new \LogicException('The exclusive command could be used only with queueNameHardcoded attribute set to true.'); - } - - $queueNameToProcessorNameMap[$subscription['queueName']] = $subscription['processorName']; - } - } - - $queueMetaRegistry->replaceArgument(0, $queueNameToProcessorNameMap); - } -} diff --git a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildProcessorRegistryPass.php b/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildProcessorRegistryPass.php deleted file mode 100644 index ffff5b4ed..000000000 --- a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildProcessorRegistryPass.php +++ /dev/null @@ -1,36 +0,0 @@ -hasDefinition($processorRegistryId)) { - return; - } - - $processorIds = []; - foreach ($container->findTaggedServiceIds($processorTagName) as $serviceId => $tagAttributes) { - $subscriptions = $this->extractSubscriptions($container, $serviceId, $tagAttributes); - - foreach ($subscriptions as $subscription) { - $processorIds[$subscription['processorName']] = $serviceId; - } - } - - $processorRegistryDef = $container->getDefinition($processorRegistryId); - $processorRegistryDef->setArguments([$processorIds]); - } -} diff --git a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildQueueMetaRegistryPass.php b/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildQueueMetaRegistryPass.php deleted file mode 100644 index 5e82f324d..000000000 --- a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildQueueMetaRegistryPass.php +++ /dev/null @@ -1,41 +0,0 @@ -hasDefinition($queueMetaRegistryId)) { - return; - } - - $queueMetaRegistry = $container->getDefinition($queueMetaRegistryId); - - $configs = []; - foreach ($container->findTaggedServiceIds($processorTagName) as $serviceId => $tagAttributes) { - $subscriptions = $this->extractSubscriptions($container, $serviceId, $tagAttributes); - - foreach ($subscriptions as $subscription) { - $configs[$subscription['queueName']]['processors'][] = $subscription['processorName']; - - if ($subscription['queueNameHardcoded']) { - $configs[$subscription['queueName']]['transportName'] = $subscription['queueName']; - } - } - } - - $queueMetaRegistry->replaceArgument(1, $configs); - } -} diff --git a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildTopicMetaSubscribersPass.php b/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildTopicMetaSubscribersPass.php deleted file mode 100644 index 2f5195fa6..000000000 --- a/pkg/enqueue-bundle/DependencyInjection/Compiler/BuildTopicMetaSubscribersPass.php +++ /dev/null @@ -1,40 +0,0 @@ -hasDefinition(TopicMetaRegistry::class)) { - return; - } - - $topicsSubscribers = []; - foreach ($container->findTaggedServiceIds($processorTagName) as $serviceId => $tagAttributes) { - $subscriptions = $this->extractSubscriptions($container, $serviceId, $tagAttributes); - - foreach ($subscriptions as $subscription) { - $topicsSubscribers[$subscription['topicName']][] = $subscription['processorName']; - } - } - - $addTopicMetaPass = AddTopicMetaPass::create(); - foreach ($topicsSubscribers as $topicName => $subscribers) { - $addTopicMetaPass->add($topicName, '', $subscribers); - } - - $addTopicMetaPass->process($container); - } -} diff --git a/pkg/enqueue-bundle/DependencyInjection/Compiler/ExtractProcessorTagSubscriptionsTrait.php b/pkg/enqueue-bundle/DependencyInjection/Compiler/ExtractProcessorTagSubscriptionsTrait.php deleted file mode 100644 index 58fa57622..000000000 --- a/pkg/enqueue-bundle/DependencyInjection/Compiler/ExtractProcessorTagSubscriptionsTrait.php +++ /dev/null @@ -1,146 +0,0 @@ -getParameter(trim($value, '%')); - } catch (ParameterNotFoundException $e) { - return $value; - } - }; - - $processorClass = $container->getDefinition($processorServiceId)->getClass(); - if (false == class_exists($processorClass)) { - throw new \LogicException(sprintf('The class "%s" could not be found.', $processorClass)); - } - - $defaultQueueName = $resolve($container->getParameter('enqueue.client.default_queue_name')); - $subscriptionPrototype = [ - 'topicName' => null, - 'queueName' => null, - 'queueNameHardcoded' => false, - 'processorName' => null, - 'exclusive' => false, - ]; - - $data = []; - if (is_subclass_of($processorClass, CommandSubscriberInterface::class)) { - /** @var CommandSubscriberInterface $processorClass */ - $params = $processorClass::getSubscribedCommand(); - if (is_string($params)) { - if (empty($params)) { - throw new \LogicException('The processor name (it is also the command name) must not be empty.'); - } - - $data[] = [ - 'topicName' => Config::COMMAND_TOPIC, - 'queueName' => $defaultQueueName, - 'queueNameHardcoded' => false, - 'processorName' => $params, - ]; - } elseif (is_array($params)) { - $params = array_replace($subscriptionPrototype, $params); - if (false == $processorName = $resolve($params['processorName'])) { - throw new \LogicException('The processor name (it is also the command name) must not be empty.'); - } - - $data[] = [ - 'topicName' => Config::COMMAND_TOPIC, - 'queueName' => $resolve($params['queueName']) ?: $defaultQueueName, - 'queueNameHardcoded' => $resolve($params['queueNameHardcoded']), - 'processorName' => $processorName, - 'exclusive' => array_key_exists('exclusive', $params) ? $params['exclusive'] : false, - ]; - } else { - throw new \LogicException(sprintf( - 'Command subscriber configuration is invalid. "%s"', - json_encode($processorClass::getSubscribedCommand()) - )); - } - } - - if (is_subclass_of($processorClass, TopicSubscriberInterface::class)) { - /** @var TopicSubscriberInterface $processorClass */ - $topics = $processorClass::getSubscribedTopics(); - if (!is_array($topics)) { - throw new \LogicException(sprintf( - 'Topic subscriber configuration is invalid for "%s::getSubscribedTopics()": expected array, got %s.', - $processorClass, - gettype($topics) - )); - } - - foreach ($topics as $topicName => $params) { - if (is_string($params)) { - $data[] = [ - 'topicName' => $params, - 'queueName' => $defaultQueueName, - 'queueNameHardcoded' => false, - 'processorName' => $processorServiceId, - ]; - } elseif (is_array($params)) { - $params = array_replace($subscriptionPrototype, $params); - - $data[] = [ - 'topicName' => $topicName, - 'queueName' => $resolve($params['queueName']) ?: $defaultQueueName, - 'queueNameHardcoded' => $resolve($params['queueNameHardcoded']), - 'processorName' => $resolve($params['processorName']) ?: $processorServiceId, - ]; - } else { - throw new \LogicException(sprintf( - 'Topic subscriber configuration is invalid for "%s::getSubscribedTopics()". "%s"', - $processorClass, - json_encode($processorClass::getSubscribedTopics()) - )); - } - } - } - - if (false == ( - is_subclass_of($processorClass, CommandSubscriberInterface::class) || - is_subclass_of($processorClass, TopicSubscriberInterface::class) - )) { - foreach ($tagAttributes as $tagAttribute) { - $tagAttribute = array_replace($subscriptionPrototype, $tagAttribute); - - if (false == $tagAttribute['topicName']) { - throw new \LogicException(sprintf('Topic name is not set on message processor tag but it is required. Service %s', $processorServiceId)); - } - - $data[] = [ - 'topicName' => $resolve($tagAttribute['topicName']), - 'queueName' => $resolve($tagAttribute['queueName']) ?: $defaultQueueName, - 'queueNameHardcoded' => $resolve($tagAttribute['queueNameHardcoded']), - 'processorName' => $resolve($tagAttribute['processorName']) ?: $processorServiceId, - 'exclusive' => Config::COMMAND_TOPIC == $resolve($tagAttribute['topicName']) && - array_key_exists('exclusive', $tagAttribute) ? $tagAttribute['exclusive'] : false, - ]; - } - } - - return $data; - } -} diff --git a/pkg/enqueue-bundle/DependencyInjection/Configuration.php b/pkg/enqueue-bundle/DependencyInjection/Configuration.php index 1db513e64..733849d35 100644 --- a/pkg/enqueue-bundle/DependencyInjection/Configuration.php +++ b/pkg/enqueue-bundle/DependencyInjection/Configuration.php @@ -2,84 +2,118 @@ namespace Enqueue\Bundle\DependencyInjection; -use Enqueue\Client\Config; -use Enqueue\Client\RouterProcessor; -use Enqueue\Symfony\TransportFactoryInterface; +use Enqueue\AsyncCommand\RunCommandProcessor; +use Enqueue\AsyncEventDispatcher\DependencyInjection\AsyncEventDispatcherExtension; +use Enqueue\JobQueue\Job; +use Enqueue\Monitoring\Symfony\DependencyInjection\MonitoringFactory; +use Enqueue\Symfony\Client\DependencyInjection\ClientFactory; +use Enqueue\Symfony\DependencyInjection\TransportFactory; +use Enqueue\Symfony\MissingComponentFactory; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -class Configuration implements ConfigurationInterface +final class Configuration implements ConfigurationInterface { private $debug; - /** - * @var TransportFactoryInterface[] - */ - private $factories; - - /** - * @param TransportFactoryInterface[] $factories - * @param bool $debug - */ - public function __construct(array $factories, $debug) + public function __construct(bool $debug) { - $this->factories = $factories; $this->debug = $debug; } - /** - * {@inheritdoc} - */ - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { - $tb = new TreeBuilder(); - $rootNode = $tb->root('enqueue'); + if (method_exists(TreeBuilder::class, 'getRootNode')) { + $tb = new TreeBuilder('enqueue'); + $rootNode = $tb->getRootNode(); + } else { + $tb = new TreeBuilder(); + $rootNode = $tb->root('enqueue'); + } - $transportChildren = $rootNode->children() - ->arrayNode('transport')->isRequired()->children(); + $rootNode + ->requiresAtLeastOneElement() + ->useAttributeAsKey('key') + ->arrayPrototype() + ->children() + ->append(TransportFactory::getConfiguration()) + ->append(TransportFactory::getQueueConsumerConfiguration()) + ->append(ClientFactory::getConfiguration($this->debug)) + ->append($this->getMonitoringConfiguration()) + ->append($this->getAsyncCommandsConfiguration()) + ->append($this->getJobConfiguration()) + ->append($this->getAsyncEventsConfiguration()) + ->arrayNode('extensions')->addDefaultsIfNotSet()->children() + ->booleanNode('doctrine_ping_connection_extension')->defaultFalse()->end() + ->booleanNode('doctrine_clear_identity_map_extension')->defaultFalse()->end() + ->booleanNode('doctrine_odm_clear_identity_map_extension')->defaultFalse()->end() + ->booleanNode('doctrine_closed_entity_manager_extension')->defaultFalse()->end() + ->booleanNode('reset_services_extension')->defaultFalse()->end() + ->booleanNode('signal_extension')->defaultValue(function_exists('pcntl_signal_dispatch'))->end() + ->booleanNode('reply_extension')->defaultTrue()->end() + ->end()->end() + ->end() + ->end() + ; + + return $tb; + } - foreach ($this->factories as $factory) { - $factory->addConfiguration( - $transportChildren->arrayNode($factory->getName()) - ); + private function getMonitoringConfiguration(): ArrayNodeDefinition + { + if (false === class_exists(MonitoringFactory::class)) { + return MissingComponentFactory::getConfiguration('monitoring', ['enqueue/monitoring']); } - $rootNode->children() - ->arrayNode('client')->children() - ->booleanNode('traceable_producer')->defaultValue($this->debug)->end() - ->scalarNode('prefix')->defaultValue('enqueue')->end() - ->scalarNode('app_name')->defaultValue('app')->end() - ->scalarNode('router_topic')->defaultValue(Config::DEFAULT_PROCESSOR_QUEUE_NAME)->cannotBeEmpty()->end() - ->scalarNode('router_queue')->defaultValue(Config::DEFAULT_PROCESSOR_QUEUE_NAME)->cannotBeEmpty()->end() - ->scalarNode('router_processor')->defaultValue(RouterProcessor::class)->end() - ->scalarNode('default_processor_queue')->defaultValue(Config::DEFAULT_PROCESSOR_QUEUE_NAME)->cannotBeEmpty()->end() - ->integerNode('redelivered_delay_time')->min(0)->defaultValue(0)->end() - ->end()->end() - ->arrayNode('consumption')->addDefaultsIfNotSet()->children() - ->integerNode('idle_timeout') - ->min(0) - ->defaultValue(0) - ->info('the time in milliseconds queue consumer waits if no message received') - ->end() - ->integerNode('receive_timeout') - ->min(0) - ->defaultValue(100) - ->info('the time in milliseconds queue consumer waits for a message (100 ms by default)') + return MonitoringFactory::getConfiguration(); + } + + private function getAsyncCommandsConfiguration(): ArrayNodeDefinition + { + if (false === class_exists(RunCommandProcessor::class)) { + return MissingComponentFactory::getConfiguration('async_commands', ['enqueue/async-command']); + } + + return (new ArrayNodeDefinition('async_commands')) + ->children() + ->booleanNode('enabled')->defaultFalse()->end() + ->integerNode('timeout')->min(0)->defaultValue(60)->end() + ->scalarNode('command_name')->defaultNull()->end() + ->scalarNode('queue_name')->defaultNull()->end() + ->end() + ->addDefaultsIfNotSet() + ->canBeEnabled() + ; + } + + private function getJobConfiguration(): ArrayNodeDefinition + { + if (false === class_exists(Job::class)) { + return MissingComponentFactory::getConfiguration('job', ['enqueue/job-queue']); + } + + return (new ArrayNodeDefinition('job')) + ->children() + ->booleanNode('default_mapping') + ->defaultTrue() + ->info('Adds bundle\'s default Job entity mapping to application\'s entity manager') ->end() - ->end()->end() - ->booleanNode('job')->defaultFalse()->end() - ->arrayNode('async_events') - ->addDefaultsIfNotSet() - ->canBeEnabled() ->end() - ->arrayNode('extensions')->addDefaultsIfNotSet()->children() - ->booleanNode('doctrine_ping_connection_extension')->defaultFalse()->end() - ->booleanNode('doctrine_clear_identity_map_extension')->defaultFalse()->end() - ->booleanNode('signal_extension')->defaultValue(function_exists('pcntl_signal_dispatch'))->end() - ->booleanNode('reply_extension')->defaultTrue()->end() - ->end()->end() + ->addDefaultsIfNotSet() + ->canBeEnabled() ; + } - return $tb; + private function getAsyncEventsConfiguration(): ArrayNodeDefinition + { + if (false == class_exists(AsyncEventDispatcherExtension::class)) { + return MissingComponentFactory::getConfiguration('async_events', ['enqueue/async-event-dispatcher']); + } + + return (new ArrayNodeDefinition('async_events')) + ->addDefaultsIfNotSet() + ->canBeEnabled() + ; } } diff --git a/pkg/enqueue-bundle/DependencyInjection/EnqueueExtension.php b/pkg/enqueue-bundle/DependencyInjection/EnqueueExtension.php index 15cd06dd0..96fca6fde 100644 --- a/pkg/enqueue-bundle/DependencyInjection/EnqueueExtension.php +++ b/pkg/enqueue-bundle/DependencyInjection/EnqueueExtension.php @@ -2,191 +2,159 @@ namespace Enqueue\Bundle\DependencyInjection; +use Enqueue\AsyncCommand\DependencyInjection\AsyncCommandExtension; use Enqueue\AsyncEventDispatcher\DependencyInjection\AsyncEventDispatcherExtension; +use Enqueue\Bundle\Consumption\Extension\DoctrineClearIdentityMapExtension; +use Enqueue\Bundle\Consumption\Extension\DoctrineClosedEntityManagerExtension; +use Enqueue\Bundle\Consumption\Extension\DoctrinePingConnectionExtension; +use Enqueue\Bundle\Consumption\Extension\ResetServicesExtension; +use Enqueue\Bundle\Profiler\MessageQueueCollector; use Enqueue\Client\CommandSubscriberInterface; -use Enqueue\Client\Producer; use Enqueue\Client\TopicSubscriberInterface; -use Enqueue\Client\TraceableProducer; -use Enqueue\Consumption\QueueConsumer; +use Enqueue\Consumption\Extension\ReplyExtension; +use Enqueue\Consumption\Extension\SignalExtension; use Enqueue\JobQueue\Job; -use Enqueue\Null\Symfony\NullTransportFactory; -use Enqueue\Symfony\DefaultTransportFactory; -use Enqueue\Symfony\DriverFactoryInterface; -use Enqueue\Symfony\TransportFactoryInterface; +use Enqueue\Monitoring\Symfony\DependencyInjection\MonitoringFactory; +use Enqueue\Symfony\Client\DependencyInjection\ClientFactory; +use Enqueue\Symfony\DependencyInjection\TransportFactory; +use Enqueue\Symfony\DiUtils; +use Interop\Queue\Context; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; -class EnqueueExtension extends Extension implements PrependExtensionInterface +final class EnqueueExtension extends Extension implements PrependExtensionInterface { - /** - * @var TransportFactoryInterface[] - */ - private $factories; - - public function __construct() - { - $this->factories = []; - - $this->addTransportFactory(new DefaultTransportFactory()); - $this->addTransportFactory(new NullTransportFactory()); - } - - /** - * @param TransportFactoryInterface $transportFactory - */ - public function addTransportFactory(TransportFactoryInterface $transportFactory) - { - $name = $transportFactory->getName(); - - if (array_key_exists($name, $this->factories)) { - throw new \LogicException(sprintf('Transport factory with such name already added. Name %s', $name)); - } - - $this->setTransportFactory($transportFactory); - } - - /** - * @param TransportFactoryInterface $transportFactory - */ - public function setTransportFactory(TransportFactoryInterface $transportFactory) - { - $name = $transportFactory->getName(); - - if (empty($name)) { - throw new \LogicException('Transport factory name cannot be empty'); - } - - $this->factories[$name] = $transportFactory; - } - - /** - * {@inheritdoc} - */ - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { $config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs); $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); - $this->setupAutowiringForProcessors($container); + // find default configuration + $defaultName = null; + foreach ($config as $name => $modules) { + // set first as default + if (null === $defaultName) { + $defaultName = $name; + } - foreach ($config['transport'] as $name => $transportConfig) { - $this->factories[$name]->createConnectionFactory($container, $transportConfig); - $this->factories[$name]->createContext($container, $transportConfig); + // or with name 'default' + if (DiUtils::DEFAULT_CONFIG === $name) { + $defaultName = $name; + } } - if (isset($config['client'])) { - $loader->load('client.yml'); - $loader->load('extensions/flush_spool_producer_extension.yml'); - $loader->load('extensions/exclusive_command_extension.yml'); - - foreach ($config['transport'] as $name => $transportConfig) { - if ($this->factories[$name] instanceof DriverFactoryInterface) { - $this->factories[$name]->createDriver($container, $transportConfig); - } + $transportNames = []; + $clientNames = []; + foreach ($config as $name => $modules) { + // transport & consumption + $transportNames[] = $name; + + $transportFactory = (new TransportFactory($name, $defaultName === $name)); + $transportFactory->buildConnectionFactory($container, $modules['transport']); + $transportFactory->buildContext($container, []); + $transportFactory->buildQueueConsumer($container, $modules['consumption']); + $transportFactory->buildRpcClient($container, []); + + // client + if (isset($modules['client'])) { + $clientNames[] = $name; + + $clientConfig = $modules['client']; + // todo + $clientConfig['transport'] = $modules['transport']; + $clientConfig['consumption'] = $modules['consumption']; + + $clientFactory = new ClientFactory($name, $defaultName === $name); + $clientFactory->build($container, $clientConfig); + $clientFactory->createDriver($container, $modules['transport']); + $clientFactory->createFlushSpoolProducerListener($container); } - if (isset($config['transport']['default']['alias']) && !isset($config['transport'][$config['transport']['default']['alias']])) { - throw new \LogicException(sprintf('Transport is not enabled: %s', $config['transport']['default']['alias'])); + // monitoring + if (isset($modules['monitoring'])) { + $monitoringFactory = new MonitoringFactory($name); + $monitoringFactory->buildStorage($container, $modules['monitoring']); + $monitoringFactory->buildConsumerExtension($container, $modules['monitoring']); + + if (isset($modules['client'])) { + $monitoringFactory->buildClientExtension($container, $modules['monitoring']); + } } - $configDef = $container->getDefinition('enqueue.client.config'); - $configDef->setArguments([ - $config['client']['prefix'], - $config['client']['app_name'], - $config['client']['router_topic'], - $config['client']['router_queue'], - $config['client']['default_processor_queue'], - $config['client']['router_processor'], - isset($config['transport']['default']['alias']) ? $config['transport'][$config['transport']['default']['alias']] : [], - ]); + // job-queue + if (false == empty($modules['job']['enabled'])) { + if (false === isset($modules['client'])) { + throw new \LogicException('Client is required for job-queue.'); + } - $container->setParameter('enqueue.client.router_queue_name', $config['client']['router_queue']); - $container->setParameter('enqueue.client.default_queue_name', $config['client']['default_processor_queue']); + if ($name !== $defaultName) { + throw new \LogicException('Job-queue supports only default configuration.'); + } - if ($config['client']['traceable_producer']) { - $container->register(TraceableProducer::class, TraceableProducer::class) - ->setDecoratedService(Producer::class) - ->setPublic(true) - ->addArgument(new Reference(sprintf('%s.inner', TraceableProducer::class))) - ; + $loader->load('job.yml'); } - if ($config['client']['redelivered_delay_time']) { - $loader->load('extensions/delay_redelivered_message_extension.yml'); + // async events + if (false == empty($modules['async_events']['enabled'])) { + if ($name !== $defaultName) { + throw new \LogicException('Async events supports only default configuration.'); + } - $container->getDefinition('enqueue.client.delay_redelivered_message_extension') - ->replaceArgument(1, $config['client']['redelivered_delay_time']) - ; + $extension = new AsyncEventDispatcherExtension(); + $extension->load([[ + 'context_service' => Context::class, + ]], $container); } } - // configure queue consumer - $container->getDefinition(QueueConsumer::class) - ->replaceArgument(2, $config['consumption']['idle_timeout']) - ->replaceArgument(3, $config['consumption']['receive_timeout']) - ; - - if ($container->hasDefinition('enqueue.client.queue_consumer')) { - $container->getDefinition('enqueue.client.queue_consumer') - ->replaceArgument(2, $config['consumption']['idle_timeout']) - ->replaceArgument(3, $config['consumption']['receive_timeout']) - ; - } - - if ($config['job']) { - if (!class_exists(Job::class)) { - throw new \LogicException('Seems "enqueue/job-queue" is not installed. Please fix this issue.'); - } - - $loader->load('job.yml'); + $defaultClient = null; + if (in_array($defaultName, $clientNames, true)) { + $defaultClient = $defaultName; } - if ($config['async_events']['enabled']) { - $extension = new AsyncEventDispatcherExtension(); - $extension->load([[ - 'context_service' => 'enqueue.transport.default.context', - ]], $container); - } + $container->setParameter('enqueue.transports', $transportNames); + $container->setParameter('enqueue.clients', $clientNames); - if ($config['extensions']['doctrine_ping_connection_extension']) { - $loader->load('extensions/doctrine_ping_connection_extension.yml'); - } + $container->setParameter('enqueue.default_transport', $defaultName); - if ($config['extensions']['doctrine_clear_identity_map_extension']) { - $loader->load('extensions/doctrine_clear_identity_map_extension.yml'); + if ($defaultClient) { + $container->setParameter('enqueue.default_client', $defaultClient); } - if ($config['extensions']['signal_extension']) { - $loader->load('extensions/signal_extension.yml'); + if ($defaultClient) { + $this->setupAutowiringForDefaultClientsProcessors($container, $defaultClient); } - if ($config['extensions']['reply_extension']) { - $loader->load('extensions/reply_extension.yml'); - } + $this->loadMessageQueueCollector($config, $container); + $this->loadAsyncCommands($config, $container); + + // extensions + $this->loadDoctrinePingConnectionExtension($config, $container); + $this->loadDoctrineClearIdentityMapExtension($config, $container); + $this->loadDoctrineOdmClearIdentityMapExtension($config, $container); + $this->loadDoctrineClosedEntityManagerExtension($config, $container); + $this->loadResetServicesExtension($config, $container); + $this->loadSignalExtension($config, $container); + $this->loadReplyExtension($config, $container); } - /** - * {@inheritdoc} - * - * @return Configuration - */ - public function getConfiguration(array $config, ContainerBuilder $container) + public function getConfiguration(array $config, ContainerBuilder $container): Configuration { $rc = new \ReflectionClass(Configuration::class); $container->addResource(new FileResource($rc->getFileName())); - return new Configuration($this->factories, $container->getParameter('kernel.debug')); + return new Configuration($container->getParameter('kernel.debug')); } - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { $this->registerJobQueueDoctrineEntityMapping($container); } @@ -203,6 +171,18 @@ private function registerJobQueueDoctrineEntityMapping(ContainerBuilder $contain return; } + $config = $container->getExtensionConfig('enqueue'); + + if (!empty($config)) { + $processedConfig = $this->processConfiguration(new Configuration(false), $config); + + foreach ($processedConfig as $name => $modules) { + if (isset($modules['job']) && false === $modules['job']['default_mapping']) { + return; + } + } + } + foreach ($container->getExtensionConfig('doctrine') as $config) { // do not register mappings if dbal not configured. if (!empty($config['dbal'])) { @@ -225,18 +205,221 @@ private function registerJobQueueDoctrineEntityMapping(ContainerBuilder $contain } } - private function setupAutowiringForProcessors(ContainerBuilder $container) + private function setupAutowiringForDefaultClientsProcessors(ContainerBuilder $container, string $defaultClient) { - if (!method_exists($container, 'registerForAutoconfiguration')) { - return; - } - $container->registerForAutoconfiguration(TopicSubscriberInterface::class) ->setPublic(true) - ->addTag('enqueue.client.processor'); + ->addTag('enqueue.topic_subscriber', ['client' => $defaultClient]) + ; $container->registerForAutoconfiguration(CommandSubscriberInterface::class) ->setPublic(true) - ->addTag('enqueue.client.processor'); + ->addTag('enqueue.command_subscriber', ['client' => $defaultClient]) + ; + } + + private function loadDoctrinePingConnectionExtension(array $config, ContainerBuilder $container): void + { + $configNames = []; + foreach ($config as $name => $modules) { + if ($modules['extensions']['doctrine_ping_connection_extension']) { + $configNames[] = $name; + } + } + + if ([] === $configNames) { + return; + } + + $extension = $container->register('enqueue.consumption.doctrine_ping_connection_extension', DoctrinePingConnectionExtension::class) + ->addArgument(new Reference('doctrine')) + ; + + foreach ($configNames as $name) { + $extension->addTag('enqueue.consumption_extension', ['client' => $name]); + $extension->addTag('enqueue.transport.consumption_extension', ['transport' => $name]); + } + } + + private function loadDoctrineClearIdentityMapExtension(array $config, ContainerBuilder $container): void + { + $configNames = []; + foreach ($config as $name => $modules) { + if ($modules['extensions']['doctrine_clear_identity_map_extension']) { + $configNames[] = $name; + } + } + + if ([] === $configNames) { + return; + } + + $extension = $container->register('enqueue.consumption.doctrine_clear_identity_map_extension', DoctrineClearIdentityMapExtension::class) + ->addArgument(new Reference('doctrine')) + ; + + foreach ($configNames as $name) { + $extension->addTag('enqueue.consumption_extension', ['client' => $name]); + $extension->addTag('enqueue.transport.consumption_extension', ['transport' => $name]); + } + } + + private function loadDoctrineOdmClearIdentityMapExtension(array $config, ContainerBuilder $container): void + { + $configNames = []; + foreach ($config as $name => $modules) { + if ($modules['extensions']['doctrine_odm_clear_identity_map_extension']) { + $configNames[] = $name; + } + } + + if ([] === $configNames) { + return; + } + + $extension = $container->register('enqueue.consumption.doctrine_odm_clear_identity_map_extension', DoctrineClearIdentityMapExtension::class) + ->addArgument(new Reference('doctrine_mongodb')) + ; + + foreach ($configNames as $name) { + $extension->addTag('enqueue.consumption_extension', ['client' => $name]); + $extension->addTag('enqueue.transport.consumption_extension', ['transport' => $name]); + } + } + + private function loadDoctrineClosedEntityManagerExtension(array $config, ContainerBuilder $container) + { + $configNames = []; + foreach ($config as $name => $modules) { + if ($modules['extensions']['doctrine_closed_entity_manager_extension']) { + $configNames[] = $name; + } + } + + if ([] === $configNames) { + return; + } + + $extension = $container->register('enqueue.consumption.doctrine_closed_entity_manager_extension', DoctrineClosedEntityManagerExtension::class) + ->addArgument(new Reference('doctrine')); + + foreach ($configNames as $name) { + $extension->addTag('enqueue.consumption_extension', ['client' => $name]); + $extension->addTag('enqueue.transport.consumption_extension', ['transport' => $name]); + } + } + + private function loadResetServicesExtension(array $config, ContainerBuilder $container) + { + $configNames = []; + foreach ($config as $name => $modules) { + if ($modules['extensions']['reset_services_extension']) { + $configNames[] = $name; + } + } + + if ([] === $configNames) { + return; + } + + $extension = $container->register('enqueue.consumption.reset_services_extension', ResetServicesExtension::class) + ->addArgument(new Reference('services_resetter')); + + foreach ($configNames as $name) { + $extension->addTag('enqueue.consumption_extension', ['client' => $name]); + $extension->addTag('enqueue.transport.consumption_extension', ['transport' => $name]); + } + } + + private function loadSignalExtension(array $config, ContainerBuilder $container): void + { + $configNames = []; + foreach ($config as $name => $modules) { + if ($modules['extensions']['signal_extension']) { + $configNames[] = $name; + } + } + + if ([] === $configNames) { + return; + } + + $extension = $container->register('enqueue.consumption.signal_extension', SignalExtension::class); + + foreach ($configNames as $name) { + $extension->addTag('enqueue.consumption_extension', ['client' => $name]); + $extension->addTag('enqueue.transport.consumption_extension', ['transport' => $name]); + } + } + + private function loadReplyExtension(array $config, ContainerBuilder $container): void + { + $configNames = []; + foreach ($config as $name => $modules) { + if ($modules['extensions']['reply_extension']) { + $configNames[] = $name; + } + } + + if ([] === $configNames) { + return; + } + + $extension = $container->register('enqueue.consumption.reply_extension', ReplyExtension::class); + + foreach ($configNames as $name) { + $extension->addTag('enqueue.consumption_extension', ['client' => $name]); + $extension->addTag('enqueue.transport.consumption_extension', ['transport' => $name]); + } + } + + private function loadAsyncCommands(array $config, ContainerBuilder $container): void + { + $configs = []; + foreach ($config as $name => $modules) { + if (false === empty($modules['async_commands']['enabled'])) { + $configs[] = [ + 'name' => $name, + 'timeout' => $modules['async_commands']['timeout'], + 'command_name' => $modules['async_commands']['command_name'], + 'queue_name' => $modules['async_commands']['queue_name'], + ]; + } + } + + if (false == $configs) { + return; + } + + if (false == class_exists(AsyncCommandExtension::class)) { + throw new \LogicException('The "enqueue/async-command" package has to be installed.'); + } + + $extension = new AsyncCommandExtension(); + $extension->load(['clients' => $configs], $container); + } + + private function loadMessageQueueCollector(array $config, ContainerBuilder $container) + { + $configNames = []; + foreach ($config as $name => $modules) { + if (isset($modules['client'])) { + $configNames[] = $name; + } + } + + if (false == $configNames) { + return; + } + + $service = $container->register('enqueue.profiler.message_queue_collector', MessageQueueCollector::class); + $service->addTag('data_collector', [ + 'template' => '@Enqueue/Profiler/panel.html.twig', + 'id' => 'enqueue.message_queue', + ]); + + foreach ($configNames as $configName) { + $service->addMethodCall('addProducer', [$configName, DiUtils::create('client', $configName)->reference('producer')]); + } } } diff --git a/pkg/enqueue-bundle/EnqueueBundle.php b/pkg/enqueue-bundle/EnqueueBundle.php index 843cd498d..5010ba0ed 100644 --- a/pkg/enqueue-bundle/EnqueueBundle.php +++ b/pkg/enqueue-bundle/EnqueueBundle.php @@ -2,124 +2,45 @@ namespace Enqueue\Bundle; -use Enqueue\AmqpBunny\AmqpConnectionFactory as AmqpBunnyConnectionFactory; -use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnectionFactory; -use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnectionFactory; +use Enqueue\AsyncEventDispatcher\DependencyInjection\AsyncEventDispatcherExtension; use Enqueue\AsyncEventDispatcher\DependencyInjection\AsyncEventsPass; use Enqueue\AsyncEventDispatcher\DependencyInjection\AsyncTransformersPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildClientExtensionsPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildClientRoutingPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildConsumptionExtensionsPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildExclusiveCommandsExtensionPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildProcessorRegistryPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildQueueMetaRegistryPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildTopicMetaSubscribersPass; -use Enqueue\Bundle\DependencyInjection\EnqueueExtension; -use Enqueue\Dbal\DbalConnectionFactory; -use Enqueue\Dbal\Symfony\DbalTransportFactory; -use Enqueue\Fs\FsConnectionFactory; -use Enqueue\Fs\Symfony\FsTransportFactory; -use Enqueue\Gps\GpsConnectionFactory; -use Enqueue\Gps\Symfony\GpsTransportFactory; -use Enqueue\Mongodb\Symfony\MongodbTransportFactory; -use Enqueue\RdKafka\RdKafkaConnectionFactory; -use Enqueue\RdKafka\Symfony\RdKafkaTransportFactory; -use Enqueue\Redis\RedisConnectionFactory; -use Enqueue\Redis\Symfony\RedisTransportFactory; -use Enqueue\Sqs\SqsConnectionFactory; -use Enqueue\Sqs\Symfony\SqsTransportFactory; -use Enqueue\Stomp\StompConnectionFactory; -use Enqueue\Stomp\Symfony\RabbitMqStompTransportFactory; -use Enqueue\Stomp\Symfony\StompTransportFactory; -use Enqueue\Symfony\AmqpTransportFactory; -use Enqueue\Symfony\MissingTransportFactory; -use Enqueue\Symfony\RabbitMqAmqpTransportFactory; +use Enqueue\Doctrine\DoctrineSchemaCompilerPass; +use Enqueue\Symfony\Client\DependencyInjection\AnalyzeRouteCollectionPass; +use Enqueue\Symfony\Client\DependencyInjection\BuildClientExtensionsPass; +use Enqueue\Symfony\Client\DependencyInjection\BuildCommandSubscriberRoutesPass as BuildClientCommandSubscriberRoutesPass; +use Enqueue\Symfony\Client\DependencyInjection\BuildConsumptionExtensionsPass as BuildClientConsumptionExtensionsPass; +use Enqueue\Symfony\Client\DependencyInjection\BuildProcessorRegistryPass as BuildClientProcessorRegistryPass; +use Enqueue\Symfony\Client\DependencyInjection\BuildProcessorRoutesPass as BuildClientProcessorRoutesPass; +use Enqueue\Symfony\Client\DependencyInjection\BuildTopicSubscriberRoutesPass as BuildClientTopicSubscriberRoutesPass; +use Enqueue\Symfony\DependencyInjection\BuildConsumptionExtensionsPass; +use Enqueue\Symfony\DependencyInjection\BuildProcessorRegistryPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class EnqueueBundle extends Bundle { - /** - * {@inheritdoc} - */ - public function build(ContainerBuilder $container) + public function build(ContainerBuilder $container): void { + // transport passes $container->addCompilerPass(new BuildConsumptionExtensionsPass()); - $container->addCompilerPass(new BuildClientRoutingPass()); $container->addCompilerPass(new BuildProcessorRegistryPass()); - $container->addCompilerPass(new BuildTopicMetaSubscribersPass()); - $container->addCompilerPass(new BuildQueueMetaRegistryPass()); - $container->addCompilerPass(new BuildClientExtensionsPass()); - $container->addCompilerPass(new BuildExclusiveCommandsExtensionPass()); - - /** @var EnqueueExtension $extension */ - $extension = $container->getExtension('enqueue'); - - if (class_exists(StompConnectionFactory::class)) { - $extension->setTransportFactory(new StompTransportFactory('stomp')); - $extension->setTransportFactory(new RabbitMqStompTransportFactory('rabbitmq_stomp')); - } else { - $extension->setTransportFactory(new MissingTransportFactory('stomp', ['enqueue/stomp'])); - $extension->setTransportFactory(new MissingTransportFactory('rabbitmq_stomp', ['enqueue/stomp'])); - } - - if ( - class_exists(AmqpBunnyConnectionFactory::class) || - class_exists(AmqpExtConnectionFactory::class) || - class_exists(AmqpLibConnectionFactory::class) - ) { - $extension->setTransportFactory(new AmqpTransportFactory('amqp')); - $extension->setTransportFactory(new RabbitMqAmqpTransportFactory('rabbitmq_amqp')); - } else { - $amqpPackages = ['enqueue/amqp-ext', 'enqueue/amqp-bunny', 'enqueue/amqp-lib']; - $extension->setTransportFactory(new MissingTransportFactory('amqp', $amqpPackages)); - $extension->setTransportFactory(new MissingTransportFactory('rabbitmq_amqp', $amqpPackages)); - } - - if (class_exists(FsConnectionFactory::class)) { - $extension->setTransportFactory(new FsTransportFactory('fs')); - } else { - $extension->setTransportFactory(new MissingTransportFactory('fs', ['enqueue/fs'])); - } - - if (class_exists(RedisConnectionFactory::class)) { - $extension->setTransportFactory(new RedisTransportFactory('redis')); - } else { - $extension->setTransportFactory(new MissingTransportFactory('redis', ['enqueue/redis'])); - } - - if (class_exists(DbalConnectionFactory::class)) { - $extension->setTransportFactory(new DbalTransportFactory('dbal')); - } else { - $extension->setTransportFactory(new MissingTransportFactory('dbal', ['enqueue/dbal'])); - } - - if (class_exists(SqsConnectionFactory::class)) { - $extension->setTransportFactory(new SqsTransportFactory('sqs')); - } else { - $extension->setTransportFactory(new MissingTransportFactory('sqs', ['enqueue/sqs'])); - } - if (class_exists(GpsConnectionFactory::class)) { - $extension->setTransportFactory(new GpsTransportFactory('gps')); - } else { - $extension->setTransportFactory(new MissingTransportFactory('gps', ['enqueue/gps'])); - } - - if (class_exists(RdKafkaConnectionFactory::class)) { - $extension->setTransportFactory(new RdKafkaTransportFactory('rdkafka')); - } else { - $extension->setTransportFactory(new MissingTransportFactory('rdkafka', ['enqueue/rdkafka'])); - } + // client passes + $container->addCompilerPass(new BuildClientConsumptionExtensionsPass()); + $container->addCompilerPass(new BuildClientExtensionsPass()); + $container->addCompilerPass(new BuildClientTopicSubscriberRoutesPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100); + $container->addCompilerPass(new BuildClientCommandSubscriberRoutesPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100); + $container->addCompilerPass(new BuildClientProcessorRoutesPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100); + $container->addCompilerPass(new AnalyzeRouteCollectionPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 30); + $container->addCompilerPass(new BuildClientProcessorRegistryPass()); - if (class_exists(MongodbTransportFactory::class)) { - $extension->setTransportFactory(new MongodbTransportFactory('mongodb')); - } else { - $extension->setTransportFactory(new MissingTransportFactory('mongodb', ['enqueue/mongodb'])); + if (class_exists(AsyncEventDispatcherExtension::class)) { + $container->addCompilerPass(new AsyncEventsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100); + $container->addCompilerPass(new AsyncTransformersPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100); } - $container->addCompilerPass(new AsyncEventsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100); - $container->addCompilerPass(new AsyncTransformersPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 100); + $container->addCompilerPass(new DoctrineSchemaCompilerPass()); } } diff --git a/pkg/enqueue-bundle/Profiler/AbstractMessageQueueCollector.php b/pkg/enqueue-bundle/Profiler/AbstractMessageQueueCollector.php new file mode 100644 index 000000000..e2e5eee5a --- /dev/null +++ b/pkg/enqueue-bundle/Profiler/AbstractMessageQueueCollector.php @@ -0,0 +1,89 @@ +producers[$name] = $producer; + } + + public function getCount(): int + { + $count = 0; + foreach ($this->data as $name => $messages) { + $count += count($messages); + } + + return $count; + } + + /** + * @return array + */ + public function getSentMessages() + { + return $this->data; + } + + /** + * @param string $priority + * + * @return string + */ + public function prettyPrintPriority($priority) + { + $map = [ + MessagePriority::VERY_LOW => 'very low', + MessagePriority::LOW => 'low', + MessagePriority::NORMAL => 'normal', + MessagePriority::HIGH => 'high', + MessagePriority::VERY_HIGH => 'very high', + ]; + + return isset($map[$priority]) ? $map[$priority] : $priority; + } + + /** + * @return string + */ + public function ensureString($body) + { + return is_string($body) ? $body : JSON::encode($body); + } + + public function getName(): string + { + return 'enqueue.message_queue'; + } + + public function reset(): void + { + $this->data = []; + } + + protected function collectInternal(Request $request, Response $response): void + { + $this->data = []; + + foreach ($this->producers as $name => $producer) { + if ($producer instanceof TraceableProducer) { + $this->data[$name] = $producer->getTraces(); + } + } + } +} diff --git a/pkg/enqueue-bundle/Profiler/MessageQueueCollector.php b/pkg/enqueue-bundle/Profiler/MessageQueueCollector.php index 86bdd7456..3c484a7d1 100644 --- a/pkg/enqueue-bundle/Profiler/MessageQueueCollector.php +++ b/pkg/enqueue-bundle/Profiler/MessageQueueCollector.php @@ -2,92 +2,13 @@ namespace Enqueue\Bundle\Profiler; -use Enqueue\Client\MessagePriority; -use Enqueue\Client\ProducerInterface; -use Enqueue\Client\TraceableProducer; -use Enqueue\Util\JSON; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\DataCollector\DataCollector; -class MessageQueueCollector extends DataCollector +class MessageQueueCollector extends AbstractMessageQueueCollector { - /** - * @var ProducerInterface - */ - private $producer; - - /** - * @param ProducerInterface $producer - */ - public function __construct(ProducerInterface $producer) - { - $this->producer = $producer; - } - - /** - * {@inheritdoc} - */ - public function collect(Request $request, Response $response, \Exception $exception = null) - { - $this->data = [ - 'sent_messages' => [], - ]; - - if ($this->producer instanceof TraceableProducer) { - $this->data['sent_messages'] = $this->producer->getTraces(); - } - } - - /** - * @return array - */ - public function getSentMessages() - { - return $this->data['sent_messages']; - } - - /** - * @param string $priority - * - * @return string - */ - public function prettyPrintPriority($priority) - { - $map = [ - MessagePriority::VERY_LOW => 'very low', - MessagePriority::LOW => 'low', - MessagePriority::NORMAL => 'normal', - MessagePriority::HIGH => 'high', - MessagePriority::VERY_HIGH => 'very high', - ]; - - return isset($map[$priority]) ? $map[$priority] : $priority; - } - - /** - * @param mixed $body - * - * @return string - */ - public function ensureString($body) - { - return is_string($body) ? $body : JSON::encode($body); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'enqueue.message_queue'; - } - - /** - * {@inheritdoc} - */ - public function reset() + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { - $this->data = []; + $this->collectInternal($request, $response); } } diff --git a/pkg/enqueue-bundle/README.md b/pkg/enqueue-bundle/README.md index a76ff621d..2b8bbfe68 100644 --- a/pkg/enqueue-bundle/README.md +++ b/pkg/enqueue-bundle/README.md @@ -1,28 +1,37 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Message Queue Bundle [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/enqueue-bundle.png?branch=master)](https://travis-ci.org/php-enqueue/enqueue-bundle) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/enqueue-bundle/ci.yml?branch=master)](https://github.com/php-enqueue/enqueue-bundle/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/enqueue-bundle/d/total.png)](https://packagist.org/packages/enqueue/enqueue-bundle) [![Latest Stable Version](https://poser.pugx.org/enqueue/enqueue-bundle/version.png)](https://packagist.org/packages/enqueue/enqueue-bundle) - -Integrates message queue components to Symfony application. + +Integrates message queue components to Symfony application. ## Resources * [Site](https://enqueue.forma-pro.com/) * [Quick tour](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/bundle/quick_tour.md) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/enqueue-bundle/Resources/config/client.yml b/pkg/enqueue-bundle/Resources/config/client.yml deleted file mode 100644 index d5bacbb03..000000000 --- a/pkg/enqueue-bundle/Resources/config/client.yml +++ /dev/null @@ -1,215 +0,0 @@ -services: - enqueue.client.config: - class: 'Enqueue\Client\Config' - public: false - - Enqueue\Client\Producer: - class: 'Enqueue\Client\Producer' - public: true - arguments: - - '@enqueue.client.driver' - - '@enqueue.client.rpc_factory' - - '@enqueue.client.extensions' - - Enqueue\Client\ProducerInterface: - public: true - alias: 'Enqueue\Client\Producer' - - # Deprecated. To be removed in 0.10. - enqueue.client.producer: - public: true - alias: 'Enqueue\Client\Producer' - - # Deprecated. To be removed in 0.10. - enqueue.producer: - public: true - alias: 'enqueue.client.producer' - - # Deprecated. To be removed in 0.10. - enqueue.client.producer_v2: - public: true - alias: 'enqueue.client.producer' - - Enqueue\Client\SpoolProducer: - class: 'Enqueue\Client\SpoolProducer' - public: true - arguments: - - '@Enqueue\Client\Producer' - - # Deprecated. To be removed in 0.10. - enqueue.client.spool_producer: - public: true - alias: 'Enqueue\Client\SpoolProducer' - - # Deprecated. To be removed in 0.10. - enqueue.spool_producer: - public: true - alias: 'enqueue.client.spool_producer' - - enqueue.client.extensions: - class: 'Enqueue\Client\ChainExtension' - public: false - arguments: - - [] - - enqueue.client.rpc_factory: - class: 'Enqueue\Rpc\RpcFactory' - public: false - arguments: - - '@enqueue.transport.context' - - Enqueue\Client\RouterProcessor: - class: 'Enqueue\Client\RouterProcessor' - public: true - arguments: - - '@enqueue.client.driver' - - [] - - [] - tags: - - - name: 'enqueue.client.processor' - topicName: '__router__' - queueName: '%enqueue.client.router_queue_name%' - - # Deprecated. To be removed in 0.10. - enqueue.client.router_processor: - public: true - alias: 'Enqueue\Client\RouterProcessor' - - enqueue.client.processor_registry: - class: 'Enqueue\Symfony\Client\ContainerAwareProcessorRegistry' - public: false - calls: - - ['setContainer', ['@service_container']] - - Enqueue\Client\Meta\TopicMetaRegistry: - class: 'Enqueue\Client\Meta\TopicMetaRegistry' - public: true - arguments: [[]] - - # Deprecated. To be removed in 0.10. - enqueue.client.meta.topic_meta_registry: - public: true - alias: 'Enqueue\Client\Meta\TopicMetaRegistry' - - Enqueue\Client\Meta\QueueMetaRegistry: - class: 'Enqueue\Client\Meta\QueueMetaRegistry' - public: true - arguments: ['@enqueue.client.config', []] - - # Deprecated. To be removed in 0.10. - enqueue.client.meta.queue_meta_registry: - public: true - alias: 'Enqueue\Client\Meta\QueueMetaRegistry' - - enqueue.client.delegate_processor: - class: 'Enqueue\Client\DelegateProcessor' - public: false - arguments: - - '@enqueue.client.processor_registry' - - enqueue.client.extension.set_router_properties: - class: 'Enqueue\Client\ConsumptionExtension\SetRouterPropertiesExtension' - public: false - arguments: - - '@enqueue.client.driver' - tags: - - { name: 'enqueue.consumption.extension', priority: 100 } - - enqueue.client.queue_consumer: - class: 'Enqueue\Consumption\QueueConsumer' - public: false - arguments: - - '@enqueue.transport.context' - - '@enqueue.consumption.extensions' - - ~ - - ~ - - Enqueue\Symfony\Client\ConsumeMessagesCommand: - class: 'Enqueue\Symfony\Client\ConsumeMessagesCommand' - public: true - arguments: - - '@enqueue.client.queue_consumer' - - '@enqueue.client.delegate_processor' - - '@Enqueue\Client\Meta\QueueMetaRegistry' - - '@enqueue.client.driver' - tags: - - { name: 'console.command' } - - # Deprecated. To be removed in 0.10. - enqueue.client.consume_messages_command: - public: true - alias: 'Enqueue\Symfony\Client\ConsumeMessagesCommand' - - Enqueue\Symfony\Client\ProduceMessageCommand: - class: 'Enqueue\Symfony\Client\ProduceMessageCommand' - public: true - arguments: - - '@Enqueue\Client\Producer' - tags: - - { name: 'console.command' } - - # Deprecated. To be removed in 0.10. - enqueue.client.produce_message_command: - public: true - alias: 'Enqueue\Symfony\Client\ProduceMessageCommand' - - Enqueue\Symfony\Client\Meta\TopicsCommand: - class: 'Enqueue\Symfony\Client\Meta\TopicsCommand' - public: true - arguments: - - '@Enqueue\Client\Meta\TopicMetaRegistry' - tags: - - { name: 'console.command' } - - # Deprecated. To be removed in 0.10. - enqueue.client.meta.topics_command: - public: true - alias: 'Enqueue\Symfony\Client\Meta\TopicsCommand' - - Enqueue\Symfony\Client\Meta\QueuesCommand: - class: 'Enqueue\Symfony\Client\Meta\QueuesCommand' - public: true - arguments: - - '@Enqueue\Client\Meta\QueueMetaRegistry' - tags: - - { name: 'console.command' } - - # Deprecated. To be removed in 0.10. - enqueue.client.meta.queues_command: - public: true - alias: 'Enqueue\Symfony\Client\Meta\QueuesCommand' - - Enqueue\Symfony\Client\SetupBrokerCommand: - class: 'Enqueue\Symfony\Client\SetupBrokerCommand' - public: true - arguments: - - '@enqueue.client.driver' - tags: - - { name: 'console.command' } - - # Deprecated. To be removed in 0.10. - enqueue.client.setup_broker_command: - public: true - alias: 'Enqueue\Symfony\Client\SetupBrokerCommand' - - enqueue.profiler.message_queue_collector: - class: 'Enqueue\Bundle\Profiler\MessageQueueCollector' - public: false - arguments: - - '@Enqueue\Client\Producer' - tags: - - { name: 'data_collector', template: '@Enqueue/Profiler/panel.html.twig', id: 'enqueue.message_queue' } - - Enqueue\Symfony\Client\FlushSpoolProducerListener: - class: 'Enqueue\Symfony\Client\FlushSpoolProducerListener' - public: true - arguments: - - '@Enqueue\Client\SpoolProducer' - tags: - - { name: 'kernel.event_subscriber' } - - # Deprecated. To be removed in 0.10. - enqueue.flush_spool_producer_listener: - public: true - alias: 'Enqueue\Symfony\Client\FlushSpoolProducerListener' diff --git a/pkg/enqueue-bundle/Resources/config/extensions/delay_redelivered_message_extension.yml b/pkg/enqueue-bundle/Resources/config/extensions/delay_redelivered_message_extension.yml deleted file mode 100644 index 24fff7eb0..000000000 --- a/pkg/enqueue-bundle/Resources/config/extensions/delay_redelivered_message_extension.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - enqueue.client.delay_redelivered_message_extension: - class: 'Enqueue\Client\ConsumptionExtension\DelayRedeliveredMessageExtension' - public: false - arguments: - - '@enqueue.client.driver' - - ~ - tags: - - { name: 'enqueue.consumption.extension', priority: 10 } \ No newline at end of file diff --git a/pkg/enqueue-bundle/Resources/config/extensions/doctrine_clear_identity_map_extension.yml b/pkg/enqueue-bundle/Resources/config/extensions/doctrine_clear_identity_map_extension.yml deleted file mode 100644 index 932cb9ba1..000000000 --- a/pkg/enqueue-bundle/Resources/config/extensions/doctrine_clear_identity_map_extension.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - enqueue.consumption.doctrine_clear_identity_map_extension: - class: 'Enqueue\Bundle\Consumption\Extension\DoctrineClearIdentityMapExtension' - public: false - arguments: - - '@doctrine' - tags: - - { name: 'enqueue.consumption.extension' } diff --git a/pkg/enqueue-bundle/Resources/config/extensions/doctrine_ping_connection_extension.yml b/pkg/enqueue-bundle/Resources/config/extensions/doctrine_ping_connection_extension.yml deleted file mode 100644 index 7b3383a79..000000000 --- a/pkg/enqueue-bundle/Resources/config/extensions/doctrine_ping_connection_extension.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - enqueue.consumption.doctrine_ping_connection_extension: - class: 'Enqueue\Bundle\Consumption\Extension\DoctrinePingConnectionExtension' - public: false - arguments: - - '@doctrine' - tags: - - { name: 'enqueue.consumption.extension' } diff --git a/pkg/enqueue-bundle/Resources/config/extensions/exclusive_command_extension.yml b/pkg/enqueue-bundle/Resources/config/extensions/exclusive_command_extension.yml deleted file mode 100644 index c79b98029..000000000 --- a/pkg/enqueue-bundle/Resources/config/extensions/exclusive_command_extension.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - enqueue.client.exclusive_command_extension: - class: 'Enqueue\Client\ConsumptionExtension\ExclusiveCommandExtension' - public: false - arguments: - - [] - tags: - - { name: 'enqueue.consumption.extension', priority: 100 } - - { name: 'enqueue.client.extension', priority: 100 } diff --git a/pkg/enqueue-bundle/Resources/config/extensions/flush_spool_producer_extension.yml b/pkg/enqueue-bundle/Resources/config/extensions/flush_spool_producer_extension.yml deleted file mode 100644 index 80bd3d455..000000000 --- a/pkg/enqueue-bundle/Resources/config/extensions/flush_spool_producer_extension.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - enqueue.client.flush_spool_producer_extension: - class: 'Enqueue\Client\ConsumptionExtension\FlushSpoolProducerExtension' - public: false - arguments: - - '@Enqueue\Client\SpoolProducer' - tags: - - { name: 'enqueue.consumption.extension', priority: -100 } \ No newline at end of file diff --git a/pkg/enqueue-bundle/Resources/config/extensions/reply_extension.yml b/pkg/enqueue-bundle/Resources/config/extensions/reply_extension.yml deleted file mode 100644 index b52c46b8f..000000000 --- a/pkg/enqueue-bundle/Resources/config/extensions/reply_extension.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - enqueue.consumption.reply_extension: - class: 'Enqueue\Consumption\Extension\ReplyExtension' - public: false - tags: - - { name: 'enqueue.consumption.extension' } \ No newline at end of file diff --git a/pkg/enqueue-bundle/Resources/config/extensions/signal_extension.yml b/pkg/enqueue-bundle/Resources/config/extensions/signal_extension.yml deleted file mode 100644 index e7609eb06..000000000 --- a/pkg/enqueue-bundle/Resources/config/extensions/signal_extension.yml +++ /dev/null @@ -1,6 +0,0 @@ -services: - enqueue.consumption.signal_extension: - class: 'Enqueue\Consumption\Extension\SignalExtension' - public: false - tags: - - { name: 'enqueue.consumption.extension' } \ No newline at end of file diff --git a/pkg/enqueue-bundle/Resources/config/job.yml b/pkg/enqueue-bundle/Resources/config/job.yml index 159aecc61..0a368aebc 100644 --- a/pkg/enqueue-bundle/Resources/config/job.yml +++ b/pkg/enqueue-bundle/Resources/config/job.yml @@ -20,7 +20,7 @@ services: public: true arguments: - '@Enqueue\JobQueue\Doctrine\JobStorage' - - '@Enqueue\Client\Producer' + - '@Enqueue\Client\ProducerInterface' # Deprecated. To be removed in 0.10. enqueue.job.processor: @@ -55,10 +55,10 @@ services: arguments: - '@Enqueue\JobQueue\Doctrine\JobStorage' - '@Enqueue\JobQueue\CalculateRootJobStatusService' - - '@Enqueue\Client\Producer' + - '@Enqueue\Client\ProducerInterface' - '@logger' tags: - - { name: 'enqueue.client.processor' } + - { name: 'enqueue.command_subscriber', client: 'default' } # Deprecated. To be removed in 0.10. enqueue.job.calculate_root_job_status_processor: @@ -70,10 +70,10 @@ services: public: true arguments: - '@Enqueue\JobQueue\Doctrine\JobStorage' - - '@Enqueue\Client\Producer' + - '@Enqueue\Client\ProducerInterface' - '@logger' tags: - - { name: 'enqueue.client.processor' } + - { name: 'enqueue.topic_subscriber', client: 'default' } # Deprecated. To be removed in 0.10. enqueue.job.dependent_job_processor: diff --git a/pkg/enqueue-bundle/Resources/config/services.yml b/pkg/enqueue-bundle/Resources/config/services.yml index 5ed953077..a207569c0 100644 --- a/pkg/enqueue-bundle/Resources/config/services.yml +++ b/pkg/enqueue-bundle/Resources/config/services.yml @@ -1,58 +1,54 @@ -parameters: - enqueue.queue_consumer.enable_subscription_consumer: false - enqueue.queue_consumer.default_idle_time: 0 - enqueue.queue_consumer.default_receive_timeout: 10 - services: - enqueue.consumption.extensions: - class: 'Enqueue\Consumption\ChainExtension' - public: false + enqueue.locator: + class: 'Symfony\Component\DependencyInjection\ServiceLocator' arguments: - [] + tags: ['container.service_locator'] - Enqueue\Consumption\QueueConsumer: - class: 'Enqueue\Consumption\QueueConsumer' - public: true + enqueue.transport.consume_command: + class: 'Enqueue\Symfony\Consumption\ConfigurableConsumeCommand' arguments: - - '@enqueue.transport.context' - - '@enqueue.consumption.extensions' - - '%enqueue.queue_consumer.default_idle_time%' - - '%enqueue.queue_consumer.default_receive_timeout%' - calls: - - ['enableSubscriptionConsumer', ['%enqueue.queue_consumer.enable_subscription_consumer%']] - - # Deprecated. To be removed in 0.10. - enqueue.consumption.queue_consumer: - public: true - alias: 'Enqueue\Consumption\QueueConsumer' + - '@enqueue.locator' + - '%enqueue.default_transport%' + - 'enqueue.transport.%s.queue_consumer' + - 'enqueue.transport.%s.processor_registry' + tags: + - { name: 'console.command' } - Enqueue\Symfony\Consumption\ContainerAwareConsumeMessagesCommand: - class: 'Enqueue\Symfony\Consumption\ContainerAwareConsumeMessagesCommand' - public: true + enqueue.client.consume_command: + class: 'Enqueue\Symfony\Client\ConsumeCommand' arguments: - - '@Enqueue\Consumption\QueueConsumer' + - '@enqueue.locator' + - '%enqueue.default_client%' + - 'enqueue.client.%s.queue_consumer' + - 'enqueue.client.%s.driver' + - 'enqueue.client.%s.delegate_processor' tags: - { name: 'console.command' } - # Deprecated. To be removed in 0.10. - enqueue.command.consume_messages: - public: true - alias: 'Enqueue\Symfony\Consumption\ContainerAwareConsumeMessagesCommand' - - enqueue.transport.rpc_factory: - class: 'Enqueue\Rpc\RpcFactory' - public: false + enqueue.client.produce_command: + class: 'Enqueue\Symfony\Client\ProduceCommand' arguments: - - '@enqueue.transport.context' + - '@enqueue.locator' + - '%enqueue.default_client%' + - 'enqueue.client.%s.producer' + tags: + - { name: 'console.command' } - Enqueue\Rpc\RpcClient: - class: 'Enqueue\Rpc\RpcClient' - public: true + enqueue.client.setup_broker_command: + class: 'Enqueue\Symfony\Client\SetupBrokerCommand' arguments: - - '@enqueue.transport.context' - - '@enqueue.transport.rpc_factory' + - '@enqueue.locator' + - '%enqueue.default_client%' + - 'enqueue.client.%s.driver' + tags: + - { name: 'console.command' } - # Deprecated. To be removed in 0.10. - enqueue.transport.rpc_client: - public: true - alias: 'Enqueue\Rpc\RpcClient' + enqueue.client.routes_command: + class: 'Enqueue\Symfony\Client\RoutesCommand' + arguments: + - '@enqueue.locator' + - '%enqueue.default_client%' + - 'enqueue.client.%s.driver' + tags: + - { name: 'console.command' } diff --git a/pkg/enqueue-bundle/Resources/views/Profiler/panel.html.twig b/pkg/enqueue-bundle/Resources/views/Profiler/panel.html.twig index 4d8056826..ddd52a1e9 100644 --- a/pkg/enqueue-bundle/Resources/views/Profiler/panel.html.twig +++ b/pkg/enqueue-bundle/Resources/views/Profiler/panel.html.twig @@ -1,17 +1,17 @@ {% extends '@WebProfiler/Profiler/layout.html.twig' %} {% block toolbar %} - {% if collector.sentMessages|length > 0 %} + {% if collector.count > 0 %} {% set icon %} {{ include('@Enqueue/Icon/icon.svg') }} - {{ collector.sentMessages|length }} + {{ collector.count }} {% endset %} {% set text %}
Sent messages - {{ collector.sentMessages|length }} + {{ collector.count }}
{% endset %} @@ -20,53 +20,63 @@ {% endblock %} {% block menu %} - + {{ include('@Enqueue/Icon/icon.svg') }} Message Queue {% endblock %} {% block panel %} + {% if collector.count > 0 %}

Sent messages

- {% if collector.sentMessages|length > 0 %} - - - - - - - - - - - - {% for sentMessage in collector.sentMessages %} - - - - - - -
#TopicCommandMessagePriority
{{ loop.index }}{{ sentMessage.topic|default(null) }}{{ sentMessage.command|default(null) }} -
{% else %}

No messages were sent.

diff --git a/pkg/enqueue-bundle/Tests/Functional/App/AbstractAsyncListener.php b/pkg/enqueue-bundle/Tests/Functional/App/AbstractAsyncListener.php new file mode 100644 index 000000000..acf7406e1 --- /dev/null +++ b/pkg/enqueue-bundle/Tests/Functional/App/AbstractAsyncListener.php @@ -0,0 +1,48 @@ +producer = $producer; + $this->registry = $registry; + } + + /** + * @param Event|ContractEvent $event + * @param string $eventName + */ + protected function onEventInternal($event, $eventName) + { + if (false == $this->isSyncMode($eventName)) { + $transformerName = $this->registry->getTransformerNameForEvent($eventName); + + $interopMessage = $this->registry->getTransformer($transformerName)->toMessage($eventName, $event); + $message = new Message($interopMessage->getBody()); + $message->setScope(Message::SCOPE_APP); + $message->setProperty('event_name', $eventName); + $message->setProperty('transformer_name', $transformerName); + + $this->producer->sendCommand(Commands::DISPATCH_ASYNC_EVENTS, $message); + } + } +} diff --git a/pkg/enqueue-bundle/Tests/Functional/App/AppKernel.php b/pkg/enqueue-bundle/Tests/Functional/App/AppKernel.php index 59ac38666..3cafeedda 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/AppKernel.php +++ b/pkg/enqueue-bundle/Tests/Functional/App/AppKernel.php @@ -7,61 +7,39 @@ class AppKernel extends Kernel { - /** - * @return array - */ - public function registerBundles() + public function registerBundles(): iterable { $bundles = [ new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new \Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), - new \Symfony\Bundle\MonologBundle\MonologBundle(), new \Enqueue\Bundle\EnqueueBundle(), ]; return $bundles; } - /** - * @return string - */ - public function getCacheDir() + public function getCacheDir(): string { return sys_get_temp_dir().'/EnqueueBundle/cache'; } - /** - * @return string - */ - public function getLogDir() + public function getLogDir(): string { return sys_get_temp_dir().'/EnqueueBundle/cache/logs'; } - /** - * @param \Symfony\Component\Config\Loader\LoaderInterface $loader - */ public function registerContainerConfiguration(LoaderInterface $loader) { - $loader->load(__DIR__.'/config/config.yml'); - } + if (self::VERSION_ID < 60000) { + $loader->load(__DIR__.'/config/config-sf5.yml'); - protected function getKernelParameters() - { - $parameters = parent::getKernelParameters(); + return; + } - // it works in all Symfony version, 2.8, 3.x, 4.x - $parameters['db.driver'] = getenv('DOCTRINE_DRIVER'); - $parameters['db.host'] = getenv('DOCTRINE_HOST'); - $parameters['db.port'] = getenv('DOCTRINE_PORT'); - $parameters['db.name'] = getenv('DOCTRINE_DB_NAME'); - $parameters['db.user'] = getenv('DOCTRINE_USER'); - $parameters['db.password'] = getenv('DOCTRINE_PASSWORD'); - - return $parameters; + $loader->load(__DIR__.'/config/config.yml'); } - protected function getContainerClass() + protected function getContainerClass(): string { return parent::getContainerClass().'BundleDefault'; } diff --git a/pkg/enqueue-bundle/Tests/Functional/App/AsyncListener.php b/pkg/enqueue-bundle/Tests/Functional/App/AsyncListener.php index b55525fc4..23ab4af79 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/AsyncListener.php +++ b/pkg/enqueue-bundle/Tests/Functional/App/AsyncListener.php @@ -2,49 +2,15 @@ namespace Enqueue\Bundle\Tests\Functional\App; -use Enqueue\AsyncEventDispatcher\Registry; -use Enqueue\Client\Message; -use Enqueue\Client\ProducerInterface; -use Symfony\Component\EventDispatcher\Event; +use Symfony\Contracts\EventDispatcher\Event; -class AsyncListener extends \Enqueue\AsyncEventDispatcher\AsyncListener +class AsyncListener extends AbstractAsyncListener { /** - * @var ProducerInterface - */ - private $producer; - - /** - * @var Registry - */ - private $registry; - - /** - * @param ProducerInterface $producer - * @param Registry $registry - */ - public function __construct(ProducerInterface $producer, Registry $registry) - { - $this->producer = $producer; - $this->registry = $registry; - } - - /** - * @param Event $event * @param string $eventName */ - public function onEvent(Event $event = null, $eventName) + public function onEvent(Event $event, $eventName) { - if (false == $this->isSyncMode($eventName)) { - $transformerName = $this->registry->getTransformerNameForEvent($eventName); - - $psrMessage = $this->registry->getTransformer($transformerName)->toMessage($eventName, $event); - $message = new Message($psrMessage->getBody()); - $message->setScope(Message::SCOPE_APP); - $message->setProperty('event_name', $eventName); - $message->setProperty('transformer_name', $transformerName); - - $this->producer->sendCommand('symfony_events', $message); - } + $this->onEventInternal($event, $eventName); } } diff --git a/pkg/enqueue-bundle/Tests/Functional/App/CustomAppKernel.php b/pkg/enqueue-bundle/Tests/Functional/App/CustomAppKernel.php index 465196149..81d73796e 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/CustomAppKernel.php +++ b/pkg/enqueue-bundle/Tests/Functional/App/CustomAppKernel.php @@ -7,7 +7,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; -use Symfony\Component\Routing\RouteCollectionBuilder; +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; class CustomAppKernel extends Kernel { @@ -16,19 +16,21 @@ class CustomAppKernel extends Kernel private $enqueueConfigId; private $enqueueConfig = [ - 'client' => [ - 'prefix' => 'enqueue', - 'app_name' => '', - 'router_topic' => 'test', - 'router_queue' => 'test', - 'default_processor_queue' => 'test', + 'default' => [ + 'client' => [ + 'prefix' => 'enqueue', + 'app_name' => '', + 'router_topic' => 'test', + 'router_queue' => 'test', + 'default_queue' => 'test', + ], ], ]; - public function setEnqueueConfig(array $config) + public function setEnqueueConfig(array $config): void { $this->enqueueConfig = array_replace_recursive($this->enqueueConfig, $config); - $this->enqueueConfig['client']['app_name'] = str_replace('.', '', uniqid('app_name', true)); + $this->enqueueConfig['default']['client']['app_name'] = str_replace('.', '', uniqid('app_name', true)); $this->enqueueConfigId = md5(json_encode($this->enqueueConfig)); $fs = new Filesystem(); @@ -36,56 +38,44 @@ public function setEnqueueConfig(array $config) $fs->mkdir(sys_get_temp_dir().'/EnqueueBundleCustom/cache/'.$this->enqueueConfigId); } - /** - * @return array - */ - public function registerBundles() + public function registerBundles(): iterable { $bundles = [ new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new \Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), - new \Symfony\Bundle\MonologBundle\MonologBundle(), new \Enqueue\Bundle\EnqueueBundle(), ]; return $bundles; } - /** - * @return string - */ - public function getCacheDir() + public function getCacheDir(): string { return sys_get_temp_dir().'/EnqueueBundleCustom/cache/'.$this->enqueueConfigId; } - /** - * @return string - */ - public function getLogDir() + public function getLogDir(): string { return sys_get_temp_dir().'/EnqueueBundleCustom/cache/logs/'.$this->enqueueConfigId; } - protected function getContainerClass() + protected function getContainerClass(): string { return parent::getContainerClass().'Custom'.$this->enqueueConfigId; } - /** - * {@inheritdoc} - */ protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader) { - $loader->load(__DIR__.'/config/custom-config.yml'); + if (self::VERSION_ID < 60000) { + $loader->load(__DIR__.'/config/custom-config-sf5.yml'); + } else { + $loader->load(__DIR__.'/config/custom-config.yml'); + } $c->loadFromExtension('enqueue', $this->enqueueConfig); } - /** - * {@inheritdoc} - */ - protected function configureRoutes(RouteCollectionBuilder $routes) + protected function configureRoutes(RoutingConfigurator $routes) { } } diff --git a/pkg/enqueue-bundle/Tests/Functional/App/SqsCustomConnectionFactoryFactory.php b/pkg/enqueue-bundle/Tests/Functional/App/SqsCustomConnectionFactoryFactory.php new file mode 100644 index 000000000..6ed70cd65 --- /dev/null +++ b/pkg/enqueue-bundle/Tests/Functional/App/SqsCustomConnectionFactoryFactory.php @@ -0,0 +1,30 @@ +container = $container; + } + + public function create($config): ConnectionFactory + { + if (false == isset($config['service'])) { + throw new \LogicException('The sqs client has to be set'); + } + + return new SqsConnectionFactory($this->container->get($config['service'])); + } +} diff --git a/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncEventTransformer.php b/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncEventTransformer.php index 975cd4214..0a83a04b6 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncEventTransformer.php +++ b/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncEventTransformer.php @@ -4,29 +4,26 @@ use Enqueue\AsyncEventDispatcher\EventTransformer; use Enqueue\Util\JSON; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Symfony\Component\EventDispatcher\Event; +use Interop\Queue\Context; +use Interop\Queue\Message; use Symfony\Component\EventDispatcher\GenericEvent; +use Symfony\Contracts\EventDispatcher\Event; class TestAsyncEventTransformer implements EventTransformer { /** - * @var PsrContext + * @var Context */ private $context; - /** - * @param PsrContext $context - */ - public function __construct(PsrContext $context) + public function __construct(Context $context) { $this->context = $context; } - public function toMessage($eventName, Event $event) + public function toMessage($eventName, ?Event $event = null) { - if (Event::class === get_class($event)) { + if (Event::class === $event::class) { return $this->context->createMessage(json_encode('')); } @@ -41,7 +38,7 @@ public function toMessage($eventName, Event $event) ])); } - public function toEvent($eventName, PsrMessage $message) + public function toEvent($eventName, Message $message) { $data = JSON::decode($message->getBody()); diff --git a/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncListener.php b/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncListener.php index aa4884bca..fd0ec1d91 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncListener.php +++ b/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncListener.php @@ -2,14 +2,13 @@ namespace Enqueue\Bundle\Tests\Functional\App; -use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcherInterface; class TestAsyncListener { public $calls = []; - public function onEvent(Event $event, $eventName, EventDispatcherInterface $dispatcher) + public function onEvent($event, $eventName, EventDispatcherInterface $dispatcher) { $this->calls[] = func_get_args(); } diff --git a/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncSubscriber.php b/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncSubscriber.php index 2b45bed6e..cd3beb45b 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncSubscriber.php +++ b/pkg/enqueue-bundle/Tests/Functional/App/TestAsyncSubscriber.php @@ -2,7 +2,6 @@ namespace Enqueue\Bundle\Tests\Functional\App; -use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -10,7 +9,7 @@ class TestAsyncSubscriber implements EventSubscriberInterface { public $calls = []; - public function onEvent(Event $event, $eventName, EventDispatcherInterface $dispatcher) + public function onEvent($event, $eventName, EventDispatcherInterface $dispatcher) { $this->calls[] = func_get_args(); } diff --git a/pkg/enqueue-bundle/Tests/Functional/App/TestCommandSubscriberProcessor.php b/pkg/enqueue-bundle/Tests/Functional/App/TestCommandSubscriberProcessor.php index bc45336d6..480992ff1 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/TestCommandSubscriberProcessor.php +++ b/pkg/enqueue-bundle/Tests/Functional/App/TestCommandSubscriberProcessor.php @@ -4,15 +4,15 @@ use Enqueue\Client\CommandSubscriberInterface; use Enqueue\Consumption\Result; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; -class TestCommandSubscriberProcessor implements PsrProcessor, CommandSubscriberInterface +class TestCommandSubscriberProcessor implements Processor, CommandSubscriberInterface { public $calls = []; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $this->calls[] = $message; @@ -23,6 +23,6 @@ public function process(PsrMessage $message, PsrContext $context) public static function getSubscribedCommand() { - return 'test_command_subscriber'; + return 'theCommand'; } } diff --git a/pkg/enqueue-bundle/Tests/Functional/App/TestExclusiveCommandSubscriberProcessor.php b/pkg/enqueue-bundle/Tests/Functional/App/TestExclusiveCommandSubscriberProcessor.php index 8f0001b92..999ad1f24 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/TestExclusiveCommandSubscriberProcessor.php +++ b/pkg/enqueue-bundle/Tests/Functional/App/TestExclusiveCommandSubscriberProcessor.php @@ -4,15 +4,15 @@ use Enqueue\Client\CommandSubscriberInterface; use Enqueue\Consumption\Result; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; -class TestExclusiveCommandSubscriberProcessor implements PsrProcessor, CommandSubscriberInterface +class TestExclusiveCommandSubscriberProcessor implements Processor, CommandSubscriberInterface { public $calls = []; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $this->calls[] = $message; @@ -22,9 +22,10 @@ public function process(PsrMessage $message, PsrContext $context) public static function getSubscribedCommand() { return [ - 'processorName' => 'theExclusiveCommandName', - 'queueName' => 'the_exclusive_command_queue', - 'queueNameHardcoded' => true, + 'command' => 'theExclusiveCommandName', + 'processor' => 'theExclusiveCommandName', + 'queue' => 'the_exclusive_command_queue', + 'prefix_queue' => true, 'exclusive' => true, ]; } diff --git a/pkg/enqueue-bundle/Tests/Functional/App/TestTopicSubscriberProcessor.php b/pkg/enqueue-bundle/Tests/Functional/App/TestTopicSubscriberProcessor.php new file mode 100644 index 000000000..2b9f16ead --- /dev/null +++ b/pkg/enqueue-bundle/Tests/Functional/App/TestTopicSubscriberProcessor.php @@ -0,0 +1,28 @@ +calls[] = $message; + + return Result::reply( + $context->createMessage($message->getBody().'Reply') + ); + } + + public static function getSubscribedTopics() + { + return 'theTopic'; + } +} diff --git a/pkg/enqueue-bundle/Tests/Functional/App/config/config-sf5.yml b/pkg/enqueue-bundle/Tests/Functional/App/config/config-sf5.yml new file mode 100644 index 000000000..e202bb86f --- /dev/null +++ b/pkg/enqueue-bundle/Tests/Functional/App/config/config-sf5.yml @@ -0,0 +1,129 @@ +parameters: + locale: 'en' + secret: 'ThisTokenIsNotSoSecretChangeIt' + + +framework: + #esi: ~ + #translator: { fallback: "%locale%" } + test: ~ + assets: false + session: + # option incompatible with Symfony 6 + storage_id: session.storage.mock_file + secret: '%secret%' + router: { resource: '%kernel.project_dir%/config/routing.yml' } + default_locale: '%locale%' + +doctrine: + dbal: + url: "%env(DOCTRINE_DSN)%" + driver: pdo_mysql + charset: UTF8 + +enqueue: + default: + transport: 'null:' + client: + traceable_producer: true + job: true + async_events: true + async_commands: + enabled: true + timeout: 60 + command_name: ~ + queue_name: ~ + +services: + test_enqueue.client.default.traceable_producer: + alias: 'enqueue.client.default.traceable_producer' + public: true + + test_enqueue.transport.default.queue_consumer: + alias: 'enqueue.transport.default.queue_consumer' + public: true + + test_enqueue.client.default.queue_consumer: + alias: 'enqueue.client.default.queue_consumer' + public: true + + test_enqueue.transport.default.rpc_client: + alias: 'enqueue.transport.default.rpc_client' + public: true + + test_enqueue.client.default.producer: + alias: 'enqueue.client.default.producer' + public: true + + test_enqueue.client.default.spool_producer: + alias: 'enqueue.client.default.spool_producer' + public: true + + test_Enqueue\Client\ProducerInterface: + alias: 'Enqueue\Client\ProducerInterface' + public: true + + test_enqueue.client.default.driver: + alias: 'enqueue.client.default.driver' + public: true + + test_enqueue.transport.default.context: + alias: 'enqueue.transport.default.context' + public: true + + test_enqueue.client.consume_command: + alias: 'enqueue.client.consume_command' + public: true + + test.enqueue.client.routes_command: + alias: 'enqueue.client.routes_command' + public: true + + test.enqueue.events.async_processor: + alias: 'enqueue.events.async_processor' + public: true + + test_async_listener: + class: 'Enqueue\Bundle\Tests\Functional\App\TestAsyncListener' + public: true + tags: + - { name: 'kernel.event_listener', async: true, event: 'test_async', method: 'onEvent', dispatcher: 'enqueue.events.event_dispatcher' } + + test_command_subscriber_processor: + class: 'Enqueue\Bundle\Tests\Functional\App\TestCommandSubscriberProcessor' + public: true + tags: + - { name: 'enqueue.command_subscriber', client: 'default' } + + test_topic_subscriber_processor: + class: 'Enqueue\Bundle\Tests\Functional\App\TestTopicSubscriberProcessor' + public: true + tags: + - { name: 'enqueue.topic_subscriber', client: 'default' } + + test_exclusive_command_subscriber_processor: + class: 'Enqueue\Bundle\Tests\Functional\App\TestExclusiveCommandSubscriberProcessor' + public: true + tags: + - { name: 'enqueue.command_subscriber', client: 'default' } + + test_async_subscriber: + class: 'Enqueue\Bundle\Tests\Functional\App\TestAsyncSubscriber' + public: true + tags: + - { name: 'kernel.event_subscriber', async: true, dispatcher: 'enqueue.events.event_dispatcher' } + + test_async_event_transformer: + class: 'Enqueue\Bundle\Tests\Functional\App\TestAsyncEventTransformer' + public: true + arguments: + - '@enqueue.transport.default.context' + tags: + - {name: 'enqueue.event_transformer', eventName: 'test_async', transformerName: 'test_async' } + - {name: 'enqueue.event_transformer', eventName: 'test_async_subscriber', transformerName: 'test_async' } + + # overwrite async listener with one based on client producer. so we can use traceable producer. + enqueue.events.async_listener: + class: 'Enqueue\Bundle\Tests\Functional\App\AsyncListener' + public: true + arguments: ['@enqueue.client.default.producer', '@enqueue.events.registry'] diff --git a/pkg/enqueue-bundle/Tests/Functional/App/config/config.yml b/pkg/enqueue-bundle/Tests/Functional/App/config/config.yml index aa1d645ff..d3ca2a37f 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/config/config.yml +++ b/pkg/enqueue-bundle/Tests/Functional/App/config/config.yml @@ -8,63 +8,109 @@ framework: #translator: { fallback: "%locale%" } test: ~ assets: false - templating: false session: - storage_id: session.storage.mock_file + storage_factory_id: session.storage.factory.mock_file secret: '%secret%' - router: { resource: '%kernel.root_dir%/config/routing.yml' } + router: { resource: '%kernel.project_dir%/config/routing.yml' } default_locale: '%locale%' -monolog: - handlers: - main: - type: 'null' - level: 'error' - doctrine: dbal: - driver: "%db.driver%" - host: "%db.host%" - port: "%db.port%" - dbname: "%db.name%" - user: "%db.user%" - password: "%db.password%" + url: "%env(DOCTRINE_DSN)%" + driver: pdo_mysql charset: UTF8 enqueue: - transport: - default: 'null' - 'null': ~ - client: - traceable_producer: true - job: true - async_events: true - + default: + transport: 'null:' + client: + traceable_producer: true + job: true + async_events: true + async_commands: + enabled: true + timeout: 60 + command_name: ~ + queue_name: ~ services: + test_enqueue.client.default.traceable_producer: + alias: 'enqueue.client.default.traceable_producer' + public: true + + test_enqueue.transport.default.queue_consumer: + alias: 'enqueue.transport.default.queue_consumer' + public: true + + test_enqueue.client.default.queue_consumer: + alias: 'enqueue.client.default.queue_consumer' + public: true + + test_enqueue.transport.default.rpc_client: + alias: 'enqueue.transport.default.rpc_client' + public: true + + test_enqueue.client.default.producer: + alias: 'enqueue.client.default.producer' + public: true + + test_enqueue.client.default.spool_producer: + alias: 'enqueue.client.default.spool_producer' + public: true + + test_Enqueue\Client\ProducerInterface: + alias: 'Enqueue\Client\ProducerInterface' + public: true + + test_enqueue.client.default.driver: + alias: 'enqueue.client.default.driver' + public: true + + test_enqueue.transport.default.context: + alias: 'enqueue.transport.default.context' + public: true + + test_enqueue.client.consume_command: + alias: 'enqueue.client.consume_command' + public: true + + test.enqueue.client.routes_command: + alias: 'enqueue.client.routes_command' + public: true + + test.enqueue.events.async_processor: + alias: 'enqueue.events.async_processor' + public: true + test_async_listener: class: 'Enqueue\Bundle\Tests\Functional\App\TestAsyncListener' public: true tags: - - { name: 'kernel.event_listener', async: true, event: 'test_async', method: 'onEvent' } + - { name: 'kernel.event_listener', async: true, event: 'test_async', method: 'onEvent', dispatcher: 'enqueue.events.event_dispatcher' } test_command_subscriber_processor: class: 'Enqueue\Bundle\Tests\Functional\App\TestCommandSubscriberProcessor' public: true tags: - - { name: 'enqueue.client.processor' } + - { name: 'enqueue.command_subscriber', client: 'default' } + + test_topic_subscriber_processor: + class: 'Enqueue\Bundle\Tests\Functional\App\TestTopicSubscriberProcessor' + public: true + tags: + - { name: 'enqueue.topic_subscriber', client: 'default' } test_exclusive_command_subscriber_processor: class: 'Enqueue\Bundle\Tests\Functional\App\TestExclusiveCommandSubscriberProcessor' public: true tags: - - { name: 'enqueue.client.processor' } + - { name: 'enqueue.command_subscriber', client: 'default' } test_async_subscriber: class: 'Enqueue\Bundle\Tests\Functional\App\TestAsyncSubscriber' public: true tags: - - { name: 'kernel.event_subscriber', async: true } + - { name: 'kernel.event_subscriber', async: true, dispatcher: 'enqueue.events.event_dispatcher' } test_async_event_transformer: class: 'Enqueue\Bundle\Tests\Functional\App\TestAsyncEventTransformer' @@ -79,4 +125,4 @@ services: enqueue.events.async_listener: class: 'Enqueue\Bundle\Tests\Functional\App\AsyncListener' public: true - arguments: ['@Enqueue\Client\Producer', '@enqueue.events.registry'] \ No newline at end of file + arguments: ['@enqueue.client.default.producer', '@enqueue.events.registry'] diff --git a/pkg/enqueue-bundle/Tests/Functional/App/config/custom-config-sf5.yml b/pkg/enqueue-bundle/Tests/Functional/App/config/custom-config-sf5.yml new file mode 100644 index 000000000..35192652e --- /dev/null +++ b/pkg/enqueue-bundle/Tests/Functional/App/config/custom-config-sf5.yml @@ -0,0 +1,85 @@ +parameters: + locale: 'en' + secret: 'ThisTokenIsNotSoSecretChangeIt' + +framework: + #esi: ~ + #translator: { fallback: "%locale%" } + test: ~ + assets: false + session: + # the only option incompatible with Symfony 6 + storage_id: session.storage.mock_file + secret: '%secret%' + router: { resource: '%kernel.project_dir%/config/routing.yml' } + default_locale: '%locale%' + +doctrine: + dbal: + connections: + custom: + url: "%env(DOCTRINE_DSN)%" + driver: pdo_mysql + charset: UTF8 + +services: + test_enqueue.client.default.driver: + alias: 'enqueue.client.default.driver' + public: true + + test_enqueue.client.default.producer: + alias: 'enqueue.client.default.producer' + public: true + + test_enqueue.client.default.lazy_producer: + alias: 'enqueue.client.default.lazy_producer' + public: true + + test_enqueue.transport.default.context: + alias: 'enqueue.transport.default.context' + public: true + + test_enqueue.transport.consume_command: + alias: 'enqueue.transport.consume_command' + public: true + + test_enqueue.client.consume_command: + alias: 'enqueue.client.consume_command' + public: true + + test_enqueue.client.produce_command: + alias: 'enqueue.client.produce_command' + public: true + + test_enqueue.client.setup_broker_command: + alias: 'enqueue.client.setup_broker_command' + public: true + + test.message.processor: + class: 'Enqueue\Bundle\Tests\Functional\TestProcessor' + public: true + tags: + - { name: 'enqueue.topic_subscriber', client: 'default' } + - { name: 'enqueue.transport.processor', transport: 'default' } + + test.message.command_processor: + class: 'Enqueue\Bundle\Tests\Functional\TestCommandProcessor' + public: true + tags: + - { name: 'enqueue.command_subscriber', client: 'default' } + + test.sqs_client: + public: true + class: 'Aws\Sqs\SqsClient' + arguments: + - + endpoint: '%env(AWS_SQS_ENDPOINT)%' + region: '%env(AWS_SQS_REGION)%' + version: '%env(AWS_SQS_VERSION)%' + credentials: + key: '%env(AWS_SQS_KEY)%' + secret: '%env(AWS_SQS_SECRET)%' + + test.sqs_custom_connection_factory_factory: + class: 'Enqueue\Bundle\Tests\Functional\App\SqsCustomConnectionFactoryFactory' + arguments: ['@service_container'] diff --git a/pkg/enqueue-bundle/Tests/Functional/App/config/custom-config.yml b/pkg/enqueue-bundle/Tests/Functional/App/config/custom-config.yml index 5407f8496..d02f3002d 100644 --- a/pkg/enqueue-bundle/Tests/Functional/App/config/custom-config.yml +++ b/pkg/enqueue-bundle/Tests/Functional/App/config/custom-config.yml @@ -1,46 +1,75 @@ parameters: locale: 'en' secret: 'ThisTokenIsNotSoSecretChangeIt' - env(AWS_SQS_REGION): 'us-east-1' - env(AWS_SQS_VERSION): 'latest' - env(AWS_SQS_KEY): 'key' - env(AWS_SQS_SECRET): 'secret' - env(AWS_SQS_ENDPOINT): 'http://localstack:4576/' framework: #esi: ~ #translator: { fallback: "%locale%" } test: ~ assets: false - templating: false session: - storage_id: session.storage.mock_file + storage_factory_id: session.storage.factory.mock_file secret: '%secret%' - router: { resource: '%kernel.root_dir%/config/routing.yml' } + router: { resource: '%kernel.project_dir%/config/routing.yml' } default_locale: '%locale%' -monolog: - handlers: - main: - type: 'null' - level: 'error' +doctrine: + dbal: + connections: + custom: + url: "%env(DOCTRINE_DSN)%" + driver: pdo_mysql + charset: UTF8 services: + test_enqueue.client.default.driver: + alias: 'enqueue.client.default.driver' + public: true + + test_enqueue.client.default.producer: + alias: 'enqueue.client.default.producer' + public: true + + test_enqueue.client.default.lazy_producer: + alias: 'enqueue.client.default.lazy_producer' + public: true + + test_enqueue.transport.default.context: + alias: 'enqueue.transport.default.context' + public: true + + test_enqueue.transport.consume_command: + alias: 'enqueue.transport.consume_command' + public: true + + test_enqueue.client.consume_command: + alias: 'enqueue.client.consume_command' + public: true + + test_enqueue.client.produce_command: + alias: 'enqueue.client.produce_command' + public: true + + test_enqueue.client.setup_broker_command: + alias: 'enqueue.client.setup_broker_command' + public: true + test.message.processor: class: 'Enqueue\Bundle\Tests\Functional\TestProcessor' public: true tags: - - { name: 'enqueue.client.processor' } + - { name: 'enqueue.topic_subscriber', client: 'default' } + - { name: 'enqueue.transport.processor', transport: 'default' } test.message.command_processor: class: 'Enqueue\Bundle\Tests\Functional\TestCommandProcessor' public: true tags: - - { name: 'enqueue.client.processor' } + - { name: 'enqueue.command_subscriber', client: 'default' } test.sqs_client: public: true - class: Aws\Sqs\SqsClient + class: 'Aws\Sqs\SqsClient' arguments: - endpoint: '%env(AWS_SQS_ENDPOINT)%' @@ -49,3 +78,7 @@ services: credentials: key: '%env(AWS_SQS_KEY)%' secret: '%env(AWS_SQS_SECRET)%' + + test.sqs_custom_connection_factory_factory: + class: 'Enqueue\Bundle\Tests\Functional\App\SqsCustomConnectionFactoryFactory' + arguments: ['@service_container'] diff --git a/pkg/enqueue-bundle/Tests/Functional/Client/ConsumeMessagesCommandTest.php b/pkg/enqueue-bundle/Tests/Functional/Client/ConsumeMessagesCommandTest.php deleted file mode 100644 index 5af610971..000000000 --- a/pkg/enqueue-bundle/Tests/Functional/Client/ConsumeMessagesCommandTest.php +++ /dev/null @@ -1,19 +0,0 @@ -get(ConsumeMessagesCommand::class); - - $this->assertInstanceOf(ConsumeMessagesCommand::class, $command); - } -} diff --git a/pkg/enqueue-bundle/Tests/Functional/Client/DriverTest.php b/pkg/enqueue-bundle/Tests/Functional/Client/DriverTest.php deleted file mode 100644 index d4b290fb3..000000000 --- a/pkg/enqueue-bundle/Tests/Functional/Client/DriverTest.php +++ /dev/null @@ -1,19 +0,0 @@ -get('enqueue.client.driver'); - - $this->assertInstanceOf(DriverInterface::class, $driver); - } -} diff --git a/pkg/enqueue-bundle/Tests/Functional/Client/ProduceMessageCommandTest.php b/pkg/enqueue-bundle/Tests/Functional/Client/ProduceMessageCommandTest.php deleted file mode 100644 index 0873705e7..000000000 --- a/pkg/enqueue-bundle/Tests/Functional/Client/ProduceMessageCommandTest.php +++ /dev/null @@ -1,19 +0,0 @@ -get(ProduceMessageCommand::class); - - $this->assertInstanceOf(ProduceMessageCommand::class, $command); - } -} diff --git a/pkg/enqueue-bundle/Tests/Functional/Client/ProducerTest.php b/pkg/enqueue-bundle/Tests/Functional/Client/ProducerTest.php index 388cfa2f8..29a96aa7d 100644 --- a/pkg/enqueue-bundle/Tests/Functional/Client/ProducerTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/Client/ProducerTest.php @@ -3,11 +3,8 @@ namespace Enqueue\Bundle\Tests\Functional\Client; use Enqueue\Bundle\Tests\Functional\WebTestCase; -use Enqueue\Client\Config; use Enqueue\Client\Message; -use Enqueue\Client\Producer; use Enqueue\Client\ProducerInterface; -use Enqueue\Client\RouterProcessor; use Enqueue\Client\TraceableProducer; use Enqueue\Rpc\Promise; @@ -16,39 +13,24 @@ */ class ProducerTest extends WebTestCase { - public function setUp() + public function testCouldBeGetFromContainerByInterface() { - parent::setUp(); + $producer = static::$container->get('test_'.ProducerInterface::class); - static::$container->get(Producer::class)->clearTraces(); + $this->assertInstanceOf(ProducerInterface::class, $producer); } - public function tearDown() + public function testCouldBeGetFromContainerByServiceId() { - static::$container->get(Producer::class)->clearTraces(); + $producer = static::$container->get('test_enqueue.client.default.producer'); - parent::tearDown(); - } - - public function testCouldBeGetFromContainerAsService() - { - $messageProducer = static::$container->get(Producer::class); - - $this->assertInstanceOf(ProducerInterface::class, $messageProducer); - } - - public function testCouldBeGetFromContainerAsShortenAlias() - { - $messageProducer = static::$container->get(Producer::class); - $aliasMessageProducer = static::$container->get('enqueue.producer'); - - $this->assertSame($messageProducer, $aliasMessageProducer); + $this->assertInstanceOf(ProducerInterface::class, $producer); } public function testShouldSendEvent() { /** @var ProducerInterface $producer */ - $producer = static::$container->get(Producer::class); + $producer = static::$container->get('test_enqueue.client.default.producer'); $producer->sendEvent('theTopic', 'theMessage'); @@ -61,13 +43,13 @@ public function testShouldSendEvent() public function testShouldSendCommandWithoutNeedForReply() { /** @var ProducerInterface $producer */ - $producer = static::$container->get(Producer::class); + $producer = static::$container->get('test_enqueue.client.default.producer'); $result = $producer->sendCommand('theCommand', 'theMessage', false); $this->assertNull($result); - $traces = $this->getTraceableProducer()->getTopicTraces(Config::COMMAND_TOPIC); + $traces = $this->getTraceableProducer()->getCommandTraces('theCommand'); $this->assertCount(1, $traces); $this->assertEquals('theMessage', $traces[0]['body']); @@ -76,7 +58,7 @@ public function testShouldSendCommandWithoutNeedForReply() public function testShouldSendMessageInstanceAsCommandWithoutNeedForReply() { /** @var ProducerInterface $producer */ - $producer = static::$container->get(Producer::class); + $producer = static::$container->get('test_enqueue.client.default.producer'); $message = new Message('theMessage'); @@ -84,25 +66,20 @@ public function testShouldSendMessageInstanceAsCommandWithoutNeedForReply() $this->assertNull($result); - $traces = $this->getTraceableProducer()->getTopicTraces(Config::COMMAND_TOPIC); + $traces = $this->getTraceableProducer()->getCommandTraces('theCommand'); $this->assertCount(1, $traces); $this->assertEquals('theMessage', $traces[0]['body']); $this->assertEquals([ - 'enqueue.topic_name' => Config::COMMAND_TOPIC, - 'enqueue.processor_name' => RouterProcessor::class, - 'enqueue.command_name' => 'theCommand', - 'enqueue.processor_queue_name' => 'default', - // compatibility with 0.9x + 'enqueue.processor' => 'test_command_subscriber_processor', 'enqueue.command' => 'theCommand', - 'enqueue.topic' => '__command__', ], $traces[0]['properties']); } public function testShouldSendExclusiveCommandWithNeedForReply() { /** @var ProducerInterface $producer */ - $producer = static::$container->get(Producer::class); + $producer = static::$container->get('test_enqueue.client.default.producer'); $message = new Message('theMessage'); @@ -115,20 +92,15 @@ public function testShouldSendExclusiveCommandWithNeedForReply() $this->assertCount(1, $traces); $this->assertEquals('theMessage', $traces[0]['body']); $this->assertEquals([ - 'enqueue.topic_name' => Config::COMMAND_TOPIC, - 'enqueue.processor_name' => 'theExclusiveCommandName', - 'enqueue.command_name' => 'theExclusiveCommandName', - 'enqueue.processor_queue_name' => 'the_exclusive_command_queue', - // compatibility with 0.9x + 'enqueue.processor' => 'theExclusiveCommandName', 'enqueue.command' => 'theExclusiveCommandName', - 'enqueue.topic' => '__command__', ], $traces[0]['properties']); } public function testShouldSendMessageInstanceCommandWithNeedForReply() { /** @var ProducerInterface $producer */ - $producer = static::$container->get(Producer::class); + $producer = static::$container->get('test_enqueue.client.default.producer'); $message = new Message('theMessage'); @@ -141,21 +113,13 @@ public function testShouldSendMessageInstanceCommandWithNeedForReply() $this->assertCount(1, $traces); $this->assertEquals('theMessage', $traces[0]['body']); $this->assertEquals([ - 'enqueue.topic_name' => Config::COMMAND_TOPIC, - 'enqueue.processor_name' => RouterProcessor::class, - 'enqueue.command_name' => 'theCommand', - 'enqueue.processor_queue_name' => 'default', - // compatibility with 0.9x + 'enqueue.processor' => 'test_command_subscriber_processor', 'enqueue.command' => 'theCommand', - 'enqueue.topic' => '__command__', ], $traces[0]['properties']); } - /** - * @return TraceableProducer|object - */ - private function getTraceableProducer() + private function getTraceableProducer(): TraceableProducer { - return static::$container->get(Producer::class); + return static::$container->get('test_enqueue.client.default.traceable_producer'); } } diff --git a/pkg/enqueue-bundle/Tests/Functional/Client/SpoolProducerTest.php b/pkg/enqueue-bundle/Tests/Functional/Client/SpoolProducerTest.php index c8a1eaa27..0bba43327 100644 --- a/pkg/enqueue-bundle/Tests/Functional/Client/SpoolProducerTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/Client/SpoolProducerTest.php @@ -12,19 +12,8 @@ class SpoolProducerTest extends WebTestCase { public function testCouldBeGetFromContainerAsService() { - $producer = static::$container->get(SpoolProducer::class); + $producer = static::$container->get('test_enqueue.client.default.spool_producer'); $this->assertInstanceOf(SpoolProducer::class, $producer); } - - /** - * @group legacy - */ - public function testCouldBeGetFromContainerAsShortenAlias() - { - $producer = static::$container->get('enqueue.client.spool_producer'); - $aliasProducer = static::$container->get('enqueue.spool_producer'); - - $this->assertSame($producer, $aliasProducer); - } } diff --git a/pkg/enqueue-bundle/Tests/Functional/ConsumeMessagesCommandTest.php b/pkg/enqueue-bundle/Tests/Functional/ConsumeMessagesCommandTest.php deleted file mode 100644 index d9313c4c5..000000000 --- a/pkg/enqueue-bundle/Tests/Functional/ConsumeMessagesCommandTest.php +++ /dev/null @@ -1,18 +0,0 @@ -get(ConsumeMessagesCommand::class); - - $this->assertInstanceOf(ConsumeMessagesCommand::class, $command); - } -} diff --git a/pkg/enqueue-bundle/Tests/Functional/ContextTest.php b/pkg/enqueue-bundle/Tests/Functional/ContextTest.php index 44c141acd..87e2d95a0 100644 --- a/pkg/enqueue-bundle/Tests/Functional/ContextTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/ContextTest.php @@ -2,7 +2,7 @@ namespace Enqueue\Bundle\Tests\Functional; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; /** * @group functional @@ -11,8 +11,8 @@ class ContextTest extends WebTestCase { public function testCouldBeGetFromContainerAsService() { - $connection = static::$container->get('enqueue.transport.context'); + $connection = static::$container->get('test_enqueue.transport.default.context'); - $this->assertInstanceOf(PsrContext::class, $connection); + $this->assertInstanceOf(Context::class, $connection); } } diff --git a/pkg/enqueue-bundle/Tests/Functional/Events/AsyncListenerTest.php b/pkg/enqueue-bundle/Tests/Functional/Events/AsyncListenerTest.php index b9be84e02..7fb6fdd86 100644 --- a/pkg/enqueue-bundle/Tests/Functional/Events/AsyncListenerTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/Events/AsyncListenerTest.php @@ -2,6 +2,8 @@ namespace Enqueue\Bundle\Tests\Functional\Events; +use Enqueue\AsyncEventDispatcher\AsyncListener; +use Enqueue\AsyncEventDispatcher\Commands; use Enqueue\Bundle\Tests\Functional\App\TestAsyncListener; use Enqueue\Bundle\Tests\Functional\WebTestCase; use Enqueue\Client\TraceableProducer; @@ -14,11 +16,16 @@ */ class AsyncListenerTest extends WebTestCase { - protected function tearDown() + protected function setUp(): void { - parent::tearDown(); + parent::setUp(); - static::$container = null; + /** @var AsyncListener $asyncListener */ + $asyncListener = static::$container->get('enqueue.events.async_listener'); + + $asyncListener->resetSyncMode(); + static::$container->get('test_async_subscriber')->calls = []; + static::$container->get('test_async_listener')->calls = []; } public function testShouldNotCallRealListenerIfMarkedAsAsync() @@ -26,7 +33,7 @@ public function testShouldNotCallRealListenerIfMarkedAsAsync() /** @var EventDispatcherInterface $dispatcher */ $dispatcher = static::$container->get('event_dispatcher'); - $dispatcher->dispatch('test_async', new GenericEvent('aSubject')); + $this->dispatch($dispatcher, new GenericEvent('aSubject'), 'test_async'); /** @var TestAsyncListener $listener */ $listener = static::$container->get('test_async_listener'); @@ -41,16 +48,16 @@ public function testShouldSendMessageToExpectedCommandInsteadOfCallingRealListen $event = new GenericEvent('theSubject', ['fooArg' => 'fooVal']); - $dispatcher->dispatch('test_async', $event); + $this->dispatch($dispatcher, $event, 'test_async'); /** @var TraceableProducer $producer */ - $producer = static::$container->get('enqueue.producer'); + $producer = static::$container->get('test_enqueue.client.default.producer'); - $traces = $producer->getCommandTraces('symfony_events'); + $traces = $producer->getCommandTraces(Commands::DISPATCH_ASYNC_EVENTS); $this->assertCount(1, $traces); - $this->assertEquals('symfony_events', $traces[0]['command']); + $this->assertEquals(Commands::DISPATCH_ASYNC_EVENTS, $traces[0]['command']); $this->assertEquals('{"subject":"theSubject","arguments":{"fooArg":"fooVal"}}', $traces[0]['body']); } @@ -59,14 +66,14 @@ public function testShouldSendMessageForEveryDispatchCall() /** @var EventDispatcherInterface $dispatcher */ $dispatcher = static::$container->get('event_dispatcher'); - $dispatcher->dispatch('test_async', new GenericEvent('theSubject', ['fooArg' => 'fooVal'])); - $dispatcher->dispatch('test_async', new GenericEvent('theSubject', ['fooArg' => 'fooVal'])); - $dispatcher->dispatch('test_async', new GenericEvent('theSubject', ['fooArg' => 'fooVal'])); + $this->dispatch($dispatcher, new GenericEvent('theSubject', ['fooArg' => 'fooVal']), 'test_async'); + $this->dispatch($dispatcher, new GenericEvent('theSubject', ['fooArg' => 'fooVal']), 'test_async'); + $this->dispatch($dispatcher, new GenericEvent('theSubject', ['fooArg' => 'fooVal']), 'test_async'); /** @var TraceableProducer $producer */ - $producer = static::$container->get('enqueue.producer'); + $producer = static::$container->get('test_enqueue.client.default.producer'); - $traces = $producer->getCommandTraces('symfony_events'); + $traces = $producer->getCommandTraces(Commands::DISPATCH_ASYNC_EVENTS); $this->assertCount(3, $traces); } @@ -76,17 +83,29 @@ public function testShouldSendMessageIfDispatchedFromInsideListener() /** @var EventDispatcherInterface $dispatcher */ $dispatcher = static::$container->get('event_dispatcher'); - $dispatcher->addListener('foo', function (Event $event, $eventName, EventDispatcherInterface $dispatcher) { - $dispatcher->dispatch('test_async', new GenericEvent('theSubject', ['fooArg' => 'fooVal'])); + $eventName = 'an_event_'.uniqid(); + $dispatcher->addListener($eventName, function ($event, $eventName, EventDispatcherInterface $dispatcher) { + $this->dispatch($dispatcher, new GenericEvent('theSubject', ['fooArg' => 'fooVal']), 'test_async'); }); - $dispatcher->dispatch('foo'); + $this->dispatch($dispatcher, new GenericEvent(), $eventName); /** @var TraceableProducer $producer */ - $producer = static::$container->get('enqueue.producer'); + $producer = static::$container->get('test_enqueue.client.default.producer'); - $traces = $producer->getCommandTraces('symfony_events'); + $traces = $producer->getCommandTraces(Commands::DISPATCH_ASYNC_EVENTS); $this->assertCount(1, $traces); } + + private function dispatch(EventDispatcherInterface $dispatcher, $event, $eventName): void + { + if (!class_exists(Event::class)) { + // Symfony 5 + $dispatcher->dispatch($event, $eventName); + } else { + // Symfony < 5 + $dispatcher->dispatch($eventName, $event); + } + } } diff --git a/pkg/enqueue-bundle/Tests/Functional/Events/AsyncProcessorTest.php b/pkg/enqueue-bundle/Tests/Functional/Events/AsyncProcessorTest.php index 4513069a2..d85567509 100644 --- a/pkg/enqueue-bundle/Tests/Functional/Events/AsyncProcessorTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/Events/AsyncProcessorTest.php @@ -2,6 +2,7 @@ namespace Enqueue\Bundle\Tests\Functional\Events; +use Enqueue\AsyncEventDispatcher\AsyncListener; use Enqueue\AsyncEventDispatcher\AsyncProcessor; use Enqueue\Bundle\Tests\Functional\App\TestAsyncListener; use Enqueue\Bundle\Tests\Functional\App\TestAsyncSubscriber; @@ -9,7 +10,7 @@ use Enqueue\Null\NullContext; use Enqueue\Null\NullMessage; use Enqueue\Util\JSON; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Processor; use Symfony\Component\EventDispatcher\GenericEvent; /** @@ -17,17 +18,22 @@ */ class AsyncProcessorTest extends WebTestCase { - protected function tearDown() + protected function setUp(): void { - parent::tearDown(); + parent::setUp(); - static::$container = null; + /** @var AsyncListener $asyncListener */ + $asyncListener = static::$container->get('enqueue.events.async_listener'); + + $asyncListener->resetSyncMode(); + static::$container->get('test_async_subscriber')->calls = []; + static::$container->get('test_async_listener')->calls = []; } public function testCouldBeGetFromContainerAsService() { /** @var AsyncProcessor $processor */ - $processor = static::$container->get('enqueue.events.async_processor'); + $processor = static::$container->get('test.enqueue.events.async_processor'); $this->assertInstanceOf(AsyncProcessor::class, $processor); } @@ -35,28 +41,28 @@ public function testCouldBeGetFromContainerAsService() public function testShouldRejectIfMessageDoesNotContainEventNameProperty() { /** @var AsyncProcessor $processor */ - $processor = static::$container->get('enqueue.events.async_processor'); + $processor = static::$container->get('test.enqueue.events.async_processor'); $message = new NullMessage(); - $this->assertEquals(PsrProcessor::REJECT, $processor->process($message, new NullContext())); + $this->assertEquals(Processor::REJECT, $processor->process($message, new NullContext())); } public function testShouldRejectIfMessageDoesNotContainTransformerNameProperty() { /** @var AsyncProcessor $processor */ - $processor = static::$container->get('enqueue.events.async_processor'); + $processor = static::$container->get('test.enqueue.events.async_processor'); $message = new NullMessage(); $message->setProperty('event_name', 'anEventName'); - $this->assertEquals(PsrProcessor::REJECT, $processor->process($message, new NullContext())); + $this->assertEquals(Processor::REJECT, $processor->process($message, new NullContext())); } public function testShouldCallRealListener() { /** @var AsyncProcessor $processor */ - $processor = static::$container->get('enqueue.events.async_processor'); + $processor = static::$container->get('test.enqueue.events.async_processor'); $message = new NullMessage(); $message->setProperty('event_name', 'test_async'); @@ -66,7 +72,7 @@ public function testShouldCallRealListener() 'arguments' => ['fooArg' => 'fooVal'], ])); - $this->assertEquals(PsrProcessor::ACK, $processor->process($message, new NullContext())); + $this->assertEquals(Processor::ACK, $processor->process($message, new NullContext())); /** @var TestAsyncListener $listener */ $listener = static::$container->get('test_async_listener'); @@ -87,7 +93,7 @@ public function testShouldCallRealListener() public function testShouldCallRealSubscriber() { /** @var AsyncProcessor $processor */ - $processor = static::$container->get('enqueue.events.async_processor'); + $processor = static::$container->get('test.enqueue.events.async_processor'); $message = new NullMessage(); $message->setProperty('event_name', 'test_async_subscriber'); @@ -97,7 +103,7 @@ public function testShouldCallRealSubscriber() 'arguments' => ['fooArg' => 'fooVal'], ])); - $this->assertEquals(PsrProcessor::ACK, $processor->process($message, new NullContext())); + $this->assertEquals(Processor::ACK, $processor->process($message, new NullContext())); /** @var TestAsyncSubscriber $subscriber */ $subscriber = static::$container->get('test_async_subscriber'); diff --git a/pkg/enqueue-bundle/Tests/Functional/Events/AsyncSubscriberTest.php b/pkg/enqueue-bundle/Tests/Functional/Events/AsyncSubscriberTest.php index c2e9dc0f6..4b145524a 100644 --- a/pkg/enqueue-bundle/Tests/Functional/Events/AsyncSubscriberTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/Events/AsyncSubscriberTest.php @@ -2,7 +2,9 @@ namespace Enqueue\Bundle\Tests\Functional\Events; -use Enqueue\Bundle\Tests\Functional\App\TestAsyncListener; +use Enqueue\AsyncEventDispatcher\AsyncListener; +use Enqueue\AsyncEventDispatcher\Commands; +use Enqueue\Bundle\Tests\Functional\App\TestAsyncSubscriber; use Enqueue\Bundle\Tests\Functional\WebTestCase; use Enqueue\Client\TraceableProducer; use Symfony\Component\EventDispatcher\Event; @@ -14,11 +16,16 @@ */ class AsyncSubscriberTest extends WebTestCase { - protected function tearDown() + protected function setUp(): void { - parent::tearDown(); + parent::setUp(); - static::$container = null; + /** @var AsyncListener $asyncListener */ + $asyncListener = static::$container->get('enqueue.events.async_listener'); + + $asyncListener->resetSyncMode(); + static::$container->get('test_async_subscriber')->calls = []; + static::$container->get('test_async_listener')->calls = []; } public function testShouldNotCallRealSubscriberIfMarkedAsAsync() @@ -26,9 +33,9 @@ public function testShouldNotCallRealSubscriberIfMarkedAsAsync() /** @var EventDispatcherInterface $dispatcher */ $dispatcher = static::$container->get('event_dispatcher'); - $dispatcher->dispatch('test_async_subscriber', new GenericEvent('aSubject')); + $this->dispatch($dispatcher, new GenericEvent('aSubject'), 'test_async_subscriber'); - /** @var TestAsyncListener $listener */ + /** @var TestAsyncSubscriber $listener */ $listener = static::$container->get('test_async_subscriber'); $this->assertEmpty($listener->calls); @@ -41,16 +48,16 @@ public function testShouldSendMessageToExpectedTopicInsteadOfCallingRealSubscrib $event = new GenericEvent('theSubject', ['fooArg' => 'fooVal']); - $dispatcher->dispatch('test_async_subscriber', $event); + $this->dispatch($dispatcher, $event, 'test_async_subscriber'); /** @var TraceableProducer $producer */ - $producer = static::$container->get('enqueue.producer'); + $producer = static::$container->get('test_enqueue.client.default.producer'); - $traces = $producer->getCommandTraces('symfony_events'); + $traces = $producer->getCommandTraces(Commands::DISPATCH_ASYNC_EVENTS); $this->assertCount(1, $traces); - $this->assertEquals('symfony_events', $traces[0]['command']); + $this->assertEquals(Commands::DISPATCH_ASYNC_EVENTS, $traces[0]['command']); $this->assertEquals('{"subject":"theSubject","arguments":{"fooArg":"fooVal"}}', $traces[0]['body']); } @@ -59,14 +66,14 @@ public function testShouldSendMessageForEveryDispatchCall() /** @var EventDispatcherInterface $dispatcher */ $dispatcher = static::$container->get('event_dispatcher'); - $dispatcher->dispatch('test_async_subscriber', new GenericEvent('theSubject', ['fooArg' => 'fooVal'])); - $dispatcher->dispatch('test_async_subscriber', new GenericEvent('theSubject', ['fooArg' => 'fooVal'])); - $dispatcher->dispatch('test_async_subscriber', new GenericEvent('theSubject', ['fooArg' => 'fooVal'])); + $this->dispatch($dispatcher, new GenericEvent('theSubject', ['fooArg' => 'fooVal']), 'test_async_subscriber'); + $this->dispatch($dispatcher, new GenericEvent('theSubject', ['fooArg' => 'fooVal']), 'test_async_subscriber'); + $this->dispatch($dispatcher, new GenericEvent('theSubject', ['fooArg' => 'fooVal']), 'test_async_subscriber'); /** @var TraceableProducer $producer */ - $producer = static::$container->get('enqueue.producer'); + $producer = static::$container->get('test_enqueue.client.default.producer'); - $traces = $producer->getCommandTraces('symfony_events'); + $traces = $producer->getCommandTraces(Commands::DISPATCH_ASYNC_EVENTS); $this->assertCount(3, $traces); } @@ -76,17 +83,29 @@ public function testShouldSendMessageIfDispatchedFromInsideListener() /** @var EventDispatcherInterface $dispatcher */ $dispatcher = static::$container->get('event_dispatcher'); - $dispatcher->addListener('foo', function (Event $event, $eventName, EventDispatcherInterface $dispatcher) { - $dispatcher->dispatch('test_async_subscriber', new GenericEvent('theSubject', ['fooArg' => 'fooVal'])); + $eventName = 'anEvent'.uniqid(); + $dispatcher->addListener($eventName, function ($event, $eventName, EventDispatcherInterface $dispatcher) { + $this->dispatch($dispatcher, new GenericEvent('theSubject', ['fooArg' => 'fooVal']), 'test_async_subscriber'); }); - $dispatcher->dispatch('foo'); + $this->dispatch($dispatcher, new GenericEvent(), $eventName); /** @var TraceableProducer $producer */ - $producer = static::$container->get('enqueue.producer'); + $producer = static::$container->get('test_enqueue.client.default.producer'); - $traces = $producer->getCommandTraces('symfony_events'); + $traces = $producer->getCommandTraces(Commands::DISPATCH_ASYNC_EVENTS); $this->assertCount(1, $traces); } + + private function dispatch(EventDispatcherInterface $dispatcher, $event, $eventName): void + { + if (!class_exists(Event::class)) { + // Symfony 5 + $dispatcher->dispatch($event, $eventName); + } else { + // Symfony < 5 + $dispatcher->dispatch($eventName, $event); + } + } } diff --git a/pkg/enqueue-bundle/Tests/Functional/LazyProducerTest.php b/pkg/enqueue-bundle/Tests/Functional/LazyProducerTest.php new file mode 100644 index 000000000..18375aef3 --- /dev/null +++ b/pkg/enqueue-bundle/Tests/Functional/LazyProducerTest.php @@ -0,0 +1,64 @@ +customSetUp([ + 'default' => [ + 'transport' => [ + 'dsn' => 'invalidDSN', + ], + ], + ]); + + /** @var LazyProducer $producer */ + $producer = static::$container->get('test_enqueue.client.default.lazy_producer'); + $this->assertInstanceOf(LazyProducer::class, $producer); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The DSN is invalid.'); + $producer->sendEvent('foo', 'foo'); + } + + public static function getKernelClass(): string + { + include_once __DIR__.'/App/CustomAppKernel.php'; + + return CustomAppKernel::class; + } + + protected function customSetUp(array $enqueueConfig) + { + static::$class = null; + + static::$client = static::createClient(['enqueue_config' => $enqueueConfig]); + static::$client->getKernel()->boot(); + static::$kernel = static::$client->getKernel(); + static::$container = static::$kernel->getContainer(); + } + + protected static function createKernel(array $options = []): CustomAppKernel + { + /** @var CustomAppKernel $kernel */ + $kernel = parent::createKernel($options); + + $kernel->setEnqueueConfig(isset($options['enqueue_config']) ? $options['enqueue_config'] : []); + + return $kernel; + } +} diff --git a/pkg/enqueue-bundle/Tests/Functional/QueueConsumerTest.php b/pkg/enqueue-bundle/Tests/Functional/QueueConsumerTest.php index 1c0b16a56..e05d1532d 100644 --- a/pkg/enqueue-bundle/Tests/Functional/QueueConsumerTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/QueueConsumerTest.php @@ -11,8 +11,10 @@ class QueueConsumerTest extends WebTestCase { public function testCouldBeGetFromContainerAsService() { - $queueConsumer = static::$container->get(QueueConsumer::class); + $queueConsumer = static::$container->get('test_enqueue.client.default.queue_consumer'); + $this->assertInstanceOf(QueueConsumer::class, $queueConsumer); + $queueConsumer = static::$container->get('test_enqueue.transport.default.queue_consumer'); $this->assertInstanceOf(QueueConsumer::class, $queueConsumer); } } diff --git a/pkg/enqueue-bundle/Tests/Functional/QueueRegistryTest.php b/pkg/enqueue-bundle/Tests/Functional/QueueRegistryTest.php deleted file mode 100644 index d608b8f05..000000000 --- a/pkg/enqueue-bundle/Tests/Functional/QueueRegistryTest.php +++ /dev/null @@ -1,18 +0,0 @@ -get(QueueMetaRegistry::class); - - $this->assertInstanceOf(QueueMetaRegistry::class, $connection); - } -} diff --git a/pkg/enqueue-bundle/Tests/Functional/QueuesCommandTest.php b/pkg/enqueue-bundle/Tests/Functional/QueuesCommandTest.php deleted file mode 100644 index 53a008656..000000000 --- a/pkg/enqueue-bundle/Tests/Functional/QueuesCommandTest.php +++ /dev/null @@ -1,55 +0,0 @@ -get(QueuesCommand::class); - - $this->assertInstanceOf(QueuesCommand::class, $command); - } - - public function testShouldDisplayRegisteredQueues() - { - $command = static::$container->get(QueuesCommand::class); - - $tester = new CommandTester($command); - $tester->execute([]); - - $display = $tester->getDisplay(); - - $this->assertContains(' default ', $display); - $this->assertContains('enqueue.app.default', $display); - - $displayId = RouterProcessor::class; - if (30300 > Kernel::VERSION_ID) { - // Symfony 3.2 and below make service identifiers lowercase, so we do the same. - // To be removed when dropping support for Symfony < 3.3. - $displayId = strtolower($displayId); - } - - $this->assertContains($displayId, $display); - } - - public function testShouldDisplayRegisteredCommand() - { - $command = static::$container->get(QueuesCommand::class); - - $tester = new CommandTester($command); - $tester->execute([]); - - $display = $tester->getDisplay(); - - $this->assertContains('test_command_subscriber', $display); - } -} diff --git a/pkg/enqueue-bundle/Tests/Functional/RoutesCommandTest.php b/pkg/enqueue-bundle/Tests/Functional/RoutesCommandTest.php new file mode 100644 index 000000000..66833b1ce --- /dev/null +++ b/pkg/enqueue-bundle/Tests/Functional/RoutesCommandTest.php @@ -0,0 +1,51 @@ +get('test.enqueue.client.routes_command'); + + $this->assertInstanceOf(RoutesCommand::class, $command); + } + + public function testShouldDisplayRegisteredTopics() + { + /** @var RoutesCommand $command */ + $command = static::$container->get('test.enqueue.client.routes_command'); + + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('| topic', $tester->getDisplay()); + $this->assertStringContainsString('| theTopic', $tester->getDisplay()); + $this->assertStringContainsString('| default (prefixed)', $tester->getDisplay()); + $this->assertStringContainsString('| test_topic_subscriber_processor', $tester->getDisplay()); + $this->assertStringContainsString('| (hidden)', $tester->getDisplay()); + } + + public function testShouldDisplayCommands() + { + /** @var RoutesCommand $command */ + $command = static::$container->get('test.enqueue.client.routes_command'); + + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertSame(0, $tester->getStatusCode()); + $this->assertStringContainsString('| command', $tester->getDisplay()); + $this->assertStringContainsString('| theCommand', $tester->getDisplay()); + $this->assertStringContainsString('| test_command_subscriber_processor', $tester->getDisplay()); + $this->assertStringContainsString('| default (prefixed)', $tester->getDisplay()); + $this->assertStringContainsString('| (hidden)', $tester->getDisplay()); + } +} diff --git a/pkg/enqueue-bundle/Tests/Functional/RpcClientTest.php b/pkg/enqueue-bundle/Tests/Functional/RpcClientTest.php index 1b60661a4..3d99bcc72 100644 --- a/pkg/enqueue-bundle/Tests/Functional/RpcClientTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/RpcClientTest.php @@ -11,8 +11,8 @@ class RpcClientTest extends WebTestCase { public function testTransportRpcClientCouldBeGetFromContainerAsService() { - $connection = static::$container->get(RpcClient::class); + $rpcClient = static::$container->get('test_enqueue.transport.default.rpc_client'); - $this->assertInstanceOf(RpcClient::class, $connection); + $this->assertInstanceOf(RpcClient::class, $rpcClient); } } diff --git a/pkg/enqueue-bundle/Tests/Functional/TestCommandProcessor.php b/pkg/enqueue-bundle/Tests/Functional/TestCommandProcessor.php index be6438ae2..dfc2bb864 100644 --- a/pkg/enqueue-bundle/Tests/Functional/TestCommandProcessor.php +++ b/pkg/enqueue-bundle/Tests/Functional/TestCommandProcessor.php @@ -3,20 +3,20 @@ namespace Enqueue\Bundle\Tests\Functional; use Enqueue\Client\CommandSubscriberInterface; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; -class TestCommandProcessor implements PsrProcessor, CommandSubscriberInterface +class TestCommandProcessor implements Processor, CommandSubscriberInterface { - const COMMAND = 'test-command'; + public const COMMAND = 'test-command'; /** - * @var PsrMessage + * @var Message */ public $message; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $this->message = $message; diff --git a/pkg/enqueue-bundle/Tests/Functional/TestProcessor.php b/pkg/enqueue-bundle/Tests/Functional/TestProcessor.php index a0086bed6..9b54bdf2d 100644 --- a/pkg/enqueue-bundle/Tests/Functional/TestProcessor.php +++ b/pkg/enqueue-bundle/Tests/Functional/TestProcessor.php @@ -3,20 +3,20 @@ namespace Enqueue\Bundle\Tests\Functional; use Enqueue\Client\TopicSubscriberInterface; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; -class TestProcessor implements PsrProcessor, TopicSubscriberInterface +class TestProcessor implements Processor, TopicSubscriberInterface { - const TOPIC = 'test-topic'; + public const TOPIC = 'test-topic'; /** - * @var PsrMessage + * @var Message */ public $message; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $this->message = $message; diff --git a/pkg/enqueue-bundle/Tests/Functional/TopicRegistryTest.php b/pkg/enqueue-bundle/Tests/Functional/TopicRegistryTest.php deleted file mode 100644 index bd15aa7d5..000000000 --- a/pkg/enqueue-bundle/Tests/Functional/TopicRegistryTest.php +++ /dev/null @@ -1,18 +0,0 @@ -get(TopicMetaRegistry::class); - - $this->assertInstanceOf(TopicMetaRegistry::class, $connection); - } -} diff --git a/pkg/enqueue-bundle/Tests/Functional/TopicsCommandTest.php b/pkg/enqueue-bundle/Tests/Functional/TopicsCommandTest.php deleted file mode 100644 index f5c1595f1..000000000 --- a/pkg/enqueue-bundle/Tests/Functional/TopicsCommandTest.php +++ /dev/null @@ -1,56 +0,0 @@ -get(TopicsCommand::class); - - $this->assertInstanceOf(TopicsCommand::class, $command); - } - - public function testShouldDisplayRegisteredTopics() - { - $command = static::$container->get(TopicsCommand::class); - - $tester = new CommandTester($command); - $tester->execute([]); - - $display = $tester->getDisplay(); - - $this->assertContains('__router__', $display); - - $displayId = RouterProcessor::class; - if (30300 > Kernel::VERSION_ID) { - // Symfony 3.2 and below make service identifiers lowercase, so we do the same. - // To be removed when dropping support for Symfony < 3.3. - $displayId = strtolower($displayId); - } - - $this->assertContains($displayId, $display); - } - - public function testShouldDisplayCommands() - { - $command = static::$container->get(TopicsCommand::class); - - $tester = new CommandTester($command); - $tester->execute([]); - - $display = $tester->getDisplay(); - - $this->assertContains(Config::COMMAND_TOPIC, $display); - $this->assertContains('test_command_subscriber', $display); - } -} diff --git a/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php b/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php index 7e0bcfbde..7417412bd 100644 --- a/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php +++ b/pkg/enqueue-bundle/Tests/Functional/UseCasesTest.php @@ -4,39 +4,31 @@ use Enqueue\Bundle\Tests\Functional\App\CustomAppKernel; use Enqueue\Client\DriverInterface; -use Enqueue\Client\Producer; use Enqueue\Client\ProducerInterface; use Enqueue\Stomp\StompDestination; -use Enqueue\Symfony\Client\ConsumeMessagesCommand; -use Enqueue\Symfony\Consumption\ContainerAwareConsumeMessagesCommand; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrQueue; +use Interop\Queue\Context; +use Interop\Queue\Exception\PurgeQueueNotSupportedException; +use Interop\Queue\Message; +use Interop\Queue\Queue; use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\HttpKernel\Kernel; /** * @group functional */ class UseCasesTest extends WebTestCase { - public function setUp() + private const RECEIVE_TIMEOUT = 500; + + protected function setUp(): void { // do not call parent::setUp. // parent::setUp(); } - public function tearDown() + protected function tearDown(): void { - if ($this->getPsrContext()) { - $this->getPsrContext()->close(); - } - - if (static::$kernel) { - $fs = new Filesystem(); - $fs->remove(static::$kernel->getLogDir()); - $fs->remove(static::$kernel->getCacheDir()); + if ($this->getContext()) { + $this->getContext()->close(); } parent::tearDown(); @@ -52,32 +44,15 @@ public function provideEnqueueConfigs() $certDir = $baseDir.'/var/rabbitmq_certificates'; $this->assertDirectoryExists($certDir); - yield 'amqp' => [[ - 'transport' => [ - 'default' => 'amqp', - 'amqp' => [ - 'driver' => 'ext', - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - 'lazy' => false, - ], - ], - ]]; - yield 'amqp_dsn' => [[ - 'transport' => [ - 'default' => 'amqp', - 'amqp' => getenv('AMQP_DSN'), + 'default' => [ + 'transport' => getenv('AMQP_DSN'), ], ]]; yield 'amqps_dsn' => [[ - 'transport' => [ - 'default' => 'amqp', - 'amqp' => [ + 'default' => [ + 'transport' => [ 'dsn' => getenv('AMQPS_DSN'), 'ssl_verify' => false, 'ssl_cacert' => $certDir.'/cacert.pem', @@ -87,154 +62,102 @@ public function provideEnqueueConfigs() ], ]]; - yield 'default_amqp_as_dsn' => [[ - 'transport' => [ - 'default' => getenv('AMQP_DSN'), + yield 'dsn_as_env' => [[ + 'default' => [ + 'transport' => '%env(AMQP_DSN)%', ], ]]; - // Symfony 2.x does not such env syntax - if (version_compare(Kernel::VERSION, '3.2', '>=')) { - yield 'default_dsn_as_env' => [[ - 'transport' => [ - 'default' => '%env(AMQP_DSN)%', - ], - ]]; - } - - yield 'default_dbal_as_dsn' => [[ - 'transport' => [ - 'default' => getenv('DOCTRINE_DSN'), + yield 'dbal_dsn' => [[ + 'default' => [ + 'transport' => getenv('DOCTRINE_DSN'), ], ]]; yield 'rabbitmq_stomp' => [[ - 'transport' => [ - 'default' => 'rabbitmq_stomp', - 'rabbitmq_stomp' => [ - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_STOMP_PORT'), - 'login' => getenv('RABBITMQ_USER'), - 'password' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), + 'default' => [ + 'transport' => [ + 'dsn' => getenv('RABITMQ_STOMP_DSN'), 'lazy' => false, 'management_plugin_installed' => true, ], ], ]]; - yield 'predis' => [[ - 'transport' => [ - 'default' => 'redis', - 'redis' => [ - 'host' => getenv('REDIS_HOST'), - 'port' => (int) getenv('REDIS_PORT'), - 'vendor' => 'predis', + yield 'predis_dsn' => [[ + 'default' => [ + 'transport' => [ + 'dsn' => getenv('PREDIS_DSN'), 'lazy' => false, ], ], ]]; - yield 'phpredis' => [[ - 'transport' => [ - 'default' => 'redis', - 'redis' => [ - 'host' => getenv('REDIS_HOST'), - 'port' => (int) getenv('REDIS_PORT'), - 'vendor' => 'phpredis', + yield 'phpredis_dsn' => [[ + 'default' => [ + 'transport' => [ + 'dsn' => getenv('PHPREDIS_DSN'), 'lazy' => false, ], ], ]]; - yield 'fs' => [[ - 'transport' => [ - 'default' => 'fs', - 'fs' => [ - 'path' => sys_get_temp_dir(), - ], - ], - ]]; - yield 'fs_dsn' => [[ - 'transport' => [ - 'default' => 'fs', - 'fs' => 'file://'.sys_get_temp_dir(), + 'default' => [ + 'transport' => 'file://'.sys_get_temp_dir(), ], ]]; - yield 'default_fs_as_dsn' => [[ - 'transport' => [ - 'default' => 'file://'.sys_get_temp_dir(), + yield 'sqs' => [[ + 'default' => [ + 'transport' => [ + 'dsn' => getenv('SQS_DSN'), + ], ], ]]; - yield 'dbal' => [[ - 'transport' => [ - 'default' => 'dbal', - 'dbal' => [ - 'connection' => [ - 'dbname' => getenv('DOCTRINE_DB_NAME'), - 'user' => getenv('DOCTRINE_USER'), - 'password' => getenv('DOCTRINE_PASSWORD'), - 'host' => getenv('DOCTRINE_HOST'), - 'port' => getenv('DOCTRINE_PORT'), - 'driver' => getenv('DOCTRINE_DRIVER'), - ], + yield 'sqs_client' => [[ + 'default' => [ + 'transport' => [ + 'dsn' => 'sqs:', + 'service' => 'test.sqs_client', + 'factory_service' => 'test.sqs_custom_connection_factory_factory', ], ], ]]; - yield 'dbal_dsn' => [[ - 'transport' => [ - 'default' => 'dbal', - 'dbal' => getenv('DOCTRINE_DSN'), + yield 'mongodb_dsn' => [[ + 'default' => [ + 'transport' => getenv('MONGO_DSN'), ], ]]; - // travis build does not have secret env vars if contribution is from outside. - if (getenv('AWS_SQS_KEY')) { - yield 'sqs' => [[ - 'transport' => [ - 'default' => 'sqs', - 'sqs' => [ - 'key' => getenv('AWS_SQS_KEY'), - 'secret' => getenv('AWS_SQS_SECRET'), - 'region' => getenv('AWS_SQS_REGION'), - 'endpoint' => getenv('AWS_SQS_ENDPOINT'), - ], - ], - ]]; + yield 'doctrine' => [[ + 'default' => [ + 'transport' => 'doctrine://custom', + ], + ]]; - yield 'sqs_client' => [[ + yield 'snsqs' => [[ + 'default' => [ 'transport' => [ - 'default' => 'sqs', - 'sqs' => [ - 'client' => 'test.sqs_client', - ], + 'dsn' => getenv('SNSQS_DSN'), ], - ]]; - } - - yield 'mongodb_dsn' => [[ - 'transport' => [ - 'default' => 'mongodb', - 'mongodb' => getenv('MONGO_DSN'), ], ]]; -// yield 'gps' => [[ -// 'transport' => [ -// 'default' => 'gps', -// 'gps' => [], -// ], -// ]]; + // + // yield 'gps' => [[ + // 'transport' => [ + // 'dsn' => getenv('GPS_DSN'), + // ], + // ]]; } /** * @dataProvider provideEnqueueConfigs */ - public function testProducerSendsMessage(array $enqueueConfig) + public function testProducerSendsEventMessage(array $enqueueConfig) { $this->customSetUp($enqueueConfig); @@ -242,10 +165,10 @@ public function testProducerSendsMessage(array $enqueueConfig) $this->getMessageProducer()->sendEvent(TestProcessor::TOPIC, $expectedBody); - $consumer = $this->getPsrContext()->createConsumer($this->getTestQueue()); + $consumer = $this->getContext()->createConsumer($this->getTestQueue()); - $message = $consumer->receive(100); - $this->assertInstanceOf(PsrMessage::class, $message); + $message = $consumer->receive(self::RECEIVE_TIMEOUT); + $this->assertInstanceOf(Message::class, $message); $consumer->acknowledge($message); $this->assertSame($expectedBody, $message->getBody()); @@ -262,24 +185,95 @@ public function testProducerSendsCommandMessage(array $enqueueConfig) $this->getMessageProducer()->sendCommand(TestCommandProcessor::COMMAND, $expectedBody); - $consumer = $this->getPsrContext()->createConsumer($this->getTestQueue()); + $consumer = $this->getContext()->createConsumer($this->getTestQueue()); - $message = $consumer->receive(100); - $this->assertInstanceOf(PsrMessage::class, $message); + $message = $consumer->receive(self::RECEIVE_TIMEOUT); + $this->assertInstanceOf(Message::class, $message); $consumer->acknowledge($message); - $this->assertInstanceOf(PsrMessage::class, $message); + $this->assertInstanceOf(Message::class, $message); $this->assertSame($expectedBody, $message->getBody()); } - /** - * @dataProvider provideEnqueueConfigs - */ - public function testClientConsumeCommandMessagesFromExplicitlySetQueue(array $enqueueConfig) + public function testProducerSendsEventMessageViaProduceCommand() { - $this->customSetUp($enqueueConfig); + $this->customSetUp([ + 'default' => [ + 'transport' => getenv('AMQP_DSN'), + ], + ]); + + $expectedBody = __METHOD__.time(); + + $command = static::$container->get('test_enqueue.client.produce_command'); + $tester = new CommandTester($command); + $tester->execute([ + 'message' => $expectedBody, + '--topic' => TestProcessor::TOPIC, + '--client' => 'default', + ]); + + $consumer = $this->getContext()->createConsumer($this->getTestQueue()); + + $message = $consumer->receive(self::RECEIVE_TIMEOUT); + $this->assertInstanceOf(Message::class, $message); + $consumer->acknowledge($message); - $command = static::$container->get(ConsumeMessagesCommand::class); + $this->assertSame($expectedBody, $message->getBody()); + } + + public function testProducerSendsCommandMessageViaProduceCommand() + { + $this->customSetUp([ + 'default' => [ + 'transport' => getenv('AMQP_DSN'), + ], + ]); + + $expectedBody = __METHOD__.time(); + + $command = static::$container->get('test_enqueue.client.produce_command'); + $tester = new CommandTester($command); + $tester->execute([ + 'message' => $expectedBody, + '--command' => TestCommandProcessor::COMMAND, + '--client' => 'default', + ]); + + $consumer = $this->getContext()->createConsumer($this->getTestQueue()); + + $message = $consumer->receive(self::RECEIVE_TIMEOUT); + $this->assertInstanceOf(Message::class, $message); + $consumer->acknowledge($message); + + $this->assertInstanceOf(Message::class, $message); + $this->assertSame($expectedBody, $message->getBody()); + } + + public function testShouldSetupBroker() + { + $this->customSetUp([ + 'default' => [ + 'transport' => 'file://'.sys_get_temp_dir(), + ], + ]); + + $command = static::$container->get('test_enqueue.client.setup_broker_command'); + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertSame("Broker set up\n", $tester->getDisplay()); + } + + public function testClientConsumeCommandMessagesFromExplicitlySetQueue() + { + $this->customSetUp([ + 'default' => [ + 'transport' => getenv('AMQP_DSN'), + ], + ]); + + $command = static::$container->get('test_enqueue.client.consume_command'); $processor = static::$container->get('test.message.command_processor'); $expectedBody = __METHOD__.time(); @@ -289,24 +283,26 @@ public function testClientConsumeCommandMessagesFromExplicitlySetQueue(array $en $tester = new CommandTester($command); $tester->execute([ '--message-limit' => 2, - '--time-limit' => 'now +10 seconds', + '--receive-timeout' => 100, + '--time-limit' => 'now + 2 seconds', 'client-queue-names' => ['test'], ]); - $this->assertInstanceOf(PsrMessage::class, $processor->message); + $this->assertInstanceOf(Message::class, $processor->message); $this->assertEquals($expectedBody, $processor->message->getBody()); } - /** - * @dataProvider provideEnqueueConfigs - */ - public function testClientConsumeMessagesFromExplicitlySetQueue(array $enqueueConfig) + public function testClientConsumeMessagesFromExplicitlySetQueue() { - $this->customSetUp($enqueueConfig); + $this->customSetUp([ + 'default' => [ + 'transport' => getenv('AMQP_DSN'), + ], + ]); $expectedBody = __METHOD__.time(); - $command = static::$container->get(ConsumeMessagesCommand::class); + $command = static::$container->get('test_enqueue.client.consume_command'); $processor = static::$container->get('test.message.processor'); $this->getMessageProducer()->sendEvent(TestProcessor::TOPIC, $expectedBody); @@ -314,20 +310,22 @@ public function testClientConsumeMessagesFromExplicitlySetQueue(array $enqueueCo $tester = new CommandTester($command); $tester->execute([ '--message-limit' => 2, - '--time-limit' => 'now +10 seconds', + '--receive-timeout' => 100, + '--time-limit' => 'now + 2 seconds', 'client-queue-names' => ['test'], ]); - $this->assertInstanceOf(PsrMessage::class, $processor->message); + $this->assertInstanceOf(Message::class, $processor->message); $this->assertEquals($expectedBody, $processor->message->getBody()); } - /** - * @dataProvider provideEnqueueConfigs - */ - public function testTransportConsumeMessagesCommandShouldConsumeMessage(array $enqueueConfig) + public function testTransportConsumeCommandShouldConsumeOneMessage() { - $this->customSetUp($enqueueConfig); + $this->customSetUp([ + 'default' => [ + 'transport' => getenv('AMQP_DSN'), + ], + ]); if ($this->getTestQueue() instanceof StompDestination) { $this->markTestSkipped('The test fails with the exception Stomp\Exception\ErrorFrameException: Error "precondition_failed". '. @@ -337,8 +335,7 @@ public function testTransportConsumeMessagesCommandShouldConsumeMessage(array $e $expectedBody = __METHOD__.time(); - $command = static::$container->get(ContainerAwareConsumeMessagesCommand::class); - $command->setContainer(static::$container); + $command = static::$container->get('test_enqueue.transport.consume_command'); $processor = static::$container->get('test.message.processor'); $this->getMessageProducer()->sendEvent(TestProcessor::TOPIC, $expectedBody); @@ -346,20 +343,17 @@ public function testTransportConsumeMessagesCommandShouldConsumeMessage(array $e $tester = new CommandTester($command); $tester->execute([ '--message-limit' => 1, - '--time-limit' => '+10sec', + '--time-limit' => '+2sec', '--receive-timeout' => 1000, - '--queue' => [$this->getTestQueue()->getQueueName()], - 'processor-service' => 'test.message.processor', + 'processor' => 'test.message.processor', + 'queues' => [$this->getTestQueue()->getQueueName()], ]); - $this->assertInstanceOf(PsrMessage::class, $processor->message); + $this->assertInstanceOf(Message::class, $processor->message); $this->assertEquals($expectedBody, $processor->message->getBody()); } - /** - * @return string - */ - public static function getKernelClass() + public static function getKernelClass(): string { include_once __DIR__.'/App/CustomAppKernel.php'; @@ -370,41 +364,35 @@ protected function customSetUp(array $enqueueConfig) { static::$class = null; - $this->client = static::createClient(['enqueue_config' => $enqueueConfig]); - $this->client->getKernel()->boot(); - static::$kernel = $this->client->getKernel(); + static::$client = static::createClient(['enqueue_config' => $enqueueConfig]); + static::$client->getKernel()->boot(); + static::$kernel = static::$client->getKernel(); static::$container = static::$kernel->getContainer(); /** @var DriverInterface $driver */ - $driver = static::$container->get('enqueue.client.driver'); - $context = $this->getPsrContext(); + $driver = static::$container->get('test_enqueue.client.default.driver'); + $context = $this->getContext(); $driver->setupBroker(); try { - if (method_exists($context, 'purgeQueue')) { - $queue = $this->getTestQueue(); - $context->purgeQueue($queue); - } - } catch (\Exception $e) { + $context->purgeQueue($this->getTestQueue()); + } catch (PurgeQueueNotSupportedException $e) { } } /** - * @return PsrQueue + * @return Queue */ protected function getTestQueue() { /** @var DriverInterface $driver */ - $driver = static::$container->get('enqueue.client.driver'); + $driver = static::$container->get('test_enqueue.client.default.driver'); return $driver->createQueue('test'); } - /** - * {@inheritdoc} - */ - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): CustomAppKernel { /** @var CustomAppKernel $kernel */ $kernel = parent::createKernel($options); @@ -414,19 +402,13 @@ protected static function createKernel(array $options = []) return $kernel; } - /** - * @return ProducerInterface|object - */ - private function getMessageProducer() + private function getMessageProducer(): ProducerInterface { - return static::$container->get(Producer::class); + return static::$container->get('test_enqueue.client.default.producer'); } - /** - * @return PsrContext|object - */ - private function getPsrContext() + private function getContext(): Context { - return static::$container->get('enqueue.transport.context'); + return static::$container->get('test_enqueue.transport.default.context'); } } diff --git a/pkg/enqueue-bundle/Tests/Functional/WebTestCase.php b/pkg/enqueue-bundle/Tests/Functional/WebTestCase.php index 4b254ccd9..6a348a9f3 100644 --- a/pkg/enqueue-bundle/Tests/Functional/WebTestCase.php +++ b/pkg/enqueue-bundle/Tests/Functional/WebTestCase.php @@ -3,6 +3,7 @@ namespace Enqueue\Bundle\Tests\Functional; use Enqueue\Bundle\Tests\Functional\App\AppKernel; +use Enqueue\Client\TraceableProducer; use Symfony\Bundle\FrameworkBundle\Client; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -12,30 +13,33 @@ abstract class WebTestCase extends BaseWebTestCase /** * @var Client */ - protected $client; + protected static $client; /** * @var ContainerInterface */ protected static $container; - protected function setUp() + protected function setUp(): void { parent::setUp(); static::$class = null; + static::$client = static::createClient(); + static::$container = static::$kernel->getContainer(); - $this->client = static::createClient(); + /** @var TraceableProducer $producer */ + $producer = static::$container->get('test_enqueue.client.default.traceable_producer'); + $producer->clearTraces(); + } - if (false == static::$container) { - static::$container = static::$kernel->getContainer(); - } + protected function tearDown(): void + { + static::ensureKernelShutdown(); + static::$client = null; } - /** - * @return string - */ - public static function getKernelClass() + public static function getKernelClass(): string { include_once __DIR__.'/App/AppKernel.php'; diff --git a/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrineClearIdentityMapExtensionTest.php b/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrineClearIdentityMapExtensionTest.php index 08403bf16..7c5c2dd5d 100644 --- a/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrineClearIdentityMapExtensionTest.php +++ b/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrineClearIdentityMapExtensionTest.php @@ -2,23 +2,20 @@ namespace Enqueue\Bundle\Tests\Unit\Consumption\Extension; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\ObjectManager; use Enqueue\Bundle\Consumption\Extension\DoctrineClearIdentityMapExtension; -use Enqueue\Consumption\Context; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Enqueue\Consumption\Context\MessageReceived; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Message; +use Interop\Queue\Processor; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; -use Symfony\Bridge\Doctrine\RegistryInterface; class DoctrineClearIdentityMapExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new DoctrineClearIdentityMapExtension($this->createRegistryMock()); - } - public function testShouldClearIdentityMap() { $manager = $this->createManagerMock(); @@ -31,10 +28,10 @@ public function testShouldClearIdentityMap() $registry ->expects($this->once()) ->method('getManagers') - ->will($this->returnValue(['manager-name' => $manager])) + ->willReturn(['manager-name' => $manager]) ; - $context = $this->createPsrContext(); + $context = $this->createContext(); $context->getLogger() ->expects($this->once()) ->method('debug') @@ -42,34 +39,33 @@ public function testShouldClearIdentityMap() ; $extension = new DoctrineClearIdentityMapExtension($registry); - $extension->onPreReceived($context); + $extension->onMessageReceived($context); } - /** - * @return Context - */ - protected function createPsrContext() + protected function createContext(): MessageReceived { - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($this->createMock(LoggerInterface::class)); - $context->setPsrConsumer($this->createMock(PsrConsumer::class)); - $context->setPsrProcessor($this->createMock(PsrProcessor::class)); - - return $context; + return new MessageReceived( + $this->createMock(InteropContext::class), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + $this->createMock(Processor::class), + 1, + $this->createMock(LoggerInterface::class) + ); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|RegistryInterface + * @return MockObject|ManagerRegistry */ - protected function createRegistryMock() + protected function createRegistryMock(): ManagerRegistry { - return $this->createMock(RegistryInterface::class); + return $this->createMock(ManagerRegistry::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ObjectManager + * @return MockObject|ObjectManager */ - protected function createManagerMock() + protected function createManagerMock(): ObjectManager { return $this->createMock(ObjectManager::class); } diff --git a/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrineClosedEntityManagerExtensionTest.php b/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrineClosedEntityManagerExtensionTest.php new file mode 100644 index 000000000..8e7120325 --- /dev/null +++ b/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrineClosedEntityManagerExtensionTest.php @@ -0,0 +1,223 @@ +createManagerMock(true); + + $registry = $this->createRegistryMock([ + 'manager' => $manager, + ]); + + $message = new PreConsume( + $this->createMock(InteropContext::class), + $this->createMock(SubscriptionConsumer::class), + $this->createMock(LoggerInterface::class), + 1, + 2, + 3 + ); + + self::assertFalse($message->isExecutionInterrupted()); + + $extension = new DoctrineClosedEntityManagerExtension($registry); + $extension->onPreConsume($message); + + self::assertFalse($message->isExecutionInterrupted()); + } + + public function testOnPreConsumeShouldInterruptExecutionIfAManagerIsClosed() + { + $manager1 = $this->createManagerMock(true); + $manager2 = $this->createManagerMock(false); + + $registry = $this->createRegistryMock([ + 'manager1' => $manager1, + 'manager2' => $manager2, + ]); + + $message = new PreConsume( + $this->createMock(InteropContext::class), + $this->createMock(SubscriptionConsumer::class), + $this->createMock(LoggerInterface::class), + 1, + 2, + 3 + ); + $message->getLogger() + ->expects($this->once()) + ->method('debug') + ->with('[DoctrineClosedEntityManagerExtension] Interrupt execution as entity manager "manager2" has been closed') + ; + + self::assertFalse($message->isExecutionInterrupted()); + + $extension = new DoctrineClosedEntityManagerExtension($registry); + $extension->onPreConsume($message); + + self::assertTrue($message->isExecutionInterrupted()); + } + + public function testOnPostConsumeShouldNotInterruptExecution() + { + $manager = $this->createManagerMock(true); + + $registry = $this->createRegistryMock([ + 'manager' => $manager, + ]); + + $message = new PostConsume( + $this->createMock(InteropContext::class), + $this->createMock(SubscriptionConsumer::class), + 1, + 1, + 1, + $this->createMock(LoggerInterface::class) + ); + + self::assertFalse($message->isExecutionInterrupted()); + + $extension = new DoctrineClosedEntityManagerExtension($registry); + $extension->onPostConsume($message); + + self::assertFalse($message->isExecutionInterrupted()); + } + + public function testOnPostConsumeShouldInterruptExecutionIfAManagerIsClosed() + { + $manager1 = $this->createManagerMock(true); + $manager2 = $this->createManagerMock(false); + + $registry = $this->createRegistryMock([ + 'manager1' => $manager1, + 'manager2' => $manager2, + ]); + + $message = new PostConsume( + $this->createMock(InteropContext::class), + $this->createMock(SubscriptionConsumer::class), + 1, + 1, + 1, + $this->createMock(LoggerInterface::class) + ); + $message->getLogger() + ->expects($this->once()) + ->method('debug') + ->with('[DoctrineClosedEntityManagerExtension] Interrupt execution as entity manager "manager2" has been closed') + ; + + self::assertFalse($message->isExecutionInterrupted()); + + $extension = new DoctrineClosedEntityManagerExtension($registry); + $extension->onPostConsume($message); + + self::assertTrue($message->isExecutionInterrupted()); + } + + public function testOnPostReceivedShouldNotInterruptExecution() + { + $manager = $this->createManagerMock(true); + + $registry = $this->createRegistryMock([ + 'manager' => $manager, + ]); + + $message = new PostMessageReceived( + $this->createMock(InteropContext::class), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + $this->createMock(LoggerInterface::class) + ); + + self::assertFalse($message->isExecutionInterrupted()); + + $extension = new DoctrineClosedEntityManagerExtension($registry); + $extension->onPostMessageReceived($message); + + self::assertFalse($message->isExecutionInterrupted()); + } + + public function testOnPostReceivedShouldInterruptExecutionIfAManagerIsClosed() + { + $manager1 = $this->createManagerMock(true); + $manager2 = $this->createManagerMock(false); + + $registry = $this->createRegistryMock([ + 'manager1' => $manager1, + 'manager2' => $manager2, + ]); + + $message = new PostMessageReceived( + $this->createMock(InteropContext::class), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + $this->createMock(LoggerInterface::class) + ); + $message->getLogger() + ->expects($this->once()) + ->method('debug') + ->with('[DoctrineClosedEntityManagerExtension] Interrupt execution as entity manager "manager2" has been closed') + ; + + self::assertFalse($message->isExecutionInterrupted()); + + $extension = new DoctrineClosedEntityManagerExtension($registry); + $extension->onPostMessageReceived($message); + + self::assertTrue($message->isExecutionInterrupted()); + } + + /** + * @return MockObject|ManagerRegistry + */ + protected function createRegistryMock(array $managers): ManagerRegistry + { + $mock = $this->createMock(ManagerRegistry::class); + + $mock + ->expects($this->once()) + ->method('getManagers') + ->willReturn($managers) + ; + + return $mock; + } + + /** + * @return MockObject|EntityManagerInterface + */ + protected function createManagerMock(bool $open): EntityManagerInterface + { + $mock = $this->createMock(EntityManagerInterface::class); + + $mock + ->expects($this->once()) + ->method('isOpen') + ->willReturn($open) + ; + + return $mock; + } +} diff --git a/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrinePingConnectionExtensionTest.php b/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrinePingConnectionExtensionTest.php index acc23bc4e..36df82e52 100644 --- a/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrinePingConnectionExtensionTest.php +++ b/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/DoctrinePingConnectionExtensionTest.php @@ -3,34 +3,39 @@ namespace Enqueue\Bundle\Tests\Unit\Consumption\Extension; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\Persistence\ManagerRegistry; use Enqueue\Bundle\Consumption\Extension\DoctrinePingConnectionExtension; -use Enqueue\Consumption\Context; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Test\TestLogger; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Message; +use Interop\Queue\Processor; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; -use Symfony\Bridge\Doctrine\RegistryInterface; class DoctrinePingConnectionExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredAttributes() - { - new DoctrinePingConnectionExtension($this->createRegistryMock()); - } - public function testShouldNotReconnectIfConnectionIsOK() { $connection = $this->createConnectionMock(); $connection ->expects($this->once()) ->method('isConnected') - ->will($this->returnValue(true)) + ->willReturn(true) ; + + $abstractPlatform = $this->createMock(AbstractPlatform::class); + $abstractPlatform->expects($this->once()) + ->method('getDummySelectSQL') + ->willReturn('dummy') + ; + $connection ->expects($this->once()) - ->method('ping') - ->will($this->returnValue(true)) + ->method('getDatabasePlatform') + ->willReturn($abstractPlatform) ; $connection ->expects($this->never()) @@ -41,21 +46,21 @@ public function testShouldNotReconnectIfConnectionIsOK() ->method('connect') ; - $context = $this->createPsrContext(); - $context->getLogger() - ->expects($this->never()) - ->method('debug') - ; + $context = $this->createContext(); $registry = $this->createRegistryMock(); $registry - ->expects($this->once()) + ->expects(self::once()) ->method('getConnections') - ->will($this->returnValue([$connection])) + ->willReturn([$connection]) ; $extension = new DoctrinePingConnectionExtension($registry); - $extension->onPreReceived($context); + $extension->onMessageReceived($context); + + /** @var TestLogger $logger */ + $logger = $context->getLogger(); + self::assertFalse($logger->hasDebugRecords()); } public function testShouldDoesReconnectIfConnectionFailed() @@ -64,12 +69,13 @@ public function testShouldDoesReconnectIfConnectionFailed() $connection ->expects($this->once()) ->method('isConnected') - ->will($this->returnValue(true)) + ->willReturn(true) ; + $connection ->expects($this->once()) - ->method('ping') - ->will($this->returnValue(false)) + ->method('getDatabasePlatform') + ->willThrowException(new \Exception()) ; $connection ->expects($this->once()) @@ -80,27 +86,30 @@ public function testShouldDoesReconnectIfConnectionFailed() ->method('connect') ; - $context = $this->createPsrContext(); - $context->getLogger() - ->expects($this->at(0)) - ->method('debug') - ->with('[DoctrinePingConnectionExtension] Connection is not active trying to reconnect.') - ; - $context->getLogger() - ->expects($this->at(1)) - ->method('debug') - ->with('[DoctrinePingConnectionExtension] Connection is active now.') - ; + $context = $this->createContext(); $registry = $this->createRegistryMock(); $registry ->expects($this->once()) ->method('getConnections') - ->will($this->returnValue([$connection])) + ->willReturn([$connection]) ; $extension = new DoctrinePingConnectionExtension($registry); - $extension->onPreReceived($context); + $extension->onMessageReceived($context); + + /** @var TestLogger $logger */ + $logger = $context->getLogger(); + self::assertTrue( + $logger->hasDebugThatContains( + '[DoctrinePingConnectionExtension] Connection is not active trying to reconnect.' + ) + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[DoctrinePingConnectionExtension] Connection is active now.' + ) + ); } public function testShouldSkipIfConnectionWasNotOpened() @@ -109,11 +118,11 @@ public function testShouldSkipIfConnectionWasNotOpened() $connection1 ->expects($this->once()) ->method('isConnected') - ->will($this->returnValue(false)) + ->willReturn(false) ; $connection1 ->expects($this->never()) - ->method('ping') + ->method('getDatabasePlatform') ; // 2nd connection was opened in the past @@ -121,56 +130,61 @@ public function testShouldSkipIfConnectionWasNotOpened() $connection2 ->expects($this->once()) ->method('isConnected') - ->will($this->returnValue(true)) + ->willReturn(true) + ; + $abstractPlatform = $this->createMock(AbstractPlatform::class); + $abstractPlatform->expects($this->once()) + ->method('getDummySelectSQL') + ->willReturn('dummy') ; + $connection2 ->expects($this->once()) - ->method('ping') - ->will($this->returnValue(true)) + ->method('getDatabasePlatform') + ->willReturn($abstractPlatform) ; - $context = $this->createPsrContext(); - $context->getLogger() - ->expects($this->never()) - ->method('debug') - ; + $context = $this->createContext(); $registry = $this->createRegistryMock(); $registry ->expects($this->once()) ->method('getConnections') - ->will($this->returnValue([$connection1, $connection2])) + ->willReturn([$connection1, $connection2]) ; $extension = new DoctrinePingConnectionExtension($registry); - $extension->onPreReceived($context); + $extension->onMessageReceived($context); + + /** @var TestLogger $logger */ + $logger = $context->getLogger(); + $this->assertFalse($logger->hasDebugRecords()); } - /** - * @return Context - */ - protected function createPsrContext() + protected function createContext(): MessageReceived { - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($this->createMock(LoggerInterface::class)); - $context->setPsrConsumer($this->createMock(PsrConsumer::class)); - $context->setPsrProcessor($this->createMock(PsrProcessor::class)); - - return $context; + return new MessageReceived( + $this->createMock(InteropContext::class), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + $this->createMock(Processor::class), + 1, + new TestLogger() + ); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|RegistryInterface + * @return MockObject|ManagerRegistry */ protected function createRegistryMock() { - return $this->createMock(RegistryInterface::class); + return $this->createMock(ManagerRegistry::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Connection + * @return MockObject|Connection */ - protected function createConnectionMock() + protected function createConnectionMock(): Connection { return $this->createMock(Connection::class); } diff --git a/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/ResetServicesExtensionTest.php b/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/ResetServicesExtensionTest.php new file mode 100644 index 000000000..63282a255 --- /dev/null +++ b/pkg/enqueue-bundle/Tests/Unit/Consumption/Extension/ResetServicesExtensionTest.php @@ -0,0 +1,57 @@ +createResetterMock(); + $resetter + ->expects($this->once()) + ->method('reset') + ; + + $context = $this->createContext(); + $context->getLogger() + ->expects($this->once()) + ->method('debug') + ->with('[ResetServicesExtension] Resetting services.') + ; + + $extension = new ResetServicesExtension($resetter); + $extension->onPostMessageReceived($context); + } + + protected function createContext(): PostMessageReceived + { + return new PostMessageReceived( + $this->createMock(InteropContext::class), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + $this->createMock(Processor::class), + 1, + $this->createMock(LoggerInterface::class) + ); + } + + /** + * @return MockObject|ManagerRegistry + */ + protected function createResetterMock(): ServicesResetter + { + return $this->createMock(ServicesResetter::class); + } +} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/AddTopicMetaPassTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/AddTopicMetaPassTest.php deleted file mode 100644 index 94537fd89..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/AddTopicMetaPassTest.php +++ /dev/null @@ -1,76 +0,0 @@ -assertClassImplements(CompilerPassInterface::class, AddTopicMetaPass::class); - } - - public function testCouldBeConstructedWithoutAntArguments() - { - new AddTopicMetaPass(); - } - - public function testCouldBeConstructedByCreateFactoryMethod() - { - $pass = AddTopicMetaPass::create(); - - $this->assertInstanceOf(AddTopicMetaPass::class, $pass); - } - - public function testShouldReturnSelfOnAdd() - { - $pass = AddTopicMetaPass::create(); - - $this->assertSame($pass, $pass->add('aTopic')); - } - - public function testShouldDoNothingIfContainerDoesNotHaveRegistryService() - { - $container = new ContainerBuilder(); - - $pass = AddTopicMetaPass::create() - ->add('fooTopic') - ->add('barTopic') - ; - - $pass->process($container); - } - - public function testShouldAddTopicsInRegistryKeepingPreviouslyAdded() - { - $container = new ContainerBuilder(); - - $registry = new Definition(null, [[ - 'bazTopic' => [], - ]]); - $container->setDefinition(TopicMetaRegistry::class, $registry); - - $pass = AddTopicMetaPass::create() - ->add('fooTopic') - ->add('barTopic') - ; - $pass->process($container); - - $expectedTopics = [ - 'bazTopic' => [], - 'fooTopic' => [], - 'barTopic' => [], - ]; - - $this->assertSame($expectedTopics, $registry->getArgument(0)); - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildClientExtensionsPassTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildClientExtensionsPassTest.php deleted file mode 100644 index e98184b84..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildClientExtensionsPassTest.php +++ /dev/null @@ -1,129 +0,0 @@ -assertClassImplements(CompilerPassInterface::class, BuildClientExtensionsPass::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new BuildClientExtensionsPass(); - } - - public function testShouldReplaceFirstArgumentOfExtensionsServiceConstructorWithTagsExtensions() - { - $container = new ContainerBuilder(); - - $extensions = new Definition(); - $extensions->addArgument([]); - $container->setDefinition('enqueue.client.extensions', $extensions); - - $extension = new Definition(); - $extension->addTag('enqueue.client.extension'); - $container->setDefinition('foo_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.client.extension'); - $container->setDefinition('bar_extension', $extension); - - $pass = new BuildClientExtensionsPass(); - $pass->process($container); - - $this->assertEquals( - [new Reference('foo_extension'), new Reference('bar_extension')], - $extensions->getArgument(0) - ); - } - - public function testShouldOrderExtensionsByPriority() - { - $container = new ContainerBuilder(); - - $extensions = new Definition(); - $extensions->addArgument([]); - $container->setDefinition('enqueue.client.extensions', $extensions); - - $extension = new Definition(); - $extension->addTag('enqueue.client.extension', ['priority' => 6]); - $container->setDefinition('foo_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.client.extension', ['priority' => -5]); - $container->setDefinition('bar_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.client.extension', ['priority' => 2]); - $container->setDefinition('baz_extension', $extension); - - $pass = new BuildClientExtensionsPass(); - $pass->process($container); - - $orderedExtensions = $extensions->getArgument(0); - - $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]); - $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]); - $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]); - } - - public function testShouldAssumePriorityZeroIfPriorityIsNotSet() - { - $container = new ContainerBuilder(); - - $extensions = new Definition(); - $extensions->addArgument([]); - $container->setDefinition('enqueue.client.extensions', $extensions); - - $extension = new Definition(); - $extension->addTag('enqueue.client.extension'); - $container->setDefinition('foo_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.client.extension', ['priority' => 1]); - $container->setDefinition('bar_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.client.extension', ['priority' => -1]); - $container->setDefinition('baz_extension', $extension); - - $pass = new BuildClientExtensionsPass(); - $pass->process($container); - - $orderedExtensions = $extensions->getArgument(0); - - $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]); - $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]); - $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]); - } - - public function testShouldDoesNothingIfClientExtensionServiceIsNotDefined() - { - $container = $this->createMock(ContainerBuilder::class); - $container - ->expects($this->once()) - ->method('hasDefinition') - ->with('enqueue.client.extensions') - ->willReturn(false) - ; - $container - ->expects($this->never()) - ->method('findTaggedServiceIds') - ; - - $pass = new BuildClientExtensionsPass(); - $pass->process($container); - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildClientRoutingPassTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildClientRoutingPassTest.php deleted file mode 100644 index f1b678570..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildClientRoutingPassTest.php +++ /dev/null @@ -1,318 +0,0 @@ -createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'topic', - 'processorName' => 'processor', - 'queueName' => 'queue', - ]); - $container->setDefinition('processor', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - $pass->process($container); - - $expectedRoutes = [ - 'topic' => [ - ['processor', 'queue'], - ], - ]; - - $this->assertEquals($expectedRoutes, $router->getArgument(1)); - } - - public function testThrowIfProcessorClassNameCouldNotBeFound() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition('notExistingClass'); - $processor->addTag('enqueue.client.processor', [ - 'processorName' => 'processor', - ]); - $container->setDefinition('processor', $processor); - - $router = new Definition(); - $router->setArguments([null, []]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The class "notExistingClass" could not be found.'); - $pass->process($container); - } - - public function testShouldThrowExceptionIfTopicNameIsNotSet() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name is not set on message processor tag but it is required.'); - $pass->process($container); - } - - public function testShouldSetServiceIdAdProcessorIdIfIsNotSetInTag() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'topic', - 'queueName' => 'queue', - ]); - $container->setDefinition('processor-service-id', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - $pass->process($container); - - $expectedRoutes = [ - 'topic' => [ - ['processor-service-id', 'queue'], - ], - ]; - - $this->assertEquals($expectedRoutes, $router->getArgument(1)); - } - - public function testShouldSetDefaultQueueIfNotSetInTag() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'topic', - ]); - $container->setDefinition('processor-service-id', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - $pass->process($container); - - $expectedRoutes = [ - 'topic' => [ - ['processor-service-id', 'aDefaultQueueName'], - ], - ]; - - $this->assertEquals($expectedRoutes, $router->getArgument(1)); - } - - public function testShouldBuildRouteFromSubscriberIfOnlyTopicNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(OnlyTopicNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - $pass->process($container); - - $expectedRoutes = [ - 'topic-subscriber-name' => [ - ['processor-service-id', 'aDefaultQueueName'], - ], - ]; - - $this->assertEquals($expectedRoutes, $router->getArgument(1)); - } - - public function testShouldBuildRouteFromSubscriberIfProcessorNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(ProcessorNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - $pass->process($container); - - $expectedRoutes = [ - 'topic-subscriber-name' => [ - ['subscriber-processor-name', 'aDefaultQueueName'], - ], - ]; - - $this->assertEquals($expectedRoutes, $router->getArgument(1)); - } - - public function testShouldBuildRouteFromSubscriberIfQueueNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(QueueNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - $pass->process($container); - - $expectedRoutes = [ - 'topic-subscriber-name' => [ - ['processor-service-id', 'subscriber-queue-name'], - ], - ]; - - $this->assertEquals($expectedRoutes, $router->getArgument(1)); - } - - public function testShouldBuildRouteFromWithoutProcessorNameTopicSubscriber() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(WithoutProcessorNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - $pass->process($container); - - $expectedRoutes = [ - 'without-processor-name' => [ - ['processor-service-id', 'a_queue_name'], - ], - ]; - - $this->assertEquals($expectedRoutes, $router->getArgument(1)); - } - - public function testShouldThrowExceptionWhenTopicSubscriberConfigurationIsInvalid() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(InvalidTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $router = new Definition(); - $router->setArguments(['', '']); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic subscriber configuration is invalid'); - - $pass->process($container); - } - - public function testShouldBuildRouteFromCommandSubscriberIfOnlyCommandNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(OnlyCommandNameSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - $pass->process($container); - - $expectedRoutes = [ - 'the-command-name' => 'aDefaultQueueName', - ]; - - $this->assertEquals([], $router->getArgument(1)); - $this->assertEquals($expectedRoutes, $router->getArgument(2)); - } - - public function testShouldBuildRouteFromCommandSubscriberIfProcessorNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(ProcessorNameCommandSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $router = new Definition(); - $router->setArguments([null, null, null]); - $container->setDefinition(RouterProcessor::class, $router); - - $pass = new BuildClientRoutingPass(); - $pass->process($container); - - $expectedRoutes = [ - 'the-command-name' => 'the-command-queue-name', - ]; - - $this->assertEquals([], $router->getArgument(1)); - $this->assertEquals($expectedRoutes, $router->getArgument(2)); - } - - /** - * @return ContainerBuilder - */ - private function createContainerBuilder() - { - $container = new ContainerBuilder(); - $container->setParameter('enqueue.client.default_queue_name', 'aDefaultQueueName'); - - return $container; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildConsumptionExtensionsPassTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildConsumptionExtensionsPassTest.php deleted file mode 100644 index a7614bc96..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildConsumptionExtensionsPassTest.php +++ /dev/null @@ -1,111 +0,0 @@ -assertClassImplements(CompilerPassInterface::class, BuildConsumptionExtensionsPass::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new BuildConsumptionExtensionsPass(); - } - - public function testShouldReplaceFirstArgumentOfExtensionsServiceConstructorWithTagsExtensions() - { - $container = new ContainerBuilder(); - - $extensions = new Definition(); - $extensions->addArgument([]); - $container->setDefinition('enqueue.consumption.extensions', $extensions); - - $extension = new Definition(); - $extension->addTag('enqueue.consumption.extension'); - $container->setDefinition('foo_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.consumption.extension'); - $container->setDefinition('bar_extension', $extension); - - $pass = new BuildConsumptionExtensionsPass(); - $pass->process($container); - - $this->assertEquals( - [new Reference('foo_extension'), new Reference('bar_extension')], - $extensions->getArgument(0) - ); - } - - public function testShouldOrderExtensionsByPriority() - { - $container = new ContainerBuilder(); - - $extensions = new Definition(); - $extensions->addArgument([]); - $container->setDefinition('enqueue.consumption.extensions', $extensions); - - $extension = new Definition(); - $extension->addTag('enqueue.consumption.extension', ['priority' => 6]); - $container->setDefinition('foo_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.consumption.extension', ['priority' => -5]); - $container->setDefinition('bar_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.consumption.extension', ['priority' => 2]); - $container->setDefinition('baz_extension', $extension); - - $pass = new BuildConsumptionExtensionsPass(); - $pass->process($container); - - $orderedExtensions = $extensions->getArgument(0); - - $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]); - $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]); - $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]); - } - - public function testShouldAssumePriorityZeroIfPriorityIsNotSet() - { - $container = new ContainerBuilder(); - - $extensions = new Definition(); - $extensions->addArgument([]); - $container->setDefinition('enqueue.consumption.extensions', $extensions); - - $extension = new Definition(); - $extension->addTag('enqueue.consumption.extension'); - $container->setDefinition('foo_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.consumption.extension', ['priority' => 1]); - $container->setDefinition('bar_extension', $extension); - - $extension = new Definition(); - $extension->addTag('enqueue.consumption.extension', ['priority' => -1]); - $container->setDefinition('baz_extension', $extension); - - $pass = new BuildConsumptionExtensionsPass(); - $pass->process($container); - - $orderedExtensions = $extensions->getArgument(0); - - $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]); - $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]); - $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]); - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildExclusiveCommandsExtensionPassTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildExclusiveCommandsExtensionPassTest.php deleted file mode 100644 index 395078ac2..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildExclusiveCommandsExtensionPassTest.php +++ /dev/null @@ -1,105 +0,0 @@ -assertClassImplements(CompilerPassInterface::class, BuildExclusiveCommandsExtensionPass::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new BuildExclusiveCommandsExtensionPass(); - } - - public function testShouldDoNothingIfExclusiveCommandExtensionServiceNotRegistered() - { - $container = new ContainerBuilder(); - - $pass = new BuildExclusiveCommandsExtensionPass(); - $pass->process($container); - } - - public function testShouldReplaceFirstArgumentOfExclusiveCommandExtensionServiceConstructorWithExpectedMap() - { - $container = new ContainerBuilder(); - $container->setParameter('enqueue.client.default_queue_name', 'default'); - $container->register('enqueue.client.exclusive_command_extension', ExclusiveCommandExtension::class) - ->addArgument([]) - ; - - $processor = new Definition(ExclusiveCommandSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $pass = new BuildExclusiveCommandsExtensionPass(); - - $pass->process($container); - - $this->assertEquals([ - 'the-queue-name' => 'the-exclusive-command-name', - ], $container->getDefinition('enqueue.client.exclusive_command_extension')->getArgument(0)); - } - - public function testShouldReplaceFirstArgumentOfExclusiveCommandConfiguredAsTagAttribute() - { - $container = new ContainerBuilder(); - $container->setParameter('enqueue.client.default_queue_name', 'default'); - $container->register('enqueue.client.exclusive_command_extension', ExclusiveCommandExtension::class) - ->addArgument([]) - ; - - $processor = new Definition($this->getMockClass(PsrProcessor::class)); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => Config::COMMAND_TOPIC, - 'processorName' => 'the-exclusive-command-name', - 'queueName' => 'the-queue-name', - 'queueNameHardcoded' => true, - 'exclusive' => true, - ]); - $container->setDefinition('processor-id', $processor); - - $pass = new BuildExclusiveCommandsExtensionPass(); - - $pass->process($container); - - $this->assertEquals([ - 'the-queue-name' => 'the-exclusive-command-name', - ], $container->getDefinition('enqueue.client.exclusive_command_extension')->getArgument(0)); - } - - public function testShouldThrowIfExclusiveSetTrueButQueueNameIsNotHardcoded() - { - $container = new ContainerBuilder(); - $container->setParameter('enqueue.client.default_queue_name', 'default'); - $container->register('enqueue.client.exclusive_command_extension', ExclusiveCommandExtension::class) - ->addArgument([]) - ; - - $processor = new Definition(ExclusiveButQueueNameHardCodedCommandSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $pass = new BuildExclusiveCommandsExtensionPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The exclusive command could be used only with queueNameHardcoded attribute set to true.'); - $pass->process($container); - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildProcessorRegistryPassTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildProcessorRegistryPassTest.php deleted file mode 100644 index 9c7a00cdd..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildProcessorRegistryPassTest.php +++ /dev/null @@ -1,293 +0,0 @@ -createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'topic', - 'processorName' => 'processor-name', - ]); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - $pass->process($container); - - $expectedValue = [ - 'processor-name' => 'processor-id', - ]; - - $this->assertEquals($expectedValue, $processorRegistry->getArgument(0)); - } - - public function testThrowIfProcessorClassNameCouldNotBeFound() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition('notExistingClass'); - $processor->addTag('enqueue.client.processor', [ - 'processorName' => 'processor', - ]); - $container->setDefinition('processor', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The class "notExistingClass" could not be found.'); - $pass->process($container); - } - - public function testShouldThrowExceptionIfTopicNameIsNotSet() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name is not set on message processor tag but it is required.'); - $pass->process($container); - } - - public function testShouldSetServiceIdAdProcessorIdIfIsNotSetInTag() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'topic', - ]); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - $pass->process($container); - - $expectedValue = [ - 'processor-id' => 'processor-id', - ]; - - $this->assertEquals($expectedValue, $processorRegistry->getArgument(0)); - } - - public function testShouldBuildRouteFromSubscriberIfOnlyTopicNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(OnlyTopicNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - $pass->process($container); - - $expectedValue = [ - 'processor-id' => 'processor-id', - ]; - - $this->assertEquals($expectedValue, $processorRegistry->getArgument(0)); - } - - public function testShouldBuildRouteFromWithoutProcessorNameTopicSubscriber() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(WithoutProcessorNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - $pass->process($container); - - $expectedValue = [ - 'processor-id' => 'processor-id', - ]; - - $this->assertEquals($expectedValue, $processorRegistry->getArgument(0)); - } - - public function testShouldBuildRouteFromSubscriberIfProcessorNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(ProcessorNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - $pass->process($container); - - $expectedValue = [ - 'subscriber-processor-name' => 'processor-id', - ]; - - $this->assertEquals($expectedValue, $processorRegistry->getArgument(0)); - } - - public function testShouldThrowExceptionWhenTopicSubscriberConfigurationIsInvalid() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic subscriber configuration is invalid'); - - $container = $this->createContainerBuilder(); - - $processor = new Definition(InvalidTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - $pass->process($container); - } - - public function testShouldBuildRouteFromOnlyNameCommandSubscriber() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(OnlyCommandNameSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - $pass->process($container); - - $expectedValue = [ - 'the-command-name' => 'processor-id', - ]; - - $this->assertEquals($expectedValue, $processorRegistry->getArgument(0)); - } - - public function testShouldBuildRouteFromProcessorNameCommandSubscriber() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(ProcessorNameCommandSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - $pass->process($container); - - $expectedValue = [ - 'the-command-name' => 'processor-id', - ]; - - $this->assertEquals($expectedValue, $processorRegistry->getArgument(0)); - } - - public function testShouldThrowExceptionWhenProcessorNameEmpty() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(EmptyCommandSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The processor name (it is also the command name) must not be empty.'); - - $pass->process($container); - } - - public function testShouldThrowExceptionWhenCommandSubscriberConfigurationIsInvalid() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(InvalidCommandSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $processorRegistry = new Definition(); - $processorRegistry->setArguments([]); - $container->setDefinition('enqueue.client.processor_registry', $processorRegistry); - - $pass = new BuildProcessorRegistryPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Command subscriber configuration is invalid. "12345"'); - - $pass->process($container); - } - - /** - * @return ContainerBuilder - */ - private function createContainerBuilder() - { - $container = new ContainerBuilder(); - $container->setParameter('enqueue.client.default_queue_name', 'aDefaultQueueName'); - - return $container; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildQueueMetaRegistryPassTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildQueueMetaRegistryPassTest.php deleted file mode 100644 index 46c315a78..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildQueueMetaRegistryPassTest.php +++ /dev/null @@ -1,252 +0,0 @@ -createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'processorName' => 'processor', - ]); - $container->setDefinition('processor', $processor); - - $pass = new BuildQueueMetaRegistryPass(); - $pass->process($container); - } - - public function testThrowIfProcessorClassNameCouldNotBeFound() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition('notExistingClass'); - $processor->addTag('enqueue.client.processor', [ - 'processorName' => 'processor', - ]); - $container->setDefinition('processor', $processor); - - $registry = new Definition(); - $registry->setArguments([null, []]); - $container->setDefinition(QueueMetaRegistry::class, $registry); - - $pass = new BuildQueueMetaRegistryPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The class "notExistingClass" could not be found.'); - $pass->process($container); - } - - public function testShouldBuildQueueMetaRegistry() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'processorName' => 'theProcessorName', - 'topicName' => 'aTopicName', - ]); - $container->setDefinition('processor', $processor); - - $registry = new Definition(); - $registry->setArguments([null, []]); - $container->setDefinition(QueueMetaRegistry::class, $registry); - - $pass = new BuildQueueMetaRegistryPass(); - $pass->process($container); - - $expectedQueues = [ - 'aDefaultQueueName' => ['processors' => ['theProcessorName']], - ]; - - $this->assertEquals($expectedQueues, $registry->getArgument(1)); - } - - public function testShouldSetServiceIdAdProcessorIdIfIsNotSetInTag() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'aTopicName', - ]); - $container->setDefinition('processor-service-id', $processor); - - $registry = new Definition(); - $registry->setArguments([null, []]); - $container->setDefinition(QueueMetaRegistry::class, $registry); - - $pass = new BuildQueueMetaRegistryPass(); - $pass->process($container); - - $expectedQueues = [ - 'aDefaultQueueName' => ['processors' => ['processor-service-id']], - ]; - - $this->assertEquals($expectedQueues, $registry->getArgument(1)); - } - - public function testShouldSetQueueIfSetInTag() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'queueName' => 'theClientQueueName', - 'topicName' => 'aTopicName', - ]); - $container->setDefinition('processor-service-id', $processor); - - $registry = new Definition(); - $registry->setArguments([null, []]); - $container->setDefinition(QueueMetaRegistry::class, $registry); - - $pass = new BuildQueueMetaRegistryPass(); - $pass->process($container); - - $expectedQueues = [ - 'theClientQueueName' => ['processors' => ['processor-service-id']], - ]; - - $this->assertEquals($expectedQueues, $registry->getArgument(1)); - } - - public function testShouldBuildQueueFromSubscriberIfOnlyTopicNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(OnlyTopicNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $registry = new Definition(); - $registry->setArguments([null, []]); - $container->setDefinition(QueueMetaRegistry::class, $registry); - - $pass = new BuildQueueMetaRegistryPass(); - $pass->process($container); - - $expectedQueues = [ - 'aDefaultQueueName' => ['processors' => ['processor-service-id']], - ]; - - $this->assertEquals($expectedQueues, $registry->getArgument(1)); - } - - public function testShouldBuildQueueFromSubscriberIfProcessorNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(ProcessorNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $registry = new Definition(); - $registry->setArguments([null, []]); - $container->setDefinition(QueueMetaRegistry::class, $registry); - - $pass = new BuildQueueMetaRegistryPass(); - $pass->process($container); - - $expectedQueues = [ - 'aDefaultQueueName' => ['processors' => ['subscriber-processor-name']], - ]; - - $this->assertEquals($expectedQueues, $registry->getArgument(1)); - } - - public function testShouldBuildQueueFromSubscriberIfQueueNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(QueueNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $registry = new Definition(); - $registry->setArguments([null, []]); - $container->setDefinition(QueueMetaRegistry::class, $registry); - - $pass = new BuildQueueMetaRegistryPass(); - $pass->process($container); - - $expectedQueues = [ - 'subscriber-queue-name' => ['processors' => ['processor-service-id']], - ]; - - $this->assertEquals($expectedQueues, $registry->getArgument(1)); - } - - public function testShouldBuildQueueFromCommandSubscriber() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(ProcessorNameCommandSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $registry = new Definition(); - $registry->setArguments([null, []]); - $container->setDefinition(QueueMetaRegistry::class, $registry); - - $pass = new BuildQueueMetaRegistryPass(); - $pass->process($container); - - $expectedQueues = [ - 'the-command-queue-name' => ['processors' => ['the-command-name']], - ]; - - $this->assertEquals($expectedQueues, $registry->getArgument(1)); - } - - public function testShouldBuildQueueFromOnlyCommandNameSubscriber() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(OnlyCommandNameSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-service-id', $processor); - - $registry = new Definition(); - $registry->setArguments([null, []]); - $container->setDefinition(QueueMetaRegistry::class, $registry); - - $pass = new BuildQueueMetaRegistryPass(); - $pass->process($container); - - $expectedQueues = [ - 'aDefaultQueueName' => ['processors' => ['the-command-name']], - ]; - - $this->assertEquals($expectedQueues, $registry->getArgument(1)); - } - - /** - * @return ContainerBuilder - */ - private function createContainerBuilder() - { - $container = new ContainerBuilder(); - $container->setParameter('enqueue.client.default_queue_name', 'aDefaultQueueName'); - - return $container; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildTopicMetaSubscribersPassTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildTopicMetaSubscribersPassTest.php deleted file mode 100644 index 7e166b06d..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/BuildTopicMetaSubscribersPassTest.php +++ /dev/null @@ -1,364 +0,0 @@ -createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'topic', - 'processorName' => 'processor-name', - ]); - $container->setDefinition('processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - 'topic' => ['processors' => ['processor-name']], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - public function testThrowIfProcessorClassNameCouldNotBeFound() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition('notExistingClass'); - $processor->addTag('enqueue.client.processor', [ - 'processorName' => 'processor', - ]); - $container->setDefinition('processor', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The class "notExistingClass" could not be found.'); - $pass->process($container); - } - - public function testShouldBuildTopicMetaSubscribersForOneTagAndSameMetaInRegistry() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'topic', - 'processorName' => 'barProcessorName', - ]); - $container->setDefinition('processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[ - 'topic' => ['description' => 'aDescription', 'processors' => ['fooProcessorName']], - ]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - 'topic' => [ - 'description' => 'aDescription', - 'processors' => ['fooProcessorName', 'barProcessorName'], - ], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - public function testShouldBuildTopicMetaSubscribersForOneTagAndSameMetaInPlusAnotherRegistry() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'fooTopic', - 'processorName' => 'barProcessorName', - ]); - $container->setDefinition('processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[ - 'fooTopic' => ['description' => 'aDescription', 'processors' => ['fooProcessorName']], - 'barTopic' => ['description' => 'aBarDescription'], - ]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - 'fooTopic' => [ - 'description' => 'aDescription', - 'processors' => ['fooProcessorName', 'barProcessorName'], - ], - 'barTopic' => ['description' => 'aBarDescription'], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - public function testShouldBuildTopicMetaSubscribersForTwoTagAndEmptyRegistry() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'fooTopic', - 'processorName' => 'fooProcessorName', - ]); - $container->setDefinition('processor-id', $processor); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'fooTopic', - 'processorName' => 'barProcessorName', - ]); - $container->setDefinition('another-processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - 'fooTopic' => [ - 'processors' => ['fooProcessorName', 'barProcessorName'], - ], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - public function testShouldBuildTopicMetaSubscribersForTwoTagSameMetaRegistry() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'fooTopic', - 'processorName' => 'fooProcessorName', - ]); - $container->setDefinition('processor-id', $processor); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'fooTopic', - 'processorName' => 'barProcessorName', - ]); - $container->setDefinition('another-processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[ - 'fooTopic' => ['description' => 'aDescription', 'processors' => ['bazProcessorName']], - ]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - 'fooTopic' => [ - 'description' => 'aDescription', - 'processors' => ['bazProcessorName', 'fooProcessorName', 'barProcessorName'], - ], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - public function testThrowIfTopicNameNotSetOnTagAsAttribute() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', []); - $container->setDefinition('processor', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name is not set on message processor tag but it is required.'); - $pass->process($container); - } - - public function testShouldSetServiceIdAdProcessorIdIfIsNotSetInTag() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(\stdClass::class); - $processor->addTag('enqueue.client.processor', [ - 'topicName' => 'topic', - ]); - $container->setDefinition('processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - 'topic' => ['processors' => ['processor-id']], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - public function testShouldBuildMetaFromSubscriberIfOnlyTopicNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(OnlyTopicNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - 'topic-subscriber-name' => ['processors' => ['processor-id']], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - public function testShouldBuildMetaFromSubscriberIfProcessorNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(ProcessorNameTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - 'topic-subscriber-name' => ['processors' => ['subscriber-processor-name']], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - public function testShouldThrowExceptionWhenTopicSubscriberConfigurationIsInvalid() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(InvalidTopicSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic subscriber configuration is invalid'); - - $pass->process($container); - } - - public function testShouldBuildMetaFromCommandSubscriberIfOnlyCommandNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(OnlyCommandNameSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - Config::COMMAND_TOPIC => ['processors' => ['the-command-name']], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - public function testShouldBuildMetaFromCommandSubscriberIfProcessorNameSpecified() - { - $container = $this->createContainerBuilder(); - - $processor = new Definition(ProcessorNameCommandSubscriber::class); - $processor->addTag('enqueue.client.processor'); - $container->setDefinition('processor-id', $processor); - - $topicMetaRegistry = new Definition(); - $topicMetaRegistry->setArguments([[]]); - $container->setDefinition(TopicMetaRegistry::class, $topicMetaRegistry); - - $pass = new BuildTopicMetaSubscribersPass(); - $pass->process($container); - - $expectedValue = [ - Config::COMMAND_TOPIC => ['processors' => ['the-command-name']], - ]; - - $this->assertEquals($expectedValue, $topicMetaRegistry->getArgument(0)); - } - - /** - * @return ContainerBuilder - */ - private function createContainerBuilder() - { - $container = new ContainerBuilder(); - $container->setParameter('enqueue.client.default_queue_name', 'aDefaultQueueName'); - - return $container; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/EmptyCommandSubscriber.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/EmptyCommandSubscriber.php deleted file mode 100644 index 27fb16138..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/EmptyCommandSubscriber.php +++ /dev/null @@ -1,13 +0,0 @@ - 'the-exclusive-command-name', - 'queueName' => 'the-queue-name', - 'queueNameHardCoded' => false, - 'exclusive' => true, - ]; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/ExclusiveCommandSubscriber.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/ExclusiveCommandSubscriber.php deleted file mode 100644 index 742758a81..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/ExclusiveCommandSubscriber.php +++ /dev/null @@ -1,18 +0,0 @@ - 'the-exclusive-command-name', - 'queueName' => 'the-queue-name', - 'queueNameHardcoded' => true, - 'exclusive' => true, - ]; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/InvalidCommandSubscriber.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/InvalidCommandSubscriber.php deleted file mode 100644 index 44b32a4de..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/InvalidCommandSubscriber.php +++ /dev/null @@ -1,13 +0,0 @@ - 'the-command-name', - 'queueName' => 'the-command-queue-name', - ]; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/ProcessorNameTopicSubscriber.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/ProcessorNameTopicSubscriber.php deleted file mode 100644 index 9574ee4e4..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/ProcessorNameTopicSubscriber.php +++ /dev/null @@ -1,17 +0,0 @@ - [ - 'processorName' => 'subscriber-processor-name', - ], - ]; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/QueueNameTopicSubscriber.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/QueueNameTopicSubscriber.php deleted file mode 100644 index 6ed163d13..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/QueueNameTopicSubscriber.php +++ /dev/null @@ -1,17 +0,0 @@ - [ - 'queueName' => 'subscriber-queue-name', - ], - ]; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/WithoutProcessorNameTopicSubscriber.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/WithoutProcessorNameTopicSubscriber.php deleted file mode 100644 index fa30d9f78..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/Compiler/Mock/WithoutProcessorNameTopicSubscriber.php +++ /dev/null @@ -1,18 +0,0 @@ - [ - 'queueName' => 'a_queue_name', - 'queueNameHardcoded' => true, - ], - ]; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/ConfigurationTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/ConfigurationTest.php index 05f4b5d38..5330cde82 100644 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/ConfigurationTest.php +++ b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/ConfigurationTest.php @@ -2,15 +2,13 @@ namespace Enqueue\Bundle\Tests\Unit\DependencyInjection; +use DMS\PHPUnitExtensions\ArraySubset\Assert; use Enqueue\Bundle\DependencyInjection\Configuration; -use Enqueue\Bundle\Tests\Unit\Mocks\FooTransportFactory; -use Enqueue\Client\RouterProcessor; -use Enqueue\Null\Symfony\NullTransportFactory; -use Enqueue\Symfony\DefaultTransportFactory; use Enqueue\Test\ClassExtensionTrait; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\Definition\Exception\InvalidTypeException; use Symfony\Component\Config\Definition\Processor; class ConfigurationTest extends TestCase @@ -22,444 +20,643 @@ public function testShouldImplementConfigurationInterface() $this->assertClassImplements(ConfigurationInterface::class, Configuration::class); } - public function testCouldBeConstructedWithFactoriesAsFirstArgument() + public function testShouldBeFinal() { - new Configuration([], true); + $this->assertClassFinal(Configuration::class); } - public function testThrowIfTransportNotConfigured() + public function testShouldProcessSeveralTransports() { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The child node "transport" at path "enqueue" must be configured.'); + $configuration = new Configuration(true); - $configuration = new Configuration([], true); + $processor = new Processor(); + $config = $processor->processConfiguration($configuration, [[ + 'default' => [ + 'transport' => 'default:', + ], + 'foo' => [ + 'transport' => 'foo:', + ], + ]]); + + $this->assertConfigEquals([ + 'default' => [ + 'transport' => [ + 'dsn' => 'default:', + ], + ], + 'foo' => [ + 'transport' => [ + 'dsn' => 'foo:', + ], + ], + ], $config); + } + + public function testTransportFactoryShouldValidateEachTransportAccordingToItsRules() + { + $configuration = new Configuration(true); $processor = new Processor(); - $processor->processConfiguration($configuration, [[]]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Both options factory_class and factory_service are set. Please choose one.'); + $processor->processConfiguration($configuration, [ + [ + 'default' => [ + 'transport' => [ + 'factory_class' => 'aClass', + 'factory_service' => 'aService', + ], + ], + ], + ]); } - public function testShouldInjectFooTransportFactoryConfig() + public function testShouldSetDefaultConfigurationForClient() { - $configuration = new Configuration([new FooTransportFactory()], true); + $configuration = new Configuration(true); $processor = new Processor(); + $config = $processor->processConfiguration($configuration, [[ + 'default' => [ + 'transport' => 'null:', + 'client' => null, + ], + ]]); + + $this->assertConfigEquals([ + 'default' => [ + 'client' => [ + 'prefix' => 'enqueue', + 'app_name' => 'app', + 'router_processor' => null, + 'router_topic' => 'default', + 'router_queue' => 'default', + 'default_queue' => 'default', + 'traceable_producer' => true, + 'redelivered_delay_time' => 0, + ], + ], + ], $config); + } + + public function testThrowIfClientDriverOptionsIsNotArray() + { + $configuration = new Configuration(true); + + $processor = new Processor(); + + $this->expectException(InvalidTypeException::class); + // Exception messages vary slightly between versions + $this->expectExceptionMessageMatches( + '/Invalid type for path "enqueue\.default\.client\.driver_options"\. Expected "?array"?, but got "?string"?/' + ); + $processor->processConfiguration($configuration, [[ - 'transport' => [ - 'foo' => [ - 'foo_param' => 'aParam', + 'default' => [ + 'transport' => 'null:', + 'client' => [ + 'driver_options' => 'invalidOption', + ], + ], + ]]); + } + + public function testShouldConfigureClientDriverOptions() + { + $configuration = new Configuration(true); + + $processor = new Processor(); + $config = $processor->processConfiguration($configuration, [[ + 'default' => [ + 'transport' => 'null:', + 'client' => [ + 'driver_options' => [ + 'foo' => 'fooVal', + ], ], ], ]]); + + $this->assertConfigEquals([ + 'default' => [ + 'client' => [ + 'prefix' => 'enqueue', + 'app_name' => 'app', + 'router_processor' => null, + 'router_topic' => 'default', + 'router_queue' => 'default', + 'default_queue' => 'default', + 'traceable_producer' => true, + 'driver_options' => [ + 'foo' => 'fooVal', + ], + ], + ], + ], $config); } - public function testThrowExceptionIfFooTransportConfigInvalid() + public function testThrowExceptionIfRouterTopicIsEmpty() { - $configuration = new Configuration([new FooTransportFactory()], true); + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The path "enqueue.default.client.router_topic" cannot contain an empty value, but got "".'); + + $configuration = new Configuration(true); $processor = new Processor(); + $processor->processConfiguration($configuration, [[ + 'default' => [ + 'transport' => ['dsn' => 'null:'], + 'client' => [ + 'router_topic' => '', + ], + ], + ]]); + } + public function testThrowExceptionIfRouterQueueIsEmpty() + { $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The path "enqueue.transport.foo.foo_param" cannot contain an empty value, but got null.'); + $this->expectExceptionMessage('The path "enqueue.default.client.router_queue" cannot contain an empty value, but got "".'); + $configuration = new Configuration(true); + + $processor = new Processor(); $processor->processConfiguration($configuration, [[ - 'transport' => [ - 'foo' => [ - 'foo_param' => null, + 'default' => [ + 'transport' => ['dsn' => 'null:'], + 'client' => [ + 'router_queue' => '', ], ], ]]); } - public function testShouldAllowConfigureDefaultTransport() + public function testShouldThrowExceptionIfDefaultProcessorQueueIsEmpty() { - $configuration = new Configuration([new DefaultTransportFactory()], true); + $configuration = new Configuration(true); $processor = new Processor(); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The path "enqueue.default.client.default_queue" cannot contain an empty value, but got "".'); $processor->processConfiguration($configuration, [[ - 'transport' => [ - 'default' => ['alias' => 'foo'], + 'default' => [ + 'transport' => ['dsn' => 'null:'], + 'client' => [ + 'default_queue' => '', + ], ], ]]); } - public function testShouldAllowConfigureNullTransport() + public function testJobShouldBeDisabledByDefault() { - $configuration = new Configuration([new NullTransportFactory()], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [ - 'null' => true, + 'default' => [ + 'transport' => [], ], ]]); - $this->assertArraySubset([ - 'transport' => [ - 'null' => [], + Assert::assertArraySubset([ + 'default' => [ + 'job' => [ + 'enabled' => false, + ], ], ], $config); } - public function testShouldAllowConfigureSeveralTransportsSameTime() + public function testCouldEnableJob() { - $configuration = new Configuration([ - new NullTransportFactory(), - new DefaultTransportFactory(), - new FooTransportFactory(), - ], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [ - 'default' => 'foo', - 'null' => true, - 'foo' => ['foo_param' => 'aParam'], + 'default' => [ + 'transport' => [], + 'job' => true, ], ]]); - $this->assertArraySubset([ - 'transport' => [ - 'default' => ['alias' => 'foo'], - 'null' => [], - 'foo' => ['foo_param' => 'aParam'], + Assert::assertArraySubset([ + 'default' => [ + 'job' => true, ], ], $config); } - public function testShouldSetDefaultConfigurationForClient() + public function testDoctrinePingConnectionExtensionShouldBeDisabledByDefault() { - $configuration = new Configuration([new DefaultTransportFactory()], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [ - 'default' => ['alias' => 'foo'], + 'default' => [ + 'transport' => null, ], - 'client' => null, ]]); - $this->assertArraySubset([ - 'transport' => [ - 'default' => ['alias' => 'foo'], - ], - 'client' => [ - 'prefix' => 'enqueue', - 'app_name' => 'app', - 'router_processor' => RouterProcessor::class, - 'router_topic' => 'default', - 'router_queue' => 'default', - 'default_processor_queue' => 'default', - 'traceable_producer' => true, - 'redelivered_delay_time' => 0, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'doctrine_ping_connection_extension' => false, + ], ], ], $config); } - public function testThrowExceptionIfRouterTopicIsEmpty() + public function testDoctrinePingConnectionExtensionCouldBeEnabled() { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The path "enqueue.client.router_topic" cannot contain an empty value, but got "".'); - - $configuration = new Configuration([new DefaultTransportFactory()], true); + $configuration = new Configuration(true); $processor = new Processor(); - $processor->processConfiguration($configuration, [[ - 'transport' => [ - 'default' => ['alias' => 'foo'], - ], - 'client' => [ - 'router_topic' => '', + $config = $processor->processConfiguration($configuration, [[ + 'default' => [ + 'transport' => null, + 'extensions' => [ + 'doctrine_ping_connection_extension' => true, + ], ], ]]); + + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'doctrine_ping_connection_extension' => true, + ], + ], + ], $config); } - public function testThrowExceptionIfRouterQueueIsEmpty() + public function testDoctrineClearIdentityMapExtensionShouldBeDisabledByDefault() { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The path "enqueue.client.router_queue" cannot contain an empty value, but got "".'); - - $configuration = new Configuration([new DefaultTransportFactory()], true); + $configuration = new Configuration(true); $processor = new Processor(); - $processor->processConfiguration($configuration, [[ - 'transport' => [ - 'default' => ['alias' => 'foo'], - ], - 'client' => [ - 'router_queue' => '', + $config = $processor->processConfiguration($configuration, [[ + 'default' => [ + 'transport' => null, ], ]]); + + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'doctrine_clear_identity_map_extension' => false, + ], + ], + ], $config); } - public function testShouldThrowExceptionIfDefaultProcessorQueueIsEmpty() + public function testDoctrineClearIdentityMapExtensionCouldBeEnabled() { - $configuration = new Configuration([new DefaultTransportFactory()], true); + $configuration = new Configuration(true); $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The path "enqueue.client.default_processor_queue" cannot contain an empty value, but got "".'); - $processor->processConfiguration($configuration, [[ - 'transport' => [ - 'default' => ['alias' => 'foo'], - ], - 'client' => [ - 'default_processor_queue' => '', + $config = $processor->processConfiguration($configuration, [[ + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_clear_identity_map_extension' => true, + ], ], ]]); + + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'doctrine_clear_identity_map_extension' => true, + ], + ], + ], $config); } - public function testJobShouldBeDisabledByDefault() + public function testDoctrineOdmClearIdentityMapExtensionShouldBeDisabledByDefault() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], + 'default' => [ + 'transport' => null, + ], ]]); - $this->assertArraySubset([ - 'job' => false, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'doctrine_odm_clear_identity_map_extension' => false, + ], + ], ], $config); } - public function testCouldEnableJob() + public function testDoctrineOdmClearIdentityMapExtensionCouldBeEnabled() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], - 'job' => true, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_odm_clear_identity_map_extension' => true, + ], + ], ]]); - $this->assertArraySubset([ - 'job' => true, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'doctrine_odm_clear_identity_map_extension' => true, + ], + ], ], $config); } - public function testDoctrinePingConnectionExtensionShouldBeDisabledByDefault() + public function testDoctrineClosedEntityManagerExtensionShouldBeDisabledByDefault() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], + 'default' => [ + 'transport' => null, + ], ]]); - $this->assertArraySubset([ - 'extensions' => [ - 'doctrine_ping_connection_extension' => false, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'doctrine_closed_entity_manager_extension' => false, + ], ], ], $config); } - public function testDoctrinePingConnectionExtensionCouldBeEnabled() + public function testDoctrineClosedEntityManagerExtensionCouldBeEnabled() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], - 'extensions' => [ - 'doctrine_ping_connection_extension' => true, + 'default' => [ + 'transport' => null, + 'extensions' => [ + 'doctrine_closed_entity_manager_extension' => true, + ], ], ]]); - $this->assertArraySubset([ - 'extensions' => [ - 'doctrine_ping_connection_extension' => true, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'doctrine_closed_entity_manager_extension' => true, + ], ], ], $config); } - public function testDoctrineClearIdentityMapExtensionShouldBeDisabledByDefault() + public function testResetServicesExtensionShouldBeDisabledByDefault() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], + 'default' => [ + 'transport' => null, + ], ]]); - $this->assertArraySubset([ - 'extensions' => [ - 'doctrine_clear_identity_map_extension' => false, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'reset_services_extension' => false, + ], ], ], $config); } - public function testDoctrineClearIdentityMapExtensionCouldBeEnabled() + public function testResetServicesExtensionCouldBeEnabled() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], - 'extensions' => [ - 'doctrine_clear_identity_map_extension' => true, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'reset_services_extension' => true, + ], ], ]]); - $this->assertArraySubset([ - 'extensions' => [ - 'doctrine_clear_identity_map_extension' => true, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'reset_services_extension' => true, + ], ], ], $config); } - public function testSignalExtensionShouldBeEnabledByDefault() + public function testSignalExtensionShouldBeEnabledIfPcntlExtensionIsLoaded() { - $configuration = new Configuration([], true); + $isLoaded = function_exists('pcntl_signal_dispatch'); + + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], + 'default' => [ + 'transport' => [], + ], ]]); - $this->assertArraySubset([ - 'extensions' => [ - 'signal_extension' => true, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'signal_extension' => $isLoaded, + ], ], ], $config); } public function testSignalExtensionCouldBeDisabled() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], - 'extensions' => [ - 'signal_extension' => false, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'signal_extension' => false, + ], ], ]]); - $this->assertArraySubset([ - 'extensions' => [ - 'signal_extension' => false, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'signal_extension' => false, + ], ], ], $config); } public function testReplyExtensionShouldBeEnabledByDefault() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], + 'default' => [ + 'transport' => [], + ], ]]); - $this->assertArraySubset([ - 'extensions' => [ - 'reply_extension' => true, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'reply_extension' => true, + ], ], ], $config); } public function testReplyExtensionCouldBeDisabled() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], - 'extensions' => [ - 'reply_extension' => false, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'reply_extension' => false, + ], ], ]]); - $this->assertArraySubset([ - 'extensions' => [ - 'reply_extension' => false, + Assert::assertArraySubset([ + 'default' => [ + 'extensions' => [ + 'reply_extension' => false, + ], ], ], $config); } public function testShouldDisableAsyncEventsByDefault() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], + 'default' => [ + 'transport' => [], + ], ]]); - $this->assertArraySubset([ - 'async_events' => [ - 'enabled' => false, + Assert::assertArraySubset([ + 'default' => [ + 'async_events' => [ + 'enabled' => false, + ], ], ], $config); } public function testShouldAllowEnableAsyncEvents() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], - 'async_events' => true, + 'default' => [ + 'transport' => [], + 'async_events' => true, + ], ]]); - $this->assertArraySubset([ - 'async_events' => [ - 'enabled' => true, + Assert::assertArraySubset([ + 'default' => [ + 'async_events' => [ + 'enabled' => true, + ], ], ], $config); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], - 'async_events' => [ - 'enabled' => true, + 'default' => [ + 'transport' => [], + 'async_events' => [ + 'enabled' => true, + ], ], ]]); - $this->assertArraySubset([ - 'async_events' => [ - 'enabled' => true, + Assert::assertArraySubset([ + 'default' => [ + 'async_events' => [ + 'enabled' => true, + ], ], ], $config); } public function testShouldSetDefaultConfigurationForConsumption() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], + 'default' => [ + 'transport' => [], + ], ]]); - $this->assertArraySubset([ - 'consumption' => [ - 'idle_timeout' => 0, - 'receive_timeout' => 100, + Assert::assertArraySubset([ + 'default' => [ + 'consumption' => [ + 'receive_timeout' => 10000, + ], ], ], $config); } public function testShouldAllowConfigureConsumption() { - $configuration = new Configuration([], true); + $configuration = new Configuration(true); $processor = new Processor(); $config = $processor->processConfiguration($configuration, [[ - 'transport' => [], - 'consumption' => [ - 'idle_timeout' => 123, - 'receive_timeout' => 456, + 'default' => [ + 'transport' => [], + 'consumption' => [ + 'receive_timeout' => 456, + ], ], ]]); - $this->assertArraySubset([ - 'consumption' => [ - 'idle_timeout' => 123, - 'receive_timeout' => 456, + Assert::assertArraySubset([ + 'default' => [ + 'consumption' => [ + 'receive_timeout' => 456, + ], ], ], $config); } + + private function assertConfigEquals(array $expected, array $actual): void + { + Assert::assertArraySubset($expected, $actual, false, var_export($actual, true)); + } } diff --git a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/EnqueueExtensionTest.php b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/EnqueueExtensionTest.php index 4a59dc35b..6358bd24d 100644 --- a/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/EnqueueExtensionTest.php +++ b/pkg/enqueue-bundle/Tests/Unit/DependencyInjection/EnqueueExtensionTest.php @@ -4,27 +4,17 @@ use Enqueue\Bundle\DependencyInjection\Configuration; use Enqueue\Bundle\DependencyInjection\EnqueueExtension; -use Enqueue\Bundle\Tests\Unit\Mocks\FooTransportFactory; -use Enqueue\Bundle\Tests\Unit\Mocks\TransportFactoryWithoutDriverFactory; use Enqueue\Client\CommandSubscriberInterface; use Enqueue\Client\Producer; use Enqueue\Client\ProducerInterface; use Enqueue\Client\TopicSubscriberInterface; use Enqueue\Client\TraceableProducer; -use Enqueue\Consumption\QueueConsumer; use Enqueue\JobQueue\JobRunner; -use Enqueue\Null\NullContext; -use Enqueue\Null\Symfony\NullTransportFactory; -use Enqueue\Symfony\DefaultTransportFactory; -use Enqueue\Symfony\MissingTransportFactory; -use Enqueue\Symfony\TransportFactoryInterface; use Enqueue\Test\ClassExtensionTrait; use PHPUnit\Framework\TestCase; -use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\HttpKernel\Kernel; class EnqueueExtensionTest extends TestCase { @@ -32,144 +22,61 @@ class EnqueueExtensionTest extends TestCase public function testShouldImplementConfigurationInterface() { - self::assertClassExtends(Extension::class, EnqueueExtension::class); + $this->assertClassExtends(Extension::class, EnqueueExtension::class); } - public function testCouldBeConstructedWithoutAnyArguments() + public function testShouldBeFinal() { - new EnqueueExtension(); + $this->assertClassFinal(EnqueueExtension::class); } - public function testShouldRegisterDefaultAndNullTransportFactoriesInConstructor() - { - $extension = new EnqueueExtension(); - - /** @var TransportFactoryInterface[] $factories */ - $factories = $this->readAttribute($extension, 'factories'); - - $this->assertInternalType('array', $factories); - $this->assertCount(2, $factories); - - $this->assertArrayHasKey('default', $factories); - $this->assertInstanceOf(DefaultTransportFactory::class, $factories['default']); - $this->assertEquals('default', $factories['default']->getName()); - - $this->assertArrayHasKey('null', $factories); - $this->assertInstanceOf(NullTransportFactory::class, $factories['null']); - $this->assertEquals('null', $factories['null']->getName()); - } - - public function testThrowIfTransportFactoryNameEmpty() - { - $extension = new EnqueueExtension(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Transport factory name cannot be empty'); - - $extension->addTransportFactory(new FooTransportFactory(null)); - } - - public function testThrowIfTransportFactoryWithSameNameAlreadyAdded() - { - $extension = new EnqueueExtension(); - - $extension->addTransportFactory(new FooTransportFactory('foo')); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Transport factory with such name already added. Name foo'); - - $extension->addTransportFactory(new FooTransportFactory('foo')); - } - - public function testShouldEnabledNullTransportAndSetItAsDefault() + public function testShouldRegisterConnectionFactory() { $container = $this->getContainerBuilder(true); $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [ - 'default' => 'null', - 'null' => true, + 'default' => [ + 'transport' => null, ], ]], $container); - self::assertTrue($container->hasAlias('enqueue.transport.default.context')); - self::assertEquals('enqueue.transport.null.context', (string) $container->getAlias('enqueue.transport.default.context')); - - self::assertTrue($container->hasDefinition('enqueue.transport.null.context')); - $context = $container->getDefinition('enqueue.transport.null.context'); - self::assertEquals(NullContext::class, $context->getClass()); - } - - public function testShouldUseNullTransportAsDefaultWhenExplicitlyConfigured() - { - $container = $this->getContainerBuilder(true); - - $extension = new EnqueueExtension(); - - $extension->load([[ - 'transport' => [ - 'default' => 'null', - 'null' => true, - ], - ]], $container); - - self::assertEquals( - 'enqueue.transport.default.context', - (string) $container->getAlias('enqueue.transport.context') - ); - self::assertEquals( - 'enqueue.transport.null.context', - (string) $container->getAlias('enqueue.transport.default.context') - ); + self::assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory')); + self::assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()); } - public function testShouldConfigureFooTransport() + public function testShouldRegisterContext() { $container = $this->getContainerBuilder(true); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'transport' => [ - 'default' => 'foo', - 'foo' => ['foo_param' => 'aParam'], + 'default' => [ + 'transport' => null, ], ]], $container); - self::assertTrue($container->hasDefinition('foo.connection_factory')); - self::assertTrue($container->hasDefinition('foo.context')); - self::assertFalse($container->hasDefinition('foo.driver')); - - $context = $container->getDefinition('foo.context'); - self::assertEquals(\stdClass::class, $context->getClass()); - self::assertEquals([['foo_param' => 'aParam']], $context->getArguments()); + self::assertTrue($container->hasDefinition('enqueue.transport.default.context')); + self::assertNotEmpty($container->getDefinition('enqueue.transport.default.context')->getFactory()); } - public function testShouldUseFooTransportAsDefault() + public function testShouldRegisterClientDriver() { $container = $this->getContainerBuilder(true); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'transport' => [ - 'default' => 'foo', - 'foo' => ['foo_param' => 'aParam'], + 'default' => [ + 'transport' => null, + 'client' => true, ], ]], $container); - self::assertEquals( - 'enqueue.transport.default.context', - (string) $container->getAlias('enqueue.transport.context') - ); - self::assertEquals( - 'enqueue.transport.foo.context', - (string) $container->getAlias('enqueue.transport.default.context') - ); + self::assertTrue($container->hasDefinition('enqueue.client.default.driver')); + self::assertNotEmpty($container->getDefinition('enqueue.client.default.driver')->getFactory()); } public function testShouldLoadClientServicesWhenEnabled() @@ -177,62 +84,33 @@ public function testShouldLoadClientServicesWhenEnabled() $container = $this->getContainerBuilder(true); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'client' => null, - 'transport' => [ - 'default' => 'foo', - 'foo' => [ - 'foo_param' => true, - ], + 'default' => [ + 'client' => null, + 'transport' => 'null:', ], ]], $container); - self::assertTrue($container->hasDefinition('foo.driver')); - self::assertTrue($container->hasDefinition('enqueue.client.config')); - self::assertTrue($container->hasDefinition(Producer::class)); + self::assertTrue($container->hasDefinition('enqueue.client.default.driver')); + self::assertTrue($container->hasDefinition('enqueue.client.default.config')); self::assertTrue($container->hasAlias(ProducerInterface::class)); } - public function testShouldNotCreateDriverIfFactoryDoesNotImplementDriverFactoryInterface() - { - $container = $this->getContainerBuilder(true); - - $extension = new EnqueueExtension(); - $extension->addTransportFactory(new TransportFactoryWithoutDriverFactory()); - - $extension->load([[ - 'client' => null, - 'transport' => [ - 'default' => 'without_driver', - 'without_driver' => [], - ], - ]], $container); - - self::assertTrue($container->hasDefinition('without_driver.context')); - self::assertTrue($container->hasDefinition('without_driver.connection_factory')); - self::assertFalse($container->hasDefinition('without_driver.driver')); - } - public function testShouldUseProducerByDefault() { $container = $this->getContainerBuilder(false); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'client' => null, - 'transport' => [ - 'default' => 'foo', - 'foo' => [ - 'foo_param' => true, - ], + 'default' => [ + 'client' => null, + 'transport' => 'null', ], ]], $container); - $producer = $container->getDefinition(Producer::class); + $producer = $container->getDefinition('enqueue.client.default.producer'); self::assertEquals(Producer::class, $producer->getClass()); } @@ -241,21 +119,17 @@ public function testShouldUseMessageProducerIfTraceableProducerOptionSetToFalseE $container = $this->getContainerBuilder(false); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'client' => [ - 'traceable_producer' => false, - ], - 'transport' => [ - 'default' => 'foo', - 'foo' => [ - 'foo_param' => true, + 'default' => [ + 'client' => [ + 'traceable_producer' => false, ], + 'transport' => 'null:', ], ]], $container); - $producer = $container->getDefinition(Producer::class); + $producer = $container->getDefinition('enqueue.client.default.producer'); self::assertEquals(Producer::class, $producer->getClass()); } @@ -264,32 +138,24 @@ public function testShouldUseTraceableMessageProducerIfDebugEnabled() $container = $this->getContainerBuilder(true); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'transport' => [ - 'default' => 'foo', - 'foo' => [ - 'foo_param' => true, - ], + 'default' => [ + 'transport' => 'null:', + 'client' => null, ], - 'client' => null, ]], $container); - $producer = $container->getDefinition(TraceableProducer::class); + $producer = $container->getDefinition('enqueue.client.default.traceable_producer'); self::assertEquals(TraceableProducer::class, $producer->getClass()); self::assertEquals( - [Producer::class, null, 0], + ['enqueue.client.default.producer', null, 0], $producer->getDecoratedService() ); self::assertInstanceOf(Reference::class, $producer->getArgument(0)); - $innerServiceName = sprintf('%s.inner', TraceableProducer::class); - if (30300 > Kernel::VERSION_ID) { - // Symfony 3.2 and below make service identifiers lowercase, so we do the same. - $innerServiceName = strtolower($innerServiceName); - } + $innerServiceName = 'enqueue.client.default.traceable_producer.inner'; self::assertEquals( $innerServiceName, @@ -302,18 +168,14 @@ public function testShouldNotUseTraceableMessageProducerIfDebugDisabledAndNotSet $container = $this->getContainerBuilder(false); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'transport' => [ - 'default' => 'foo', - 'foo' => [ - 'foo_param' => true, - ], + 'default' => [ + 'transport' => 'null:', ], ]], $container); - $this->assertFalse($container->hasDefinition(TraceableProducer::class)); + $this->assertFalse($container->hasDefinition('enqueue.client.default.traceable_producer')); } public function testShouldUseTraceableMessageProducerIfDebugDisabledButTraceableProducerOptionSetToTrueExplicitly() @@ -321,34 +183,26 @@ public function testShouldUseTraceableMessageProducerIfDebugDisabledButTraceable $container = $this->getContainerBuilder(false); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'client' => [ - 'traceable_producer' => true, - ], - 'transport' => [ - 'default' => 'foo', - 'foo' => [ - 'foo_param' => true, + 'default' => [ + 'client' => [ + 'traceable_producer' => true, ], + 'transport' => 'null:', ], ]], $container); - $producer = $container->getDefinition(TraceableProducer::class); + $producer = $container->getDefinition('enqueue.client.default.traceable_producer'); self::assertEquals(TraceableProducer::class, $producer->getClass()); self::assertEquals( - [Producer::class, null, 0], + ['enqueue.client.default.producer', null, 0], $producer->getDecoratedService() ); self::assertInstanceOf(Reference::class, $producer->getArgument(0)); - $innerServiceName = sprintf('%s.inner', TraceableProducer::class); - if (30300 > Kernel::VERSION_ID) { - // Symfony 3.2 and below make service identifiers lowercase, so we do the same. - $innerServiceName = strtolower($innerServiceName); - } + $innerServiceName = 'enqueue.client.default.traceable_producer.inner'; self::assertEquals( $innerServiceName, @@ -361,21 +215,17 @@ public function testShouldLoadDelayRedeliveredMessageExtensionIfRedeliveredDelay $container = $this->getContainerBuilder(true); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'transport' => [ - 'default' => 'foo', - 'foo' => [ - 'foo_param' => true, + 'default' => [ + 'transport' => 'null:', + 'client' => [ + 'redelivered_delay_time' => 12345, ], ], - 'client' => [ - 'redelivered_delay_time' => 12345, - ], ]], $container); - $extension = $container->getDefinition('enqueue.client.delay_redelivered_message_extension'); + $extension = $container->getDefinition('enqueue.client.default.delay_redelivered_message_extension'); self::assertEquals(12345, $extension->getArgument(1)); } @@ -385,21 +235,17 @@ public function testShouldNotLoadDelayRedeliveredMessageExtensionIfRedeliveredDe $container = $this->getContainerBuilder(true); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new FooTransportFactory()); $extension->load([[ - 'transport' => [ - 'default' => 'foo', - 'foo' => [ - 'foo_param' => true, + 'default' => [ + 'transport' => 'null:', + 'client' => [ + 'redelivered_delay_time' => 0, ], ], - 'client' => [ - 'redelivered_delay_time' => 0, - ], ]], $container); - $this->assertFalse($container->hasDefinition('enqueue.client.delay_redelivered_message_extension')); + $this->assertFalse($container->hasDefinition('enqueue.client.default.delay_redelivered_message_extension')); } public function testShouldLoadJobServicesIfEnabled() @@ -409,13 +255,33 @@ public function testShouldLoadJobServicesIfEnabled() $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'job' => true, + 'default' => [ + 'transport' => [], + 'client' => null, + 'job' => true, + ], ]], $container); self::assertTrue($container->hasDefinition(JobRunner::class)); } + public function testShouldThrowExceptionIfClientIsNotEnabledOnJobLoad() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Client is required for job-queue.'); + + $container = $this->getContainerBuilder(true); + + $extension = new EnqueueExtension(); + + $extension->load([[ + 'default' => [ + 'transport' => [], + 'job' => true, + ], + ]], $container); + } + public function testShouldNotLoadJobServicesIfDisabled() { $container = $this->getContainerBuilder(true); @@ -423,8 +289,10 @@ public function testShouldNotLoadJobServicesIfDisabled() $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'job' => false, + 'default' => [ + 'transport' => [], + 'job' => false, + ], ]], $container); self::assertFalse($container->hasDefinition(JobRunner::class)); @@ -446,9 +314,11 @@ public function testShouldLoadDoctrinePingConnectionExtensionServiceIfEnabled() $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'extensions' => [ - 'doctrine_ping_connection_extension' => true, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_ping_connection_extension' => true, + ], ], ]], $container); @@ -462,9 +332,11 @@ public function testShouldNotLoadDoctrinePingConnectionExtensionServiceIfDisable $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'extensions' => [ - 'doctrine_ping_connection_extension' => false, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_ping_connection_extension' => false, + ], ], ]], $container); @@ -478,9 +350,11 @@ public function testShouldLoadDoctrineClearIdentityMapExtensionServiceIfEnabled( $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'extensions' => [ - 'doctrine_clear_identity_map_extension' => true, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_clear_identity_map_extension' => true, + ], ], ]], $container); @@ -494,15 +368,125 @@ public function testShouldNotLoadDoctrineClearIdentityMapExtensionServiceIfDisab $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'extensions' => [ - 'doctrine_clear_identity_map_extension' => false, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_clear_identity_map_extension' => false, + ], ], ]], $container); self::assertFalse($container->hasDefinition('enqueue.consumption.doctrine_clear_identity_map_extension')); } + public function testShouldLoadDoctrineOdmClearIdentityMapExtensionServiceIfEnabled() + { + $container = $this->getContainerBuilder(true); + + $extension = new EnqueueExtension(); + + $extension->load([[ + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_odm_clear_identity_map_extension' => true, + ], + ], + ]], $container); + + self::assertTrue($container->hasDefinition('enqueue.consumption.doctrine_odm_clear_identity_map_extension')); + } + + public function testShouldNotLoadDoctrineOdmClearIdentityMapExtensionServiceIfDisabled() + { + $container = $this->getContainerBuilder(true); + + $extension = new EnqueueExtension(); + + $extension->load([[ + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_odm_clear_identity_map_extension' => false, + ], + ], + ]], $container); + + self::assertFalse($container->hasDefinition('enqueue.consumption.doctrine_odm_clear_identity_map_extension')); + } + + public function testShouldLoadDoctrineClosedEntityManagerExtensionServiceIfEnabled() + { + $container = $this->getContainerBuilder(true); + + $extension = new EnqueueExtension(); + + $extension->load([[ + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_closed_entity_manager_extension' => true, + ], + ], + ]], $container); + + self::assertTrue($container->hasDefinition('enqueue.consumption.doctrine_closed_entity_manager_extension')); + } + + public function testShouldNotLoadDoctrineClosedEntityManagerExtensionServiceIfDisabled() + { + $container = $this->getContainerBuilder(true); + + $extension = new EnqueueExtension(); + + $extension->load([[ + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'doctrine_closed_entity_manager_extension' => false, + ], + ], + ]], $container); + + self::assertFalse($container->hasDefinition('enqueue.consumption.doctrine_closed_entity_manager_extension')); + } + + public function testShouldLoadResetServicesExtensionServiceIfEnabled() + { + $container = $this->getContainerBuilder(true); + + $extension = new EnqueueExtension(); + + $extension->load([[ + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'reset_services_extension' => true, + ], + ], + ]], $container); + + self::assertTrue($container->hasDefinition('enqueue.consumption.reset_services_extension')); + } + + public function testShouldNotLoadResetServicesExtensionServiceIfDisabled() + { + $container = $this->getContainerBuilder(true); + + $extension = new EnqueueExtension(); + + $extension->load([[ + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'reset_services_extension' => false, + ], + ], + ]], $container); + + self::assertFalse($container->hasDefinition('enqueue.consumption.reset_services_extension')); + } + public function testShouldLoadSignalExtensionServiceIfEnabled() { $container = $this->getContainerBuilder(true); @@ -510,9 +494,11 @@ public function testShouldLoadSignalExtensionServiceIfEnabled() $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'extensions' => [ - 'signal_extension' => true, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'signal_extension' => true, + ], ], ]], $container); @@ -526,9 +512,11 @@ public function testShouldNotLoadSignalExtensionServiceIfDisabled() $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'extensions' => [ - 'signal_extension' => false, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'signal_extension' => false, + ], ], ]], $container); @@ -542,9 +530,11 @@ public function testShouldLoadReplyExtensionServiceIfEnabled() $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'extensions' => [ - 'reply_extension' => true, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'reply_extension' => true, + ], ], ]], $container); @@ -558,9 +548,11 @@ public function testShouldNotLoadReplyExtensionServiceIfDisabled() $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [], - 'extensions' => [ - 'reply_extension' => false, + 'default' => [ + 'transport' => [], + 'extensions' => [ + 'reply_extension' => false, + ], ], ]], $container); @@ -601,62 +593,92 @@ public function testShouldConfigureQueueConsumer() $extension = new EnqueueExtension(); $extension->load([[ - 'client' => [], - 'transport' => [ - ], - 'consumption' => [ - 'idle_timeout' => 123, - 'receive_timeout' => 456, + 'default' => [ + 'client' => [], + 'transport' => [ + ], + 'consumption' => [ + 'receive_timeout' => 456, + ], ], ]], $container); - $def = $container->getDefinition(QueueConsumer::class); - $this->assertSame(123, $def->getArgument(2)); - $this->assertSame(456, $def->getArgument(3)); + $def = $container->getDefinition('enqueue.transport.default.queue_consumer'); + $this->assertSame('%enqueue.transport.default.receive_timeout%', $def->getArgument(4)); + + $this->assertSame(456, $container->getParameter('enqueue.transport.default.receive_timeout')); - $def = $container->getDefinition('enqueue.client.queue_consumer'); - $this->assertSame(123, $def->getArgument(2)); - $this->assertSame(456, $def->getArgument(3)); + $def = $container->getDefinition('enqueue.client.default.queue_consumer'); + $this->assertSame(456, $def->getArgument(4)); } - public function testShouldThrowIfPackageShouldBeInstalledToUseTransport() + public function testShouldSetPropertyWithAllConfiguredTransports() { $container = $this->getContainerBuilder(true); $extension = new EnqueueExtension(); - $extension->addTransportFactory(new MissingTransportFactory('need_package', ['a_package', 'another_package'])); + $extension->load([[ + 'default' => [ + 'transport' => 'default:', + 'client' => [], + ], + 'foo' => [ + 'transport' => 'foo:', + 'client' => [], + ], + 'bar' => [ + 'transport' => 'bar:', + 'client' => [], + ], + ]], $container); - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('In order to use the transport "need_package" install'); + $this->assertTrue($container->hasParameter('enqueue.transports')); + $this->assertEquals(['default', 'foo', 'bar'], $container->getParameter('enqueue.transports')); + } + + public function testShouldSetPropertyWithAllConfiguredClients() + { + $container = $this->getContainerBuilder(true); + + $extension = new EnqueueExtension(); $extension->load([[ - 'transport' => [ - 'need_package' => true, + 'default' => [ + 'transport' => 'default:', + 'client' => [], + ], + 'foo' => [ + 'transport' => 'foo:', + ], + 'bar' => [ + 'transport' => 'bar:', + 'client' => [], ], ]], $container); + + $this->assertTrue($container->hasParameter('enqueue.clients')); + $this->assertEquals(['default', 'bar'], $container->getParameter('enqueue.clients')); } public function testShouldLoadProcessAutoconfigureChildDefinition() { - if (30300 >= Kernel::VERSION_ID) { - $this->markTestSkipped('The autoconfigure feature is available since Symfony 3.3 version'); - } - $container = $this->getContainerBuilder(true); $extension = new EnqueueExtension(); $extension->load([[ - 'client' => [], - 'transport' => [], + 'default' => [ + 'client' => [], + 'transport' => [], + ], ]], $container); $autoconfigured = $container->getAutoconfiguredInstanceof(); self::assertArrayHasKey(CommandSubscriberInterface::class, $autoconfigured); - self::assertTrue($autoconfigured[CommandSubscriberInterface::class]->hasTag('enqueue.client.processor')); + self::assertTrue($autoconfigured[CommandSubscriberInterface::class]->hasTag('enqueue.command_subscriber')); self::assertTrue($autoconfigured[CommandSubscriberInterface::class]->isPublic()); self::assertArrayHasKey(TopicSubscriberInterface::class, $autoconfigured); - self::assertTrue($autoconfigured[TopicSubscriberInterface::class]->hasTag('enqueue.client.processor')); + self::assertTrue($autoconfigured[TopicSubscriberInterface::class]->hasTag('enqueue.topic_subscriber')); self::assertTrue($autoconfigured[TopicSubscriberInterface::class]->isPublic()); } diff --git a/pkg/enqueue-bundle/Tests/Unit/EnqueueBundleTest.php b/pkg/enqueue-bundle/Tests/Unit/EnqueueBundleTest.php index 270c4d315..7d5b0232b 100644 --- a/pkg/enqueue-bundle/Tests/Unit/EnqueueBundleTest.php +++ b/pkg/enqueue-bundle/Tests/Unit/EnqueueBundleTest.php @@ -2,27 +2,9 @@ namespace Enqueue\Bundle\Tests\Unit; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildClientExtensionsPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildClientRoutingPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildConsumptionExtensionsPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildExclusiveCommandsExtensionPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildProcessorRegistryPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildQueueMetaRegistryPass; -use Enqueue\Bundle\DependencyInjection\Compiler\BuildTopicMetaSubscribersPass; -use Enqueue\Bundle\DependencyInjection\EnqueueExtension; use Enqueue\Bundle\EnqueueBundle; -use Enqueue\Dbal\Symfony\DbalTransportFactory; -use Enqueue\Fs\Symfony\FsTransportFactory; -use Enqueue\Gps\Symfony\GpsTransportFactory; -use Enqueue\Redis\Symfony\RedisTransportFactory; -use Enqueue\Sqs\Symfony\SqsTransportFactory; -use Enqueue\Stomp\Symfony\RabbitMqStompTransportFactory; -use Enqueue\Stomp\Symfony\StompTransportFactory; -use Enqueue\Symfony\AmqpTransportFactory; -use Enqueue\Symfony\RabbitMqAmqpTransportFactory; use Enqueue\Test\ClassExtensionTrait; use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class EnqueueBundleTest extends TestCase @@ -33,214 +15,4 @@ public function testShouldExtendBundleClass() { $this->assertClassExtends(Bundle::class, EnqueueBundle::class); } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new EnqueueBundle(); - } - - public function testShouldRegisterExpectedCompilerPasses() - { - $extensionMock = $this->createMock(EnqueueExtension::class); - - $container = $this->createMock(ContainerBuilder::class); - $container - ->expects($this->at(0)) - ->method('addCompilerPass') - ->with($this->isInstanceOf(BuildConsumptionExtensionsPass::class)) - ; - $container - ->expects($this->at(1)) - ->method('addCompilerPass') - ->with($this->isInstanceOf(BuildClientRoutingPass::class)) - ; - $container - ->expects($this->at(2)) - ->method('addCompilerPass') - ->with($this->isInstanceOf(BuildProcessorRegistryPass::class)) - ; - $container - ->expects($this->at(3)) - ->method('addCompilerPass') - ->with($this->isInstanceOf(BuildTopicMetaSubscribersPass::class)) - ; - $container - ->expects($this->at(4)) - ->method('addCompilerPass') - ->with($this->isInstanceOf(BuildQueueMetaRegistryPass::class)) - ; - $container - ->expects($this->at(5)) - ->method('addCompilerPass') - ->with($this->isInstanceOf(BuildClientExtensionsPass::class)) - ; - $container - ->expects($this->at(6)) - ->method('addCompilerPass') - ->with($this->isInstanceOf(BuildExclusiveCommandsExtensionPass::class)) - ; - $container - ->expects($this->at(7)) - ->method('getExtension') - ->willReturn($extensionMock) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - - public function testShouldRegisterStompAndRabbitMqStompTransportFactories() - { - $extensionMock = $this->createEnqueueExtensionMock(); - - $container = new ContainerBuilder(); - $container->registerExtension($extensionMock); - - $extensionMock - ->expects($this->at(0)) - ->method('setTransportFactory') - ->with($this->isInstanceOf(StompTransportFactory::class)) - ; - $extensionMock - ->expects($this->at(1)) - ->method('setTransportFactory') - ->with($this->isInstanceOf(RabbitMqStompTransportFactory::class)) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - - public function testShouldRegisterAmqpAndRabbitMqAmqpTransportFactories() - { - $extensionMock = $this->createEnqueueExtensionMock(); - - $container = new ContainerBuilder(); - $container->registerExtension($extensionMock); - - $extensionMock - ->expects($this->at(2)) - ->method('setTransportFactory') - ->with($this->isInstanceOf(AmqpTransportFactory::class)) - ->willReturnCallback(function (AmqpTransportFactory $factory) { - $this->assertSame('amqp', $factory->getName()); - }) - ; - $extensionMock - ->expects($this->at(3)) - ->method('setTransportFactory') - ->with($this->isInstanceOf(RabbitMqAmqpTransportFactory::class)) - ->willReturnCallback(function (RabbitMqAmqpTransportFactory $factory) { - $this->assertSame('rabbitmq_amqp', $factory->getName()); - }) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - - public function testShouldRegisterFSTransportFactory() - { - $extensionMock = $this->createEnqueueExtensionMock(); - - $container = new ContainerBuilder(); - $container->registerExtension($extensionMock); - - $extensionMock - ->expects($this->at(4)) - ->method('setTransportFactory') - ->with($this->isInstanceOf(FsTransportFactory::class)) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - - public function testShouldRegisterRedisTransportFactory() - { - $extensionMock = $this->createEnqueueExtensionMock(); - - $container = new ContainerBuilder(); - $container->registerExtension($extensionMock); - - $extensionMock - ->expects($this->at(5)) - ->method('setTransportFactory') - ->with($this->isInstanceOf(RedisTransportFactory::class)) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - - public function testShouldRegisterDbalTransportFactory() - { - $extensionMock = $this->createEnqueueExtensionMock(); - - $container = new ContainerBuilder(); - $container->registerExtension($extensionMock); - - $extensionMock - ->expects($this->at(6)) - ->method('setTransportFactory') - ->with($this->isInstanceOf(DbalTransportFactory::class)) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - - public function testShouldRegisterSqsTransportFactory() - { - $extensionMock = $this->createEnqueueExtensionMock(); - - $container = new ContainerBuilder(); - $container->registerExtension($extensionMock); - - $extensionMock - ->expects($this->at(7)) - ->method('setTransportFactory') - ->with($this->isInstanceOf(SqsTransportFactory::class)) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - - public function testShouldRegisterGpsTransportFactory() - { - $extensionMock = $this->createEnqueueExtensionMock(); - - $container = new ContainerBuilder(); - $container->registerExtension($extensionMock); - - $extensionMock - ->expects($this->at(8)) - ->method('setTransportFactory') - ->with($this->isInstanceOf(GpsTransportFactory::class)) - ; - - $bundle = new EnqueueBundle(); - $bundle->build($container); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|EnqueueExtension - */ - private function createEnqueueExtensionMock() - { - $extensionMock = $this->createMock(EnqueueExtension::class); - $extensionMock - ->expects($this->once()) - ->method('getAlias') - ->willReturn('enqueue') - ; - $extensionMock - ->expects($this->once()) - ->method('getNamespace') - ->willReturn(false) - ; - - return $extensionMock; - } } diff --git a/pkg/enqueue-bundle/Tests/Unit/Mocks/FooTransportFactory.php b/pkg/enqueue-bundle/Tests/Unit/Mocks/FooTransportFactory.php deleted file mode 100644 index 5507d193d..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/Mocks/FooTransportFactory.php +++ /dev/null @@ -1,83 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->children() - ->scalarNode('foo_param')->isRequired()->cannotBeEmpty()->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factoryId = 'foo.connection_factory'; - - $container->setDefinition($factoryId, new Definition(\stdClass::class, [$config])); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $contextId = 'foo.context'; - - $context = new Definition(\stdClass::class, [$config]); - $context->setPublic(true); - - $container->setDefinition($contextId, $context); - - return $contextId; - } - - public function createDriver(ContainerBuilder $container, array $config) - { - $driverId = 'foo.driver'; - - $driver = new Definition(\stdClass::class, [$config]); - $driver->setPublic(true); - - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/Mocks/TransportFactoryWithoutDriverFactory.php b/pkg/enqueue-bundle/Tests/Unit/Mocks/TransportFactoryWithoutDriverFactory.php deleted file mode 100644 index f3a003201..000000000 --- a/pkg/enqueue-bundle/Tests/Unit/Mocks/TransportFactoryWithoutDriverFactory.php +++ /dev/null @@ -1,71 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factoryId = 'without_driver.connection_factory'; - - $container->setDefinition($factoryId, new Definition(\stdClass::class, [$config])); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $contextId = 'without_driver.context'; - - $context = new Definition(\stdClass::class, [$config]); - $context->setPublic(true); - - $container->setDefinition($contextId, $context); - - return $contextId; - } - - public function createDriver(ContainerBuilder $container, array $config) - { - throw new \LogicException('It should not be called. The method will be removed'); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/enqueue-bundle/Tests/Unit/Profiler/MessageQueueCollectorTest.php b/pkg/enqueue-bundle/Tests/Unit/Profiler/MessageQueueCollectorTest.php index 62f7d6c1c..d6d638d75 100644 --- a/pkg/enqueue-bundle/Tests/Unit/Profiler/MessageQueueCollectorTest.php +++ b/pkg/enqueue-bundle/Tests/Unit/Profiler/MessageQueueCollectorTest.php @@ -2,11 +2,13 @@ namespace Enqueue\Bundle\Tests\Unit\Profiler; +use DMS\PHPUnitExtensions\ArraySubset\Assert; use Enqueue\Bundle\Profiler\MessageQueueCollector; use Enqueue\Client\MessagePriority; use Enqueue\Client\ProducerInterface; use Enqueue\Client\TraceableProducer; use Enqueue\Test\ClassExtensionTrait; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -21,59 +23,105 @@ public function testShouldExtendDataCollectorClass() $this->assertClassExtends(DataCollector::class, MessageQueueCollector::class); } - public function testCouldBeConstructedWithMessageProducerAsFirstArgument() - { - new MessageQueueCollector($this->createProducerMock()); - } - public function testShouldReturnExpectedName() { - $collector = new MessageQueueCollector($this->createProducerMock()); + $collector = new MessageQueueCollector(); $this->assertEquals('enqueue.message_queue', $collector->getName()); } public function testShouldReturnEmptySentMessageArrayIfNotTraceableProducer() { - $collector = new MessageQueueCollector($this->createProducerMock()); + $collector = new MessageQueueCollector(); + $collector->addProducer('default', $this->createProducerMock()); $collector->collect(new Request(), new Response()); $this->assertSame([], $collector->getSentMessages()); } - public function testShouldReturnSentMessageArrayTakenFromTraceableProducer() + public function testShouldReturnSentMessageArrayTakenFromTraceableProducers() { - $producerMock = $this->createTraceableProducerMock(); - $producerMock - ->expects($this->once()) - ->method('getTraces') - ->willReturn([['foo'], ['bar']]); + $producer1 = new TraceableProducer($this->createProducerMock()); + $producer1->sendEvent('fooTopic1', 'fooMessage'); + $producer1->sendCommand('barCommand1', 'barMessage'); + + $producer2 = new TraceableProducer($this->createProducerMock()); + $producer2->sendEvent('fooTopic2', 'fooMessage'); - $collector = new MessageQueueCollector($producerMock); + $collector = new MessageQueueCollector(); + $collector->addProducer('foo', $producer1); + $collector->addProducer('bar', $producer2); $collector->collect(new Request(), new Response()); - $this->assertSame([['foo'], ['bar']], $collector->getSentMessages()); + Assert::assertArraySubset( + [ + 'foo' => [ + [ + 'topic' => 'fooTopic1', + 'command' => null, + 'body' => 'fooMessage', + 'headers' => [], + 'properties' => [], + 'priority' => null, + 'expire' => null, + 'delay' => null, + 'timestamp' => null, + 'contentType' => null, + 'messageId' => null, + ], + [ + 'topic' => null, + 'command' => 'barCommand1', + 'body' => 'barMessage', + 'headers' => [], + 'properties' => [], + 'priority' => null, + 'expire' => null, + 'delay' => null, + 'timestamp' => null, + 'contentType' => null, + 'messageId' => null, + ], + ], + 'bar' => [ + [ + 'topic' => 'fooTopic2', + 'command' => null, + 'body' => 'fooMessage', + 'headers' => [], + 'properties' => [], + 'priority' => null, + 'expire' => null, + 'delay' => null, + 'timestamp' => null, + 'contentType' => null, + 'messageId' => null, + ], + ], + ], + $collector->getSentMessages() + ); } public function testShouldPrettyPrintKnownPriority() { - $collector = new MessageQueueCollector($this->createProducerMock()); + $collector = new MessageQueueCollector(); $this->assertEquals('normal', $collector->prettyPrintPriority(MessagePriority::NORMAL)); } public function testShouldPrettyPrintUnknownPriority() { - $collector = new MessageQueueCollector($this->createProducerMock()); + $collector = new MessageQueueCollector(); $this->assertEquals('unknownPriority', $collector->prettyPrintPriority('unknownPriority')); } public function testShouldEnsureStringKeepStringSame() { - $collector = new MessageQueueCollector($this->createProducerMock()); + $collector = new MessageQueueCollector(); $this->assertEquals('foo', $collector->ensureString('foo')); $this->assertEquals('bar baz', $collector->ensureString('bar baz')); @@ -81,13 +129,13 @@ public function testShouldEnsureStringKeepStringSame() public function testShouldEnsureStringEncodeArrayToJson() { - $collector = new MessageQueueCollector($this->createProducerMock()); + $collector = new MessageQueueCollector(); $this->assertEquals('["foo","bar"]', $collector->ensureString(['foo', 'bar'])); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface + * @return MockObject|ProducerInterface */ protected function createProducerMock() { @@ -95,7 +143,7 @@ protected function createProducerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|TraceableProducer + * @return MockObject|TraceableProducer */ protected function createTraceableProducerMock() { diff --git a/pkg/enqueue-bundle/Tests/fix_composer_json.php b/pkg/enqueue-bundle/Tests/fix_composer_json.php index fc430e276..5c80237ea 100644 --- a/pkg/enqueue-bundle/Tests/fix_composer_json.php +++ b/pkg/enqueue-bundle/Tests/fix_composer_json.php @@ -4,6 +4,7 @@ $composerJson = json_decode(file_get_contents(__DIR__.'/../composer.json'), true); -$composerJson['config']['platform']['ext-amqp'] = '1.7'; +$composerJson['config']['platform']['ext-amqp'] = '1.9.3'; +$composerJson['config']['platform']['ext-mongo'] = '1.6.14'; -file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, JSON_PRETTY_PRINT)); +file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, \JSON_PRETTY_PRINT)); diff --git a/pkg/enqueue-bundle/composer.json b/pkg/enqueue-bundle/composer.json index 8890feb0c..99d237bf6 100644 --- a/pkg/enqueue-bundle/composer.json +++ b/pkg/enqueue-bundle/composer.json @@ -6,11 +6,12 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "symfony/framework-bundle": "^2.8|^3|^4", - "enqueue/enqueue": "^0.8.35@dev", - "enqueue/null": "^0.8@dev", - "enqueue/async-event-dispatcher": "^0.8@dev" + "php": "^8.1", + "symfony/framework-bundle": "^6.2|^7.0", + "queue-interop/amqp-interop": "^0.8.2", + "queue-interop/queue-interop": "^0.8", + "enqueue/enqueue": "^0.10", + "enqueue/null": "^0.10" }, "support": { "email": "opensource@forma-pro.com", @@ -20,23 +21,32 @@ "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" }, "require-dev": { - "phpunit/phpunit": "~5.5", - "enqueue/stomp": "^0.8@dev", - "enqueue/amqp-ext": "^0.8@dev", - "php-amqplib/php-amqplib": "^2.7@dev", - "enqueue/amqp-lib": "^0.8@dev", - "enqueue/amqp-bunny": "^0.8@dev", - "enqueue/job-queue": "^0.8@dev", - "enqueue/fs": "^0.8@dev", - "enqueue/redis": "^0.8@dev", - "enqueue/dbal": "^0.8@dev", - "enqueue/sqs": "^0.8@dev", - "enqueue/gps": "^0.8@dev", - "enqueue/test": "^0.8@dev", - "doctrine/doctrine-bundle": "~1.2", - "symfony/monolog-bundle": "^2.8|^3|^4", - "symfony/browser-kit": "^2.8|^3|^4", - "symfony/expression-language": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/stomp": "0.10.x-dev", + "enqueue/amqp-ext": "0.10.x-dev", + "enqueue/amqp-lib": "0.10.x-dev", + "enqueue/amqp-bunny": "0.10.x-dev", + "enqueue/job-queue": "0.10.x-dev", + "enqueue/fs": "0.10.x-dev", + "enqueue/redis": "0.10.x-dev", + "enqueue/dbal": "0.10.x-dev", + "enqueue/sqs": "0.10.x-dev", + "enqueue/gps": "0.10.x-dev", + "enqueue/test": "0.10.x-dev", + "enqueue/async-event-dispatcher": "0.10.x-dev", + "enqueue/async-command": "0.10.x-dev", + "php-amqplib/php-amqplib": "^3.0", + "doctrine/doctrine-bundle": "^2.3.2", + "doctrine/mongodb-odm-bundle": "^3.5|^4.3|^5.0", + "alcaeus/mongo-php-adapter": "^1.0", + "symfony/browser-kit": "^6.2|^7.0", + "symfony/expression-language": "^6.2|^7.0", + "symfony/validator": "^6.2|^7.0", + "symfony/yaml": "^6.2|^7.0" + }, + "suggest": { + "enqueue/async-command": "If want to run Symfony command via message queue", + "enqueue/async-event-dispatcher": "If you want dispatch and process events asynchronously" }, "autoload": { "psr-4": { "Enqueue\\Bundle\\": "" }, @@ -46,7 +56,12 @@ }, "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } } } diff --git a/pkg/enqueue-bundle/phpunit.xml.dist b/pkg/enqueue-bundle/phpunit.xml.dist index ac0770ea9..974d2c3f5 100644 --- a/pkg/enqueue-bundle/phpunit.xml.dist +++ b/pkg/enqueue-bundle/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/enqueue/.github/workflows/ci.yml b/pkg/enqueue/.github/workflows/ci.yml new file mode 100644 index 000000000..28a46e908 --- /dev/null +++ b/pkg/enqueue/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: mongodb + + - run: php Tests/fix_composer_json.php + + - uses: "ramsey/composer-install@v1" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/enqueue/.travis.yml b/pkg/enqueue/.travis.yml deleted file mode 100644 index 2c830d0a6..000000000 --- a/pkg/enqueue/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '7.1' - -services: - - mongodb - -before_install: - - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - -cache: - directories: - - $HOME/.composer/cache - -install: - - php Tests/fix_composer_json.php - - composer self-update - - composer install - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/enqueue/Client/ArrayProcessorRegistry.php b/pkg/enqueue/ArrayProcessorRegistry.php similarity index 55% rename from pkg/enqueue/Client/ArrayProcessorRegistry.php rename to pkg/enqueue/ArrayProcessorRegistry.php index d6cb300d0..592908c51 100644 --- a/pkg/enqueue/Client/ArrayProcessorRegistry.php +++ b/pkg/enqueue/ArrayProcessorRegistry.php @@ -1,37 +1,33 @@ processors = $processors; + $this->processors = []; + array_walk($processors, function (Processor $processor, string $key) { + $this->processors[$key] = $processor; + }); } - /** - * @param string $name - * @param PsrProcessor $processor - */ - public function add($name, PsrProcessor $processor) + public function add(string $name, Processor $processor): void { $this->processors[$name] = $processor; } - /** - * {@inheritdoc} - */ - public function get($processorName) + public function get(string $processorName): Processor { if (false == isset($this->processors[$processorName])) { throw new \LogicException(sprintf('Processor was not found. processorName: "%s"', $processorName)); diff --git a/pkg/enqueue/Client/Amqp/AmqpDriver.php b/pkg/enqueue/Client/Amqp/AmqpDriver.php deleted file mode 100644 index e8b5871d3..000000000 --- a/pkg/enqueue/Client/Amqp/AmqpDriver.php +++ /dev/null @@ -1,205 +0,0 @@ -context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $topic = $this->createRouterTopic(); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($topic, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $log = function ($text, ...$args) use ($logger) { - $logger->debug(sprintf('[AmqpDriver] '.$text, ...$args)); - }; - - // setup router - $routerTopic = $this->createRouterTopic(); - $routerQueue = $this->createQueue($this->config->getRouterQueueName()); - - $log('Declare router exchange: %s', $routerTopic->getTopicName()); - $this->context->declareTopic($routerTopic); - $log('Declare router queue: %s', $routerQueue->getQueueName()); - $this->context->declareQueue($routerQueue); - $log('Bind router queue to exchange: %s -> %s', $routerQueue->getQueueName(), $routerTopic->getTopicName()); - $this->context->bind(new AmqpBind($routerTopic, $routerQueue, $routerQueue->getQueueName())); - - // setup queues - foreach ($this->queueMetaRegistry->getQueuesMeta() as $meta) { - $queue = $this->createQueue($meta->getClientName()); - - $log('Declare processor queue: %s', $queue->getQueueName()); - $this->context->declareQueue($queue); - } - } - - /** - * {@inheritdoc} - * - * @return AmqpQueue - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - $queue = $this->context->createQueue($transportName); - $queue->addFlag(AmqpQueue::FLAG_DURABLE); - - return $queue; - } - - /** - * {@inheritdoc} - * - * @return AmqpMessage - */ - public function createTransportMessage(Message $message) - { - $headers = $message->getHeaders(); - $properties = $message->getProperties(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($properties); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - $transportMessage->setContentType($message->getContentType()); - $transportMessage->setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT); - - if ($message->getExpire()) { - $transportMessage->setExpiration((string) ($message->getExpire() * 1000)); - } - - return $transportMessage; - } - - /** - * @param AmqpMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - $clientMessage->setContentType($message->getContentType()); - - if ($expiration = $message->getExpiration()) { - if (false == is_numeric($expiration)) { - throw new \LogicException(sprintf('expiration header is not numeric. "%s"', $expiration)); - } - - $clientMessage->setExpire((int) ((int) $expiration) / 1000); - } - - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - return $clientMessage; - } - - /** - * @return Config - */ - public function getConfig() - { - return $this->config; - } - - /** - * @return AmqpTopic - */ - private function createRouterTopic() - { - $topic = $this->context->createTopic( - $this->config->createTransportRouterTopicName($this->config->getRouterTopicName()) - ); - $topic->setType(AmqpTopic::TYPE_FANOUT); - $topic->addFlag(AmqpTopic::FLAG_DURABLE); - - return $topic; - } -} diff --git a/pkg/enqueue/Client/Amqp/RabbitMqDriver.php b/pkg/enqueue/Client/Amqp/RabbitMqDriver.php deleted file mode 100644 index 518b90056..000000000 --- a/pkg/enqueue/Client/Amqp/RabbitMqDriver.php +++ /dev/null @@ -1,154 +0,0 @@ -config = $config; - $this->context = $context; - $this->queueMetaRegistry = $queueMetaRegistry; - - $this->priorityMap = [ - MessagePriority::VERY_LOW => 0, - MessagePriority::LOW => 1, - MessagePriority::NORMAL => 2, - MessagePriority::HIGH => 3, - MessagePriority::VERY_HIGH => 4, - ]; - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - $producer = $this->context->createProducer(); - - if ($message->getDelay()) { - $producer->setDeliveryDelay($message->getDelay() * 1000); - } - - $producer->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - * - * @return AmqpQueue - */ - public function createQueue($queueName) - { - $queue = parent::createQueue($queueName); - $queue->setArguments(['x-max-priority' => 4]); - - return $queue; - } - - /** - * {@inheritdoc} - * - * @return AmqpMessage - */ - public function createTransportMessage(Message $message) - { - $transportMessage = parent::createTransportMessage($message); - - if ($priority = $message->getPriority()) { - if (false == array_key_exists($priority, $this->priorityMap)) { - throw new \InvalidArgumentException(sprintf( - 'Given priority could not be converted to client\'s one. Got: %s', - $priority - )); - } - - $transportMessage->setPriority($this->priorityMap[$priority]); - } - - if ($message->getDelay()) { - if (false == $this->config->getTransportOption('delay_strategy', false)) { - throw new LogicException('The message delaying is not supported. In order to use delay feature install RabbitMQ delay strategy.'); - } - - $transportMessage->setProperty('enqueue-delay', $message->getDelay() * 1000); - } - - return $transportMessage; - } - - /** - * @param AmqpMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = parent::createClientMessage($message); - - if ($priority = $message->getPriority()) { - if (false === $clientPriority = array_search($priority, $this->priorityMap, true)) { - throw new \LogicException(sprintf('Cant convert transport priority to client: "%s"', $priority)); - } - - $clientMessage->setPriority($clientPriority); - } - - if ($delay = $message->getProperty('enqueue-delay')) { - if (false == is_numeric($delay)) { - throw new \LogicException(sprintf('"enqueue-delay" header is not numeric. "%s"', $delay)); - } - - $clientMessage->setDelay((int) ((int) $delay) / 1000); - } - - return $clientMessage; - } -} diff --git a/pkg/enqueue/Client/ChainExtension.php b/pkg/enqueue/Client/ChainExtension.php index c202e98e0..655b75f6a 100644 --- a/pkg/enqueue/Client/ChainExtension.php +++ b/pkg/enqueue/Client/ChainExtension.php @@ -2,38 +2,101 @@ namespace Enqueue\Client; -class ChainExtension implements ExtensionInterface +final class ChainExtension implements ExtensionInterface { /** - * @var ExtensionInterface[] + * @var PreSendEventExtensionInterface[] */ - private $extensions; + private $preSendEventExtensions; /** - * @param ExtensionInterface[] $extensions + * @var PreSendCommandExtensionInterface[] */ + private $preSendCommandExtensions; + + /** + * @var DriverPreSendExtensionInterface[] + */ + private $driverPreSendExtensions; + + /** + * @var PostSendExtensionInterface[] + */ + private $postSendExtensions; + public function __construct(array $extensions) { - $this->extensions = $extensions; + $this->preSendEventExtensions = []; + $this->preSendCommandExtensions = []; + $this->driverPreSendExtensions = []; + $this->postSendExtensions = []; + + array_walk($extensions, function ($extension) { + if ($extension instanceof ExtensionInterface) { + $this->preSendEventExtensions[] = $extension; + $this->preSendCommandExtensions[] = $extension; + $this->driverPreSendExtensions[] = $extension; + $this->postSendExtensions[] = $extension; + + return; + } + + $extensionValid = false; + if ($extension instanceof PreSendEventExtensionInterface) { + $this->preSendEventExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PreSendCommandExtensionInterface) { + $this->preSendCommandExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof DriverPreSendExtensionInterface) { + $this->driverPreSendExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PostSendExtensionInterface) { + $this->postSendExtensions[] = $extension; + + $extensionValid = true; + } + + if (false == $extensionValid) { + throw new \LogicException(sprintf('Invalid extension given %s', $extension::class)); + } + }); + } + + public function onPreSendEvent(PreSend $context): void + { + foreach ($this->preSendEventExtensions as $extension) { + $extension->onPreSendEvent($context); + } } - /** - * {@inheritdoc} - */ - public function onPreSend($topic, Message $message) + public function onPreSendCommand(PreSend $context): void { - foreach ($this->extensions as $extension) { - $extension->onPreSend($topic, $message); + foreach ($this->preSendCommandExtensions as $extension) { + $extension->onPreSendCommand($context); } } - /** - * {@inheritdoc} - */ - public function onPostSend($topic, Message $message) + public function onDriverPreSend(DriverPreSend $context): void + { + foreach ($this->driverPreSendExtensions as $extension) { + $extension->onDriverPreSend($context); + } + } + + public function onPostSend(PostSend $context): void { - foreach ($this->extensions as $extension) { - $extension->onPostSend($topic, $message); + foreach ($this->postSendExtensions as $extension) { + $extension->onPostSend($context); } } } diff --git a/pkg/enqueue/Client/CommandSubscriberInterface.php b/pkg/enqueue/Client/CommandSubscriberInterface.php index 99f6b274b..d7b06daaf 100644 --- a/pkg/enqueue/Client/CommandSubscriberInterface.php +++ b/pkg/enqueue/Client/CommandSubscriberInterface.php @@ -2,6 +2,15 @@ namespace Enqueue\Client; +/** + * @phpstan-type CommandConfig = array{ + * command: string, + * processor?: string, + * queue?: string, + * prefix_queue?: bool, + * exclusive?: bool, + * } + */ interface CommandSubscriberInterface { /** @@ -12,17 +21,40 @@ interface CommandSubscriberInterface * or * * [ - * 'processorName' => 'aCommandName', - * 'queueName' => 'a_client_queue_name', - * 'queueNameHardcoded' => true, + * 'command' => 'aSubscribedCommand', + * 'processor' => 'aProcessorName', + * 'queue' => 'a_client_queue_name', + * 'prefix_queue' => true, * 'exclusive' => true, * ] * - * queueName, exclusive and queueNameHardcoded are optional. + * or + * + * [ + * [ + * 'command' => 'aSubscribedCommand', + * 'processor' => 'aProcessorName', + * 'queue' => 'a_client_queue_name', + * 'prefix_queue' => true, + * 'exclusive' => true, + * ], + * [ + * 'command' => 'aSubscribedCommand', + * 'processor' => 'aProcessorName', + * 'queue' => 'a_client_queue_name', + * 'prefix_queue' => true, + * 'exclusive' => true, + * ] + * ] + * + * queue, processor, prefix_queue, and exclusive are optional. + * It is possible to pass other options, they could be accessible on a route instance through options. * - * Note: If you set queueNameHardcoded to true then the queueName is used as is and therefor the driver is not used to create a transport queue name. + * Note: If you set "prefix_queue" to true then the "queue" is used as is and therefor the driver is not used to create a transport queue name. * * @return string|array + * + * @phpstan-return string|CommandConfig|array */ public static function getSubscribedCommand(); } diff --git a/pkg/enqueue/Client/Config.php b/pkg/enqueue/Client/Config.php index d5d8b072a..8210dff68 100644 --- a/pkg/enqueue/Client/Config.php +++ b/pkg/enqueue/Client/Config.php @@ -4,12 +4,13 @@ class Config { - const PARAMETER_TOPIC_NAME = 'enqueue.topic_name'; - const PARAMETER_COMMAND_NAME = 'enqueue.command_name'; - const PARAMETER_PROCESSOR_NAME = 'enqueue.processor_name'; - const PARAMETER_PROCESSOR_QUEUE_NAME = 'enqueue.processor_queue_name'; - const DEFAULT_PROCESSOR_QUEUE_NAME = 'default'; - const COMMAND_TOPIC = '__command__'; + public const TOPIC = 'enqueue.topic'; + public const COMMAND = 'enqueue.command'; + public const PROCESSOR = 'enqueue.processor'; + public const EXPIRE = 'enqueue.expire'; + public const PRIORITY = 'enqueue.priority'; + public const DELAY = 'enqueue.delay'; + public const CONTENT_TYPE = 'enqueue.content_type'; /** * @var string @@ -19,27 +20,32 @@ class Config /** * @var string */ - private $appName; + private $separator; /** * @var string */ - private $routerTopicName; + private $app; /** * @var string */ - private $routerQueueName; + private $routerTopic; /** * @var string */ - private $defaultProcessorQueueName; + private $routerQueue; /** * @var string */ - private $routerProcessorName; + private $defaultQueue; + + /** + * @var string + */ + private $routerProcessor; /** * @var array @@ -47,116 +53,126 @@ class Config private $transportConfig; /** - * @param string $prefix - * @param string $appName - * @param string $routerTopicName - * @param string $routerQueueName - * @param string $defaultProcessorQueueName - * @param string $routerProcessorName - * @param array $transportConfig + * @var array */ - public function __construct($prefix, $appName, $routerTopicName, $routerQueueName, $defaultProcessorQueueName, $routerProcessorName, array $transportConfig = []) - { - $this->prefix = $prefix; - $this->appName = $appName; - $this->routerTopicName = $routerTopicName; - $this->routerQueueName = $routerQueueName; - $this->defaultProcessorQueueName = $defaultProcessorQueueName; - $this->routerProcessorName = $routerProcessorName; + private $driverConfig; + + public function __construct( + string $prefix, + string $separator, + string $app, + string $routerTopic, + string $routerQueue, + string $defaultQueue, + string $routerProcessor, + array $transportConfig, + array $driverConfig, + ) { + $this->prefix = trim($prefix); + $this->app = trim($app); + + $this->routerTopic = trim($routerTopic); + if (empty($this->routerTopic)) { + throw new \InvalidArgumentException('Router topic is empty.'); + } + + $this->routerQueue = trim($routerQueue); + if (empty($this->routerQueue)) { + throw new \InvalidArgumentException('Router queue is empty.'); + } + + $this->defaultQueue = trim($defaultQueue); + if (empty($this->defaultQueue)) { + throw new \InvalidArgumentException('Default processor queue name is empty.'); + } + + $this->routerProcessor = trim($routerProcessor); + if (empty($this->routerProcessor)) { + throw new \InvalidArgumentException('Router processor name is empty.'); + } + $this->transportConfig = $transportConfig; + $this->driverConfig = $driverConfig; + + $this->separator = $separator; } - /** - * @return string - */ - public function getRouterTopicName() + public function getPrefix(): string { - return $this->routerTopicName; + return $this->prefix; } - /** - * @return string - */ - public function getRouterQueueName() + public function getSeparator(): string { - return $this->routerQueueName; + return $this->separator; } - /** - * @return string - */ - public function getDefaultProcessorQueueName() + public function getApp(): string { - return $this->defaultProcessorQueueName; + return $this->app; } - /** - * @return string - */ - public function getRouterProcessorName() + public function getRouterTopic(): string { - return $this->routerProcessorName; + return $this->routerTopic; } - /** - * @param string $name - * - * @return string - */ - public function createTransportRouterTopicName($name) + public function getRouterQueue(): string { - return strtolower(implode('.', array_filter([trim($this->prefix), trim($name)]))); + return $this->routerQueue; } - /** - * @param string $name - * - * @return string - */ - public function createTransportQueueName($name) + public function getDefaultQueue(): string { - return strtolower(implode('.', array_filter([trim($this->prefix), trim($this->appName), trim($name)]))); + return $this->defaultQueue; } - /** - * @param string $name - * @param mixed|null $default - * - * @return array - */ - public function getTransportOption($name, $default = null) + public function getRouterProcessor(): string + { + return $this->routerProcessor; + } + + public function getTransportOption(string $name, $default = null) { return array_key_exists($name, $this->transportConfig) ? $this->transportConfig[$name] : $default; } - /** - * @param string|null $prefix - * @param string|null $appName - * @param string|null $routerTopicName - * @param string|null $routerQueueName - * @param string|null $defaultProcessorQueueName - * @param string|null $routerProcessorName - * @param array $transportConfig - * - * @return static - */ + public function getTransportOptions(): array + { + return $this->transportConfig; + } + + public function getDriverOption(string $name, $default = null) + { + return array_key_exists($name, $this->driverConfig) ? $this->driverConfig[$name] : $default; + } + + public function getDriverOptions(): array + { + return $this->driverConfig; + } + public static function create( - $prefix = null, - $appName = null, - $routerTopicName = null, - $routerQueueName = null, - $defaultProcessorQueueName = null, - $routerProcessorName = null, - array $transportConfig = [] - ) { - return new static( + ?string $prefix = null, + ?string $separator = null, + ?string $app = null, + ?string $routerTopic = null, + ?string $routerQueue = null, + ?string $defaultQueue = null, + ?string $routerProcessor = null, + array $transportConfig = [], + array $driverConfig = [], + ): self { + return new self( $prefix ?: '', - $appName ?: '', - $routerTopicName ?: 'router', - $routerQueueName ?: 'default', - $defaultProcessorQueueName ?: 'default', - $routerProcessorName ?: 'router', - $transportConfig + $separator ?: '.', + $app ?: '', + $routerTopic ?: 'router', + $routerQueue ?: 'default', + $defaultQueue ?: 'default', + $routerProcessor ?: 'router', + $transportConfig, + $driverConfig ); } } diff --git a/pkg/enqueue/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php b/pkg/enqueue/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php index 98fa483fa..475e2cf5b 100644 --- a/pkg/enqueue/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php +++ b/pkg/enqueue/Client/ConsumptionExtension/DelayRedeliveredMessageExtension.php @@ -3,16 +3,13 @@ namespace Enqueue\Client\ConsumptionExtension; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; use Enqueue\Consumption\Result; -class DelayRedeliveredMessageExtension implements ExtensionInterface +class DelayRedeliveredMessageExtension implements MessageReceivedExtensionInterface { - use EmptyExtensionTrait; - - const PROPERTY_REDELIVER_COUNT = 'enqueue.redelivery_count'; + public const PROPERTY_REDELIVER_COUNT = 'enqueue.redelivery_count'; /** * @var DriverInterface @@ -27,8 +24,7 @@ class DelayRedeliveredMessageExtension implements ExtensionInterface private $delay; /** - * @param DriverInterface $driver - * @param int $delay The number of seconds the message should be delayed + * @param int $delay The number of seconds the message should be delayed */ public function __construct(DriverInterface $driver, $delay) { @@ -36,12 +32,9 @@ public function __construct(DriverInterface $driver, $delay) $this->delay = $delay; } - /** - * {@inheritdoc} - */ - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { - $message = $context->getPsrMessage(); + $message = $context->getMessage(); if (false == $message->isRedelivered()) { return; } diff --git a/pkg/enqueue/Client/ConsumptionExtension/ExclusiveCommandExtension.php b/pkg/enqueue/Client/ConsumptionExtension/ExclusiveCommandExtension.php index a9afde004..7ab88ae0f 100644 --- a/pkg/enqueue/Client/ConsumptionExtension/ExclusiveCommandExtension.php +++ b/pkg/enqueue/Client/ConsumptionExtension/ExclusiveCommandExtension.php @@ -3,83 +3,75 @@ namespace Enqueue\Client\ConsumptionExtension; use Enqueue\Client\Config; -use Enqueue\Client\ExtensionInterface as ClientExtensionInterface; -use Enqueue\Client\Message; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface as ConsumptionExtensionInterface; +use Enqueue\Client\DriverInterface; +use Enqueue\Client\Route; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; -class ExclusiveCommandExtension implements ConsumptionExtensionInterface, ClientExtensionInterface +final class ExclusiveCommandExtension implements MessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** - * @var string[] + * @var DriverInterface */ - private $queueNameToProcessorNameMap; + private $driver; /** - * @var string[] + * @var Route[] */ - private $processorNameToQueueNameMap; + private $queueToRouteMap; - /** - * @param string[] $queueNameToProcessorNameMap - */ - public function __construct(array $queueNameToProcessorNameMap) + public function __construct(DriverInterface $driver) { - $this->queueNameToProcessorNameMap = $queueNameToProcessorNameMap; - $this->processorNameToQueueNameMap = array_flip($queueNameToProcessorNameMap); + $this->driver = $driver; } - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { - $message = $context->getPsrMessage(); - $queue = $context->getPsrQueue(); - - if ($message->getProperty(Config::PARAMETER_TOPIC_NAME)) { + $message = $context->getMessage(); + if ($message->getProperty(Config::TOPIC)) { return; } - if ($message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { + if ($message->getProperty(Config::COMMAND)) { return; } - if ($message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { + if ($message->getProperty(Config::PROCESSOR)) { return; } - if ($message->getProperty(Config::PARAMETER_COMMAND_NAME)) { - return; + + if (null === $this->queueToRouteMap) { + $this->queueToRouteMap = $this->buildMap(); } - if (array_key_exists($queue->getQueueName(), $this->queueNameToProcessorNameMap)) { + $queue = $context->getConsumer()->getQueue(); + if (array_key_exists($queue->getQueueName(), $this->queueToRouteMap)) { $context->getLogger()->debug('[ExclusiveCommandExtension] This is a exclusive command queue and client\'s properties are not set. Setting them'); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, Config::COMMAND_TOPIC); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $queue->getQueueName()); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $this->queueNameToProcessorNameMap[$queue->getQueueName()]); - $message->setProperty(Config::PARAMETER_COMMAND_NAME, $this->queueNameToProcessorNameMap[$queue->getQueueName()]); + $route = $this->queueToRouteMap[$queue->getQueueName()]; + $message->setProperty(Config::PROCESSOR, $route->getProcessor()); + $message->setProperty(Config::COMMAND, $route->getSource()); } } - /** - * {@inheritdoc} - */ - public function onPreSend($topic, Message $message) + private function buildMap(): array { - if (Config::COMMAND_TOPIC != $topic) { - return; - } + $map = []; + foreach ($this->driver->getRouteCollection()->all() as $route) { + if (false == $route->isCommand()) { + continue; + } + + if (false == $route->isProcessorExclusive()) { + continue; + } + + $queueName = $this->driver->createRouteQueue($route)->getQueueName(); + if (array_key_exists($queueName, $map)) { + throw new \LogicException('The queue name has been already bound by another exclusive command processor'); + } - $commandName = $message->getProperty(Config::PARAMETER_COMMAND_NAME); - if (array_key_exists($commandName, $this->processorNameToQueueNameMap)) { - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $commandName); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $this->processorNameToQueueNameMap[$commandName]); + $map[$queueName] = $route; } - } - /** - * {@inheritdoc} - */ - public function onPostSend($topic, Message $message) - { + return $map; } } diff --git a/pkg/enqueue/Client/ConsumptionExtension/FlushSpoolProducerExtension.php b/pkg/enqueue/Client/ConsumptionExtension/FlushSpoolProducerExtension.php index efb6848b8..6682cad8e 100644 --- a/pkg/enqueue/Client/ConsumptionExtension/FlushSpoolProducerExtension.php +++ b/pkg/enqueue/Client/ConsumptionExtension/FlushSpoolProducerExtension.php @@ -3,36 +3,29 @@ namespace Enqueue\Client\ConsumptionExtension; use Enqueue\Client\SpoolProducer; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\EndExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; -class FlushSpoolProducerExtension implements ExtensionInterface +class FlushSpoolProducerExtension implements PostMessageReceivedExtensionInterface, EndExtensionInterface { - use EmptyExtensionTrait; - /** * @var SpoolProducer */ private $producer; - /** - * @param SpoolProducer $producer - */ public function __construct(SpoolProducer $producer) { $this->producer = $producer; } - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { $this->producer->flush(); } - public function onInterrupted(Context $context) + public function onEnd(End $context): void { $this->producer->flush(); } diff --git a/pkg/enqueue/Client/ConsumptionExtension/LogExtension.php b/pkg/enqueue/Client/ConsumptionExtension/LogExtension.php new file mode 100644 index 000000000..693be2035 --- /dev/null +++ b/pkg/enqueue/Client/ConsumptionExtension/LogExtension.php @@ -0,0 +1,69 @@ +getResult(); + $message = $context->getMessage(); + + $logLevel = Result::REJECT == ((string) $result) ? LogLevel::ERROR : LogLevel::INFO; + + if ($command = $message->getProperty(Config::COMMAND)) { + $reason = ''; + $logMessage = "[client] Processed {command}\t{body}\t{result}"; + if ($result instanceof Result && $result->getReason()) { + $reason = $result->getReason(); + + $logMessage .= ' {reason}'; + } + + $context->getLogger()->log($logLevel, $logMessage, [ + 'result' => str_replace('enqueue.', '', $result), + 'reason' => $reason, + 'command' => $command, + 'queueName' => $context->getConsumer()->getQueue()->getQueueName(), + 'body' => Stringify::that($message->getBody()), + 'properties' => Stringify::that($message->getProperties()), + 'headers' => Stringify::that($message->getHeaders()), + ]); + + return; + } + + $topic = $message->getProperty(Config::TOPIC); + $processor = $message->getProperty(Config::PROCESSOR); + if ($topic && $processor) { + $reason = ''; + $logMessage = "[client] Processed {topic} -> {processor}\t{body}\t{result}"; + if ($result instanceof Result && $result->getReason()) { + $reason = $result->getReason(); + + $logMessage .= ' {reason}'; + } + + $context->getLogger()->log($logLevel, $logMessage, [ + 'result' => str_replace('enqueue.', '', $result), + 'reason' => $reason, + 'topic' => $topic, + 'processor' => $processor, + 'queueName' => $context->getConsumer()->getQueue()->getQueueName(), + 'body' => Stringify::that($message->getBody()), + 'properties' => Stringify::that($message->getProperties()), + 'headers' => Stringify::that($message->getHeaders()), + ]); + + return; + } + + parent::onPostMessageReceived($context); + } +} diff --git a/pkg/enqueue/Client/ConsumptionExtension/SetRouterPropertiesExtension.php b/pkg/enqueue/Client/ConsumptionExtension/SetRouterPropertiesExtension.php index d294a9d87..0d2278349 100644 --- a/pkg/enqueue/Client/ConsumptionExtension/SetRouterPropertiesExtension.php +++ b/pkg/enqueue/Client/ConsumptionExtension/SetRouterPropertiesExtension.php @@ -4,45 +4,43 @@ use Enqueue\Client\Config; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; -class SetRouterPropertiesExtension implements ExtensionInterface +class SetRouterPropertiesExtension implements MessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** * @var DriverInterface */ private $driver; - /** - * @param DriverInterface $driver - */ public function __construct(DriverInterface $driver) { $this->driver = $driver; } - /** - * {@inheritdoc} - */ - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { - $message = $context->getPsrMessage(); - if ($message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { + $message = $context->getMessage(); + if (false == $message->getProperty(Config::TOPIC)) { + return; + } + if ($message->getProperty(Config::PROCESSOR)) { return; } $config = $this->driver->getConfig(); - $queue = $this->driver->createQueue($config->getRouterQueueName()); - if ($context->getPsrQueue()->getQueueName() != $queue->getQueueName()) { + $queue = $this->driver->createQueue($config->getRouterQueue()); + if ($context->getConsumer()->getQueue()->getQueueName() != $queue->getQueueName()) { return; } // RouterProcessor is our default message processor when that header is not set - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $config->getRouterProcessorName()); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $config->getRouterQueueName()); + $message->setProperty(Config::PROCESSOR, $config->getRouterProcessor()); + + $context->getLogger()->debug( + '[SetRouterPropertiesExtension] '. + sprintf('Set router processor "%s"', $config->getRouterProcessor()) + ); } } diff --git a/pkg/enqueue/Client/ConsumptionExtension/SetupBrokerExtension.php b/pkg/enqueue/Client/ConsumptionExtension/SetupBrokerExtension.php index 8b6aecbc1..44d610fb9 100644 --- a/pkg/enqueue/Client/ConsumptionExtension/SetupBrokerExtension.php +++ b/pkg/enqueue/Client/ConsumptionExtension/SetupBrokerExtension.php @@ -3,14 +3,11 @@ namespace Enqueue\Client\ConsumptionExtension; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\Start; +use Enqueue\Consumption\StartExtensionInterface; -class SetupBrokerExtension implements ExtensionInterface +class SetupBrokerExtension implements StartExtensionInterface { - use EmptyExtensionTrait; - /** * @var DriverInterface */ @@ -21,19 +18,13 @@ class SetupBrokerExtension implements ExtensionInterface */ private $isDone; - /** - * @param DriverInterface $driver - */ public function __construct(DriverInterface $driver) { $this->driver = $driver; $this->isDone = false; } - /** - * {@inheritdoc} - */ - public function onStart(Context $context) + public function onStart(Start $context): void { if (false == $this->isDone) { $this->isDone = true; diff --git a/pkg/enqueue/Client/DelegateProcessor.php b/pkg/enqueue/Client/DelegateProcessor.php index 6cb8f9866..7582c52dc 100644 --- a/pkg/enqueue/Client/DelegateProcessor.php +++ b/pkg/enqueue/Client/DelegateProcessor.php @@ -2,36 +2,31 @@ namespace Enqueue\Client; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Enqueue\ProcessorRegistryInterface; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class DelegateProcessor implements PsrProcessor +class DelegateProcessor implements Processor { /** * @var ProcessorRegistryInterface */ private $registry; - /** - * @param ProcessorRegistryInterface $registry - */ public function __construct(ProcessorRegistryInterface $registry) { $this->registry = $registry; } /** - * {@inheritdoc} + * @return string|object */ - public function process(PsrMessage $message, PsrContext $context) + public function process(InteropMessage $message, Context $context) { - $processorName = $message->getProperty(Config::PARAMETER_PROCESSOR_NAME); + $processorName = $message->getProperty(Config::PROCESSOR); if (false == $processorName) { - throw new \LogicException(sprintf( - 'Got message without required parameter: "%s"', - Config::PARAMETER_PROCESSOR_NAME - )); + throw new \LogicException(sprintf('Got message without required parameter: "%s"', Config::PROCESSOR)); } return $this->registry->get($processorName)->process($message, $context); diff --git a/pkg/enqueue/Client/Driver/AmqpDriver.php b/pkg/enqueue/Client/Driver/AmqpDriver.php new file mode 100644 index 000000000..1def3fb23 --- /dev/null +++ b/pkg/enqueue/Client/Driver/AmqpDriver.php @@ -0,0 +1,132 @@ +setDeliveryMode(AmqpMessage::DELIVERY_MODE_PERSISTENT); + $transportMessage->setContentType($clientMessage->getContentType()); + + if ($clientMessage->getExpire()) { + $transportMessage->setExpiration($clientMessage->getExpire() * 1000); + } + + $priorityMap = $this->getPriorityMap(); + if ($priority = $clientMessage->getPriority()) { + if (false == array_key_exists($priority, $priorityMap)) { + throw new \InvalidArgumentException(sprintf('Cant convert client priority "%s" to transport one. Could be one of "%s"', $priority, implode('", "', array_keys($priorityMap)))); + } + + $transportMessage->setPriority($priorityMap[$priority]); + } + + return $transportMessage; + } + + public function setupBroker(?LoggerInterface $logger = null): void + { + $logger = $logger ?: new NullLogger(); + $log = function ($text, ...$args) use ($logger) { + $logger->debug(sprintf('[AmqpDriver] '.$text, ...$args)); + }; + + // setup router + $routerTopic = $this->createRouterTopic(); + $log('Declare router exchange: %s', $routerTopic->getTopicName()); + $this->getContext()->declareTopic($routerTopic); + + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + $log('Declare router queue: %s', $routerQueue->getQueueName()); + $this->getContext()->declareQueue($routerQueue); + + $log('Bind router queue to exchange: %s -> %s', $routerQueue->getQueueName(), $routerTopic->getTopicName()); + $this->getContext()->bind(new AmqpBind($routerTopic, $routerQueue, $routerQueue->getQueueName())); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var AmqpQueue $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $log('Declare processor queue: %s', $queue->getQueueName()); + $this->getContext()->declareQueue($queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + } + + /** + * @return AmqpTopic + */ + protected function createRouterTopic(): Destination + { + $topic = $this->doCreateTopic( + $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true) + ); + $topic->setType(AmqpTopic::TYPE_FANOUT); + $topic->addFlag(AmqpTopic::FLAG_DURABLE); + + return $topic; + } + + /** + * @return AmqpQueue + */ + protected function doCreateQueue(string $transportQueueName): InteropQueue + { + /** @var AmqpQueue $queue */ + $queue = parent::doCreateQueue($transportQueueName); + $queue->addFlag(AmqpQueue::FLAG_DURABLE); + + return $queue; + } + + /** + * @param AmqpProducer $producer + * @param AmqpTopic $topic + * @param AmqpMessage $transportMessage + */ + protected function doSendToRouter(InteropProducer $producer, Destination $topic, InteropMessage $transportMessage): void + { + // We should not handle priority, expiration, and delay at this stage. + // The router will take care of it while re-sending the message to the final destinations. + $transportMessage->setPriority(null); + $transportMessage->setExpiration(null); + + $producer->send($topic, $transportMessage); + } +} diff --git a/pkg/enqueue/Client/Driver/DbalDriver.php b/pkg/enqueue/Client/Driver/DbalDriver.php new file mode 100644 index 000000000..34875eff7 --- /dev/null +++ b/pkg/enqueue/Client/Driver/DbalDriver.php @@ -0,0 +1,29 @@ +debug(sprintf('[DbalDriver] '.$text, ...$args)); + }; + + $log('Creating database table: "%s"', $this->getContext()->getTableName()); + $this->getContext()->createDataBaseTable(); + } +} diff --git a/pkg/enqueue/Client/Driver/FsDriver.php b/pkg/enqueue/Client/Driver/FsDriver.php new file mode 100644 index 000000000..f578b172d --- /dev/null +++ b/pkg/enqueue/Client/Driver/FsDriver.php @@ -0,0 +1,49 @@ +debug(sprintf('[FsDriver] '.$text, ...$args)); + }; + + // setup router + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + + $log('Declare router queue "%s" file: %s', $routerQueue->getQueueName(), $routerQueue->getFileInfo()); + $this->getContext()->declareDestination($routerQueue); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var FsDestination $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $log('Declare processor queue "%s" file: %s', $queue->getQueueName(), $queue->getFileInfo()); + $this->getContext()->declareDestination($queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + } +} diff --git a/pkg/enqueue/Client/Driver/GenericDriver.php b/pkg/enqueue/Client/Driver/GenericDriver.php new file mode 100644 index 000000000..d509677df --- /dev/null +++ b/pkg/enqueue/Client/Driver/GenericDriver.php @@ -0,0 +1,278 @@ +context = $context; + $this->config = $config; + $this->routeCollection = $routeCollection; + } + + public function sendToRouter(Message $message): DriverSendResult + { + if ($message->getProperty(Config::COMMAND)) { + throw new \LogicException('Command must not be send to router but go directly to its processor.'); + } + if (false == $message->getProperty(Config::TOPIC)) { + throw new \LogicException('Topic name parameter is required but is not set'); + } + + $topic = $this->createRouterTopic(); + $transportMessage = $this->createTransportMessage($message); + $producer = $this->getContext()->createProducer(); + + $this->doSendToRouter($producer, $topic, $transportMessage); + + return new DriverSendResult($topic, $transportMessage); + } + + public function sendToProcessor(Message $message): DriverSendResult + { + $topic = $message->getProperty(Config::TOPIC); + $command = $message->getProperty(Config::COMMAND); + + /** @var InteropQueue $queue */ + $queue = null; + $routerProcessor = $this->config->getRouterProcessor(); + $processor = $message->getProperty(Config::PROCESSOR); + if ($topic && $processor && $processor !== $routerProcessor) { + $route = $this->routeCollection->topicAndProcessor($topic, $processor); + if (false == $route) { + throw new \LogicException(sprintf('There is no route for topic "%s" and processor "%s"', $topic, $processor)); + } + + $message->setProperty(Config::PROCESSOR, $route->getProcessor()); + $queue = $this->createRouteQueue($route); + } elseif ($topic && (false == $processor || $processor === $routerProcessor)) { + $message->setProperty(Config::PROCESSOR, $routerProcessor); + + $queue = $this->createQueue($this->config->getRouterQueue()); + } elseif ($command) { + $route = $this->routeCollection->command($command); + if (false == $route) { + throw new \LogicException(sprintf('There is no route for command "%s".', $command)); + } + + $message->setProperty(Config::PROCESSOR, $route->getProcessor()); + $queue = $this->createRouteQueue($route); + } else { + throw new \LogicException('Either topic or command parameter must be set.'); + } + + $transportMessage = $this->createTransportMessage($message); + + $producer = $this->context->createProducer(); + + if (null !== $delay = $transportMessage->getProperty(Config::DELAY)) { + $producer->setDeliveryDelay($delay * 1000); + } + + if (null !== $expire = $transportMessage->getProperty(Config::EXPIRE)) { + $producer->setTimeToLive($expire * 1000); + } + + if (null !== $priority = $transportMessage->getProperty(Config::PRIORITY)) { + $priorityMap = $this->getPriorityMap(); + + $producer->setPriority($priorityMap[$priority]); + } + + $this->doSendToProcessor($producer, $queue, $transportMessage); + + return new DriverSendResult($queue, $transportMessage); + } + + public function setupBroker(?LoggerInterface $logger = null): void + { + } + + public function createQueue(string $clientQueueName, bool $prefix = true): InteropQueue + { + $transportName = $this->createTransportQueueName($clientQueueName, $prefix); + + return $this->doCreateQueue($transportName); + } + + public function createRouteQueue(Route $route): InteropQueue + { + $transportName = $this->createTransportQueueName( + $route->getQueue() ?: $this->config->getDefaultQueue(), + $route->isPrefixQueue() + ); + + return $this->doCreateQueue($transportName); + } + + public function createTransportMessage(Message $clientMessage): InteropMessage + { + $headers = $clientMessage->getHeaders(); + $properties = $clientMessage->getProperties(); + + $transportMessage = $this->context->createMessage(); + $transportMessage->setBody($clientMessage->getBody()); + $transportMessage->setHeaders($headers); + $transportMessage->setProperties($properties); + $transportMessage->setMessageId($clientMessage->getMessageId()); + $transportMessage->setTimestamp($clientMessage->getTimestamp()); + $transportMessage->setReplyTo($clientMessage->getReplyTo()); + $transportMessage->setCorrelationId($clientMessage->getCorrelationId()); + + if ($contentType = $clientMessage->getContentType()) { + $transportMessage->setProperty(Config::CONTENT_TYPE, $contentType); + } + + if ($priority = $clientMessage->getPriority()) { + $transportMessage->setProperty(Config::PRIORITY, $priority); + } + + if ($expire = $clientMessage->getExpire()) { + $transportMessage->setProperty(Config::EXPIRE, $expire); + } + + if ($delay = $clientMessage->getDelay()) { + $transportMessage->setProperty(Config::DELAY, $delay); + } + + return $transportMessage; + } + + public function createClientMessage(InteropMessage $transportMessage): Message + { + $clientMessage = new Message(); + + $clientMessage->setBody($transportMessage->getBody()); + $clientMessage->setHeaders($transportMessage->getHeaders()); + $clientMessage->setProperties($transportMessage->getProperties()); + $clientMessage->setMessageId($transportMessage->getMessageId()); + $clientMessage->setTimestamp($transportMessage->getTimestamp()); + $clientMessage->setReplyTo($transportMessage->getReplyTo()); + $clientMessage->setCorrelationId($transportMessage->getCorrelationId()); + + if ($contentType = $transportMessage->getProperty(Config::CONTENT_TYPE)) { + $clientMessage->setContentType($contentType); + } + + if ($priority = $transportMessage->getProperty(Config::PRIORITY)) { + $clientMessage->setPriority($priority); + } + + if ($delay = $transportMessage->getProperty(Config::DELAY)) { + $clientMessage->setDelay((int) $delay); + } + + if ($expire = $transportMessage->getProperty(Config::EXPIRE)) { + $clientMessage->setExpire((int) $expire); + } + + return $clientMessage; + } + + public function getConfig(): Config + { + return $this->config; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getRouteCollection(): RouteCollection + { + return $this->routeCollection; + } + + protected function doSendToRouter(InteropProducer $producer, Destination $topic, InteropMessage $transportMessage): void + { + $producer->send($topic, $transportMessage); + } + + protected function doSendToProcessor(InteropProducer $producer, InteropQueue $queue, InteropMessage $transportMessage): void + { + $producer->send($queue, $transportMessage); + } + + protected function createRouterTopic(): Destination + { + return $this->createQueue($this->getConfig()->getRouterQueue()); + } + + protected function createTransportRouterTopicName(string $name, bool $prefix): string + { + $clientPrefix = $prefix ? $this->config->getPrefix() : ''; + + return strtolower(implode($this->config->getSeparator(), array_filter([$clientPrefix, $name]))); + } + + protected function createTransportQueueName(string $name, bool $prefix): string + { + $clientPrefix = $prefix ? $this->config->getPrefix() : ''; + $clientAppName = $prefix ? $this->config->getApp() : ''; + + return strtolower(implode($this->config->getSeparator(), array_filter([$clientPrefix, $clientAppName, $name]))); + } + + protected function doCreateQueue(string $transportQueueName): InteropQueue + { + return $this->context->createQueue($transportQueueName); + } + + protected function doCreateTopic(string $transportTopicName): InteropTopic + { + return $this->context->createTopic($transportTopicName); + } + + /** + * [client message priority => transport message priority]. + * + * @return int[] + */ + protected function getPriorityMap(): array + { + return [ + MessagePriority::VERY_LOW => 0, + MessagePriority::LOW => 1, + MessagePriority::NORMAL => 2, + MessagePriority::HIGH => 3, + MessagePriority::VERY_HIGH => 4, + ]; + } +} diff --git a/pkg/enqueue/Client/Driver/GpsDriver.php b/pkg/enqueue/Client/Driver/GpsDriver.php new file mode 100644 index 000000000..32d14f721 --- /dev/null +++ b/pkg/enqueue/Client/Driver/GpsDriver.php @@ -0,0 +1,64 @@ +debug(sprintf('[GpsDriver] '.$text, ...$args)); + }; + + // setup router + $routerTopic = $this->createRouterTopic(); + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + + $log('Subscribe router topic to queue: %s -> %s', $routerTopic->getTopicName(), $routerQueue->getQueueName()); + $this->getContext()->subscribe($routerTopic, $routerQueue); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var GpsQueue $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $topic = $this->getContext()->createTopic($queue->getQueueName()); + + $log('Subscribe processor topic to queue: %s -> %s', $topic->getTopicName(), $queue->getQueueName()); + $this->getContext()->subscribe($topic, $queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + } + + /** + * @return GpsTopic + */ + protected function createRouterTopic(): Destination + { + return $this->doCreateTopic( + $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true) + ); + } +} diff --git a/pkg/enqueue/Client/Driver/MongodbDriver.php b/pkg/enqueue/Client/Driver/MongodbDriver.php new file mode 100644 index 000000000..1c9cff4bc --- /dev/null +++ b/pkg/enqueue/Client/Driver/MongodbDriver.php @@ -0,0 +1,30 @@ +debug(sprintf('[MongodbDriver] '.$text, ...$args)); + }; + + $contextConfig = $this->getContext()->getConfig(); + $log('Creating database and collection: "%s" "%s"', $contextConfig['dbname'], $contextConfig['collection_name']); + $this->getContext()->createCollection(); + } +} diff --git a/pkg/enqueue/Client/Driver/RabbitMqDriver.php b/pkg/enqueue/Client/Driver/RabbitMqDriver.php new file mode 100644 index 000000000..f215d555e --- /dev/null +++ b/pkg/enqueue/Client/Driver/RabbitMqDriver.php @@ -0,0 +1,20 @@ +setArguments(['x-max-priority' => 4]); + + return $queue; + } +} diff --git a/pkg/enqueue/Client/Driver/RabbitMqStompDriver.php b/pkg/enqueue/Client/Driver/RabbitMqStompDriver.php new file mode 100644 index 000000000..7af2db850 --- /dev/null +++ b/pkg/enqueue/Client/Driver/RabbitMqStompDriver.php @@ -0,0 +1,191 @@ +management = $management; + } + + /** + * @return StompMessage + */ + public function createTransportMessage(Message $message): InteropMessage + { + $transportMessage = parent::createTransportMessage($message); + + if ($message->getExpire()) { + $transportMessage->setHeader('expiration', (string) ($message->getExpire() * 1000)); + } + + if ($priority = $message->getPriority()) { + $priorityMap = $this->getPriorityMap(); + + if (false == array_key_exists($priority, $priorityMap)) { + throw new \LogicException(sprintf('Cant convert client priority to transport: "%s"', $priority)); + } + + $transportMessage->setHeader('priority', $priorityMap[$priority]); + } + + if ($message->getDelay()) { + if (false == $this->getConfig()->getTransportOption('delay_plugin_installed', false)) { + throw new \LogicException('The message delaying is not supported. In order to use delay feature install RabbitMQ delay plugin.'); + } + + $transportMessage->setHeader('x-delay', (string) ($message->getDelay() * 1000)); + } + + return $transportMessage; + } + + public function setupBroker(?LoggerInterface $logger = null): void + { + $logger = $logger ?: new NullLogger(); + $log = function ($text, ...$args) use ($logger) { + $logger->debug(sprintf('[RabbitMqStompDriver] '.$text, ...$args)); + }; + + if (false == $this->getConfig()->getTransportOption('management_plugin_installed', false)) { + $log('Could not setup broker. The option `management_plugin_installed` is not enabled. Please enable that option and install rabbit management plugin'); + + return; + } + + // setup router + $routerExchange = $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true); + $log('Declare router exchange: %s', $routerExchange); + $this->management->declareExchange($routerExchange, [ + 'type' => 'fanout', + 'durable' => true, + 'auto_delete' => false, + ]); + + $routerQueue = $this->createTransportQueueName($this->getConfig()->getRouterQueue(), true); + $log('Declare router queue: %s', $routerQueue); + $this->management->declareQueue($routerQueue, [ + 'auto_delete' => false, + 'durable' => true, + 'arguments' => [ + 'x-max-priority' => 4, + ], + ]); + + $log('Bind router queue to exchange: %s -> %s', $routerQueue, $routerExchange); + $this->management->bind($routerExchange, $routerQueue, $routerQueue); + + // setup queues + foreach ($this->getRouteCollection()->all() as $route) { + $queue = $this->createRouteQueue($route); + + $log('Declare processor queue: %s', $queue->getStompName()); + $this->management->declareQueue($queue->getStompName(), [ + 'auto_delete' => false, + 'durable' => true, + 'arguments' => [ + 'x-max-priority' => 4, + ], + ]); + } + + // setup delay exchanges + if ($this->getConfig()->getTransportOption('delay_plugin_installed', false)) { + foreach ($this->getRouteCollection()->all() as $route) { + $queue = $this->createRouteQueue($route); + $delayExchange = $queue->getStompName().'.delayed'; + + $log('Declare delay exchange: %s', $delayExchange); + $this->management->declareExchange($delayExchange, [ + 'type' => 'x-delayed-message', + 'durable' => true, + 'auto_delete' => false, + 'arguments' => [ + 'x-delayed-type' => 'direct', + ], + ]); + + $log('Bind processor queue to delay exchange: %s -> %s', $queue->getStompName(), $delayExchange); + $this->management->bind($delayExchange, $queue->getStompName(), $queue->getStompName()); + } + } else { + $log('Delay exchange and bindings are not setup. if you\'d like to use delays please install delay rabbitmq plugin and set delay_plugin_installed option to true'); + } + } + + /** + * @return StompDestination + */ + protected function doCreateQueue(string $transportQueueName): InteropQueue + { + $queue = parent::doCreateQueue($transportQueueName); + $queue->setHeader('x-max-priority', 4); + + return $queue; + } + + /** + * @param StompProducer $producer + * @param StompDestination $topic + * @param StompMessage $transportMessage + */ + protected function doSendToRouter(InteropProducer $producer, Destination $topic, InteropMessage $transportMessage): void + { + // We should not handle priority, expiration, and delay at this stage. + // The router will take care of it while re-sending the message to the final destinations. + $transportMessage->setHeader('expiration', null); + $transportMessage->setHeader('priority', null); + $transportMessage->setHeader('x-delay', null); + + $producer->send($topic, $transportMessage); + } + + /** + * @param StompProducer $producer + * @param StompDestination $destination + * @param StompMessage $transportMessage + */ + protected function doSendToProcessor(InteropProducer $producer, InteropQueue $destination, InteropMessage $transportMessage): void + { + if ($delay = $transportMessage->getProperty(Config::DELAY)) { + $producer->setDeliveryDelay(null); + $destination = $this->createDelayedTopic($destination); + } + + $producer->send($destination, $transportMessage); + } + + private function createDelayedTopic(StompDestination $queue): StompDestination + { + // in order to use delay feature make sure the rabbitmq_delayed_message_exchange plugin is installed. + $destination = $this->getContext()->createTopic($queue->getStompName().'.delayed'); + $destination->setType(StompDestination::TYPE_EXCHANGE); + $destination->setDurable(true); + $destination->setAutoDelete(false); + $destination->setRoutingKey($queue->getStompName()); + + return $destination; + } +} diff --git a/pkg/enqueue/Client/Driver/RdKafkaDriver.php b/pkg/enqueue/Client/Driver/RdKafkaDriver.php new file mode 100644 index 000000000..2609e3f91 --- /dev/null +++ b/pkg/enqueue/Client/Driver/RdKafkaDriver.php @@ -0,0 +1,48 @@ +debug('[RdKafkaDriver] setup broker'); + $log = function ($text, ...$args) use ($logger) { + $logger->debug(sprintf('[RdKafkaDriver] '.$text, ...$args)); + }; + + // setup router + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + $log('Create router queue: %s', $routerQueue->getQueueName()); + $this->getContext()->createConsumer($routerQueue); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var RdKafkaTopic $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $log('Create processor queue: %s', $queue->getQueueName()); + $this->getContext()->createConsumer($queue); + } + } +} diff --git a/pkg/enqueue/Client/Driver/RedisDriver.php b/pkg/enqueue/Client/Driver/RedisDriver.php new file mode 100644 index 000000000..493cb7c96 --- /dev/null +++ b/pkg/enqueue/Client/Driver/RedisDriver.php @@ -0,0 +1,20 @@ +debug(sprintf('[SqsQsDriver] '.$text, ...$args)); + }; + + // setup router + $routerTopic = $this->createRouterTopic(); + $log('Declare router topic: %s', $routerTopic->getTopicName()); + $this->getContext()->declareTopic($routerTopic); + + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + $log('Declare router queue: %s', $routerQueue->getQueueName()); + $this->getContext()->declareQueue($routerQueue); + + $log('Bind router queue to topic: %s -> %s', $routerQueue->getQueueName(), $routerTopic->getTopicName()); + $this->getContext()->bind($routerTopic, $routerQueue); + + // setup queues + $declaredQueues = []; + $declaredTopics = []; + foreach ($this->getRouteCollection()->all() as $route) { + $queue = $this->createRouteQueue($route); + if (false === array_key_exists($queue->getQueueName(), $declaredQueues)) { + $log('Declare processor queue: %s', $queue->getQueueName()); + $this->getContext()->declareQueue($queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + + if ($route->isCommand()) { + continue; + } + + $topic = $this->doCreateTopic($this->createTransportQueueName($route->getSource(), true)); + if (false === array_key_exists($topic->getTopicName(), $declaredTopics)) { + $log('Declare processor topic: %s', $topic->getTopicName()); + $this->getContext()->declareTopic($topic); + + $declaredTopics[$topic->getTopicName()] = true; + } + + $log('Bind processor queue to topic: %s -> %s', $queue->getQueueName(), $topic->getTopicName()); + $this->getContext()->bind($topic, $queue); + } + } + + protected function createRouterTopic(): Destination + { + return $this->doCreateTopic( + $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true) + ); + } + + protected function createTransportRouterTopicName(string $name, bool $prefix): string + { + $name = parent::createTransportRouterTopicName($name, $prefix); + + return str_replace('.', '_dot_', $name); + } + + protected function createTransportQueueName(string $name, bool $prefix): string + { + $name = parent::createTransportQueueName($name, $prefix); + + return str_replace('.', '_dot_', $name); + } +} diff --git a/pkg/enqueue/Client/Driver/SqsDriver.php b/pkg/enqueue/Client/Driver/SqsDriver.php new file mode 100644 index 000000000..49b696aae --- /dev/null +++ b/pkg/enqueue/Client/Driver/SqsDriver.php @@ -0,0 +1,62 @@ +debug(sprintf('[SqsDriver] '.$text, ...$args)); + }; + + // setup router + $routerQueue = $this->createQueue($this->getConfig()->getRouterQueue()); + $log('Declare router queue: %s', $routerQueue->getQueueName()); + $this->getContext()->declareQueue($routerQueue); + + // setup queues + $declaredQueues = []; + foreach ($this->getRouteCollection()->all() as $route) { + /** @var SqsDestination $queue */ + $queue = $this->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $declaredQueues)) { + continue; + } + + $log('Declare processor queue: %s', $queue->getQueueName()); + $this->getContext()->declareQueue($queue); + + $declaredQueues[$queue->getQueueName()] = true; + } + } + + protected function createTransportRouterTopicName(string $name, bool $prefix): string + { + $name = parent::createTransportRouterTopicName($name, $prefix); + + return str_replace('.', '_dot_', $name); + } + + protected function createTransportQueueName(string $name, bool $prefix): string + { + $name = parent::createTransportQueueName($name, $prefix); + + return str_replace('.', '_dot_', $name); + } +} diff --git a/pkg/enqueue/Client/Driver/StompDriver.php b/pkg/enqueue/Client/Driver/StompDriver.php new file mode 100644 index 000000000..811ad76e7 --- /dev/null +++ b/pkg/enqueue/Client/Driver/StompDriver.php @@ -0,0 +1,71 @@ +debug('[StompDriver] Stomp protocol does not support broker configuration'); + } + + /** + * @return StompMessage + */ + public function createTransportMessage(Message $message): InteropMessage + { + /** @var StompMessage $transportMessage */ + $transportMessage = parent::createTransportMessage($message); + $transportMessage->setPersistent(true); + + return $transportMessage; + } + + /** + * @return StompDestination + */ + protected function doCreateQueue(string $transportQueueName): InteropQueue + { + /** @var StompDestination $queue */ + $queue = parent::doCreateQueue($transportQueueName); + $queue->setDurable(true); + $queue->setAutoDelete(false); + $queue->setExclusive(false); + + return $queue; + } + + /** + * @return StompDestination + */ + protected function createRouterTopic(): Destination + { + /** @var StompDestination $topic */ + $topic = $this->doCreateTopic( + $this->createTransportRouterTopicName($this->getConfig()->getRouterTopic(), true) + ); + $topic->setDurable(true); + $topic->setAutoDelete(false); + + return $topic; + } +} diff --git a/pkg/enqueue/Client/Driver/StompManagementClient.php b/pkg/enqueue/Client/Driver/StompManagementClient.php new file mode 100644 index 000000000..0d64450dd --- /dev/null +++ b/pkg/enqueue/Client/Driver/StompManagementClient.php @@ -0,0 +1,44 @@ +client = $client; + $this->vhost = $vhost; + } + + public static function create(string $vhost = '/', string $host = 'localhost', int $port = 15672, string $login = 'guest', string $password = 'guest'): self + { + return new self(new Client(null, 'http://'.$host.':'.$port, $login, $password), $vhost); + } + + public function declareQueue(string $name, array $options) + { + return $this->client->queues()->create($this->vhost, $name, $options); + } + + public function declareExchange(string $name, array $options) + { + return $this->client->exchanges()->create($this->vhost, $name, $options); + } + + public function bind(string $exchange, string $queue, ?string $routingKey = null, $arguments = null) + { + return $this->client->bindings()->create($this->vhost, $exchange, $queue, $routingKey, $arguments); + } +} diff --git a/pkg/enqueue/Client/DriverFactory.php b/pkg/enqueue/Client/DriverFactory.php new file mode 100644 index 000000000..1d383ac86 --- /dev/null +++ b/pkg/enqueue/Client/DriverFactory.php @@ -0,0 +1,91 @@ +getTransportOption('dsn'); + + if (empty($dsn)) { + throw new \LogicException('This driver factory relies on dsn option from transport config. The option is empty or not set.'); + } + + $dsn = Dsn::parseFirst($dsn); + + if ($driverInfo = $this->findDriverInfo($dsn, Resources::getAvailableDrivers())) { + $driverClass = $driverInfo['driverClass']; + + if (RabbitMqStompDriver::class === $driverClass) { + return $this->createRabbitMqStompDriver($factory, $dsn, $config, $collection); + } + + return new $driverClass($factory->createContext(), $config, $collection); + } + + $knownDrivers = Resources::getKnownDrivers(); + if ($driverInfo = $this->findDriverInfo($dsn, $knownDrivers)) { + throw new \LogicException(sprintf('To use given scheme "%s" a package has to be installed. Run "composer req %s" to add it.', $dsn->getScheme(), implode(' ', $driverInfo['packages']))); + } + + throw new \LogicException(sprintf('A given scheme "%s" is not supported. Maybe it is a custom driver, make sure you registered it with "%s::addDriver".', $dsn->getScheme(), Resources::class)); + } + + private function findDriverInfo(Dsn $dsn, array $factories): ?array + { + $protocol = $dsn->getSchemeProtocol(); + + if ($dsn->getSchemeExtensions()) { + foreach ($factories as $info) { + if (empty($info['requiredSchemeExtensions'])) { + continue; + } + + if (false == in_array($protocol, $info['schemes'], true)) { + continue; + } + + $diff = array_diff($dsn->getSchemeExtensions(), $info['requiredSchemeExtensions']); + if (empty($diff)) { + return $info; + } + } + } + + foreach ($factories as $driverClass => $info) { + if (false == empty($info['requiredSchemeExtensions'])) { + continue; + } + + if (false == in_array($protocol, $info['schemes'], true)) { + continue; + } + + return $info; + } + + return null; + } + + private function createRabbitMqStompDriver(ConnectionFactory $factory, Dsn $dsn, Config $config, RouteCollection $collection): RabbitMqStompDriver + { + $defaultManagementHost = $dsn->getHost() ?: $config->getTransportOption('host', 'localhost'); + $managementVast = ltrim($dsn->getPath(), '/') ?: $config->getTransportOption('vhost', '/'); + + $managementClient = StompManagementClient::create( + urldecode($managementVast), + $config->getDriverOption('rabbitmq_management_host', $defaultManagementHost), + $config->getDriverOption('rabbitmq_management_port', 15672), + (string) $dsn->getUser() ?: $config->getTransportOption('user', 'guest'), + (string) $dsn->getPassword() ?: $config->getTransportOption('pass', 'guest') + ); + + return new RabbitMqStompDriver($factory->createContext(), $config, $collection, $managementClient); + } +} diff --git a/pkg/enqueue/Client/DriverFactoryInterface.php b/pkg/enqueue/Client/DriverFactoryInterface.php new file mode 100644 index 000000000..698ad05a4 --- /dev/null +++ b/pkg/enqueue/Client/DriverFactoryInterface.php @@ -0,0 +1,10 @@ +message = $message; + $this->producer = $producer; + $this->driver = $driver; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getProducer(): ProducerInterface + { + return $this->producer; + } + + public function getDriver(): DriverInterface + { + return $this->driver; + } + + public function isEvent(): bool + { + return (bool) $this->message->getProperty(Config::TOPIC); + } + + public function isCommand(): bool + { + return (bool) $this->message->getProperty(Config::COMMAND); + } + + public function getCommand(): string + { + return $this->message->getProperty(Config::COMMAND); + } + + public function getTopic(): string + { + return $this->message->getProperty(Config::TOPIC); + } +} diff --git a/pkg/enqueue/Client/DriverPreSendExtensionInterface.php b/pkg/enqueue/Client/DriverPreSendExtensionInterface.php new file mode 100644 index 000000000..fd95c9328 --- /dev/null +++ b/pkg/enqueue/Client/DriverPreSendExtensionInterface.php @@ -0,0 +1,8 @@ +transportDestination = $transportDestination; + $this->transportMessage = $transportMessage; + } + + public function getTransportDestination(): Destination + { + return $this->transportDestination; + } + + public function getTransportMessage(): TransportMessage + { + return $this->transportMessage; + } +} diff --git a/pkg/enqueue/Client/Extension/PrepareBodyExtension.php b/pkg/enqueue/Client/Extension/PrepareBodyExtension.php new file mode 100644 index 000000000..e7924548c --- /dev/null +++ b/pkg/enqueue/Client/Extension/PrepareBodyExtension.php @@ -0,0 +1,51 @@ +prepareBody($context->getMessage()); + } + + public function onPreSendCommand(PreSend $context): void + { + $this->prepareBody($context->getMessage()); + } + + private function prepareBody(Message $message): void + { + $body = $message->getBody(); + $contentType = $message->getContentType(); + + if (is_scalar($body) || null === $body) { + $contentType = $contentType ?: 'text/plain'; + $body = (string) $body; + } elseif (is_array($body)) { + // only array of scalars is allowed. + array_walk_recursive($body, function ($value) { + if (!is_scalar($value) && null !== $value) { + throw new \LogicException(sprintf('The message\'s body must be an array of scalars. Found not scalar in the array: %s', is_object($value) ? $value::class : gettype($value))); + } + }); + + $contentType = $contentType ?: 'application/json'; + $body = JSON::encode($body); + } elseif ($body instanceof \JsonSerializable) { + $contentType = $contentType ?: 'application/json'; + $body = JSON::encode($body); + } else { + throw new \InvalidArgumentException(sprintf('The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: %s', is_object($body) ? $body::class : gettype($body))); + } + + $message->setContentType($contentType); + $message->setBody($body); + } +} diff --git a/pkg/enqueue/Client/ExtensionInterface.php b/pkg/enqueue/Client/ExtensionInterface.php index 4f9fd66ea..596b1b9af 100644 --- a/pkg/enqueue/Client/ExtensionInterface.php +++ b/pkg/enqueue/Client/ExtensionInterface.php @@ -2,21 +2,6 @@ namespace Enqueue\Client; -interface ExtensionInterface +interface ExtensionInterface extends PreSendEventExtensionInterface, PreSendCommandExtensionInterface, DriverPreSendExtensionInterface, PostSendExtensionInterface { - /** - * @param string $topic - * @param Message $message - * - * @return - */ - public function onPreSend($topic, Message $message); - - /** - * @param string $topic - * @param Message $message - * - * @return - */ - public function onPostSend($topic, Message $message); } diff --git a/pkg/enqueue/Client/Message.php b/pkg/enqueue/Client/Message.php index d58371b38..7e51ea10d 100644 --- a/pkg/enqueue/Client/Message.php +++ b/pkg/enqueue/Client/Message.php @@ -7,12 +7,12 @@ class Message /** * @const string */ - const SCOPE_MESSAGE_BUS = 'enqueue.scope.message_bus'; + public const SCOPE_MESSAGE_BUS = 'enqueue.scope.message_bus'; /** * @const string */ - const SCOPE_APP = 'enqueue.scope.app'; + public const SCOPE_APP = 'enqueue.scope.app'; /** * @var string|null @@ -88,7 +88,7 @@ public function __construct($body = '', array $properties = [], array $headers = } /** - * @return null|string + * @return string|null */ public function getBody() { @@ -96,7 +96,7 @@ public function getBody() } /** - * @param null|string|int|float|array|\JsonSerializable $body + * @param string|int|float|array|\JsonSerializable|null $body */ public function setBody($body) { @@ -205,18 +205,12 @@ public function setDelay($delay) $this->delay = $delay; } - /** - * @param string $scope - */ - public function setScope($scope) + public function setScope(string $scope): void { $this->scope = $scope; } - /** - * @return string - */ - public function getScope() + public function getScope(): string { return $this->scope; } @@ -262,10 +256,8 @@ public function getHeaders() } /** - * @param string $name - * @param mixed $default - * - * @return mixed + * @param string $name + * @param mixed|null $default */ public function getHeader($name, $default = null) { @@ -274,16 +266,12 @@ public function getHeader($name, $default = null) /** * @param string $name - * @param mixed $value */ public function setHeader($name, $value) { $this->headers[$name] = $value; } - /** - * @param array $headers - */ public function setHeaders(array $headers) { $this->headers = $headers; @@ -297,19 +285,14 @@ public function getProperties() return $this->properties; } - /** - * @param array $properties - */ public function setProperties(array $properties) { $this->properties = $properties; } /** - * @param string $name - * @param mixed $default - * - * @return mixed + * @param string $name + * @param mixed|null $default */ public function getProperty($name, $default = null) { @@ -318,7 +301,6 @@ public function getProperty($name, $default = null) /** * @param string $name - * @param mixed $value */ public function setProperty($name, $value) { diff --git a/pkg/enqueue/Client/MessagePriority.php b/pkg/enqueue/Client/MessagePriority.php index efa658c14..e14be9a7d 100644 --- a/pkg/enqueue/Client/MessagePriority.php +++ b/pkg/enqueue/Client/MessagePriority.php @@ -4,9 +4,9 @@ class MessagePriority { - const VERY_LOW = 'enqueue.message_queue.client.very_low_message_priority'; - const LOW = 'enqueue.message_queue.client.low_message_priority'; - const NORMAL = 'enqueue.message_queue.client.normal_message_priority'; - const HIGH = 'enqueue.message_queue.client.high_message_priority'; - const VERY_HIGH = 'enqueue.message_queue.client.very_high_message_priority'; + public const VERY_LOW = 'enqueue.message_queue.client.very_low_message_priority'; + public const LOW = 'enqueue.message_queue.client.low_message_priority'; + public const NORMAL = 'enqueue.message_queue.client.normal_message_priority'; + public const HIGH = 'enqueue.message_queue.client.high_message_priority'; + public const VERY_HIGH = 'enqueue.message_queue.client.very_high_message_priority'; } diff --git a/pkg/enqueue/Client/Meta/QueueMeta.php b/pkg/enqueue/Client/Meta/QueueMeta.php deleted file mode 100644 index bee32bd81..000000000 --- a/pkg/enqueue/Client/Meta/QueueMeta.php +++ /dev/null @@ -1,57 +0,0 @@ -clientName = $clientName; - $this->transportName = $transportName; - $this->processors = $processors; - } - - /** - * @return string - */ - public function getClientName() - { - return $this->clientName; - } - - /** - * @return string - */ - public function getTransportName() - { - return $this->transportName; - } - - /** - * @return string[] - */ - public function getProcessors() - { - return $this->processors; - } -} diff --git a/pkg/enqueue/Client/Meta/QueueMetaRegistry.php b/pkg/enqueue/Client/Meta/QueueMetaRegistry.php deleted file mode 100644 index 29c2d69a3..000000000 --- a/pkg/enqueue/Client/Meta/QueueMetaRegistry.php +++ /dev/null @@ -1,95 +0,0 @@ - [ - * 'transportName' => 'aTransportQueueName', - * 'processors' => ['aFooProcessorName', 'aBarProcessorName'], - * ] - * ]. - * - * - * @param Config $config - * @param array $meta - */ - public function __construct(Config $config, array $meta) - { - $this->config = $config; - $this->meta = $meta; - } - - /** - * @param string $queueName - * @param string|null $transportName - */ - public function add($queueName, $transportName = null) - { - $this->meta[$queueName] = [ - 'transportName' => $transportName, - 'processors' => [], - ]; - } - - /** - * @param string $queueName - * @param string $processorName - */ - public function addProcessor($queueName, $processorName) - { - if (false == array_key_exists($queueName, $this->meta)) { - $this->add($queueName); - } - - $this->meta[$queueName]['processors'][] = $processorName; - } - - /** - * @param string $queueName - * - * @return QueueMeta - */ - public function getQueueMeta($queueName) - { - if (false == array_key_exists($queueName, $this->meta)) { - throw new \InvalidArgumentException(sprintf( - 'The queue meta not found. Requested name `%s`', - $queueName - )); - } - - $transportName = $this->config->createTransportQueueName($queueName); - - $meta = array_replace([ - 'processors' => [], - 'transportName' => $transportName, - ], array_filter($this->meta[$queueName])); - - return new QueueMeta($queueName, $meta['transportName'], $meta['processors']); - } - - /** - * @return \Generator|QueueMeta[] - */ - public function getQueuesMeta() - { - foreach (array_keys($this->meta) as $queueName) { - yield $this->getQueueMeta($queueName); - } - } -} diff --git a/pkg/enqueue/Client/Meta/TopicMeta.php b/pkg/enqueue/Client/Meta/TopicMeta.php deleted file mode 100644 index abb0e33ed..000000000 --- a/pkg/enqueue/Client/Meta/TopicMeta.php +++ /dev/null @@ -1,57 +0,0 @@ -name = $name; - $this->description = $description; - $this->processors = $processors; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @return string - */ - public function getDescription() - { - return $this->description; - } - - /** - * @return string[] - */ - public function getProcessors() - { - return $this->processors; - } -} diff --git a/pkg/enqueue/Client/Meta/TopicMetaRegistry.php b/pkg/enqueue/Client/Meta/TopicMetaRegistry.php deleted file mode 100644 index efceb9a11..000000000 --- a/pkg/enqueue/Client/Meta/TopicMetaRegistry.php +++ /dev/null @@ -1,80 +0,0 @@ - [ - * 'description' => 'A desc', - * 'processors' => ['aProcessorNameFoo', 'aProcessorNameBar], - * ], - * ]. - * - * @param array $meta - */ - public function __construct(array $meta) - { - $this->meta = $meta; - } - - /** - * @param string $topicName - * @param string $description - */ - public function add($topicName, $description = null) - { - $this->meta[$topicName] = [ - 'description' => $description, - 'processors' => [], - ]; - } - - /** - * @param string $topicName - * @param string $processorName - */ - public function addProcessor($topicName, $processorName) - { - if (false == array_key_exists($topicName, $this->meta)) { - $this->add($topicName); - } - - $this->meta[$topicName]['processors'][] = $processorName; - } - - /** - * @param string $topicName - * - * @return TopicMeta - */ - public function getTopicMeta($topicName) - { - if (false == array_key_exists($topicName, $this->meta)) { - throw new \InvalidArgumentException(sprintf('The topic meta not found. Requested name `%s`', $topicName)); - } - - $topic = array_replace([ - 'description' => '', - 'processors' => [], - ], $this->meta[$topicName]); - - return new TopicMeta($topicName, $topic['description'], $topic['processors']); - } - - /** - * @return \Generator|TopicMeta[] - */ - public function getTopicsMeta() - { - foreach (array_keys($this->meta) as $topicName) { - yield $this->getTopicMeta($topicName); - } - } -} diff --git a/pkg/enqueue/Client/PostSend.php b/pkg/enqueue/Client/PostSend.php new file mode 100644 index 000000000..5d9526ea4 --- /dev/null +++ b/pkg/enqueue/Client/PostSend.php @@ -0,0 +1,78 @@ +message = $message; + $this->producer = $producer; + $this->driver = $driver; + $this->transportDestination = $transportDestination; + $this->transportMessage = $transportMessage; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getProducer(): ProducerInterface + { + return $this->producer; + } + + public function getDriver(): DriverInterface + { + return $this->driver; + } + + public function getTransportDestination(): Destination + { + return $this->transportDestination; + } + + public function getTransportMessage(): TransportMessage + { + return $this->transportMessage; + } + + public function isEvent(): bool + { + return (bool) $this->message->getProperty(Config::TOPIC); + } + + public function isCommand(): bool + { + return (bool) $this->message->getProperty(Config::COMMAND); + } + + public function getCommand(): string + { + return $this->message->getProperty(Config::COMMAND); + } + + public function getTopic(): string + { + return $this->message->getProperty(Config::TOPIC); + } +} diff --git a/pkg/enqueue/Client/PostSendExtensionInterface.php b/pkg/enqueue/Client/PostSendExtensionInterface.php new file mode 100644 index 000000000..dd3ca8b71 --- /dev/null +++ b/pkg/enqueue/Client/PostSendExtensionInterface.php @@ -0,0 +1,8 @@ +message = $message; + $this->commandOrTopic = $commandOrTopic; + $this->producer = $producer; + $this->driver = $driver; + + $this->originalMessage = clone $message; + } + + public function getCommand(): string + { + return $this->commandOrTopic; + } + + public function getTopic(): string + { + return $this->commandOrTopic; + } + + public function changeCommand(string $newCommand): void + { + $this->commandOrTopic = $newCommand; + } + + public function changeTopic(string $newTopic): void + { + $this->commandOrTopic = $newTopic; + } + + public function changeBody($body, ?string $contentType = null): void + { + $this->message->setBody($body); + + if (null !== $contentType) { + $this->message->setContentType($contentType); + } + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getOriginalMessage(): Message + { + return $this->originalMessage; + } + + public function getProducer(): ProducerInterface + { + return $this->producer; + } + + public function getDriver(): DriverInterface + { + return $this->driver; + } +} diff --git a/pkg/enqueue/Client/PreSendCommandExtensionInterface.php b/pkg/enqueue/Client/PreSendCommandExtensionInterface.php new file mode 100644 index 000000000..cefec097f --- /dev/null +++ b/pkg/enqueue/Client/PreSendCommandExtensionInterface.php @@ -0,0 +1,11 @@ +driver = $driver; $this->rpcFactory = $rpcFactory; - $this->extension = $extension ?: new ChainExtension([]); + + $this->extension = $extension ? + new ChainExtension([$extension, new PrepareBodyExtension()]) : + new ChainExtension([new PrepareBodyExtension()]) + ; } - /** - * {@inheritdoc} - */ - public function sendEvent($topic, $message) + public function sendEvent(string $topic, $message): void { if (false == $message instanceof Message) { - $body = $message; - $message = new Message(); - $message->setBody($body); - } - - $this->prepareBody($message); - - $message->setProperty(Config::PARAMETER_TOPIC_NAME, $topic); - $message->setProperty(self::TOPIC_09X, $topic); - - if (!$message->getMessageId()) { - $message->setMessageId(UUID::generate()); - } - - if (!$message->getTimestamp()) { - $message->setTimestamp(time()); - } - - if (!$message->getPriority()) { - $message->setPriority(MessagePriority::NORMAL); + $message = new Message($message); } - if (Message::SCOPE_MESSAGE_BUS == $message->getScope()) { - if ($message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException(sprintf('The %s property must not be set for messages that are sent to message bus.', Config::PARAMETER_PROCESSOR_QUEUE_NAME)); - } - if ($message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException(sprintf('The %s property must not be set for messages that are sent to message bus.', Config::PARAMETER_PROCESSOR_NAME)); - } + $preSend = new PreSend($topic, $message, $this, $this->driver); + $this->extension->onPreSendEvent($preSend); - $this->extension->onPreSend($topic, $message); - $this->driver->sendToRouter($message); - $this->extension->onPostSend($topic, $message); - } elseif (Message::SCOPE_APP == $message->getScope()) { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, $this->driver->getConfig()->getRouterProcessorName()); - } - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $this->driver->getConfig()->getRouterQueueName()); - } + $message = $preSend->getMessage(); + $message->setProperty(Config::TOPIC, $preSend->getTopic()); - $this->extension->onPreSend($topic, $message); - $this->driver->sendToProcessor($message); - $this->extension->onPostSend($topic, $message); - } else { - throw new \LogicException(sprintf('The message scope "%s" is not supported.', $message->getScope())); - } + $this->doSend($message); } - /** - * {@inheritdoc} - */ - public function sendCommand($command, $message, $needReply = false) + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise { if (false == $message instanceof Message) { $message = new Message($message); } + $preSend = new PreSend($command, $message, $this, $this->driver); + $this->extension->onPreSendCommand($preSend); + + $command = $preSend->getCommand(); + $message = $preSend->getMessage(); + $deleteReplyQueue = false; $replyTo = $message->getReplyTo(); @@ -124,12 +79,10 @@ public function sendCommand($command, $message, $needReply = false) } } - $message->setProperty(Config::PARAMETER_TOPIC_NAME, Config::COMMAND_TOPIC); - $message->setProperty(Config::PARAMETER_COMMAND_NAME, $command); - $message->setProperty(self::COMMAND_09X, $command); + $message->setProperty(Config::COMMAND, $command); $message->setScope(Message::SCOPE_APP); - $this->sendEvent(Config::COMMAND_TOPIC, $message); + $this->doSend($message); if ($needReply) { $promise = $this->rpcFactory->createPromise($replyTo, $message->getCorrelationId(), 60000); @@ -137,59 +90,38 @@ public function sendCommand($command, $message, $needReply = false) return $promise; } - } - /** - * {@inheritdoc} - */ - public function send($topic, $message) - { - $this->sendEvent($topic, $message); + return null; } - /** - * @param Message $message - */ - private function prepareBody(Message $message) + private function doSend(Message $message): void { - $body = $message->getBody(); - $contentType = $message->getContentType(); - - if (is_scalar($body) || null === $body) { - $contentType = $contentType ?: 'text/plain'; - $body = (string) $body; - } elseif (is_array($body)) { - if ($contentType && 'application/json' !== $contentType) { - throw new \LogicException(sprintf('Content type "application/json" only allowed when body is array')); - } + if (false === is_string($message->getBody())) { + throw new \LogicException(sprintf('The message body must be string at this stage, got "%s". Make sure you passed string as message or there is an extension that converts custom input to string.', is_object($message->getBody()) ? get_class($message->getBody()) : gettype($message->getBody()))); + } - // only array of scalars is allowed. - array_walk_recursive($body, function ($value) { - if (!is_scalar($value) && null !== $value) { - throw new \LogicException(sprintf( - 'The message\'s body must be an array of scalars. Found not scalar in the array: %s', - is_object($value) ? get_class($value) : gettype($value) - )); - } - }); - - $contentType = 'application/json'; - $body = JSON::encode($body); - } elseif ($body instanceof \JsonSerializable) { - if ($contentType && 'application/json' !== $contentType) { - throw new \LogicException(sprintf('Content type "application/json" only allowed when body is array')); - } + if ($message->getProperty(Config::PROCESSOR)) { + throw new \LogicException(sprintf('The %s property must not be set.', Config::PROCESSOR)); + } + + if (!$message->getMessageId()) { + $message->setMessageId(UUID::generate()); + } + + if (!$message->getTimestamp()) { + $message->setTimestamp(time()); + } + + $this->extension->onDriverPreSend(new DriverPreSend($message, $this, $this->driver)); - $contentType = 'application/json'; - $body = JSON::encode($body); + if (Message::SCOPE_MESSAGE_BUS == $message->getScope()) { + $result = $this->driver->sendToRouter($message); + } elseif (Message::SCOPE_APP == $message->getScope()) { + $result = $this->driver->sendToProcessor($message); } else { - throw new \InvalidArgumentException(sprintf( - 'The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: %s', - is_object($body) ? get_class($body) : gettype($body) - )); + throw new \LogicException(sprintf('The message scope "%s" is not supported.', $message->getScope())); } - $message->setContentType($contentType); - $message->setBody($body); + $this->extension->onPostSend(new PostSend($message, $this, $this->driver, $result->getTransportDestination(), $result->getTransportMessage())); } } diff --git a/pkg/enqueue/Client/ProducerInterface.php b/pkg/enqueue/Client/ProducerInterface.php index 2fc829eb2..3c884808a 100644 --- a/pkg/enqueue/Client/ProducerInterface.php +++ b/pkg/enqueue/Client/ProducerInterface.php @@ -7,17 +7,21 @@ interface ProducerInterface { /** - * @param string $topic + * The message could be pretty much everything as long as you have a client extension that transforms a body to string on onPreSendEvent. + * * @param string|array|Message $message + * + * @throws \Exception */ - public function sendEvent($topic, $message); + public function sendEvent(string $topic, $message): void; /** - * @param string $command + * The message could be pretty much everything as long as you have a client extension that transforms a body to string on onPreSendCommand. + * The promise is returned if needReply argument is true. + * * @param string|array|Message $message - * @param bool $needReply * - * @return Promise|null the promise is returned if needReply argument is true + * @throws \Exception */ - public function sendCommand($command, $message, $needReply = false); + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise; } diff --git a/pkg/enqueue/Client/Resources.php b/pkg/enqueue/Client/Resources.php new file mode 100644 index 000000000..a5cc6847c --- /dev/null +++ b/pkg/enqueue/Client/Resources.php @@ -0,0 +1,194 @@ + [ + * schemes => [schemes strings], + * package => package name, + * ]. + * + * @var array + */ + private static $knownDrivers; + + private function __construct() + { + } + + public static function getAvailableDrivers(): array + { + $map = self::getKnownDrivers(); + + $availableMap = []; + foreach ($map as $item) { + if (class_exists($item['driverClass'])) { + $availableMap[] = $item; + } + } + + return $availableMap; + } + + public static function getKnownDrivers(): array + { + if (null === self::$knownDrivers) { + $map = []; + + $map[] = [ + 'schemes' => ['amqp', 'amqps'], + 'driverClass' => AmqpDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/amqp-bunny'], + ]; + $map[] = [ + 'schemes' => ['amqp', 'amqps'], + 'driverClass' => RabbitMqDriver::class, + 'requiredSchemeExtensions' => ['rabbitmq'], + 'packages' => ['enqueue/enqueue', 'enqueue/amqp-bunny'], + ]; + $map[] = [ + 'schemes' => ['file'], + 'driverClass' => FsDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/fs'], + ]; + $map[] = [ + 'schemes' => ['null'], + 'driverClass' => GenericDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/null'], + ]; + $map[] = [ + 'schemes' => ['gps'], + 'driverClass' => GpsDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/gps'], + ]; + $map[] = [ + 'schemes' => ['redis', 'rediss'], + 'driverClass' => RedisDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/redis'], + ]; + $map[] = [ + 'schemes' => ['sqs'], + 'driverClass' => SqsDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/sqs'], + ]; + $map[] = [ + 'schemes' => ['sns'], + 'driverClass' => GenericDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/sns'], + ]; + $map[] = [ + 'schemes' => ['snsqs'], + 'driverClass' => SnsQsDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/sqs', 'enqueue/sns', 'enqueue/snsqs'], + ]; + $map[] = [ + 'schemes' => ['stomp'], + 'driverClass' => StompDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/stomp'], + ]; + $map[] = [ + 'schemes' => ['stomp'], + 'driverClass' => RabbitMqStompDriver::class, + 'requiredSchemeExtensions' => ['rabbitmq'], + 'packages' => ['enqueue/enqueue', 'enqueue/stomp'], + ]; + $map[] = [ + 'schemes' => ['kafka', 'rdkafka'], + 'driverClass' => RdKafkaDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/rdkafka'], + ]; + $map[] = [ + 'schemes' => ['mongodb'], + 'driverClass' => MongodbDriver::class, + 'requiredSchemeExtensions' => [], + 'packages' => ['enqueue/enqueue', 'enqueue/mongodb'], + ]; + $map[] = [ + 'schemes' => [ + 'db2', + 'ibm-db2', + 'mssql', + 'sqlsrv', + 'mysql', + 'mysql2', + 'mysql', + 'pgsql', + 'postgres', + 'pgsql', + 'sqlite', + 'sqlite3', + 'sqlite', + ], + 'driverClass' => DbalDriver::class, + 'requiredSchemeExtensions' => [], + 'package' => ['enqueue/enqueue', 'enqueue/dbal'], + ]; + $map[] = [ + 'schemes' => ['gearman'], + 'driverClass' => GenericDriver::class, + 'requiredSchemeExtensions' => [], + 'package' => ['enqueue/enqueue', 'enqueue/gearman'], + ]; + $map[] = [ + 'schemes' => ['beanstalk'], + 'driverClass' => GenericDriver::class, + 'requiredSchemeExtensions' => [], + 'package' => ['enqueue/enqueue', 'enqueue/pheanstalk'], + ]; + + self::$knownDrivers = $map; + } + + return self::$knownDrivers; + } + + public static function addDriver(string $driverClass, array $schemes, array $requiredExtensions, array $packages): void + { + if (class_exists($driverClass)) { + if (false == (new \ReflectionClass($driverClass))->implementsInterface(DriverInterface::class)) { + throw new \InvalidArgumentException(sprintf('The driver class "%s" must implement "%s" interface.', $driverClass, DriverInterface::class)); + } + } + + if (empty($schemes)) { + throw new \InvalidArgumentException('Schemes could not be empty.'); + } + if (empty($packages)) { + throw new \InvalidArgumentException('Packages could not be empty.'); + } + + self::getKnownDrivers(); + self::$knownDrivers[] = [ + 'schemes' => $schemes, + 'driverClass' => $driverClass, + 'requiredSchemeExtensions' => $requiredExtensions, + 'packages' => $packages, + ]; + } +} diff --git a/pkg/enqueue/Client/Route.php b/pkg/enqueue/Client/Route.php new file mode 100644 index 000000000..8b9e31e36 --- /dev/null +++ b/pkg/enqueue/Client/Route.php @@ -0,0 +1,114 @@ +source = $source; + $this->sourceType = $sourceType; + $this->processor = $processor; + $this->options = $options; + } + + public function getSource(): string + { + return $this->source; + } + + public function isCommand(): bool + { + return self::COMMAND === $this->sourceType; + } + + public function isTopic(): bool + { + return self::TOPIC === $this->sourceType; + } + + public function getProcessor(): string + { + return $this->processor; + } + + public function isProcessorExclusive(): bool + { + return (bool) $this->getOption('exclusive', false); + } + + public function isProcessorExternal(): bool + { + return (bool) $this->getOption('external', false); + } + + public function getQueue(): ?string + { + return $this->getOption('queue'); + } + + public function isPrefixQueue(): bool + { + return (bool) $this->getOption('prefix_queue', true); + } + + public function getOptions(): array + { + return $this->options; + } + + public function getOption(string $name, $default = null) + { + return array_key_exists($name, $this->options) ? $this->options[$name] : $default; + } + + public function toArray(): array + { + return array_replace($this->options, [ + 'source' => $this->source, + 'source_type' => $this->sourceType, + 'processor' => $this->processor, + ]); + } + + public static function fromArray(array $route): self + { + list( + 'source' => $source, + 'source_type' => $sourceType, + 'processor' => $processor) = $route; + + unset($route['source'], $route['source_type'], $route['processor']); + $options = $route; + + return new self($source, $sourceType, $processor, $options); + } +} diff --git a/pkg/enqueue/Client/RouteCollection.php b/pkg/enqueue/Client/RouteCollection.php new file mode 100644 index 000000000..76bcbe451 --- /dev/null +++ b/pkg/enqueue/Client/RouteCollection.php @@ -0,0 +1,114 @@ +routes = $routes; + } + + public function add(Route $route): void + { + $this->routes[] = $route; + $this->topicRoutes = null; + $this->commandRoutes = null; + } + + /** + * @return Route[] + */ + public function all(): array + { + return $this->routes; + } + + /** + * @return Route[] + */ + public function command(string $command): ?Route + { + if (null === $this->commandRoutes) { + $commandRoutes = []; + foreach ($this->routes as $route) { + if ($route->isCommand()) { + $commandRoutes[$route->getSource()] = $route; + } + } + + $this->commandRoutes = $commandRoutes; + } + + return array_key_exists($command, $this->commandRoutes) ? $this->commandRoutes[$command] : null; + } + + /** + * @return Route[] + */ + public function topic(string $topic): array + { + if (null === $this->topicRoutes) { + $topicRoutes = []; + foreach ($this->routes as $route) { + if ($route->isTopic()) { + $topicRoutes[$route->getSource()][$route->getProcessor()] = $route; + } + } + + $this->topicRoutes = $topicRoutes; + } + + return array_key_exists($topic, $this->topicRoutes) ? $this->topicRoutes[$topic] : []; + } + + public function topicAndProcessor(string $topic, string $processor): ?Route + { + $routes = $this->topic($topic); + foreach ($routes as $route) { + if ($route->getProcessor() === $processor) { + return $route; + } + } + + return null; + } + + public function toArray(): array + { + $rawRoutes = []; + foreach ($this->routes as $route) { + $rawRoutes[] = $route->toArray(); + } + + return $rawRoutes; + } + + public static function fromArray(array $rawRoutes): self + { + $routes = []; + foreach ($rawRoutes as $rawRoute) { + $routes[] = Route::fromArray($rawRoute); + } + + return new self($routes); + } +} diff --git a/pkg/enqueue/Client/RouterProcessor.php b/pkg/enqueue/Client/RouterProcessor.php index 35efe1b02..c441ceb8b 100644 --- a/pkg/enqueue/Client/RouterProcessor.php +++ b/pkg/enqueue/Client/RouterProcessor.php @@ -3,119 +3,62 @@ namespace Enqueue\Client; use Enqueue\Consumption\Result; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class RouterProcessor implements PsrProcessor +final class RouterProcessor implements Processor { /** - * @var DriverInterface - */ - private $driver; - - /** - * @var array + * compatibility with 0.8x. */ - private $eventRoutes; + private const COMMAND_TOPIC_08X = '__command__'; /** - * @var array + * @var DriverInterface */ - private $commandRoutes; + private $driver; - /** - * @param DriverInterface $driver - * @param array $eventRoutes - * @param array $commandRoutes - */ - public function __construct(DriverInterface $driver, array $eventRoutes = [], array $commandRoutes = []) + public function __construct(DriverInterface $driver) { $this->driver = $driver; - - $this->eventRoutes = $eventRoutes; - $this->commandRoutes = $commandRoutes; } - /** - * @param string $topicName - * @param string $queueName - * @param string $processorName - */ - public function add($topicName, $queueName, $processorName) + public function process(InteropMessage $message, Context $context): Result { - if (Config::COMMAND_TOPIC === $topicName) { - $this->commandRoutes[$processorName] = $queueName; - } else { - $this->eventRoutes[$topicName][] = [$processorName, $queueName]; + // compatibility with 0.8x + if (self::COMMAND_TOPIC_08X === $message->getProperty(Config::TOPIC)) { + $clientMessage = $this->driver->createClientMessage($message); + $clientMessage->setProperty(Config::TOPIC, null); + + $this->driver->sendToProcessor($clientMessage); + + return Result::ack('Legacy 0.8x message routed to processor'); } - } + // compatibility with 0.8x - /** - * {@inheritdoc} - */ - public function process(PsrMessage $message, PsrContext $context) - { - $topicName = $message->getProperty(Config::PARAMETER_TOPIC_NAME); - if (false == $topicName) { + if ($message->getProperty(Config::COMMAND)) { return Result::reject(sprintf( - 'Got message without required parameter: "%s"', - Config::PARAMETER_TOPIC_NAME + 'Unexpected command "%s" got. Command must not go to the router.', + $message->getProperty(Config::COMMAND) )); } - if (Config::COMMAND_TOPIC === $topicName) { - return $this->routeCommand($message); + $topic = $message->getProperty(Config::TOPIC); + if (false == $topic) { + return Result::reject(sprintf('Topic property "%s" is required but not set or empty.', Config::TOPIC)); } - return $this->routeEvent($message); - } - - /** - * @param PsrMessage $message - * - * @return string|Result - */ - private function routeEvent(PsrMessage $message) - { - $topicName = $message->getProperty(Config::PARAMETER_TOPIC_NAME); - - if (array_key_exists($topicName, $this->eventRoutes)) { - foreach ($this->eventRoutes[$topicName] as $route) { - $processorMessage = clone $message; - $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_NAME, $route[0]); - $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $route[1]); - - $this->driver->sendToProcessor($this->driver->createClientMessage($processorMessage)); - } - } - - return self::ACK; - } - - /** - * @param PsrMessage $message - * - * @return string|Result - */ - private function routeCommand(PsrMessage $message) - { - $commandName = $message->getProperty(Config::PARAMETER_COMMAND_NAME); - if (false == $commandName) { - return Result::reject(sprintf( - 'Got message without required parameter: "%s"', - Config::PARAMETER_COMMAND_NAME - )); - } + $count = 0; + foreach ($this->driver->getRouteCollection()->topic($topic) as $route) { + $clientMessage = $this->driver->createClientMessage($message); + $clientMessage->setProperty(Config::PROCESSOR, $route->getProcessor()); - if (isset($this->commandRoutes[$commandName])) { - $processorMessage = clone $message; - $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, $this->commandRoutes[$commandName]); - $processorMessage->setProperty(Config::PARAMETER_PROCESSOR_NAME, $commandName); + $this->driver->sendToProcessor($clientMessage); - $this->driver->sendToProcessor($this->driver->createClientMessage($processorMessage)); + ++$count; } - return self::ACK; + return Result::ack(sprintf('Routed to "%d" event subscribers', $count)); } } diff --git a/pkg/enqueue/Client/SpoolProducer.php b/pkg/enqueue/Client/SpoolProducer.php index c5388770f..8ad0940f5 100644 --- a/pkg/enqueue/Client/SpoolProducer.php +++ b/pkg/enqueue/Client/SpoolProducer.php @@ -2,6 +2,8 @@ namespace Enqueue\Client; +use Enqueue\Rpc\Promise; + class SpoolProducer implements ProducerInterface { /** @@ -19,9 +21,6 @@ class SpoolProducer implements ProducerInterface */ private $commands; - /** - * @param ProducerInterface $realProducer - */ public function __construct(ProducerInterface $realProducer) { $this->realProducer = $realProducer; @@ -30,38 +29,26 @@ public function __construct(ProducerInterface $realProducer) $this->commands = new \SplQueue(); } - /** - * {@inheritdoc} - */ - public function sendCommand($command, $message, $needReply = false) + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise { if ($needReply) { return $this->realProducer->sendCommand($command, $message, $needReply); } $this->commands->enqueue([$command, $message]); - } - /** - * {@inheritdoc} - */ - public function sendEvent($topic, $message) - { - $this->events->enqueue([$topic, $message]); + return null; } - /** - * {@inheritdoc} - */ - public function send($topic, $message) + public function sendEvent(string $topic, $message): void { - $this->sendEvent($topic, $message); + $this->events->enqueue([$topic, $message]); } /** * When it is called it sends all previously queued messages. */ - public function flush() + public function flush(): void { while (false == $this->events->isEmpty()) { list($topic, $message) = $this->events->dequeue(); diff --git a/pkg/enqueue/Client/TopicSubscriberInterface.php b/pkg/enqueue/Client/TopicSubscriberInterface.php index bdaa43f61..849a7827f 100644 --- a/pkg/enqueue/Client/TopicSubscriberInterface.php +++ b/pkg/enqueue/Client/TopicSubscriberInterface.php @@ -7,21 +7,35 @@ interface TopicSubscriberInterface /** * The result maybe either:. * - * ['aTopicName'] + * 'aTopicName' * * or * - * ['aTopicName' => [ - * 'processorName' => 'processor', - * 'queueName' => 'a_client_queue_name', - * 'queueNameHardcoded' => true, - * ]] + * ['aTopicName', 'anotherTopicName'] * - * processorName, queueName and queueNameHardcoded are optional. + * or + * + * [ + * [ + * 'topic' => 'aTopicName', + * 'processor' => 'fooProcessor', + * 'queue' => 'a_client_queue_name', + * + * 'aCustomOption' => 'aVal', + * ], + * [ + * 'topic' => 'anotherTopicName', + * 'processor' => 'barProcessor', + * 'queue' => 'a_client_queue_name', + * + * 'aCustomOption' => 'aVal', + * ], + * ] * - * Note: If you set queueNameHardcoded to true then the queueName is used as is and therefor the driver is not used to create a transport queue name. + * Note: If you set prefix_queue to true then the queue is used as is and therefor the driver is not used to prepare a transport queue name. + * It is possible to pass other options, they could be accessible on a route instance through options. * - * @return array + * @return string|array */ public static function getSubscribedTopics(); } diff --git a/pkg/enqueue/Client/TraceableProducer.php b/pkg/enqueue/Client/TraceableProducer.php index cd55a91d6..b0bd613c3 100644 --- a/pkg/enqueue/Client/TraceableProducer.php +++ b/pkg/enqueue/Client/TraceableProducer.php @@ -2,61 +2,42 @@ namespace Enqueue\Client; -class TraceableProducer implements ProducerInterface +use Enqueue\Rpc\Promise; + +final class TraceableProducer implements ProducerInterface { /** * @var array */ - protected $traces = []; + private $traces = []; + /** * @var ProducerInterface */ private $producer; - /** - * @param ProducerInterface $producer - */ public function __construct(ProducerInterface $producer) { $this->producer = $producer; } - /** - * {@inheritdoc} - */ - public function sendEvent($topic, $message) + public function sendEvent(string $topic, $message): void { $this->producer->sendEvent($topic, $message); $this->collectTrace($topic, null, $message); } - /** - * {@inheritdoc} - */ - public function sendCommand($command, $message, $needReply = false) + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise { $result = $this->producer->sendCommand($command, $message, $needReply); - $this->collectTrace(Config::COMMAND_TOPIC, $command, $message); + $this->collectTrace(null, $command, $message); return $result; } - /** - * {@inheritdoc} - */ - public function send($topic, $message) - { - $this->sendEvent($topic, $message); - } - - /** - * @param string $topic - * - * @return array - */ - public function getTopicTraces($topic) + public function getTopicTraces(string $topic): array { $topicTraces = []; foreach ($this->traces as $trace) { @@ -68,12 +49,7 @@ public function getTopicTraces($topic) return $topicTraces; } - /** - * @param string $command - * - * @return array - */ - public function getCommandTraces($command) + public function getCommandTraces(string $command): array { $commandTraces = []; foreach ($this->traces as $trace) { @@ -85,25 +61,17 @@ public function getCommandTraces($command) return $commandTraces; } - /** - * @return array - */ - public function getTraces() + public function getTraces(): array { return $this->traces; } - public function clearTraces() + public function clearTraces(): void { $this->traces = []; } - /** - * @param string|null $topic - * @param string|null $command - * @param mixed $message - */ - private function collectTrace($topic, $command, $message) + private function collectTrace(?string $topic, ?string $command, $message): void { $trace = [ 'topic' => $topic, @@ -117,7 +85,9 @@ private function collectTrace($topic, $command, $message) 'timestamp' => null, 'contentType' => null, 'messageId' => null, + 'sentAt' => (new \DateTime())->format('Y-m-d H:i:s.u'), ]; + if ($message instanceof Message) { $trace['body'] = $message->getBody(); $trace['headers'] = $message->getHeaders(); diff --git a/pkg/enqueue/ConnectionFactoryFactory.php b/pkg/enqueue/ConnectionFactoryFactory.php new file mode 100644 index 000000000..d23518c1b --- /dev/null +++ b/pkg/enqueue/ConnectionFactoryFactory.php @@ -0,0 +1,69 @@ + $config]; + } + + if (false == is_array($config)) { + throw new \InvalidArgumentException('The config must be either array or DSN string.'); + } + + if (false == array_key_exists('dsn', $config)) { + throw new \InvalidArgumentException('The config must have dsn key set.'); + } + + $dsn = Dsn::parseFirst($config['dsn']); + + if ($factoryClass = $this->findFactoryClass($dsn, Resources::getAvailableConnections())) { + return new $factoryClass(1 === count($config) ? $config['dsn'] : $config); + } + + $knownConnections = Resources::getKnownConnections(); + if ($factoryClass = $this->findFactoryClass($dsn, $knownConnections)) { + throw new \LogicException(sprintf('To use given scheme "%s" a package has to be installed. Run "composer req %s" to add it.', $dsn->getScheme(), $knownConnections[$factoryClass]['package'])); + } + + throw new \LogicException(sprintf('A given scheme "%s" is not supported. Maybe it is a custom connection, make sure you registered it with "%s::addConnection".', $dsn->getScheme(), Resources::class)); + } + + private function findFactoryClass(Dsn $dsn, array $factories): ?string + { + $protocol = $dsn->getSchemeProtocol(); + + if ($dsn->getSchemeExtensions()) { + foreach ($factories as $connectionClass => $info) { + if (empty($info['supportedSchemeExtensions'])) { + continue; + } + + if (false == in_array($protocol, $info['schemes'], true)) { + continue; + } + + $diff = array_diff($info['supportedSchemeExtensions'], $dsn->getSchemeExtensions()); + if (empty($diff)) { + return $connectionClass; + } + } + } + + foreach ($factories as $driverClass => $info) { + if (false == in_array($protocol, $info['schemes'], true)) { + continue; + } + + return $driverClass; + } + + return null; + } +} diff --git a/pkg/enqueue/ConnectionFactoryFactoryInterface.php b/pkg/enqueue/ConnectionFactoryFactoryInterface.php new file mode 100644 index 000000000..f4ca4a6d3 --- /dev/null +++ b/pkg/enqueue/ConnectionFactoryFactoryInterface.php @@ -0,0 +1,21 @@ +queue = $queue; + $this->processor = $processor; + } + + public function getQueue(): Queue + { + return $this->queue; + } + + public function getProcessor(): Processor + { + return $this->processor; + } +} diff --git a/pkg/enqueue/Consumption/CallbackProcessor.php b/pkg/enqueue/Consumption/CallbackProcessor.php index b002235ba..d15978fcb 100644 --- a/pkg/enqueue/Consumption/CallbackProcessor.php +++ b/pkg/enqueue/Consumption/CallbackProcessor.php @@ -2,29 +2,23 @@ namespace Enqueue\Consumption; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class CallbackProcessor implements PsrProcessor +class CallbackProcessor implements Processor { /** * @var callable */ private $callback; - /** - * @param callable $callback - */ public function __construct(callable $callback) { $this->callback = $callback; } - /** - * {@inheritdoc} - */ - public function process(PsrMessage $message, PsrContext $context) + public function process(InteropMessage $message, Context $context) { return call_user_func($this->callback, $message, $context); } diff --git a/pkg/enqueue/Consumption/ChainExtension.php b/pkg/enqueue/Consumption/ChainExtension.php index d7a24adc7..83b4eba3a 100644 --- a/pkg/enqueue/Consumption/ChainExtension.php +++ b/pkg/enqueue/Consumption/ChainExtension.php @@ -2,90 +2,193 @@ namespace Enqueue\Consumption; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\Context\MessageResult; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\PreSubscribe; +use Enqueue\Consumption\Context\ProcessorException; +use Enqueue\Consumption\Context\Start; + class ChainExtension implements ExtensionInterface { - use EmptyExtensionTrait; - - /** - * @var ExtensionInterface[] - */ - private $extensions; + private $startExtensions; + private $initLoggerExtensions; + private $preSubscribeExtensions; + private $preConsumeExtensions; + private $messageReceivedExtensions; + private $messageResultExtensions; + private $postMessageReceivedExtensions; + private $processorExceptionExtensions; + private $postConsumeExtensions; + private $endExtensions; - /** - * @param ExtensionInterface[] $extensions - */ public function __construct(array $extensions) { - $this->extensions = $extensions; + $this->startExtensions = []; + $this->initLoggerExtensions = []; + $this->preSubscribeExtensions = []; + $this->preConsumeExtensions = []; + $this->messageReceivedExtensions = []; + $this->messageResultExtensions = []; + $this->postMessageReceivedExtensions = []; + $this->processorExceptionExtensions = []; + $this->postConsumeExtensions = []; + $this->endExtensions = []; + + array_walk($extensions, function ($extension) { + if ($extension instanceof ExtensionInterface) { + $this->startExtensions[] = $extension; + $this->initLoggerExtensions[] = $extension; + $this->preSubscribeExtensions[] = $extension; + $this->preConsumeExtensions[] = $extension; + $this->messageReceivedExtensions[] = $extension; + $this->messageResultExtensions[] = $extension; + $this->postMessageReceivedExtensions[] = $extension; + $this->processorExceptionExtensions[] = $extension; + $this->postConsumeExtensions[] = $extension; + $this->endExtensions[] = $extension; + + return; + } + + $extensionValid = false; + if ($extension instanceof StartExtensionInterface) { + $this->startExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof InitLoggerExtensionInterface) { + $this->initLoggerExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PreSubscribeExtensionInterface) { + $this->preSubscribeExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PreConsumeExtensionInterface) { + $this->preConsumeExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof MessageReceivedExtensionInterface) { + $this->messageReceivedExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof MessageResultExtensionInterface) { + $this->messageResultExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof ProcessorExceptionExtensionInterface) { + $this->processorExceptionExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PostMessageReceivedExtensionInterface) { + $this->postMessageReceivedExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof PostConsumeExtensionInterface) { + $this->postConsumeExtensions[] = $extension; + + $extensionValid = true; + } + + if ($extension instanceof EndExtensionInterface) { + $this->endExtensions[] = $extension; + + $extensionValid = true; + } + + if (false == $extensionValid) { + throw new \LogicException(sprintf('Invalid extension given %s', $extension::class)); + } + }); + } + + public function onInitLogger(InitLogger $context): void + { + foreach ($this->initLoggerExtensions as $extension) { + $extension->onInitLogger($context); + } } - /** - * @param Context $context - */ - public function onStart(Context $context) + public function onStart(Start $context): void { - foreach ($this->extensions as $extension) { + foreach ($this->startExtensions as $extension) { $extension->onStart($context); } } - /** - * @param Context $context - */ - public function onBeforeReceive(Context $context) + public function onPreSubscribe(PreSubscribe $context): void + { + foreach ($this->preSubscribeExtensions as $extension) { + $extension->onPreSubscribe($context); + } + } + + public function onPreConsume(PreConsume $context): void { - foreach ($this->extensions as $extension) { - $extension->onBeforeReceive($context); + foreach ($this->preConsumeExtensions as $extension) { + $extension->onPreConsume($context); } } - /** - * @param Context $context - */ - public function onPreReceived(Context $context) + public function onMessageReceived(MessageReceived $context): void { - foreach ($this->extensions as $extension) { - $extension->onPreReceived($context); + foreach ($this->messageReceivedExtensions as $extension) { + $extension->onMessageReceived($context); } } - /** - * @param Context $context - */ - public function onResult(Context $context) + public function onResult(MessageResult $context): void { - foreach ($this->extensions as $extension) { + foreach ($this->messageResultExtensions as $extension) { $extension->onResult($context); } } - /** - * @param Context $context - */ - public function onPostReceived(Context $context) + public function onProcessorException(ProcessorException $context): void + { + foreach ($this->processorExceptionExtensions as $extension) { + $extension->onProcessorException($context); + } + } + + public function onPostMessageReceived(PostMessageReceived $context): void { - foreach ($this->extensions as $extension) { - $extension->onPostReceived($context); + foreach ($this->postMessageReceivedExtensions as $extension) { + $extension->onPostMessageReceived($context); } } - /** - * @param Context $context - */ - public function onIdle(Context $context) + public function onPostConsume(PostConsume $context): void { - foreach ($this->extensions as $extension) { - $extension->onIdle($context); + foreach ($this->postConsumeExtensions as $extension) { + $extension->onPostConsume($context); } } - /** - * @param Context $context - */ - public function onInterrupted(Context $context) + public function onEnd(End $context): void { - foreach ($this->extensions as $extension) { - $extension->onInterrupted($context); + foreach ($this->endExtensions as $extension) { + $extension->onEnd($context); } } } diff --git a/pkg/enqueue/Consumption/Context.php b/pkg/enqueue/Consumption/Context.php deleted file mode 100644 index 09332b9b3..000000000 --- a/pkg/enqueue/Consumption/Context.php +++ /dev/null @@ -1,233 +0,0 @@ -psrContext = $psrContext; - - $this->executionInterrupted = false; - } - - /** - * @return PsrMessage - */ - public function getPsrMessage() - { - return $this->psrMessage; - } - - /** - * @param PsrMessage $psrMessage - */ - public function setPsrMessage(PsrMessage $psrMessage) - { - if ($this->psrMessage) { - throw new IllegalContextModificationException('The message could be set once'); - } - - $this->psrMessage = $psrMessage; - } - - /** - * @return PsrContext - */ - public function getPsrContext() - { - return $this->psrContext; - } - - /** - * @return PsrConsumer - */ - public function getPsrConsumer() - { - return $this->psrConsumer; - } - - /** - * @param PsrConsumer $psrConsumer - */ - public function setPsrConsumer(PsrConsumer $psrConsumer) - { - if ($this->psrConsumer) { - throw new IllegalContextModificationException('The message consumer could be set once'); - } - - $this->psrConsumer = $psrConsumer; - } - - /** - * @return PsrProcessor - */ - public function getPsrProcessor() - { - return $this->psrProcessor; - } - - /** - * @param PsrProcessor $psrProcessor - */ - public function setPsrProcessor(PsrProcessor $psrProcessor) - { - if ($this->psrProcessor) { - throw new IllegalContextModificationException('The message processor could be set once'); - } - - $this->psrProcessor = $psrProcessor; - } - - /** - * @return \Exception - */ - public function getException() - { - return $this->exception; - } - - /** - * @param \Exception $exception - */ - public function setException(\Exception $exception) - { - $this->exception = $exception; - } - - /** - * @return Result|string - */ - public function getResult() - { - return $this->result; - } - - /** - * @param Result|string $result - */ - public function setResult($result) - { - if ($this->result) { - throw new IllegalContextModificationException('The result modification is not allowed'); - } - - $this->result = $result; - } - - /** - * @return bool - */ - public function isExecutionInterrupted() - { - return $this->executionInterrupted; - } - - /** - * @param bool $executionInterrupted - */ - public function setExecutionInterrupted($executionInterrupted) - { - if (false == $executionInterrupted && $this->executionInterrupted) { - throw new IllegalContextModificationException('The execution once interrupted could not be roll backed'); - } - - $this->executionInterrupted = $executionInterrupted; - } - - /** - * @return LoggerInterface - */ - public function getLogger() - { - return $this->logger; - } - - /** - * @param LoggerInterface $logger - */ - public function setLogger(LoggerInterface $logger) - { - if ($this->logger) { - throw new IllegalContextModificationException('The logger modification is not allowed'); - } - - $this->logger = $logger; - } - - /** - * @return PsrQueue - */ - public function getPsrQueue() - { - return $this->psrQueue; - } - - /** - * @param PsrQueue $psrQueue - */ - public function setPsrQueue(PsrQueue $psrQueue) - { - if ($this->psrQueue) { - throw new IllegalContextModificationException('The queue modification is not allowed'); - } - - $this->psrQueue = $psrQueue; - } -} diff --git a/pkg/enqueue/Consumption/Context/End.php b/pkg/enqueue/Consumption/Context/End.php new file mode 100644 index 000000000..07853b3d3 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/End.php @@ -0,0 +1,79 @@ +context = $context; + $this->logger = $logger; + $this->startTime = $startTime; + $this->endTime = $endTime; + $this->exitStatus = $exitStatus; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + /** + * In milliseconds. + */ + public function getStartTime(): int + { + return $this->startTime; + } + + /** + * In milliseconds. + */ + public function getEndTime(): int + { + return $this->startTime; + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } +} diff --git a/pkg/enqueue/Consumption/Context/InitLogger.php b/pkg/enqueue/Consumption/Context/InitLogger.php new file mode 100644 index 000000000..c48057268 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/InitLogger.php @@ -0,0 +1,28 @@ +logger = $logger; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function changeLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } +} diff --git a/pkg/enqueue/Consumption/Context/MessageReceived.php b/pkg/enqueue/Consumption/Context/MessageReceived.php new file mode 100644 index 000000000..35abf1ca8 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/MessageReceived.php @@ -0,0 +1,109 @@ +context = $context; + $this->consumer = $consumer; + $this->message = $message; + $this->processor = $processor; + $this->receivedAt = $receivedAt; + $this->logger = $logger; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getProcessor(): Processor + { + return $this->processor; + } + + public function changeProcessor(Processor $processor): void + { + $this->processor = $processor; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getReceivedAt(): int + { + return $this->receivedAt; + } + + public function getResult(): ?Result + { + return $this->result; + } + + public function setResult(Result $result): void + { + $this->result = $result; + } +} diff --git a/pkg/enqueue/Consumption/Context/MessageResult.php b/pkg/enqueue/Consumption/Context/MessageResult.php new file mode 100644 index 000000000..4fa8f7de0 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/MessageResult.php @@ -0,0 +1,93 @@ +context = $context; + $this->consumer = $consumer; + $this->message = $message; + $this->logger = $logger; + $this->result = $result; + $this->receivedAt = $receivedAt; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getReceivedAt(): int + { + return $this->receivedAt; + } + + /** + * @return Result|object|string|null + */ + public function getResult() + { + return $this->result; + } + + /** + * @param Result|string|object|null $result + */ + public function changeResult($result): void + { + $this->result = $result; + } +} diff --git a/pkg/enqueue/Consumption/Context/PostConsume.php b/pkg/enqueue/Consumption/Context/PostConsume.php new file mode 100644 index 000000000..a6f1d8375 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/PostConsume.php @@ -0,0 +1,108 @@ +context = $context; + $this->subscriptionConsumer = $subscriptionConsumer; + $this->receivedMessagesCount = $receivedMessagesCount; + $this->cycle = $cycle; + $this->startTime = $startTime; + $this->logger = $logger; + + $this->executionInterrupted = false; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getSubscriptionConsumer(): SubscriptionConsumer + { + return $this->subscriptionConsumer; + } + + public function getReceivedMessagesCount(): int + { + return $this->receivedMessagesCount; + } + + public function getCycle(): int + { + return $this->cycle; + } + + public function getStartTime(): int + { + return $this->startTime; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } + + public function isExecutionInterrupted(): bool + { + return $this->executionInterrupted; + } + + public function interruptExecution(?int $exitStatus = null): void + { + $this->exitStatus = $exitStatus; + $this->executionInterrupted = true; + } +} diff --git a/pkg/enqueue/Consumption/Context/PostMessageReceived.php b/pkg/enqueue/Consumption/Context/PostMessageReceived.php new file mode 100644 index 000000000..23df2c849 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/PostMessageReceived.php @@ -0,0 +1,119 @@ +context = $context; + $this->consumer = $consumer; + $this->message = $message; + $this->result = $result; + $this->receivedAt = $receivedAt; + $this->logger = $logger; + + $this->executionInterrupted = false; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getReceivedAt(): int + { + return $this->receivedAt; + } + + /** + * @return Result|object|string|null + */ + public function getResult() + { + return $this->result; + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } + + public function isExecutionInterrupted(): bool + { + return $this->executionInterrupted; + } + + public function interruptExecution(?int $exitStatus = null): void + { + $this->exitStatus = $exitStatus; + $this->executionInterrupted = true; + } +} diff --git a/pkg/enqueue/Consumption/Context/PreConsume.php b/pkg/enqueue/Consumption/Context/PreConsume.php new file mode 100644 index 000000000..77cc7d030 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/PreConsume.php @@ -0,0 +1,108 @@ +context = $context; + $this->subscriptionConsumer = $subscriptionConsumer; + $this->logger = $logger; + $this->cycle = $cycle; + $this->receiveTimeout = $receiveTimeout; + $this->startTime = $startTime; + + $this->executionInterrupted = false; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getSubscriptionConsumer(): SubscriptionConsumer + { + return $this->subscriptionConsumer; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getCycle(): int + { + return $this->cycle; + } + + public function getReceiveTimeout(): int + { + return $this->receiveTimeout; + } + + public function getStartTime(): int + { + return $this->startTime; + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } + + public function isExecutionInterrupted(): bool + { + return $this->executionInterrupted; + } + + public function interruptExecution(?int $exitStatus = null): void + { + $this->exitStatus = $exitStatus; + $this->executionInterrupted = true; + } +} diff --git a/pkg/enqueue/Consumption/Context/PreSubscribe.php b/pkg/enqueue/Consumption/Context/PreSubscribe.php new file mode 100644 index 000000000..dbc74bb69 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/PreSubscribe.php @@ -0,0 +1,59 @@ +context = $context; + $this->processor = $processor; + $this->consumer = $consumer; + $this->logger = $logger; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getProcessor(): Processor + { + return $this->processor; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } +} diff --git a/pkg/enqueue/Consumption/Context/ProcessorException.php b/pkg/enqueue/Consumption/Context/ProcessorException.php new file mode 100644 index 000000000..329b13d93 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/ProcessorException.php @@ -0,0 +1,96 @@ +context = $context; + $this->consumer = $consumer; + $this->message = $message; + $this->exception = $exception; + $this->logger = $logger; + $this->receivedAt = $receivedAt; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getConsumer(): Consumer + { + return $this->consumer; + } + + public function getMessage(): Message + { + return $this->message; + } + + public function getException(): \Throwable + { + return $this->exception; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + public function getReceivedAt(): int + { + return $this->receivedAt; + } + + public function getResult(): ?Result + { + return $this->result; + } + + public function setResult(Result $result): void + { + $this->result = $result; + } +} diff --git a/pkg/enqueue/Consumption/Context/Start.php b/pkg/enqueue/Consumption/Context/Start.php new file mode 100644 index 000000000..84db29c44 --- /dev/null +++ b/pkg/enqueue/Consumption/Context/Start.php @@ -0,0 +1,128 @@ +context = $context; + $this->logger = $logger; + $this->processors = $processors; + $this->receiveTimeout = $receiveTimeout; + $this->startTime = $startTime; + + $this->executionInterrupted = false; + } + + public function getContext(): Context + { + return $this->context; + } + + public function getLogger(): LoggerInterface + { + return $this->logger; + } + + /** + * In milliseconds. + */ + public function getReceiveTimeout(): int + { + return $this->receiveTimeout; + } + + /** + * In milliseconds. + */ + public function changeReceiveTimeout(int $timeout): void + { + $this->receiveTimeout = $timeout; + } + + /** + * In milliseconds. + */ + public function getStartTime(): int + { + return $this->startTime; + } + + /** + * @return BoundProcessor[] + */ + public function getBoundProcessors(): array + { + return $this->processors; + } + + /** + * @param BoundProcessor[] $processors + */ + public function changeBoundProcessors(array $processors): void + { + $this->processors = []; + array_walk($processors, function (BoundProcessor $processor) { + $this->processors[] = $processor; + }); + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } + + public function isExecutionInterrupted(): bool + { + return $this->executionInterrupted; + } + + public function interruptExecution(?int $exitStatus = null): void + { + $this->exitStatus = $exitStatus; + $this->executionInterrupted = true; + } +} diff --git a/pkg/enqueue/Consumption/EmptyExtensionTrait.php b/pkg/enqueue/Consumption/EmptyExtensionTrait.php deleted file mode 100644 index 0f6b849c4..000000000 --- a/pkg/enqueue/Consumption/EmptyExtensionTrait.php +++ /dev/null @@ -1,55 +0,0 @@ -exitStatus = $context->getExitStatus(); + } + + public function getExitStatus(): ?int + { + return $this->exitStatus; + } +} diff --git a/pkg/enqueue/Consumption/Extension/LimitConsumedMessagesExtension.php b/pkg/enqueue/Consumption/Extension/LimitConsumedMessagesExtension.php index ef6ec527e..0dc6feceb 100644 --- a/pkg/enqueue/Consumption/Extension/LimitConsumedMessagesExtension.php +++ b/pkg/enqueue/Consumption/Extension/LimitConsumedMessagesExtension.php @@ -2,14 +2,14 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; +use Enqueue\Consumption\PreConsumeExtensionInterface; +use Psr\Log\LoggerInterface; -class LimitConsumedMessagesExtension implements ExtensionInterface +class LimitConsumedMessagesExtension implements PreConsumeExtensionInterface, PostMessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** * @var int */ @@ -20,54 +20,41 @@ class LimitConsumedMessagesExtension implements ExtensionInterface */ protected $messageConsumed; - /** - * @param int $messageLimit - */ - public function __construct($messageLimit) + public function __construct(int $messageLimit) { - if (false == is_int($messageLimit)) { - throw new \InvalidArgumentException(sprintf( - 'Expected message limit is int but got: "%s"', - is_object($messageLimit) ? get_class($messageLimit) : gettype($messageLimit) - )); - } - $this->messageLimit = $messageLimit; $this->messageConsumed = 0; } - /** - * {@inheritdoc} - */ - public function onBeforeReceive(Context $context) + public function onPreConsume(PreConsume $context): void { // this is added here to handle an edge case. when a user sets zero as limit. - $this->checkMessageLimit($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { ++$this->messageConsumed; - $this->checkMessageLimit($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * @param Context $context - */ - protected function checkMessageLimit(Context $context) + protected function shouldBeStopped(LoggerInterface $logger): bool { if ($this->messageConsumed >= $this->messageLimit) { - $context->getLogger()->debug(sprintf( + $logger->debug(sprintf( '[LimitConsumedMessagesExtension] Message consumption is interrupted since the message limit reached.'. ' limit: "%s"', $this->messageLimit )); - $context->setExecutionInterrupted(true); + return true; } + + return false; } } diff --git a/pkg/enqueue/Consumption/Extension/LimitConsumerMemoryExtension.php b/pkg/enqueue/Consumption/Extension/LimitConsumerMemoryExtension.php index c03686f33..7edbf232c 100644 --- a/pkg/enqueue/Consumption/Extension/LimitConsumerMemoryExtension.php +++ b/pkg/enqueue/Consumption/Extension/LimitConsumerMemoryExtension.php @@ -2,14 +2,16 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\PostConsumeExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; +use Enqueue\Consumption\PreConsumeExtensionInterface; +use Psr\Log\LoggerInterface; -class LimitConsumerMemoryExtension implements ExtensionInterface +class LimitConsumerMemoryExtension implements PreConsumeExtensionInterface, PostMessageReceivedExtensionInterface, PostConsumeExtensionInterface { - use EmptyExtensionTrait; - /** * @var int */ @@ -21,53 +23,46 @@ class LimitConsumerMemoryExtension implements ExtensionInterface public function __construct($memoryLimit) { if (false == is_int($memoryLimit)) { - throw new \InvalidArgumentException(sprintf( - 'Expected memory limit is int but got: "%s"', - is_object($memoryLimit) ? get_class($memoryLimit) : gettype($memoryLimit) - )); + throw new \InvalidArgumentException(sprintf('Expected memory limit is int but got: "%s"', is_object($memoryLimit) ? $memoryLimit::class : gettype($memoryLimit))); } $this->memoryLimit = $memoryLimit * 1024 * 1024; } - /** - * {@inheritdoc} - */ - public function onBeforeReceive(Context $context) + public function onPreConsume(PreConsume $context): void { - $this->checkMemory($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { - $this->checkMemory($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onIdle(Context $context) + public function onPostConsume(PostConsume $context): void { - $this->checkMemory($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * @param Context $context - */ - protected function checkMemory(Context $context) + protected function shouldBeStopped(LoggerInterface $logger): bool { $memoryUsage = memory_get_usage(true); if ($memoryUsage >= $this->memoryLimit) { - $context->getLogger()->debug(sprintf( + $logger->debug(sprintf( '[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached. limit: "%s", used: "%s"', $this->memoryLimit, $memoryUsage )); - $context->setExecutionInterrupted(true); + return true; } + + return false; } } diff --git a/pkg/enqueue/Consumption/Extension/LimitConsumptionTimeExtension.php b/pkg/enqueue/Consumption/Extension/LimitConsumptionTimeExtension.php index 65221f5c0..1953aa2e6 100644 --- a/pkg/enqueue/Consumption/Extension/LimitConsumptionTimeExtension.php +++ b/pkg/enqueue/Consumption/Extension/LimitConsumptionTimeExtension.php @@ -2,66 +2,61 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; - -class LimitConsumptionTimeExtension implements ExtensionInterface +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\PostConsumeExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; +use Enqueue\Consumption\PreConsumeExtensionInterface; +use Psr\Log\LoggerInterface; + +class LimitConsumptionTimeExtension implements PreConsumeExtensionInterface, PostConsumeExtensionInterface, PostMessageReceivedExtensionInterface { - use EmptyExtensionTrait; - /** * @var \DateTime */ protected $timeLimit; - /** - * @param \DateTime $timeLimit - */ public function __construct(\DateTime $timeLimit) { $this->timeLimit = $timeLimit; } - /** - * {@inheritdoc} - */ - public function onBeforeReceive(Context $context) + public function onPreConsume(PreConsume $context): void { - $this->checkTime($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onIdle(Context $context) + public function onPostConsume(PostConsume $context): void { - $this->checkTime($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { - $this->checkTime($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * @param Context $context - */ - protected function checkTime(Context $context) + protected function shouldBeStopped(LoggerInterface $logger): bool { $now = new \DateTime(); if ($now >= $this->timeLimit) { - $context->getLogger()->debug(sprintf( + $logger->debug(sprintf( '[LimitConsumptionTimeExtension] Execution interrupted as limit time has passed.'. ' now: "%s", time-limit: "%s"', - $now->format(DATE_ISO8601), - $this->timeLimit->format(DATE_ISO8601) + $now->format(\DATE_ISO8601), + $this->timeLimit->format(\DATE_ISO8601) )); - $context->setExecutionInterrupted(true); + return true; } + + return false; } } diff --git a/pkg/enqueue/Consumption/Extension/LogExtension.php b/pkg/enqueue/Consumption/Extension/LogExtension.php new file mode 100644 index 000000000..14383c4d1 --- /dev/null +++ b/pkg/enqueue/Consumption/Extension/LogExtension.php @@ -0,0 +1,67 @@ +getLogger()->debug('Consumption has started'); + } + + public function onEnd(End $context): void + { + $context->getLogger()->debug('Consumption has ended'); + } + + public function onMessageReceived(MessageReceived $context): void + { + $message = $context->getMessage(); + + $context->getLogger()->debug("Received from {queueName}\t{body}", [ + 'queueName' => $context->getConsumer()->getQueue()->getQueueName(), + 'redelivered' => $message->isRedelivered(), + 'body' => Stringify::that($message->getBody()), + 'properties' => Stringify::that($message->getProperties()), + 'headers' => Stringify::that($message->getHeaders()), + ]); + } + + public function onPostMessageReceived(PostMessageReceived $context): void + { + $message = $context->getMessage(); + $queue = $context->getConsumer()->getQueue(); + $result = $context->getResult(); + + $reason = ''; + $logMessage = "Processed from {queueName}\t{body}\t{result}"; + if ($result instanceof Result && $result->getReason()) { + $reason = $result->getReason(); + $logMessage .= ' {reason}'; + } + $logContext = [ + 'result' => str_replace('enqueue.', '', $result), + 'reason' => $reason, + 'queueName' => $queue->getQueueName(), + 'body' => Stringify::that($message->getBody()), + 'properties' => Stringify::that($message->getProperties()), + 'headers' => Stringify::that($message->getHeaders()), + ]; + + $logLevel = Result::REJECT == ((string) $result) ? LogLevel::ERROR : LogLevel::INFO; + + $context->getLogger()->log($logLevel, $logMessage, $logContext); + } +} diff --git a/pkg/enqueue/Consumption/Extension/LoggerExtension.php b/pkg/enqueue/Consumption/Extension/LoggerExtension.php index 0779c9033..90e92be8a 100644 --- a/pkg/enqueue/Consumption/Extension/LoggerExtension.php +++ b/pkg/enqueue/Consumption/Extension/LoggerExtension.php @@ -2,89 +2,30 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; -use Enqueue\Consumption\Result; -use Interop\Queue\PsrMessage; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\InitLoggerExtensionInterface; use Psr\Log\LoggerInterface; -class LoggerExtension implements ExtensionInterface +class LoggerExtension implements InitLoggerExtensionInterface { - use EmptyExtensionTrait; - /** * @var LoggerInterface */ private $logger; - /** - * @param LoggerInterface $logger - */ public function __construct(LoggerInterface $logger) { $this->logger = $logger; } - /** - * {@inheritdoc} - */ - public function onStart(Context $context) + public function onInitLogger(InitLogger $context): void { - if ($context->getLogger()) { - $context->getLogger()->debug(sprintf( - 'Skip setting context\'s logger "%s". Another one "%s" has already been set.', - get_class($this->logger), - get_class($context->getLogger()) - )); - } else { - $context->setLogger($this->logger); - $this->logger->debug(sprintf('Set context\'s logger "%s"', get_class($this->logger))); - } - } + $previousLogger = $context->getLogger(); - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) - { - if (false == $context->getResult() instanceof Result) { - return; - } - - /** @var $result Result */ - $result = $context->getResult(); - - switch ($result->getStatus()) { - case Result::REJECT: - case Result::REQUEUE: - if ($result->getReason()) { - $this->logger->error($result->getReason(), $this->messageToLogContext($context->getPsrMessage())); - } + if ($previousLogger !== $this->logger) { + $context->changeLogger($this->logger); - break; - case Result::ACK: - if ($result->getReason()) { - $this->logger->info($result->getReason(), $this->messageToLogContext($context->getPsrMessage())); - } - - break; - default: - throw new \LogicException(sprintf('Got unexpected message result. "%s"', $result->getStatus())); + $this->logger->debug(sprintf('Change logger from "%s" to "%s"', $previousLogger::class, get_class($this->logger))); } } - - /** - * @param PsrMessage $message - * - * @return array - */ - private function messageToLogContext(PsrMessage $message) - { - return [ - 'body' => $message->getBody(), - 'headers' => $message->getHeaders(), - 'properties' => $message->getProperties(), - ]; - } } diff --git a/pkg/enqueue/Consumption/Extension/NicenessExtension.php b/pkg/enqueue/Consumption/Extension/NicenessExtension.php index 01a5383a2..436a8ec0f 100644 --- a/pkg/enqueue/Consumption/Extension/NicenessExtension.php +++ b/pkg/enqueue/Consumption/Extension/NicenessExtension.php @@ -2,14 +2,11 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\Start; +use Enqueue\Consumption\StartExtensionInterface; -class NicenessExtension implements ExtensionInterface +class NicenessExtension implements StartExtensionInterface { - use EmptyExtensionTrait; - /** * @var int */ @@ -23,27 +20,18 @@ class NicenessExtension implements ExtensionInterface public function __construct($niceness) { if (false === is_int($niceness)) { - throw new \InvalidArgumentException(sprintf( - 'Expected niceness value is int but got: "%s"', - is_object($niceness) ? get_class($niceness) : gettype($niceness) - )); + throw new \InvalidArgumentException(sprintf('Expected niceness value is int but got: "%s"', is_object($niceness) ? $niceness::class : gettype($niceness))); } $this->niceness = $niceness; } - /** - * {@inheritdoc} - */ - public function onStart(Context $context) + public function onStart(Start $context): void { if (0 !== $this->niceness) { $changed = @proc_nice($this->niceness); if (!$changed) { - throw new \InvalidArgumentException(sprintf( - 'Cannot change process niceness, got warning: "%s"', - error_get_last()['message'] - )); + throw new \InvalidArgumentException(sprintf('Cannot change process niceness, got warning: "%s"', error_get_last()['message'])); } } } diff --git a/pkg/enqueue/Consumption/Extension/ReplyExtension.php b/pkg/enqueue/Consumption/Extension/ReplyExtension.php index 0d7a76eeb..c1ac19bd7 100644 --- a/pkg/enqueue/Consumption/Extension/ReplyExtension.php +++ b/pkg/enqueue/Consumption/Extension/ReplyExtension.php @@ -2,21 +2,15 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; use Enqueue\Consumption\Result; -class ReplyExtension implements ExtensionInterface +class ReplyExtension implements PostMessageReceivedExtensionInterface { - use EmptyExtensionTrait; - - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { - $replyTo = $context->getPsrMessage()->getReplyTo(); + $replyTo = $context->getMessage()->getReplyTo(); if (false == $replyTo) { return; } @@ -31,13 +25,13 @@ public function onPostReceived(Context $context) return; } - $correlationId = $context->getPsrMessage()->getCorrelationId(); + $correlationId = $context->getMessage()->getCorrelationId(); $replyMessage = clone $result->getReply(); $replyMessage->setCorrelationId($correlationId); - $replyQueue = $context->getPsrContext()->createQueue($replyTo); + $replyQueue = $context->getContext()->createQueue($replyTo); $context->getLogger()->debug(sprintf('[ReplyExtension] Send reply to "%s"', $replyTo)); - $context->getPsrContext()->createProducer()->send($replyQueue, $replyMessage); + $context->getContext()->createProducer()->send($replyQueue, $replyMessage); } } diff --git a/pkg/enqueue/Consumption/Extension/SignalExtension.php b/pkg/enqueue/Consumption/Extension/SignalExtension.php index a8b53e8b8..8ea5307d5 100644 --- a/pkg/enqueue/Consumption/Extension/SignalExtension.php +++ b/pkg/enqueue/Consumption/Extension/SignalExtension.php @@ -2,16 +2,19 @@ namespace Enqueue\Consumption\Extension; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\Start; use Enqueue\Consumption\Exception\LogicException; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\PostConsumeExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; +use Enqueue\Consumption\PreConsumeExtensionInterface; +use Enqueue\Consumption\StartExtensionInterface; use Psr\Log\LoggerInterface; -class SignalExtension implements ExtensionInterface +class SignalExtension implements StartExtensionInterface, PreConsumeExtensionInterface, PostMessageReceivedExtensionInterface, PostConsumeExtensionInterface { - use EmptyExtensionTrait; - /** * @var bool */ @@ -22,95 +25,55 @@ class SignalExtension implements ExtensionInterface */ protected $logger; - /** - * {@inheritdoc} - */ - public function onStart(Context $context) + public function onStart(Start $context): void { if (false == extension_loaded('pcntl')) { throw new LogicException('The pcntl extension is required in order to catch signals.'); } - if (function_exists('pcntl_async_signals')) { - pcntl_async_signals(true); - } + pcntl_async_signals(true); - pcntl_signal(SIGTERM, [$this, 'handleSignal']); - pcntl_signal(SIGQUIT, [$this, 'handleSignal']); - pcntl_signal(SIGINT, [$this, 'handleSignal']); + pcntl_signal(\SIGTERM, [$this, 'handleSignal']); + pcntl_signal(\SIGQUIT, [$this, 'handleSignal']); + pcntl_signal(\SIGINT, [$this, 'handleSignal']); + $this->logger = $context->getLogger(); $this->interruptConsumption = false; } - /** - * @param Context $context - */ - public function onBeforeReceive(Context $context) + public function onPreConsume(PreConsume $context): void { $this->logger = $context->getLogger(); - $this->dispatchSignal(); - - $this->interruptExecutionIfNeeded($context); - } - - /** - * {@inheritdoc} - */ - public function onPreReceived(Context $context) - { - $this->interruptExecutionIfNeeded($context); - } - - /** - * {@inheritdoc} - */ - public function onPostReceived(Context $context) - { - $this->dispatchSignal(); - - $this->interruptExecutionIfNeeded($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * {@inheritdoc} - */ - public function onIdle(Context $context) + public function onPostMessageReceived(PostMessageReceived $context): void { - $this->dispatchSignal(); - - $this->interruptExecutionIfNeeded($context); + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); + } } - /** - * @param Context $context - */ - public function interruptExecutionIfNeeded(Context $context) + public function onPostConsume(PostConsume $context): void { - if (false == $context->isExecutionInterrupted() && $this->interruptConsumption) { - if ($this->logger) { - $this->logger->debug('[SignalExtension] Interrupt execution'); - } - - $context->setExecutionInterrupted($this->interruptConsumption); - - $this->interruptConsumption = false; + if ($this->shouldBeStopped($context->getLogger())) { + $context->interruptExecution(); } } - /** - * @param int $signal - */ - public function handleSignal($signal) + public function handleSignal(int $signal): void { if ($this->logger) { $this->logger->debug(sprintf('[SignalExtension] Caught signal: %s', $signal)); } switch ($signal) { - case SIGTERM: // 15 : supervisor default stop - case SIGQUIT: // 3 : kill -s QUIT - case SIGINT: // 2 : ctrl+c + case \SIGTERM: // 15 : supervisor default stop + case \SIGQUIT: // 3 : kill -s QUIT + case \SIGINT: // 2 : ctrl+c if ($this->logger) { $this->logger->debug('[SignalExtension] Interrupt consumption'); } @@ -122,10 +85,16 @@ public function handleSignal($signal) } } - private function dispatchSignal() + private function shouldBeStopped(LoggerInterface $logger): bool { - if (false == function_exists('pcntl_async_signals')) { - pcntl_signal_dispatch(); + if ($this->interruptConsumption) { + $logger->debug('[SignalExtension] Interrupt execution'); + + $this->interruptConsumption = false; + + return true; } + + return false; } } diff --git a/pkg/enqueue/Consumption/ExtensionInterface.php b/pkg/enqueue/Consumption/ExtensionInterface.php index 2a5d7bb72..326a98f0d 100644 --- a/pkg/enqueue/Consumption/ExtensionInterface.php +++ b/pkg/enqueue/Consumption/ExtensionInterface.php @@ -2,65 +2,6 @@ namespace Enqueue\Consumption; -interface ExtensionInterface +interface ExtensionInterface extends StartExtensionInterface, PreSubscribeExtensionInterface, PreConsumeExtensionInterface, MessageReceivedExtensionInterface, PostMessageReceivedExtensionInterface, MessageResultExtensionInterface, ProcessorExceptionExtensionInterface, PostConsumeExtensionInterface, EndExtensionInterface, InitLoggerExtensionInterface { - /** - * Executed only once at the very begining of the consumption. - * At this stage the context does not contain processor, consumer and queue. - * - * @param Context $context - */ - public function onStart(Context $context); - - /** - * Executed at every new cycle before we asked a broker for a new message. - * At this stage the context already contains processor, consumer and queue. - * The consumption could be interrupted at this step. - * - * @param Context $context - */ - public function onBeforeReceive(Context $context); - - /** - * Executed when a new message is received from a broker but before it was passed to processor - * The context contains a message. - * The extension may set a status. If the status is set the exception is thrown - * The consumption could be interrupted at this step but it exits after the message is processed. - * - * @param Context $context - */ - public function onPreReceived(Context $context); - - /** - * Executed when a message is processed by a processor or a result was set in onPreReceived method. - * BUT before the message status was sent to the broker - * The consumption could be interrupted at this step but it exits after the message is processed. - * - * @param Context $context - */ - public function onResult(Context $context); - - /** - * Executed when a message is processed by a processor. - * The context contains a status, which could not be changed. - * The consumption could be interrupted at this step but it exits after the message is processed. - * - * @param Context $context - */ - public function onPostReceived(Context $context); - - /** - * Called each time at the end of the cycle if nothing was done. - * - * @param Context $context - */ - public function onIdle(Context $context); - - /** - * Called when the consumption was interrupted by an extension or exception - * In case of exception it will be present in the context. - * - * @param Context $context - */ - public function onInterrupted(Context $context); } diff --git a/pkg/enqueue/Consumption/FallbackSubscriptionConsumer.php b/pkg/enqueue/Consumption/FallbackSubscriptionConsumer.php index f4176a696..15e2f273b 100644 --- a/pkg/enqueue/Consumption/FallbackSubscriptionConsumer.php +++ b/pkg/enqueue/Consumption/FallbackSubscriptionConsumer.php @@ -2,13 +2,13 @@ namespace Enqueue\Consumption; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrSubscriptionConsumer; +use Interop\Queue\Consumer; +use Interop\Queue\SubscriptionConsumer; -final class FallbackSubscriptionConsumer implements PsrSubscriptionConsumer +final class FallbackSubscriptionConsumer implements SubscriptionConsumer { /** - * an item contains an array: [PsrConsumer $consumer, callable $callback];. + * an item contains an array: [Consumer $consumer, callable $callback];. * an item key is a queue name. * * @var array @@ -16,7 +16,7 @@ final class FallbackSubscriptionConsumer implements PsrSubscriptionConsumer private $subscribers; /** - * @var int|float the time in milliseconds the consumer waits if no message has been received + * @var int */ private $idleTime = 0; @@ -25,32 +25,29 @@ public function __construct() $this->subscribers = []; } - /** - * {@inheritdoc} - */ - public function consume($timeout = 0) + public function consume(int $timeoutMs = 0): void { - if (empty($this->subscribers)) { + if (!$subscriberCount = \count($this->subscribers)) { throw new \LogicException('No subscribers'); } - $timeout /= 1000; + $timeout = $timeoutMs / 1000; $endAt = microtime(true) + $timeout; while (true) { /** * @var string - * @var PsrConsumer $consumer - * @var callable $processor + * @var Consumer $consumer + * @var callable $processor */ foreach ($this->subscribers as $queueName => list($consumer, $callback)) { - $message = $consumer->receiveNoWait(); + $message = 1 === $subscriberCount ? $consumer->receive($timeoutMs) : $consumer->receiveNoWait(); if ($message) { if (false === call_user_func($callback, $message, $consumer)) { return; } - } else { + } elseif (1 !== $subscriberCount) { if ($timeout && microtime(true) >= $endAt) { return; } @@ -65,10 +62,7 @@ public function consume($timeout = 0) } } - /** - * {@inheritdoc} - */ - public function subscribe(PsrConsumer $consumer, callable $callback) + public function subscribe(Consumer $consumer, callable $callback): void { $queueName = $consumer->getQueue()->getQueueName(); if (array_key_exists($queueName, $this->subscribers)) { @@ -82,10 +76,7 @@ public function subscribe(PsrConsumer $consumer, callable $callback) $this->subscribers[$queueName] = [$consumer, $callback]; } - /** - * {@inheritdoc} - */ - public function unsubscribe(PsrConsumer $consumer) + public function unsubscribe(Consumer $consumer): void { if (false == array_key_exists($consumer->getQueue()->getQueueName(), $this->subscribers)) { return; @@ -98,26 +89,20 @@ public function unsubscribe(PsrConsumer $consumer) unset($this->subscribers[$consumer->getQueue()->getQueueName()]); } - /** - * {@inheritdoc} - */ - public function unsubscribeAll() + public function unsubscribeAll(): void { $this->subscribers = []; } - /** - * @return float|int - */ - public function getIdleTime() + public function getIdleTime(): int { return $this->idleTime; } /** - * @param float|int $idleTime + * The time in milliseconds the consumer waits if no message has been received. */ - public function setIdleTime($idleTime) + public function setIdleTime(int $idleTime): void { $this->idleTime = $idleTime; } diff --git a/pkg/enqueue/Consumption/InitLoggerExtensionInterface.php b/pkg/enqueue/Consumption/InitLoggerExtensionInterface.php new file mode 100644 index 000000000..936e32d6e --- /dev/null +++ b/pkg/enqueue/Consumption/InitLoggerExtensionInterface.php @@ -0,0 +1,14 @@ +psrContext = $psrContext; - $this->staticExtension = $extension ?: new ChainExtension([]); - $this->idleTimeout = $idleTimeout; + $this->interopContext = $interopContext; $this->receiveTimeout = $receiveTimeout; - $this->boundProcessors = []; - $this->logger = new NullLogger(); - - $this->enableSubscriptionConsumer = false; - } + $this->staticExtension = $extension ?: new ChainExtension([]); + $this->logger = $logger ?: new NullLogger(); - /** - * @param int $timeout - */ - public function setIdleTimeout($timeout) - { - $this->idleTimeout = (int) $timeout; - } + $this->boundProcessors = []; + array_walk($boundProcessors, function (BoundProcessor $processor) { + $this->boundProcessors[] = $processor; + }); - /** - * @return int - */ - public function getIdleTimeout() - { - return $this->idleTimeout; + $this->fallbackSubscriptionConsumer = new FallbackSubscriptionConsumer(); } - /** - * @param int $timeout - */ - public function setReceiveTimeout($timeout) + public function setReceiveTimeout(int $timeout): void { - $this->receiveTimeout = (int) $timeout; + $this->receiveTimeout = $timeout; } - /** - * @return int - */ - public function getReceiveTimeout() + public function getReceiveTimeout(): int { return $this->receiveTimeout; } - /** - * @return PsrContext - */ - public function getPsrContext() + public function getContext(): InteropContext { - return $this->psrContext; + return $this->interopContext; } - /** - * @param PsrQueue|string $queue - * @param PsrProcessor|callable $processor - * - * @return QueueConsumer - */ - public function bind($queue, $processor) + public function bind($queue, Processor $processor): QueueConsumerInterface { if (is_string($queue)) { - $queue = $this->psrContext->createQueue($queue); - } - if (is_callable($processor)) { - $processor = new CallbackProcessor($processor); + $queue = $this->interopContext->createQueue($queue); } - InvalidArgumentException::assertInstanceOf($queue, PsrQueue::class); - InvalidArgumentException::assertInstanceOf($processor, PsrProcessor::class); + InvalidArgumentException::assertInstanceOf($queue, InteropQueue::class); if (empty($queue->getQueueName())) { throw new LogicException('The queue name must be not empty.'); @@ -154,326 +112,221 @@ public function bind($queue, $processor) throw new LogicException(sprintf('The queue was already bound. Queue: %s', $queue->getQueueName())); } - $this->boundProcessors[$queue->getQueueName()] = [$queue, $processor]; + $this->boundProcessors[$queue->getQueueName()] = new BoundProcessor($queue, $processor); return $this; } - /** - * Runtime extension - is an extension or a collection of extensions which could be set on runtime. - * Here's a good example: @see LimitsExtensionsCommandTrait. - * - * @param ExtensionInterface|ChainExtension|null $runtimeExtension - * - * @throws \Exception - */ - public function consume(ExtensionInterface $runtimeExtension = null) + public function bindCallback($queue, callable $processor): QueueConsumerInterface { - if (empty($this->boundProcessors)) { - throw new \LogicException('There is nothing to consume. It is required to bind something before calling consume method.'); - } - - /** @var PsrConsumer[] $consumers */ - $consumers = []; - /** @var PsrQueue $queue */ - foreach ($this->boundProcessors as list($queue, $processor)) { - $consumers[$queue->getQueueName()] = $this->psrContext->createConsumer($queue); - } + return $this->bind($queue, new CallbackProcessor($processor)); + } - $this->extension = $runtimeExtension ? + public function consume(?ExtensionInterface $runtimeExtension = null): void + { + $extension = $runtimeExtension ? new ChainExtension([$this->staticExtension, $runtimeExtension]) : $this->staticExtension ; - $context = new Context($this->psrContext); - $this->extension->onStart($context); + $initLogger = new InitLogger($this->logger); + $extension->onInitLogger($initLogger); - if ($context->getLogger()) { - $this->logger = $context->getLogger(); - } else { - $this->logger = new NullLogger(); - $context->setLogger($this->logger); - } + $this->logger = $initLogger->getLogger(); - $this->logger->info('Start consuming'); + $startTime = (int) (microtime(true) * 1000); - $subscriptionConsumer = null; - if ($this->enableSubscriptionConsumer) { - $subscriptionConsumer = new FallbackSubscriptionConsumer(); - if ($context instanceof PsrSubscriptionConsumerAwareContext) { - $subscriptionConsumer = $context->createSubscriptionConsumer(); - } + $start = new Start( + $this->interopContext, + $this->logger, + $this->boundProcessors, + $this->receiveTimeout, + $startTime + ); - $callback = function (PsrMessage $message, PsrConsumer $consumer) use (&$context) { - $currentProcessor = null; + $extension->onStart($start); - /** @var PsrQueue $queue */ - foreach ($this->boundProcessors as list($queue, $processor)) { - if ($queue->getQueueName() === $consumer->getQueue()->getQueueName()) { - $currentProcessor = $processor; - } - } + if ($start->isExecutionInterrupted()) { + $this->onEnd($extension, $startTime, $start->getExitStatus()); - if (false == $currentProcessor) { - throw new \LogicException(sprintf('The processor for the queue "%s" could not be found.', $consumer->getQueue()->getQueueName())); - } + return; + } - $context = new Context($this->psrContext); - $context->setLogger($this->logger); - $context->setPsrQueue($consumer->getQueue()); - $context->setPsrConsumer($consumer); - $context->setPsrProcessor($currentProcessor); - $context->setPsrMessage($message); + $this->logger = $start->getLogger(); + $this->receiveTimeout = $start->getReceiveTimeout(); + $this->boundProcessors = $start->getBoundProcessors(); - $this->doConsume($this->extension, $context); + if (empty($this->boundProcessors)) { + throw new \LogicException('There is nothing to consume. It is required to bind something before calling consume method.'); + } - return true; - }; + /** @var Consumer[] $consumers */ + $consumers = []; + foreach ($this->boundProcessors as $queueName => $boundProcessor) { + $queue = $boundProcessor->getQueue(); - foreach ($consumers as $consumer) { - /* @var AmqpConsumer $consumer */ + $consumers[$queue->getQueueName()] = $this->interopContext->createConsumer($queue); + } - $subscriptionConsumer->subscribe($consumer, $callback); + try { + $subscriptionConsumer = $this->interopContext->createSubscriptionConsumer(); + } catch (SubscriptionConsumerNotSupportedException $e) { + $subscriptionConsumer = $this->fallbackSubscriptionConsumer; + } + + $receivedMessagesCount = 0; + $interruptExecution = false; + + $callback = function (InteropMessage $message, Consumer $consumer) use (&$receivedMessagesCount, &$interruptExecution, $extension) { + ++$receivedMessagesCount; + + $receivedAt = (int) (microtime(true) * 1000); + $queue = $consumer->getQueue(); + if (false == array_key_exists($queue->getQueueName(), $this->boundProcessors)) { + throw new \LogicException(sprintf('The processor for the queue "%s" could not be found.', $queue->getQueueName())); } - } elseif ($this->psrContext instanceof AmqpContext) { - $callback = function (AmqpMessage $message, AmqpConsumer $consumer) use (&$context) { - $currentProcessor = null; - - /** @var PsrQueue $queue */ - foreach ($this->boundProcessors as list($queue, $processor)) { - if ($queue->getQueueName() === $consumer->getQueue()->getQueueName()) { - $currentProcessor = $processor; - } - } - if (false == $currentProcessor) { - throw new \LogicException(sprintf('The processor for the queue "%s" could not be found.', $consumer->getQueue()->getQueueName())); - } + $processor = $this->boundProcessors[$queue->getQueueName()]->getProcessor(); - $context = new Context($this->psrContext); - $context->setLogger($this->logger); - $context->setPsrQueue($consumer->getQueue()); - $context->setPsrConsumer($consumer); - $context->setPsrProcessor($currentProcessor); - $context->setPsrMessage($message); + $messageReceived = new MessageReceived($this->interopContext, $consumer, $message, $processor, $receivedAt, $this->logger); + $extension->onMessageReceived($messageReceived); + $result = $messageReceived->getResult(); + $processor = $messageReceived->getProcessor(); + if (null === $result) { + try { + $result = $processor->process($message, $this->interopContext); + } catch (\Exception|\Throwable $e) { + $result = $this->onProcessorException($extension, $consumer, $message, $e, $receivedAt); + } + } - $this->doConsume($this->extension, $context); + $messageResult = new MessageResult($this->interopContext, $consumer, $message, $result, $receivedAt, $this->logger); + $extension->onResult($messageResult); + $result = $messageResult->getResult(); + + switch ($result) { + case Result::ACK: + $consumer->acknowledge($message); + break; + case Result::REJECT: + $consumer->reject($message, false); + break; + case Result::REQUEUE: + $consumer->reject($message, true); + break; + case Result::ALREADY_ACKNOWLEDGED: + break; + default: + throw new \LogicException(sprintf('Status is not supported: %s', $result)); + } - return true; - }; + $postMessageReceived = new PostMessageReceived($this->interopContext, $consumer, $message, $result, $receivedAt, $this->logger); + $extension->onPostMessageReceived($postMessageReceived); - foreach ($consumers as $consumer) { - /* @var AmqpConsumer $consumer */ + if ($postMessageReceived->isExecutionInterrupted()) { + $interruptExecution = true; - $this->psrContext->subscribe($consumer, $callback); + return false; } - } - while (true) { - try { - if ($this->enableSubscriptionConsumer) { - $this->extension->onBeforeReceive($context); - - if ($context->isExecutionInterrupted()) { - throw new ConsumptionInterruptedException(); - } - - $subscriptionConsumer->consume($this->receiveTimeout); - - usleep($this->idleTimeout * 1000); - $this->extension->onIdle($context); - } elseif ($this->psrContext instanceof AmqpContext) { - $this->extension->onBeforeReceive($context); - - if ($context->isExecutionInterrupted()) { - throw new ConsumptionInterruptedException(); - } - - $this->psrContext->consume($this->receiveTimeout); - - usleep($this->idleTimeout * 1000); - $this->extension->onIdle($context); - } else { - /** @var PsrQueue $queue */ - foreach ($this->boundProcessors as list($queue, $processor)) { - $consumer = $consumers[$queue->getQueueName()]; - - $context = new Context($this->psrContext); - $context->setLogger($this->logger); - $context->setPsrQueue($queue); - $context->setPsrConsumer($consumer); - $context->setPsrProcessor($processor); - - $this->doConsume($this->extension, $context); - } - } - } catch (ConsumptionInterruptedException $e) { - $this->logger->info(sprintf('Consuming interrupted')); + return true; + }; - if ($this->enableSubscriptionConsumer) { - foreach ($consumers as $consumer) { - /* @var PsrConsumer $consumer */ + foreach ($consumers as $queueName => $consumer) { + /* @var Consumer $consumer */ - $subscriptionConsumer->unsubscribe($consumer); - } - } elseif ($this->psrContext instanceof AmqpContext) { - foreach ($consumers as $consumer) { - /* @var AmqpConsumer $consumer */ + $preSubscribe = new PreSubscribe( + $this->interopContext, + $this->boundProcessors[$queueName]->getProcessor(), + $consumer, + $this->logger + ); - $this->psrContext->unsubscribe($consumer); - } - } + $extension->onPreSubscribe($preSubscribe); - $context->setExecutionInterrupted(true); + $subscriptionConsumer->subscribe($consumer, $callback); + } + + $cycle = 1; + while (true) { + $receivedMessagesCount = 0; + $interruptExecution = false; + + $preConsume = new PreConsume($this->interopContext, $subscriptionConsumer, $this->logger, $cycle, $this->receiveTimeout, $startTime); + $extension->onPreConsume($preConsume); - $this->extension->onInterrupted($context); + if ($preConsume->isExecutionInterrupted()) { + $this->onEnd($extension, $startTime, $preConsume->getExitStatus(), $subscriptionConsumer); return; - } catch (\Exception $exception) { - $context->setExecutionInterrupted(true); - $context->setException($exception); + } - try { - $this->onInterruptionByException($this->extension, $context); - } catch (\Exception $e) { - // for some reason finally does not work here on php5.5 + $subscriptionConsumer->consume($this->receiveTimeout); - throw $e; - } + $postConsume = new PostConsume($this->interopContext, $subscriptionConsumer, $receivedMessagesCount, $cycle, $startTime, $this->logger); + $extension->onPostConsume($postConsume); + + if ($interruptExecution || $postConsume->isExecutionInterrupted()) { + $this->onEnd($extension, $startTime, $postConsume->getExitStatus(), $subscriptionConsumer); + + return; } + + ++$cycle; } } /** - * @param bool $enableSubscriptionConsumer - * @throws \Enqueue\Consumption\Exception\InvalidArgumentException + * @internal */ - public function enableSubscriptionConsumer($enableSubscriptionConsumer) + public function setFallbackSubscriptionConsumer(SubscriptionConsumer $fallbackSubscriptionConsumer): void { - if (!is_bool($enableSubscriptionConsumer)) { - throw new InvalidArgumentException( - sprintf( - 'The argument must be a boolean but got %s.', - is_object($enableSubscriptionConsumer) ? get_class($enableSubscriptionConsumer) : gettype($enableSubscriptionConsumer) - ) - ); - } - - $this->enableSubscriptionConsumer = $enableSubscriptionConsumer; + $this->fallbackSubscriptionConsumer = $fallbackSubscriptionConsumer; } - /** - * @param ExtensionInterface $extension - * @param Context $context - * - * @throws ConsumptionInterruptedException - * - * @return bool - */ - protected function doConsume(ExtensionInterface $extension, Context $context) + private function onEnd(ExtensionInterface $extension, int $startTime, ?int $exitStatus = null, ?SubscriptionConsumer $subscriptionConsumer = null): void { - $processor = $context->getPsrProcessor(); - $consumer = $context->getPsrConsumer(); - $this->logger = $context->getLogger(); - - if ($context->isExecutionInterrupted()) { - throw new ConsumptionInterruptedException(); - } - - $message = $context->getPsrMessage(); - if (false == $message) { - $this->extension->onBeforeReceive($context); + $endTime = (int) (microtime(true) * 1000); - if ($message = $consumer->receive($this->receiveTimeout)) { - $context->setPsrMessage($message); - } - } + $endContext = new End($this->interopContext, $startTime, $endTime, $this->logger, $exitStatus); + $extension->onEnd($endContext); - if ($message) { - $this->processMessage($consumer, $processor, $message, $context); - } else { - usleep($this->idleTimeout * 1000); - $this->extension->onIdle($context); - } - - if ($context->isExecutionInterrupted()) { - throw new ConsumptionInterruptedException(); + if ($subscriptionConsumer) { + $subscriptionConsumer->unsubscribeAll(); } } /** - * @param ExtensionInterface $extension - * @param Context $context + * The logic is similar to one in Symfony's ExceptionListener::onKernelException(). * - * @throws \Exception + * https://github.com/symfony/symfony/blob/cbe289517470eeea27162fd2d523eb29c95f775f/src/Symfony/Component/HttpKernel/EventListener/ExceptionListener.php#L77 */ - protected function onInterruptionByException(ExtensionInterface $extension, Context $context) + private function onProcessorException(ExtensionInterface $extension, Consumer $consumer, Message $message, \Throwable $exception, int $receivedAt) { - $this->logger = $context->getLogger(); - $this->logger->error(sprintf('Consuming interrupted by exception')); - - $exception = $context->getException(); + $processorException = new ProcessorException($this->interopContext, $consumer, $message, $exception, $receivedAt, $this->logger); try { - $this->extension->onInterrupted($context); + $extension->onProcessorException($processorException); + + $result = $processorException->getResult(); + if (null === $result) { + throw $exception; + } + + return $result; } catch (\Exception $e) { - // logic is similar to one in Symfony's ExceptionListener::onKernelException - $this->logger->error(sprintf( - 'Exception thrown when handling an exception (%s: %s at %s line %s)', - get_class($e), - $e->getMessage(), - $e->getFile(), - $e->getLine() - )); - - $wrapper = $e; - while ($prev = $wrapper->getPrevious()) { + $prev = $e; + do { if ($exception === $wrapper = $prev) { throw $e; } - } + } while ($prev = $wrapper->getPrevious()); - $prev = new \ReflectionProperty('Exception', 'previous'); + $prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous'); $prev->setAccessible(true); $prev->setValue($wrapper, $exception); throw $e; } - - throw $exception; - } - - private function processMessage(PsrConsumer $consumer, PsrProcessor $processor, PsrMessage $message, Context $context) - { - $this->logger->info('Message received from the queue: '.$context->getPsrQueue()->getQueueName()); - $this->logger->debug('Headers: {headers}', ['headers' => new VarExport($message->getHeaders())]); - $this->logger->debug('Properties: {properties}', ['properties' => new VarExport($message->getProperties())]); - $this->logger->debug('Payload: {payload}', ['payload' => new VarExport($message->getBody())]); - - $this->extension->onPreReceived($context); - if (!$context->getResult()) { - $result = $processor->process($message, $this->psrContext); - $context->setResult($result); - } - - $this->extension->onResult($context); - - switch ($context->getResult()) { - case Result::ACK: - $consumer->acknowledge($message); - break; - case Result::REJECT: - $consumer->reject($message, false); - break; - case Result::REQUEUE: - $consumer->reject($message, true); - break; - default: - throw new \LogicException(sprintf('Status is not supported: %s', $context->getResult())); - } - - $this->logger->info(sprintf('Message processed: %s', $context->getResult())); - - $this->extension->onPostReceived($context); } } diff --git a/pkg/enqueue/Consumption/QueueConsumerInterface.php b/pkg/enqueue/Consumption/QueueConsumerInterface.php new file mode 100644 index 000000000..ee2565252 --- /dev/null +++ b/pkg/enqueue/Consumption/QueueConsumerInterface.php @@ -0,0 +1,40 @@ +status = (string) $status; @@ -72,17 +70,14 @@ public function getReason() } /** - * @return PsrMessage|null + * @return InteropMessage|null */ public function getReply() { return $this->reply; } - /** - * @param PsrMessage|null $reply - */ - public function setReply(PsrMessage $reply = null) + public function setReply(?InteropMessage $reply = null) { $this->reply = $reply; } @@ -94,7 +89,7 @@ public function setReply(PsrMessage $reply = null) */ public static function ack($reason = '') { - return new static(self::ACK, $reason); + return new self(self::ACK, $reason); } /** @@ -104,7 +99,7 @@ public static function ack($reason = '') */ public static function reject($reason) { - return new static(self::REJECT, $reason); + return new self(self::REJECT, $reason); } /** @@ -114,21 +109,20 @@ public static function reject($reason) */ public static function requeue($reason = '') { - return new static(self::REQUEUE, $reason); + return new self(self::REQUEUE, $reason); } /** - * @param PsrMessage $replyMessage * @param string $status * @param string|null $reason * * @return static */ - public static function reply(PsrMessage $replyMessage, $status = self::ACK, $reason = null) + public static function reply(InteropMessage $replyMessage, $status = self::ACK, $reason = null) { $status = null === $status ? self::ACK : $status; - $result = new static($status, $reason); + $result = new self($status, $reason); $result->setReply($replyMessage); return $result; diff --git a/pkg/enqueue/Consumption/StartExtensionInterface.php b/pkg/enqueue/Consumption/StartExtensionInterface.php new file mode 100644 index 000000000..98571061c --- /dev/null +++ b/pkg/enqueue/Consumption/StartExtensionInterface.php @@ -0,0 +1,13 @@ +services = $services; + } + + public function get($id) + { + if (false == $this->has($id)) { + throw new NotFoundException(sprintf('The service "%s" not found.', $id)); + } + + return $this->services[$id]; + } + + public function has(string $id): bool + { + return array_key_exists($id, $this->services); + } +} diff --git a/pkg/enqueue/Container/NotFoundException.php b/pkg/enqueue/Container/NotFoundException.php new file mode 100644 index 000000000..fcc3386e6 --- /dev/null +++ b/pkg/enqueue/Container/NotFoundException.php @@ -0,0 +1,9 @@ +doctrine = $doctrine; + $this->fallbackFactory = $fallbackFactory; + } + + public function create($config): ConnectionFactory + { + if (is_string($config)) { + $config = ['dsn' => $config]; + } + + if (false == is_array($config)) { + throw new \InvalidArgumentException('The config must be either array or DSN string.'); + } + + if (false == array_key_exists('dsn', $config)) { + throw new \InvalidArgumentException('The config must have dsn key set.'); + } + + $dsn = Dsn::parseFirst($config['dsn']); + + if ('doctrine' === $dsn->getScheme()) { + $config = $dsn->getQuery(); + $config['connection_name'] = $dsn->getHost(); + + return new ManagerRegistryConnectionFactory($this->doctrine, $config); + } + + return $this->fallbackFactory->create($config); + } +} diff --git a/pkg/enqueue/Doctrine/DoctrineDriverFactory.php b/pkg/enqueue/Doctrine/DoctrineDriverFactory.php new file mode 100644 index 000000000..aab6489aa --- /dev/null +++ b/pkg/enqueue/Doctrine/DoctrineDriverFactory.php @@ -0,0 +1,41 @@ +fallbackFactory = $fallbackFactory; + } + + public function create(ConnectionFactory $factory, Config $config, RouteCollection $collection): DriverInterface + { + $dsn = $config->getTransportOption('dsn'); + + if (empty($dsn)) { + throw new \LogicException('This driver factory relies on dsn option from transport config. The option is empty or not set.'); + } + + $dsn = Dsn::parseFirst($dsn); + + if ('doctrine' === $dsn->getScheme()) { + return new DbalDriver($factory->createContext(), $config, $collection); + } + + return $this->fallbackFactory->create($factory, $config, $collection); + } +} diff --git a/pkg/enqueue/Doctrine/DoctrineSchemaCompilerPass.php b/pkg/enqueue/Doctrine/DoctrineSchemaCompilerPass.php new file mode 100644 index 000000000..0eb378470 --- /dev/null +++ b/pkg/enqueue/Doctrine/DoctrineSchemaCompilerPass.php @@ -0,0 +1,39 @@ +hasDefinition('doctrine')) { + return; + } + + foreach ($container->getParameter('enqueue.transports') as $name) { + $diUtils = DiUtils::create(TransportFactory::MODULE, $name); + + $container->register($diUtils->format('connection_factory_factory.outer'), DoctrineConnectionFactoryFactory::class) + ->setDecoratedService($diUtils->format('connection_factory_factory'), $diUtils->format('connection_factory_factory.inner')) + ->addArgument(new Reference('doctrine')) + ->addArgument(new Reference($diUtils->format('connection_factory_factory.inner'))) + ; + } + + foreach ($container->getParameter('enqueue.clients') as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $container->register($diUtils->format('driver_factory.outer'), DoctrineDriverFactory::class) + ->setDecoratedService($diUtils->format('driver_factory'), $diUtils->format('driver_factory.inner')) + ->addArgument(new Reference($diUtils->format('driver_factory.inner'))) + ; + } + } +} diff --git a/pkg/enqueue/ProcessorRegistryInterface.php b/pkg/enqueue/ProcessorRegistryInterface.php new file mode 100644 index 000000000..5306c3035 --- /dev/null +++ b/pkg/enqueue/ProcessorRegistryInterface.php @@ -0,0 +1,12 @@ +Supporting Enqueue + +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Message Queue. [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/enqueue.png?branch=master)](https://travis-ci.org/php-enqueue/enqueue) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/enqueue/ci.yml?branch=master)](https://github.com/php-enqueue/enqueue/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/enqueue/d/total.png)](https://packagist.org/packages/enqueue/enqueue) [![Latest Stable Version](https://poser.pugx.org/enqueue/enqueue/version.png)](https://packagist.org/packages/enqueue/enqueue) - -It contains advanced features build on top of a transport component. + +It contains advanced features build on top of a transport component. Client component kind of plug and play things or consumption component that simplify message processing a lot. -Read more about it in documentation. +Read more about it in documentation. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/enqueue/Resources.php b/pkg/enqueue/Resources.php new file mode 100644 index 000000000..4c500006f --- /dev/null +++ b/pkg/enqueue/Resources.php @@ -0,0 +1,211 @@ + [ + * schemes => [schemes strings], + * package => package name, + * ]. + * + * @var array + */ + private static $knownConnections; + + private function __construct() + { + } + + public static function getAvailableConnections(): array + { + $map = self::getKnownConnections(); + + $availableMap = []; + foreach ($map as $connectionClass => $item) { + if (\class_exists($connectionClass)) { + $availableMap[$connectionClass] = $item; + } + } + + return $availableMap; + } + + public static function getKnownSchemes(): array + { + $map = self::getKnownConnections(); + + $schemes = []; + foreach ($map as $connectionClass => $item) { + foreach ($item['schemes'] as $scheme) { + $schemes[$scheme] = $connectionClass; + } + } + + return $schemes; + } + + public static function getAvailableSchemes(): array + { + $map = self::getAvailableConnections(); + + $schemes = []; + foreach ($map as $connectionClass => $item) { + foreach ($item['schemes'] as $scheme) { + $schemes[$scheme] = $connectionClass; + } + } + + return $schemes; + } + + public static function getKnownConnections(): array + { + if (null === self::$knownConnections) { + $map = []; + + $map[FsConnectionFactory::class] = [ + 'schemes' => ['file'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/fs', + ]; + $map[AmqpBunnyConnectionFactory::class] = [ + 'schemes' => ['amqp'], + 'supportedSchemeExtensions' => ['bunny'], + 'package' => 'enqueue/amqp-bunny', + ]; + $map[AmqpExtConnectionFactory::class] = [ + 'schemes' => ['amqp', 'amqps'], + 'supportedSchemeExtensions' => ['ext'], + 'package' => 'enqueue/amqp-ext', + ]; + $map[AmqpLibConnectionFactory::class] = [ + 'schemes' => ['amqp', 'amqps'], + 'supportedSchemeExtensions' => ['lib'], + 'package' => 'enqueue/amqp-lib', + ]; + + $map[DbalConnectionFactory::class] = [ + 'schemes' => [ + 'db2', + 'ibm-db2', + 'mssql', + 'sqlsrv', + 'mysql', + 'mysql2', + 'mysql', + 'pgsql', + 'postgres', + 'sqlite', + 'sqlite3', + 'sqlite', + ], + 'supportedSchemeExtensions' => ['pdo'], + 'package' => 'enqueue/dbal', + ]; + + $map[NullConnectionFactory::class] = [ + 'schemes' => ['null'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/null', + ]; + $map[GearmanConnectionFactory::class] = [ + 'schemes' => ['gearman'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/gearman', + ]; + $map[PheanstalkConnectionFactory::class] = [ + 'schemes' => ['beanstalk'], + 'supportedSchemeExtensions' => ['pheanstalk'], + 'package' => 'enqueue/pheanstalk', + ]; + $map[RdKafkaConnectionFactory::class] = [ + 'schemes' => ['kafka', 'rdkafka'], + 'supportedSchemeExtensions' => ['rdkafka'], + 'package' => 'enqueue/rdkafka', + ]; + $map[RedisConnectionFactory::class] = [ + 'schemes' => ['redis', 'rediss'], + 'supportedSchemeExtensions' => ['predis', 'phpredis'], + 'package' => 'enqueue/redis', + ]; + $map[StompConnectionFactory::class] = [ + 'schemes' => ['stomp'], + 'supportedSchemeExtensions' => ['rabbitmq'], + 'package' => 'enqueue/stomp', ]; + $map[SqsConnectionFactory::class] = [ + 'schemes' => ['sqs'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/sqs', ]; + $map[SnsConnectionFactory::class] = [ + 'schemes' => ['sns'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/sns', ]; + $map[SnsQsConnectionFactory::class] = [ + 'schemes' => ['snsqs'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/snsqs', ]; + $map[GpsConnectionFactory::class] = [ + 'schemes' => ['gps'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/gps', ]; + $map[MongodbConnectionFactory::class] = [ + 'schemes' => ['mongodb'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/mongodb', + ]; + $map[WampConnectionFactory::class] = [ + 'schemes' => ['wamp', 'ws'], + 'supportedSchemeExtensions' => [], + 'package' => 'enqueue/wamp', + ]; + + self::$knownConnections = $map; + } + + return self::$knownConnections; + } + + public static function addConnection(string $connectionFactoryClass, array $schemes, array $extensions, string $package): void + { + if (\class_exists($connectionFactoryClass)) { + if (false == (new \ReflectionClass($connectionFactoryClass))->implementsInterface(ConnectionFactory::class)) { + throw new \InvalidArgumentException(\sprintf('The connection factory class "%s" must implement "%s" interface.', $connectionFactoryClass, ConnectionFactory::class)); + } + } + + if (empty($schemes)) { + throw new \InvalidArgumentException('Schemes could not be empty.'); + } + if (empty($package)) { + throw new \InvalidArgumentException('Package name could not be empty.'); + } + + self::getKnownConnections(); + self::$knownConnections[$connectionFactoryClass] = [ + 'schemes' => $schemes, + 'supportedSchemeExtensions' => $extensions, + 'package' => $package, + ]; + } +} diff --git a/pkg/enqueue/Router/Recipient.php b/pkg/enqueue/Router/Recipient.php index dc0ab4260..d2f668f42 100644 --- a/pkg/enqueue/Router/Recipient.php +++ b/pkg/enqueue/Router/Recipient.php @@ -2,33 +2,29 @@ namespace Enqueue\Router; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; +use Interop\Queue\Destination; +use Interop\Queue\Message as InteropMessage; class Recipient { /** - * @var PsrDestination + * @var Destination */ private $destination; /** - * @var PsrMessage + * @var InteropMessage */ private $message; - /** - * @param PsrDestination $destination - * @param PsrMessage $message - */ - public function __construct(PsrDestination $destination, PsrMessage $message) + public function __construct(Destination $destination, InteropMessage $message) { $this->destination = $destination; $this->message = $message; } /** - * @return PsrDestination + * @return Destination */ public function getDestination() { @@ -36,7 +32,7 @@ public function getDestination() } /** - * @return PsrMessage + * @return InteropMessage */ public function getMessage() { diff --git a/pkg/enqueue/Router/RecipientListRouterInterface.php b/pkg/enqueue/Router/RecipientListRouterInterface.php index 78a354c6a..6bb950fdc 100644 --- a/pkg/enqueue/Router/RecipientListRouterInterface.php +++ b/pkg/enqueue/Router/RecipientListRouterInterface.php @@ -2,14 +2,12 @@ namespace Enqueue\Router; -use Interop\Queue\PsrMessage; +use Interop\Queue\Message as InteropMessage; interface RecipientListRouterInterface { /** - * @param PsrMessage $message - * * @return \Traversable|Recipient[] */ - public function route(PsrMessage $message); + public function route(InteropMessage $message); } diff --git a/pkg/enqueue/Router/RouteRecipientListProcessor.php b/pkg/enqueue/Router/RouteRecipientListProcessor.php index 59df35510..22488e33f 100644 --- a/pkg/enqueue/Router/RouteRecipientListProcessor.php +++ b/pkg/enqueue/Router/RouteRecipientListProcessor.php @@ -2,29 +2,23 @@ namespace Enqueue\Router; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class RouteRecipientListProcessor implements PsrProcessor +class RouteRecipientListProcessor implements Processor { /** * @var RecipientListRouterInterface */ private $router; - /** - * @param RecipientListRouterInterface $router - */ public function __construct(RecipientListRouterInterface $router) { $this->router = $router; } - /** - * {@inheritdoc} - */ - public function process(PsrMessage $message, PsrContext $context) + public function process(InteropMessage $message, Context $context) { $producer = $context->createProducer(); foreach ($this->router->route($message) as $recipient) { diff --git a/pkg/enqueue/Rpc/Promise.php b/pkg/enqueue/Rpc/Promise.php index 91e0aaa22..01b47e1f6 100644 --- a/pkg/enqueue/Rpc/Promise.php +++ b/pkg/enqueue/Rpc/Promise.php @@ -2,7 +2,7 @@ namespace Enqueue\Rpc; -use Interop\Queue\PsrMessage; +use Interop\Queue\Message as InteropMessage; class Promise { @@ -27,15 +27,10 @@ class Promise private $deleteReplyQueue; /** - * @var PsrMessage + * @var InteropMessage */ private $message; - /** - * @param \Closure $receiveCallback - * @param \Closure $receiveNoWaitCallback - * @param \Closure $finallyCallback - */ public function __construct(\Closure $receiveCallback, \Closure $receiveNoWaitCallback, \Closure $finallyCallback) { $this->receiveCallback = $receiveCallback; @@ -52,7 +47,7 @@ public function __construct(\Closure $receiveCallback, \Closure $receiveNoWaitCa * * @throws TimeoutException if the wait timeout is reached * - * @return PsrMessage + * @return InteropMessage */ public function receive($timeout = null) { @@ -72,7 +67,7 @@ public function receive($timeout = null) /** * Non blocking function. Returns message or null. * - * @return PsrMessage|null + * @return InteropMessage|null */ public function receiveNoWait() { @@ -106,18 +101,16 @@ public function isDeleteReplyQueue() } /** - * @param \Closure $cb - * @param array $args + * @param array $args * - * @return PsrMessage + * @return InteropMessage */ private function doReceive(\Closure $cb, ...$args) { $message = call_user_func_array($cb, $args); - if (null !== $message && false == $message instanceof PsrMessage) { - throw new \RuntimeException(sprintf( - 'Expected "%s" but got: "%s"', PsrMessage::class, is_object($message) ? get_class($message) : gettype($message))); + if (null !== $message && false == $message instanceof InteropMessage) { + throw new \RuntimeException(sprintf('Expected "%s" but got: "%s"', InteropMessage::class, is_object($message) ? $message::class : gettype($message))); } return $message; diff --git a/pkg/enqueue/Rpc/RpcClient.php b/pkg/enqueue/Rpc/RpcClient.php index d429ac9af..bd3d7cedb 100644 --- a/pkg/enqueue/Rpc/RpcClient.php +++ b/pkg/enqueue/Rpc/RpcClient.php @@ -3,14 +3,14 @@ namespace Enqueue\Rpc; use Enqueue\Util\UUID; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; +use Interop\Queue\Context; +use Interop\Queue\Destination; +use Interop\Queue\Message as InteropMessage; class RpcClient { /** - * @var PsrContext + * @var Context */ private $context; @@ -19,38 +19,30 @@ class RpcClient */ private $rpcFactory; - /** - * @param PsrContext $context - * @param RpcFactory $promiseFactory - */ - public function __construct(PsrContext $context, RpcFactory $promiseFactory = null) + public function __construct(Context $context, ?RpcFactory $promiseFactory = null) { $this->context = $context; $this->rpcFactory = $promiseFactory ?: new RpcFactory($context); } /** - * @param PsrDestination $destination - * @param PsrMessage $message - * @param int $timeout + * @param int $timeout * * @throws TimeoutException if the wait timeout is reached * - * @return PsrMessage + * @return InteropMessage */ - public function call(PsrDestination $destination, PsrMessage $message, $timeout) + public function call(Destination $destination, InteropMessage $message, $timeout) { return $this->callAsync($destination, $message, $timeout)->receive(); } /** - * @param PsrDestination $destination - * @param PsrMessage $message - * @param int $timeout + * @param int $timeout * * @return Promise */ - public function callAsync(PsrDestination $destination, PsrMessage $message, $timeout) + public function callAsync(Destination $destination, InteropMessage $message, $timeout) { if ($timeout < 1) { throw new \InvalidArgumentException(sprintf('Timeout must be positive not zero integer. Got %s', $timeout)); diff --git a/pkg/enqueue/Rpc/RpcFactory.php b/pkg/enqueue/Rpc/RpcFactory.php index 195fd959a..9100babd3 100644 --- a/pkg/enqueue/Rpc/RpcFactory.php +++ b/pkg/enqueue/Rpc/RpcFactory.php @@ -2,19 +2,16 @@ namespace Enqueue\Rpc; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; class RpcFactory { /** - * @var PsrContext + * @var Context */ private $context; - /** - * @param PsrContext $context - */ - public function __construct(PsrContext $context) + public function __construct(Context $context) { $this->context = $context; } diff --git a/pkg/enqueue/Rpc/TimeoutException.php b/pkg/enqueue/Rpc/TimeoutException.php index a0b065511..a7f68b967 100644 --- a/pkg/enqueue/Rpc/TimeoutException.php +++ b/pkg/enqueue/Rpc/TimeoutException.php @@ -12,6 +12,6 @@ class TimeoutException extends \LogicException */ public static function create($timeout, $correlationId) { - return new static(sprintf('Rpc call timeout is reached without receiving a reply message. Timeout: %s, CorrelationId: %s', $timeout, $correlationId)); + return new self(sprintf('Rpc call timeout is reached without receiving a reply message. Timeout: %s, CorrelationId: %s', $timeout, $correlationId)); } } diff --git a/pkg/enqueue/Symfony/AmqpTransportFactory.php b/pkg/enqueue/Symfony/AmqpTransportFactory.php deleted file mode 100644 index e69bf44de..000000000 --- a/pkg/enqueue/Symfony/AmqpTransportFactory.php +++ /dev/null @@ -1,253 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $transportsMap = static::getAvailableTransportsMap(); - - $builder - ->beforeNormalization() - ->ifTrue(function ($v) { - return empty($v); - }) - ->then(function ($v) { - return ['dsn' => 'amqp:']; - }) - ->ifString() - ->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('driver') - ->validate() - ->always(function ($v) use ($transportsMap) { - $drivers = array_keys($transportsMap); - if (empty($transportsMap)) { - throw new \InvalidArgumentException('There is no amqp driver available. Please consider installing one of the packages: enqueue/amqp-ext, enqueue/amqp-lib, enqueue/amqp-bunny.'); - } - - if ($v && false == in_array($v, $drivers, true)) { - throw new \InvalidArgumentException(sprintf('Unexpected driver given "%s". Available are "%s"', $v, implode('", "', $drivers))); - } - - return $v; - }) - ->end() - ->end() - ->scalarNode('dsn') - ->info('The connection to AMQP broker set as a string. Other parameters could be used as defaults') - ->end() - ->scalarNode('host') - ->info('The host to connect too. Note: Max 1024 characters') - ->end() - ->scalarNode('port') - ->info('Port on the host.') - ->end() - ->scalarNode('user') - ->info('The user name to use. Note: Max 128 characters.') - ->end() - ->scalarNode('pass') - ->info('Password. Note: Max 128 characters.') - ->end() - ->scalarNode('vhost') - ->info('The virtual host on the host. Note: Max 128 characters.') - ->end() - ->floatNode('connection_timeout') - ->min(0) - ->info('Connection timeout. Note: 0 or greater seconds. May be fractional.') - ->end() - ->floatNode('read_timeout') - ->min(0) - ->info('Timeout in for income activity. Note: 0 or greater seconds. May be fractional.') - ->end() - ->floatNode('write_timeout') - ->min(0) - ->info('Timeout in for outcome activity. Note: 0 or greater seconds. May be fractional.') - ->end() - ->floatNode('heartbeat') - ->min(0) - ->info('How often to send heartbeat. 0 means off.') - ->end() - ->booleanNode('persisted')->end() - ->booleanNode('lazy')->end() - ->enumNode('receive_method') - ->values(['basic_get', 'basic_consume']) - ->info('The receive strategy to be used. We suggest to use basic_consume as it is more performant. Though you need AMQP extension 1.9.1 or higher') - ->end() - ->floatNode('qos_prefetch_size') - ->min(0) - ->info('The server will send a message in advance if it is equal to or smaller in size than the available prefetch size. May be set to zero, meaning "no specific limit"') - ->end() - ->floatNode('qos_prefetch_count') - ->min(0) - ->info('Specifies a prefetch window in terms of whole messages') - ->end() - ->booleanNode('qos_global') - ->info('If "false" the QoS settings apply to the current channel only. If this field is "true", they are applied to the entire connection.') - ->end() - ->variableNode('driver_options') - ->info('The options that are specific to the amqp transport you chose. For example amqp+lib have insist, keepalive, stream options. amqp+bunny has tcp_nodelay extra option.') - ->end() - ->booleanNode('ssl_on') - ->info('Should be true if you want to use secure connections. False by default') - ->end() - ->booleanNode('ssl_verify') - ->info('This option determines whether ssl client verifies that the server cert is for the server it is known as. True by default.') - ->end() - ->scalarNode('ssl_cacert') - ->info('Location of Certificate Authority file on local filesystem which should be used with the verify_peer context option to authenticate the identity of the remote peer. A string.') - ->end() - ->scalarNode('ssl_cert') - ->info('Path to local certificate file on filesystem. It must be a PEM encoded file which contains your certificate and private key. A string') - ->end() - ->scalarNode('ssl_key') - ->info('Path to local private key file on filesystem in case of separate files for certificate (local_cert) and private key. A string.') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - if (array_key_exists('driver_options', $config) && is_array($config['driver_options'])) { - $driverOptions = $config['driver_options']; - unset($config['driver_options']); - - $config = array_replace($driverOptions, $config); - } - - $factory = new Definition(AmqpConnectionFactory::class); - $factory->setFactory([self::class, 'createConnectionFactoryFactory']); - $factory->setArguments([$config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(AmqpContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(AmqpDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } - - public static function createConnectionFactoryFactory(array $config) - { - if (false == empty($config['driver'])) { - $transportsMap = static::getAvailableTransportsMap(); - - if (false == array_key_exists($config['driver'], $transportsMap)) { - throw new \InvalidArgumentException(sprintf('Unexpected driver given "invalidDriver". Available are "%s"', implode('", "', array_keys($transportsMap)))); - } - - $connectionFactoryClass = $transportsMap[$config['driver']]; - - unset($config['driver']); - - return new $connectionFactoryClass($config); - } - - $dsn = array_key_exists('dsn', $config) ? $config['dsn'] : 'amqp:'; - $factory = dsn_to_connection_factory($dsn); - - if (false == $factory instanceof AmqpConnectionFactory) { - throw new \LogicException(sprintf('Factory must be instance of "%s" but got "%s"', AmqpConnectionFactory::class, get_class($factory))); - } - - $factoryClass = get_class($factory); - - return new $factoryClass($config); - } - - /** - * @return string[] - */ - private static function getAvailableTransportsMap() - { - $map = []; - if (class_exists(AmqpExtConnectionFactory::class)) { - $map['ext'] = AmqpExtConnectionFactory::class; - } - if (class_exists(AmqpLibConnectionFactory::class)) { - $map['lib'] = AmqpLibConnectionFactory::class; - } - if (class_exists(AmqpBunnyConnectionFactory::class)) { - $map['bunny'] = AmqpBunnyConnectionFactory::class; - } - - return $map; - } -} diff --git a/pkg/enqueue/Symfony/Client/ConsumeCommand.php b/pkg/enqueue/Symfony/Client/ConsumeCommand.php new file mode 100644 index 000000000..5a0676705 --- /dev/null +++ b/pkg/enqueue/Symfony/Client/ConsumeCommand.php @@ -0,0 +1,181 @@ +container = $container; + $this->defaultClient = $defaultClient; + $this->queueConsumerIdPattern = $queueConsumerIdPattern; + $this->driverIdPattern = $driverIdPattern; + $this->processorIdPattern = $processorIdPatter; + + parent::__construct(); + } + + protected function configure(): void + { + $this->configureLimitsExtensions(); + $this->configureSetupBrokerExtension(); + $this->configureQueueConsumerOptions(); + $this->configureLoggerExtension(); + + $this + ->setAliases(['enq:c']) + ->setDescription('A client\'s worker that processes messages. '. + 'By default it connects to default queue. '. + 'It select an appropriate message processor based on a message headers') + ->addArgument('client-queue-names', InputArgument::IS_ARRAY, 'Queues to consume messages from') + ->addOption('skip', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Queues to skip consumption of messages from', []) + ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $client = $input->getOption('client'); + + try { + $consumer = $this->getQueueConsumer($client); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Client "%s" is not supported.', $client), null, $e); + } + + $driver = $this->getDriver($client); + $processor = $this->getProcessor($client); + + $this->setQueueConsumerOptions($consumer, $input); + + $allQueues[$driver->getConfig()->getDefaultQueue()] = true; + $allQueues[$driver->getConfig()->getRouterQueue()] = true; + foreach ($driver->getRouteCollection()->all() as $route) { + if (false == $route->getQueue()) { + continue; + } + if ($route->isProcessorExternal()) { + continue; + } + + $allQueues[$route->getQueue()] = $route->isPrefixQueue(); + } + + $selectedQueues = $input->getArgument('client-queue-names'); + if (empty($selectedQueues)) { + $queues = $allQueues; + } else { + $queues = []; + foreach ($selectedQueues as $queue) { + if (false == array_key_exists($queue, $allQueues)) { + throw new \LogicException(sprintf('There is no such queue "%s". Available are "%s"', $queue, implode('", "', array_keys($allQueues)))); + } + + $queues[$queue] = $allQueues[$queue]; + } + } + + foreach ($input->getOption('skip') as $skipQueue) { + unset($queues[$skipQueue]); + } + + foreach ($queues as $queue => $prefix) { + $queue = $driver->createQueue($queue, $prefix); + $consumer->bind($queue, $processor); + } + + $runtimeExtensionChain = $this->getRuntimeExtensions($input, $output); + $exitStatusExtension = new ExitStatusExtension(); + + $consumer->consume(new ChainExtension([$runtimeExtensionChain, $exitStatusExtension])); + + return $exitStatusExtension->getExitStatus() ?? 0; + } + + protected function getRuntimeExtensions(InputInterface $input, OutputInterface $output): ExtensionInterface + { + $extensions = []; + $extensions = array_merge($extensions, $this->getLimitsExtensions($input, $output)); + + $driver = $this->getDriver($input->getOption('client')); + + if ($setupBrokerExtension = $this->getSetupBrokerExtension($input, $driver)) { + $extensions[] = $setupBrokerExtension; + } + + if ($loggerExtension = $this->getLoggerExtension($input, $output)) { + array_unshift($extensions, $loggerExtension); + } + + return new ChainExtension($extensions); + } + + private function getDriver(string $name): DriverInterface + { + return $this->container->get(sprintf($this->driverIdPattern, $name)); + } + + private function getQueueConsumer(string $name): QueueConsumerInterface + { + return $this->container->get(sprintf($this->queueConsumerIdPattern, $name)); + } + + private function getProcessor(string $name): Processor + { + return $this->container->get(sprintf($this->processorIdPattern, $name)); + } +} diff --git a/pkg/enqueue/Symfony/Client/ConsumeMessagesCommand.php b/pkg/enqueue/Symfony/Client/ConsumeMessagesCommand.php deleted file mode 100644 index a3817b3e5..000000000 --- a/pkg/enqueue/Symfony/Client/ConsumeMessagesCommand.php +++ /dev/null @@ -1,135 +0,0 @@ -consumer = $consumer; - $this->processor = $processor; - $this->queueMetaRegistry = $queueMetaRegistry; - $this->driver = $driver; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->configureLimitsExtensions(); - $this->configureSetupBrokerExtension(); - $this->configureQueueConsumerOptions(); - - $this - ->setName('enqueue:consume') - ->setAliases(['enq:c']) - ->setDescription('A client\'s worker that processes messages. '. - 'By default it connects to default queue. '. - 'It select an appropriate message processor based on a message headers') - ->addArgument('client-queue-names', InputArgument::IS_ARRAY, 'Queues to consume messages from') - ->addOption('skip', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Queues to skip consumption of messages from', []) - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->setQueueConsumerOptions($this->consumer, $input); - - $queueMetas = []; - if ($clientQueueNames = $input->getArgument('client-queue-names')) { - foreach ($clientQueueNames as $clientQueueName) { - $queueMetas[] = $this->queueMetaRegistry->getQueueMeta($clientQueueName); - } - } else { - /** @var QueueMeta[] $queueMetas */ - $queueMetas = iterator_to_array($this->queueMetaRegistry->getQueuesMeta()); - - foreach ($queueMetas as $index => $queueMeta) { - if (in_array($queueMeta->getClientName(), $input->getOption('skip'), true)) { - unset($queueMetas[$index]); - } - } - } - - foreach ($queueMetas as $queueMeta) { - $queue = $this->driver->createQueue($queueMeta->getClientName()); - $this->consumer->bind($queue, $this->processor); - } - - $this->consumer->consume($this->getRuntimeExtensions($input, $output)); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * - * @return ChainExtension - */ - protected function getRuntimeExtensions(InputInterface $input, OutputInterface $output) - { - $extensions = [new LoggerExtension(new ConsoleLogger($output))]; - $extensions = array_merge($extensions, $this->getLimitsExtensions($input, $output)); - - if ($setupBrokerExtension = $this->getSetupBrokerExtension($input, $this->driver)) { - $extensions[] = $setupBrokerExtension; - } - - return new ChainExtension($extensions); - } -} diff --git a/pkg/enqueue/Symfony/Client/ContainerAwareProcessorRegistry.php b/pkg/enqueue/Symfony/Client/ContainerAwareProcessorRegistry.php deleted file mode 100644 index b378eb45d..000000000 --- a/pkg/enqueue/Symfony/Client/ContainerAwareProcessorRegistry.php +++ /dev/null @@ -1,68 +0,0 @@ -processors = $processors; - } - - /** - * @param string $processorName - * @param string $serviceId - */ - public function set($processorName, $serviceId) - { - $this->processors[$processorName] = $serviceId; - } - - /** - * {@inheritdoc} - */ - public function get($processorName) - { - if (30300 > Kernel::VERSION_ID) { - // Symfony 3.2 and below make service identifiers lowercase, so we do the same. - // To be removed when dropping support for Symfony < 3.3. - $processorName = strtolower($processorName); - } - - if (false == isset($this->processors[$processorName])) { - throw new \LogicException(sprintf('Processor was not found. processorName: "%s"', $processorName)); - } - - if (null === $this->container) { - throw new \LogicException('Container was not set'); - } - - $processor = $this->container->get($this->processors[$processorName]); - - if (false == $processor instanceof PsrProcessor) { - throw new \LogicException(sprintf( - 'Invalid instance of message processor. expected: "%s", got: "%s"', - PsrProcessor::class, - is_object($processor) ? get_class($processor) : gettype($processor) - )); - } - - return $processor; - } -} diff --git a/pkg/enqueue/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPass.php b/pkg/enqueue/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPass.php new file mode 100644 index 000000000..577f15902 --- /dev/null +++ b/pkg/enqueue/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPass.php @@ -0,0 +1,107 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $collection = RouteCollection::fromArray($container->getDefinition($routeCollectionId)->getArgument(0)); + + $this->exclusiveCommandsCouldNotBeRunOnDefaultQueue($collection); + $this->exclusiveCommandProcessorMustBeSingleOnGivenQueue($collection); + $this->customQueueNamesUnique($collection); + $this->defaultQueueMustBePrefixed($collection); + } + } + + private function exclusiveCommandsCouldNotBeRunOnDefaultQueue(RouteCollection $collection): void + { + foreach ($collection->all() as $route) { + if ($route->isCommand() && $route->isProcessorExclusive() && false == $route->getQueue()) { + throw new \LogicException(sprintf('The command "%s" processor "%s" is exclusive but queue is not specified. Exclusive processors could not be run on a default queue.', $route->getSource(), $route->getProcessor())); + } + } + } + + private function exclusiveCommandProcessorMustBeSingleOnGivenQueue(RouteCollection $collection): void + { + $prefixedQueues = []; + $queues = []; + foreach ($collection->all() as $route) { + if (false == $route->isCommand()) { + continue; + } + if (false == $route->isProcessorExclusive()) { + continue; + } + + if ($route->isPrefixQueue()) { + if (array_key_exists($route->getQueue(), $prefixedQueues)) { + throw new \LogicException(sprintf('The command "%s" processor "%s" is exclusive. The queue "%s" already has another exclusive command processor "%s" bound to it.', $route->getSource(), $route->getProcessor(), $route->getQueue(), $prefixedQueues[$route->getQueue()])); + } + + $prefixedQueues[$route->getQueue()] = $route->getProcessor(); + } else { + if (array_key_exists($route->getQueue(), $queues)) { + throw new \LogicException(sprintf('The command "%s" processor "%s" is exclusive. The queue "%s" already has another exclusive command processor "%s" bound to it.', $route->getSource(), $route->getProcessor(), $route->getQueue(), $queues[$route->getQueue()])); + } + + $queues[$route->getQueue()] = $route->getProcessor(); + } + } + } + + private function defaultQueueMustBePrefixed(RouteCollection $collection): void + { + foreach ($collection->all() as $route) { + if (false == $route->getQueue() && false == $route->isPrefixQueue()) { + throw new \LogicException('The default queue must be prefixed.'); + } + } + } + + private function customQueueNamesUnique(RouteCollection $collection): void + { + $prefixedQueues = []; + $notPrefixedQueues = []; + + foreach ($collection->all() as $route) { + // default queue + $queueName = $route->getQueue(); + if (false == $queueName) { + return; + } + + $route->isPrefixQueue() ? + $prefixedQueues[$queueName] = $queueName : + $notPrefixedQueues[$queueName] = $queueName + ; + } + + foreach ($notPrefixedQueues as $queueName) { + if (array_key_exists($queueName, $prefixedQueues)) { + throw new \LogicException(sprintf('There are prefixed and not prefixed queue with the same name "%s". This is not allowed.', $queueName)); + } + } + } +} diff --git a/pkg/enqueue/Symfony/Client/DependencyInjection/BuildClientExtensionsPass.php b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildClientExtensionsPass.php new file mode 100644 index 000000000..92124f243 --- /dev/null +++ b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildClientExtensionsPass.php @@ -0,0 +1,63 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $extensionsId = $diUtils->format('client_extensions'); + if (false == $container->hasDefinition($extensionsId)) { + throw new \LogicException(sprintf('Service "%s" not found', $extensionsId)); + } + + $tags = array_merge( + $container->findTaggedServiceIds('enqueue.client_extension'), + $container->findTaggedServiceIds('enqueue.client.extension') // TODO BC + ); + + $groupByPriority = []; + foreach ($tags as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + $priority = (int) ($tagAttribute['priority'] ?? 0); + + $groupByPriority[$priority][] = new Reference($serviceId); + } + } + + krsort($groupByPriority, \SORT_NUMERIC); + + $flatExtensions = []; + foreach ($groupByPriority as $extension) { + $flatExtensions = array_merge($flatExtensions, $extension); + } + + $extensionsService = $container->getDefinition($extensionsId); + $extensionsService->replaceArgument(0, array_merge( + $extensionsService->getArgument(0), + $flatExtensions + )); + } + } +} diff --git a/pkg/enqueue/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPass.php b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPass.php new file mode 100644 index 000000000..4adc09e9d --- /dev/null +++ b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPass.php @@ -0,0 +1,135 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $tag = 'enqueue.command_subscriber'; + $routeCollection = new RouteCollection([]); + foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) { + $processorDefinition = $container->getDefinition($serviceId); + if ($processorDefinition->getFactory()) { + throw new \LogicException('The command subscriber tag could not be applied to a service created by factory.'); + } + + $processorClass = $processorDefinition->getClass() ?? $serviceId; + if (false == class_exists($processorClass)) { + throw new \LogicException(sprintf('The processor class "%s" could not be found.', $processorClass)); + } + + if (false == is_subclass_of($processorClass, CommandSubscriberInterface::class)) { + throw new \LogicException(sprintf('The processor must implement "%s" interface to be used with the tag "%s"', CommandSubscriberInterface::class, $tag)); + } + + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + /** @var CommandSubscriberInterface $processorClass */ + $commands = $processorClass::getSubscribedCommand(); + + if (empty($commands)) { + throw new \LogicException('Command subscriber must return something.'); + } + + if (is_string($commands)) { + $commands = [$commands]; + } + + if (!is_array($commands)) { + throw new \LogicException('Command subscriber configuration is invalid. Should be an array or string.'); + } + + // 0.8 command subscriber + if (isset($commands['processorName'])) { + @trigger_error('The command subscriber 0.8 syntax is deprecated since Enqueue 0.9.', \E_USER_DEPRECATED); + + $source = $commands['processorName']; + $processor = $params['processorName'] ?? $serviceId; + + $options = $commands; + unset( + $options['processorName'], + $options['queueName'], + $options['queueNameHardcoded'], + $options['exclusive'], + $options['topic'], + $options['source'], + $options['source_type'], + $options['processor'], + $options['options'] + ); + + $options['processor_service_id'] = $serviceId; + + if (isset($commands['queueName'])) { + $options['queue'] = $commands['queueName']; + } + + if (isset($commands['queueNameHardcoded']) && $commands['queueNameHardcoded']) { + $options['prefix_queue'] = false; + } + + $routeCollection->add(new Route($source, Route::COMMAND, $processor, $options)); + + continue; + } + + if (isset($commands['command'])) { + $commands = [$commands]; + } + + foreach ($commands as $key => $params) { + if (is_string($params)) { + $routeCollection->add(new Route($params, Route::COMMAND, $serviceId, ['processor_service_id' => $serviceId])); + } elseif (is_array($params)) { + $source = $params['command'] ?? null; + $processor = $params['processor'] ?? $serviceId; + unset($params['command'], $params['source'], $params['source_type'], $params['processor'], $params['options']); + $options = $params; + $options['processor_service_id'] = $serviceId; + + $routeCollection->add(new Route($source, Route::COMMAND, $processor, $options)); + } else { + throw new \LogicException(sprintf('Command subscriber configuration is invalid for "%s::getSubscribedCommand()". "%s"', $processorClass, json_encode($processorClass::getSubscribedCommand()))); + } + } + } + } + + $rawRoutes = $routeCollection->toArray(); + + $routeCollectionService = $container->getDefinition($routeCollectionId); + $routeCollectionService->replaceArgument(0, array_merge( + $routeCollectionService->getArgument(0), + $rawRoutes + )); + } + } +} diff --git a/pkg/enqueue/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPass.php b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPass.php new file mode 100644 index 000000000..274847c90 --- /dev/null +++ b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPass.php @@ -0,0 +1,63 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $extensionsId = $diUtils->format('consumption_extensions'); + if (false == $container->hasDefinition($extensionsId)) { + throw new \LogicException(sprintf('Service "%s" not found', $extensionsId)); + } + + $tags = array_merge( + $container->findTaggedServiceIds('enqueue.consumption_extension'), + $container->findTaggedServiceIds('enqueue.consumption.extension') // TODO BC + ); + + $groupByPriority = []; + foreach ($tags as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + $priority = (int) ($tagAttribute['priority'] ?? 0); + + $groupByPriority[$priority][] = new Reference($serviceId); + } + } + + krsort($groupByPriority, \SORT_NUMERIC); + + $flatExtensions = []; + foreach ($groupByPriority as $extension) { + $flatExtensions = array_merge($flatExtensions, $extension); + } + + $extensionsService = $container->getDefinition($extensionsId); + $extensionsService->replaceArgument(0, array_merge( + $extensionsService->getArgument(0), + $flatExtensions + )); + } + } +} diff --git a/pkg/enqueue/Symfony/Client/DependencyInjection/BuildProcessorRegistryPass.php b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildProcessorRegistryPass.php new file mode 100644 index 000000000..3759dd209 --- /dev/null +++ b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildProcessorRegistryPass.php @@ -0,0 +1,57 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + + $processorRegistryId = $diUtils->format('processor_registry'); + if (false == $container->hasDefinition($processorRegistryId)) { + throw new \LogicException(sprintf('Service "%s" not found', $processorRegistryId)); + } + + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $routerProcessorId = $diUtils->format('router_processor'); + if (false == $container->hasDefinition($routerProcessorId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routerProcessorId)); + } + + $routeCollection = RouteCollection::fromArray($container->getDefinition($routeCollectionId)->getArgument(0)); + + $map = []; + foreach ($routeCollection->all() as $route) { + if (false == $processorServiceId = $route->getOption('processor_service_id')) { + throw new \LogicException('The route option "processor_service_id" is required'); + } + + $map[$route->getProcessor()] = new Reference($processorServiceId); + } + + $map[$diUtils->parameter('router_processor')] = new Reference($routerProcessorId); + + $registry = $container->getDefinition($processorRegistryId); + $registry->setArgument(0, ServiceLocatorTagPass::register($container, $map, $processorRegistryId)); + } + } +} diff --git a/pkg/enqueue/Symfony/Client/DependencyInjection/BuildProcessorRoutesPass.php b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildProcessorRoutesPass.php new file mode 100644 index 000000000..e88cb1f83 --- /dev/null +++ b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildProcessorRoutesPass.php @@ -0,0 +1,77 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $tag = 'enqueue.processor'; + $routeCollection = new RouteCollection([]); + foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + $topic = $tagAttribute['topic'] ?? null; + $command = $tagAttribute['command'] ?? null; + + if (false == $topic && false == $command) { + throw new \LogicException(sprintf('Either "topic" or "command" tag attribute must be set on service "%s". None is set.', $serviceId)); + } + if ($topic && $command) { + throw new \LogicException(sprintf('Either "topic" or "command" tag attribute must be set on service "%s". Both are set.', $serviceId)); + } + + $source = $command ?: $topic; + $sourceType = $command ? Route::COMMAND : Route::TOPIC; + $processor = $tagAttribute['processor'] ?? $serviceId; + + unset( + $tagAttribute['topic'], + $tagAttribute['command'], + $tagAttribute['source'], + $tagAttribute['source_type'], + $tagAttribute['processor'], + $tagAttribute['options'] + ); + $options = $tagAttribute; + $options['processor_service_id'] = $serviceId; + + $routeCollection->add(new Route($source, $sourceType, $processor, $options)); + } + } + + $rawRoutes = $routeCollection->toArray(); + + $routeCollectionService = $container->getDefinition($routeCollectionId); + $routeCollectionService->replaceArgument(0, array_merge( + $routeCollectionService->getArgument(0), + $rawRoutes + )); + } + } +} diff --git a/pkg/enqueue/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPass.php b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPass.php new file mode 100644 index 000000000..ef01e6fcf --- /dev/null +++ b/pkg/enqueue/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPass.php @@ -0,0 +1,127 @@ +hasParameter('enqueue.clients')) { + throw new \LogicException('The "enqueue.clients" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.clients'); + $defaultName = $container->getParameter('enqueue.default_client'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(ClientFactory::MODULE, $name); + $routeCollectionId = $diUtils->format('route_collection'); + if (false == $container->hasDefinition($routeCollectionId)) { + throw new \LogicException(sprintf('Service "%s" not found', $routeCollectionId)); + } + + $tag = 'enqueue.topic_subscriber'; + $routeCollection = new RouteCollection([]); + foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) { + $processorDefinition = $container->getDefinition($serviceId); + if ($processorDefinition->getFactory()) { + throw new \LogicException('The topic subscriber tag could not be applied to a service created by factory.'); + } + + $processorClass = $processorDefinition->getClass() ?? $serviceId; + if (false == class_exists($processorClass)) { + throw new \LogicException(sprintf('The processor class "%s" could not be found.', $processorClass)); + } + + if (false == is_subclass_of($processorClass, TopicSubscriberInterface::class)) { + throw new \LogicException(sprintf('The processor must implement "%s" interface to be used with the tag "%s"', TopicSubscriberInterface::class, $tag)); + } + + foreach ($tagAttributes as $tagAttribute) { + $client = $tagAttribute['client'] ?? $defaultName; + + if ($client !== $name && 'all' !== $client) { + continue; + } + + /** @var TopicSubscriberInterface $processorClass */ + $topics = $processorClass::getSubscribedTopics(); + + if (empty($topics)) { + throw new \LogicException('Topic subscriber must return something.'); + } + + if (is_string($topics)) { + $topics = [$topics]; + } + + if (!is_array($topics)) { + throw new \LogicException('Topic subscriber configuration is invalid. Should be an array or string.'); + } + + foreach ($topics as $key => $params) { + if (is_string($params)) { + $routeCollection->add(new Route($params, Route::TOPIC, $serviceId, ['processor_service_id' => $serviceId])); + + // 0.8 topic subscriber + } elseif (is_array($params) && is_string($key)) { + @trigger_error('The topic subscriber 0.8 syntax is deprecated since Enqueue 0.9.', \E_USER_DEPRECATED); + + $source = $key; + $processor = $params['processorName'] ?? $serviceId; + + $options = $params; + unset( + $options['processorName'], + $options['queueName'], + $options['queueNameHardcoded'], + $options['topic'], + $options['source'], + $options['source_type'], + $options['processor'], + $options['options'] + ); + + $options['processor_service_id'] = $serviceId; + + if (isset($params['queueName'])) { + $options['queue'] = $params['queueName']; + } + + if (isset($params['queueNameHardcoded']) && $params['queueNameHardcoded']) { + $options['prefix_queue'] = false; + } + + $routeCollection->add(new Route($source, Route::TOPIC, $processor, $options)); + } elseif (is_array($params)) { + $source = $params['topic'] ?? null; + $processor = $params['processor'] ?? $serviceId; + unset($params['topic'], $params['source'], $params['source_type'], $params['processor'], $params['options']); + $options = $params; + $options['processor_service_id'] = $serviceId; + + $routeCollection->add(new Route($source, Route::TOPIC, $processor, $options)); + } else { + throw new \LogicException(sprintf('Topic subscriber configuration is invalid for "%s::getSubscribedTopics()". Got "%s"', $processorClass, json_encode($processorClass::getSubscribedTopics()))); + } + } + } + } + + $rawRoutes = $routeCollection->toArray(); + + $routeCollectionService = $container->getDefinition($routeCollectionId); + $routeCollectionService->replaceArgument(0, array_merge( + $routeCollectionService->getArgument(0), + $rawRoutes + )); + } + } +} diff --git a/pkg/enqueue/Symfony/Client/DependencyInjection/ClientFactory.php b/pkg/enqueue/Symfony/Client/DependencyInjection/ClientFactory.php new file mode 100644 index 000000000..be020dcff --- /dev/null +++ b/pkg/enqueue/Symfony/Client/DependencyInjection/ClientFactory.php @@ -0,0 +1,254 @@ +default = $default; + $this->diUtils = DiUtils::create(self::MODULE, $name); + } + + public static function getConfiguration(bool $debug, string $name = 'client'): NodeDefinition + { + $builder = new ArrayNodeDefinition($name); + + $builder->children() + ->booleanNode('traceable_producer')->defaultValue($debug)->end() + ->scalarNode('prefix')->defaultValue('enqueue')->end() + ->scalarNode('separator')->defaultValue('.')->end() + ->scalarNode('app_name')->defaultValue('app')->end() + ->scalarNode('router_topic')->defaultValue('default')->cannotBeEmpty()->end() + ->scalarNode('router_queue')->defaultValue('default')->cannotBeEmpty()->end() + ->scalarNode('router_processor')->defaultNull()->end() + ->integerNode('redelivered_delay_time')->min(0)->defaultValue(0)->end() + ->scalarNode('default_queue')->defaultValue('default')->cannotBeEmpty()->end() + ->arrayNode('driver_options')->addDefaultsIfNotSet()->info('The array contains driver specific options')->ignoreExtraKeys(false)->end() + ->end() + ; + + return $builder; + } + + public function build(ContainerBuilder $container, array $config): void + { + $container->register($this->diUtils->format('context'), Context::class) + ->setFactory([$this->diUtils->reference('driver'), 'getContext']) + ; + + $container->register($this->diUtils->format('driver_factory'), DriverFactory::class); + + $routerProcessor = empty($config['router_processor']) + ? $this->diUtils->format('router_processor') + : $config['router_processor'] + ; + + $container->register($this->diUtils->format('config'), Config::class) + ->setArguments([ + $config['prefix'], + $config['separator'], + $config['app_name'], + $config['router_topic'], + $config['router_queue'], + $config['default_queue'], + $routerProcessor, + $config['transport'], + $config['driver_options'] ?? [], + ]); + + $container->setParameter($this->diUtils->format('router_processor'), $routerProcessor); + $container->setParameter($this->diUtils->format('router_queue_name'), $config['router_queue']); + $container->setParameter($this->diUtils->format('default_queue_name'), $config['default_queue']); + + $container->register($this->diUtils->format('route_collection'), RouteCollection::class) + ->addArgument([]) + ->setFactory([RouteCollection::class, 'fromArray']) + ; + + $container->register($this->diUtils->format('producer'), Producer::class) + // @deprecated + ->setPublic(true) + ->addArgument($this->diUtils->reference('driver')) + ->addArgument($this->diUtils->reference('rpc_factory')) + ->addArgument($this->diUtils->reference('client_extensions')) + ; + + $lazyProducer = $container->register($this->diUtils->format('lazy_producer'), LazyProducer::class); + $lazyProducer->addArgument(ServiceLocatorTagPass::register($container, [ + $this->diUtils->format('producer') => new Reference($this->diUtils->format('producer')), + ])); + $lazyProducer->addArgument($this->diUtils->format('producer')); + + $container->register($this->diUtils->format('spool_producer'), SpoolProducer::class) + ->addArgument($this->diUtils->reference('lazy_producer')) + ; + + $container->register($this->diUtils->format('client_extensions'), ChainExtension::class) + ->addArgument([]) + ; + + $container->register($this->diUtils->format('rpc_factory'), RpcFactory::class) + ->addArgument($this->diUtils->reference('context')) + ; + + $container->register($this->diUtils->format('router_processor'), RouterProcessor::class) + ->addArgument($this->diUtils->reference('driver')) + ; + + $container->register($this->diUtils->format('processor_registry'), ContainerProcessorRegistry::class); + + $container->register($this->diUtils->format('delegate_processor'), DelegateProcessor::class) + ->addArgument($this->diUtils->reference('processor_registry')) + ; + + $container->register($this->diUtils->format('set_router_properties_extension'), SetRouterPropertiesExtension::class) + ->addArgument($this->diUtils->reference('driver')) + ->addTag('enqueue.consumption_extension', ['priority' => 100, 'client' => $this->diUtils->getConfigName()]) + ; + + $container->register($this->diUtils->format('queue_consumer'), QueueConsumer::class) + ->addArgument($this->diUtils->reference('context')) + ->addArgument($this->diUtils->reference('consumption_extensions')) + ->addArgument([]) + ->addArgument(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->addArgument($config['consumption']['receive_timeout']) + ; + + $container->register($this->diUtils->format('consumption_extensions'), ConsumptionChainExtension::class) + ->addArgument([]) + ; + + $container->register($this->diUtils->format('flush_spool_producer_extension'), FlushSpoolProducerExtension::class) + ->addArgument($this->diUtils->reference('spool_producer')) + ->addTag('enqueue.consumption.extension', ['priority' => -100, 'client' => $this->diUtils->getConfigName()]) + ; + + $container->register($this->diUtils->format('exclusive_command_extension'), ExclusiveCommandExtension::class) + ->addArgument($this->diUtils->reference('driver')) + ->addTag('enqueue.consumption.extension', ['priority' => 100, 'client' => $this->diUtils->getConfigName()]) + ; + + if ($config['traceable_producer']) { + $container->register($this->diUtils->format('traceable_producer'), TraceableProducer::class) + ->setDecoratedService($this->diUtils->format('producer')) + ->addArgument($this->diUtils->reference('traceable_producer.inner')) + ; + } + + if ($config['redelivered_delay_time']) { + $container->register($this->diUtils->format('delay_redelivered_message_extension'), DelayRedeliveredMessageExtension::class) + ->addArgument($this->diUtils->reference('driver')) + ->addArgument($config['redelivered_delay_time']) + ->addTag('enqueue.consumption_extension', ['priority' => 10, 'client' => $this->diUtils->getConfigName()]) + ; + + $container->getDefinition($this->diUtils->format('delay_redelivered_message_extension')) + ->replaceArgument(1, $config['redelivered_delay_time']) + ; + } + + $locatorId = 'enqueue.locator'; + if ($container->hasDefinition($locatorId)) { + $locator = $container->getDefinition($locatorId); + $locator->replaceArgument(0, array_replace($locator->getArgument(0), [ + $this->diUtils->format('queue_consumer') => $this->diUtils->reference('queue_consumer'), + $this->diUtils->format('driver') => $this->diUtils->reference('driver'), + $this->diUtils->format('delegate_processor') => $this->diUtils->reference('delegate_processor'), + $this->diUtils->format('producer') => $this->diUtils->reference('lazy_producer'), + ])); + } + + if ($this->default) { + $container->setAlias(ProducerInterface::class, $this->diUtils->format('lazy_producer')); + $container->setAlias(SpoolProducer::class, $this->diUtils->format('spool_producer')); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('producer'), $this->diUtils->format('producer')); + $container->setAlias($this->diUtils->formatDefault('spool_producer'), $this->diUtils->format('spool_producer')); + } + } + } + + public function createDriver(ContainerBuilder $container, array $config): string + { + $factoryId = DiUtils::create(TransportFactory::MODULE, $this->diUtils->getConfigName())->format('connection_factory'); + $driverId = $this->diUtils->format('driver'); + $driverFactoryId = $this->diUtils->format('driver_factory'); + + $container->register($driverId, DriverInterface::class) + ->setFactory([new Reference($driverFactoryId), 'create']) + ->addArgument(new Reference($factoryId)) + ->addArgument($this->diUtils->reference('config')) + ->addArgument($this->diUtils->reference('route_collection')) + ; + + if ($this->default) { + $container->setAlias(DriverInterface::class, $driverId); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('driver'), $driverId); + } + } + + return $driverId; + } + + public function createFlushSpoolProducerListener(ContainerBuilder $container): void + { + $container->register($this->diUtils->format('flush_spool_producer_listener'), FlushSpoolProducerListener::class) + ->addArgument($this->diUtils->reference('spool_producer')) + ->addTag('kernel.event_subscriber') + ; + } +} diff --git a/pkg/enqueue/Symfony/Client/FlushSpoolProducerListener.php b/pkg/enqueue/Symfony/Client/FlushSpoolProducerListener.php index 1f5fdcba7..00543f6de 100644 --- a/pkg/enqueue/Symfony/Client/FlushSpoolProducerListener.php +++ b/pkg/enqueue/Symfony/Client/FlushSpoolProducerListener.php @@ -14,9 +14,6 @@ class FlushSpoolProducerListener implements EventSubscriberInterface */ private $producer; - /** - * @param SpoolProducer $producer - */ public function __construct(SpoolProducer $producer) { $this->producer = $producer; @@ -27,10 +24,7 @@ public function flushMessages() $this->producer->flush(); } - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { $events = []; diff --git a/pkg/enqueue/Symfony/Client/LazyProducer.php b/pkg/enqueue/Symfony/Client/LazyProducer.php new file mode 100644 index 000000000..8dd3aadba --- /dev/null +++ b/pkg/enqueue/Symfony/Client/LazyProducer.php @@ -0,0 +1,37 @@ +container = $container; + $this->producerId = $producerId; + } + + public function sendEvent(string $topic, $message): void + { + $this->getRealProducer()->sendEvent($topic, $message); + } + + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise + { + return $this->getRealProducer()->sendCommand($command, $message, $needReply); + } + + private function getRealProducer(): ProducerInterface + { + return $this->container->get($this->producerId); + } +} diff --git a/pkg/enqueue/Symfony/Client/Meta/QueuesCommand.php b/pkg/enqueue/Symfony/Client/Meta/QueuesCommand.php deleted file mode 100644 index a1f9f54fe..000000000 --- a/pkg/enqueue/Symfony/Client/Meta/QueuesCommand.php +++ /dev/null @@ -1,73 +0,0 @@ -queueMetaRegistry = $queueRegistry; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setName('enqueue:queues') - ->setAliases([ - 'enq:m:q', - 'debug:enqueue:queues', - ]) - ->setDescription('A command shows all available queues and some information about them.') - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $table = new Table($output); - $table->setHeaders(['Client Name', 'Transport Name', 'Processors']); - - $count = 0; - $firstRow = true; - foreach ($this->queueMetaRegistry->getQueuesMeta() as $queueMeta) { - if (false == $firstRow) { - $table->addRow(new TableSeparator()); - } - - $table->addRow([ - $queueMeta->getClientName(), - $queueMeta->getClientName() == $queueMeta->getTransportName() ? '(same)' : $queueMeta->getTransportName(), - implode(PHP_EOL, $queueMeta->getProcessors()), - ]); - - ++$count; - $firstRow = false; - } - - $output->writeln(sprintf('Found %s destinations', $count)); - $output->writeln(''); - $table->render(); - } -} diff --git a/pkg/enqueue/Symfony/Client/Meta/TopicsCommand.php b/pkg/enqueue/Symfony/Client/Meta/TopicsCommand.php deleted file mode 100644 index 51019a7d6..000000000 --- a/pkg/enqueue/Symfony/Client/Meta/TopicsCommand.php +++ /dev/null @@ -1,69 +0,0 @@ -topicRegistry = $topicRegistry; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setName('enqueue:topics') - ->setAliases([ - 'enq:m:t', - 'debug:enqueue:topics', - ]) - ->setDescription('A command shows all available topics and some information about them.') - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $table = new Table($output); - $table->setHeaders(['Topic', 'Description', 'Processors']); - - $count = 0; - $firstRow = true; - foreach ($this->topicRegistry->getTopicsMeta() as $topic) { - if (false == $firstRow) { - $table->addRow(new TableSeparator()); - } - - $table->addRow([$topic->getName(), $topic->getDescription(), implode(PHP_EOL, $topic->getProcessors())]); - - ++$count; - $firstRow = false; - } - - $output->writeln(sprintf('Found %s topics', $count)); - $output->writeln(''); - $table->render(); - } -} diff --git a/pkg/enqueue/Symfony/Client/ProduceCommand.php b/pkg/enqueue/Symfony/Client/ProduceCommand.php new file mode 100644 index 000000000..bd23c16c3 --- /dev/null +++ b/pkg/enqueue/Symfony/Client/ProduceCommand.php @@ -0,0 +1,92 @@ +container = $container; + $this->defaultClient = $defaultClient; + $this->producerIdPattern = $producerIdPattern; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDescription('Sends an event to the topic') + ->addArgument('message', InputArgument::REQUIRED, 'A message') + ->addOption('header', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The message headers') + ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient) + ->addOption('topic', null, InputOption::VALUE_OPTIONAL, 'The topic to send a message to') + ->addOption('command', null, InputOption::VALUE_OPTIONAL, 'The command to send a message to') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $topic = $input->getOption('topic'); + $command = $input->getOption('command'); + $message = $input->getArgument('message'); + $headers = (array) $input->getOption('header'); + $client = $input->getOption('client'); + + if ($topic && $command) { + throw new \LogicException('Either topic or command option should be set, both are set.'); + } + + try { + $producer = $this->getProducer($client); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Client "%s" is not supported.', $client), null, $e); + } + + if ($topic) { + $producer->sendEvent($topic, new Message($message, [], $headers)); + + $output->writeln('An event is sent'); + } elseif ($command) { + $producer->sendCommand($command, $message); + + $output->writeln('A command is sent'); + } else { + throw new \LogicException('Either topic or command option should be set, none is set.'); + } + + return 0; + } + + private function getProducer(string $client): ProducerInterface + { + return $this->container->get(sprintf($this->producerIdPattern, $client)); + } +} diff --git a/pkg/enqueue/Symfony/Client/ProduceMessageCommand.php b/pkg/enqueue/Symfony/Client/ProduceMessageCommand.php deleted file mode 100644 index a9f9ef90a..000000000 --- a/pkg/enqueue/Symfony/Client/ProduceMessageCommand.php +++ /dev/null @@ -1,54 +0,0 @@ -producer = $producer; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this - ->setName('enqueue:produce') - ->setAliases(['enq:p']) - ->setDescription('A command to send a message to topic') - ->addArgument('topic', InputArgument::REQUIRED, 'A topic to send message to') - ->addArgument('message', InputArgument::REQUIRED, 'A message to send') - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->producer->sendEvent( - $input->getArgument('topic'), - $input->getArgument('message') - ); - - $output->writeln('Message is sent'); - } -} diff --git a/pkg/enqueue/Symfony/Client/RoutesCommand.php b/pkg/enqueue/Symfony/Client/RoutesCommand.php new file mode 100644 index 000000000..0646e37bb --- /dev/null +++ b/pkg/enqueue/Symfony/Client/RoutesCommand.php @@ -0,0 +1,162 @@ +container = $container; + $this->defaultClient = $defaultClient; + $this->driverIdPatter = $driverIdPatter; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setAliases(['debug:enqueue:routes']) + ->setDescription('A command lists all registered routes.') + ->addOption('show-route-options', null, InputOption::VALUE_NONE, 'Adds ability to hide options.') + ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient) + ; + + $this->driver = null; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + $this->driver = $this->getDriver($input->getOption('client')); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Client "%s" is not supported.', $input->getOption('client')), null, $e); + } + + $routes = $this->driver->getRouteCollection()->all(); + $output->writeln(sprintf('Found %s routes', count($routes))); + $output->writeln(''); + + if ($routes) { + $table = new Table($output); + $table->setHeaders(['Type', 'Source', 'Queue', 'Processor', 'Options']); + + $firstRow = true; + foreach ($routes as $route) { + if (false == $firstRow) { + $table->addRow(new TableSeparator()); + + $firstRow = false; + } + + if ($route->isCommand()) { + continue; + } + + $table->addRow([ + $this->formatSourceType($route), + $route->getSource(), + $this->formatQueue($route), + $this->formatProcessor($route), + $input->getOption('show-route-options') ? $this->formatOptions($route) : '(hidden)', + ]); + } + + foreach ($routes as $route) { + if ($route->isTopic()) { + continue; + } + + $table->addRow([ + $this->formatSourceType($route), + $route->getSource(), + $this->formatQueue($route), + $this->formatProcessor($route), + $input->getOption('show-route-options') ? $this->formatOptions($route) : '(hidden)', + ]); + } + + $table->render(); + } + + return 0; + } + + private function formatSourceType(Route $route): string + { + if ($route->isCommand()) { + return 'command'; + } + + if ($route->isTopic()) { + return 'topic'; + } + + return 'unknown'; + } + + private function formatProcessor(Route $route): string + { + if ($route->isProcessorExternal()) { + return 'n\a (external)'; + } + + $processor = $route->getProcessor(); + + return $route->isProcessorExclusive() ? $processor.' (exclusive)' : $processor; + } + + private function formatQueue(Route $route): string + { + $queue = $route->getQueue() ?: $this->driver->getConfig()->getDefaultQueue(); + + return $route->isPrefixQueue() ? $queue.' (prefixed)' : $queue.' (as is)'; + } + + private function formatOptions(Route $route): string + { + return var_export($route->getOptions(), true); + } + + private function getDriver(string $client): DriverInterface + { + return $this->container->get(sprintf($this->driverIdPatter, $client)); + } +} + +function enqueue() +{ +} diff --git a/pkg/enqueue/Symfony/Client/SetupBrokerCommand.php b/pkg/enqueue/Symfony/Client/SetupBrokerCommand.php index c825bd2c4..b74902aa6 100644 --- a/pkg/enqueue/Symfony/Client/SetupBrokerCommand.php +++ b/pkg/enqueue/Symfony/Client/SetupBrokerCommand.php @@ -3,47 +3,68 @@ namespace Enqueue\Symfony\Client; use Enqueue\Client\DriverInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Logger\ConsoleLogger; use Symfony\Component\Console\Output\OutputInterface; +#[AsCommand('enqueue:setup-broker')] class SetupBrokerCommand extends Command { /** - * @var DriverInterface + * @var ContainerInterface */ - private $driver; + private $container; /** - * @param DriverInterface $driver + * @var string */ - public function __construct(DriverInterface $driver) + private $defaultClient; + + /** + * @var string + */ + private $driverIdPattern; + + public function __construct(ContainerInterface $container, string $defaultClient, string $driverIdPattern = 'enqueue.client.%s.driver') { - parent::__construct(null); + $this->container = $container; + $this->defaultClient = $defaultClient; + $this->driverIdPattern = $driverIdPattern; - $this->driver = $driver; + parent::__construct(); } - /** - * {@inheritdoc} - */ - protected function configure() + protected function configure(): void { $this - ->setName('enqueue:setup-broker') ->setAliases(['enq:sb']) - ->setDescription('Creates all required queues') + ->setDescription('Setup broker. Configure the broker, creates queues, topics and so on.') + ->addOption('client', 'c', InputOption::VALUE_OPTIONAL, 'The client to consume messages from.', $this->defaultClient) ; } - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln('Setup Broker'); + $client = $input->getOption('client'); + + try { + $this->getDriver($client)->setupBroker(new ConsoleLogger($output)); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Client "%s" is not supported.', $client), null, $e); + } + + $output->writeln('Broker set up'); - $this->driver->setupBroker(new ConsoleLogger($output)); + return 0; + } + + private function getDriver(string $client): DriverInterface + { + return $this->container->get(sprintf($this->driverIdPattern, $client)); } } diff --git a/pkg/enqueue/Symfony/Client/SetupBrokerExtensionCommandTrait.php b/pkg/enqueue/Symfony/Client/SetupBrokerExtensionCommandTrait.php index 2888f5636..bcc4f7bb2 100644 --- a/pkg/enqueue/Symfony/Client/SetupBrokerExtensionCommandTrait.php +++ b/pkg/enqueue/Symfony/Client/SetupBrokerExtensionCommandTrait.php @@ -10,9 +10,6 @@ trait SetupBrokerExtensionCommandTrait { - /** - * {@inheritdoc} - */ protected function configureSetupBrokerExtension() { $this @@ -21,10 +18,7 @@ protected function configureSetupBrokerExtension() } /** - * @param InputInterface $input - * @param DriverInterface $driver - * - * @return ExtensionInterface + * @return ExtensionInterface|null */ protected function getSetupBrokerExtension(InputInterface $input, DriverInterface $driver) { diff --git a/pkg/enqueue/Symfony/Client/SimpleConsumeCommand.php b/pkg/enqueue/Symfony/Client/SimpleConsumeCommand.php new file mode 100644 index 000000000..fafc35d05 --- /dev/null +++ b/pkg/enqueue/Symfony/Client/SimpleConsumeCommand.php @@ -0,0 +1,26 @@ + $queueConsumer, + 'driver' => $driver, + 'processor' => $processor, + ]), + 'default', + 'queue_consumer', + 'driver', + 'processor' + ); + } +} diff --git a/pkg/enqueue/Symfony/Client/SimpleProduceCommand.php b/pkg/enqueue/Symfony/Client/SimpleProduceCommand.php new file mode 100644 index 000000000..5d7f76533 --- /dev/null +++ b/pkg/enqueue/Symfony/Client/SimpleProduceCommand.php @@ -0,0 +1,18 @@ + $producer]), + 'default', + 'producer' + ); + } +} diff --git a/pkg/enqueue/Symfony/Client/SimpleRoutesCommand.php b/pkg/enqueue/Symfony/Client/SimpleRoutesCommand.php new file mode 100644 index 000000000..0023f14ae --- /dev/null +++ b/pkg/enqueue/Symfony/Client/SimpleRoutesCommand.php @@ -0,0 +1,18 @@ + $driver]), + 'default', + 'driver' + ); + } +} diff --git a/pkg/enqueue/Symfony/Client/SimpleSetupBrokerCommand.php b/pkg/enqueue/Symfony/Client/SimpleSetupBrokerCommand.php new file mode 100644 index 000000000..aae19f84b --- /dev/null +++ b/pkg/enqueue/Symfony/Client/SimpleSetupBrokerCommand.php @@ -0,0 +1,18 @@ + $driver]), + 'default', + 'driver' + ); + } +} diff --git a/pkg/enqueue/Symfony/Consumption/ChooseLoggerCommandTrait.php b/pkg/enqueue/Symfony/Consumption/ChooseLoggerCommandTrait.php new file mode 100644 index 000000000..c229c14b0 --- /dev/null +++ b/pkg/enqueue/Symfony/Consumption/ChooseLoggerCommandTrait.php @@ -0,0 +1,35 @@ +addOption('logger', null, InputOption::VALUE_OPTIONAL, 'A logger to be used. Could be "default", "null", "stdout".', 'default') + ; + } + + protected function getLoggerExtension(InputInterface $input, OutputInterface $output): ?LoggerExtension + { + $logger = $input->getOption('logger'); + switch ($logger) { + case 'null': + return new LoggerExtension(new NullLogger()); + case 'stdout': + return new LoggerExtension(new ConsoleLogger($output)); + case 'default': + return null; + default: + throw new \LogicException(sprintf('The logger "%s" is not supported', $logger)); + } + } +} diff --git a/pkg/enqueue/Symfony/Consumption/ConfigurableConsumeCommand.php b/pkg/enqueue/Symfony/Consumption/ConfigurableConsumeCommand.php new file mode 100644 index 000000000..230a0dc69 --- /dev/null +++ b/pkg/enqueue/Symfony/Consumption/ConfigurableConsumeCommand.php @@ -0,0 +1,122 @@ +container = $container; + $this->defaultTransport = $defaultTransport; + $this->queueConsumerIdPattern = $queueConsumerIdPattern; + $this->processorRegistryIdPattern = $processorRegistryIdPattern; + + parent::__construct(); + } + + protected function configure(): void + { + $this->configureLimitsExtensions(); + $this->configureQueueConsumerOptions(); + $this->configureLoggerExtension(); + + $this + ->setDescription('A worker that consumes message from a broker. '. + 'To use this broker you have to explicitly set a queue to consume from '. + 'and a message processor service') + ->addArgument('processor', InputArgument::REQUIRED, 'A message processor.') + ->addArgument('queues', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'A queue to consume from', []) + ->addOption('transport', 't', InputOption::VALUE_OPTIONAL, 'The transport to consume messages from.', $this->defaultTransport) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $transport = $input->getOption('transport'); + + try { + $consumer = $this->getQueueConsumer($transport); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Transport "%s" is not supported.', $transport), null, $e); + } + + $this->setQueueConsumerOptions($consumer, $input); + + $processor = $this->getProcessorRegistry($transport)->get($input->getArgument('processor')); + + $queues = $input->getArgument('queues'); + if (empty($queues) && $processor instanceof QueueSubscriberInterface) { + $queues = $processor::getSubscribedQueues(); + } + + if (empty($queues)) { + throw new \LogicException(sprintf('The queue is not provided. The processor must implement "%s" interface and it must return not empty array of queues or a queue set using as a second argument.', QueueSubscriberInterface::class)); + } + + $extensions = $this->getLimitsExtensions($input, $output); + + if ($loggerExtension = $this->getLoggerExtension($input, $output)) { + array_unshift($extensions, $loggerExtension); + } + + foreach ($queues as $queue) { + $consumer->bind($queue, $processor); + } + + $consumer->consume(new ChainExtension($extensions)); + + return 0; + } + + private function getQueueConsumer(string $name): QueueConsumerInterface + { + return $this->container->get(sprintf($this->queueConsumerIdPattern, $name)); + } + + private function getProcessorRegistry(string $name): ProcessorRegistryInterface + { + return $this->container->get(sprintf($this->processorRegistryIdPattern, $name)); + } +} diff --git a/pkg/enqueue/Symfony/Consumption/ConsumeCommand.php b/pkg/enqueue/Symfony/Consumption/ConsumeCommand.php new file mode 100644 index 000000000..1c82f3cfb --- /dev/null +++ b/pkg/enqueue/Symfony/Consumption/ConsumeCommand.php @@ -0,0 +1,91 @@ +container = $container; + $this->defaultTransport = $defaultTransport; + $this->queueConsumerIdPattern = $queueConsumerIdPattern; + + parent::__construct(); + } + + protected function configure(): void + { + $this->configureLimitsExtensions(); + $this->configureQueueConsumerOptions(); + $this->configureLoggerExtension(); + + $this + ->addOption('transport', 't', InputOption::VALUE_OPTIONAL, 'The transport to consume messages from.', $this->defaultTransport) + ->setDescription('A worker that consumes message from a broker. '. + 'To use this broker you have to configure queue consumer before adding to the command') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $transport = $input->getOption('transport'); + + try { + // QueueConsumer must be pre configured outside of the command! + $consumer = $this->getQueueConsumer($transport); + } catch (NotFoundExceptionInterface $e) { + throw new \LogicException(sprintf('Transport "%s" is not supported.', $transport), null, $e); + } + + $this->setQueueConsumerOptions($consumer, $input); + + $extensions = $this->getLimitsExtensions($input, $output); + + if ($loggerExtension = $this->getLoggerExtension($input, $output)) { + array_unshift($extensions, $loggerExtension); + } + + $exitStatusExtension = new ExitStatusExtension(); + array_unshift($extensions, $exitStatusExtension); + + $consumer->consume(new ChainExtension($extensions)); + + return $exitStatusExtension->getExitStatus() ?? 0; + } + + private function getQueueConsumer(string $name): QueueConsumerInterface + { + return $this->container->get(sprintf($this->queueConsumerIdPattern, $name)); + } +} diff --git a/pkg/enqueue/Symfony/Consumption/ConsumeMessagesCommand.php b/pkg/enqueue/Symfony/Consumption/ConsumeMessagesCommand.php deleted file mode 100644 index c2c7695f1..000000000 --- a/pkg/enqueue/Symfony/Consumption/ConsumeMessagesCommand.php +++ /dev/null @@ -1,65 +0,0 @@ -consumer = $consumer; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->configureLimitsExtensions(); - $this->configureQueueConsumerOptions(); - - $this - ->setName('enqueue:transport:consume') - ->setDescription('A worker that consumes message from a broker. '. - 'To use this broker you have to configure queue consumer before adding to the command') - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->setQueueConsumerOptions($this->consumer, $input); - - $extensions = $this->getLimitsExtensions($input, $output); - array_unshift($extensions, new LoggerExtension(new ConsoleLogger($output))); - - $runtimeExtensions = new ChainExtension($extensions); - - $this->consumer->consume($runtimeExtensions); - } -} diff --git a/pkg/enqueue/Symfony/Consumption/ContainerAwareConsumeMessagesCommand.php b/pkg/enqueue/Symfony/Consumption/ContainerAwareConsumeMessagesCommand.php deleted file mode 100644 index 297dfa58d..000000000 --- a/pkg/enqueue/Symfony/Consumption/ContainerAwareConsumeMessagesCommand.php +++ /dev/null @@ -1,100 +0,0 @@ -consumer = $consumer; - } - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->configureLimitsExtensions(); - $this->configureQueueConsumerOptions(); - - $this - ->setName('enqueue:transport:consume') - ->setDescription('A worker that consumes message from a broker. '. - 'To use this broker you have to explicitly set a queue to consume from '. - 'and a message processor service') - ->addArgument('processor-service', InputArgument::REQUIRED, 'A message processor service') - ->addOption('queue', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Queues to consume from', []) - ; - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->setQueueConsumerOptions($this->consumer, $input); - - /** @var PsrProcessor $processor */ - $processor = $this->container->get($input->getArgument('processor-service')); - if (false == $processor instanceof PsrProcessor) { - throw new \LogicException(sprintf( - 'Invalid message processor service given. It must be an instance of %s but %s', - PsrProcessor::class, - get_class($processor) - )); - } - - $queues = $input->getOption('queue'); - if (empty($queues) && $processor instanceof QueueSubscriberInterface) { - $queues = $processor::getSubscribedQueues(); - } - - if (empty($queues)) { - throw new \LogicException(sprintf( - 'The queues are not provided. The processor must implement "%s" interface and it must return not empty array of queues or queues set using --queue option.', - QueueSubscriberInterface::class - )); - } - - $extensions = $this->getLimitsExtensions($input, $output); - array_unshift($extensions, new LoggerExtension(new ConsoleLogger($output))); - - $runtimeExtensions = new ChainExtension($extensions); - - foreach ($queues as $queue) { - $this->consumer->bind($queue, $processor); - } - - $this->consumer->consume($runtimeExtensions); - } -} diff --git a/pkg/enqueue/Symfony/Consumption/LimitsExtensionsCommandTrait.php b/pkg/enqueue/Symfony/Consumption/LimitsExtensionsCommandTrait.php index 2a756180c..d8351acc5 100644 --- a/pkg/enqueue/Symfony/Consumption/LimitsExtensionsCommandTrait.php +++ b/pkg/enqueue/Symfony/Consumption/LimitsExtensionsCommandTrait.php @@ -13,9 +13,6 @@ trait LimitsExtensionsCommandTrait { - /** - * {@inheritdoc} - */ protected function configureLimitsExtensions() { $this @@ -26,9 +23,6 @@ protected function configureLimitsExtensions() } /** - * @param InputInterface $input - * @param OutputInterface $output - * * @throws \Exception * * @return ExtensionInterface[] @@ -61,8 +55,8 @@ protected function getLimitsExtensions(InputInterface $input, OutputInterface $o } $niceness = $input->getOption('niceness'); - if ($niceness) { - $extensions[] = new NicenessExtension($niceness); + if (!empty($niceness) && is_numeric($niceness)) { + $extensions[] = new NicenessExtension((int) $niceness); } return $extensions; diff --git a/pkg/enqueue/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php b/pkg/enqueue/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php index c6ffd985f..fd736f226 100644 --- a/pkg/enqueue/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php +++ b/pkg/enqueue/Symfony/Consumption/QueueConsumerOptionsCommandTrait.php @@ -2,33 +2,21 @@ namespace Enqueue\Symfony\Consumption; -use Enqueue\Consumption\QueueConsumer; +use Enqueue\Consumption\QueueConsumerInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; trait QueueConsumerOptionsCommandTrait { - /** - * {@inheritdoc} - */ protected function configureQueueConsumerOptions() { $this - ->addOption('idle-timeout', null, InputOption::VALUE_REQUIRED, 'The time in milliseconds queue consumer idle if no message has been received.') ->addOption('receive-timeout', null, InputOption::VALUE_REQUIRED, 'The time in milliseconds queue consumer waits for a message.') ; } - /** - * @param QueueConsumer $consumer - * @param InputInterface $input - */ - protected function setQueueConsumerOptions(QueueConsumer $consumer, InputInterface $input) + protected function setQueueConsumerOptions(QueueConsumerInterface $consumer, InputInterface $input) { - if (null !== $idleTimeout = $input->getOption('idle-timeout')) { - $consumer->setIdleTimeout((int) $idleTimeout); - } - if (null !== $receiveTimeout = $input->getOption('receive-timeout')) { $consumer->setReceiveTimeout((int) $receiveTimeout); } diff --git a/pkg/enqueue/Symfony/Consumption/SimpleConsumeCommand.php b/pkg/enqueue/Symfony/Consumption/SimpleConsumeCommand.php new file mode 100644 index 000000000..90d0e362e --- /dev/null +++ b/pkg/enqueue/Symfony/Consumption/SimpleConsumeCommand.php @@ -0,0 +1,18 @@ + $consumer]), + 'default', + 'queue_consumer' + ); + } +} diff --git a/pkg/enqueue/Symfony/ContainerProcessorRegistry.php b/pkg/enqueue/Symfony/ContainerProcessorRegistry.php new file mode 100644 index 000000000..b259d2379 --- /dev/null +++ b/pkg/enqueue/Symfony/ContainerProcessorRegistry.php @@ -0,0 +1,29 @@ +locator = $locator; + } + + public function get(string $processorName): Processor + { + if (false == $this->locator->has($processorName)) { + throw new \LogicException(sprintf('Service locator does not have a processor with name "%s".', $processorName)); + } + + return $this->locator->get($processorName); + } +} diff --git a/pkg/enqueue/Symfony/DefaultTransportFactory.php b/pkg/enqueue/Symfony/DefaultTransportFactory.php deleted file mode 100644 index fa45d2544..000000000 --- a/pkg/enqueue/Symfony/DefaultTransportFactory.php +++ /dev/null @@ -1,230 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->always(function ($v) { - if (is_array($v)) { - if (empty($v['dsn']) && empty($v['alias'])) { - throw new \LogicException('Either dsn or alias option must be set'); - } - - return $v; - } - - if (empty($v)) { - return ['dsn' => 'null:']; - } - - if (is_string($v)) { - return false !== strpos($v, ':') || false !== strpos($v, 'env_') ? - ['dsn' => $v] : - ['alias' => $v]; - } - }) - ->end() - ->children() - ->scalarNode('alias')->cannotBeEmpty()->end() - ->scalarNode('dsn')->cannotBeEmpty()->end() - ->end() - ->end() - ; - } - - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - if (isset($config['alias'])) { - $aliasId = sprintf('enqueue.transport.%s.connection_factory', $config['alias']); - } else { - $dsn = $this->resolveDSN($container, $config['dsn']); - - $aliasId = $this->findFactory($dsn)->createConnectionFactory($container, $config); - } - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $container->setAlias($factoryId, new Alias($aliasId, true)); - $container->setAlias('enqueue.transport.connection_factory', new Alias($factoryId, true)); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - if (isset($config['alias'])) { - $aliasId = sprintf('enqueue.transport.%s.context', $config['alias']); - } else { - $dsn = $this->resolveDSN($container, $config['dsn']); - - $aliasId = $this->findFactory($dsn)->createContext($container, $config); - } - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - - $container->setAlias($contextId, new Alias($aliasId, true)); - $container->setAlias('enqueue.transport.context', new Alias($contextId, true)); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - if (isset($config['alias'])) { - $aliasId = sprintf('enqueue.client.%s.driver', $config['alias']); - } else { - $dsn = $this->resolveDSN($container, $config['dsn']); - - $aliasId = $this->findFactory($dsn)->createDriver($container, $config); - } - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - - $container->setAlias($driverId, new Alias($aliasId, true)); - $container->setAlias('enqueue.client.driver', new Alias($driverId, true)); - - return $driverId; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * This is a quick fix to the exception "Incompatible use of dynamic environment variables "ENQUEUE_DSN" found in parameters." - * TODO: We'll have to come up with a better solution. - * - * @param ContainerBuilder $container - * @param $dsn - * - * @return array|false|string - */ - private function resolveDSN(ContainerBuilder $container, $dsn) - { - if (method_exists($container, 'resolveEnvPlaceholders')) { - $dsn = $container->resolveEnvPlaceholders($dsn); - - $matches = []; - if (preg_match('/%env\((.*?)\)/', $dsn, $matches)) { - if (false === $realDsn = getenv($matches[1])) { - throw new \LogicException(sprintf('The env "%s" var is not defined', $matches[1])); - } - - return $realDsn; - } - } - - return $dsn; - } - - /** - * @param string - * @param mixed $dsn - * - * @return TransportFactoryInterface - */ - private function findFactory($dsn) - { - $factory = dsn_to_connection_factory($dsn); - - if ($factory instanceof AmqpConnectionFactory) { - return new AmqpTransportFactory('default_amqp'); - } - - if ($factory instanceof FsConnectionFactory) { - return new FsTransportFactory('default_fs'); - } - - if ($factory instanceof DbalConnectionFactory) { - return new DbalTransportFactory('default_dbal'); - } - - if ($factory instanceof NullConnectionFactory) { - return new NullTransportFactory('default_null'); - } - - if ($factory instanceof GpsConnectionFactory) { - return new GpsTransportFactory('default_gps'); - } - - if ($factory instanceof RedisConnectionFactory) { - return new RedisTransportFactory('default_redis'); - } - - if ($factory instanceof SqsConnectionFactory) { - return new SqsTransportFactory('default_sqs'); - } - - if ($factory instanceof StompConnectionFactory) { - return new StompTransportFactory('default_stomp'); - } - - if ($factory instanceof RdKafkaConnectionFactory) { - return new RdKafkaTransportFactory('default_kafka'); - } - - if ($factory instanceof MongodbConnectionFactory) { - return new MongodbTransportFactory('default_mongodb'); - } - - throw new \LogicException(sprintf( - 'There is no supported transport factory for the connection factory "%s" created from DSN "%s"', - get_class($factory), - $dsn - )); - } -} diff --git a/pkg/enqueue/Symfony/DependencyInjection/BuildConsumptionExtensionsPass.php b/pkg/enqueue/Symfony/DependencyInjection/BuildConsumptionExtensionsPass.php new file mode 100644 index 000000000..99f274ec5 --- /dev/null +++ b/pkg/enqueue/Symfony/DependencyInjection/BuildConsumptionExtensionsPass.php @@ -0,0 +1,60 @@ +hasParameter('enqueue.transports')) { + throw new \LogicException('The "enqueue.transports" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.transports'); + $defaultName = $container->getParameter('enqueue.default_transport'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(TransportFactory::MODULE, $name); + + $extensionsId = $diUtils->format('consumption_extensions'); + if (false == $container->hasDefinition($extensionsId)) { + throw new \LogicException(sprintf('Service "%s" not found', $extensionsId)); + } + + $tags = $container->findTaggedServiceIds('enqueue.transport.consumption_extension'); + + $groupByPriority = []; + foreach ($tags as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $transport = $tagAttribute['transport'] ?? $defaultName; + + if ($transport !== $name && 'all' !== $transport) { + continue; + } + + $priority = (int) ($tagAttribute['priority'] ?? 0); + + $groupByPriority[$priority][] = new Reference($serviceId); + } + } + + krsort($groupByPriority, \SORT_NUMERIC); + + $flatExtensions = []; + foreach ($groupByPriority as $extension) { + $flatExtensions = array_merge($flatExtensions, $extension); + } + + $extensionsService = $container->getDefinition($extensionsId); + $extensionsService->replaceArgument(0, array_merge( + $extensionsService->getArgument(0), + $flatExtensions + )); + } + } +} diff --git a/pkg/enqueue/Symfony/DependencyInjection/BuildProcessorRegistryPass.php b/pkg/enqueue/Symfony/DependencyInjection/BuildProcessorRegistryPass.php new file mode 100644 index 000000000..cc6e04270 --- /dev/null +++ b/pkg/enqueue/Symfony/DependencyInjection/BuildProcessorRegistryPass.php @@ -0,0 +1,50 @@ +hasParameter('enqueue.transports')) { + throw new \LogicException('The "enqueue.transports" parameter must be set.'); + } + + $names = $container->getParameter('enqueue.transports'); + $defaultName = $container->getParameter('enqueue.default_transport'); + + foreach ($names as $name) { + $diUtils = DiUtils::create(TransportFactory::MODULE, $name); + + $processorRegistryId = $diUtils->format('processor_registry'); + if (false == $container->hasDefinition($processorRegistryId)) { + throw new \LogicException(sprintf('Service "%s" not found', $processorRegistryId)); + } + + $tag = 'enqueue.transport.processor'; + $map = []; + foreach ($container->findTaggedServiceIds($tag) as $serviceId => $tagAttributes) { + foreach ($tagAttributes as $tagAttribute) { + $transport = $tagAttribute['transport'] ?? $defaultName; + + if ($transport !== $name && 'all' !== $transport) { + continue; + } + + $processor = $tagAttribute['processor'] ?? $serviceId; + + $map[$processor] = new Reference($serviceId); + } + } + + $registry = $container->getDefinition($processorRegistryId); + $registry->setArgument(0, ServiceLocatorTagPass::register($container, $map, $processorRegistryId)); + } + } +} diff --git a/pkg/enqueue/Symfony/DependencyInjection/TransportFactory.php b/pkg/enqueue/Symfony/DependencyInjection/TransportFactory.php new file mode 100644 index 000000000..944b1a30d --- /dev/null +++ b/pkg/enqueue/Symfony/DependencyInjection/TransportFactory.php @@ -0,0 +1,266 @@ +default = $default; + $this->diUtils = DiUtils::create(self::MODULE, $name); + } + + public static function getConfiguration(string $name = 'transport'): NodeDefinition + { + $knownSchemes = array_keys(Resources::getKnownSchemes()); + $availableSchemes = array_keys(Resources::getAvailableSchemes()); + + $builder = new ArrayNodeDefinition($name); + $builder + ->info('The transport option could accept a string DSN, an array with DSN key, or null. It accept extra options. To find out what option you can set, look at connection factory constructor docblock.') + ->beforeNormalization() + ->always(function ($v) { + if (empty($v)) { + return ['dsn' => 'null:']; + } + + if (is_array($v)) { + if (isset($v['factory_class']) && isset($v['factory_service'])) { + throw new \LogicException('Both options factory_class and factory_service are set. Please choose one.'); + } + + if (isset($v['connection_factory_class']) && (isset($v['factory_class']) || isset($v['factory_service']))) { + throw new \LogicException('The option connection_factory_class must not be used with factory_class or factory_service at the same time. Please choose one.'); + } + + return $v; + } + + if (is_string($v)) { + return ['dsn' => $v]; + } + + throw new \LogicException(sprintf('The value must be array, null or string. Got "%s"', gettype($v))); + }) + ->end() + ->isRequired() + ->ignoreExtraKeys(false) + ->children() + ->scalarNode('dsn') + ->cannotBeEmpty() + ->isRequired() + ->info(sprintf( + 'The MQ broker DSN. These schemes are supported: "%s", to use these "%s" you have to install a package.', + implode('", "', $knownSchemes), + implode('", "', $availableSchemes) + )) + ->end() + ->scalarNode('connection_factory_class') + ->info(sprintf('The connection factory class should implement "%s" interface', ConnectionFactory::class)) + ->end() + ->scalarNode('factory_service') + ->info(sprintf('The factory class should implement "%s" interface', ConnectionFactoryFactoryInterface::class)) + ->end() + ->scalarNode('factory_class') + ->info(sprintf('The factory service should be a class that implements "%s" interface', ConnectionFactoryFactoryInterface::class)) + ->end() + ->end() + ; + + return $builder; + } + + public static function getQueueConsumerConfiguration(string $name = 'consumption'): ArrayNodeDefinition + { + $builder = new ArrayNodeDefinition($name); + + $builder + ->addDefaultsIfNotSet()->children() + ->integerNode('receive_timeout') + ->min(0) + ->defaultValue(10000) + ->info('the time in milliseconds queue consumer waits for a message (10000 ms by default)') + ->end() + ; + + return $builder; + } + + public function buildConnectionFactory(ContainerBuilder $container, array $config): void + { + $factoryId = $this->diUtils->format('connection_factory'); + + $factoryFactoryId = $this->diUtils->format('connection_factory_factory'); + $container->register($factoryFactoryId, $config['factory_class'] ?? ConnectionFactoryFactory::class); + + $factoryFactoryService = new Reference( + $config['factory_service'] ?? $factoryFactoryId + ); + + unset($config['factory_service'], $config['factory_class']); + + $connectionFactoryClass = $config['connection_factory_class'] ?? null; + unset($config['connection_factory_class']); + + if (isset($connectionFactoryClass)) { + $container->register($factoryId, $connectionFactoryClass) + ->addArgument($config) + ; + } else { + $container->register($factoryId, ConnectionFactory::class) + ->setFactory([$factoryFactoryService, 'create']) + ->addArgument($config) + ; + } + + if ($this->default) { + $container->setAlias(ConnectionFactory::class, $factoryId); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('connection_factory'), $factoryId); + } + } + } + + public function buildContext(ContainerBuilder $container, array $config): void + { + $factoryId = $this->diUtils->format('connection_factory'); + $this->assertServiceExists($container, $factoryId); + + $contextId = $this->diUtils->format('context'); + + $container->register($contextId, Context::class) + ->setFactory([new Reference($factoryId), 'createContext']) + ; + + $this->addServiceToLocator($container, 'context'); + + if ($this->default) { + $container->setAlias(Context::class, $contextId); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('context'), $contextId); + } + } + } + + public function buildQueueConsumer(ContainerBuilder $container, array $config): void + { + $contextId = $this->diUtils->format('context'); + $this->assertServiceExists($container, $contextId); + + $container->setParameter($this->diUtils->format('receive_timeout'), $config['receive_timeout'] ?? 10000); + + $logExtensionId = $this->diUtils->format('log_extension'); + $container->register($logExtensionId, LogExtension::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => $this->diUtils->getConfigName(), 'priority' => -100]) + ; + + $container->register($this->diUtils->format('consumption_extensions'), ChainExtension::class) + ->addArgument([]) + ; + + $container->register($this->diUtils->format('queue_consumer'), QueueConsumer::class) + ->addArgument(new Reference($contextId)) + ->addArgument(new Reference($this->diUtils->format('consumption_extensions'))) + ->addArgument([]) + ->addArgument(new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->addArgument($this->diUtils->parameter('receive_timeout')) + ; + + $container->register($this->diUtils->format('processor_registry'), ContainerProcessorRegistry::class); + + $this->addServiceToLocator($container, 'queue_consumer'); + $this->addServiceToLocator($container, 'processor_registry'); + + if ($this->default) { + $container->setAlias(QueueConsumerInterface::class, $this->diUtils->format('queue_consumer')); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('queue_consumer'), $this->diUtils->format('queue_consumer')); + } + } + } + + public function buildRpcClient(ContainerBuilder $container, array $config): void + { + $contextId = $this->diUtils->format('context'); + $this->assertServiceExists($container, $contextId); + + $container->register($this->diUtils->format('rpc_factory'), RpcFactory::class) + ->addArgument(new Reference($contextId)) + ; + + $container->register($this->diUtils->format('rpc_client'), RpcClient::class) + ->addArgument(new Reference($contextId)) + ->addArgument(new Reference($this->diUtils->format('rpc_factory'))) + ; + + if ($this->default) { + $container->setAlias(RpcClient::class, $this->diUtils->format('rpc_client')); + + if (DiUtils::DEFAULT_CONFIG !== $this->diUtils->getConfigName()) { + $container->setAlias($this->diUtils->formatDefault('rpc_client'), $this->diUtils->format('rpc_client')); + } + } + } + + private function assertServiceExists(ContainerBuilder $container, string $serviceId): void + { + if (false == $container->hasDefinition($serviceId)) { + throw new \InvalidArgumentException(sprintf('The service "%s" does not exist.', $serviceId)); + } + } + + private function addServiceToLocator(ContainerBuilder $container, string $serviceName): void + { + $locatorId = 'enqueue.locator'; + + if ($container->hasDefinition($locatorId)) { + $locator = $container->getDefinition($locatorId); + + $map = $locator->getArgument(0); + $map[$this->diUtils->format($serviceName)] = $this->diUtils->reference($serviceName); + + $locator->replaceArgument(0, $map); + } + } +} diff --git a/pkg/enqueue/Symfony/DiUtils.php b/pkg/enqueue/Symfony/DiUtils.php new file mode 100644 index 000000000..be45287be --- /dev/null +++ b/pkg/enqueue/Symfony/DiUtils.php @@ -0,0 +1,81 @@ +moduleName = $moduleName; + $this->configName = $configName; + } + + public static function create(string $moduleName, string $configName): self + { + return new self($moduleName, $configName); + } + + public function getModuleName(): string + { + return $this->moduleName; + } + + public function getConfigName(): string + { + return $this->configName; + } + + public function reference(string $serviceName, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): Reference + { + return new Reference($this->format($serviceName), $invalidBehavior); + } + + public function referenceDefault(string $serviceName, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE): Reference + { + return new Reference($this->formatDefault($serviceName), $invalidBehavior); + } + + public function parameter(string $serviceName): string + { + $fullName = $this->format($serviceName); + + return "%$fullName%"; + } + + public function parameterDefault(string $serviceName): string + { + $fullName = $this->formatDefault($serviceName); + + return "%$fullName%"; + } + + public function format(string $serviceName): string + { + return $this->doFormat($this->moduleName, $this->configName, $serviceName); + } + + public function formatDefault(string $serviceName): string + { + return $this->doFormat($this->moduleName, self::DEFAULT_CONFIG, $serviceName); + } + + private function doFormat(string $moduleName, string $configName, string $serviceName): string + { + return sprintf('enqueue.%s.%s.%s', $moduleName, $configName, $serviceName); + } +} diff --git a/pkg/enqueue/Symfony/DriverFactoryInterface.php b/pkg/enqueue/Symfony/DriverFactoryInterface.php deleted file mode 100644 index 0b1ab027d..000000000 --- a/pkg/enqueue/Symfony/DriverFactoryInterface.php +++ /dev/null @@ -1,21 +0,0 @@ -info($message) + ->beforeNormalization() + ->always(function () { + return []; + }) + ->end() + ->validate() + ->always(function () use ($message) { + throw new \InvalidArgumentException($message); + }) + ->end() + ; + + return $node; + } +} diff --git a/pkg/enqueue/Symfony/MissingTransportFactory.php b/pkg/enqueue/Symfony/MissingTransportFactory.php deleted file mode 100644 index 25e6791fa..000000000 --- a/pkg/enqueue/Symfony/MissingTransportFactory.php +++ /dev/null @@ -1,94 +0,0 @@ -name = $name; - $this->packages = $packages; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - if (1 == count($this->packages)) { - $message = sprintf( - 'In order to use the transport "%s" install a package "%s"', - $this->getName(), - implode('", "', $this->packages) - ); - } else { - $message = sprintf( - 'In order to use the transport "%s" install one of the packages "%s"', - $this->getName(), - implode('", "', $this->packages) - ); - } - - $builder - ->info($message) - ->beforeNormalization() - ->always(function () { - return []; - }) - ->end() - ->validate() - ->always(function () use ($message) { - throw new \InvalidArgumentException($message); - }) - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - throw new \LogicException('Should not be called'); - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - throw new \LogicException('Should not be called'); - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - throw new \LogicException('Should not be called'); - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/enqueue/Symfony/RabbitMqAmqpTransportFactory.php b/pkg/enqueue/Symfony/RabbitMqAmqpTransportFactory.php deleted file mode 100644 index 94ee9234e..000000000 --- a/pkg/enqueue/Symfony/RabbitMqAmqpTransportFactory.php +++ /dev/null @@ -1,69 +0,0 @@ -children() - ->scalarNode('delay_strategy') - ->defaultValue('dlx') - ->info('The delay strategy to be used. Possible values are "dlx", "delayed_message_plugin" or service id') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factoryId = parent::createConnectionFactory($container, $config); - - $this->registerDelayStrategy($container, $config, $factoryId, $this->getName()); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(RabbitMqDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } -} diff --git a/pkg/enqueue/Symfony/TransportFactoryInterface.php b/pkg/enqueue/Symfony/TransportFactoryInterface.php deleted file mode 100644 index d2be801ca..000000000 --- a/pkg/enqueue/Symfony/TransportFactoryInterface.php +++ /dev/null @@ -1,35 +0,0 @@ -assertClassImplements(ProcessorRegistryInterface::class, ArrayProcessorRegistry::class); } - public function testCouldBeConstructedWithoutAnyArgument() - { - new ArrayProcessorRegistry(); - } - public function testShouldThrowExceptionIfProcessorIsNotSet() { $registry = new ArrayProcessorRegistry(); @@ -51,10 +47,10 @@ public function testShouldAllowGetProcessorAddedViaAddMethod() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor + * @return MockObject|Processor */ protected function createProcessorMock() { - return $this->createMock(PsrProcessor::class); + return $this->createMock(Processor::class); } } diff --git a/pkg/enqueue/Tests/Client/Amqp/AmqpDriverTest.php b/pkg/enqueue/Tests/Client/Amqp/AmqpDriverTest.php deleted file mode 100644 index a82fda082..000000000 --- a/pkg/enqueue/Tests/Client/Amqp/AmqpDriverTest.php +++ /dev/null @@ -1,417 +0,0 @@ -assertClassImplements(DriverInterface::class, AmqpDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = $this->createDummyConfig(); - - $driver = new AmqpDriver($this->createAmqpContextMock(), $config, $this->createDummyQueueMetaRegistry()); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new AmqpQueue('aName'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new AmqpDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - $this->assertSame([], $queue->getArguments()); - $this->assertSame(2, $queue->getFlags()); - $this->assertNull($queue->getConsumerTag()); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new AmqpQueue('aName'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new AmqpDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setHeader('expiration', '12345000'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'expiration' => '12345000', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame(12345, $clientMessage->getExpire()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - } - - public function testShouldThrowExceptionIfExpirationIsNotNumeric() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setHeader('expiration', 'is-not-numeric'); - - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('expiration header is not numeric. "is-not-numeric"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new AmqpMessage()) - ; - - $driver = new AmqpDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(AmqpMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - 'content_type' => 'ContentType', - 'delivery_mode' => 2, - 'expiration' => '123000', - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testShouldSendMessageToRouter() - { - $topic = new AmqpTopic(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new AmqpDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new AmqpQueue(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new AmqpDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new AmqpDriver( - $this->createAmqpContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBroker() - { - $routerTopic = new AmqpTopic(''); - $routerQueue = new AmqpQueue(''); - - $processorQueue = new AmqpQueue(''); - - $context = $this->createAmqpContextMock(); - // setup router - $context - ->expects($this->at(0)) - ->method('createTopic') - ->willReturn($routerTopic) - ; - $context - ->expects($this->at(1)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(2)) - ->method('declareTopic') - ->with($this->identicalTo($routerTopic)) - ; - $context - ->expects($this->at(3)) - ->method('declareQueue') - ->with($this->identicalTo($routerQueue)) - ; - $context - ->expects($this->at(4)) - ->method('bind') - ->with($this->isInstanceOf(AmqpBind::class)) - ; - // setup processor queue - $context - ->expects($this->at(5)) - ->method('createQueue') - ->willReturn($processorQueue) - ; - $context - ->expects($this->at(6)) - ->method('declareQueue') - ->with($this->identicalTo($processorQueue)) - ; - - $meta = new QueueMetaRegistry($this->createDummyConfig(), [ - 'default' => [], - ]); - - $driver = new AmqpDriver( - $context, - $this->createDummyConfig(), - $meta - ); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext - */ - private function createAmqpContextMock() - { - return $this->createMock(AmqpContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpProducer - */ - private function createAmqpProducerMock() - { - return $this->createMock(AmqpProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/enqueue/Tests/Client/Amqp/RabbitMqDriverTest.php b/pkg/enqueue/Tests/Client/Amqp/RabbitMqDriverTest.php deleted file mode 100644 index 3b67ef52e..000000000 --- a/pkg/enqueue/Tests/Client/Amqp/RabbitMqDriverTest.php +++ /dev/null @@ -1,599 +0,0 @@ -assertClassImplements(DriverInterface::class, RabbitMqDriver::class); - } - - public function testShouldExtendsAmqpDriverClass() - { - $this->assertClassExtends(AmqpDriver::class, RabbitMqDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = Config::create(); - - $driver = new RabbitMqDriver($this->createAmqpContextMock(), $config, $this->createDummyQueueMetaRegistry()); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new AmqpQueue('aName'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new AmqpDriver($context, Config::create(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - $this->assertSame([], $queue->getArguments()); - $this->assertSame(2, $queue->getFlags()); - $this->assertNull($queue->getConsumerTag()); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new AmqpQueue('aName'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new AmqpDriver($context, Config::create(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setProperty('enqueue-delay', '5678000'); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setHeader('expiration', '12345000'); - $transportMessage->setHeader('priority', 3); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - new Config('', '', '', '', '', '', ['delay_strategy' => 'dlx']), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'expiration' => '12345000', - 'priority' => 3, - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - 'enqueue-delay' => '5678000', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame(12345, $clientMessage->getExpire()); - $this->assertSame(5678, $clientMessage->getDelay()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame(MessagePriority::HIGH, $clientMessage->getPriority()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - } - - public function testShouldThrowExceptionIfXDelayIsNotNumeric() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setProperty('enqueue-delay', 'is-not-numeric'); - - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('"enqueue-delay" header is not numeric. "is-not-numeric"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldThrowExceptionIfExpirationIsNotNumeric() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setHeader('expiration', 'is-not-numeric'); - - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('expiration header is not numeric. "is-not-numeric"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldThrowExceptionIfCantConvertTransportPriorityToClientPriority() - { - $transportMessage = new AmqpMessage(); - $transportMessage->setHeader('priority', 'unknown'); - - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Cant convert transport priority to client: "unknown"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldThrowExceptionIfCantConvertClientPriorityToTransportPriority() - { - $clientMessage = new Message(); - $clientMessage->setPriority('unknown'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new AmqpMessage()) - ; - - $driver = new RabbitMqDriver( - $context, - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Given priority could not be converted to client\'s one. Got: unknown'); - - $driver->createTransportMessage($clientMessage); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setPriority(MessagePriority::VERY_HIGH); - $clientMessage->setDelay(432); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new AmqpMessage()) - ; - - $driver = new RabbitMqDriver( - $context, - new Config('', '', '', '', '', '', ['delay_strategy' => 'dlx']), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(AmqpMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - 'content_type' => 'ContentType', - 'delivery_mode' => 2, - 'expiration' => '123000', - 'priority' => 4, - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - 'enqueue-delay' => 432000, - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testThrowIfDelayNotSupportedOnConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setDelay(432); - - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new AmqpMessage()) - ; - - $driver = new RabbitMqDriver( - $context, - new Config('', '', '', '', '', '', ['delay_strategy' => null]), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The message delaying is not supported. In order to use delay feature install RabbitMQ delay strategy.'); - $driver->createTransportMessage($clientMessage); - } - - public function testShouldSendMessageToRouter() - { - $topic = new AmqpTopic(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqDriver( - $context, - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new AmqpQueue(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqDriver( - $context, - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldSendMessageToProcessorWithDeliveryDelay() - { - $queue = new AmqpQueue(''); - $transportMessage = new AmqpMessage(); - - $producer = $this->createAmqpProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $producer - ->expects($this->once()) - ->method('setDeliveryDelay') - ->with($this->identicalTo(10000)) - ; - $context = $this->createAmqpContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqDriver( - $context, - new Config('', '', '', '', '', '', ['delay_strategy' => 'dlx']), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - $message->setDelay(10); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new RabbitMqDriver( - $this->createAmqpContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBrokerWhenDelayPluginNotInstalled() - { - $routerTopic = new AmqpTopic(''); - $routerQueue = new AmqpQueue(''); - - $processorQueue = new AmqpQueue(''); - - $context = $this->createAmqpContextMock(); - // setup router - $context - ->expects($this->at(0)) - ->method('createTopic') - ->willReturn($routerTopic) - ; - $context - ->expects($this->at(1)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(2)) - ->method('declareTopic') - ->with($this->identicalTo($routerTopic)) - ; - $context - ->expects($this->at(3)) - ->method('declareQueue') - ->with($this->identicalTo($routerQueue)) - ; - $context - ->expects($this->at(4)) - ->method('bind') - ->with($this->isInstanceOf(AmqpBind::class)) - ; - // setup processor queue - $context - ->expects($this->at(5)) - ->method('createQueue') - ->willReturn($processorQueue) - ; - - $config = Config::create('', '', '', '', '', '', ['delay_strategy' => null]); - - $meta = new QueueMetaRegistry($config, ['default' => []]); - - $driver = new RabbitMqDriver($context, $config, $meta); - - $driver->setupBroker(); - } - - public function testShouldSetupBroker() - { - $routerTopic = new AmqpTopic(''); - $routerQueue = new AmqpQueue(''); - - $processorQueue = new AmqpQueue(''); - - $context = $this->createAmqpContextMock(); - // setup router - $context - ->expects($this->at(0)) - ->method('createTopic') - ->willReturn($routerTopic) - ; - $context - ->expects($this->at(1)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(2)) - ->method('declareTopic') - ->with($this->identicalTo($routerTopic)) - ; - $context - ->expects($this->at(3)) - ->method('declareQueue') - ->with($this->identicalTo($routerQueue)) - ; - $context - ->expects($this->at(4)) - ->method('bind') - ->with($this->isInstanceOf(AmqpBind::class)) - ; - // setup processor queue - $context - ->expects($this->at(5)) - ->method('createQueue') - ->willReturn($processorQueue) - ; - $context - ->expects($this->at(6)) - ->method('declareQueue') - ->with($this->identicalTo($processorQueue)) - ; - - $config = Config::create('', '', '', '', '', '', ['delay_strategy' => 'dlx']); - - $meta = new QueueMetaRegistry($config, ['default' => []]); - - $driver = new RabbitMqDriver($context, $config, $meta); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpContext - */ - private function createAmqpContextMock() - { - return $this->createMock(AmqpContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|AmqpProducer - */ - private function createAmqpProducerMock() - { - return $this->createMock(AmqpProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry(Config::create('aPrefix'), []); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } -} diff --git a/pkg/enqueue/Tests/Client/ChainExtensionTest.php b/pkg/enqueue/Tests/Client/ChainExtensionTest.php index 3b1d82f9a..0f42bcf18 100644 --- a/pkg/enqueue/Tests/Client/ChainExtensionTest.php +++ b/pkg/enqueue/Tests/Client/ChainExtensionTest.php @@ -3,9 +3,16 @@ namespace Enqueue\Tests\Client; use Enqueue\Client\ChainExtension; +use Enqueue\Client\DriverInterface; +use Enqueue\Client\DriverPreSend; use Enqueue\Client\ExtensionInterface; use Enqueue\Client\Message; +use Enqueue\Client\PostSend; +use Enqueue\Client\PreSend; +use Enqueue\Client\ProducerInterface; use Enqueue\Test\ClassExtensionTrait; +use Interop\Queue\Destination; +use Interop\Queue\Message as TransportMessage; use PHPUnit\Framework\TestCase; class ChainExtensionTest extends TestCase @@ -17,57 +24,129 @@ public function testShouldImplementExtensionInterface() $this->assertClassImplements(ExtensionInterface::class, ChainExtension::class); } - public function testCouldBeConstructedWithExtensionsArray() + public function testShouldBeFinal() { - new ChainExtension([$this->createExtension(), $this->createExtension()]); + $this->assertClassFinal(ChainExtension::class); } - public function testShouldProxyOnPreSendToAllInternalExtensions() + public function testThrowIfArrayContainsNotExtension() { - $message = new Message(); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Invalid extension given'); + + new ChainExtension([$this->createExtension(), new \stdClass()]); + } + + public function testShouldProxyOnPreSendEventToAllInternalExtensions() + { + $preSend = new PreSend( + 'aCommandOrTopic', + new Message(), + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class) + ); + + $fooExtension = $this->createExtension(); + $fooExtension + ->expects($this->once()) + ->method('onPreSendEvent') + ->with($this->identicalTo($preSend)) + ; + $barExtension = $this->createExtension(); + $barExtension + ->expects($this->once()) + ->method('onPreSendEvent') + ->with($this->identicalTo($preSend)) + ; + + $extensions = new ChainExtension([$fooExtension, $barExtension]); + + $extensions->onPreSendEvent($preSend); + } + + public function testShouldProxyOnPreSendCommandToAllInternalExtensions() + { + $preSend = new PreSend( + 'aCommandOrTopic', + new Message(), + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class) + ); + + $fooExtension = $this->createExtension(); + $fooExtension + ->expects($this->once()) + ->method('onPreSendCommand') + ->with($this->identicalTo($preSend)) + ; + $barExtension = $this->createExtension(); + $barExtension + ->expects($this->once()) + ->method('onPreSendCommand') + ->with($this->identicalTo($preSend)) + ; + + $extensions = new ChainExtension([$fooExtension, $barExtension]); + + $extensions->onPreSendCommand($preSend); + } + + public function testShouldProxyOnDriverPreSendToAllInternalExtensions() + { + $driverPreSend = new DriverPreSend( + new Message(), + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class) + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onPreSend') - ->with('topic', $this->identicalTo($message)) + ->method('onDriverPreSend') + ->with($this->identicalTo($driverPreSend)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onPreSend') - ->with('topic', $this->identicalTo($message)) + ->method('onDriverPreSend') + ->with($this->identicalTo($driverPreSend)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onPreSend('topic', $message); + $extensions->onDriverPreSend($driverPreSend); } - public function testShouldProxyOnPostSendToAllInternalExtensions() + public function testShouldProxyOnPostSentToAllInternalExtensions() { - $message = new Message(); + $postSend = new PostSend( + new Message(), + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class), + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) ->method('onPostSend') - ->with('topic', $this->identicalTo($message)) + ->with($this->identicalTo($postSend)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) ->method('onPostSend') - ->with('topic', $this->identicalTo($message)) + ->with($this->identicalTo($postSend)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onPostSend('topic', $message); + $extensions->onPostSend($postSend); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ExtensionInterface + * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface */ protected function createExtension() { diff --git a/pkg/enqueue/Tests/Client/ConfigTest.php b/pkg/enqueue/Tests/Client/ConfigTest.php index 1ecb0f952..09b80e2c1 100644 --- a/pkg/enqueue/Tests/Client/ConfigTest.php +++ b/pkg/enqueue/Tests/Client/ConfigTest.php @@ -7,139 +7,246 @@ class ConfigTest extends TestCase { - public function testShouldReturnRouterProcessorNameSetInConstructor() + public function testShouldReturnPrefixSetInConstructor() { $config = new Config( - 'aPrefix', + 'thePrefix', + 'theSeparator', 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aRouterProcessorName', $config->getRouterProcessorName()); + $this->assertEquals('thePrefix', $config->getPrefix()); } - public function testShouldReturnRouterTopicNameSetInConstructor() + /** + * @dataProvider provideEmptyStrings + */ + public function testShouldTrimReturnPrefixSetInConstructor(string $empty) { $config = new Config( - 'aPrefix', + $empty, 'aApp', + 'theSeparator', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aRouterTopicName', $config->getRouterTopicName()); + $this->assertSame('', $config->getPrefix()); } - public function testShouldReturnRouterQueueNameSetInConstructor() + public function testShouldReturnAppNameSetInConstructor() { $config = new Config( 'aPrefix', - 'aApp', + 'theSeparator', + 'theApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aRouterQueueName', $config->getRouterQueueName()); + $this->assertEquals('theApp', $config->getApp()); } - public function testShouldReturnDefaultQueueNameSetInConstructor() + /** + * @dataProvider provideEmptyStrings + */ + public function testShouldTrimReturnAppNameSetInConstructor(string $empty) { $config = new Config( 'aPrefix', - 'aApp', + 'theSeparator', + $empty, 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aDefaultQueueName', $config->getDefaultProcessorQueueName()); + $this->assertSame('', $config->getApp()); } - public function testShouldCreateRouterTopicName() + public function testShouldReturnRouterProcessorNameSetInConstructor() { $config = new Config( 'aPrefix', + 'theSeparator', 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aprefix.aname', $config->createTransportRouterTopicName('aName')); + $this->assertEquals('aRouterProcessorName', $config->getRouterProcessor()); } - public function testShouldCreateProcessorQueueName() + public function testShouldReturnRouterTopicNameSetInConstructor() { $config = new Config( 'aPrefix', + 'theSeparator', 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aprefix.aapp.aname', $config->createTransportQueueName('aName')); + $this->assertEquals('aRouterTopicName', $config->getRouterTopic()); } - public function testShouldCreateProcessorQueueNameWithoutAppName() + public function testShouldReturnRouterQueueNameSetInConstructor() { $config = new Config( 'aPrefix', - '', + 'theSeparator', + 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aprefix.aname', $config->createTransportQueueName('aName')); + $this->assertEquals('aRouterQueueName', $config->getRouterQueue()); } - public function testShouldCreateProcessorQueueNameWithoutPrefix() + public function testShouldReturnDefaultQueueNameSetInConstructor() { $config = new Config( - '', + 'aPrefix', + 'theSeparator', 'aApp', 'aRouterTopicName', 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] ); - $this->assertEquals('aapp.aname', $config->createTransportQueueName('aName')); + $this->assertEquals('aDefaultQueueName', $config->getDefaultQueue()); } - public function testShouldCreateProcessorQueueNameWithoutPrefixAndAppName() + public function testShouldCreateDefaultConfig() { - $config = new Config( + $config = Config::create(); + + $this->assertSame('default', $config->getDefaultQueue()); + $this->assertSame('router', $config->getRouterProcessor()); + $this->assertSame('default', $config->getRouterQueue()); + $this->assertSame('router', $config->getRouterTopic()); + } + + /** + * @dataProvider provideEmptyStrings + */ + public function testThrowIfRouterTopicNameIsEmpty(string $empty) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Router topic is empty.'); + new Config( '', '', - 'aRouterTopicName', + '', + $empty, 'aRouterQueueName', 'aDefaultQueueName', - 'aRouterProcessorName' + 'aRouterProcessorName', + [], + [] + ); + } + + /** + * @dataProvider provideEmptyStrings + */ + public function testThrowIfRouterQueueNameIsEmpty(string $empty) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Router queue is empty.'); + new Config( + '', + '', + '', + 'aRouterTopicName', + $empty, + 'aDefaultQueueName', + 'aRouterProcessorName', + [], + [] + ); + } + + /** + * @dataProvider provideEmptyStrings + */ + public function testThrowIfDefaultQueueNameIsEmpty(string $empty) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Default processor queue name is empty.'); + new Config( + '', + '', + '', + 'aRouterTopicName', + 'aRouterQueueName', + $empty, + 'aRouterProcessorName', + [], + [] ); + } - $this->assertEquals('aname', $config->createTransportQueueName('aName')); + /** + * @dataProvider provideEmptyStrings + */ + public function testThrowIfRouterProcessorNameIsEmpty(string $empty) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Router processor name is empty.'); + new Config( + '', + '', + '', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueueName', + $empty, + [], + [] + ); } - public function testShouldCreateDefaultConfig() + public function provideEmptyStrings() { - $config = Config::create(); + yield ['']; + + yield [' ']; + + yield [' ']; - $this->assertSame('default', $config->getDefaultProcessorQueueName()); - $this->assertSame('router', $config->getRouterProcessorName()); - $this->assertSame('default', $config->getRouterQueueName()); - $this->assertSame('router', $config->getRouterTopicName()); + yield ["\t"]; } } diff --git a/pkg/enqueue/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php b/pkg/enqueue/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php index b61a3416c..a660126ad 100644 --- a/pkg/enqueue/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php +++ b/pkg/enqueue/Tests/Client/ConsumptionExtension/DelayRedeliveredMessageExtensionTest.php @@ -4,22 +4,25 @@ use Enqueue\Client\ConsumptionExtension\DelayRedeliveredMessageExtension; use Enqueue\Client\DriverInterface; +use Enqueue\Client\DriverSendResult; use Enqueue\Client\Message; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\MessageReceived; use Enqueue\Consumption\Result; use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; -use Interop\Queue\PsrContext; +use Enqueue\Test\TestLogger; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Destination; +use Interop\Queue\Message as TransportMessage; +use Interop\Queue\Processor; +use Interop\Queue\Queue; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class DelayRedeliveredMessageExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new DelayRedeliveredMessageExtension($this->createDriverMock(), 12345); - } - public function testShouldSendDelayedMessageAndRejectOriginalMessage() { $queue = new NullQueue('queue'); @@ -38,6 +41,7 @@ public function testShouldSendDelayedMessageAndRejectOriginalMessage() ->expects(self::once()) ->method('sendToProcessor') ->with(self::isInstanceOf(Message::class)) + ->willReturn($this->createDriverSendResult()) ; $driver ->expects(self::once()) @@ -46,32 +50,23 @@ public function testShouldSendDelayedMessageAndRejectOriginalMessage() ->willReturn($delayedMessage) ; - $logger = $this->createLoggerMock(); - $logger - ->expects(self::at(0)) - ->method('debug') - ->with('[DelayRedeliveredMessageExtension] Send delayed message') - ; - $logger - ->expects(self::at(1)) - ->method('debug') - ->with( - '[DelayRedeliveredMessageExtension] '. - 'Reject redelivered original message by setting reject status to context.' - ) - ; + $logger = new TestLogger(); - $context = new Context($this->createPsrContextMock()); - $context->setPsrQueue($queue); - $context->setPsrMessage($originMessage); - $context->setLogger($logger); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub($queue), + $originMessage, + $this->createProcessorMock(), + 1, + $logger + ); - $this->assertNull($context->getResult()); + $this->assertNull($messageReceived->getResult()); $extension = new DelayRedeliveredMessageExtension($driver, 12345); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - $result = $context->getResult(); + $result = $messageReceived->getResult(); $this->assertInstanceOf(Result::class, $result); $this->assertSame(Result::REJECT, $result->getStatus()); $this->assertSame('A new copy of the message was sent with a delay. The original message is rejected', $result->getReason()); @@ -80,6 +75,16 @@ public function testShouldSendDelayedMessageAndRejectOriginalMessage() $this->assertEquals([ 'enqueue.redelivery_count' => 1, ], $delayedMessage->getProperties()); + + self::assertTrue( + $logger->hasDebugThatContains('[DelayRedeliveredMessageExtension] Send delayed message') + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[DelayRedeliveredMessageExtension] '. + 'Reject redelivered original message by setting reject status to context.' + ) + ); } public function testShouldDoNothingIfMessageIsNotRedelivered() @@ -92,13 +97,19 @@ public function testShouldDoNothingIfMessageIsNotRedelivered() ->method('sendToProcessor') ; - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); $extension = new DelayRedeliveredMessageExtension($driver, 12345); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - $this->assertNull($context->getResult()); + $this->assertNull($messageReceived->getResult()); } public function testShouldDoNothingIfMessageIsRedeliveredButResultWasAlreadySetOnContext() @@ -112,35 +123,64 @@ public function testShouldDoNothingIfMessageIsRedeliveredButResultWasAlreadySetO ->method('sendToProcessor') ; - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); - $context->setResult('aStatus'); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + $messageReceived->setResult(Result::ack()); $extension = new DelayRedeliveredMessageExtension($driver, 12345); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return MockObject */ - private function createDriverMock() + private function createDriverMock(): DriverInterface { return $this->createMock(DriverInterface::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject + */ + private function createContextMock(): InteropContext + { + return $this->createMock(InteropContext::class); + } + + /** + * @return MockObject */ - private function createPsrContextMock() + private function createProcessorMock(): Processor { - return $this->createMock(PsrContext::class); + return $this->createMock(Processor::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + * @return MockObject|Consumer */ - private function createLoggerMock() + private function createConsumerStub(?Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue ?? new NullQueue('queue')) + ; + + return $consumerMock; + } + + private function createDriverSendResult(): DriverSendResult { - return $this->createMock(LoggerInterface::class); + return new DriverSendResult( + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); } } diff --git a/pkg/enqueue/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php b/pkg/enqueue/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php index acbcb41ab..b1e47c898 100644 --- a/pkg/enqueue/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php +++ b/pkg/enqueue/Tests/Client/ConsumptionExtension/ExclusiveCommandExtensionTest.php @@ -4,14 +4,19 @@ use Enqueue\Client\Config; use Enqueue\Client\ConsumptionExtension\ExclusiveCommandExtension; -use Enqueue\Client\ExtensionInterface as ClientExtensionInterface; -use Enqueue\Client\Message; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\ExtensionInterface as ConsumptionExtensionInterface; -use Enqueue\Null\NullContext; +use Enqueue\Client\DriverInterface; +use Enqueue\Client\Route; +use Enqueue\Client\RouteCollection; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Processor; +use Interop\Queue\Queue; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; @@ -19,176 +24,263 @@ class ExclusiveCommandExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementConsumptionExtensionInterface() + public function testShouldImplementMessageReceivedExtensionInterface() { - $this->assertClassImplements(ConsumptionExtensionInterface::class, ExclusiveCommandExtension::class); + $this->assertClassImplements(MessageReceivedExtensionInterface::class, ExclusiveCommandExtension::class); } - public function testShouldImplementClientExtensionInterface() + public function testShouldBeFinal() { - $this->assertClassImplements(ClientExtensionInterface::class, ExclusiveCommandExtension::class); - } - - public function testCouldBeConstructedWithQueueNameToProcessorNameMap() - { - new ExclusiveCommandExtension([]); - - new ExclusiveCommandExtension(['fooQueueName' => 'fooProcessorName']); + $this->assertClassFinal(ExclusiveCommandExtension::class); } public function testShouldDoNothingIfMessageHasTopicPropertySetOnPreReceive() { $message = new NullMessage(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'aTopic'); + $message->setProperty(Config::TOPIC, 'aTopic'); - $context = new Context(new NullContext()); - $context->setPsrMessage($message); + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('createQueue') + ; - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + + $extension = new ExclusiveCommandExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([ - 'enqueue.topic_name' => 'aTopic', + Config::TOPIC => 'aTopic', ], $message->getProperties()); } - public function testShouldDoNothingIfMessageHasProcessorNamePropertySetOnPreReceive() + public function testShouldDoNothingIfMessageHasCommandPropertySetOnPreReceive() { $message = new NullMessage(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'aProcessor'); + $message->setProperty(Config::COMMAND, 'aCommand'); - $context = new Context(new NullContext()); - $context->setPsrMessage($message); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('createQueue') + ; + + $extension = new ExclusiveCommandExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([ - 'enqueue.processor_name' => 'aProcessor', + Config::COMMAND => 'aCommand', ], $message->getProperties()); } - public function testShouldDoNothingIfMessageHasProcessorQueueNamePropertySetOnPreReceive() + public function testShouldDoNothingIfMessageHasProcessorPropertySetOnPreReceive() { $message = new NullMessage(); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aProcessorQueueName'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); - $context = new Context(new NullContext()); - $context->setPsrMessage($message); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('createQueue') + ; + + $extension = new ExclusiveCommandExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([ - 'enqueue.processor_queue_name' => 'aProcessorQueueName', + Config::PROCESSOR => 'aProcessor', ], $message->getProperties()); } - public function testShouldDoNothingIfCurrentQueueIsNotInTheMap() + public function testShouldDoNothingIfCurrentQueueHasNoExclusiveProcessor() { $message = new NullMessage(); $queue = new NullQueue('aBarQueueName'); - $context = new Context(new NullContext()); - $context->setPsrMessage($message); - $context->setPsrQueue($queue); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub($queue), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + $extension = new ExclusiveCommandExtension($this->createDriverStub(new RouteCollection([]))); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([], $message->getProperties()); } - public function testShouldSetCommandPropertiesIfCurrentQueueInTheMap() + public function testShouldSetCommandPropertiesIfCurrentQueueHasExclusiveCommandProcessor() { $message = new NullMessage(); - $queue = new NullQueue('aFooQueueName'); - - $context = new Context(new NullContext()); - $context->setPsrMessage($message); - $context->setPsrQueue($queue); - $context->setLogger(new NullLogger()); - - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', + $queue = new NullQueue('fooQueue'); + + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub($queue), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'theFooProcessor', [ + 'exclusive' => true, + 'queue' => 'fooQueue', + ]), + new Route('barCommand', Route::COMMAND, 'theFooProcessor', [ + 'exclusive' => true, + 'queue' => 'barQueue', + ]), ]); - $extension->onPreReceived($context); + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->any()) + ->method('createRouteQueue') + ->with($this->isInstanceOf(Route::class)) + ->willReturnCallback(function (Route $route) { + return new NullQueue($route->getQueue()); + }) + ; + + $extension = new ExclusiveCommandExtension($driver); + $extension->onMessageReceived($messageReceived); - self::assertNull($context->getResult()); + self::assertNull($messageReceived->getResult()); $this->assertEquals([ - 'enqueue.topic_name' => '__command__', - 'enqueue.processor_queue_name' => 'aFooQueueName', - 'enqueue.processor_name' => 'aFooProcessorName', - 'enqueue.command_name' => 'aFooProcessorName', + Config::PROCESSOR => 'theFooProcessor', + Config::COMMAND => 'fooCommand', ], $message->getProperties()); } - public function testShouldDoNothingOnPreSendIfTopicNotCommandOne() + public function testShouldDoNothingIfAnotherQueue() { - $message = new Message(); - - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', + $message = new NullMessage(); + $queue = new NullQueue('barQueue'); + + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub($queue), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'theFooProcessor', [ + 'exclusive' => true, + 'queue' => 'fooQueue', + ]), + new Route('barCommand', Route::COMMAND, 'theFooProcessor', [ + 'exclusive' => false, + 'queue' => 'barQueue', + ]), ]); - $extension->onPreSend('aTopic', $message); + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) + ; + + $extension = new ExclusiveCommandExtension($driver); + $extension->onMessageReceived($messageReceived); + + self::assertNull($messageReceived->getResult()); $this->assertEquals([], $message->getProperties()); } - public function testShouldDoNothingIfCommandNotExclusive() + /** + * @return MockObject|DriverInterface + */ + private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface { - $message = new Message(); - $message->setProperty(Config::PARAMETER_COMMAND_NAME, 'theBarProcessorName'); - - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); - - $extension->onPreSend(Config::COMMAND_TOPIC, $message); - - $this->assertEquals([ - 'enqueue.command_name' => 'theBarProcessorName', - ], $message->getProperties()); + $driver = $this->createMock(DriverInterface::class); + $driver + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection ?? new RouteCollection([])) + ; + + return $driver; } - public function testShouldForceExclusiveCommandQueue() + /** + * @return MockObject + */ + private function createContextMock(): InteropContext { - $message = new Message(); - $message->setProperty(Config::PARAMETER_COMMAND_NAME, 'aFooProcessorName'); - - $extension = new ExclusiveCommandExtension([ - 'aFooQueueName' => 'aFooProcessorName', - ]); + return $this->createMock(InteropContext::class); + } - $extension->onPreSend(Config::COMMAND_TOPIC, $message); + /** + * @return MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } - $this->assertEquals([ - 'enqueue.command_name' => 'aFooProcessorName', - 'enqueue.processor_name' => 'aFooProcessorName', - 'enqueue.processor_queue_name' => 'aFooQueueName', - ], $message->getProperties()); + /** + * @return MockObject|Consumer + */ + private function createConsumerStub(?Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue ?? new NullQueue('queue')) + ; + + return $consumerMock; } } diff --git a/pkg/enqueue/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php b/pkg/enqueue/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php index 1469c99bb..6a782c524 100644 --- a/pkg/enqueue/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php +++ b/pkg/enqueue/Tests/Client/ConsumptionExtension/FlushSpoolProducerExtensionTest.php @@ -4,86 +4,33 @@ use Enqueue\Client\ConsumptionExtension\FlushSpoolProducerExtension; use Enqueue\Client\SpoolProducer; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\EndExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; use Enqueue\Test\ClassExtensionTrait; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; class FlushSpoolProducerExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementPostMessageReceivedExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, FlushSpoolProducerExtension::class); + $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, FlushSpoolProducerExtension::class); } - public function testCouldBeConstructedWithSpoolProducerAsFirstArgument() + public function testShouldImplementEndExtensionInterface() { - new FlushSpoolProducerExtension($this->createSpoolProducerMock()); + $this->assertClassImplements(EndExtensionInterface::class, FlushSpoolProducerExtension::class); } - public function testShouldDoNothingOnStart() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onStart($this->createContextMock()); - } - - public function testShouldDoNothingOnBeforeReceive() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onBeforeReceive($this->createContextMock()); - } - - public function testShouldDoNothingOnPreReceived() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onPreReceived($this->createContextMock()); - } - - public function testShouldDoNothingOnResult() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onResult($this->createContextMock()); - } - - public function testShouldDoNothingOnIdle() - { - $producer = $this->createSpoolProducerMock(); - $producer - ->expects(self::never()) - ->method('flush') - ; - - $extension = new FlushSpoolProducerExtension($producer); - $extension->onIdle($this->createContextMock()); - } - - public function testShouldFlushSpoolProducerOnInterrupted() + public function testShouldFlushSpoolProducerOnEnd() { $producer = $this->createSpoolProducerMock(); $producer @@ -91,8 +38,10 @@ public function testShouldFlushSpoolProducerOnInterrupted() ->method('flush') ; + $end = new End($this->createInteropContextMock(), 1, 2, new NullLogger()); + $extension = new FlushSpoolProducerExtension($producer); - $extension->onInterrupted($this->createContextMock()); + $extension->onEnd($end); } public function testShouldFlushSpoolProducerOnPostReceived() @@ -103,20 +52,29 @@ public function testShouldFlushSpoolProducerOnPostReceived() ->method('flush') ; + $context = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); + $extension = new FlushSpoolProducerExtension($producer); - $extension->onPostReceived($this->createContextMock()); + $extension->onPostMessageReceived($context); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Context + * @return MockObject */ - private function createContextMock() + private function createInteropContextMock(): Context { return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SpoolProducer + * @return MockObject|SpoolProducer */ private function createSpoolProducerMock() { diff --git a/pkg/enqueue/Tests/Client/ConsumptionExtension/LogExtensionTest.php b/pkg/enqueue/Tests/Client/ConsumptionExtension/LogExtensionTest.php new file mode 100644 index 000000000..db757676b --- /dev/null +++ b/pkg/enqueue/Tests/Client/ConsumptionExtension/LogExtensionTest.php @@ -0,0 +1,536 @@ +assertClassImplements(StartExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementEndExtensionInterface() + { + $this->assertClassImplements(EndExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementMessageReceivedExtensionInterface() + { + $this->assertClassImplements(MessageReceivedExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementPostMessageReceivedExtensionInterface() + { + $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, LogExtension::class); + } + + public function testShouldSubClassOfLogExtension() + { + $this->assertClassExtends(\Enqueue\Consumption\Extension\LogExtension::class, LogExtension::class); + } + + public function testShouldLogStartOnStart() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Consumption has started') + ; + + $context = new Start($this->createContextMock(), $logger, [], 1, 1); + + $extension = new LogExtension(); + $extension->onStart($context); + } + + public function testShouldLogEndOnEnd() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Consumption has ended') + ; + + $context = new End($this->createContextMock(), 1, 2, $logger); + + $extension = new LogExtension(); + $extension->onEnd($context); + } + + public function testShouldLogMessageReceived() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Received from {queueName} {body}', [ + 'queueName' => 'aQueue', + 'redelivered' => false, + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + ]) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new MessageReceived($this->createContextMock(), $consumerMock, $message, $this->createProcessorMock(), 1, $logger); + + $extension = new LogExtension(); + $extension->onMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithStringResult() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'aResult', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogRejectedMessageAsError() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::ERROR, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'reject', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithReasonResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result} {reason}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => 'aReason', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedCommandMessageWithStringResult() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {command} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'aResult', + 'reason' => '', + 'command' => 'aCommand', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::COMMAND, 'aCommand'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogRejectedCommandMessageAsError() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::ERROR, + '[client] Processed {command} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'reject', + 'reason' => '', + 'command' => 'aCommand', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setProperty(Config::COMMAND, 'aCommand'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedCommandMessageWithResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {command} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => '', + 'command' => 'aCommand', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setProperty(Config::COMMAND, 'aCommand'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedCommandMessageWithReasonResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {command} {body} {result} {reason}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::COMMAND => 'aCommand']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => 'aReason', + 'command' => 'aCommand', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setProperty(Config::COMMAND, 'aCommand'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedTopicProcessorMessageWithStringResult() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {topic} -> {processor} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'aResult', + 'reason' => '', + 'topic' => 'aTopic', + 'processor' => 'aProcessor', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::TOPIC, 'aTopic'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogRejectedTopicProcessorMessageAsError() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::ERROR, + '[client] Processed {topic} -> {processor} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'reject', + 'reason' => '', + 'topic' => 'aTopic', + 'processor' => 'aProcessor', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::TOPIC, 'aTopic'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedTopicProcessorMessageWithResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {topic} -> {processor} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => '', + 'topic' => 'aTopic', + 'processor' => 'aProcessor', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::TOPIC, 'aTopic'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogProcessedTopicProcessorMessageWithReasonResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + '[client] Processed {topic} -> {processor} {body} {result} {reason}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal', Config::TOPIC => 'aTopic', Config::PROCESSOR => 'aProcessor']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => 'aReason', + 'topic' => 'aTopic', + 'processor' => 'aProcessor', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty(Config::TOPIC, 'aTopic'); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + /** + * @return MockObject + */ + private function createConsumerStub(Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } + + /** + * @return MockObject + */ + private function createContextMock(): Context + { + return $this->createMock(Context::class); + } + + /** + * @return MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return MockObject|LoggerInterface + */ + private function createLogger() + { + return $this->createMock(LoggerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php b/pkg/enqueue/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php index 6dc723c37..d521aefca 100644 --- a/pkg/enqueue/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php +++ b/pkg/enqueue/Tests/Client/ConsumptionExtension/SetRouterPropertiesExtensionTest.php @@ -5,31 +5,31 @@ use Enqueue\Client\Config; use Enqueue\Client\ConsumptionExtension\SetRouterPropertiesExtension; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\MessageReceivedExtensionInterface; use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Processor; +use Interop\Queue\Queue; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; class SetRouterPropertiesExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementMessageReceivedExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, SetRouterPropertiesExtension::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new SetRouterPropertiesExtension($this->createDriverMock()); + $this->assertClassImplements(MessageReceivedExtensionInterface::class, SetRouterPropertiesExtension::class); } public function testShouldSetRouterProcessorPropertyIfNotSetAndOnRouterQueue() { - $config = Config::create('test', '', '', 'router-queue', '', 'router-processor-name'); + $config = Config::create('test', '.', '', '', 'router-queue', '', 'router-processor-name'); $queue = new NullQueue('test.router-queue'); $driver = $this->createDriverMock(); @@ -46,17 +46,23 @@ public function testShouldSetRouterProcessorPropertyIfNotSetAndOnRouterQueue() ; $message = new NullMessage(); + $message->setProperty(Config::TOPIC, 'aTopic'); - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); - $context->setPsrQueue(new NullQueue('test.router-queue')); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(new NullQueue('test.router-queue')), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); $extension = new SetRouterPropertiesExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); $this->assertEquals([ - 'enqueue.processor_name' => 'router-processor-name', - 'enqueue.processor_queue_name' => 'router-queue', + Config::PROCESSOR => 'router-processor-name', + Config::TOPIC => 'aTopic', ], $message->getProperties()); } @@ -79,15 +85,23 @@ public function testShouldNotSetRouterProcessorPropertyIfNotSetAndNotOnRouterQue ; $message = new NullMessage(); + $message->setProperty(Config::TOPIC, 'aTopic'); - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); - $context->setPsrQueue(new NullQueue('test.another-queue')); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(new NullQueue('test.another-queue')), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); $extension = new SetRouterPropertiesExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); - $this->assertEquals([], $message->getProperties()); + $this->assertEquals([ + Config::TOPIC => 'aTopic', + ], $message->getProperties()); } public function testShouldNotSetAnyPropertyIfProcessorNamePropertyAlreadySet() @@ -99,32 +113,86 @@ public function testShouldNotSetAnyPropertyIfProcessorNamePropertyAlreadySet() ; $message = new NullMessage(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'non-router-processor'); + $message->setProperty(Config::PROCESSOR, 'non-router-processor'); - $context = new Context($this->createPsrContextMock()); - $context->setPsrMessage($message); + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); $extension = new SetRouterPropertiesExtension($driver); - $extension->onPreReceived($context); + $extension->onMessageReceived($messageReceived); $this->assertEquals([ - 'enqueue.processor_name' => 'non-router-processor', + 'enqueue.processor' => 'non-router-processor', ], $message->getProperties()); } + public function testShouldSkipMessagesWithoutTopicPropertySet() + { + $driver = $this->createDriverMock(); + $driver + ->expects($this->never()) + ->method('getConfig') + ; + + $message = new NullMessage(); + + $messageReceived = new MessageReceived( + $this->createContextMock(), + $this->createConsumerStub(null), + $message, + $this->createProcessorMock(), + 1, + new NullLogger() + ); + + $extension = new SetRouterPropertiesExtension($driver); + $extension->onMessageReceived($messageReceived); + + $this->assertEquals([], $message->getProperties()); + } + /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|InteropContext */ - protected function createPsrContextMock() + protected function createContextMock(): InteropContext { - return $this->createMock(PsrContext::class); + return $this->createMock(InteropContext::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return MockObject|DriverInterface */ - protected function createDriverMock() + protected function createDriverMock(): DriverInterface { return $this->createMock(DriverInterface::class); } + + /** + * @return MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return MockObject|Consumer + */ + private function createConsumerStub(?Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue ?? new NullQueue('queue')) + ; + + return $consumerMock; + } } diff --git a/pkg/enqueue/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php b/pkg/enqueue/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php index 21b011fe2..fbd367975 100644 --- a/pkg/enqueue/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php +++ b/pkg/enqueue/Tests/Client/ConsumptionExtension/SetupBrokerExtensionTest.php @@ -4,10 +4,11 @@ use Enqueue\Client\ConsumptionExtension\SetupBrokerExtension; use Enqueue\Client\DriverInterface; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\Context\Start; +use Enqueue\Consumption\StartExtensionInterface; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; +use Interop\Queue\Context as InteropContext; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; @@ -15,14 +16,9 @@ class SetupBrokerExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementStartExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, SetupBrokerExtension::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new SetupBrokerExtension($this->createDriverMock()); + $this->assertClassImplements(StartExtensionInterface::class, SetupBrokerExtension::class); } public function testShouldSetupBroker() @@ -36,8 +32,7 @@ public function testShouldSetupBroker() ->with($this->identicalTo($logger)) ; - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($logger); + $context = new Start($this->createMock(InteropContext::class), $logger, [], 0, 0); $extension = new SetupBrokerExtension($driver); $extension->onStart($context); @@ -54,8 +49,7 @@ public function testShouldSetupBrokerOnlyOnce() ->with($this->identicalTo($logger)) ; - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($logger); + $context = new Start($this->createMock(InteropContext::class), $logger, [], 0, 0); $extension = new SetupBrokerExtension($driver); $extension->onStart($context); @@ -63,7 +57,7 @@ public function testShouldSetupBrokerOnlyOnce() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return MockObject|DriverInterface */ private function createDriverMock() { diff --git a/pkg/enqueue/Tests/Client/DelegateProcessorTest.php b/pkg/enqueue/Tests/Client/DelegateProcessorTest.php index f107dde6f..9743cf4f3 100644 --- a/pkg/enqueue/Tests/Client/DelegateProcessorTest.php +++ b/pkg/enqueue/Tests/Client/DelegateProcessorTest.php @@ -4,36 +4,30 @@ use Enqueue\Client\Config; use Enqueue\Client\DelegateProcessor; -use Enqueue\Client\ProcessorRegistryInterface; use Enqueue\Null\NullMessage; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Enqueue\ProcessorRegistryInterface; +use Interop\Queue\Context; +use Interop\Queue\Processor; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class DelegateProcessorTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new DelegateProcessor($this->createProcessorRegistryMock()); - } - public function testShouldThrowExceptionIfProcessorNameIsNotSet() { - $this->setExpectedException( - \LogicException::class, - 'Got message without required parameter: "enqueue.processor_name"' - ); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Got message without required parameter: "enqueue.processor"'); $processor = new DelegateProcessor($this->createProcessorRegistryMock()); - $processor->process(new NullMessage(), $this->createPsrContextMock()); + $processor->process(new NullMessage(), $this->createContextMock()); } public function testShouldProcessMessage() { - $session = $this->createPsrContextMock(); + $session = $this->createContextMock(); $message = new NullMessage(); $message->setProperties([ - Config::PARAMETER_PROCESSOR_NAME => 'processor-name', + Config::PROCESSOR => 'processor-name', ]); $processor = $this->createProcessorMock(); @@ -41,7 +35,7 @@ public function testShouldProcessMessage() ->expects($this->once()) ->method('process') ->with($this->identicalTo($message), $this->identicalTo($session)) - ->will($this->returnValue('return-value')) + ->willReturn('return-value') ; $processorRegistry = $this->createProcessorRegistryMock(); @@ -49,7 +43,7 @@ public function testShouldProcessMessage() ->expects($this->once()) ->method('get') ->with('processor-name') - ->will($this->returnValue($processor)) + ->willReturn($processor) ; $processor = new DelegateProcessor($processorRegistry); @@ -59,7 +53,7 @@ public function testShouldProcessMessage() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProcessorRegistryInterface + * @return MockObject|ProcessorRegistryInterface */ protected function createProcessorRegistryMock() { @@ -67,18 +61,18 @@ protected function createProcessorRegistryMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|Context */ - protected function createPsrContextMock() + protected function createContextMock() { - return $this->createMock(PsrContext::class); + return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor + * @return MockObject|Processor */ protected function createProcessorMock() { - return $this->createMock(PsrProcessor::class); + return $this->createMock(Processor::class); } } diff --git a/pkg/enqueue/Tests/Client/Driver/AmqpDriverTest.php b/pkg/enqueue/Tests/Client/Driver/AmqpDriverTest.php new file mode 100644 index 000000000..2cfb170b9 --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/AmqpDriverTest.php @@ -0,0 +1,360 @@ +assertClassImplements(DriverInterface::class, AmqpDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, AmqpDriver::class); + } + + public function testThrowIfPriorityIsNotSupportedOnCreateTransportMessage() + { + $clientMessage = new Message(); + $clientMessage->setPriority('invalidPriority'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Cant convert client priority "invalidPriority" to transport one. Could be one of "enqueue.message_queue.client.very_low_message_priority", "enqueue.message_queue.client.low_message_priority", "enqueue.message_queue.client.normal_message_priority'); + $driver->createTransportMessage($clientMessage); + } + + public function testShouldSetExpirationHeaderFromClientMessageExpireInMillisecondsOnCreateTransportMessage() + { + $clientMessage = new Message(); + $clientMessage->setExpire(333); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var AmqpMessage $transportMessage */ + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertSame(333000, $transportMessage->getExpiration()); + $this->assertSame('333000', $transportMessage->getHeader('expiration')); + } + + public function testShouldSetPersistedDeliveryModeOnCreateTransportMessage() + { + $clientMessage = new Message(); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var AmqpMessage $transportMessage */ + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertSame(AmqpMessage::DELIVERY_MODE_PERSISTENT, $transportMessage->getDeliveryMode()); + } + + public function testShouldCreateDurableQueue() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($this->createQueue('aName')) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var AmqpQueue $queue */ + $queue = $driver->createQueue('aName'); + + $this->assertSame(AmqpQueue::FLAG_DURABLE, $queue->getFlags()); + } + + public function testShouldResetPriorityAndExpirationAndNeverCallProducerDeliveryDelayOnSendMessageToRouter() + { + $topic = $this->createTopic(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) + ; + $producer + ->expects($this->never()) + ->method('setDeliveryDelay') + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createTopic') + ->willReturn($topic) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setExpire(123); + $message->setPriority(MessagePriority::HIGH); + + $driver->sendToRouter($message); + + $this->assertNull($transportMessage->getExpiration()); + $this->assertNull($transportMessage->getPriority()); + } + + public function testShouldSetupBroker() + { + $routerTopic = $this->createTopic(''); + $routerQueue = $this->createQueue(''); + $processorWithDefaultQueue = $this->createQueue('default'); + $processorWithCustomQueue = $this->createQueue('custom'); + $context = $this->createContextMock(); + // setup router + $context + ->expects($this->at(0)) + ->method('createTopic') + ->willReturn($routerTopic) + ; + $context + ->expects($this->at(1)) + ->method('declareTopic') + ->with($this->identicalTo($routerTopic)) + ; + + $context + ->expects($this->at(2)) + ->method('createQueue') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(3)) + ->method('declareQueue') + ->with($this->identicalTo($routerQueue)) + ; + + $context + ->expects($this->at(4)) + ->method('bind') + ->with($this->isInstanceOf(AmqpBind::class)) + ; + + // setup processor with default queue + $context + ->expects($this->at(5)) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($processorWithDefaultQueue) + ; + $context + ->expects($this->at(6)) + ->method('declareQueue') + ->with($this->identicalTo($processorWithDefaultQueue)) + ; + + $context + ->expects($this->at(7)) + ->method('createQueue') + ->with($this->getCustomQueueTransportName()) + ->willReturn($processorWithCustomQueue) + ; + $context + ->expects($this->at(8)) + ->method('declareQueue') + ->with($this->identicalTo($processorWithCustomQueue)) + ; + + $driver = new AmqpDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('aTopic', Route::TOPIC, 'aProcessor'), + new Route('aCommand', Route::COMMAND, 'aProcessor', ['queue' => 'custom']), + ]) + ); + $driver->setupBroker(); + } + + public function testShouldNotDeclareSameQueues() + { + $context = $this->createContextMock(); + + // setup processor with default queue + $context + ->expects($this->any()) + ->method('createTopic') + ->willReturn($this->createTopic('')) + ; + $context + ->expects($this->any()) + ->method('createQueue') + ->willReturn($this->createQueue('custom')) + ; + $context + ->expects($this->exactly(2)) + ->method('declareQueue') + ; + + $driver = new AmqpDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('aTopic', Route::TOPIC, 'aProcessor', ['queue' => 'custom']), + new Route('aCommand', Route::COMMAND, 'aProcessor', ['queue' => 'custom']), + ]) + ); + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new AmqpDriver(...$args); + } + + /** + * @return AmqpContext + */ + protected function createContextMock(): Context + { + return $this->createMock(AmqpContext::class); + } + + /** + * @return AmqpProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(AmqpProducer::class); + } + + /** + * @return AmqpQueue + */ + protected function createQueue(string $name): InteropQueue + { + return new AmqpQueue($name); + } + + protected function createTopic(string $name): AmqpTopic + { + return new AmqpTopic($name); + } + + /** + * @return AmqpMessage + */ + protected function createMessage(): InteropMessage + { + return new AmqpMessage(); + } + + protected function getRouterTransportName(): string + { + return 'aprefix.router'; + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + Assert::assertArraySubset([ + 'hkey' => 'hval', + 'delivery_mode' => AmqpMessage::DELIVERY_MODE_PERSISTENT, + 'content_type' => 'ContentType', + 'expiration' => '123000', + 'priority' => 3, + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply_to' => 'theReplyTo', + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/DbalDriverTest.php b/pkg/enqueue/Tests/Client/Driver/DbalDriverTest.php new file mode 100644 index 000000000..554a399f9 --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/DbalDriverTest.php @@ -0,0 +1,101 @@ +assertClassImplements(DriverInterface::class, DbalDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, DbalDriver::class); + } + + public function testShouldSetupBroker() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getTableName') + ; + $context + ->expects($this->once()) + ->method('createDataBaseTable') + ; + + $driver = new DbalDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new DbalDriver(...$args); + } + + /** + * @return DbalContext + */ + protected function createContextMock(): Context + { + return $this->createMock(DbalContext::class); + } + + /** + * @return DbalProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(DbalProducer::class); + } + + /** + * @return DbalDestination + */ + protected function createQueue(string $name): InteropQueue + { + return new DbalDestination($name); + } + + /** + * @return DbalDestination + */ + protected function createTopic(string $name): InteropTopic + { + return new DbalDestination($name); + } + + /** + * @return DbalMessage + */ + protected function createMessage(): InteropMessage + { + return new DbalMessage(); + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/FsDriverTest.php b/pkg/enqueue/Tests/Client/Driver/FsDriverTest.php new file mode 100644 index 000000000..f1cd02f9b --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/FsDriverTest.php @@ -0,0 +1,125 @@ +assertClassImplements(DriverInterface::class, FsDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, FsDriver::class); + } + + public function testShouldSetupBroker() + { + $routerQueue = new FsDestination(TempFile::generate()); + + $processorQueue = new FsDestination(TempFile::generate()); + + $context = $this->createContextMock(); + // setup router + $context + ->expects($this->at(0)) + ->method('createQueue') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(1)) + ->method('declareDestination') + ->with($this->identicalTo($routerQueue)) + ; + // setup processor queue + $context + ->expects($this->at(2)) + ->method('createQueue') + ->willReturn($processorQueue) + ; + $context + ->expects($this->at(3)) + ->method('declareDestination') + ->with($this->identicalTo($processorQueue)) + ; + + $routeCollection = new RouteCollection([ + new Route('aTopic', Route::TOPIC, 'aProcessor'), + ]); + + $driver = new FsDriver( + $context, + $this->createDummyConfig(), + $routeCollection + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new FsDriver(...$args); + } + + /** + * @return FsContext + */ + protected function createContextMock(): Context + { + return $this->createMock(FsContext::class); + } + + /** + * @return FsProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(FsProducer::class); + } + + /** + * @return FsDestination + */ + protected function createQueue(string $name): InteropQueue + { + return new FsDestination(new \SplFileInfo($name)); + } + + /** + * @return FsDestination + */ + protected function createTopic(string $name): InteropTopic + { + return new FsDestination(new \SplFileInfo($name)); + } + + /** + * @return FsMessage + */ + protected function createMessage(): InteropMessage + { + return new FsMessage(); + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/GenericDriverTest.php b/pkg/enqueue/Tests/Client/Driver/GenericDriverTest.php new file mode 100644 index 000000000..78f7f6e83 --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/GenericDriverTest.php @@ -0,0 +1,83 @@ +assertClassImplements(DriverInterface::class, GenericDriver::class); + } + + protected function createDriver(...$args): DriverInterface + { + return new GenericDriver(...$args); + } + + protected function createContextMock(): Context + { + return $this->createMock(Context::class); + } + + protected function createProducerMock(): InteropProducer + { + return $this->createMock(InteropProducer::class); + } + + protected function createQueue(string $name): InteropQueue + { + return new NullQueue($name); + } + + protected function createTopic(string $name): InteropTopic + { + return new NullTopic($name); + } + + protected function createMessage(): InteropMessage + { + return new NullMessage(); + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + Assert::assertArraySubset([ + 'hkey' => 'hval', + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply_to' => 'theReplyTo', + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/GenericDriverTestsTrait.php b/pkg/enqueue/Tests/Client/Driver/GenericDriverTestsTrait.php new file mode 100644 index 000000000..d5ad498a9 --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/GenericDriverTestsTrait.php @@ -0,0 +1,1249 @@ +createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $this->assertInstanceOf(DriverInterface::class, $driver); + } + + public function testShouldReturnContextSetInConstructor() + { + $context = $this->createContextMock(); + + $driver = $this->createDriver($context, $this->createDummyConfig(), new RouteCollection([])); + + $this->assertSame($context, $driver->getContext()); + } + + public function testShouldReturnConfigObjectSetInConstructor() + { + $config = $this->createDummyConfig(); + + $driver = $this->createDriver($this->createContextMock(), $config, new RouteCollection([])); + + $this->assertSame($config, $driver->getConfig()); + } + + public function testShouldReturnRouteCollectionSetInConstructor() + { + $routeCollection = new RouteCollection([]); + + /** @var DriverInterface $driver */ + $driver = $this->createDriver($this->createContextMock(), $this->createDummyConfig(), $routeCollection); + + $this->assertSame($routeCollection, $driver->getRouteCollection()); + } + + public function testShouldCreateAndReturnQueueInstanceWithPrefixAndAppName() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getPrefixAppFooQueueTransportName()) + ->willReturn($expectedQueue) + ; + + $config = new Config( + 'aPrefix', + '.', + 'anAppName', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueue', + 'aRouterProcessor', + [], + [] + ); + + $driver = $this->createDriver($context, $config, new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateAndReturnQueueInstanceWithPrefixWithoutAppName() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getPrefixFooQueueTransportName()) + ->willReturn($expectedQueue) + ; + + $config = new Config( + 'aPrefix', + '.', + '', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueue', + 'aRouterProcessor', + [], + [] + ); + + $driver = $this->createDriver($context, $config, new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateAndReturnQueueInstanceWithAppNameAndWithoutPrefix() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getAppFooQueueTransportName()) + ->willReturn($expectedQueue) + ; + + $config = new Config( + '', + '.', + 'anAppName', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueue', + 'aRouterProcessor', + [], + [] + ); + + $driver = $this->createDriver($context, $config, new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateAndReturnQueueInstanceWithoutPrefixAndAppName() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with('afooqueue') + ->willReturn($expectedQueue) + ; + + $config = new Config( + '', + '.', + '', + 'aRouterTopicName', + 'aRouterQueueName', + 'aDefaultQueue', + 'aRouterProcessor', + [], + [] + ); + + $driver = $this->createDriver($context, $config, new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateAndReturnQueueInstance() + { + $expectedQueue = $this->createQueue('aName'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getPrefixFooQueueTransportName()) + ->willReturn($expectedQueue) + ; + + $driver = $this->createDriver($context, $this->createDummyConfig(), new RouteCollection([])); + + $queue = $driver->createQueue('aFooQueue'); + + $this->assertSame($expectedQueue, $queue); + } + + public function testShouldCreateClientMessageFromTransportOne() + { + $transportMessage = $this->createMessage(); + $transportMessage->setBody('body'); + $transportMessage->setHeaders(['hkey' => 'hval']); + $transportMessage->setProperty('pkey', 'pval'); + $transportMessage->setProperty(Config::CONTENT_TYPE, 'theContentType'); + $transportMessage->setProperty(Config::EXPIRE, '22'); + $transportMessage->setProperty(Config::PRIORITY, MessagePriority::HIGH); + $transportMessage->setProperty('enqueue.delay', '44'); + $transportMessage->setMessageId('theMessageId'); + $transportMessage->setTimestamp(1000); + $transportMessage->setReplyTo('theReplyTo'); + $transportMessage->setCorrelationId('theCorrelationId'); + + $driver = $this->createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $clientMessage = $driver->createClientMessage($transportMessage); + + $this->assertClientMessage($clientMessage); + } + + public function testShouldCreateTransportMessageFromClientOne() + { + $clientMessage = new Message(); + $clientMessage->setBody('body'); + $clientMessage->setHeaders(['hkey' => 'hval']); + $clientMessage->setProperties(['pkey' => 'pval']); + $clientMessage->setContentType('ContentType'); + $clientMessage->setExpire(123); + $clientMessage->setDelay(345); + $clientMessage->setPriority(MessagePriority::HIGH); + $clientMessage->setMessageId('theMessageId'); + $clientMessage->setTimestamp(1000); + $clientMessage->setReplyTo('theReplyTo'); + $clientMessage->setCorrelationId('theCorrelationId'); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertTransportMessage($transportMessage); + } + + public function testShouldSendMessageToRouter() + { + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->willReturnCallback(function (Destination $topic, InteropMessage $message) use ($transportMessage) { + $this->assertSame( + $this->getRouterTransportName(), + $topic instanceof InteropTopic ? $topic->getTopicName() : $topic->getQueueName()); + $this->assertSame($transportMessage, $message); + }) + ; + $context = $this->createContextStub(); + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToRouter($message); + } + + public function testShouldNotInitDeliveryDelayOnSendMessageToRouter() + { + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ; + $producer + ->expects($this->never()) + ->method('setDeliveryDelay') + ; + + $context = $this->createContextStub(); + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setDelay(456); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToRouter($message); + } + + public function testShouldNotInitTimeToLiveOnSendMessageToRouter() + { + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ; + $producer + ->expects($this->never()) + ->method('setTimeToLive') + ; + + $context = $this->createContextStub(); + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setExpire(456); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToRouter($message); + } + + public function testShouldNotInitPriorityOnSendMessageToRouter() + { + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ; + $producer + ->expects($this->never()) + ->method('setPriority') + ; + + $context = $this->createContextStub(); + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setPriority(MessagePriority::HIGH); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToRouter($message); + } + + public function testThrowIfTopicIsNotSetOnSendToRouter() + { + $driver = $this->createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Topic name parameter is required but is not set'); + + $driver->sendToRouter(new Message()); + } + + public function testThrowIfCommandSetOnSendToRouter() + { + $driver = $this->createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'aCommand'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Command must not be send to router but go directly to its processor.'); + + $driver->sendToRouter($message); + } + + public function testShouldSendMessageToRouterProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $config = $this->createDummyConfig(); + + $driver = $this->createDriver( + $context, + $config, + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', [ + 'queue' => 'custom', + ]), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, $config->getRouterProcessor()); + + $driver->sendToProcessor($message); + } + + public function testShouldSendTopicMessageToProcessorToDefaultQueue() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldSendTopicMessageToProcessorToCustomQueue() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getCustomQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldInitDeliveryDelayIfDelayPropertyOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('setDeliveryDelay') + ->with(456000) + ; + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $message = new Message(); + $message->setDelay(456); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldSetInitTimeToLiveIfExpirePropertyOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('setTimeToLive') + ->with(678000) + ; + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $message = new Message(); + $message->setExpire(678); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldSetInitPriorityIfPriorityPropertyOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('setPriority') + ->with(3) + ; + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $message = new Message(); + $message->setPriority(MessagePriority::HIGH); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testThrowIfNoRouteFoundForTopicMessageOnSendToProcessor() + { + $context = $this->createContextMock(); + $context + ->expects($this->never()) + ->method('createProducer') + ; + $context + ->expects($this->never()) + ->method('createMessage') + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('There is no route for topic "topic" and processor "processor"'); + $driver->sendToProcessor($message); + } + + public function testShouldSetRouterProcessorIfProcessorPropertyEmptyOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'expectedProcessor'), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + + $driver->sendToProcessor($message); + + $this->assertSame('router', $message->getProperty(Config::PROCESSOR)); + } + + public function testShouldSendCommandMessageToProcessorToDefaultQueue() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'processor'), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testShouldSendCommandMessageToProcessorToCustomQueue() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getCustomQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'processor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $driver->sendToProcessor($message); + } + + public function testThrowIfNoRouteFoundForCommandMessageOnSendToProcessor() + { + $context = $this->createContextMock(); + $context + ->expects($this->never()) + ->method('createProducer') + ; + $context + ->expects($this->never()) + ->method('createMessage') + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('There is no route for command "command".'); + $driver->sendToProcessor($message); + } + + public function testShouldOverwriteProcessorPropertySetByOneFromCommandRouteOnSendToProcessor() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with($this->getCustomQueueTransportName()) + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setProperty(Config::PROCESSOR, 'processorShouldBeOverwritten'); + + $driver->sendToProcessor($message); + + $this->assertSame('expectedProcessor', $message->getProperty(Config::PROCESSOR)); + } + + public function testShouldNotInitDeliveryDelayOnSendMessageToProcessorIfPropertyNull() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->never()) + ->method('setDeliveryDelay') + ; + $producer + ->expects($this->once()) + ->method('send') + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setDelay(null); + + $driver->sendToProcessor($message); + } + + public function testShouldNotInitPriorityOnSendMessageToProcessorIfPropertyNull() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->never()) + ->method('setPriority') + ; + $producer + ->expects($this->once()) + ->method('send') + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setPriority(null); + + $driver->sendToProcessor($message); + } + + public function testShouldNotInitTimeToLiveOnSendMessageToProcessorIfPropertyNull() + { + $queue = $this->createQueue(''); + $transportMessage = $this->createMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->never()) + ->method('setTimeToLive') + ; + $producer + ->expects($this->once()) + ->method('send') + ; + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('command', Route::COMMAND, 'expectedProcessor', ['queue' => 'custom']), + ]) + ); + + $message = new Message(); + $message->setProperty(Config::COMMAND, 'command'); + $message->setExpire(null); + + $driver->sendToProcessor($message); + } + + public function testThrowIfNeitherTopicNorCommandAreSentOnSendToProcessor() + { + $driver = $this->createDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Queue name parameter is required but is not set'); + + $message = new Message(); + $message->setProperty(Config::PROCESSOR, 'processor'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either topic or command parameter must be set.'); + $driver->sendToProcessor($message); + } + + abstract protected function createDriver(...$args): DriverInterface; + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + abstract protected function createContextMock(): Context; + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + abstract protected function createProducerMock(): InteropProducer; + + abstract protected function createQueue(string $name): InteropQueue; + + abstract protected function createTopic(string $name): InteropTopic; + + abstract protected function createMessage(): InteropMessage; + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + protected function createContextStub(): Context + { + $context = $this->createContextMock(); + + $context + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $name) { + return $this->createQueue($name); + }) + ; + + $context + ->expects($this->any()) + ->method('createTopic') + ->willReturnCallback(function (string $name) { + return $this->createTopic($name); + }) + ; + + return $context; + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + $this->assertEquals([ + 'hkey' => 'hval', + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply_to' => 'theReplyTo', + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } + + protected function assertClientMessage(Message $clientMessage): void + { + $this->assertSame('body', $clientMessage->getBody()); + Assert::assertArraySubset([ + 'hkey' => 'hval', + ], $clientMessage->getHeaders()); + Assert::assertArraySubset([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'theContentType', + Config::EXPIRE => '22', + Config::PRIORITY => MessagePriority::HIGH, + Config::DELAY => '44', + ], $clientMessage->getProperties()); + $this->assertSame('theMessageId', $clientMessage->getMessageId()); + $this->assertSame(22, $clientMessage->getExpire()); + $this->assertSame(44, $clientMessage->getDelay()); + $this->assertSame(MessagePriority::HIGH, $clientMessage->getPriority()); + $this->assertSame('theContentType', $clientMessage->getContentType()); + $this->assertSame(1000, $clientMessage->getTimestamp()); + $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); + } + + protected function createDummyConfig(): Config + { + return Config::create('aPrefix'); + } + + protected function getDefaultQueueTransportName(): string + { + return 'aprefix.default'; + } + + protected function getCustomQueueTransportName(): string + { + return 'aprefix.custom'; + } + + protected function getRouterTransportName(): string + { + return 'aprefix.default'; + } + + protected function getPrefixAppFooQueueTransportName(): string + { + return 'aprefix.anappname.afooqueue'; + } + + protected function getPrefixFooQueueTransportName(): string + { + return 'aprefix.afooqueue'; + } + + protected function getAppFooQueueTransportName(): string + { + return 'anappname.afooqueue'; + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/GpsDriverTest.php b/pkg/enqueue/Tests/Client/Driver/GpsDriverTest.php new file mode 100644 index 000000000..c0cac0458 --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/GpsDriverTest.php @@ -0,0 +1,142 @@ +assertClassImplements(DriverInterface::class, GpsDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, GpsDriver::class); + } + + public function testShouldSetupBroker() + { + $routerTopic = new GpsTopic(''); + $routerQueue = new GpsQueue(''); + + $processorTopic = new GpsTopic($this->getDefaultQueueTransportName()); + $processorQueue = new GpsQueue($this->getDefaultQueueTransportName()); + + $context = $this->createContextMock(); + // setup router + $context + ->expects($this->at(0)) + ->method('createTopic') + ->willReturn($routerTopic) + ; + $context + ->expects($this->at(1)) + ->method('createQueue') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(2)) + ->method('subscribe') + ->with($this->identicalTo($routerTopic), $this->identicalTo($routerQueue)) + ; + $context + ->expects($this->at(3)) + ->method('createQueue') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($processorQueue) + ; + // setup processor queue + $context + ->expects($this->at(4)) + ->method('createTopic') + ->with($this->getDefaultQueueTransportName()) + ->willReturn($processorTopic) + ; + $context + ->expects($this->at(5)) + ->method('subscribe') + ->with($this->identicalTo($processorTopic), $this->identicalTo($processorQueue)) + ; + + $driver = new GpsDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('aTopic', Route::TOPIC, 'aProcessor'), + ]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new GpsDriver(...$args); + } + + /** + * @return GpsContext + */ + protected function createContextMock(): Context + { + return $this->createMock(GpsContext::class); + } + + /** + * @return GpsProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(GpsProducer::class); + } + + /** + * @return GpsQueue + */ + protected function createQueue(string $name): InteropQueue + { + return new GpsQueue($name); + } + + /** + * @return GpsTopic + */ + protected function createTopic(string $name): InteropTopic + { + return new GpsTopic($name); + } + + /** + * @return GpsMessage + */ + protected function createMessage(): InteropMessage + { + return new GpsMessage(); + } + + protected function getRouterTransportName(): string + { + return 'aprefix.router'; + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/MongodbDriverTest.php b/pkg/enqueue/Tests/Client/Driver/MongodbDriverTest.php new file mode 100644 index 000000000..697c757d8 --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/MongodbDriverTest.php @@ -0,0 +1,105 @@ +assertClassImplements(DriverInterface::class, MongodbDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, MongodbDriver::class); + } + + public function testShouldSetupBroker() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createCollection') + ; + $context + ->expects($this->once()) + ->method('getConfig') + ->willReturn([ + 'dbname' => 'aDb', + 'collection_name' => 'aCol', + ]) + ; + + $driver = new MongodbDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new MongodbDriver(...$args); + } + + /** + * @return MongodbContext + */ + protected function createContextMock(): Context + { + return $this->createMock(MongodbContext::class); + } + + /** + * @return MongodbProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(MongodbProducer::class); + } + + /** + * @return MongodbDestination + */ + protected function createQueue(string $name): InteropQueue + { + return new MongodbDestination($name); + } + + /** + * @return MongodbDestination + */ + protected function createTopic(string $name): InteropTopic + { + return new MongodbDestination($name); + } + + /** + * @return MongodbMessage + */ + protected function createMessage(): InteropMessage + { + return new MongodbMessage(); + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/RabbitMqDriverTest.php b/pkg/enqueue/Tests/Client/Driver/RabbitMqDriverTest.php new file mode 100644 index 000000000..b209d85bc --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/RabbitMqDriverTest.php @@ -0,0 +1,139 @@ +assertClassImplements(DriverInterface::class, RabbitMqDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, RabbitMqDriver::class); + } + + public function testShouldBeSubClassOfAmqpDriver() + { + $this->assertClassExtends(AmqpDriver::class, RabbitMqDriver::class); + } + + public function testShouldCreateQueueWithMaxPriorityArgument() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($this->createQueue('aName')) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var AmqpQueue $queue */ + $queue = $driver->createQueue('aName'); + + $this->assertSame(['x-max-priority' => 4], $queue->getArguments()); + } + + protected function createDriver(...$args): DriverInterface + { + return new RabbitMqDriver(...$args); + } + + /** + * @return AmqpContext + */ + protected function createContextMock(): Context + { + return $this->createMock(AmqpContext::class); + } + + /** + * @return AmqpProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(AmqpProducer::class); + } + + /** + * @return AmqpQueue + */ + protected function createQueue(string $name): InteropQueue + { + return new AmqpQueue($name); + } + + protected function createTopic(string $name): AmqpTopic + { + return new AmqpTopic($name); + } + + /** + * @return AmqpMessage + */ + protected function createMessage(): InteropMessage + { + return new AmqpMessage(); + } + + protected function getRouterTransportName(): string + { + return 'aprefix.router'; + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + Assert::assertArraySubset([ + 'hkey' => 'hval', + 'delivery_mode' => AmqpMessage::DELIVERY_MODE_PERSISTENT, + 'content_type' => 'ContentType', + 'expiration' => '123000', + 'priority' => 3, + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply_to' => 'theReplyTo', + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/RabbitMqStompDriverTest.php b/pkg/enqueue/Tests/Client/Driver/RabbitMqStompDriverTest.php new file mode 100644 index 000000000..9fc72be1e --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/RabbitMqStompDriverTest.php @@ -0,0 +1,590 @@ +assertClassImplements(DriverInterface::class, RabbitMqStompDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, RabbitMqStompDriver::class); + } + + public function testShouldBeSubClassOfStompDriver() + { + $this->assertClassExtends(StompDriver::class, RabbitMqStompDriver::class); + } + + public function testShouldCreateAndReturnStompQueueInstance() + { + $expectedQueue = new StompDestination(ExtensionType::RABBITMQ); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->with('aprefix.afooqueue') + ->willReturn($expectedQueue) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $queue = $driver->createQueue('aFooQueue'); + + $expectedHeaders = [ + 'durable' => true, + 'auto-delete' => false, + 'exclusive' => false, + 'x-max-priority' => 4, + ]; + + $this->assertSame($expectedQueue, $queue); + $this->assertTrue($queue->isDurable()); + $this->assertFalse($queue->isAutoDelete()); + $this->assertFalse($queue->isExclusive()); + $this->assertSame($expectedHeaders, $queue->getHeaders()); + } + + public function testThrowIfClientPriorityInvalidOnCreateTransportMessage() + { + $clientMessage = new Message(); + $clientMessage->setPriority('unknown'); + + $transportMessage = new StompMessage(); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Cant convert client priority to transport: "unknown"'); + + $driver->createTransportMessage($clientMessage); + } + + public function testThrowIfDelayIsSetButDelayPluginInstalledOptionIsFalse() + { + $clientMessage = new Message(); + $clientMessage->setDelay(123); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn(new StompMessage()) + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => false] + ); + + $driver = $this->createDriver( + $context, + $config, + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message delaying is not supported. In order to use delay feature install RabbitMQ delay plugin.'); + + $driver->createTransportMessage($clientMessage); + } + + public function testShouldSetXDelayHeaderIfDelayPluginInstalledOptionIsTrue() + { + $clientMessage = new Message(); + $clientMessage->setDelay(123); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn(new StompMessage()) + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => true] + ); + + $driver = $this->createDriver( + $context, + $config, + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertSame('123000', $transportMessage->getHeader('x-delay')); + } + + public function testShouldInitDeliveryDelayIfDelayPropertyOnSendToProcessor() + { + $this->shouldSendMessageToDelayExchangeIfDelaySet(); + } + + public function shouldSendMessageToDelayExchangeIfDelaySet() + { + $queue = new StompDestination(ExtensionType::RABBITMQ); + $queue->setStompName('queueName'); + + $delayTopic = new StompDestination(ExtensionType::RABBITMQ); + $delayTopic->setStompName('delayTopic'); + + $transportMessage = new StompMessage(); + + $producer = $this->createProducerMock(); + $producer + ->expects($this->at(0)) + ->method('setDeliveryDelay') + ->with(10000) + ; + $producer + ->expects($this->at(1)) + ->method('setDeliveryDelay') + ->with(null) + ; + $producer + ->expects($this->once()) + ->method('send') + ->with($this->identicalTo($delayTopic), $this->identicalTo($transportMessage)) + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($queue) + ; + $context + ->expects($this->once()) + ->method('createTopic') + ->willReturn($delayTopic) + ; + $context + ->expects($this->once()) + ->method('createProducer') + ->willReturn($producer) + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($transportMessage) + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => true] + ); + + $driver = $this->createDriver( + $context, + $config, + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]), + $this->createManagementClientMock() + ); + + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topic'); + $message->setProperty(Config::PROCESSOR, 'processor'); + $message->setDelay(10); + + $driver->sendToProcessor($message); + } + + public function testShouldNotSetupBrokerIfManagementPluginInstalledOptionIsNotEnabled() + { + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['management_plugin_installed' => false] + ); + + $driver = $this->createDriver( + $this->createContextMock(), + $config, + new RouteCollection([]), + $this->createManagementClientMock() + ); + + $logger = new TestLogger(); + + $driver->setupBroker($logger); + + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Could not setup broker. The option `management_plugin_installed` is not enabled. Please enable that option and install rabbit management plugin' + ) + ); + } + + public function testShouldSetupBroker() + { + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]); + + $managementClient = $this->createManagementClientMock(); + $managementClient + ->expects($this->at(0)) + ->method('declareExchange') + ->with('aprefix.router', [ + 'type' => 'fanout', + 'durable' => true, + 'auto_delete' => false, + ]) + ; + $managementClient + ->expects($this->at(1)) + ->method('declareQueue') + ->with('aprefix.default', [ + 'durable' => true, + 'auto_delete' => false, + 'arguments' => [ + 'x-max-priority' => 4, + ], + ]) + ; + $managementClient + ->expects($this->at(2)) + ->method('bind') + ->with('aprefix.router', 'aprefix.default', 'aprefix.default') + ; + $managementClient + ->expects($this->at(3)) + ->method('declareQueue') + ->with('aprefix.default', [ + 'durable' => true, + 'auto_delete' => false, + 'arguments' => [ + 'x-max-priority' => 4, + ], + ]) + ; + + $contextMock = $this->createContextMock(); + $contextMock + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $name) { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_QUEUE); + $destination->setStompName($name); + + return $destination; + }) + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => false, 'management_plugin_installed' => true] + ); + + $driver = $this->createDriver( + $contextMock, + $config, + $routeCollection, + $managementClient + ); + + $logger = new TestLogger(); + + $driver->setupBroker($logger); + + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Declare router exchange: aprefix.router' + ) + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Declare router queue: aprefix.default' + ) + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Bind router queue to exchange: aprefix.default -> aprefix.router' + ) + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Declare processor queue: aprefix.default' + ) + ); + } + + public function testSetupBrokerShouldCreateDelayExchangeIfEnabled() + { + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]); + + $managementClient = $this->createManagementClientMock(); + $managementClient + ->expects($this->at(4)) + ->method('declareExchange') + ->with('aprefix.default.delayed', [ + 'type' => 'x-delayed-message', + 'durable' => true, + 'auto_delete' => false, + 'arguments' => [ + 'x-delayed-type' => 'direct', + ], + ]) + ; + $managementClient + ->expects($this->at(5)) + ->method('bind') + ->with('aprefix.default.delayed', 'aprefix.default', 'aprefix.default') + ; + + $config = Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => true, 'management_plugin_installed' => true] + ); + + $contextMock = $this->createContextMock(); + $contextMock + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $name) { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_QUEUE); + $destination->setStompName($name); + + return $destination; + }) + ; + $contextMock + ->expects($this->any()) + ->method('createTopic') + ->willReturnCallback(function (string $name) { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_TOPIC); + $destination->setStompName($name); + + return $destination; + }) + ; + + $driver = $this->createDriver( + $contextMock, + $config, + $routeCollection, + $managementClient + ); + + $logger = new TestLogger(); + + $driver->setupBroker($logger); + + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Declare delay exchange: aprefix.default.delayed' + ) + ); + self::assertTrue( + $logger->hasDebugThatContains( + '[RabbitMqStompDriver] Bind processor queue to delay exchange: aprefix.default -> aprefix.default.delayed' + ) + ); + } + + protected function createDriver(...$args): DriverInterface + { + return new RabbitMqStompDriver( + $args[0], + $args[1], + $args[2], + isset($args[3]) ? $args[3] : $this->createManagementClientMock() + ); + } + + /** + * @return StompContext + */ + protected function createContextMock(): Context + { + return $this->createMock(StompContext::class); + } + + /** + * @return StompProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(StompProducer::class); + } + + /** + * @return StompDestination + */ + protected function createQueue(string $name): InteropQueue + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_QUEUE); + $destination->setStompName($name); + + return $destination; + } + + /** + * @return StompDestination + */ + protected function createTopic(string $name): InteropTopic + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_TOPIC); + $destination->setStompName($name); + + return $destination; + } + + /** + * @return StompMessage + */ + protected function createMessage(): InteropMessage + { + return new StompMessage(); + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + $this->assertEquals([ + 'hkey' => 'hval', + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply-to' => 'theReplyTo', + 'persistent' => true, + 'correlation_id' => 'theCorrelationId', + 'expiration' => '123000', + 'priority' => 3, + 'x-delay' => '345000', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } + + protected function createDummyConfig(): Config + { + return Config::create( + 'aPrefix', + '.', + '', + null, + null, + null, + null, + ['delay_plugin_installed' => true, 'management_plugin_installed' => true] + ); + } + + protected function getRouterTransportName(): string + { + return '/topic/aprefix.router'; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createManagementClientMock(): StompManagementClient + { + return $this->createMock(StompManagementClient::class); + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/RdKafkaDriverTest.php b/pkg/enqueue/Tests/Client/Driver/RdKafkaDriverTest.php new file mode 100644 index 000000000..c5e40e71d --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/RdKafkaDriverTest.php @@ -0,0 +1,122 @@ +assertClassImplements(DriverInterface::class, RdKafkaDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, RdKafkaDriver::class); + } + + public function testShouldSetupBroker() + { + $routerTopic = new RdKafkaTopic(''); + $routerQueue = new RdKafkaTopic(''); + + $processorTopic = new RdKafkaTopic(''); + + $context = $this->createContextMock(); + + $context + ->expects($this->at(0)) + ->method('createQueue') + ->willReturn($routerTopic) + ; + $context + ->expects($this->at(1)) + ->method('createQueue') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(2)) + ->method('createQueue') + ->willReturn($processorTopic) + ; + + $driver = new RdKafkaDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new RdKafkaDriver(...$args); + } + + /** + * @return RdKafkaContext + */ + protected function createContextMock(): Context + { + return $this->createMock(RdKafkaContext::class); + } + + /** + * @return RdKafkaProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(RdKafkaProducer::class); + } + + /** + * @return RdKafkaTopic + */ + protected function createQueue(string $name): InteropQueue + { + return new RdKafkaTopic($name); + } + + protected function createTopic(string $name): RdKafkaTopic + { + return new RdKafkaTopic($name); + } + + /** + * @return RdKafkaMessage + */ + protected function createMessage(): InteropMessage + { + return new RdKafkaMessage(); + } + + /** + * @return Config + */ + private function createDummyConfig() + { + return Config::create('aPrefix'); + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/SqsDriverTest.php b/pkg/enqueue/Tests/Client/Driver/SqsDriverTest.php new file mode 100644 index 000000000..2e3005e6a --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/SqsDriverTest.php @@ -0,0 +1,153 @@ +assertClassImplements(DriverInterface::class, SqsDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, SqsDriver::class); + } + + public function testShouldSetupBroker() + { + $routerQueue = new SqsDestination(''); + $processorQueue = new SqsDestination(''); + + $context = $this->createContextMock(); + // setup router + $context + ->expects($this->at(0)) + ->method('createQueue') + ->with('aprefix_dot_default') + ->willReturn($routerQueue) + ; + $context + ->expects($this->at(1)) + ->method('declareQueue') + ->with($this->identicalTo($routerQueue)) + ; + // setup processor queue + $context + ->expects($this->at(2)) + ->method('createQueue') + ->with('aprefix_dot_default') + ->willReturn($processorQueue) + ; + $context + ->expects($this->at(3)) + ->method('declareQueue') + ->with($this->identicalTo($processorQueue)) + ; + + $driver = new SqsDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]) + ); + + $driver->setupBroker(); + } + + protected function createDriver(...$args): DriverInterface + { + return new SqsDriver(...$args); + } + + /** + * @return SqsContext + */ + protected function createContextMock(): Context + { + return $this->createMock(SqsContext::class); + } + + /** + * @return SqsProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(SqsProducer::class); + } + + /** + * @return SqsDestination + */ + protected function createQueue(string $name): InteropQueue + { + return new SqsDestination($name); + } + + /** + * @return SqsDestination + */ + protected function createTopic(string $name): InteropTopic + { + return new SqsDestination($name); + } + + /** + * @return SqsMessage + */ + protected function createMessage(): InteropMessage + { + return new SqsMessage(); + } + + protected function getPrefixAppFooQueueTransportName(): string + { + return 'aprefix_dot_anappname_dot_afooqueue'; + } + + protected function getPrefixFooQueueTransportName(): string + { + return 'aprefix_dot_afooqueue'; + } + + protected function getAppFooQueueTransportName(): string + { + return 'anappname_dot_afooqueue'; + } + + protected function getDefaultQueueTransportName(): string + { + return 'aprefix_dot_default'; + } + + protected function getCustomQueueTransportName(): string + { + return 'aprefix_dot_custom'; + } + + protected function getRouterTransportName(): string + { + return 'aprefix_dot_default'; + } +} diff --git a/pkg/enqueue/Tests/Client/Driver/StompDriverTest.php b/pkg/enqueue/Tests/Client/Driver/StompDriverTest.php new file mode 100644 index 000000000..8f777fdbd --- /dev/null +++ b/pkg/enqueue/Tests/Client/Driver/StompDriverTest.php @@ -0,0 +1,191 @@ +assertClassImplements(DriverInterface::class, StompDriver::class); + } + + public function testShouldBeSubClassOfGenericDriver() + { + $this->assertClassExtends(GenericDriver::class, StompDriver::class); + } + + public function testSetupBrokerShouldOnlyLogMessageThatStompDoesNotSupportBrokerSetup() + { + $driver = new StompDriver( + $this->createContextMock(), + $this->createDummyConfig(), + new RouteCollection([]) + ); + + $logger = $this->createLoggerMock(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('[StompDriver] Stomp protocol does not support broker configuration') + ; + + $driver->setupBroker($logger); + } + + public function testShouldCreateDurableQueue() + { + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createQueue') + ->willReturn($this->createQueue('aName')) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var StompDestination $queue */ + $queue = $driver->createQueue('aName'); + + $this->assertTrue($queue->isDurable()); + $this->assertFalse($queue->isAutoDelete()); + $this->assertFalse($queue->isExclusive()); + } + + public function testShouldSetPersistedTrueOnCreateTransportMessage() + { + $clientMessage = new Message(); + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn($this->createMessage()) + ; + + $driver = $this->createDriver( + $context, + $this->createDummyConfig(), + new RouteCollection([]) + ); + + /** @var StompMessage $transportMessage */ + $transportMessage = $driver->createTransportMessage($clientMessage); + + $this->assertTrue($transportMessage->isPersistent()); + } + + protected function createDriver(...$args): DriverInterface + { + return new StompDriver(...$args); + } + + /** + * @return StompContext + */ + protected function createContextMock(): Context + { + return $this->createMock(StompContext::class); + } + + /** + * @return StompProducer + */ + protected function createProducerMock(): InteropProducer + { + return $this->createMock(StompProducer::class); + } + + /** + * @return StompDestination + */ + protected function createQueue(string $name): InteropQueue + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_QUEUE); + $destination->setStompName($name); + + return $destination; + } + + /** + * @return StompDestination + */ + protected function createTopic(string $name): InteropTopic + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setType(StompDestination::TYPE_TOPIC); + $destination->setStompName($name); + + return $destination; + } + + /** + * @return StompMessage + */ + protected function createMessage(): InteropMessage + { + return new StompMessage(); + } + + protected function assertTransportMessage(InteropMessage $transportMessage): void + { + $this->assertSame('body', $transportMessage->getBody()); + $this->assertEquals([ + 'hkey' => 'hval', + 'message_id' => 'theMessageId', + 'timestamp' => 1000, + 'reply-to' => 'theReplyTo', + 'persistent' => true, + 'correlation_id' => 'theCorrelationId', + ], $transportMessage->getHeaders()); + $this->assertEquals([ + 'pkey' => 'pval', + Config::CONTENT_TYPE => 'ContentType', + Config::PRIORITY => MessagePriority::HIGH, + Config::EXPIRE => 123, + Config::DELAY => 345, + ], $transportMessage->getProperties()); + $this->assertSame('theMessageId', $transportMessage->getMessageId()); + $this->assertSame(1000, $transportMessage->getTimestamp()); + $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); + $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); + } + + protected function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); + } + + protected function getRouterTransportName(): string + { + return '/topic/aprefix.router'; + } +} diff --git a/pkg/stomp/Tests/Client/ManagementClientTest.php b/pkg/enqueue/Tests/Client/Driver/StompManagementClientTest.php similarity index 72% rename from pkg/stomp/Tests/Client/ManagementClientTest.php rename to pkg/enqueue/Tests/Client/Driver/StompManagementClientTest.php index cc11007c9..081a62c5f 100644 --- a/pkg/stomp/Tests/Client/ManagementClientTest.php +++ b/pkg/enqueue/Tests/Client/Driver/StompManagementClientTest.php @@ -1,14 +1,14 @@ expects($this->once()) ->method('create') ->with('vhost', 'name', ['options']) + ->willReturn([]) ; $client = $this->createClientMock(); @@ -26,7 +27,7 @@ public function testCouldDeclareExchange() ->willReturn($exchange) ; - $management = new ManagementClient($client, 'vhost'); + $management = new StompManagementClient($client, 'vhost'); $management->declareExchange('name', ['options']); } @@ -37,6 +38,7 @@ public function testCouldDeclareQueue() ->expects($this->once()) ->method('create') ->with('vhost', 'name', ['options']) + ->willReturn([]) ; $client = $this->createClientMock(); @@ -46,7 +48,7 @@ public function testCouldDeclareQueue() ->willReturn($queue) ; - $management = new ManagementClient($client, 'vhost'); + $management = new StompManagementClient($client, 'vhost'); $management->declareQueue('name', ['options']); } @@ -57,6 +59,7 @@ public function testCouldBind() ->expects($this->once()) ->method('create') ->with('vhost', 'exchange', 'queue', 'routing-key', ['arguments']) + ->willReturn([]) ; $client = $this->createClientMock(); @@ -66,19 +69,19 @@ public function testCouldBind() ->willReturn($binding) ; - $management = new ManagementClient($client, 'vhost'); + $management = new StompManagementClient($client, 'vhost'); $management->bind('exchange', 'queue', 'routing-key', ['arguments']); } public function testCouldCreateNewInstanceUsingFactory() { - $instance = ManagementClient::create('', ''); + $instance = StompManagementClient::create('', ''); - $this->assertInstanceOf(ManagementClient::class, $instance); + $this->assertInstanceOf(StompManagementClient::class, $instance); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Client + * @return \PHPUnit\Framework\MockObject\MockObject|Client */ private function createClientMock() { @@ -86,7 +89,7 @@ private function createClientMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Exchange + * @return \PHPUnit\Framework\MockObject\MockObject|Exchange */ private function createExchangeMock() { @@ -94,7 +97,7 @@ private function createExchangeMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Queue + * @return \PHPUnit\Framework\MockObject\MockObject|Queue */ private function createQueueMock() { @@ -102,7 +105,7 @@ private function createQueueMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Binding + * @return \PHPUnit\Framework\MockObject\MockObject|Binding */ private function createBindingMock() { diff --git a/pkg/enqueue/Tests/Client/DriverFactoryTest.php b/pkg/enqueue/Tests/Client/DriverFactoryTest.php new file mode 100644 index 000000000..3d9d7b9b5 --- /dev/null +++ b/pkg/enqueue/Tests/Client/DriverFactoryTest.php @@ -0,0 +1,186 @@ +assertTrue($rc->implementsInterface(DriverFactoryInterface::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(DriverFactory::class); + + $this->assertTrue($rc->isFinal()); + } + + public function testThrowIfPackageThatSupportSchemeNotInstalled() + { + $scheme = 'scheme5b7aa7d7cd213'; + $class = 'ConnectionClass5b7aa7d7cd213'; + + Resources::addDriver($class, [$scheme], [], ['thePackage', 'theOtherPackage']); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('To use given scheme "scheme5b7aa7d7cd213" a package has to be installed. Run "composer req thePackage theOtherPackage" to add it.'); + $factory = new DriverFactory(); + + $factory->create($this->createConnectionFactoryMock(), $this->createDummyConfig($scheme.'://foo'), new RouteCollection([])); + } + + public function testThrowIfSchemeIsNotKnown() + { + $scheme = 'scheme5b7aa862e70a5'; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('A given scheme "scheme5b7aa862e70a5" is not supported. Maybe it is a custom driver, make sure you registered it with "Enqueue\Client\Resources::addDriver".'); + + $factory = new DriverFactory(); + + $factory->create($this->createConnectionFactoryMock(), $this->createDummyConfig($scheme.'://foo'), new RouteCollection([])); + } + + public function testThrowIfDsnInvalid() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The DSN is invalid. It does not have scheme separator ":".'); + + $factory = new DriverFactory(); + + $factory->create($this->createConnectionFactoryMock(), $this->createDummyConfig('invalidDsn'), new RouteCollection([])); + } + + /** + * @dataProvider provideDSN + */ + public function testReturnsExpectedFactories( + string $dsn, + string $connectionFactoryClass, + string $contextClass, + array $conifg, + string $expectedDriverClass, + ) { + $connectionFactoryMock = $this->createMock($connectionFactoryClass); + $connectionFactoryMock + ->expects($this->once()) + ->method('createContext') + ->willReturn($this->createMock($contextClass)) + ; + + $driverFactory = new DriverFactory(); + + $driver = $driverFactory->create($connectionFactoryMock, $this->createDummyConfig($dsn), new RouteCollection([])); + + $this->assertInstanceOf($expectedDriverClass, $driver); + } + + public static function provideDSN() + { + yield ['null:', NullConnectionFactory::class, NullContext::class, [], GenericDriver::class]; + + yield ['amqp:', AmqpConnectionFactory::class, AmqpContext::class, [], AmqpDriver::class]; + + yield ['amqp+rabbitmq:', AmqpConnectionFactory::class, AmqpContext::class, [], RabbitMqDriver::class]; + + yield ['mysql:', DbalConnectionFactory::class, DbalContext::class, [], DbalDriver::class]; + + yield ['file:', FsConnectionFactory::class, FsContext::class, [], FsDriver::class]; + + // https://github.com/php-enqueue/enqueue-dev/issues/511 + // yield ['gearman:', GearmanConnectionFactory::class, NullContext::class, [], NullDriver::class]; + + yield ['gps:', GpsConnectionFactory::class, GpsContext::class, [], GpsDriver::class]; + + yield ['mongodb:', MongodbConnectionFactory::class, MongodbContext::class, [], MongodbDriver::class]; + + yield ['kafka:', RdKafkaConnectionFactory::class, RdKafkaContext::class, [], RdKafkaDriver::class]; + + yield ['redis:', RedisConnectionFactory::class, RedisContext::class, [], GenericDriver::class]; + + yield ['redis+predis:', RedisConnectionFactory::class, RedisContext::class, [], GenericDriver::class]; + + yield ['sqs:', SqsConnectionFactory::class, SqsContext::class, [], SqsDriver::class]; + + yield ['stomp:', StompConnectionFactory::class, StompContext::class, [], StompDriver::class]; + + yield ['stomp+rabbitmq:', StompConnectionFactory::class, StompContext::class, [], RabbitMqStompDriver::class]; + + yield ['stomp+foo+bar:', StompConnectionFactory::class, StompContext::class, [], StompDriver::class]; + + yield ['gearman:', GearmanConnectionFactory::class, GearmanContext::class, [], GenericDriver::class]; + + yield ['beanstalk:', PheanstalkConnectionFactory::class, PheanstalkContext::class, [], GenericDriver::class]; + } + + private function createDummyConfig(string $dsn): Config + { + return Config::create( + null, + null, + null, + null, + null, + null, + null, + ['dsn' => $dsn], + [] + ); + } + + private function createConnectionFactoryMock(): ConnectionFactory + { + return $this->createMock(ConnectionFactory::class); + } + + private function createConfigMock(): Config + { + return $this->createMock(Config::class); + } +} diff --git a/pkg/enqueue/Tests/Client/DriverPreSendTest.php b/pkg/enqueue/Tests/Client/DriverPreSendTest.php new file mode 100644 index 000000000..32af2a81f --- /dev/null +++ b/pkg/enqueue/Tests/Client/DriverPreSendTest.php @@ -0,0 +1,84 @@ +createProducerMock(); + $expectedDriver = $this->createDriverMock(); + + $context = new DriverPreSend( + $expectedMessage, + $expectedProducer, + $expectedDriver + ); + + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertSame($expectedProducer, $context->getProducer()); + $this->assertSame($expectedDriver, $context->getDriver()); + } + + public function testShouldAllowGetCommand() + { + $message = new Message(); + $message->setProperty(Config::COMMAND, 'theCommand'); + + $context = new DriverPreSend( + $message, + $this->createProducerMock(), + $this->createDriverMock() + ); + + $this->assertFalse($context->isEvent()); + $this->assertSame('theCommand', $context->getCommand()); + } + + public function testShouldAllowGetTopic() + { + $message = new Message(); + $message->setProperty(Config::TOPIC, 'theTopic'); + + $context = new DriverPreSend( + $message, + $this->createProducerMock(), + $this->createDriverMock() + ); + + $this->assertTrue($context->isEvent()); + $this->assertSame('theTopic', $context->getTopic()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverMock(): DriverInterface + { + return $this->createMock(DriverInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createProducerMock(): ProducerInterface + { + return $this->createMock(ProducerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Client/Extension/PrepareBodyExtensionTest.php b/pkg/enqueue/Tests/Client/Extension/PrepareBodyExtensionTest.php new file mode 100644 index 000000000..c3032ccc8 --- /dev/null +++ b/pkg/enqueue/Tests/Client/Extension/PrepareBodyExtensionTest.php @@ -0,0 +1,131 @@ +assertTrue($rc->implementsInterface(PreSendEventExtensionInterface::class)); + $this->assertTrue($rc->implementsInterface(PreSendCommandExtensionInterface::class)); + } + + /** + * @dataProvider provideMessages + * + * @param mixed|null $contentType + */ + public function testShouldSendStringUnchangedAndAddPlainTextContentTypeIfEmpty( + $body, + $contentType, + string $expectedBody, + string $expectedContentType, + ) { + $message = new Message($body); + $message->setContentType($contentType); + + $context = $this->createDummyPreSendContext('aTopic', $message); + + $extension = new PrepareBodyExtension(); + + $extension->onPreSendEvent($context); + + $this->assertSame($expectedBody, $message->getBody()); + $this->assertSame($expectedContentType, $message->getContentType()); + } + + public function testThrowIfBodyIsObject() + { + $message = new Message(new \stdClass()); + + $context = $this->createDummyPreSendContext('aTopic', $message); + + $extension = new PrepareBodyExtension(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: stdClass'); + + $extension->onPreSendEvent($context); + } + + public function testThrowIfBodyIsArrayWithObjectsInsideOnSend() + { + $message = new Message(['foo' => new \stdClass()]); + + $context = $this->createDummyPreSendContext('aTopic', $message); + + $extension = new PrepareBodyExtension(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass'); + + $extension->onPreSendEvent($context); + } + + public function testShouldThrowExceptionIfBodyIsArrayWithObjectsInSubArraysInsideOnSend() + { + $message = new Message(['foo' => ['bar' => new \stdClass()]]); + + $context = $this->createDummyPreSendContext('aTopic', $message); + + $extension = new PrepareBodyExtension(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass'); + + $extension->onPreSendEvent($context); + } + + public static function provideMessages() + { + yield ['theBody', null, 'theBody', 'text/plain']; + + yield ['theBody', 'foo/bar', 'theBody', 'foo/bar']; + + yield [12345, null, '12345', 'text/plain']; + + yield [12345, 'foo/bar', '12345', 'foo/bar']; + + yield [12.345, null, '12.345', 'text/plain']; + + yield [12.345, 'foo/bar', '12.345', 'foo/bar']; + + yield [true, null, '1', 'text/plain']; + + yield [true, 'foo/bar', '1', 'foo/bar']; + + yield [null, null, '', 'text/plain']; + + yield [null, 'foo/bar', '', 'foo/bar']; + + yield [['foo' => 'fooVal'], null, '{"foo":"fooVal"}', 'application/json']; + + yield [['foo' => 'fooVal'], 'foo/bar', '{"foo":"fooVal"}', 'foo/bar']; + + yield [new JsonSerializableObject(), null, '{"foo":"fooVal"}', 'application/json']; + + yield [new JsonSerializableObject(), 'foo/bar', '{"foo":"fooVal"}', 'foo/bar']; + } + + private function createDummyPreSendContext($commandOrTopic, $message): PreSend + { + return new PreSend( + $commandOrTopic, + $message, + $this->createMock(ProducerInterface::class), + $this->createMock(DriverInterface::class) + ); + } +} diff --git a/pkg/enqueue/Tests/Client/Meta/QueueMetaRegistryTest.php b/pkg/enqueue/Tests/Client/Meta/QueueMetaRegistryTest.php deleted file mode 100644 index 787290fe6..000000000 --- a/pkg/enqueue/Tests/Client/Meta/QueueMetaRegistryTest.php +++ /dev/null @@ -1,146 +0,0 @@ - [], - 'anotherQueueName' => [], - ]; - - $registry = new QueueMetaRegistry($this->createConfig(), $meta); - - $this->assertAttributeEquals($meta, 'meta', $registry); - } - - public function testShouldAllowAddQueueMetaUsingAddMethod() - { - $registry = new QueueMetaRegistry($this->createConfig(), []); - - $registry->add('theFooQueueName', 'theTransportQueueName'); - $registry->add('theBarQueueName'); - - $this->assertAttributeSame([ - 'theFooQueueName' => [ - 'transportName' => 'theTransportQueueName', - 'processors' => [], - ], - 'theBarQueueName' => [ - 'transportName' => null, - 'processors' => [], - ], - ], 'meta', $registry); - } - - public function testShouldAllowAddSubscriber() - { - $registry = new QueueMetaRegistry($this->createConfig(), []); - - $registry->addProcessor('theFooQueueName', 'theFooProcessorName'); - $registry->addProcessor('theFooQueueName', 'theBarProcessorName'); - $registry->addProcessor('theBarQueueName', 'theBazProcessorName'); - - $this->assertAttributeSame([ - 'theFooQueueName' => [ - 'transportName' => null, - 'processors' => ['theFooProcessorName', 'theBarProcessorName'], - ], - 'theBarQueueName' => [ - 'transportName' => null, - 'processors' => ['theBazProcessorName'], - ], - ], 'meta', $registry); - } - - public function testThrowIfThereIsNotMetaForRequestedClientQueueName() - { - $registry = new QueueMetaRegistry($this->createConfig(), []); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The queue meta not found. Requested name `aName`'); - $registry->getQueueMeta('aName'); - } - - public function testShouldAllowGetQueueByNameWithDefaultInfo() - { - $queues = [ - 'theQueueName' => [], - ]; - - $registry = new QueueMetaRegistry($this->createConfig(), $queues); - - $queue = $registry->getQueueMeta('theQueueName'); - - $this->assertInstanceOf(QueueMeta::class, $queue); - $this->assertSame('theQueueName', $queue->getClientName()); - $this->assertSame('aprefix.anappname.thequeuename', $queue->getTransportName()); - $this->assertSame([], $queue->getProcessors()); - } - - public function testShouldAllowGetQueueByNameWithCustomInfo() - { - $queues = [ - 'theClientQueueName' => ['transportName' => 'theTransportName', 'processors' => ['theSubscriber']], - ]; - - $registry = new QueueMetaRegistry($this->createConfig(), $queues); - - $queue = $registry->getQueueMeta('theClientQueueName'); - $this->assertInstanceOf(QueueMeta::class, $queue); - $this->assertSame('theClientQueueName', $queue->getClientName()); - $this->assertSame('theTransportName', $queue->getTransportName()); - $this->assertSame(['theSubscriber'], $queue->getProcessors()); - } - - public function testShouldNotAllowToOverwriteDefaultTransportNameByEmptyValue() - { - $registry = new QueueMetaRegistry($this->createConfig(), [ - 'theClientQueueName' => ['transportName' => null, 'processors' => []], - ]); - - $queue = $registry->getQueueMeta('theClientQueueName'); - $this->assertInstanceOf(QueueMeta::class, $queue); - $this->assertSame('aprefix.anappname.theclientqueuename', $queue->getTransportName()); - } - - public function testShouldAllowGetAllQueues() - { - $queues = [ - 'fooQueueName' => [], - 'barQueueName' => [], - ]; - - $registry = new QueueMetaRegistry($this->createConfig(), $queues); - - $queues = $registry->getQueuesMeta(); - $this->assertInstanceOf(\Generator::class, $queues); - - $queues = iterator_to_array($queues); - /* @var QueueMeta[] $queues */ - - $this->assertContainsOnly(QueueMeta::class, $queues); - $this->assertCount(2, $queues); - - $this->assertSame('fooQueueName', $queues[0]->getClientName()); - $this->assertSame('aprefix.anappname.fooqueuename', $queues[0]->getTransportName()); - - $this->assertSame('barQueueName', $queues[1]->getClientName()); - $this->assertSame('aprefix.anappname.barqueuename', $queues[1]->getTransportName()); - } - - /** - * @return Config - */ - private function createConfig() - { - return new Config('aPrefix', 'anAppName', 'aRouterTopic', 'aRouterQueueName', 'aDefaultQueueName', 'aRouterProcessorName'); - } -} diff --git a/pkg/enqueue/Tests/Client/Meta/QueueMetaTest.php b/pkg/enqueue/Tests/Client/Meta/QueueMetaTest.php deleted file mode 100644 index fde6dc52f..000000000 --- a/pkg/enqueue/Tests/Client/Meta/QueueMetaTest.php +++ /dev/null @@ -1,39 +0,0 @@ -assertAttributeEquals('aClientName', 'clientName', $meta); - $this->assertAttributeEquals('aTransportName', 'transportName', $meta); - $this->assertAttributeEquals([], 'processors', $meta); - } - - public function testShouldAllowGetClientNameSetInConstructor() - { - $meta = new QueueMeta('theClientName', 'aTransportName'); - - $this->assertSame('theClientName', $meta->getClientName()); - } - - public function testShouldAllowGetTransportNameSetInConstructor() - { - $meta = new QueueMeta('aClientName', 'theTransportName'); - - $this->assertSame('theTransportName', $meta->getTransportName()); - } - - public function testShouldAllowGetSubscribersSetInConstructor() - { - $meta = new QueueMeta('aClientName', 'aTransportName', ['aSubscriber']); - - $this->assertSame(['aSubscriber'], $meta->getProcessors()); - } -} diff --git a/pkg/enqueue/Tests/Client/Meta/TopicMetaRegistryTest.php b/pkg/enqueue/Tests/Client/Meta/TopicMetaRegistryTest.php deleted file mode 100644 index ce074b6b1..000000000 --- a/pkg/enqueue/Tests/Client/Meta/TopicMetaRegistryTest.php +++ /dev/null @@ -1,124 +0,0 @@ - [], - 'anotherTopicName' => [], - ]; - - $registry = new TopicMetaRegistry($topics); - - $this->assertAttributeEquals($topics, 'meta', $registry); - } - - public function testShouldAllowAddTopicMetaUsingAddMethod() - { - $registry = new TopicMetaRegistry([]); - - $registry->add('theFooTopicName', 'aDescription'); - $registry->add('theBarTopicName'); - - $this->assertAttributeSame([ - 'theFooTopicName' => [ - 'description' => 'aDescription', - 'processors' => [], - ], - 'theBarTopicName' => [ - 'description' => null, - 'processors' => [], - ], - ], 'meta', $registry); - } - - public function testShouldAllowAddSubscriber() - { - $registry = new TopicMetaRegistry([]); - - $registry->addProcessor('theFooTopicName', 'theFooProcessorName'); - $registry->addProcessor('theFooTopicName', 'theBarProcessorName'); - $registry->addProcessor('theBarTopicName', 'theBazProcessorName'); - - $this->assertAttributeSame([ - 'theFooTopicName' => [ - 'description' => null, - 'processors' => ['theFooProcessorName', 'theBarProcessorName'], - ], - 'theBarTopicName' => [ - 'description' => null, - 'processors' => ['theBazProcessorName'], - ], - ], 'meta', $registry); - } - - public function testThrowIfThereIsNotMetaForRequestedTopicName() - { - $registry = new TopicMetaRegistry([]); - - $this->setExpectedException( - \InvalidArgumentException::class, - 'The topic meta not found. Requested name `aName`' - ); - $registry->getTopicMeta('aName'); - } - - public function testShouldAllowGetTopicByNameWithDefaultInfo() - { - $topics = [ - 'theTopicName' => [], - ]; - - $registry = new TopicMetaRegistry($topics); - - $topic = $registry->getTopicMeta('theTopicName'); - $this->assertInstanceOf(TopicMeta::class, $topic); - $this->assertSame('theTopicName', $topic->getName()); - $this->assertSame('', $topic->getDescription()); - $this->assertSame([], $topic->getProcessors()); - } - - public function testShouldAllowGetTopicByNameWithCustomInfo() - { - $topics = [ - 'theTopicName' => ['description' => 'theDescription', 'processors' => ['theSubscriber']], - ]; - - $registry = new TopicMetaRegistry($topics); - - $topic = $registry->getTopicMeta('theTopicName'); - $this->assertInstanceOf(TopicMeta::class, $topic); - $this->assertSame('theTopicName', $topic->getName()); - $this->assertSame('theDescription', $topic->getDescription()); - $this->assertSame(['theSubscriber'], $topic->getProcessors()); - } - - public function testShouldAllowGetAllTopics() - { - $topics = [ - 'fooTopicName' => [], - 'barTopicName' => [], - ]; - - $registry = new TopicMetaRegistry($topics); - - $topics = $registry->getTopicsMeta(); - $this->assertInstanceOf(\Generator::class, $topics); - - $topics = iterator_to_array($topics); - /* @var TopicMeta[] $topics */ - - $this->assertContainsOnly(TopicMeta::class, $topics); - $this->assertCount(2, $topics); - - $this->assertSame('fooTopicName', $topics[0]->getName()); - $this->assertSame('barTopicName', $topics[1]->getName()); - } -} diff --git a/pkg/enqueue/Tests/Client/Meta/TopicMetaTest.php b/pkg/enqueue/Tests/Client/Meta/TopicMetaTest.php deleted file mode 100644 index 565a8f821..000000000 --- a/pkg/enqueue/Tests/Client/Meta/TopicMetaTest.php +++ /dev/null @@ -1,57 +0,0 @@ -assertAttributeEquals('aName', 'name', $topic); - $this->assertAttributeEquals('', 'description', $topic); - $this->assertAttributeEquals([], 'processors', $topic); - } - - public function testCouldBeConstructedWithNameAndDescriptionOnly() - { - $topic = new TopicMeta('aName', 'aDescription'); - - $this->assertAttributeEquals('aName', 'name', $topic); - $this->assertAttributeEquals('aDescription', 'description', $topic); - $this->assertAttributeEquals([], 'processors', $topic); - } - - public function testCouldBeConstructedWithNameAndDescriptionAndSubscribers() - { - $topic = new TopicMeta('aName', 'aDescription', ['aSubscriber']); - - $this->assertAttributeEquals('aName', 'name', $topic); - $this->assertAttributeEquals('aDescription', 'description', $topic); - $this->assertAttributeEquals(['aSubscriber'], 'processors', $topic); - } - - public function testShouldAllowGetNameSetInConstructor() - { - $topic = new TopicMeta('theName', 'aDescription'); - - $this->assertSame('theName', $topic->getName()); - } - - public function testShouldAllowGetDescriptionSetInConstructor() - { - $topic = new TopicMeta('aName', 'theDescription'); - - $this->assertSame('theDescription', $topic->getDescription()); - } - - public function testShouldAllowGetSubscribersSetInConstructor() - { - $topic = new TopicMeta('aName', '', ['aSubscriber']); - - $this->assertSame(['aSubscriber'], $topic->getProcessors()); - } -} diff --git a/pkg/enqueue/Tests/Client/PostSendTest.php b/pkg/enqueue/Tests/Client/PostSendTest.php new file mode 100644 index 000000000..ba51710e7 --- /dev/null +++ b/pkg/enqueue/Tests/Client/PostSendTest.php @@ -0,0 +1,112 @@ +createProducerMock(); + $expectedDriver = $this->createDriverMock(); + $expectedDestination = $this->createDestinationMock(); + $expectedTransportMessage = $this->createTransportMessageMock(); + + $context = new PostSend( + $expectedMessage, + $expectedProducer, + $expectedDriver, + $expectedDestination, + $expectedTransportMessage + ); + + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertSame($expectedProducer, $context->getProducer()); + $this->assertSame($expectedDriver, $context->getDriver()); + $this->assertSame($expectedDestination, $context->getTransportDestination()); + $this->assertSame($expectedTransportMessage, $context->getTransportMessage()); + } + + public function testShouldAllowGetCommand() + { + $message = new Message(); + $message->setProperty(Config::COMMAND, 'theCommand'); + + $context = new PostSend( + $message, + $this->createProducerMock(), + $this->createDriverMock(), + $this->createDestinationMock(), + $this->createTransportMessageMock() + ); + + $this->assertFalse($context->isEvent()); + $this->assertSame('theCommand', $context->getCommand()); + } + + public function testShouldAllowGetTopic() + { + $message = new Message(); + $message->setProperty(Config::TOPIC, 'theTopic'); + + $context = new PostSend( + $message, + $this->createProducerMock(), + $this->createDriverMock(), + $this->createDestinationMock(), + $this->createTransportMessageMock() + ); + + $this->assertTrue($context->isEvent()); + $this->assertSame('theTopic', $context->getTopic()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverMock(): DriverInterface + { + return $this->createMock(DriverInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createProducerMock(): ProducerInterface + { + return $this->createMock(ProducerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|Destination + */ + private function createDestinationMock(): Destination + { + return $this->createMock(Destination::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|TransportMessage + */ + private function createTransportMessageMock(): TransportMessage + { + return $this->createMock(TransportMessage::class); + } +} diff --git a/pkg/enqueue/Tests/Client/PreSendTest.php b/pkg/enqueue/Tests/Client/PreSendTest.php new file mode 100644 index 000000000..01a7e5055 --- /dev/null +++ b/pkg/enqueue/Tests/Client/PreSendTest.php @@ -0,0 +1,116 @@ +createProducerMock(); + $expectedDriver = $this->createDriverMock(); + + $context = new PreSend( + $expectedCommandOrTopic, + $expectedMessage, + $expectedProducer, + $expectedDriver + ); + + $this->assertSame($expectedCommandOrTopic, $context->getTopic()); + $this->assertSame($expectedCommandOrTopic, $context->getCommand()); + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertSame($expectedProducer, $context->getProducer()); + $this->assertSame($expectedDriver, $context->getDriver()); + + $this->assertEquals($expectedMessage, $context->getOriginalMessage()); + $this->assertNotSame($expectedMessage, $context->getOriginalMessage()); + } + + public function testCouldChangeTopic() + { + $context = new PreSend( + 'aCommandOrTopic', + new Message(), + $this->createProducerMock(), + $this->createDriverMock() + ); + + // guard + $this->assertSame('aCommandOrTopic', $context->getTopic()); + + $context->changeTopic('theChangedTopic'); + + $this->assertSame('theChangedTopic', $context->getTopic()); + } + + public function testCouldChangeCommand() + { + $context = new PreSend( + 'aCommandOrTopic', + new Message(), + $this->createProducerMock(), + $this->createDriverMock() + ); + + // guard + $this->assertSame('aCommandOrTopic', $context->getCommand()); + + $context->changeCommand('theChangedCommand'); + + $this->assertSame('theChangedCommand', $context->getCommand()); + } + + public function testCouldChangeBody() + { + $context = new PreSend( + 'aCommandOrTopic', + new Message('aBody'), + $this->createProducerMock(), + $this->createDriverMock() + ); + + // guard + $this->assertSame('aBody', $context->getMessage()->getBody()); + $this->assertNull($context->getMessage()->getContentType()); + + $context->changeBody('theChangedBody'); + $this->assertSame('theChangedBody', $context->getMessage()->getBody()); + $this->assertNull($context->getMessage()->getContentType()); + + $context->changeBody('theChangedBodyAgain', 'foo/bar'); + $this->assertSame('theChangedBodyAgain', $context->getMessage()->getBody()); + $this->assertSame('foo/bar', $context->getMessage()->getContentType()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverMock(): DriverInterface + { + return $this->createMock(DriverInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createProducerMock(): ProducerInterface + { + return $this->createMock(ProducerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Client/ProducerSendCommandTest.php b/pkg/enqueue/Tests/Client/ProducerSendCommandTest.php new file mode 100644 index 000000000..9500e9d62 --- /dev/null +++ b/pkg/enqueue/Tests/Client/ProducerSendCommandTest.php @@ -0,0 +1,537 @@ +createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + $expectedProperties = [ + 'enqueue.command' => 'command', + ]; + + self::assertEquals($expectedProperties, $message->getProperties()); + } + + public function testShouldSendCommandWithReply() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + + $expectedPromiseMock = $this->createMock(Promise::class); + + $rpcFactoryMock = $this->createRpcFactoryMock(); + $rpcFactoryMock + ->expects($this->once()) + ->method('createReplyTo') + ->willReturn('theReplyQueue') + ; + $rpcFactoryMock + ->expects($this->once()) + ->method('createPromise') + ->with( + 'theReplyQueue', + $this->logicalNot($this->isEmpty()), + 60000 + ) + ->willReturn($expectedPromiseMock) + ; + + $producer = new Producer($driver, $rpcFactoryMock); + $actualPromise = $producer->sendCommand('command', $message, true); + + $this->assertSame($expectedPromiseMock, $actualPromise); + + self::assertEquals('theReplyQueue', $message->getReplyTo()); + self::assertNotEmpty($message->getCorrelationId()); + } + + public function testShouldSendCommandWithReplyAndCustomReplyQueueAndCorrelationId() + { + $message = new Message(); + $message->setReplyTo('theCustomReplyQueue'); + $message->setCorrelationId('theCustomCorrelationId'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + + $expectedPromiseMock = $this->createMock(Promise::class); + + $rpcFactoryMock = $this->createRpcFactoryMock(); + $rpcFactoryMock + ->expects($this->never()) + ->method('createReplyTo') + ; + $rpcFactoryMock + ->expects($this->once()) + ->method('createPromise') + ->with( + 'theCustomReplyQueue', + 'theCustomCorrelationId', + 60000 + ) + ->willReturn($expectedPromiseMock) + ; + + $producer = new Producer($driver, $rpcFactoryMock); + $actualPromise = $producer->sendCommand('command', $message, true); + + $this->assertSame($expectedPromiseMock, $actualPromise); + + self::assertEquals('theCustomReplyQueue', $message->getReplyTo()); + self::assertSame('theCustomCorrelationId', $message->getCorrelationId()); + } + + public function testShouldOverwriteExpectedMessageProperties() + { + $message = new Message(); + $message->setProperty(Config::COMMAND, 'commandShouldBeOverwritten'); + $message->setScope('scopeShouldBeOverwritten'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('expectedCommand', $message); + + $expectedProperties = [ + 'enqueue.command' => 'expectedCommand', + ]; + + self::assertEquals($expectedProperties, $message->getProperties()); + self::assertSame(Message::SCOPE_APP, $message->getScope()); + } + + public function testShouldSendCommandWithoutPriorityByDefault() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertNull($message->getPriority()); + } + + public function testShouldSendCommandWithCustomPriority() + { + $message = new Message(); + $message->setPriority(MessagePriority::HIGH); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertSame(MessagePriority::HIGH, $message->getPriority()); + } + + public function testShouldSendCommandWithGeneratedMessageId() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertNotEmpty($message->getMessageId()); + } + + public function testShouldSendCommandWithCustomMessageId() + { + $message = new Message(); + $message->setMessageId('theCustomMessageId'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertSame('theCustomMessageId', $message->getMessageId()); + } + + public function testShouldSendCommandWithGeneratedTimestamp() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertNotEmpty($message->getTimestamp()); + } + + public function testShouldSendCommandWithCustomTimestamp() + { + $message = new Message(); + $message->setTimestamp('theCustomTimestamp'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + + self::assertSame('theCustomTimestamp', $message->getTimestamp()); + } + + public function testShouldSerializeMessageToJsonByDefault() + { + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturnCallback(function (Message $message) { + $this->assertSame('{"foo":"fooVal"}', $message->getBody()); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', ['foo' => 'fooVal']); + } + + public function testShouldSerializeMessageByCustomExtension() + { + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturnCallback(function (Message $message) { + $this->assertSame('theCommandBodySerializedByCustomExtension', $message->getBody()); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock(), new ChainExtension([new CustomPrepareBodyClientExtension()])); + $producer->sendCommand('command', ['foo' => 'fooVal']); + } + + public function testShouldSendCommandToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturnCallback(function (Message $message) { + self::assertSame('aBody', $message->getBody()); + self::assertNull($message->getProperty(Config::PROCESSOR)); + self::assertSame('command', $message->getProperty(Config::COMMAND)); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendCommand('command', $message); + } + + public function testThrowWhenProcessorNamePropertySetToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + $message->setProperty(Config::PROCESSOR, 'aCustomProcessor'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The enqueue.processor property must not be set.'); + $producer->sendCommand('command', $message); + } + + public function testShouldCallPreSendCommandExtensionMethodWhenSendToBus() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_MESSAGE_BUS); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPreSendCommand') + ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('command', $context->getCommand()); + + $this->assertEquals($message, $context->getOriginalMessage()); + $this->assertNotSame($message, $context->getOriginalMessage()); + }); + + $extension + ->expects($this->never()) + ->method('onPreSendEvent') + ; + + $producer->sendCommand('command', $message); + } + + public function testShouldCallPreSendCommandExtensionMethodWhenSendToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPreSendCommand') + ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('command', $context->getCommand()); + + $this->assertEquals($message, $context->getOriginalMessage()); + $this->assertNotSame($message, $context->getOriginalMessage()); + }); + + $extension + ->expects($this->never()) + ->method('onPreSendEvent') + ; + + $producer->sendCommand('command', $message); + } + + public function testShouldCallPreDriverSendExtensionMethod() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onDriverPreSend') + ->willReturnCallback(function (DriverPreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('command', $context->getCommand()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendCommand('command', $message); + } + + public function testShouldCallPostSendExtensionMethod() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPostSend') + ->willReturnCallback(function (PostSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('command', $context->getCommand()); + + $this->assertFalse($context->isEvent()); + }); + + $producer->sendCommand('command', $message); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createRpcFactoryMock(): RpcFactory + { + return $this->createMock(RpcFactory::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverStub(): DriverInterface + { + $config = new Config( + 'a_prefix', + '.', + 'an_app', + 'a_router_topic', + 'a_router_queue', + 'a_default_processor_queue', + 'a_router_processor_name', + [], + [] + ); + + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn($config) + ; + + return $driverMock; + } + + private function createDriverSendResult(): DriverSendResult + { + return new DriverSendResult( + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); + } +} diff --git a/pkg/enqueue/Tests/Client/ProducerSendEventTest.php b/pkg/enqueue/Tests/Client/ProducerSendEventTest.php new file mode 100644 index 000000000..c92b49560 --- /dev/null +++ b/pkg/enqueue/Tests/Client/ProducerSendEventTest.php @@ -0,0 +1,557 @@ +createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + $expectedProperties = [ + 'enqueue.topic' => 'topic', + ]; + + self::assertEquals($expectedProperties, $message->getProperties()); + } + + public function testShouldOverwriteTopicProperty() + { + $message = new Message(); + $message->setProperty(Config::TOPIC, 'topicShouldBeOverwritten'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('expectedTopic', $message); + + $expectedProperties = [ + 'enqueue.topic' => 'expectedTopic', + ]; + + self::assertEquals($expectedProperties, $message->getProperties()); + } + + public function testShouldSendEventWithoutPriorityByDefault() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertNull($message->getPriority()); + } + + public function testShouldSendEventWithCustomPriority() + { + $message = new Message(); + $message->setPriority(MessagePriority::HIGH); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertSame(MessagePriority::HIGH, $message->getPriority()); + } + + public function testShouldSendEventWithGeneratedMessageId() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertNotEmpty($message->getMessageId()); + } + + public function testShouldSendEventWithCustomMessageId() + { + $message = new Message(); + $message->setMessageId('theCustomMessageId'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertSame('theCustomMessageId', $message->getMessageId()); + } + + public function testShouldSendEventWithGeneratedTimestamp() + { + $message = new Message(); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertNotEmpty($message->getTimestamp()); + } + + public function testShouldSendEventWithCustomTimestamp() + { + $message = new Message(); + $message->setTimestamp('theCustomTimestamp'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->with(self::identicalTo($message)) + ->willReturn($this->createDriverSendResult()) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + + self::assertSame('theCustomTimestamp', $message->getTimestamp()); + } + + public function testShouldSerializeMessageToJsonByDefault() + { + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturnCallback(function (Message $message) { + $this->assertSame('{"foo":"fooVal"}', $message->getBody()); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', ['foo' => 'fooVal']); + } + + public function testShouldSerializeMessageByCustomExtension() + { + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturnCallback(function (Message $message) { + $this->assertSame('theEventBodySerializedByCustomExtension', $message->getBody()); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock(), new ChainExtension([new CustomPrepareBodyClientExtension()])); + $producer->sendEvent('topic', ['foo' => 'fooVal']); + } + + public function testThrowIfSendEventToMessageBusWithProcessorNamePropertySet() + { + $message = new Message(); + $message->setBody(''); + $message->setProperty(Config::PROCESSOR, 'aProcessor'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The enqueue.processor property must not be set.'); + $producer->sendEvent('topic', $message); + } + + public function testShouldSendEventToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturnCallback(function (Message $message) { + self::assertSame('aBody', $message->getBody()); + + // null means a driver sends a message to router processor. + self::assertNull($message->getProperty(Config::PROCESSOR)); + + return $this->createDriverSendResult(); + }) + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + $producer->sendEvent('topic', $message); + } + + public function testThrowWhenProcessorNamePropertySetToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + $message->setProperty(Config::PROCESSOR, 'aCustomProcessor'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The enqueue.processor property must not be set.'); + $producer->sendEvent('topic', $message); + } + + public function testThrowIfUnSupportedScopeGivenOnSend() + { + $message = new Message(); + $message->setScope('iDontKnowScope'); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->never()) + ->method('sendToRouter') + ; + $driver + ->expects($this->never()) + ->method('sendToProcessor') + ; + + $producer = new Producer($driver, $this->createRpcFactoryMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message scope "iDontKnowScope" is not supported.'); + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPreSendEventExtensionMethodWhenSendToBus() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_MESSAGE_BUS); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPreSendEvent') + ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertEquals($message, $context->getOriginalMessage()); + $this->assertNotSame($message, $context->getOriginalMessage()); + }); + + $extension + ->expects($this->never()) + ->method('onPreSendCommand') + ; + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPreSendEventExtensionMethodWhenSendToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPreSendEvent') + ->willReturnCallback(function (PreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertEquals($message, $context->getOriginalMessage()); + $this->assertNotSame($message, $context->getOriginalMessage()); + }); + + $extension + ->expects($this->never()) + ->method('onPreSendCommand') + ; + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPreDriverSendExtensionMethodWhenSendToMessageBus() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_MESSAGE_BUS); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onDriverPreSend') + ->willReturnCallback(function (DriverPreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPreDriverSendExtensionMethodWhenSendToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onDriverPreSend') + ->willReturnCallback(function (DriverPreSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPostSendExtensionMethodWhenSendToMessageBus() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_MESSAGE_BUS); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToRouter') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPostSend') + ->willReturnCallback(function (PostSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendEvent('topic', $message); + } + + public function testShouldCallPostSendExtensionMethodWhenSendToApplicationRouter() + { + $message = new Message(); + $message->setBody('aBody'); + $message->setScope(Message::SCOPE_APP); + + $driver = $this->createDriverStub(); + $driver + ->expects($this->once()) + ->method('sendToProcessor') + ->willReturn($this->createDriverSendResult()) + ; + + $extension = $this->createMock(ExtensionInterface::class); + + $producer = new Producer($driver, $this->createRpcFactoryMock(), $extension); + + $extension + ->expects($this->at(0)) + ->method('onPostSend') + ->willReturnCallback(function (PostSend $context) use ($message, $producer, $driver) { + $this->assertSame($message, $context->getMessage()); + $this->assertSame($producer, $context->getProducer()); + $this->assertSame($driver, $context->getDriver()); + $this->assertSame('topic', $context->getTopic()); + + $this->assertTrue($context->isEvent()); + }); + + $producer->sendEvent('topic', $message); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createRpcFactoryMock(): RpcFactory + { + return $this->createMock(RpcFactory::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverStub(): DriverInterface + { + $config = new Config( + 'a_prefix', + '.', + 'an_app', + 'a_router_topic', + 'a_router_queue', + 'a_default_processor_queue', + 'a_router_processor_name', + [], + [] + ); + + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn($config) + ; + + return $driverMock; + } + + private function createDriverSendResult(): DriverSendResult + { + return new DriverSendResult( + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); + } +} diff --git a/pkg/enqueue/Tests/Client/ProducerTest.php b/pkg/enqueue/Tests/Client/ProducerTest.php index 0716dc50d..23b004ac3 100644 --- a/pkg/enqueue/Tests/Client/ProducerTest.php +++ b/pkg/enqueue/Tests/Client/ProducerTest.php @@ -2,14 +2,9 @@ namespace Enqueue\Tests\Client; -use Enqueue\Client\Config; use Enqueue\Client\DriverInterface; -use Enqueue\Client\ExtensionInterface; -use Enqueue\Client\Message; -use Enqueue\Client\MessagePriority; use Enqueue\Client\Producer; use Enqueue\Client\ProducerInterface; -use Enqueue\Null\NullQueue; use Enqueue\Rpc\RpcFactory; use Enqueue\Test\ClassExtensionTrait; use PHPUnit\Framework\TestCase; @@ -23,620 +18,24 @@ public function testShouldImplementProducerInterface() self::assertClassImplements(ProducerInterface::class, Producer::class); } - public function testCouldBeConstructedWithDriverAsFirstArgument() + public function testShouldBeFinal() { - new Producer($this->createDriverStub(), $this->createRpcFactory()); - } - - public function testShouldSendMessageToRouter() - { - $message = new Message(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - $expectedProperties = [ - 'enqueue.topic_name' => 'topic', - 'enqueue.topic' => 'topic', - ]; - - self::assertEquals($expectedProperties, $message->getProperties()); - } - - public function testShouldSendMessageWithNormalPriorityByDefault() - { - $message = new Message(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertSame(MessagePriority::NORMAL, $message->getPriority()); - } - - public function testShouldSendMessageWithCustomPriority() - { - $message = new Message(); - $message->setPriority(MessagePriority::HIGH); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertSame(MessagePriority::HIGH, $message->getPriority()); - } - - public function testShouldSendMessageWithGeneratedMessageId() - { - $message = new Message(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertNotEmpty($message->getMessageId()); - } - - public function testShouldSendMessageWithCustomMessageId() - { - $message = new Message(); - $message->setMessageId('theCustomMessageId'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertSame('theCustomMessageId', $message->getMessageId()); - } - - public function testShouldSendMessageWithGeneratedTimestamp() - { - $message = new Message(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertNotEmpty($message->getTimestamp()); - } - - public function testShouldSendMessageWithCustomTimestamp() - { - $message = new Message(); - $message->setTimestamp('theCustomTimestamp'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->with(self::identicalTo($message)) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - - self::assertSame('theCustomTimestamp', $message->getTimestamp()); - } - - public function testShouldSendStringAsPlainText() - { - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('theStringMessage', $message->getBody()); - self::assertSame('text/plain', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', 'theStringMessage'); - } - - public function testShouldSendArrayAsJsonString() - { - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('{"foo":"fooVal"}', $message->getBody()); - self::assertSame('application/json', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', ['foo' => 'fooVal']); - } - - public function testShouldConvertMessageArrayBodyJsonString() - { - $message = new Message(); - $message->setBody(['foo' => 'fooVal']); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('{"foo":"fooVal"}', $message->getBody()); - self::assertSame('application/json', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testSendShouldForceScalarsToStringAndSetTextContentType() - { - $queue = new NullQueue(''); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertEquals('text/plain', $message->getContentType()); - - self::assertInternalType('string', $message->getBody()); - self::assertEquals('12345', $message->getBody()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent($queue, 12345); - } - - public function testSendShouldForceMessageScalarsBodyToStringAndSetTextContentType() - { - $queue = new NullQueue(''); - - $message = new Message(); - $message->setBody(12345); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertEquals('text/plain', $message->getContentType()); - - self::assertInternalType('string', $message->getBody()); - self::assertEquals('12345', $message->getBody()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent($queue, $message); - } - - public function testSendShouldForceNullToEmptyStringAndSetTextContentType() - { - $queue = new NullQueue(''); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertEquals('text/plain', $message->getContentType()); - - self::assertInternalType('string', $message->getBody()); - self::assertEquals('', $message->getBody()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent($queue, null); - } - - public function testSendShouldForceNullBodyToEmptyStringAndSetTextContentType() - { - $queue = new NullQueue(''); - - $message = new Message(); - $message->setBody(null); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertEquals('text/plain', $message->getContentType()); - - self::assertInternalType('string', $message->getBody()); - self::assertEquals('', $message->getBody()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent($queue, $message); - } - - public function testShouldThrowExceptionIfBodyIsObjectOnSend() - { - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The message\'s body must be either null, scalar, array or object (implements \JsonSerializable). Got: stdClass'); - - $producer->sendEvent('topic', new \stdClass()); - } - - public function testShouldThrowExceptionIfBodyIsArrayWithObjectsInsideOnSend() - { - $queue = new NullQueue('queue'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass'); - - $producer->sendEvent($queue, ['foo' => new \stdClass()]); - } - - public function testShouldThrowExceptionIfBodyIsArrayWithObjectsInSubArraysInsideOnSend() - { - $queue = new NullQueue('queue'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The message\'s body must be an array of scalars. Found not scalar in the array: stdClass'); - - $producer->sendEvent($queue, ['foo' => ['bar' => new \stdClass()]]); - } - - public function testShouldSendJsonSerializableObjectAsJsonStringToMessageBus() - { - $object = new JsonSerializableObject(); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('{"foo":"fooVal"}', $message->getBody()); - self::assertSame('application/json', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $object); - } - - public function testShouldSendMessageJsonSerializableBodyAsJsonStringToMessageBus() - { - $object = new JsonSerializableObject(); - - $message = new Message(); - $message->setBody($object); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ->willReturnCallback(function (Message $message) { - self::assertSame('{"foo":"fooVal"}', $message->getBody()); - self::assertSame('application/json', $message->getContentType()); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testThrowIfTryToSendMessageToMessageBusWithProcessorNamePropertySet() - { - $object = new JsonSerializableObject(); - - $message = new Message(); - $message->setBody($object); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'aProcessor'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The enqueue.processor_name property must not be set for messages that are sent to message bus.'); - $producer->sendEvent('topic', $message); - } - - public function testThrowIfTryToSendMessageToMessageBusWithProcessorQueueNamePropertySet() - { - $object = new JsonSerializableObject(); - - $message = new Message(); - $message->setBody($object); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aProcessorQueue'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The enqueue.processor_queue_name property must not be set for messages that are sent to message bus.'); - $producer->sendEvent('topic', $message); - } - - public function testThrowIfNotApplicationJsonContentTypeSetWithJsonSerializableBody() - { - $object = new JsonSerializableObject(); - - $message = new Message(); - $message->setBody($object); - $message->setContentType('foo/bar'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Content type "application/json" only allowed when body is array'); - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testShouldSendMessageToApplicationRouter() - { - $message = new Message(); - $message->setBody('aBody'); - $message->setScope(Message::SCOPE_APP); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->once()) - ->method('sendToProcessor') - ->willReturnCallback(function (Message $message) { - self::assertSame('aBody', $message->getBody()); - self::assertSame('a_router_processor_name', $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)); - self::assertSame('a_router_queue', $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testShouldSendToCustomMessageToApplicationRouter() - { - $message = new Message(); - $message->setBody('aBody'); - $message->setScope(Message::SCOPE_APP); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'aCustomProcessor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aCustomProcessorQueue'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->once()) - ->method('sendToProcessor') - ->willReturnCallback(function (Message $message) { - self::assertSame('aBody', $message->getBody()); - self::assertSame('aCustomProcessor', $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)); - self::assertSame('aCustomProcessorQueue', $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)); - }) - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - $producer->sendEvent('topic', $message); - } - - public function testThrowIfUnSupportedScopeGivenOnSend() - { - $message = new Message(); - $message->setScope('iDontKnowScope'); - - $driver = $this->createDriverStub(); - $driver - ->expects($this->never()) - ->method('sendToRouter') - ; - $driver - ->expects($this->never()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory()); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The message scope "iDontKnowScope" is not supported.'); - $producer->sendEvent('topic', $message); - } - - public function testShouldCallPreSendPostSendExtensionMethodsWhenSendToRouter() - { - $message = new Message(); - $message->setBody('aBody'); - $message->setScope(Message::SCOPE_MESSAGE_BUS); - - $extension = $this->createMock(ExtensionInterface::class); - $extension - ->expects($this->at(0)) - ->method('onPreSend') - ->with($this->identicalTo('topic'), $this->identicalTo($message)) - ; - $extension - ->expects($this->at(1)) - ->method('onPostSend') - ->with($this->identicalTo('topic'), $this->identicalTo($message)) - ; - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToRouter') - ; - - $producer = new Producer($driver, $this->createRpcFactory(), $extension); - $producer->sendEvent('topic', $message); - } - - public function testShouldCallPreSendPostSendExtensionMethodsWhenSendToProcessor() - { - $message = new Message(); - $message->setBody('aBody'); - $message->setScope(Message::SCOPE_APP); - - $extension = $this->createMock(ExtensionInterface::class); - $extension - ->expects($this->at(0)) - ->method('onPreSend') - ->with($this->identicalTo('topic'), $this->identicalTo($message)) - ; - $extension - ->expects($this->at(1)) - ->method('onPostSend') - ->with($this->identicalTo('topic'), $this->identicalTo($message)) - ; - - $driver = $this->createDriverStub(); - $driver - ->expects($this->once()) - ->method('sendToProcessor') - ; - - $producer = new Producer($driver, $this->createRpcFactory(), $extension); - $producer->sendEvent('topic', $message); + self::assertClassFinal(Producer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|RpcFactory + * @return \PHPUnit\Framework\MockObject\MockObject */ - private function createRpcFactory() + private function createRpcFactoryMock(): RpcFactory { return $this->createMock(RpcFactory::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return \PHPUnit\Framework\MockObject\MockObject */ - private function createDriverStub() - { - $config = new Config( - 'a_prefix', - 'an_app', - 'a_router_topic', - 'a_router_queue', - 'a_default_processor_queue', - 'a_router_processor_name' - ); - - $driverMock = $this->createMock(DriverInterface::class); - $driverMock - ->expects($this->any()) - ->method('getConfig') - ->willReturn($config) - ; - - return $driverMock; - } -} - -class JsonSerializableObject implements \JsonSerializable -{ - public function jsonSerialize() + private function createDriverMock(): DriverInterface { - return ['foo' => 'fooVal']; + return $this->createMock(DriverInterface::class); } } diff --git a/pkg/enqueue/Tests/Client/ResourcesTest.php b/pkg/enqueue/Tests/Client/ResourcesTest.php new file mode 100644 index 000000000..e79fb9dda --- /dev/null +++ b/pkg/enqueue/Tests/Client/ResourcesTest.php @@ -0,0 +1,159 @@ +assertTrue($rc->isFinal()); + } + + public function testShouldConstructorBePrivate() + { + $rc = new \ReflectionClass(Resources::class); + + $this->assertTrue($rc->getConstructor()->isPrivate()); + } + + public function testShouldGetAvailableDriverInExpectedFormat() + { + $availableDrivers = Resources::getAvailableDrivers(); + + self::assertIsArray($availableDrivers); + $this->assertGreaterThan(0, count($availableDrivers)); + + $driverInfo = $availableDrivers[0]; + + $this->assertArrayHasKey('driverClass', $driverInfo); + $this->assertSame(AmqpDriver::class, $driverInfo['driverClass']); + + $this->assertArrayHasKey('schemes', $driverInfo); + $this->assertSame(['amqp', 'amqps'], $driverInfo['schemes']); + + $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo); + $this->assertSame([], $driverInfo['requiredSchemeExtensions']); + + $this->assertArrayHasKey('packages', $driverInfo); + $this->assertSame(['enqueue/enqueue', 'enqueue/amqp-bunny'], $driverInfo['packages']); + } + + public function testShouldGetAvailableDriverWithRequiredExtensionInExpectedFormat() + { + $availableDrivers = Resources::getAvailableDrivers(); + + self::assertIsArray($availableDrivers); + $this->assertGreaterThan(0, count($availableDrivers)); + + $driverInfo = $availableDrivers[1]; + + $this->assertArrayHasKey('driverClass', $driverInfo); + $this->assertSame(RabbitMqDriver::class, $driverInfo['driverClass']); + + $this->assertArrayHasKey('schemes', $driverInfo); + $this->assertSame(['amqp', 'amqps'], $driverInfo['schemes']); + + $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo); + $this->assertSame(['rabbitmq'], $driverInfo['requiredSchemeExtensions']); + + $this->assertArrayHasKey('packages', $driverInfo); + $this->assertSame(['enqueue/enqueue', 'enqueue/amqp-bunny'], $driverInfo['packages']); + } + + public function testShouldGetKnownDriversInExpectedFormat() + { + $knownDrivers = Resources::getAvailableDrivers(); + + self::assertIsArray($knownDrivers); + $this->assertGreaterThan(0, count($knownDrivers)); + + $driverInfo = $knownDrivers[0]; + + $this->assertArrayHasKey('driverClass', $driverInfo); + $this->assertSame(AmqpDriver::class, $driverInfo['driverClass']); + + $this->assertArrayHasKey('schemes', $driverInfo); + $this->assertSame(['amqp', 'amqps'], $driverInfo['schemes']); + + $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo); + $this->assertSame([], $driverInfo['requiredSchemeExtensions']); + + $this->assertArrayHasKey('packages', $driverInfo); + $this->assertSame(['enqueue/enqueue', 'enqueue/amqp-bunny'], $driverInfo['packages']); + } + + public function testThrowsIfDriverClassNotImplementDriverFactoryInterfaceOnAddDriver() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The driver class "stdClass" must implement "Enqueue\Client\DriverInterface" interface.'); + + Resources::addDriver(\stdClass::class, [], [], ['foo']); + } + + public function testThrowsIfNoSchemesProvidedOnAddDriver() + { + $driverClass = $this->getMockClass(DriverInterface::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Schemes could not be empty.'); + + Resources::addDriver($driverClass, [], [], ['foo']); + } + + public function testThrowsIfNoPackageProvidedOnAddDriver() + { + $driverClass = $this->getMockClass(DriverInterface::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Packages could not be empty.'); + + Resources::addDriver($driverClass, ['foo'], [], []); + } + + public function testShouldAllowRegisterDriverThatIsNotInstalled() + { + Resources::addDriver('theDriverClass', ['foo'], ['barExtension'], ['foo']); + + $availableDrivers = Resources::getKnownDrivers(); + + $driverInfo = end($availableDrivers); + + $this->assertSame('theDriverClass', $driverInfo['driverClass']); + } + + public function testShouldAllowGetPreviouslyRegisteredDriver() + { + $driverClass = $this->getMockClass(DriverInterface::class); + + Resources::addDriver( + $driverClass, + ['fooscheme', 'barscheme'], + ['fooextension', 'barextension'], + ['foo/bar'] + ); + + $availableDrivers = Resources::getAvailableDrivers(); + + $driverInfo = end($availableDrivers); + + $this->assertArrayHasKey('driverClass', $driverInfo); + $this->assertSame($driverClass, $driverInfo['driverClass']); + + $this->assertArrayHasKey('schemes', $driverInfo); + $this->assertSame(['fooscheme', 'barscheme'], $driverInfo['schemes']); + + $this->assertArrayHasKey('requiredSchemeExtensions', $driverInfo); + $this->assertSame(['fooextension', 'barextension'], $driverInfo['requiredSchemeExtensions']); + + $this->assertArrayHasKey('packages', $driverInfo); + $this->assertSame(['foo/bar'], $driverInfo['packages']); + } +} diff --git a/pkg/enqueue/Tests/Client/RouterProcessorTest.php b/pkg/enqueue/Tests/Client/RouterProcessorTest.php index e362c1e23..7d2971189 100644 --- a/pkg/enqueue/Tests/Client/RouterProcessorTest.php +++ b/pkg/enqueue/Tests/Client/RouterProcessorTest.php @@ -4,203 +4,218 @@ use Enqueue\Client\Config; use Enqueue\Client\DriverInterface; +use Enqueue\Client\DriverSendResult; use Enqueue\Client\Message; +use Enqueue\Client\Route; +use Enqueue\Client\RouteCollection; use Enqueue\Client\RouterProcessor; use Enqueue\Consumption\Result; use Enqueue\Null\NullContext; use Enqueue\Null\NullMessage; +use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\Destination; +use Interop\Queue\Message as TransportMessage; +use Interop\Queue\Processor; use PHPUnit\Framework\TestCase; class RouterProcessorTest extends TestCase { - public function testCouldBeConstructedWithDriverAsFirstArgument() + use ClassExtensionTrait; + use ReadAttributeTrait; + + public function testShouldImplementProcessorInterface() + { + $this->assertClassImplements(Processor::class, RouterProcessor::class); + } + + public function testShouldBeFinal() { - new RouterProcessor($this->createDriverMock()); + $this->assertClassFinal(RouterProcessor::class); } - public function testCouldBeConstructedWithSessionAndRoutes() + public function testCouldBeConstructedWithDriver() { - $routes = [ - 'aTopicName' => [['aProcessorName', 'aQueueName']], - 'anotherTopicName' => [['aProcessorName', 'aQueueName']], - ]; + $driver = $this->createDriverStub(); - $router = new RouterProcessor($this->createDriverMock(), $routes); + $processor = new RouterProcessor($driver); - $this->assertAttributeEquals($routes, 'eventRoutes', $router); + $this->assertAttributeSame($driver, 'driver', $processor); } - public function testShouldRejectIfTopicNameParameterIsNotSet() + public function testShouldRejectIfTopicNotSet() { - $router = new RouterProcessor($this->createDriverMock()); + $router = new RouterProcessor($this->createDriverStub()); $result = $router->process(new NullMessage(), new NullContext()); - $this->assertInstanceOf(Result::class, $result); $this->assertEquals(Result::REJECT, $result->getStatus()); - $this->assertEquals('Got message without required parameter: "enqueue.topic_name"', $result->getReason()); + $this->assertEquals('Topic property "enqueue.topic" is required but not set or empty.', $result->getReason()); + } + + public function testShouldRejectIfCommandSet() + { + $router = new RouterProcessor($this->createDriverStub()); + + $message = new NullMessage(); + $message->setProperty(Config::COMMAND, 'aCommand'); + + $result = $router->process($message, new NullContext()); + + $this->assertEquals(Result::REJECT, $result->getStatus()); + $this->assertEquals('Unexpected command "aCommand" got. Command must not go to the router.', $result->getReason()); } - public function testShouldRouteOriginalMessageToEventRecipient() + public function testShouldRouteOriginalMessageToAllRecipients() { $message = new NullMessage(); $message->setBody('theBody'); $message->setHeaders(['aHeader' => 'aHeaderVal']); - $message->setProperties(['aProp' => 'aPropVal', Config::PARAMETER_TOPIC_NAME => 'theTopicName']); + $message->setProperties(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName']); - $clientMessage = new Message(); + /** @var Message[] $routedMessages */ + $routedMessages = new \ArrayObject(); - $routedMessage = null; + $routeCollection = new RouteCollection([ + new Route('theTopicName', Route::TOPIC, 'aFooProcessor'), + new Route('theTopicName', Route::TOPIC, 'aBarProcessor'), + new Route('theTopicName', Route::TOPIC, 'aBazProcessor'), + ]); - $driver = $this->createDriverMock(); + $driver = $this->createDriverStub($routeCollection); $driver - ->expects($this->once()) + ->expects($this->exactly(3)) ->method('sendToProcessor') - ->with($this->identicalTo($clientMessage)) + ->willReturnCallback(function (Message $message) use ($routedMessages) { + $routedMessages->append($message); + + return $this->createDriverSendResult(); + }) ; $driver - ->expects($this->once()) + ->expects($this->exactly(3)) ->method('createClientMessage') - ->willReturnCallback(function (NullMessage $message) use (&$routedMessage, $clientMessage) { - $routedMessage = $message; - - return $clientMessage; + ->willReturnCallback(function (NullMessage $message) { + return new Message($message->getBody(), $message->getProperties(), $message->getHeaders()); }) ; - $routes = [ - 'theTopicName' => [['aFooProcessor', 'aQueueName']], - ]; + $processor = new RouterProcessor($driver); - $router = new RouterProcessor($driver, $routes); + $result = $processor->process($message, new NullContext()); - $result = $router->process($message, new NullContext()); + $this->assertEquals(Result::ACK, $result->getStatus()); + $this->assertEquals('Routed to "3" event subscribers', $result->getReason()); + + $this->assertContainsOnly(Message::class, $routedMessages); + $this->assertCount(3, $routedMessages); - $this->assertEquals(Result::ACK, $result); - $this->assertEquals([ - 'aProp' => 'aPropVal', - 'enqueue.topic_name' => 'theTopicName', - 'enqueue.processor_name' => 'aFooProcessor', - 'enqueue.processor_queue_name' => 'aQueueName', - ], $routedMessage->getProperties()); + $this->assertSame('aFooProcessor', $routedMessages[0]->getProperty(Config::PROCESSOR)); + $this->assertSame('aBarProcessor', $routedMessages[1]->getProperty(Config::PROCESSOR)); + $this->assertSame('aBazProcessor', $routedMessages[2]->getProperty(Config::PROCESSOR)); } - public function testShouldRouteOriginalMessageToCommandRecipient() + public function testShouldDoNothingIfNoRoutes() { $message = new NullMessage(); $message->setBody('theBody'); $message->setHeaders(['aHeader' => 'aHeaderVal']); - $message->setProperties([ - 'aProp' => 'aPropVal', - Config::PARAMETER_TOPIC_NAME => Config::COMMAND_TOPIC, - Config::PARAMETER_COMMAND_NAME => 'theCommandName', - ]); + $message->setProperties(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName']); - $clientMessage = new Message(); + /** @var Message[] $routedMessages */ + $routedMessages = new \ArrayObject(); - $routedMessage = null; + $routeCollection = new RouteCollection([]); - $driver = $this->createDriverMock(); + $driver = $this->createDriverStub($routeCollection); $driver - ->expects($this->once()) + ->expects($this->never()) ->method('sendToProcessor') - ->with($this->identicalTo($clientMessage)) + ->willReturnCallback(function (Message $message) use ($routedMessages) { + $routedMessages->append($message); + }) ; $driver - ->expects($this->once()) + ->expects($this->never()) ->method('createClientMessage') - ->willReturnCallback(function (NullMessage $message) use (&$routedMessage, $clientMessage) { - $routedMessage = $message; - - return $clientMessage; + ->willReturnCallback(function (NullMessage $message) { + return new Message($message->getBody(), $message->getProperties(), $message->getHeaders()); }) ; - $routes = [ - 'theCommandName' => 'aQueueName', - ]; + $processor = new RouterProcessor($driver); - $router = new RouterProcessor($driver, [], $routes); + $result = $processor->process($message, new NullContext()); - $result = $router->process($message, new NullContext()); + $this->assertEquals(Result::ACK, $result->getStatus()); + $this->assertEquals('Routed to "0" event subscribers', $result->getReason()); - $this->assertEquals(Result::ACK, $result); - $this->assertEquals([ - 'aProp' => 'aPropVal', - 'enqueue.topic_name' => Config::COMMAND_TOPIC, - 'enqueue.processor_name' => 'theCommandName', - 'enqueue.command_name' => 'theCommandName', - 'enqueue.processor_queue_name' => 'aQueueName', - ], $routedMessage->getProperties()); + $this->assertCount(0, $routedMessages); } - public function testShouldRejectCommandMessageIfCommandNamePropertyMissing() + public function testShouldDoNotModifyOriginalMessage() { $message = new NullMessage(); $message->setBody('theBody'); $message->setHeaders(['aHeader' => 'aHeaderVal']); - $message->setProperties([ - 'aProp' => 'aPropVal', - Config::PARAMETER_TOPIC_NAME => Config::COMMAND_TOPIC, + $message->setProperties(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName']); + + /** @var Message[] $routedMessages */ + $routedMessages = new \ArrayObject(); + + $routeCollection = new RouteCollection([ + new Route('theTopicName', Route::TOPIC, 'aFooProcessor'), + new Route('theTopicName', Route::TOPIC, 'aBarProcessor'), ]); - $driver = $this->createDriverMock(); + $driver = $this->createDriverStub($routeCollection); $driver - ->expects($this->never()) + ->expects($this->atLeastOnce()) ->method('sendToProcessor') - ; + ->willReturnCallback(function (Message $message) use ($routedMessages) { + $routedMessages->append($message); + + return $this->createDriverSendResult(); + }); $driver - ->expects($this->never()) + ->expects($this->atLeastOnce()) ->method('createClientMessage') - ; - - $routes = [ - 'theCommandName' => 'aQueueName', - ]; - - $router = new RouterProcessor($driver, [], $routes); - - $result = $router->process($message, new NullContext()); - - $this->assertInstanceOf(Result::class, $result); - $this->assertEquals(Result::REJECT, $result->getStatus()); - $this->assertEquals('Got message without required parameter: "enqueue.command_name"', $result->getReason()); - } - - public function testShouldAddEventRoute() - { - $router = new RouterProcessor($this->createDriverMock(), []); + ->willReturnCallback(function (NullMessage $message) { + return new Message($message->getBody(), $message->getProperties(), $message->getHeaders()); + }); - $this->assertAttributeSame([], 'eventRoutes', $router); + $processor = new RouterProcessor($driver); - $router->add('theTopicName', 'theQueueName', 'aProcessorName'); + $result = $processor->process($message, new NullContext()); - $this->assertAttributeSame([ - 'theTopicName' => [ - ['aProcessorName', 'theQueueName'], - ], - ], 'eventRoutes', $router); + // guard + $this->assertEquals(Result::ACK, $result->getStatus()); - $this->assertAttributeSame([], 'commandRoutes', $router); + $this->assertSame('theBody', $message->getBody()); + $this->assertSame(['aProp' => 'aPropVal', Config::TOPIC => 'theTopicName'], $message->getProperties()); + $this->assertSame(['aHeader' => 'aHeaderVal'], $message->getHeaders()); } - public function testShouldAddCommandRoute() + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface { - $router = new RouterProcessor($this->createDriverMock(), []); - - $this->assertAttributeSame([], 'eventRoutes', $router); - - $router->add(Config::COMMAND_TOPIC, 'theQueueName', 'aProcessorName'); + $driver = $this->createMock(DriverInterface::class); + $driver + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection ?? new RouteCollection([])) + ; - $this->assertAttributeSame(['aProcessorName' => 'theQueueName'], 'commandRoutes', $router); - $this->assertAttributeSame([], 'eventRoutes', $router); + return $driver; } - /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface - */ - protected function createDriverMock() + private function createDriverSendResult(): DriverSendResult { - return $this->createMock(DriverInterface::class); + return new DriverSendResult( + $this->createMock(Destination::class), + $this->createMock(TransportMessage::class) + ); } } diff --git a/pkg/enqueue/Tests/Client/SpoolProducerTest.php b/pkg/enqueue/Tests/Client/SpoolProducerTest.php index 8c00dedcd..014fe4962 100644 --- a/pkg/enqueue/Tests/Client/SpoolProducerTest.php +++ b/pkg/enqueue/Tests/Client/SpoolProducerTest.php @@ -18,11 +18,6 @@ public function testShouldImplementProducerInterface() self::assertClassImplements(ProducerInterface::class, SpoolProducer::class); } - public function testCouldBeConstructedWithRealProducer() - { - new SpoolProducer($this->createProducerMock()); - } - public function testShouldQueueEventMessageOnSend() { $message = new Message(); @@ -154,7 +149,7 @@ public function testShouldSendImmediatelyCommandMessageWithNeedReplyTrue() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface */ protected function createProducerMock() { diff --git a/pkg/enqueue/Tests/Client/TraceableProducerTest.php b/pkg/enqueue/Tests/Client/TraceableProducerTest.php index 42c9c8b8f..b0df066ce 100644 --- a/pkg/enqueue/Tests/Client/TraceableProducerTest.php +++ b/pkg/enqueue/Tests/Client/TraceableProducerTest.php @@ -2,7 +2,7 @@ namespace Enqueue\Tests\Client; -use Enqueue\Client\Config; +use DMS\PHPUnitExtensions\ArraySubset\Assert; use Enqueue\Client\Message; use Enqueue\Client\ProducerInterface; use Enqueue\Client\TraceableProducer; @@ -18,11 +18,6 @@ public function testShouldImplementProducerInterface() $this->assertClassImplements(ProducerInterface::class, TraceableProducer::class); } - public function testCouldBeConstructedWithInternalMessageProducer() - { - new TraceableProducer($this->createProducerMock()); - } - public function testShouldPassAllArgumentsToInternalEventMessageProducerSendMethod() { $topic = 'theTopic'; @@ -46,7 +41,7 @@ public function testShouldCollectInfoIfStringGivenAsEventMessage() $producer->sendEvent('aFooTopic', 'aFooBody'); - $this->assertSame([ + Assert::assertArraySubset([ [ 'topic' => 'aFooTopic', 'command' => null, @@ -61,6 +56,8 @@ public function testShouldCollectInfoIfStringGivenAsEventMessage() 'messageId' => null, ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldCollectInfoIfArrayGivenAsEventMessage() @@ -69,7 +66,7 @@ public function testShouldCollectInfoIfArrayGivenAsEventMessage() $producer->sendEvent('aFooTopic', ['foo' => 'fooVal', 'bar' => 'barVal']); - $this->assertSame([ + Assert::assertArraySubset([ [ 'topic' => 'aFooTopic', 'command' => null, @@ -84,6 +81,8 @@ public function testShouldCollectInfoIfArrayGivenAsEventMessage() 'messageId' => null, ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldCollectInfoIfEventMessageObjectGivenAsMessage() @@ -103,7 +102,7 @@ public function testShouldCollectInfoIfEventMessageObjectGivenAsMessage() $producer->sendEvent('aFooTopic', $message); - $this->assertSame([ + Assert::assertArraySubset([ [ 'topic' => 'aFooTopic', 'command' => null, @@ -118,6 +117,8 @@ public function testShouldCollectInfoIfEventMessageObjectGivenAsMessage() 'messageId' => 'theMessageId', ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldNotStoreAnythingIfInternalEventMessageProducerThrowsException() @@ -163,9 +164,9 @@ public function testShouldCollectInfoIfStringGivenAsCommandMessage() $producer->sendCommand('aFooCommand', 'aFooBody'); - $this->assertSame([ + Assert::assertArraySubset([ [ - 'topic' => Config::COMMAND_TOPIC, + 'topic' => null, 'command' => 'aFooCommand', 'body' => 'aFooBody', 'headers' => [], @@ -178,6 +179,8 @@ public function testShouldCollectInfoIfStringGivenAsCommandMessage() 'messageId' => null, ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldCollectInfoIfArrayGivenAsCommandMessage() @@ -186,9 +189,9 @@ public function testShouldCollectInfoIfArrayGivenAsCommandMessage() $producer->sendCommand('aFooCommand', ['foo' => 'fooVal', 'bar' => 'barVal']); - $this->assertSame([ + Assert::assertArraySubset([ [ - 'topic' => Config::COMMAND_TOPIC, + 'topic' => null, 'command' => 'aFooCommand', 'body' => ['foo' => 'fooVal', 'bar' => 'barVal'], 'headers' => [], @@ -201,6 +204,8 @@ public function testShouldCollectInfoIfArrayGivenAsCommandMessage() 'messageId' => null, ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldCollectInfoIfCommandMessageObjectGivenAsMessage() @@ -220,9 +225,9 @@ public function testShouldCollectInfoIfCommandMessageObjectGivenAsMessage() $producer->sendCommand('aFooCommand', $message); - $this->assertSame([ + Assert::assertArraySubset([ [ - 'topic' => Config::COMMAND_TOPIC, + 'topic' => null, 'command' => 'aFooCommand', 'body' => ['foo' => 'fooVal', 'bar' => 'barVal'], 'headers' => ['fooHeader' => 'fooVal'], @@ -235,6 +240,8 @@ public function testShouldCollectInfoIfCommandMessageObjectGivenAsMessage() 'messageId' => 'theMessageId', ], ], $producer->getTraces()); + + $this->assertArrayHasKey('sentAt', $producer->getTraces()[0]); } public function testShouldNotStoreAnythingIfInternalCommandMessageProducerThrowsException() @@ -264,9 +271,9 @@ public function testShouldAllowGetInfoSentToSameTopic() $producer->sendEvent('aFooTopic', 'aFooBody'); $producer->sendEvent('aFooTopic', 'aFooBody'); - $this->assertArraySubset([ - ['topic' => 'aFooTopic', 'body' => 'aFooBody'], - ['topic' => 'aFooTopic', 'body' => 'aFooBody'], + Assert::assertArraySubset([ + ['topic' => 'aFooTopic', 'body' => 'aFooBody'], + ['topic' => 'aFooTopic', 'body' => 'aFooBody'], ], $producer->getTraces()); } @@ -277,7 +284,7 @@ public function testShouldAllowGetInfoSentToDifferentTopics() $producer->sendEvent('aFooTopic', 'aFooBody'); $producer->sendEvent('aBarTopic', 'aBarBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['topic' => 'aFooTopic', 'body' => 'aFooBody'], ['topic' => 'aBarTopic', 'body' => 'aBarBody'], ], $producer->getTraces()); @@ -290,11 +297,11 @@ public function testShouldAllowGetInfoSentToSpecialTopic() $producer->sendEvent('aFooTopic', 'aFooBody'); $producer->sendEvent('aBarTopic', 'aBarBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['topic' => 'aFooTopic', 'body' => 'aFooBody'], ], $producer->getTopicTraces('aFooTopic')); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['topic' => 'aBarTopic', 'body' => 'aBarBody'], ], $producer->getTopicTraces('aBarTopic')); } @@ -306,7 +313,7 @@ public function testShouldAllowGetInfoSentToSameCommand() $producer->sendCommand('aFooCommand', 'aFooBody'); $producer->sendCommand('aFooCommand', 'aFooBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['command' => 'aFooCommand', 'body' => 'aFooBody'], ['command' => 'aFooCommand', 'body' => 'aFooBody'], ], $producer->getTraces()); @@ -319,7 +326,7 @@ public function testShouldAllowGetInfoSentToDifferentCommands() $producer->sendCommand('aFooCommand', 'aFooBody'); $producer->sendCommand('aBarCommand', 'aBarBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['command' => 'aFooCommand', 'body' => 'aFooBody'], ['command' => 'aBarCommand', 'body' => 'aBarBody'], ], $producer->getTraces()); @@ -332,11 +339,11 @@ public function testShouldAllowGetInfoSentToSpecialCommand() $producer->sendCommand('aFooCommand', 'aFooBody'); $producer->sendCommand('aBarCommand', 'aBarBody'); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['command' => 'aFooCommand', 'body' => 'aFooBody'], ], $producer->getCommandTraces('aFooCommand')); - $this->assertArraySubset([ + Assert::assertArraySubset([ ['command' => 'aBarCommand', 'body' => 'aBarBody'], ], $producer->getCommandTraces('aBarCommand')); } @@ -347,7 +354,7 @@ public function testShouldAllowClearStoredTraces() $producer->sendEvent('aFooTopic', 'aFooBody'); - //guard + // guard $this->assertNotEmpty($producer->getTraces()); $producer->clearTraces(); @@ -355,7 +362,7 @@ public function testShouldAllowClearStoredTraces() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface */ protected function createProducerMock() { diff --git a/pkg/enqueue/Tests/ConnectionFactoryFactoryTest.php b/pkg/enqueue/Tests/ConnectionFactoryFactoryTest.php new file mode 100644 index 000000000..b6b5b4d67 --- /dev/null +++ b/pkg/enqueue/Tests/ConnectionFactoryFactoryTest.php @@ -0,0 +1,183 @@ +assertTrue($rc->implementsInterface(ConnectionFactoryFactoryInterface::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(ConnectionFactoryFactory::class); + + $this->assertTrue($rc->isFinal()); + } + + /** + * @doesNotPerformAssertions + */ + public function testShouldAcceptStringDSN() + { + $factory = new ConnectionFactoryFactory(); + + $factory->create('null:'); + } + + /** + * @doesNotPerformAssertions + */ + public function testShouldAcceptArrayWithDsnKey() + { + $factory = new ConnectionFactoryFactory(); + + $factory->create(['dsn' => 'null:']); + } + + public function testThrowIfInvalidConfigGiven() + { + $factory = new ConnectionFactoryFactory(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The config must be either array or DSN string.'); + $factory->create(new \stdClass()); + } + + public function testThrowIfArrayConfigMissDsnKeyInvalidConfigGiven() + { + $factory = new ConnectionFactoryFactory(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The config must be either array or DSN string.'); + $factory->create(new \stdClass()); + } + + public function testThrowIfPackageThatSupportSchemeNotInstalled() + { + $scheme = 'scheme5b7aa7d7cd213'; + $class = 'ConnectionClass5b7aa7d7cd213'; + + Resources::addConnection($class, [$scheme], [], 'thePackage'); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('To use given scheme "scheme5b7aa7d7cd213" a package has to be installed. Run "composer req thePackage" to add it.'); + (new ConnectionFactoryFactory())->create($scheme.'://foo'); + } + + public function testThrowIfSchemeIsNotKnown() + { + $scheme = 'scheme5b7aa862e70a5'; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('A given scheme "scheme5b7aa862e70a5" is not supported. Maybe it is a custom connection, make sure you registered it with "Enqueue\Resources::addConnection".'); + (new ConnectionFactoryFactory())->create($scheme.'://foo'); + } + + public function testThrowIfDsnInvalid() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The DSN is invalid. It does not have scheme separator ":".'); + + (new ConnectionFactoryFactory())->create('invalid-scheme'); + } + + /** + * @dataProvider provideDSN + */ + public function testReturnsExpectedFactories(string $dsn, string $expectedFactoryClass) + { + $connectionFactory = (new ConnectionFactoryFactory())->create($dsn); + + $this->assertInstanceOf($expectedFactoryClass, $connectionFactory); + } + + public static function provideDSN() + { + yield ['null:', NullConnectionFactory::class]; + + yield ['amqp:', AmqpBunnyConnectionFactory::class]; + + yield ['amqp+bunny:', AmqpBunnyConnectionFactory::class]; + + yield ['amqp+lib:', AmqpLibConnectionFactory::class]; + + yield ['amqp+ext:', AmqpExtConnectionFactory::class]; + + yield ['amqp+rabbitmq:', AmqpBunnyConnectionFactory::class]; + + yield ['amqp+rabbitmq+bunny:', AmqpBunnyConnectionFactory::class]; + + yield ['amqp+foo+bar+lib:', AmqpLibConnectionFactory::class]; + + yield ['amqp+rabbitmq+ext:', AmqpExtConnectionFactory::class]; + + yield ['amqp+rabbitmq+lib:', AmqpLibConnectionFactory::class]; + + // bunny does not support amqps, so it is skipped + yield ['amqps:', AmqpExtConnectionFactory::class]; + + // bunny does not support amqps, so it is skipped + yield ['amqps+ext:', AmqpExtConnectionFactory::class]; + + // bunny does not support amqps, so it is skipped + yield ['amqps+rabbitmq:', AmqpExtConnectionFactory::class]; + + yield ['amqps+ext+rabbitmq:', AmqpExtConnectionFactory::class]; + + yield ['amqps+lib+rabbitmq:', AmqpLibConnectionFactory::class]; + + yield ['mssql:', DbalConnectionFactory::class]; + + yield ['mysql:', DbalConnectionFactory::class]; + + yield ['pgsql:', DbalConnectionFactory::class]; + + yield ['file:', FsConnectionFactory::class]; + + // https://github.com/php-enqueue/enqueue-dev/issues/511 + // yield ['gearman:', GearmanConnectionFactory::class]; + + yield ['gps:', GpsConnectionFactory::class]; + + yield ['mongodb:', MongodbConnectionFactory::class]; + + yield ['beanstalk:', PheanstalkConnectionFactory::class]; + + yield ['kafka:', RdKafkaConnectionFactory::class]; + + yield ['redis:', RedisConnectionFactory::class]; + + yield ['redis+predis:', RedisConnectionFactory::class]; + + yield ['redis+foo+bar+phpredis:', RedisConnectionFactory::class]; + + yield ['redis+phpredis:', RedisConnectionFactory::class]; + + yield ['sqs:', SqsConnectionFactory::class]; + + yield ['stomp:', StompConnectionFactory::class]; + } +} diff --git a/pkg/enqueue/Tests/Consumption/CallbackProcessorTest.php b/pkg/enqueue/Tests/Consumption/CallbackProcessorTest.php index 1b034d908..86adbd3a9 100644 --- a/pkg/enqueue/Tests/Consumption/CallbackProcessorTest.php +++ b/pkg/enqueue/Tests/Consumption/CallbackProcessorTest.php @@ -6,7 +6,7 @@ use Enqueue\Null\NullContext; use Enqueue\Null\NullMessage; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Processor; use PHPUnit\Framework\TestCase; class CallbackProcessorTest extends TestCase @@ -15,13 +15,7 @@ class CallbackProcessorTest extends TestCase public function testShouldImplementProcessorInterface() { - $this->assertClassImplements(PsrProcessor::class, CallbackProcessor::class); - } - - public function testCouldBeConstructedWithCallableAsArgument() - { - new CallbackProcessor(function () { - }); + $this->assertClassImplements(Processor::class, CallbackProcessor::class); } public function testShouldCallCallbackAndProxyItsReturnedValue() diff --git a/pkg/enqueue/Tests/Consumption/ChainExtensionTest.php b/pkg/enqueue/Tests/Consumption/ChainExtensionTest.php index 4c412c661..198d00012 100644 --- a/pkg/enqueue/Tests/Consumption/ChainExtensionTest.php +++ b/pkg/enqueue/Tests/Consumption/ChainExtensionTest.php @@ -3,10 +3,26 @@ namespace Enqueue\Tests\Consumption; use Enqueue\Consumption\ChainExtension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\Context\MessageResult; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\PreSubscribe; +use Enqueue\Consumption\Context\Start; use Enqueue\Consumption\ExtensionInterface; use Enqueue\Test\ClassExtensionTrait; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; +use Interop\Queue\SubscriptionConsumer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class ChainExtensionTest extends TestCase { @@ -17,14 +33,31 @@ public function testShouldImplementExtensionInterface() $this->assertClassImplements(ExtensionInterface::class, ChainExtension::class); } - public function testCouldBeConstructedWithExtensionsArray() + public function testShouldProxyOnInitLoggerToAllInternalExtensions() { - new ChainExtension([$this->createExtension(), $this->createExtension()]); + $context = new InitLogger(new NullLogger()); + + $fooExtension = $this->createExtension(); + $fooExtension + ->expects($this->once()) + ->method('onInitLogger') + ->with($this->identicalTo($context)) + ; + $barExtension = $this->createExtension(); + $barExtension + ->expects($this->once()) + ->method('onInitLogger') + ->with($this->identicalTo($context)) + ; + + $extensions = new ChainExtension([$fooExtension, $barExtension]); + + $extensions->onInitLogger($context); } public function testShouldProxyOnStartToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new Start($this->createInteropContextMock(), $this->createLoggerMock(), [], 0, 0); $fooExtension = $this->createExtension(); $fooExtension @@ -44,53 +77,100 @@ public function testShouldProxyOnStartToAllInternalExtensions() $extensions->onStart($context); } - public function testShouldProxyOnBeforeReceiveToAllInternalExtensions() + public function testShouldProxyOnPreSubscribeToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new PreSubscribe( + $this->createInteropContextMock(), + $this->createInteropProcessorMock(), + $this->createInteropConsumerMock(), + $this->createLoggerMock() + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onBeforeReceive') + ->method('onPreSubscribe') ->with($this->identicalTo($context)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onBeforeReceive') + ->method('onPreSubscribe') ->with($this->identicalTo($context)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onBeforeReceive($context); + $extensions->onPreSubscribe($context); + } + + public function testShouldProxyOnPreConsumeToAllInternalExtensions() + { + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + new NullLogger(), + 1, + 2, + 3 + ); + + $fooExtension = $this->createExtension(); + $fooExtension + ->expects($this->once()) + ->method('onPreConsume') + ->with($this->identicalTo($context)) + ; + $barExtension = $this->createExtension(); + $barExtension + ->expects($this->once()) + ->method('onPreConsume') + ->with($this->identicalTo($context)) + ; + + $extensions = new ChainExtension([$fooExtension, $barExtension]); + $extensions->onPreConsume($context); } public function testShouldProxyOnPreReceiveToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new MessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + $this->createMock(Processor::class), + 1, + new NullLogger() + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onPreReceived') + ->method('onMessageReceived') ->with($this->identicalTo($context)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onPreReceived') + ->method('onMessageReceived') ->with($this->identicalTo($context)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onPreReceived($context); + $extensions->onMessageReceived($context); } public function testShouldProxyOnResultToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new MessageResult( + $this->createInteropContextMock(), + $this->createInteropConsumerMock(), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); $fooExtension = $this->createExtension(); $fooExtension @@ -112,83 +192,129 @@ public function testShouldProxyOnResultToAllInternalExtensions() public function testShouldProxyOnPostReceiveToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onPostReceived') + ->method('onPostMessageReceived') ->with($this->identicalTo($context)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onPostReceived') + ->method('onPostMessageReceived') ->with($this->identicalTo($context)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onPostReceived($context); + $extensions->onPostMessageReceived($context); } - public function testShouldProxyOnIdleToAllInternalExtensions() + public function testShouldProxyOnPostConsumeToAllInternalExtensions() { - $context = $this->createContextMock(); + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + new NullLogger() + ); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onIdle') - ->with($this->identicalTo($context)) + ->method('onPostConsume') + ->with($this->identicalTo($postConsume)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onIdle') - ->with($this->identicalTo($context)) + ->method('onPostConsume') + ->with($this->identicalTo($postConsume)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onIdle($context); + $extensions->onPostConsume($postConsume); } - public function testShouldProxyOnInterruptedToAllInternalExtensions() + public function testShouldProxyOnEndToAllInternalExtensions() { - $context = $this->createContextMock(); + $context = new End($this->createInteropContextMock(), 1, 2, new NullLogger()); $fooExtension = $this->createExtension(); $fooExtension ->expects($this->once()) - ->method('onInterrupted') + ->method('onEnd') ->with($this->identicalTo($context)) ; $barExtension = $this->createExtension(); $barExtension ->expects($this->once()) - ->method('onInterrupted') + ->method('onEnd') ->with($this->identicalTo($context)) ; $extensions = new ChainExtension([$fooExtension, $barExtension]); - $extensions->onInterrupted($context); + $extensions->onEnd($context); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Context + * @return MockObject */ - protected function createContextMock() + protected function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); + } + + /** + * @return MockObject + */ + protected function createInteropContextMock(): Context { return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ExtensionInterface + * @return MockObject + */ + protected function createInteropConsumerMock(): Consumer + { + return $this->createMock(Consumer::class); + } + + /** + * @return MockObject + */ + protected function createInteropProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return MockObject|ExtensionInterface */ protected function createExtension() { return $this->createMock(ExtensionInterface::class); } + + /** + * @return MockObject + */ + private function createSubscriptionConsumerMock(): SubscriptionConsumer + { + return $this->createMock(SubscriptionConsumer::class); + } } diff --git a/pkg/enqueue/Tests/Consumption/ContextTest.php b/pkg/enqueue/Tests/Consumption/ContextTest.php deleted file mode 100644 index a0f6b266a..000000000 --- a/pkg/enqueue/Tests/Consumption/ContextTest.php +++ /dev/null @@ -1,258 +0,0 @@ -createPsrContext()); - } - - public function testShouldAllowGetSessionSetInConstructor() - { - $psrContext = $this->createPsrContext(); - - $context = new Context($psrContext); - - $this->assertSame($psrContext, $context->getPsrContext()); - } - - public function testShouldAllowGetMessageConsumerPreviouslySet() - { - $messageConsumer = $this->createPsrConsumer(); - - $context = new Context($this->createPsrContext()); - $context->setPsrConsumer($messageConsumer); - - $this->assertSame($messageConsumer, $context->getPsrConsumer()); - } - - public function testThrowOnTryToChangeMessageConsumerIfAlreadySet() - { - $messageConsumer = $this->createPsrConsumer(); - $anotherMessageConsumer = $this->createPsrConsumer(); - - $context = new Context($this->createPsrContext()); - - $context->setPsrConsumer($messageConsumer); - - $this->expectException(IllegalContextModificationException::class); - - $context->setPsrConsumer($anotherMessageConsumer); - } - - public function testShouldAllowGetMessageProducerPreviouslySet() - { - $processorMock = $this->createProcessorMock(); - - $context = new Context($this->createPsrContext()); - $context->setPsrProcessor($processorMock); - - $this->assertSame($processorMock, $context->getPsrProcessor()); - } - - public function testThrowOnTryToChangeProcessorIfAlreadySet() - { - $processor = $this->createProcessorMock(); - $anotherProcessor = $this->createProcessorMock(); - - $context = new Context($this->createPsrContext()); - - $context->setPsrProcessor($processor); - - $this->expectException(IllegalContextModificationException::class); - - $context->setPsrProcessor($anotherProcessor); - } - - public function testShouldAllowGetLoggerPreviouslySet() - { - $logger = new NullLogger(); - - $context = new Context($this->createPsrContext()); - $context->setLogger($logger); - - $this->assertSame($logger, $context->getLogger()); - } - - public function testShouldSetExecutionInterruptedToFalseInConstructor() - { - $context = new Context($this->createPsrContext()); - - $this->assertFalse($context->isExecutionInterrupted()); - } - - public function testShouldAllowGetPreviouslySetMessage() - { - /** @var PsrMessage $message */ - $message = $this->createMock(PsrMessage::class); - - $context = new Context($this->createPsrContext()); - - $context->setPsrMessage($message); - - $this->assertSame($message, $context->getPsrMessage()); - } - - public function testThrowOnTryToChangeMessageIfAlreadySet() - { - /** @var PsrMessage $message */ - $message = $this->createMock(PsrMessage::class); - - $context = new Context($this->createPsrContext()); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The message could be set once'); - - $context->setPsrMessage($message); - $context->setPsrMessage($message); - } - - public function testShouldAllowGetPreviouslySetException() - { - $exception = new \Exception(); - - $context = new Context($this->createPsrContext()); - - $context->setException($exception); - - $this->assertSame($exception, $context->getException()); - } - - public function testShouldAllowGetPreviouslySetResult() - { - $result = 'aResult'; - - $context = new Context($this->createPsrContext()); - - $context->setResult($result); - - $this->assertSame($result, $context->getResult()); - } - - public function testThrowOnTryToChangeResultIfAlreadySet() - { - $result = 'aResult'; - - $context = new Context($this->createPsrContext()); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The result modification is not allowed'); - - $context->setResult($result); - $context->setResult($result); - } - - public function testShouldAllowGetPreviouslySetExecutionInterrupted() - { - $context = new Context($this->createPsrContext()); - - // guard - $this->assertFalse($context->isExecutionInterrupted()); - - $context->setExecutionInterrupted(true); - - $this->assertTrue($context->isExecutionInterrupted()); - } - - public function testThrowOnTryToRollbackExecutionInterruptedIfAlreadySetToTrue() - { - $context = new Context($this->createPsrContext()); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The execution once interrupted could not be roll backed'); - - $context->setExecutionInterrupted(true); - $context->setExecutionInterrupted(false); - } - - public function testNotThrowOnSettingExecutionInterruptedToTrueIfAlreadySetToTrue() - { - $context = new Context($this->createPsrContext()); - - $context->setExecutionInterrupted(true); - $context->setExecutionInterrupted(true); - } - - public function testShouldAllowGetPreviouslySetLogger() - { - $expectedLogger = new NullLogger(); - - $context = new Context($this->createPsrContext()); - - $context->setLogger($expectedLogger); - - $this->assertSame($expectedLogger, $context->getLogger()); - } - - public function testThrowOnSettingLoggerIfAlreadySet() - { - $context = new Context($this->createPsrContext()); - - $context->setLogger(new NullLogger()); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The logger modification is not allowed'); - - $context->setLogger(new NullLogger()); - } - - public function testShouldAllowGetPreviouslySetQueue() - { - $context = new Context($this->createPsrContext()); - - $context->setPsrQueue($queue = new NullQueue('')); - - $this->assertSame($queue, $context->getPsrQueue()); - } - - public function testThrowOnSettingQueueNameIfAlreadySet() - { - $context = new Context($this->createPsrContext()); - - $context->setPsrQueue(new NullQueue('')); - - $this->expectException(IllegalContextModificationException::class); - $this->expectExceptionMessage('The queue modification is not allowed'); - - $context->setPsrQueue(new NullQueue('')); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - protected function createPsrContext() - { - return $this->createMock(PsrContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer - */ - protected function createPsrConsumer() - { - return $this->createMock(PsrConsumer::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor - */ - protected function createProcessorMock() - { - return $this->createMock(PsrProcessor::class); - } -} diff --git a/pkg/enqueue/Tests/Consumption/EmptyExtensionTraitTest.php b/pkg/enqueue/Tests/Consumption/EmptyExtensionTraitTest.php deleted file mode 100644 index 32ea8612a..000000000 --- a/pkg/enqueue/Tests/Consumption/EmptyExtensionTraitTest.php +++ /dev/null @@ -1,20 +0,0 @@ -assertClassImplements(ExceptionInterface::class, ConsumptionInterruptedException::class); - } - - public function testShouldExtendLogicException() - { - $this->assertClassExtends(\LogicException::class, ConsumptionInterruptedException::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new ConsumptionInterruptedException(); - } -} diff --git a/pkg/enqueue/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php b/pkg/enqueue/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php index 0885b500e..241f4adf9 100644 --- a/pkg/enqueue/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php +++ b/pkg/enqueue/Tests/Consumption/Exception/IllegalContextModificationExceptionTest.php @@ -20,9 +20,4 @@ public function testShouldExtendLogicException() { $this->assertClassExtends(\LogicException::class, IllegalContextModificationException::class); } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new IllegalContextModificationException(); - } } diff --git a/pkg/enqueue/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php b/pkg/enqueue/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php index 296c76225..c1c5db362 100644 --- a/pkg/enqueue/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php +++ b/pkg/enqueue/Tests/Consumption/Exception/InvalidArgumentExceptionTest.php @@ -21,11 +21,6 @@ public function testShouldExtendLogicException() $this->assertClassExtends(\LogicException::class, InvalidArgumentException::class); } - public function testCouldBeConstructedWithoutAnyArguments() - { - new InvalidArgumentException(); - } - public function testThrowIfAssertInstanceOfNotSameAsExpected() { $this->expectException(InvalidArgumentException::class); @@ -36,6 +31,9 @@ public function testThrowIfAssertInstanceOfNotSameAsExpected() InvalidArgumentException::assertInstanceOf(new \SplStack(), \SplQueue::class); } + /** + * @doesNotPerformAssertions + */ public function testShouldDoNothingIfAssertDestinationInstanceOfSameAsExpected() { InvalidArgumentException::assertInstanceOf(new \SplQueue(), \SplQueue::class); diff --git a/pkg/enqueue/Tests/Consumption/Exception/LogicExceptionTest.php b/pkg/enqueue/Tests/Consumption/Exception/LogicExceptionTest.php index ddd258098..2655609ae 100644 --- a/pkg/enqueue/Tests/Consumption/Exception/LogicExceptionTest.php +++ b/pkg/enqueue/Tests/Consumption/Exception/LogicExceptionTest.php @@ -20,9 +20,4 @@ public function testShouldExtendLogicException() { $this->assertClassExtends(\LogicException::class, LogicException::class); } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new LogicException(); - } } diff --git a/pkg/enqueue/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php b/pkg/enqueue/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php index 250c5778d..137e30ba4 100644 --- a/pkg/enqueue/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php +++ b/pkg/enqueue/Tests/Consumption/Extension/LimitConsumedMessagesExtensionTest.php @@ -2,41 +2,84 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; use Enqueue\Consumption\Extension\LimitConsumedMessagesExtension; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\SubscriptionConsumer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class LimitConsumedMessagesExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() + public function testOnPreConsumeShouldInterruptWhenLimitIsReached() { - new LimitConsumedMessagesExtension(12345); - } + $logger = $this->createLoggerMock(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'. + ' the message limit reached. limit: "3"') + ; - public function testShouldThrowExceptionIfMessageLimitIsNotInt() - { - $this->setExpectedException( - \InvalidArgumentException::class, - 'Expected message limit is int but got: "double"' + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + $logger, + 1, + 2, + 3 + ); + + // guard + $this->assertFalse($context->isExecutionInterrupted()); + + // test + $extension = new LimitConsumedMessagesExtension(3); + + $extension->onPreConsume($context); + $this->assertFalse($context->isExecutionInterrupted()); + + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() ); - new LimitConsumedMessagesExtension(0.0); + $extension->onPostMessageReceived($postReceivedMessage); + $extension->onPostMessageReceived($postReceivedMessage); + $extension->onPostMessageReceived($postReceivedMessage); + + $extension->onPreConsume($context); + $this->assertTrue($context->isExecutionInterrupted()); } - public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsZero() + public function testOnPreConsumeShouldInterruptExecutionIfLimitIsZero() { - $context = $this->createContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'. ' the message limit reached. limit: "0"') ; + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + $logger, + 1, + 2, + 3 + ); + // guard $this->assertFalse($context->isExecutionInterrupted()); @@ -44,20 +87,29 @@ public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsZero() $extension = new LimitConsumedMessagesExtension(0); // consume 1 - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertTrue($context->isExecutionInterrupted()); } - public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsLessThatZero() + public function testOnPreConsumeShouldInterruptExecutionIfLimitIsLessThatZero() { - $context = $this->createContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'. ' the message limit reached. limit: "-1"') ; + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + $logger, + 1, + 2, + 3 + ); + // guard $this->assertFalse($context->isExecutionInterrupted()); @@ -65,45 +117,65 @@ public function testOnBeforeReceiveShouldInterruptExecutionIfLimitIsLessThatZero $extension = new LimitConsumedMessagesExtension(-1); // consume 1 - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertTrue($context->isExecutionInterrupted()); } public function testOnPostReceivedShouldInterruptExecutionIfMessageLimitExceeded() { - $context = $this->createContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with('[LimitConsumedMessagesExtension] Message consumption is interrupted since'. ' the message limit reached. limit: "2"') ; + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + $logger + ); + // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test $extension = new LimitConsumedMessagesExtension(2); // consume 1 - $extension->onPostReceived($context); - $this->assertFalse($context->isExecutionInterrupted()); + $extension->onPostMessageReceived($postReceivedMessage); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // consume 2 and exit - $extension->onPostReceived($context); - $this->assertTrue($context->isExecutionInterrupted()); + $extension->onPostMessageReceived($postReceivedMessage); + $this->assertTrue($postReceivedMessage->isExecutionInterrupted()); } /** - * @return Context + * @return MockObject */ - protected function createContext() + protected function createInteropContextMock(): Context { - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($this->createMock(LoggerInterface::class)); - $context->setPsrConsumer($this->createMock(PsrConsumer::class)); - $context->setPsrProcessor($this->createMock(PsrProcessor::class)); + return $this->createMock(Context::class); + } - return $context; + /** + * @return MockObject + */ + private function createSubscriptionConsumerMock(): SubscriptionConsumer + { + return $this->createMock(SubscriptionConsumer::class); + } + + /** + * @return MockObject + */ + private function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); } } diff --git a/pkg/enqueue/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php b/pkg/enqueue/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php index 74514e58c..25ac85895 100644 --- a/pkg/enqueue/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php +++ b/pkg/enqueue/Tests/Consumption/Extension/LimitConsumerMemoryExtensionTest.php @@ -2,137 +2,197 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; use Enqueue\Consumption\Extension\LimitConsumerMemoryExtension; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\SubscriptionConsumer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class LimitConsumerMemoryExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new LimitConsumerMemoryExtension(12345); - } - public function testShouldThrowExceptionIfMemoryLimitIsNotInt() { - $this->setExpectedException(\InvalidArgumentException::class, 'Expected memory limit is int but got: "double"'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected memory limit is int but got: "double"'); new LimitConsumerMemoryExtension(0.0); } - public function testOnIdleShouldInterruptExecutionIfMemoryLimitReached() + public function testOnPostConsumeShouldInterruptExecutionIfMemoryLimitReached() { - $context = $this->createPsrContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with($this->stringContains('[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached.')) ; + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + $logger + ); + // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); // test $extension = new LimitConsumerMemoryExtension(1); - $extension->onIdle($context); + $extension->onPostConsume($postConsume); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertTrue($postConsume->isExecutionInterrupted()); } public function testOnPostReceivedShouldInterruptExecutionIfMemoryLimitReached() { - $context = $this->createPsrContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with($this->stringContains('[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached.')) ; + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + $logger + ); + // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test $extension = new LimitConsumerMemoryExtension(1); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertTrue($postReceivedMessage->isExecutionInterrupted()); } - public function testOnBeforeReceivedShouldInterruptExecutionIfMemoryLimitReached() + public function testOnPreConsumeShouldInterruptExecutionIfMemoryLimitReached() { - $context = $this->createPsrContext(); - $context->getLogger() + $logger = $this->createLoggerMock(); + $logger ->expects($this->once()) ->method('debug') ->with($this->stringContains('[LimitConsumerMemoryExtension] Interrupt execution as memory limit reached.')) ; + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + $logger, + 1, + 2, + 3 + ); + // guard $this->assertFalse($context->isExecutionInterrupted()); // test $extension = new LimitConsumerMemoryExtension(1); - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertTrue($context->isExecutionInterrupted()); } - public function testOnBeforeReceiveShouldNotInterruptExecutionIfMemoryLimitIsNotReached() + public function testOnPreConsumeShouldNotInterruptExecutionIfMemoryLimitIsNotReached() { - $context = $this->createPsrContext(); + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + new NullLogger(), + 1, + 2, + 3 + ); // guard $this->assertFalse($context->isExecutionInterrupted()); // test - $extension = new LimitConsumerMemoryExtension(PHP_INT_MAX); - $extension->onBeforeReceive($context); + $extension = new LimitConsumerMemoryExtension(\PHP_INT_MAX); + $extension->onPreConsume($context); $this->assertFalse($context->isExecutionInterrupted()); } - public function testOnIdleShouldNotInterruptExecutionIfMemoryLimitIsNotReached() + public function testOnPostConsumeShouldNotInterruptExecutionIfMemoryLimitIsNotReached() { - $context = $this->createPsrContext(); + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); // test - $extension = new LimitConsumerMemoryExtension(PHP_INT_MAX); - $extension->onIdle($context); + $extension = new LimitConsumerMemoryExtension(\PHP_INT_MAX); + $extension->onPostConsume($postConsume); - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); } - public function testOnPostReceivedShouldNotInterruptExecutionIfMemoryLimitIsNotReached() + public function testOnPostMessageReceivedShouldNotInterruptExecutionIfMemoryLimitIsNotReached() { - $context = $this->createPsrContext(); + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test - $extension = new LimitConsumerMemoryExtension(PHP_INT_MAX); - $extension->onPostReceived($context); + $extension = new LimitConsumerMemoryExtension(\PHP_INT_MAX); + $extension->onPostMessageReceived($postReceivedMessage); - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); } /** - * @return Context + * @return MockObject */ - protected function createPsrContext() + protected function createInteropContextMock(): Context { - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($this->createMock(LoggerInterface::class)); - $context->setPsrConsumer($this->createMock(PsrConsumer::class)); - $context->setPsrProcessor($this->createMock(PsrProcessor::class)); + return $this->createMock(Context::class); + } - return $context; + /** + * @return MockObject + */ + private function createSubscriptionConsumerMock(): SubscriptionConsumer + { + return $this->createMock(SubscriptionConsumer::class); + } + + /** + * @return MockObject + */ + private function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); } } diff --git a/pkg/enqueue/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php b/pkg/enqueue/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php index fffeb9439..fa6cb76a1 100644 --- a/pkg/enqueue/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php +++ b/pkg/enqueue/Tests/Consumption/Extension/LimitConsumptionTimeExtensionTest.php @@ -2,24 +2,31 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; use Enqueue\Consumption\Extension\LimitConsumptionTimeExtension; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\SubscriptionConsumer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class LimitConsumptionTimeExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() + public function testOnPreConsumeShouldInterruptExecutionIfConsumptionTimeExceeded() { - new LimitConsumptionTimeExtension(new \DateTime('+1 day')); - } - - public function testOnBeforeReceiveShouldInterruptExecutionIfConsumptionTimeExceeded() - { - $context = $this->createContext(); + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + new NullLogger(), + 1, + 2, + 3 + ); // guard $this->assertFalse($context->isExecutionInterrupted()); @@ -27,44 +34,65 @@ public function testOnBeforeReceiveShouldInterruptExecutionIfConsumptionTimeExce // test $extension = new LimitConsumptionTimeExtension(new \DateTime('-2 second')); - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertTrue($context->isExecutionInterrupted()); } - public function testOnIdleShouldInterruptExecutionIfConsumptionTimeExceeded() + public function testOnPostConsumeShouldInterruptExecutionIfConsumptionTimeExceeded() { - $context = $this->createContext(); + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); // test $extension = new LimitConsumptionTimeExtension(new \DateTime('-2 second')); - $extension->onIdle($context); + $extension->onPostConsume($postConsume); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertTrue($postConsume->isExecutionInterrupted()); } public function testOnPostReceivedShouldInterruptExecutionIfConsumptionTimeExceeded() { - $context = $this->createContext(); + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test $extension = new LimitConsumptionTimeExtension(new \DateTime('-2 second')); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertTrue($postReceivedMessage->isExecutionInterrupted()); } - public function testOnBeforeReceiveShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() + public function testOnPreConsumeShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() { - $context = $this->createContext(); + $context = new PreConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + new NullLogger(), + 1, + 2, + 3 + ); // guard $this->assertFalse($context->isExecutionInterrupted()); @@ -72,51 +100,76 @@ public function testOnBeforeReceiveShouldNotInterruptExecutionIfConsumptionTimeI // test $extension = new LimitConsumptionTimeExtension(new \DateTime('+2 second')); - $extension->onBeforeReceive($context); + $extension->onPreConsume($context); $this->assertFalse($context->isExecutionInterrupted()); } - public function testOnIdleShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() + public function testOnPostConsumeShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() { - $context = $this->createContext(); + $postConsume = new PostConsume( + $this->createInteropContextMock(), + $this->createSubscriptionConsumerMock(), + 1, + 1, + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); // test $extension = new LimitConsumptionTimeExtension(new \DateTime('+2 second')); - $extension->onIdle($context); + $extension->onPostConsume($postConsume); - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postConsume->isExecutionInterrupted()); } public function testOnPostReceivedShouldNotInterruptExecutionIfConsumptionTimeIsNotExceeded() { - $context = $this->createContext(); + $postReceivedMessage = new PostMessageReceived( + $this->createInteropContextMock(), + $this->createMock(Consumer::class), + $this->createMock(Message::class), + 'aResult', + 1, + new NullLogger() + ); // guard - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); // test $extension = new LimitConsumptionTimeExtension(new \DateTime('+2 second')); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); - $this->assertFalse($context->isExecutionInterrupted()); + $this->assertFalse($postReceivedMessage->isExecutionInterrupted()); + } + + /** + * @return MockObject + */ + protected function createInteropContextMock(): Context + { + return $this->createMock(Context::class); } /** - * @return Context + * @return MockObject */ - protected function createContext() + private function createSubscriptionConsumerMock(): SubscriptionConsumer { - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($this->createMock(LoggerInterface::class)); - $context->setPsrConsumer($this->createMock(PsrConsumer::class)); - $context->setPsrProcessor($this->createMock(PsrProcessor::class)); + return $this->createMock(SubscriptionConsumer::class); + } - return $context; + /** + * @return MockObject + */ + private function createLoggerMock(): LoggerInterface + { + return $this->createMock(LoggerInterface::class); } } diff --git a/pkg/enqueue/Tests/Consumption/Extension/LogExtensionTest.php b/pkg/enqueue/Tests/Consumption/Extension/LogExtensionTest.php new file mode 100644 index 000000000..006a2c549 --- /dev/null +++ b/pkg/enqueue/Tests/Consumption/Extension/LogExtensionTest.php @@ -0,0 +1,266 @@ +assertClassImplements(StartExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementEndExtensionInterface() + { + $this->assertClassImplements(EndExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementMessageReceivedExtensionInterface() + { + $this->assertClassImplements(MessageReceivedExtensionInterface::class, LogExtension::class); + } + + public function testShouldImplementPostMessageReceivedExtensionInterface() + { + $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, LogExtension::class); + } + + public function testShouldLogStartOnStart() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Consumption has started') + ; + + $context = new Start($this->createContextMock(), $logger, [], 1, 1); + + $extension = new LogExtension(); + $extension->onStart($context); + } + + public function testShouldLogEndOnEnd() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Consumption has ended') + ; + + $context = new End($this->createContextMock(), 1, 2, $logger); + + $extension = new LogExtension(); + $extension->onEnd($context); + } + + public function testShouldLogMessageReceived() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('debug') + ->with('Received from {queueName} {body}', [ + 'queueName' => 'aQueue', + 'redelivered' => false, + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + ]) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new MessageReceived($this->createContextMock(), $consumerMock, $message, $this->createProcessorMock(), 1, $logger); + + $extension = new LogExtension(); + $extension->onMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithStringResult() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'aResult', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, 'aResult', 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogRejectedMessageAsError() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::ERROR, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'reject', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Processor::REJECT, 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => '', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack(), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + public function testShouldLogMessageProcessedWithReasonResultObject() + { + $logger = $this->createLogger(); + $logger + ->expects($this->once()) + ->method('log') + ->with(LogLevel::INFO, + 'Processed from {queueName} {body} {result} {reason}', + [ + 'queueName' => 'aQueue', + 'body' => Stringify::that('aBody'), + 'properties' => Stringify::that(['aProp' => 'aPropVal']), + 'headers' => Stringify::that(['aHeader' => 'aHeaderVal']), + 'result' => 'ack', + 'reason' => 'aReason', + ] + ) + ; + + $consumerMock = $this->createConsumerStub(new NullQueue('aQueue')); + $message = new NullMessage('aBody'); + $message->setProperty('aProp', 'aPropVal'); + $message->setHeader('aHeader', 'aHeaderVal'); + + $context = new PostMessageReceived($this->createContextMock(), $consumerMock, $message, Result::ack('aReason'), 1, $logger); + + $extension = new LogExtension(); + $extension->onPostMessageReceived($context); + } + + /** + * @return MockObject + */ + private function createConsumerStub(Queue $queue): Consumer + { + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } + + /** + * @return MockObject + */ + private function createContextMock(): Context + { + return $this->createMock(Context::class); + } + + /** + * @return MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return MockObject|LoggerInterface + */ + private function createLogger() + { + return $this->createMock(LoggerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Consumption/Extension/LoggerExtensionTest.php b/pkg/enqueue/Tests/Consumption/Extension/LoggerExtensionTest.php index 68bc7003f..666892e0e 100644 --- a/pkg/enqueue/Tests/Consumption/Extension/LoggerExtensionTest.php +++ b/pkg/enqueue/Tests/Consumption/Extension/LoggerExtensionTest.php @@ -2,199 +2,78 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\InitLogger; use Enqueue\Consumption\Extension\LoggerExtension; -use Enqueue\Consumption\ExtensionInterface; -use Enqueue\Consumption\Result; -use Enqueue\Null\NullMessage; +use Enqueue\Consumption\InitLoggerExtensionInterface; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class LoggerExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementInitLoggerExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, LoggerExtension::class); + $this->assertClassImplements(InitLoggerExtensionInterface::class, LoggerExtension::class); } - public function testCouldBeConstructedWithLoggerAsFirstArgument() - { - new LoggerExtension($this->createLogger()); - } - - public function testShouldSetLoggerToContextOnStart() + public function testShouldSetLoggerToContextOnInitLogger() { $logger = $this->createLogger(); $extension = new LoggerExtension($logger); - $context = new Context($this->createPsrContextMock()); + $previousLogger = new NullLogger(); + $context = new InitLogger($previousLogger); - $extension->onStart($context); + $extension->onInitLogger($context); $this->assertSame($logger, $context->getLogger()); } public function testShouldAddInfoMessageOnStart() { - $logger = $this->createLogger(); - $logger - ->expects($this->once()) - ->method('debug') - ->with($this->stringStartsWith('Set context\'s logger')) - ; - - $extension = new LoggerExtension($logger); - - $context = new Context($this->createPsrContextMock()); - - $extension->onStart($context); - } - - public function testShouldLogRejectMessageStatus() - { - $logger = $this->createLogger(); - $logger - ->expects($this->once()) - ->method('error') - ->with('reason', ['body' => 'message body', 'headers' => [], 'properties' => []]) - ; - - $extension = new LoggerExtension($logger); - - $message = new NullMessage(); - $message->setBody('message body'); - - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::reject('reason')); - $context->setPsrMessage($message); - - $extension->onPostReceived($context); - } - - public function testShouldLogRequeueMessageStatus() - { - $logger = $this->createLogger(); - $logger - ->expects($this->once()) - ->method('error') - ->with('reason', ['body' => 'message body', 'headers' => [], 'properties' => []]) - ; - - $extension = new LoggerExtension($logger); - - $message = new NullMessage(); - $message->setBody('message body'); - - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::requeue('reason')); - $context->setPsrMessage($message); - - $extension->onPostReceived($context); - } - - public function testShouldNotLogRequeueMessageStatusIfReasonIsEmpty() - { - $logger = $this->createLogger(); - $logger - ->expects($this->never()) - ->method('error') - ; - - $extension = new LoggerExtension($logger); + $previousLogger = $this->createLogger(); - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::requeue()); - - $extension->onPostReceived($context); - } - - public function testShouldLogAckMessageStatus() - { $logger = $this->createLogger(); $logger ->expects($this->once()) - ->method('info') - ->with('reason', ['body' => 'message body', 'headers' => [], 'properties' => []]) + ->method('debug') + ->with(sprintf('Change logger from "%s" to "%s"', $logger::class, $previousLogger::class)) ; $extension = new LoggerExtension($logger); - $message = new NullMessage(); - $message->setBody('message body'); + $context = new InitLogger($previousLogger); - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::ack('reason')); - $context->setPsrMessage($message); - - $extension->onPostReceived($context); + $extension->onInitLogger($context); } - public function testShouldNotLogAckMessageStatusIfReasonIsEmpty() + public function testShouldDoNothingIfSameLoggerInstanceAlreadySet() { $logger = $this->createLogger(); $logger ->expects($this->never()) - ->method('info') - ; - - $extension = new LoggerExtension($logger); - - $context = new Context($this->createPsrContextMock()); - $context->setResult(Result::ack()); - - $extension->onPostReceived($context); - } - - public function testShouldNotSetLoggerIfOneHasBeenSetOnStart() - { - $logger = $this->createLogger(); - - $alreadySetLogger = $this->createLogger(); - $alreadySetLogger - ->expects($this->once()) ->method('debug') - ->with(sprintf( - 'Skip setting context\'s logger "%s". Another one "%s" has already been set.', - get_class($logger), - get_class($alreadySetLogger) - )) ; $extension = new LoggerExtension($logger); - $context = new Context($this->createPsrContextMock()); - $context->setLogger($alreadySetLogger); + $context = new InitLogger($logger); - $extension->onStart($context); - } + $extension->onInitLogger($context); - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - protected function createPsrContextMock() - { - return $this->createMock(PsrContext::class); + $this->assertSame($logger, $context->getLogger()); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + * @return MockObject|LoggerInterface */ protected function createLogger() { return $this->createMock(LoggerInterface::class); } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer - */ - protected function createConsumerMock() - { - return $this->createMock(PsrConsumer::class); - } } diff --git a/pkg/enqueue/Tests/Consumption/Extension/NicenessExtensionTest.php b/pkg/enqueue/Tests/Consumption/Extension/NicenessExtensionTest.php index 1cabdaff0..734bc8417 100644 --- a/pkg/enqueue/Tests/Consumption/Extension/NicenessExtensionTest.php +++ b/pkg/enqueue/Tests/Consumption/Extension/NicenessExtensionTest.php @@ -2,21 +2,15 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\Start; use Enqueue\Consumption\Extension\NicenessExtension; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context as InteropContext; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class NicenessExtensionTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new NicenessExtension(0); - } - public function testShouldThrowExceptionOnInvalidArgument() { $this->expectException(\InvalidArgumentException::class); @@ -28,20 +22,17 @@ public function testShouldThrowWarningOnInvalidArgument() $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('proc_nice(): Only a super user may attempt to increase the priority of a process'); + $context = new Start($this->createContextMock(), new NullLogger(), [], 0, 0); + $extension = new NicenessExtension(-1); - $extension->onStart($this->createContext()); + $extension->onStart($context); } /** - * @return Context + * @return MockObject|InteropContext */ - protected function createContext() + protected function createContextMock(): InteropContext { - $context = new Context($this->createMock(PsrContext::class)); - $context->setLogger($this->createMock(LoggerInterface::class)); - $context->setPsrConsumer($this->createMock(PsrConsumer::class)); - $context->setPsrProcessor($this->createMock(PsrProcessor::class)); - - return $context; + return $this->createMock(InteropContext::class); } } diff --git a/pkg/enqueue/Tests/Consumption/Extension/ReplyExtensionTest.php b/pkg/enqueue/Tests/Consumption/Extension/ReplyExtensionTest.php index 49120559e..cb65816ce 100644 --- a/pkg/enqueue/Tests/Consumption/Extension/ReplyExtensionTest.php +++ b/pkg/enqueue/Tests/Consumption/Extension/ReplyExtensionTest.php @@ -2,15 +2,17 @@ namespace Enqueue\Tests\Consumption\Extension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\PostMessageReceived; use Enqueue\Consumption\Extension\ReplyExtension; -use Enqueue\Consumption\ExtensionInterface; +use Enqueue\Consumption\PostMessageReceivedExtensionInterface; use Enqueue\Consumption\Result; use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProducer; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Producer as InteropProducer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; @@ -18,52 +20,25 @@ class ReplyExtensionTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementExtensionInterface() + public function testShouldImplementPostMessageReceivedExtensionInterface() { - $this->assertClassImplements(ExtensionInterface::class, ReplyExtension::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new ReplyExtension(); - } - - public function testShouldDoNothingOnPreReceived() - { - $extension = new ReplyExtension(); - - $extension->onPreReceived(new Context($this->createNeverUsedContextMock())); - } - - public function testShouldDoNothingOnStart() - { - $extension = new ReplyExtension(); - - $extension->onStart(new Context($this->createNeverUsedContextMock())); - } - - public function testShouldDoNothingOnBeforeReceive() - { - $extension = new ReplyExtension(); - - $extension->onBeforeReceive(new Context($this->createNeverUsedContextMock())); - } - - public function testShouldDoNothingOnInterrupted() - { - $extension = new ReplyExtension(); - - $extension->onInterrupted(new Context($this->createNeverUsedContextMock())); + $this->assertClassImplements(PostMessageReceivedExtensionInterface::class, ReplyExtension::class); } public function testShouldDoNothingIfReceivedMessageNotHaveReplyToSet() { $extension = new ReplyExtension(); - $context = new Context($this->createNeverUsedContextMock()); - $context->setPsrMessage(new NullMessage()); + $postReceivedMessage = new PostMessageReceived( + $this->createNeverUsedContextMock(), + $this->createMock(Consumer::class), + new NullMessage(), + 'aResult', + 1, + new NullLogger() + ); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); } public function testShouldDoNothingIfContextResultIsNotInstanceOfResult() @@ -73,11 +48,16 @@ public function testShouldDoNothingIfContextResultIsNotInstanceOfResult() $message = new NullMessage(); $message->setReplyTo('aReplyToQueue'); - $context = new Context($this->createNeverUsedContextMock()); - $context->setPsrMessage($message); - $context->setResult('notInstanceOfResult'); + $postReceivedMessage = new PostMessageReceived( + $this->createNeverUsedContextMock(), + $this->createMock(Consumer::class), + $message, + 'notInstanceOfResult', + 1, + new NullLogger() + ); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); } public function testShouldDoNothingIfResultInstanceOfResultButReplyMessageNotSet() @@ -87,11 +67,16 @@ public function testShouldDoNothingIfResultInstanceOfResultButReplyMessageNotSet $message = new NullMessage(); $message->setReplyTo('aReplyToQueue'); - $context = new Context($this->createNeverUsedContextMock()); - $context->setPsrMessage($message); - $context->setResult(Result::ack()); + $postReceivedMessage = new PostMessageReceived( + $this->createNeverUsedContextMock(), + $this->createMock(Consumer::class), + $message, + Result::ack(), + 1, + new NullLogger() + ); - $extension->onPostReceived($context); + $extension->onPostMessageReceived($postReceivedMessage); } public function testShouldSendReplyMessageToReplyQueueOnPostReceived() @@ -107,15 +92,15 @@ public function testShouldSendReplyMessageToReplyQueueOnPostReceived() $replyQueue = new NullQueue('aReplyName'); - $producerMock = $this->createMock(PsrProducer::class); + $producerMock = $this->createMock(InteropProducer::class); $producerMock ->expects($this->once()) ->method('send') ->with($replyQueue, $replyMessage) ; - /** @var \PHPUnit_Framework_MockObject_MockObject|PsrContext $contextMock */ - $contextMock = $this->createMock(PsrContext::class); + /** @var MockObject|Context $contextMock */ + $contextMock = $this->createMock(Context::class); $contextMock ->expects($this->once()) ->method('createQueue') @@ -127,20 +112,32 @@ public function testShouldSendReplyMessageToReplyQueueOnPostReceived() ->willReturn($producerMock) ; - $context = new Context($contextMock); - $context->setPsrMessage($message); - $context->setResult(Result::reply($replyMessage)); - $context->setLogger(new NullLogger()); + $postReceivedMessage = new PostMessageReceived( + $contextMock, + $this->createMock(Consumer::class), + $message, + Result::reply($replyMessage), + 1, + new NullLogger() + ); + + $extension->onPostMessageReceived($postReceivedMessage); + } - $extension->onPostReceived($context); + /** + * @return MockObject + */ + protected function createInteropContextMock(): Context + { + return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject */ - private function createNeverUsedContextMock() + private function createNeverUsedContextMock(): Context { - $contextMock = $this->createMock(PsrContext::class); + $contextMock = $this->createMock(Context::class); $contextMock ->expects($this->never()) ->method('createProducer') diff --git a/pkg/enqueue/Tests/Consumption/FallbackSubscriptionConsumerTest.php b/pkg/enqueue/Tests/Consumption/FallbackSubscriptionConsumerTest.php index b054180ed..73fba7bfd 100644 --- a/pkg/enqueue/Tests/Consumption/FallbackSubscriptionConsumerTest.php +++ b/pkg/enqueue/Tests/Consumption/FallbackSubscriptionConsumerTest.php @@ -3,23 +3,22 @@ namespace Enqueue\Tests\Consumption; use Enqueue\Consumption\FallbackSubscriptionConsumer; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrQueue; -use Interop\Queue\PsrSubscriptionConsumer; - -class FallbackSubscriptionConsumerTest extends \PHPUnit_Framework_TestCase +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\Consumer; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Queue as InteropQueue; +use Interop\Queue\SubscriptionConsumer; +use PHPUnit\Framework\TestCase; + +class FallbackSubscriptionConsumerTest extends TestCase { - public function testShouldImplementPsrSubscriptionConsumerInterface() + use ReadAttributeTrait; + + public function testShouldImplementSubscriptionConsumerInterface() { $rc = new \ReflectionClass(FallbackSubscriptionConsumer::class); - $this->assertTrue($rc->implementsInterface(PsrSubscriptionConsumer::class)); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new FallbackSubscriptionConsumer(); + $this->assertTrue($rc->implementsInterface(SubscriptionConsumer::class)); } public function testShouldInitSubscribersPropertyWithEmptyArray() @@ -65,6 +64,9 @@ public function testThrowsIfTrySubscribeAnotherConsumerToAlreadySubscribedQueue( $subscriptionConsumer->subscribe($barConsumer, $barCallback); } + /** + * @doesNotPerformAssertions + */ public function testShouldAllowSubscribeSameConsumerAndCallbackSecondTime() { $subscriptionConsumer = new FallbackSubscriptionConsumer(); @@ -145,38 +147,22 @@ public function testShouldConsumeMessagesFromTwoQueuesInExpectedOrder() $fourthMessage = $this->createMessageStub('fourth'); $fifthMessage = $this->createMessageStub('fifth'); - $fooMessages = [null, $firstMessage, null, $secondMessage, $thirdMessage]; - $fooConsumer = $this->createConsumerStub('foo_queue'); $fooConsumer ->expects($this->any()) ->method('receiveNoWait') - ->willReturnCallback(function () use (&$fooMessages) { - if (empty($fooMessages)) { - return null; - } - - return array_shift($fooMessages); - }) + ->willReturnOnConsecutiveCalls(null, $firstMessage, null, $secondMessage, $thirdMessage) ; - $barMessages = [$fourthMessage, null, null, $fifthMessage]; - $barConsumer = $this->createConsumerStub('bar_queue'); $barConsumer ->expects($this->any()) ->method('receiveNoWait') - ->willReturnCallback(function () use (&$barMessages) { - if (empty($barMessages)) { - return null; - } - - return array_shift($barMessages); - }) + ->willReturnOnConsecutiveCalls($fourthMessage, null, null, $fifthMessage) ; $actualOrder = []; - $callback = function (PsrMessage $message, PsrConsumer $consumer) use (&$actualOrder) { + $callback = function (InteropMessage $message, Consumer $consumer) use (&$actualOrder) { $actualOrder[] = [$message->getBody(), $consumer->getQueue()->getQueueName()]; }; @@ -226,13 +212,13 @@ public function testShouldConsumeTillTimeoutIsReached() } /** - * @param null|mixed $body + * @param mixed|null $body * - * @return PsrMessage|\PHPUnit_Framework_MockObject_MockObject + * @return InteropMessage|\PHPUnit\Framework\MockObject\MockObject */ private function createMessageStub($body = null) { - $messageMock = $this->createMock(PsrMessage::class); + $messageMock = $this->createMock(InteropMessage::class); $messageMock ->expects($this->any()) ->method('getBody') @@ -243,19 +229,19 @@ private function createMessageStub($body = null) } /** - * @param null|mixed $queueName + * @param mixed|null $queueName * - * @return PsrConsumer|\PHPUnit_Framework_MockObject_MockObject + * @return Consumer|\PHPUnit\Framework\MockObject\MockObject */ private function createConsumerStub($queueName = null) { - $queueMock = $this->createMock(PsrQueue::class); + $queueMock = $this->createMock(InteropQueue::class); $queueMock ->expects($this->any()) ->method('getQueueName') ->willReturn($queueName); - $consumerMock = $this->createMock(PsrConsumer::class); + $consumerMock = $this->createMock(Consumer::class); $consumerMock ->expects($this->any()) ->method('getQueue') diff --git a/pkg/enqueue/Tests/Consumption/Mock/BreakCycleExtension.php b/pkg/enqueue/Tests/Consumption/Mock/BreakCycleExtension.php index b61d8dd8d..cbc2f8b1e 100644 --- a/pkg/enqueue/Tests/Consumption/Mock/BreakCycleExtension.php +++ b/pkg/enqueue/Tests/Consumption/Mock/BreakCycleExtension.php @@ -2,14 +2,20 @@ namespace Enqueue\Tests\Consumption\Mock; -use Enqueue\Consumption\Context; -use Enqueue\Consumption\EmptyExtensionTrait; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\Context\MessageResult; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\PreSubscribe; +use Enqueue\Consumption\Context\ProcessorException; +use Enqueue\Consumption\Context\Start; use Enqueue\Consumption\ExtensionInterface; class BreakCycleExtension implements ExtensionInterface { - use EmptyExtensionTrait; - protected $cycles = 1; private $limit; @@ -19,15 +25,51 @@ public function __construct($limit) $this->limit = $limit; } - public function onPostReceived(Context $context) + public function onInitLogger(InitLogger $context): void + { + } + + public function onPostMessageReceived(PostMessageReceived $context): void + { + if ($this->cycles >= $this->limit) { + $context->interruptExecution(); + } else { + ++$this->cycles; + } + } + + public function onEnd(End $context): void + { + } + + public function onMessageReceived(MessageReceived $context): void + { + } + + public function onResult(MessageResult $context): void + { + } + + public function onPreConsume(PreConsume $context): void + { + } + + public function onPreSubscribe(PreSubscribe $context): void + { + } + + public function onProcessorException(ProcessorException $context): void + { + } + + public function onStart(Start $context): void { - $this->onIdle($context); } - public function onIdle(Context $context) + public function onPostConsume(PostConsume $context): void { if ($this->cycles >= $this->limit) { - $context->setExecutionInterrupted(true); + $context->interruptExecution(); } else { ++$this->cycles; } diff --git a/pkg/enqueue/Tests/Consumption/Mock/DummySubscriptionConsumer.php b/pkg/enqueue/Tests/Consumption/Mock/DummySubscriptionConsumer.php new file mode 100644 index 000000000..40351484d --- /dev/null +++ b/pkg/enqueue/Tests/Consumption/Mock/DummySubscriptionConsumer.php @@ -0,0 +1,48 @@ +messages as list($message, $queueName)) { + /** @var InteropMessage $message */ + /** @var string $queueName */ + if (false == call_user_func($this->subscriptions[$queueName][1], $message, $this->subscriptions[$queueName][0])) { + return; + } + } + } + + public function subscribe(Consumer $consumer, callable $callback): void + { + $this->subscriptions[$consumer->getQueue()->getQueueName()] = [$consumer, $callback]; + } + + public function unsubscribe(Consumer $consumer): void + { + unset($this->subscriptions[$consumer->getQueue()->getQueueName()]); + } + + public function unsubscribeAll(): void + { + $this->subscriptions = []; + } + + public function addMessage(InteropMessage $message, string $queueName): void + { + $this->messages[] = [$message, $queueName]; + } +} diff --git a/pkg/enqueue/Tests/Consumption/QueueConsumerTest.php b/pkg/enqueue/Tests/Consumption/QueueConsumerTest.php index 196f75598..2bcc253e7 100644 --- a/pkg/enqueue/Tests/Consumption/QueueConsumerTest.php +++ b/pkg/enqueue/Tests/Consumption/QueueConsumerTest.php @@ -2,61 +2,92 @@ namespace Enqueue\Tests\Consumption; +use Enqueue\Consumption\BoundProcessor; use Enqueue\Consumption\CallbackProcessor; use Enqueue\Consumption\ChainExtension; -use Enqueue\Consumption\Context; +use Enqueue\Consumption\Context\End; +use Enqueue\Consumption\Context\InitLogger; +use Enqueue\Consumption\Context\MessageReceived; +use Enqueue\Consumption\Context\MessageResult; +use Enqueue\Consumption\Context\PostConsume; +use Enqueue\Consumption\Context\PostMessageReceived; +use Enqueue\Consumption\Context\PreConsume; +use Enqueue\Consumption\Context\PreSubscribe; +use Enqueue\Consumption\Context\ProcessorException; +use Enqueue\Consumption\Context\Start; use Enqueue\Consumption\Exception\InvalidArgumentException; +use Enqueue\Consumption\Extension\ExitStatusExtension; use Enqueue\Consumption\ExtensionInterface; use Enqueue\Consumption\QueueConsumer; use Enqueue\Consumption\Result; use Enqueue\Null\NullQueue; +use Enqueue\Test\ReadAttributeTrait; use Enqueue\Tests\Consumption\Mock\BreakCycleExtension; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; -use Interop\Queue\PsrQueue; +use Enqueue\Tests\Consumption\Mock\DummySubscriptionConsumer; +use Interop\Queue\Consumer; +use Interop\Queue\Context as InteropContext; +use Interop\Queue\Exception\SubscriptionConsumerNotSupportedException; +use Interop\Queue\Message; +use Interop\Queue\Processor; +use Interop\Queue\Queue; +use Interop\Queue\SubscriptionConsumer; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; class QueueConsumerTest extends TestCase { - public function testCouldBeConstructedWithConnectionAndExtensionsAsArguments() + use ReadAttributeTrait; + + public function testShouldSetEmptyArrayToBoundProcessorsPropertyInConstructor() { - new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub(), null, [], null, 0); + + $this->assertAttributeSame([], 'boundProcessors', $consumer); } - public function testCouldBeConstructedWithConnectionOnly() + public function testShouldSetProvidedBoundProcessorsToThePropertyInConstructor() { - new QueueConsumer($this->createPsrContextStub()); + $boundProcessors = [ + new BoundProcessor(new NullQueue('foo'), $this->createProcessorMock()), + new BoundProcessor(new NullQueue('bar'), $this->createProcessorMock()), + ]; + + $consumer = new QueueConsumer($this->createContextStub(), null, $boundProcessors, null, 0); + + $this->assertAttributeSame($boundProcessors, 'boundProcessors', $consumer); } - public function testCouldBeConstructedWithConnectionAndSingleExtension() + public function testShouldSetNullLoggerIfNoneProvidedInConstructor() { - new QueueConsumer($this->createPsrContextStub(), $this->createExtension()); + $consumer = new QueueConsumer($this->createContextStub(), null, [], null, 0); + + $this->assertAttributeInstanceOf(NullLogger::class, 'logger', $consumer); } - public function testShouldSetEmptyArrayToBoundProcessorsPropertyInConstructor() + public function testShouldSetProvidedLoggerToThePropertyInConstructor() { - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $expectedLogger = $this->createMock(LoggerInterface::class); - $this->assertAttributeSame([], 'boundProcessors', $consumer); + $consumer = new QueueConsumer($this->createContextStub(), null, [], $expectedLogger, 0); + + $this->assertAttributeSame($expectedLogger, 'logger', $consumer); } - public function testShouldAllowGetConnectionSetInConstructor() + public function testShouldAllowGetContextSetInConstructor() { - $expectedConnection = $this->createPsrContextStub(); + $expectedContext = $this->createContextStub(); - $consumer = new QueueConsumer($expectedConnection, null, 0); + $consumer = new QueueConsumer($expectedContext, null, [], null, 0); - $this->assertSame($expectedConnection, $consumer->getPsrContext()); + $this->assertSame($expectedContext, $consumer->getContext()); } public function testThrowIfQueueNameEmptyOnBind() { $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $this->expectException(\LogicException::class); $this->expectExceptionMessage('The queue name must be not empty.'); @@ -67,7 +98,7 @@ public function testThrowIfQueueAlreadyBoundToProcessorOnBind() { $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $consumer->bind(new NullQueue('theQueueName'), $processorMock); @@ -81,45 +112,31 @@ public function testShouldAllowBindProcessorToQueue() $queue = new NullQueue('theQueueName'); $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $consumer->bind($queue, $processorMock); - $this->assertAttributeSame(['theQueueName' => [$queue, $processorMock]], 'boundProcessors', $consumer); + $this->assertAttributeEquals( + ['theQueueName' => new BoundProcessor($queue, $processorMock)], + 'boundProcessors', + $consumer + ); } public function testThrowIfQueueNeitherInstanceOfQueueNorString() { $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The argument must be an instance of Interop\Queue\PsrQueue but got stdClass.'); + $this->expectExceptionMessage('The argument must be an instance of Interop\Queue\Queue but got stdClass.'); $consumer->bind(new \stdClass(), $processorMock); } - public function testThrowIfProcessorNeitherInstanceOfProcessorNorCallable() - { - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The argument must be an instance of Interop\Queue\PsrProcessor but got stdClass.'); - $consumer->bind(new NullQueue(''), new \stdClass()); - } - - public function testCouldSetGetIdleTimeout() - { - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); - - $consumer->setIdleTimeout(123456); - - $this->assertSame(123456, $consumer->getIdleTimeout()); - } - public function testCouldSetGetReceiveTimeout() { - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); $consumer->setReceiveTimeout(123456); @@ -134,7 +151,7 @@ public function testShouldAllowBindCallbackToQueueName() $queueName = 'theQueueName'; $queue = new NullQueue($queueName); - $context = $this->createMock(PsrContext::class); + $context = $this->createContextWithoutSubscriptionConsumerMock(); $context ->expects($this->once()) ->method('createQueue') @@ -142,49 +159,129 @@ public function testShouldAllowBindCallbackToQueueName() ->willReturn($queue) ; - $consumer = new QueueConsumer($context, null, 0); + $consumer = new QueueConsumer($context); - $consumer->bind($queueName, $callback); + $consumer->bindCallback($queueName, $callback); $boundProcessors = $this->readAttribute($consumer, 'boundProcessors'); - $this->assertInternalType('array', $boundProcessors); + self::assertIsArray($boundProcessors); $this->assertCount(1, $boundProcessors); $this->assertArrayHasKey($queueName, $boundProcessors); - $this->assertInternalType('array', $boundProcessors[$queueName]); - $this->assertCount(2, $boundProcessors[$queueName]); - $this->assertSame($queue, $boundProcessors[$queueName][0]); - $this->assertInstanceOf(CallbackProcessor::class, $boundProcessors[$queueName][1]); + $this->assertInstanceOf(BoundProcessor::class, $boundProcessors[$queueName]); + $this->assertSame($queue, $boundProcessors[$queueName]->getQueue()); + $this->assertInstanceOf(CallbackProcessor::class, $boundProcessors[$queueName]->getProcessor()); } public function testShouldReturnSelfOnBind() { $processorMock = $this->createProcessorMock(); - $consumer = new QueueConsumer($this->createPsrContextStub(), null, 0); + $consumer = new QueueConsumer($this->createContextStub()); - $this->assertSame($consumer, $consumer->bind(new NullQueue('aQueueName'), $processorMock)); + $this->assertSame($consumer, $consumer->bind(new NullQueue('foo_queue'), $processorMock)); + } + + public function testShouldUseContextSubscriptionConsumerIfSupport() + { + $expectedQueue = new NullQueue('theQueueName'); + + $contextSubscriptionConsumer = $this->createSubscriptionConsumerMock(); + $contextSubscriptionConsumer + ->expects($this->once()) + ->method('consume') + ; + + $fallbackSubscriptionConsumer = $this->createSubscriptionConsumerMock(); + $fallbackSubscriptionConsumer + ->expects($this->never()) + ->method('consume') + ; + + $contextMock = $this->createMock(InteropContext::class); + $contextMock + ->expects($this->once()) + ->method('createConsumer') + ->with($this->identicalTo($expectedQueue)) + ->willReturn($this->createConsumerStub()) + ; + $contextMock + ->expects($this->once()) + ->method('createSubscriptionConsumer') + ->willReturn($contextSubscriptionConsumer) + ; + + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->never()) + ->method('process') + ; + + $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($fallbackSubscriptionConsumer); + $queueConsumer->bind($expectedQueue, $processorMock); + $queueConsumer->consume(); + } + + public function testShouldUseFallbackSubscriptionConsumerIfNotSupported() + { + $expectedQueue = new NullQueue('theQueueName'); + + $contextSubscriptionConsumer = $this->createSubscriptionConsumerMock(); + $contextSubscriptionConsumer + ->expects($this->never()) + ->method('consume') + ; + + $fallbackSubscriptionConsumer = $this->createSubscriptionConsumerMock(); + $fallbackSubscriptionConsumer + ->expects($this->once()) + ->method('consume') + ; + + $contextMock = $this->createContextWithoutSubscriptionConsumerMock(); + $contextMock + ->expects($this->once()) + ->method('createConsumer') + ->with($this->identicalTo($expectedQueue)) + ->willReturn($this->createConsumerStub()) + ; + $contextMock + ->expects($this->once()) + ->method('createSubscriptionConsumer') + ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt()) + ; + + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->never()) + ->method('process') + ; + + $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($fallbackSubscriptionConsumer); + $queueConsumer->bind($expectedQueue, $processorMock); + $queueConsumer->consume(); } public function testShouldSubscribeToGivenQueueWithExpectedTimeout() { $expectedQueue = new NullQueue('theQueueName'); - $messageConsumerMock = $this->createMock(PsrConsumer::class); - $messageConsumerMock + $subscriptionConsumerMock = $this->createSubscriptionConsumerMock(); + $subscriptionConsumerMock ->expects($this->once()) - ->method('receive') + ->method('consume') ->with(12345) - ->willReturn(null) ; - $contextMock = $this->createMock(PsrContext::class); + $contextMock = $this->createContextWithoutSubscriptionConsumerMock(); $contextMock ->expects($this->once()) ->method('createConsumer') ->with($this->identicalTo($expectedQueue)) - ->willReturn($messageConsumerMock) + ->willReturn($this->createConsumerStub()) ; $processorMock = $this->createProcessorMock(); @@ -193,28 +290,28 @@ public function testShouldSubscribeToGivenQueueWithExpectedTimeout() ->method('process') ; - $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1), 0, 12345); + $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(1), [], null, 12345); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); $queueConsumer->bind($expectedQueue, $processorMock); $queueConsumer->consume(); } - public function testShouldSubscribeToGivenQueueAndQuitAfterFifthIdleCycle() + public function testShouldSubscribeToGivenQueueAndQuitAfterFifthConsumeCycle() { $expectedQueue = new NullQueue('theQueueName'); - $messageConsumerMock = $this->createMock(PsrConsumer::class); - $messageConsumerMock + $subscriptionConsumerMock = $this->createSubscriptionConsumerMock(); + $subscriptionConsumerMock ->expects($this->exactly(5)) - ->method('receive') - ->willReturn(null) + ->method('consume') ; - $contextMock = $this->createMock(PsrContext::class); + $contextMock = $this->createContextWithoutSubscriptionConsumerMock(); $contextMock ->expects($this->once()) ->method('createConsumer') ->with($this->identicalTo($expectedQueue)) - ->willReturn($messageConsumerMock) + ->willReturn($this->createConsumerStub()) ; $processorMock = $this->createProcessorMock(); @@ -223,17 +320,30 @@ public function testShouldSubscribeToGivenQueueAndQuitAfterFifthIdleCycle() ->method('process') ; - $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(5), 0); + $queueConsumer = new QueueConsumer($contextMock, new BreakCycleExtension(5)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); $queueConsumer->bind($expectedQueue, $processorMock); $queueConsumer->consume(); } public function testShouldProcessFiveMessagesAndQuit() { - $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); + $fooQueue = new NullQueue('foo_queue'); + + $firstMessageMock = $this->createMessageMock(); + $secondMessageMock = $this->createMessageMock(); + $thirdMessageMock = $this->createMessageMock(); + $fourthMessageMock = $this->createMessageMock(); + $fifthMessageMock = $this->createMessageMock(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($firstMessageMock, 'foo_queue'); + $subscriptionConsumerMock->addMessage($secondMessageMock, 'foo_queue'); + $subscriptionConsumerMock->addMessage($thirdMessageMock, 'foo_queue'); + $subscriptionConsumerMock->addMessage($fourthMessageMock, 'foo_queue'); + $subscriptionConsumerMock->addMessage($fifthMessageMock, 'foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub(); $processorMock = $this->createProcessorMock(); $processorMock @@ -242,8 +352,9 @@ public function testShouldProcessFiveMessagesAndQuit() ->willReturn(Result::ACK) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(5), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(5)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind($fooQueue, $processorMock); $queueConsumer->consume(); } @@ -251,14 +362,18 @@ public function testShouldProcessFiveMessagesAndQuit() public function testShouldAckMessageIfProcessorReturnSuchStatus() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $messageConsumerStub + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + $consumerStub ->expects($this->once()) ->method('acknowledge') ->with($this->identicalTo($messageMock)) ; - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -268,8 +383,9 @@ public function testShouldAckMessageIfProcessorReturnSuchStatus() ->willReturn(Result::ACK) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } @@ -277,9 +393,13 @@ public function testShouldAckMessageIfProcessorReturnSuchStatus() public function testThrowIfProcessorReturnNull() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -289,8 +409,9 @@ public function testThrowIfProcessorReturnNull() ->willReturn(null) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $this->expectException(\LogicException::class); $this->expectExceptionMessage('Status is not supported'); @@ -300,14 +421,18 @@ public function testThrowIfProcessorReturnNull() public function testShouldRejectMessageIfProcessorReturnSuchStatus() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $messageConsumerStub + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + $consumerStub ->expects($this->once()) ->method('reject') ->with($this->identicalTo($messageMock), false) ; - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -317,8 +442,43 @@ public function testShouldRejectMessageIfProcessorReturnSuchStatus() ->willReturn(Result::REJECT) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); + + $queueConsumer->consume(); + } + + public function testShouldDoNothingIfProcessorReturnsAlreadyAcknowledged() + { + $messageMock = $this->createMessageMock(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + $consumerStub + ->expects($this->never()) + ->method('reject') + ; + $consumerStub + ->expects($this->never()) + ->method('acknowledge') + ; + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->once()) + ->method('process') + ->with($this->identicalTo($messageMock)) + ->willReturn(Result::ALREADY_ACKNOWLEDGED) + ; + + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } @@ -326,14 +486,18 @@ public function testShouldRejectMessageIfProcessorReturnSuchStatus() public function testShouldRequeueMessageIfProcessorReturnSuchStatus() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $messageConsumerStub + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + $consumerStub ->expects($this->once()) ->method('reject') ->with($this->identicalTo($messageMock), true) ; - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -343,8 +507,9 @@ public function testShouldRequeueMessageIfProcessorReturnSuchStatus() ->willReturn(Result::REQUEUE) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } @@ -352,9 +517,13 @@ public function testShouldRequeueMessageIfProcessorReturnSuchStatus() public function testThrowIfProcessorReturnInvalidStatus() { $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -364,8 +533,9 @@ public function testThrowIfProcessorReturnInvalidStatus() ->willReturn('invalidStatus') ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $this->expectException(\LogicException::class); $this->expectExceptionMessage('Status is not supported: invalidStatus'); @@ -377,17 +547,21 @@ public function testShouldNotPassMessageToProcessorIfItWasProcessedByExtension() $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setResult(Result::ACK); + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) + ->willReturnCallback(function (MessageReceived $context) { + $context->setResult(Result::ack()); }) ; $messageMock = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($messageMock); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($messageMock, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -396,17 +570,46 @@ public function testShouldNotPassMessageToProcessorIfItWasProcessedByExtension() ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); + + $queueConsumer->consume(); + } + + public function testShouldCallOnInitLoggerExtensionMethod() + { + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorMock(); + + $logger = $this->createMock(LoggerInterface::class); + + $extension = $this->createExtension(); + $extension + ->expects($this->once()) + ->method('onInitLogger') + ->with($this->isInstanceOf(InitLogger::class)) + ->willReturnCallback(function (InitLogger $context) use ($logger) { + $this->assertSame($logger, $context->getLogger()); + }) + ; + + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, [], $logger); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } public function testShouldCallOnStartExtensionMethod() { - $messageConsumerStub = $this->createMessageConsumerStub($message = null); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); @@ -414,472 +617,530 @@ public function testShouldCallOnStartExtensionMethod() $extension ->expects($this->once()) ->method('onStart') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($contextStub) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertNull($context->getPsrConsumer()); - $this->assertNull($context->getPsrProcessor()); - $this->assertNull($context->getLogger()); - $this->assertNull($context->getPsrMessage()); - $this->assertNull($context->getException()); - $this->assertNull($context->getResult()); - $this->assertNull($context->getPsrQueue()); - $this->assertFalse($context->isExecutionInterrupted()); + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($contextStub) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertInstanceOf(NullLogger::class, $context->getLogger()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnIdleExtensionMethod() + public function testShouldCallOnStartWithLoggerProvidedInConstructor() { - $messageConsumerStub = $this->createMessageConsumerStub($message = null); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); + $expectedLogger = $this->createMock(LoggerInterface::class); + $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onIdle') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getPsrMessage()); - $this->assertNull($context->getException()); - $this->assertNull($context->getResult()); - $this->assertFalse($context->isExecutionInterrupted()); + ->method('onStart') + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($expectedLogger) { + $this->assertSame($expectedLogger, $context->getLogger()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, [], $expectedLogger); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnBeforeReceiveExtensionMethod() + public function testShouldInterruptExecutionOnStart() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); - $processorMock = $this->createProcessorStub(); + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->never()) + ->method('process') + ; - $queue = new NullQueue('aQueueName'); + $expectedLogger = $this->createMock(LoggerInterface::class); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $queue - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getPsrMessage()); - $this->assertNull($context->getException()); - $this->assertNull($context->getResult()); - $this->assertFalse($context->isExecutionInterrupted()); - $this->assertSame($queue, $context->getPsrQueue()); + ->method('onStart') + ->willReturnCallback(function (Start $context) { + $context->interruptExecution(); }) ; + $extension + ->expects($this->once()) + ->method('onEnd') + ; + $extension + ->expects($this->never()) + ->method('onPreConsume') + ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind($queue, $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, [], $expectedLogger); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnPreReceivedExtensionMethodWithExpectedContext() + public function testShouldCallPreSubscribeExtensionMethod() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); - $processorMock = $this->createProcessorStub(); + $processorMock = $this->createProcessorMock(); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + ->method('onPreSubscribe') + ->with($this->isInstanceOf(PreSubscribe::class)) + ->willReturnCallback(function (PreSubscribe $context) use ($contextStub, $consumerStub, $processorMock) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($consumerStub, $context->getConsumer()); + $this->assertSame($processorMock, $context->getProcessor()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertNull($context->getResult()); - $this->assertFalse($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnResultExtensionMethodWithExpectedContext() + public function testShouldCallPreSubscribeForEachBoundProcessor() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); - $processorMock = $this->createProcessorStub(); + $processorMock = $this->createProcessorMock(); + + $extension = $this->createExtension(); + $extension + ->expects($this->exactly(3)) + ->method('onPreSubscribe') + ->with($this->isInstanceOf(PreSubscribe::class)) + ; + + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); + $queueConsumer->bind(new NullQueue('bar_queue'), $processorMock); + $queueConsumer->bind(new NullQueue('baz_queue'), $processorMock); + + $queueConsumer->consume(); + } + + public function testShouldCallOnPostConsumeExtensionMethod() + { + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $subscriptionConsumer = new DummySubscriptionConsumer(); + + $processorMock = $this->createProcessorMock(); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onResult') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + ->method('onPostConsume') + ->with($this->isInstanceOf(PostConsume::class)) + ->willReturnCallback(function (PostConsume $context) use ($contextStub, $subscriptionConsumer) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($subscriptionConsumer, $context->getSubscriptionConsumer()); + $this->assertSame(1, $context->getCycle()); + $this->assertSame(0, $context->getReceivedMessagesCount()); + $this->assertGreaterThan(1, $context->getStartTime()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertSame(Result::ACK, $context->getResult()); $this->assertFalse($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumer); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnPostReceivedExtensionMethodWithExpectedContext() + public function testShouldCallOnPreConsumeExtensionMethod() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorStub(); + $queue = new NullQueue('foo_queue'); + $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPostReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + ->method('onPreConsume') + ->with($this->isInstanceOf(PreConsume::class)) + ->willReturnCallback(function (PreConsume $context) use ($contextStub) { + $this->assertSame($contextStub, $context->getContext()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertSame(Result::ACK, $context->getResult()); + $this->assertInstanceOf(SubscriptionConsumer::class, $context->getSubscriptionConsumer()); + $this->assertSame(10000, $context->getReceiveTimeout()); + $this->assertSame(1, $context->getCycle()); + $this->assertGreaterThan(0, $context->getStartTime()); $this->assertFalse($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind($queue, $processorMock); $queueConsumer->consume(); } - public function testShouldAllowInterruptConsumingOnIdle() + public function testShouldCallOnPreConsumeExpectedAmountOfTimes() { - $messageConsumerStub = $this->createMessageConsumerStub($message = null); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); - $processorMock = $this->createProcessorMock(); + $processorMock = $this->createProcessorStub(); + + $queue = new NullQueue('foo_queue'); $extension = $this->createExtension(); $extension - ->expects($this->once()) - ->method('onIdle') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); - }) + ->expects($this->exactly(3)) + ->method('onPreConsume') ; + + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(3)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind($queue, $processorMock); + + $queueConsumer->consume(); + } + + public function testShouldCallOnPreReceivedExtensionMethodWithExpectedContext() + { + $expectedMessage = $this->createMessageMock(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); + + $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) + ->willReturnCallback(function (MessageReceived $context) use ( $contextStub, - $messageConsumerStub, - $processorMock + $consumerStub, + $processorMock, + $expectedMessage ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($consumerStub, $context->getConsumer()); + $this->assertSame($processorMock, $context->getProcessor()); + $this->assertSame($expectedMessage, $context->getMessage()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getPsrMessage()); - $this->assertNull($context->getException()); $this->assertNull($context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldNotCloseContextWhenConsumptionInterrupted() + public function testShouldCallOnResultExtensionMethodWithExpectedContext() { - $messageConsumerStub = $this->createMessageConsumerStub($message = null); + $expectedMessage = $this->createMessageMock(); - $contextStub = $this->createPsrContextStub($messageConsumerStub); - $contextStub - ->expects($this->never()) - ->method('close') - ; + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); - $processorMock = $this->createProcessorMock(); + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onIdle') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); + ->method('onResult') + ->with($this->isInstanceOf(MessageResult::class)) + ->willReturnCallback(function (MessageResult $context) use ($contextStub, $expectedMessage) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertInstanceOf(NullLogger::class, $context->getLogger()); + $this->assertSame(Result::ACK, $context->getResult()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldNotCloseContextWhenConsumptionInterruptedByException() + public function testShouldCallOnProcessorExceptionExtensionMethodWithExpectedContext() { - $expectedException = new \Exception(); + $exception = new \LogicException('Exception exception'); - $messageConsumerStub = $this->createMessageConsumerStub($message = $this->createMessageMock()); + $expectedMessage = $this->createMessageMock(); - $contextStub = $this->createPsrContextStub($messageConsumerStub); - $contextStub - ->expects($this->never()) - ->method('close') - ; + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); - $processorMock = $this->createProcessorMock(); + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); $processorMock ->expects($this->once()) ->method('process') - ->willThrowException($expectedException) + ->willThrowException($exception) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); - - try { - $queueConsumer->consume(); - } catch (\Exception $e) { - $this->assertSame($expectedException, $e); - $this->assertNull($e->getPrevious()); + $extension = $this->createExtension(); + $extension + ->expects($this->never()) + ->method('onResult') + ; + $extension + ->expects($this->once()) + ->method('onProcessorException') + ->with($this->isInstanceOf(ProcessorException::class)) + ->willReturnCallback(function (ProcessorException $context) use ($contextStub, $expectedMessage, $exception) { + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($expectedMessage, $context->getMessage()); + $this->assertSame($exception, $context->getException()); + $this->assertGreaterThan(1, $context->getReceivedAt()); + $this->assertInstanceOf(NullLogger::class, $context->getLogger()); + $this->assertNull($context->getResult()); + }) + ; - return; - } + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); - $this->fail('Exception throw is expected.'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Exception exception'); + $queueConsumer->consume(); } - public function testShouldSetMainExceptionAsPreviousToExceptionThrownOnInterrupt() + public function testShouldContinueConsumptionIfResultSetOnProcessorExceptionExtension() { - $mainException = new \Exception(); - $expectedException = new \Exception(); + $result = Result::ack(); - $messageConsumerStub = $this->createMessageConsumerStub($message = $this->createMessageMock()); + $expectedMessage = $this->createMessageMock(); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); - $processorMock = $this->createProcessorMock(); + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); $processorMock ->expects($this->once()) ->method('process') - ->willThrowException($mainException) + ->willThrowException(new \LogicException()) ; $extension = $this->createExtension(); $extension - ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->willThrowException($expectedException) + ->expects($this->once()) + ->method('onProcessorException') + ->willReturnCallback(function (ProcessorException $context) use ($result) { + $context->setResult($result); + }) + ; + $extension + ->expects($this->once()) + ->method('onResult') + ->willReturnCallback(function (MessageResult $context) use ($result) { + $this->assertSame($result, $context->getResult()); + }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); - try { - $queueConsumer->consume(); - } catch (\Exception $e) { - $this->assertSame($expectedException, $e); - $this->assertSame($mainException, $e->getPrevious()); - - return; - } - - $this->fail('Exception throw is expected.'); + $queueConsumer->consume(); } - public function testShouldAllowInterruptConsumingOnPreReceiveButProcessCurrentMessage() + public function testShouldCallOnPostMessageReceivedExtensionMethodWithExpectedContext() { $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); - $processorMock = $this->createProcessorMock(); - $processorMock - ->expects($this->once()) - ->method('process') - ->willReturn(Result::ACK) - ; + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorStub(); $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); - }) - ; - $extension - ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( + ->method('onPostMessageReceived') + ->with($this->isInstanceOf(PostMessageReceived::class)) + ->willReturnCallback(function (PostMessageReceived $context) use ( $contextStub, - $messageConsumerStub, - $processorMock, $expectedMessage ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + $this->assertSame($contextStub, $context->getContext()); + $this->assertSame($expectedMessage, $context->getMessage()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); $this->assertSame(Result::ACK, $context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertFalse($context->isExecutionInterrupted()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldAllowInterruptConsumingOnResult() + public function testShouldAllowInterruptConsumingOnPostConsume() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $consumerStub = $this->createConsumerStub('foo_queue'); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); - $processorMock - ->expects($this->once()) - ->method('process') - ->willReturn(Result::ACK) - ; $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onResult') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); + ->method('onPostConsume') + ->with($this->isInstanceOf(PostConsume::class)) + ->willReturnCallback(function (PostConsume $context) { + $context->interruptExecution(); }) ; $extension - ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); + ->expects($this->once()) + ->method('onEnd') + ->with($this->isInstanceOf(End::class)) + ->willReturnCallback(function (End $context) use ($contextStub) { + $this->assertSame($contextStub, $context->getContext()); $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertSame(Result::ACK, $context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); + $this->assertGreaterThan(1, $context->getStartTime()); + $this->assertGreaterThan(1, $context->getEndTime()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer(new DummySubscriptionConsumer()); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldAllowInterruptConsumingOnPostReceive() + public function testShouldSetMainExceptionAsPreviousToExceptionThrownOnProcessorException() + { + $mainException = new \Exception(); + $expectedException = new \Exception(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($this->createMessageMock(), 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); + + $processorMock = $this->createProcessorMock(); + $processorMock + ->expects($this->once()) + ->method('process') + ->willThrowException($mainException) + ; + + $extension = $this->createExtension(); + $extension + ->expects($this->atLeastOnce()) + ->method('onProcessorException') + ->willThrowException($expectedException) + ; + + $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); + + try { + $queueConsumer->consume(); + } catch (\Exception $e) { + $this->assertSame($expectedException, $e); + $this->assertSame($mainException, $e->getPrevious()); + + return; + } + + $this->fail('Exception throw is expected.'); + } + + public function testShouldAllowInterruptConsumingOnPostMessageReceived() { $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -891,47 +1152,37 @@ public function testShouldAllowInterruptConsumingOnPostReceive() $extension = $this->createExtension(); $extension ->expects($this->once()) - ->method('onPostReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) { - $context->setExecutionInterrupted(true); + ->method('onPostMessageReceived') + ->with($this->isInstanceOf(PostMessageReceived::class)) + ->willReturnCallback(function (PostMessageReceived $context) { + $context->interruptExecution(); }) ; $extension ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); - $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getException()); - $this->assertSame(Result::ACK, $context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); - }) + ->method('onEnd') + ->with($this->isInstanceOf(End::class)) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallOnInterruptedIfExceptionThrow() + public function testShouldNotCallOnEndIfExceptionThrow() { $expectedException = new \Exception('Process failed'); $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -942,30 +1193,14 @@ public function testShouldCallOnInterruptedIfExceptionThrow() $extension = $this->createExtension(); $extension - ->expects($this->atLeastOnce()) - ->method('onInterrupted') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ( - $contextStub, - $messageConsumerStub, - $processorMock, - $expectedMessage, - $expectedException - ) { - $this->assertSame($contextStub, $context->getPsrContext()); - $this->assertSame($messageConsumerStub, $context->getPsrConsumer()); - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($expectedMessage, $context->getPsrMessage()); - $this->assertSame($expectedException, $context->getException()); - $this->assertInstanceOf(NullLogger::class, $context->getLogger()); - $this->assertNull($context->getResult()); - $this->assertTrue($context->isExecutionInterrupted()); - }) + ->expects($this->never()) + ->method('onEnd') ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $this->expectException(\Exception::class); $this->expectExceptionMessage('Process failed'); @@ -975,9 +1210,13 @@ public function testShouldCallOnInterruptedIfExceptionThrow() public function testShouldCallExtensionPassedOnRuntime() { $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -987,44 +1226,59 @@ public function testShouldCallExtensionPassedOnRuntime() ; $runtimeExtension = $this->createExtension(); + $runtimeExtension + ->expects($this->once()) + ->method('onInitLogger') + ->with($this->isInstanceOf(InitLogger::class)) + ; $runtimeExtension ->expects($this->once()) ->method('onStart') - ->with($this->isInstanceOf(Context::class)) + ->with($this->isInstanceOf(Start::class)) ; $runtimeExtension ->expects($this->once()) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) + ->method('onPreSubscribe') + ->with($this->isInstanceOf(PreSubscribe::class)) ; $runtimeExtension ->expects($this->once()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) + ->method('onPreConsume') + ->with($this->isInstanceOf(PreConsume::class)) + ; + $runtimeExtension + ->expects($this->once()) + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) ; $runtimeExtension ->expects($this->once()) ->method('onResult') - ->with($this->isInstanceOf(Context::class)) + ->with($this->isInstanceOf(MessageResult::class)) ; $runtimeExtension ->expects($this->once()) - ->method('onPostReceived') - ->with($this->isInstanceOf(Context::class)) + ->method('onPostMessageReceived') + ->with($this->isInstanceOf(PostMessageReceived::class)) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1), 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(1)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(new ChainExtension([$runtimeExtension])); } - public function testShouldChangeLoggerOnStart() + public function testShouldChangeLoggerOnInitLogger() { $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($expectedMessage, 'foo_queue'); + + $consumerStub = $this->createConsumerStub('foo_queue'); + + $contextStub = $this->createContextStub($consumerStub); $processorMock = $this->createProcessorMock(); $processorMock @@ -1036,136 +1290,204 @@ public function testShouldChangeLoggerOnStart() $expectedLogger = new NullLogger(); $extension = $this->createExtension(); + $extension + ->expects($this->atLeastOnce()) + ->method('onInitLogger') + ->with($this->isInstanceOf(InitLogger::class)) + ->willReturnCallback(function (InitLogger $context) use ($expectedLogger) { + $context->changeLogger($expectedLogger); + }) + ; $extension ->expects($this->atLeastOnce()) ->method('onStart') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($expectedLogger) { - $context->setLogger($expectedLogger); + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($expectedLogger) { + $this->assertSame($expectedLogger, $context->getLogger()); + }) + ; + $extension + ->expects($this->atLeastOnce()) + ->method('onPreSubscribe') + ->with($this->isInstanceOf(PreSubscribe::class)) + ->willReturnCallback(function (PreSubscribe $context) use ($expectedLogger) { + $this->assertSame($expectedLogger, $context->getLogger()); }) ; $extension ->expects($this->atLeastOnce()) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($expectedLogger) { + ->method('onPreConsume') + ->with($this->isInstanceOf(PreConsume::class)) + ->willReturnCallback(function (PreConsume $context) use ($expectedLogger) { $this->assertSame($expectedLogger, $context->getLogger()); }) ; $extension ->expects($this->atLeastOnce()) - ->method('onPreReceived') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($expectedLogger) { + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) + ->willReturnCallback(function (MessageReceived $context) use ($expectedLogger) { $this->assertSame($expectedLogger, $context->getLogger()); }) ; $chainExtensions = new ChainExtension([$extension, new BreakCycleExtension(1)]); - $queueConsumer = new QueueConsumer($contextStub, $chainExtensions, 0); - $queueConsumer->bind(new NullQueue('aQueueName'), $processorMock); + $queueConsumer = new QueueConsumer($contextStub, $chainExtensions); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); + $queueConsumer->bind(new NullQueue('foo_queue'), $processorMock); $queueConsumer->consume(); } - public function testShouldCallEachQueueOneByOne() + public function testShouldCallProcessorAsMessageComeAlong() { - $expectedMessage = $this->createMessageMock(); - $messageConsumerStub = $this->createMessageConsumerStub($expectedMessage); + $queue1 = new NullQueue('foo_queue'); + $queue2 = new NullQueue('bar_queue'); + + $firstMessage = $this->createMessageMock(); + $secondMessage = $this->createMessageMock(); + $thirdMessage = $this->createMessageMock(); + + $subscriptionConsumerMock = new DummySubscriptionConsumer(); + $subscriptionConsumerMock->addMessage($firstMessage, 'foo_queue'); + $subscriptionConsumerMock->addMessage($secondMessage, 'bar_queue'); + $subscriptionConsumerMock->addMessage($thirdMessage, 'foo_queue'); + + $fooConsumerStub = $this->createConsumerStub($queue1); + $barConsumerStub = $this->createConsumerStub($queue2); - $contextStub = $this->createPsrContextStub($messageConsumerStub); + $consumers = [ + 'foo_queue' => $fooConsumerStub, + 'bar_queue' => $barConsumerStub, + ]; + + $contextStub = $this->createContextWithoutSubscriptionConsumerMock(); + $contextStub + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) + ; + $contextStub + ->expects($this->any()) + ->method('createConsumer') + ->willReturnCallback(function (Queue $queue) use ($consumers) { + return $consumers[$queue->getQueueName()]; + }) + ; $processorMock = $this->createProcessorStub(); $anotherProcessorMock = $this->createProcessorStub(); - $queue1 = new NullQueue('aQueueName'); - $queue2 = new NullQueue('aAnotherQueueName'); + /** @var MessageReceived[] $actualContexts */ + $actualContexts = []; $extension = $this->createExtension(); $extension - ->expects($this->at(1)) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($processorMock, $queue1) { - $this->assertSame($processorMock, $context->getPsrProcessor()); - $this->assertSame($queue1, $context->getPsrQueue()); - }) - ; - $extension - ->expects($this->at(5)) - ->method('onBeforeReceive') - ->with($this->isInstanceOf(Context::class)) - ->willReturnCallback(function (Context $context) use ($anotherProcessorMock, $queue2) { - $this->assertSame($anotherProcessorMock, $context->getPsrProcessor()); - $this->assertSame($queue2, $context->getPsrQueue()); + ->expects($this->any()) + ->method('onMessageReceived') + ->with($this->isInstanceOf(MessageReceived::class)) + ->willReturnCallback(function (MessageReceived $context) use (&$actualContexts) { + $actualContexts[] = clone $context; }) ; - $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(2), 0); + $queueConsumer = new QueueConsumer($contextStub, new BreakCycleExtension(3)); + $queueConsumer->setFallbackSubscriptionConsumer($subscriptionConsumerMock); $queueConsumer ->bind($queue1, $processorMock) ->bind($queue2, $anotherProcessorMock) ; $queueConsumer->consume(new ChainExtension([$extension])); + + $this->assertCount(3, $actualContexts); + + $this->assertSame($firstMessage, $actualContexts[0]->getMessage()); + $this->assertSame($secondMessage, $actualContexts[1]->getMessage()); + $this->assertSame($thirdMessage, $actualContexts[2]->getMessage()); + + $this->assertSame($fooConsumerStub, $actualContexts[0]->getConsumer()); + $this->assertSame($barConsumerStub, $actualContexts[1]->getConsumer()); + $this->assertSame($fooConsumerStub, $actualContexts[2]->getConsumer()); + } + + public function testCaptureExitStatus() + { + $testExitCode = 5; + + $stubExtension = $this->createExtension(); + + $stubExtension + ->expects($this->once()) + ->method('onStart') + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($testExitCode) { + $context->interruptExecution($testExitCode); + }) + ; + + $exitExtension = new ExitStatusExtension(); + + $consumer = new QueueConsumer($this->createContextStub(), $stubExtension); + $consumer->consume(new ChainExtension([$exitExtension])); + + $this->assertEquals($testExitCode, $exitExtension->getExitStatus()); } /** - * @param null|mixed $message - * - * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer + * @return \PHPUnit\Framework\MockObject\MockObject */ - protected function createMessageConsumerStub($message = null) + private function createContextWithoutSubscriptionConsumerMock(): InteropContext { - $messageConsumerMock = $this->createMock(PsrConsumer::class); - $messageConsumerMock + $contextMock = $this->createMock(InteropContext::class); + $contextMock ->expects($this->any()) - ->method('receive') - ->willReturn($message) + ->method('createSubscriptionConsumer') + ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt()) ; - return $messageConsumerMock; + return $contextMock; } /** - * @param null|mixed $messageConsumer - * - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return \PHPUnit\Framework\MockObject\MockObject|InteropContext */ - protected function createPsrContextStub($messageConsumer = null) + private function createContextStub(?Consumer $consumer = null): InteropContext { - $context = $this->createMock(PsrContext::class); - $context - ->expects($this->any()) - ->method('createConsumer') - ->willReturn($messageConsumer) - ; + $context = $this->createContextWithoutSubscriptionConsumerMock(); $context ->expects($this->any()) ->method('createQueue') - ->willReturn($this->createMock(PsrQueue::class)) + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) ; $context ->expects($this->any()) - ->method('close') + ->method('createConsumer') + ->willReturnCallback(function (Queue $queue) use ($consumer) { + return $consumer ?: $this->createConsumerStub($queue); + }) ; return $context; } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor + * @return \PHPUnit\Framework\MockObject\MockObject|Processor */ - protected function createProcessorMock() + private function createProcessorMock() { - return $this->createMock(PsrProcessor::class); + return $this->createMock(Processor::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor + * @return \PHPUnit\Framework\MockObject\MockObject|Processor */ - protected function createProcessorStub() + private function createProcessorStub() { $processorMock = $this->createProcessorMock(); $processorMock @@ -1178,18 +1500,50 @@ protected function createProcessorStub() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrMessage + * @return \PHPUnit\Framework\MockObject\MockObject|Message */ - protected function createMessageMock() + private function createMessageMock(): Message { - return $this->createMock(PsrMessage::class); + return $this->createMock(Message::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ExtensionInterface + * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface */ - protected function createExtension() + private function createExtension() { return $this->createMock(ExtensionInterface::class); } + + /** + * @param mixed|null $queue + * + * @return \PHPUnit\Framework\MockObject\MockObject|Consumer + */ + private function createConsumerStub($queue = null): Consumer + { + if (null === $queue) { + $queue = 'queue'; + } + if (is_string($queue)) { + $queue = new NullQueue($queue); + } + + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } + + /** + * @return SubscriptionConsumer|\PHPUnit\Framework\MockObject\MockObject + */ + private function createSubscriptionConsumerMock(): SubscriptionConsumer + { + return $this->createMock(SubscriptionConsumer::class); + } } diff --git a/pkg/enqueue/Tests/DoctrineConnectionFactoryFactoryTest.php b/pkg/enqueue/Tests/DoctrineConnectionFactoryFactoryTest.php new file mode 100644 index 000000000..14f7b1006 --- /dev/null +++ b/pkg/enqueue/Tests/DoctrineConnectionFactoryFactoryTest.php @@ -0,0 +1,71 @@ +registry = $this->prophesize(ManagerRegistry::class); + $this->fallbackFactory = $this->prophesize(ConnectionFactoryFactoryInterface::class); + + $this->factory = new DoctrineConnectionFactoryFactory($this->registry->reveal(), $this->fallbackFactory->reveal()); + } + + public function testCreateWithoutArray() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The config must be either array or DSN string.'); + + $this->factory->create(true); + } + + public function testCreateWithoutDsn() + { + $this->expectExceptionMessage(\InvalidArgumentException::class); + $this->expectExceptionMessage('The config must have dsn key set.'); + + $this->factory->create(['foo' => 'bar']); + } + + public function testCreateWithDoctrineSchema() + { + $this->assertInstanceOf( + ManagerRegistryConnectionFactory::class, + $this->factory->create('doctrine://localhost:3306') + ); + } + + public function testCreateFallback() + { + $this->fallbackFactory + ->create(['dsn' => 'fallback://']) + ->shouldBeCalled(); + + $this->factory->create(['dsn' => 'fallback://']); + } +} diff --git a/pkg/enqueue/Tests/Functions/DsnToConnectionFactoryFunctionTest.php b/pkg/enqueue/Tests/Functions/DsnToConnectionFactoryFunctionTest.php deleted file mode 100644 index 2c65eb6d9..000000000 --- a/pkg/enqueue/Tests/Functions/DsnToConnectionFactoryFunctionTest.php +++ /dev/null @@ -1,104 +0,0 @@ -expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme could not be parsed from DSN ""'); - - \Enqueue\dsn_to_connection_factory(''); - } - - public function testThrowIfDsnMissingScheme() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme could not be parsed from DSN "dsnMissingScheme"'); - - \Enqueue\dsn_to_connection_factory('dsnMissingScheme'); - } - - public function testThrowIfDsnNotSupported() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme "http" is not supported. Supported "file", "amqp+ext"'); - - \Enqueue\dsn_to_connection_factory('http://schemenotsupported/'); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedFactoryClass - */ - public function testReturnsExpectedFactoryInstance($dsn, $expectedFactoryClass) - { - $factory = \Enqueue\dsn_to_connection_factory($dsn); - - $this->assertInstanceOf($expectedFactoryClass, $factory); - } - - public static function provideDSNs() - { - yield ['amqp:', AmqpExtConnectionFactory::class]; - - yield ['amqps:', AmqpExtConnectionFactory::class]; - - yield ['amqp+ext:', AmqpExtConnectionFactory::class]; - - yield ['amqps+ext:', AmqpExtConnectionFactory::class]; - - yield ['amqp+lib:', AmqpLibConnectionFactory::class]; - - yield ['amqps+lib:', AmqpLibConnectionFactory::class]; - - yield ['amqp+bunny:', AmqpBunnyConnectionFactory::class]; - - yield ['amqp://user:pass@foo/vhost', AmqpExtConnectionFactory::class]; - - yield ['file:', FsConnectionFactory::class]; - - yield ['file:///foo/bar/baz', FsConnectionFactory::class]; - - yield ['null:', NullConnectionFactory::class]; - - yield ['mysql:', DbalConnectionFactory::class]; - - yield ['pgsql:', DbalConnectionFactory::class]; - - yield ['beanstalk:', PheanstalkConnectionFactory::class]; - -// yield ['gearman:', GearmanConnectionFactory::class]; - - yield ['kafka:', RdKafkaConnectionFactory::class]; - - yield ['redis:', RedisConnectionFactory::class]; - - yield ['stomp:', StompConnectionFactory::class]; - - yield ['sqs:', SqsConnectionFactory::class]; - - yield ['gps:', GpsConnectionFactory::class]; - - yield ['mongodb:', MongodbConnectionFactory::class]; - } -} diff --git a/pkg/enqueue/Tests/Functions/DsnToContextFunctionTest.php b/pkg/enqueue/Tests/Functions/DsnToContextFunctionTest.php deleted file mode 100644 index 2a8bc83bc..000000000 --- a/pkg/enqueue/Tests/Functions/DsnToContextFunctionTest.php +++ /dev/null @@ -1,73 +0,0 @@ -expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme could not be parsed from DSN ""'); - - \Enqueue\dsn_to_context(''); - } - - public function testThrowIfDsnMissingScheme() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme could not be parsed from DSN "dsnMissingScheme"'); - - \Enqueue\dsn_to_context('dsnMissingScheme'); - } - - public function testThrowIfDsnNotSupported() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The scheme "http" is not supported. Supported "file", "amqp+ext"'); - - \Enqueue\dsn_to_context('http://schemenotsupported/'); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedFactoryClass - */ - public function testReturnsExpectedFactoryInstance($dsn, $expectedFactoryClass) - { - $factory = \Enqueue\dsn_to_context($dsn); - - $this->assertInstanceOf($expectedFactoryClass, $factory); - } - - public static function provideDSNs() - { - yield ['amqp:', AmqpContext::class]; - - yield ['amqp://user:pass@foo/vhost', AmqpContext::class]; - - yield ['file:', FsContext::class]; - - yield ['file://'.sys_get_temp_dir(), FsContext::class]; - - yield ['null:', NullContext::class]; - - yield ['redis:', RedisContext::class]; - - yield ['stomp:', StompContext::class]; - - yield ['sqs:', SqsContext::class]; - - yield ['gps:', GpsContext::class]; - } -} diff --git a/pkg/enqueue/Tests/Mocks/CustomPrepareBodyClientExtension.php b/pkg/enqueue/Tests/Mocks/CustomPrepareBodyClientExtension.php new file mode 100644 index 000000000..dd0e1a69e --- /dev/null +++ b/pkg/enqueue/Tests/Mocks/CustomPrepareBodyClientExtension.php @@ -0,0 +1,20 @@ +getMessage()->setBody('theCommandBodySerializedByCustomExtension'); + } + + public function onPreSendEvent(PreSend $context): void + { + $context->getMessage()->setBody('theEventBodySerializedByCustomExtension'); + } +} diff --git a/pkg/enqueue/Tests/Mocks/JsonSerializableObject.php b/pkg/enqueue/Tests/Mocks/JsonSerializableObject.php new file mode 100644 index 000000000..84885c316 --- /dev/null +++ b/pkg/enqueue/Tests/Mocks/JsonSerializableObject.php @@ -0,0 +1,12 @@ + 'fooVal']; + } +} diff --git a/pkg/enqueue/Tests/ResourcesTest.php b/pkg/enqueue/Tests/ResourcesTest.php new file mode 100644 index 000000000..ec713fd03 --- /dev/null +++ b/pkg/enqueue/Tests/ResourcesTest.php @@ -0,0 +1,149 @@ +assertTrue($rc->isFinal()); + } + + public function testShouldConstructorBePrivate() + { + $rc = new \ReflectionClass(Resources::class); + + $this->assertTrue($rc->getConstructor()->isPrivate()); + } + + public function testShouldGetAvailableConnectionsInExpectedFormat() + { + $availableConnections = Resources::getAvailableConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayHasKey(RedisConnectionFactory::class, $availableConnections); + + $connectionInfo = $availableConnections[RedisConnectionFactory::class]; + $this->assertArrayHasKey('schemes', $connectionInfo); + $this->assertSame(['redis', 'rediss'], $connectionInfo['schemes']); + + $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo); + $this->assertSame(['predis', 'phpredis'], $connectionInfo['supportedSchemeExtensions']); + + $this->assertArrayHasKey('package', $connectionInfo); + $this->assertSame('enqueue/redis', $connectionInfo['package']); + } + + public function testShouldGetKnownConnectionsInExpectedFormat() + { + $availableConnections = Resources::getKnownConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayHasKey(RedisConnectionFactory::class, $availableConnections); + + $connectionInfo = $availableConnections[RedisConnectionFactory::class]; + $this->assertArrayHasKey('schemes', $connectionInfo); + $this->assertSame(['redis', 'rediss'], $connectionInfo['schemes']); + + $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo); + $this->assertSame(['predis', 'phpredis'], $connectionInfo['supportedSchemeExtensions']); + + $this->assertArrayHasKey('package', $connectionInfo); + $this->assertSame('enqueue/redis', $connectionInfo['package']); + } + + public function testThrowsIfConnectionClassNotImplementConnectionFactoryInterfaceOnAddConnection() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The connection factory class "stdClass" must implement "Interop\Queue\ConnectionFactory" interface.'); + + Resources::addConnection(\stdClass::class, [], [], 'foo'); + } + + public function testThrowsIfNoSchemesProvidedOnAddConnection() + { + $connectionClass = $this->getMockClass(ConnectionFactory::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Schemes could not be empty.'); + + Resources::addConnection($connectionClass, [], [], 'foo'); + } + + public function testThrowsIfNoPackageProvidedOnAddConnection() + { + $connectionClass = $this->getMockClass(ConnectionFactory::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Package name could not be empty.'); + + Resources::addConnection($connectionClass, ['foo'], [], ''); + } + + public function testShouldAllowRegisterConnectionThatIsNotInstalled() + { + Resources::addConnection('theConnectionClass', ['foo'], [], 'foo'); + + $knownConnections = Resources::getKnownConnections(); + self::assertIsArray($knownConnections); + $this->assertArrayHasKey('theConnectionClass', $knownConnections); + + $availableConnections = Resources::getAvailableConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayNotHasKey('theConnectionClass', $availableConnections); + } + + public function testShouldAllowGetPreviouslyRegisteredConnection() + { + $connectionClass = $this->getMockClass(ConnectionFactory::class); + + Resources::addConnection( + $connectionClass, + ['fooscheme', 'barscheme'], + ['fooextension', 'barextension'], + 'foo/bar' + ); + + $availableConnections = Resources::getAvailableConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayHasKey($connectionClass, $availableConnections); + + $connectionInfo = $availableConnections[$connectionClass]; + $this->assertArrayHasKey('schemes', $connectionInfo); + $this->assertSame(['fooscheme', 'barscheme'], $connectionInfo['schemes']); + + $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo); + $this->assertSame(['fooextension', 'barextension'], $connectionInfo['supportedSchemeExtensions']); + + $this->assertArrayHasKey('package', $connectionInfo); + $this->assertSame('foo/bar', $connectionInfo['package']); + } + + public function testShouldHaveRegisteredWampConfiguration() + { + $availableConnections = Resources::getKnownConnections(); + + self::assertIsArray($availableConnections); + $this->assertArrayHasKey(WampConnectionFactory::class, $availableConnections); + + $connectionInfo = $availableConnections[WampConnectionFactory::class]; + $this->assertArrayHasKey('schemes', $connectionInfo); + $this->assertSame(['wamp', 'ws'], $connectionInfo['schemes']); + + $this->assertArrayHasKey('supportedSchemeExtensions', $connectionInfo); + $this->assertSame([], $connectionInfo['supportedSchemeExtensions']); + + $this->assertArrayHasKey('package', $connectionInfo); + $this->assertSame('enqueue/wamp', $connectionInfo['package']); + } +} diff --git a/pkg/enqueue/Tests/Router/RecipientTest.php b/pkg/enqueue/Tests/Router/RecipientTest.php index 3f3737234..57cc83eed 100644 --- a/pkg/enqueue/Tests/Router/RecipientTest.php +++ b/pkg/enqueue/Tests/Router/RecipientTest.php @@ -3,26 +3,26 @@ namespace Enqueue\Tests\Router; use Enqueue\Router\Recipient; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrMessage; +use Interop\Queue\Destination; +use Interop\Queue\Message as InteropMessage; use PHPUnit\Framework\TestCase; class RecipientTest extends TestCase { public function testShouldAllowGetMessageSetInConstructor() { - $message = $this->createMock(PsrMessage::class); + $message = $this->createMock(InteropMessage::class); - $recipient = new Recipient($this->createMock(PsrDestination::class), $message); + $recipient = new Recipient($this->createMock(Destination::class), $message); $this->assertSame($message, $recipient->getMessage()); } public function testShouldAllowGetDestinationSetInConstructor() { - $destination = $this->createMock(PsrDestination::class); + $destination = $this->createMock(Destination::class); - $recipient = new Recipient($destination, $this->createMock(PsrMessage::class)); + $recipient = new Recipient($destination, $this->createMock(InteropMessage::class)); $this->assertSame($destination, $recipient->getDestination()); } diff --git a/pkg/enqueue/Tests/Router/RouteRecipientListProcessorTest.php b/pkg/enqueue/Tests/Router/RouteRecipientListProcessorTest.php index b7980430f..ae878fc53 100644 --- a/pkg/enqueue/Tests/Router/RouteRecipientListProcessorTest.php +++ b/pkg/enqueue/Tests/Router/RouteRecipientListProcessorTest.php @@ -9,9 +9,9 @@ use Enqueue\Router\RecipientListRouterInterface; use Enqueue\Router\RouteRecipientListProcessor; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; -use Interop\Queue\PsrProducer; +use Interop\Queue\Context; +use Interop\Queue\Processor; +use Interop\Queue\Producer as InteropProducer; use PHPUnit\Framework\TestCase; class RouteRecipientListProcessorTest extends TestCase @@ -20,12 +20,7 @@ class RouteRecipientListProcessorTest extends TestCase public function testShouldImplementProcessorInterface() { - $this->assertClassImplements(PsrProcessor::class, RouteRecipientListProcessor::class); - } - - public function testCouldBeConstructedWithRouterAsFirstArgument() - { - new RouteRecipientListProcessor($this->createRecipientListRouterMock()); + $this->assertClassImplements(Processor::class, RouteRecipientListProcessor::class); } public function testShouldProduceRecipientsMessagesAndAckOriginalMessage() @@ -55,7 +50,7 @@ public function testShouldProduceRecipientsMessagesAndAckOriginalMessage() ->with($this->identicalTo($barRecipient->getDestination()), $this->identicalTo($barRecipient->getMessage())) ; - $sessionMock = $this->createPsrContextMock(); + $sessionMock = $this->createContextMock(); $sessionMock ->expects($this->once()) ->method('createProducer') @@ -70,23 +65,23 @@ public function testShouldProduceRecipientsMessagesAndAckOriginalMessage() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer + * @return \PHPUnit\Framework\MockObject\MockObject|InteropProducer */ protected function createProducerMock() { - return $this->createMock(PsrProducer::class); + return $this->createMock(InteropProducer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return \PHPUnit\Framework\MockObject\MockObject|Context */ - protected function createPsrContextMock() + protected function createContextMock() { - return $this->createMock(PsrContext::class); + return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|RecipientListRouterInterface + * @return \PHPUnit\Framework\MockObject\MockObject|RecipientListRouterInterface */ protected function createRecipientListRouterMock() { diff --git a/pkg/enqueue/Tests/Rpc/PromiseTest.php b/pkg/enqueue/Tests/Rpc/PromiseTest.php index 7202e67fc..6762149ef 100644 --- a/pkg/enqueue/Tests/Rpc/PromiseTest.php +++ b/pkg/enqueue/Tests/Rpc/PromiseTest.php @@ -50,10 +50,10 @@ public function testOnReceiveShouldCallReceiveCallBackWithTimeout() $receiveInvoked = false; $receivePromise = null; $receiveTimeout = null; - $receivecb = function ($promise, $timout) use (&$receiveInvoked, &$receivePromise, &$receiveTimeout) { + $receivecb = function ($promise, $timeout) use (&$receiveInvoked, &$receivePromise, &$receiveTimeout) { $receiveInvoked = true; $receivePromise = $promise; - $receiveTimeout = $timout; + $receiveTimeout = $timeout; }; $promise = new Promise($receivecb, function () {}, function () {}); @@ -122,7 +122,7 @@ public function testOnReceiveShouldThrowExceptionIfCallbackReturnNotMessageInsta $promise = new Promise($receivecb, function () {}, function () {}); $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Expected "Interop\Queue\PsrMessage" but got: "stdClass"'); + $this->expectExceptionMessage('Expected "Interop\Queue\Message" but got: "stdClass"'); $promise->receive(); } @@ -136,7 +136,7 @@ public function testOnReceiveNoWaitShouldThrowExceptionIfCallbackReturnNotMessag $promise = new Promise(function () {}, $receivecb, function () {}); $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Expected "Interop\Queue\PsrMessage" but got: "stdClass"'); + $this->expectExceptionMessage('Expected "Interop\Queue\Message" but got: "stdClass"'); $promise->receiveNoWait(); } diff --git a/pkg/enqueue/Tests/Rpc/RpcClientTest.php b/pkg/enqueue/Tests/Rpc/RpcClientTest.php index ce7aa2785..db4813c88 100644 --- a/pkg/enqueue/Tests/Rpc/RpcClientTest.php +++ b/pkg/enqueue/Tests/Rpc/RpcClientTest.php @@ -7,18 +7,14 @@ use Enqueue\Null\NullQueue; use Enqueue\Rpc\Promise; use Enqueue\Rpc\RpcClient; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProducer; +use Interop\Queue\Consumer; +use Interop\Queue\Context; +use Interop\Queue\Producer as InteropProducer; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class RpcClientTest extends TestCase { - public function testCouldBeConstructedWithPsrContextAsFirstArgument() - { - new RpcClient($this->createPsrContextMock()); - } - public function testShouldSetReplyToIfNotSet() { $context = new NullContext(); @@ -80,14 +76,14 @@ public function testShouldProduceMessageToQueue() $message->setCorrelationId('theCorrelationId'); $message->setReplyTo('theReplyTo'); - $producer = $this->createPsrProducerMock(); + $producer = $this->createInteropProducerMock(); $producer ->expects($this->once()) ->method('send') ->with($this->identicalTo($queue), $this->identicalTo($message)) ; - $context = $this->createPsrContextMock(); + $context = $this->createContextMock(); $context ->expects($this->once()) ->method('createProducer') @@ -110,7 +106,7 @@ public function testShouldReceiveMessageAndAckMessageIfCorrelationEquals() $receivedMessage = new NullMessage(); $receivedMessage->setCorrelationId('theCorrelationId'); - $consumer = $this->createPsrConsumerMock(); + $consumer = $this->createConsumerMock(); $consumer ->expects($this->once()) ->method('receive') @@ -127,11 +123,11 @@ public function testShouldReceiveMessageAndAckMessageIfCorrelationEquals() ->method('reject') ; - $context = $this->createPsrContextMock(); + $context = $this->createContextMock(); $context ->expects($this->once()) ->method('createProducer') - ->willReturn($this->createPsrProducerMock()) + ->willReturn($this->createInteropProducerMock()) ; $context ->expects($this->atLeastOnce()) @@ -162,7 +158,7 @@ public function testShouldReceiveNoWaitMessageAndAckMessageIfCorrelationEquals() $receivedMessage = new NullMessage(); $receivedMessage->setCorrelationId('theCorrelationId'); - $consumer = $this->createPsrConsumerMock(); + $consumer = $this->createConsumerMock(); $consumer ->expects($this->once()) ->method('receiveNoWait') @@ -178,11 +174,11 @@ public function testShouldReceiveNoWaitMessageAndAckMessageIfCorrelationEquals() ->method('reject') ; - $context = $this->createPsrContextMock(); + $context = $this->createContextMock(); $context ->expects($this->once()) ->method('createProducer') - ->willReturn($this->createPsrProducerMock()) + ->willReturn($this->createInteropProducerMock()) ; $context ->expects($this->atLeastOnce()) @@ -213,14 +209,14 @@ public function testShouldDeleteQueueAfterReceiveIfDeleteReplyQueueIsTrue() $receivedMessage = new NullMessage(); $receivedMessage->setCorrelationId('theCorrelationId'); - $consumer = $this->createPsrConsumerMock(); + $consumer = $this->createConsumerMock(); $consumer ->expects($this->once()) ->method('receive') ->willReturn($receivedMessage) ; - $context = $this->getMockBuilder(PsrContext::class) + $context = $this->getMockBuilder(Context::class) ->disableOriginalConstructor() ->setMethods(['deleteQueue']) ->getMockForAbstractClass() @@ -229,7 +225,7 @@ public function testShouldDeleteQueueAfterReceiveIfDeleteReplyQueueIsTrue() $context ->expects($this->once()) ->method('createProducer') - ->willReturn($this->createPsrProducerMock()) + ->willReturn($this->createInteropProducerMock()) ; $context ->expects($this->atLeastOnce()) @@ -267,18 +263,18 @@ public function testShouldNotCallDeleteQueueIfDeleteReplyQueueIsTrueButContextHa $receivedMessage = new NullMessage(); $receivedMessage->setCorrelationId('theCorrelationId'); - $consumer = $this->createPsrConsumerMock(); + $consumer = $this->createConsumerMock(); $consumer ->expects($this->once()) ->method('receive') ->willReturn($receivedMessage) ; - $context = $this->createPsrContextMock(); + $context = $this->createContextMock(); $context ->expects($this->once()) ->method('createProducer') - ->willReturn($this->createPsrProducerMock()) + ->willReturn($this->createInteropProducerMock()) ; $context ->expects($this->once()) @@ -329,26 +325,26 @@ public function testShouldDoSyncCall() } /** - * @return PsrContext|\PHPUnit_Framework_MockObject_MockObject|PsrProducer + * @return Context|MockObject|InteropProducer */ - private function createPsrProducerMock() + private function createInteropProducerMock() { - return $this->createMock(PsrProducer::class); + return $this->createMock(InteropProducer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrConsumer + * @return MockObject|Consumer */ - private function createPsrConsumerMock() + private function createConsumerMock() { - return $this->createMock(PsrConsumer::class); + return $this->createMock(Consumer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|Context */ - private function createPsrContextMock() + private function createContextMock() { - return $this->createMock(PsrContext::class); + return $this->createMock(Context::class); } } diff --git a/pkg/enqueue/Tests/Symfony/AmqpTransportFactoryTest.php b/pkg/enqueue/Tests/Symfony/AmqpTransportFactoryTest.php deleted file mode 100644 index b27f5b5ca..000000000 --- a/pkg/enqueue/Tests/Symfony/AmqpTransportFactoryTest.php +++ /dev/null @@ -1,404 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, AmqpTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new AmqpTransportFactory(); - - $this->assertEquals('amqp', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new AmqpTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testThrowIfCouldBeConstructedWithCustomName() - { - $transport = new AmqpTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'read_timeout' => 3., - 'write_timeout' => 3., - 'connection_timeout' => 3., - 'heartbeat' => 0, - 'persisted' => false, - 'lazy' => true, - 'qos_global' => false, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'receive_method' => 'basic_get', - ]]); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'read_timeout' => 3., - 'write_timeout' => 3., - 'connection_timeout' => 3., - 'heartbeat' => 0, - 'persisted' => false, - 'lazy' => true, - 'qos_global' => false, - 'qos_prefetch_size' => 0, - 'qos_prefetch_count' => 1, - 'receive_method' => 'basic_get', - ], $config); - } - - public function testShouldAllowAddConfigurationWithDriverOptions() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'host' => 'localhost', - 'driver_options' => [ - 'foo' => 'fooVal', - ], - ]]); - - $this->assertEquals([ - 'host' => 'localhost', - 'driver_options' => [ - 'foo' => 'fooVal', - ], - ], $config); - } - - public function testShouldAllowAddSslOptions() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'ssl_on' => true, - 'ssl_verify' => false, - 'ssl_cacert' => '/path/to/cacert.pem', - 'ssl_cert' => '/path/to/cert.pem', - 'ssl_key' => '/path/to/key.pem', - ]]); - - $this->assertEquals([ - 'ssl_on' => true, - 'ssl_verify' => false, - 'ssl_cacert' => '/path/to/cacert.pem', - 'ssl_cert' => '/path/to/cert.pem', - 'ssl_key' => '/path/to/key.pem', - ], $config); - } - - public function testThrowIfNotSupportedDriverSet() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Invalid configuration for path "foo.driver": Unexpected driver given "invalidDriver"'); - $processor->process($tb->buildTree(), [[ - 'driver' => 'invalidDriver', - ]]); - } - - public function testShouldAllowSetDriver() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'driver' => 'ext', - ]]); - - $this->assertEquals([ - 'driver' => 'ext', - ], $config); - } - - public function testShouldAllowAddConfigurationAsString() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['amqpDSN']); - - $this->assertEquals([ - 'dsn' => 'amqpDSN', - ], $config); - } - - public function testThrowIfInvalidReceiveMethodIsSet() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The value "anInvalidMethod" is not allowed for path "foo.receive_method". Permissible values: "basic_get", "basic_consume"'); - $processor->process($tb->buildTree(), [[ - 'receive_method' => 'anInvalidMethod', - ]]); - } - - public function testShouldAllowChangeReceiveMethod() - { - $transport = new AmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'receive_method' => 'basic_consume', - ]]); - - $this->assertEquals([ - 'receive_method' => 'basic_consume', - ], $config); - } - - public function testShouldCreateConnectionFactoryForEmptyConfig() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, []); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - - $this->assertSame([[]], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryFromDsnString() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theConnectionDSN:', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([['dsn' => 'theConnectionDSN:']], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryAndMergeDriverOptionsIfSet() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'aHost', - 'driver_options' => [ - 'foo' => 'fooVal', - ], - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([['foo' => 'fooVal', 'host' => 'aHost']], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryFromDsnStringPlushArrayOptions() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - ]], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - ]); - - $this->assertEquals('enqueue.transport.amqp.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.amqp.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.amqp.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new AmqpTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.amqp.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(AmqpDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.amqp.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } - - public function testShouldCreateAmqpExtConnectionFactoryBySetDriver() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'ext']); - - $this->assertInstanceOf(\Enqueue\AmqpExt\AmqpConnectionFactory::class, $factory); - } - - public function testShouldCreateAmqpLibConnectionFactoryBySetDriver() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'lib']); - - $this->assertInstanceOf(\Enqueue\AmqpLib\AmqpConnectionFactory::class, $factory); - } - - public function testShouldCreateAmqpBunnyConnectionFactoryBySetDriver() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'bunny']); - - $this->assertInstanceOf(\Enqueue\AmqpBunny\AmqpConnectionFactory::class, $factory); - } - - public function testShouldCreateAmqpExtFromConfigWithoutDriverAndDsn() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['host' => 'aHost']); - - $this->assertInstanceOf(\Enqueue\AmqpExt\AmqpConnectionFactory::class, $factory); - } - - public function testThrowIfInvalidDriverGiven() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Unexpected driver given "invalidDriver"'); - - AmqpTransportFactory::createConnectionFactoryFactory(['driver' => 'invalidDriver']); - } - - public function testShouldCreateAmqpExtFromDsn() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['dsn' => 'amqp:']); - - $this->assertInstanceOf(\Enqueue\AmqpExt\AmqpConnectionFactory::class, $factory); - } - - public function testShouldCreateAmqpBunnyFromDsnWithDriver() - { - $factory = AmqpTransportFactory::createConnectionFactoryFactory(['dsn' => 'amqp+bunny:']); - - $this->assertInstanceOf(\Enqueue\AmqpBunny\AmqpConnectionFactory::class, $factory); - } - - public function testThrowIfNotAmqpDsnProvided() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Factory must be instance of "Interop\Amqp\AmqpConnectionFactory" but got "Enqueue\Sqs\SqsConnectionFactory"'); - - AmqpTransportFactory::createConnectionFactoryFactory(['dsn' => 'sqs:']); - } -} diff --git a/pkg/enqueue/Tests/Symfony/Client/ConsumeCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/ConsumeCommandTest.php new file mode 100644 index 000000000..3758ca96a --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/ConsumeCommandTest.php @@ -0,0 +1,703 @@ +assertClassExtends(Command::class, ConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(ConsumeCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = ConsumeCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:consume', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveExpectedOptions() + { + $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(9, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('client', $options); + $this->assertArrayHasKey('logger', $options); + $this->assertArrayHasKey('skip', $options); + $this->assertArrayHasKey('setup-broker', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(1, $arguments); + $this->assertArrayHasKey('client-queue-names', $arguments); + } + + public function testShouldBindDefaultQueueOnly() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([]); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('default', true) + ->willReturn($queue) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldUseRequestedClient() + { + $defaultProcessor = $this->createDelegateProcessorMock(); + + $defaultConsumer = $this->createQueueConsumerMock(); + $defaultConsumer + ->expects($this->never()) + ->method('bind') + ; + $defaultConsumer + ->expects($this->never()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $defaultDriver = $this->createDriverStub(new RouteCollection([])); + $defaultDriver + ->expects($this->never()) + ->method('createQueue') + ; + + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([]); + + $fooProcessor = $this->createDelegateProcessorMock(); + + $fooConsumer = $this->createQueueConsumerMock(); + $fooConsumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($fooProcessor)) + ; + $fooConsumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $fooDriver = $this->createDriverStub($routeCollection); + $fooDriver + ->expects($this->once()) + ->method('createQueue') + ->with('default', true) + ->willReturn($queue) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $defaultConsumer, + 'enqueue.client.default.driver' => $defaultDriver, + 'enqueue.client.default.delegate_processor' => $defaultProcessor, + 'enqueue.client.foo.queue_consumer' => $fooConsumer, + 'enqueue.client.foo.driver' => $fooDriver, + 'enqueue.client.foo.delegate_processor' => $fooProcessor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + '--client' => 'foo', + ]); + } + + public function testThrowIfNotDefinedClientRequested() + { + $routeCollection = new RouteCollection([]); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->never()) + ->method('bind') + ; + $consumer + ->expects($this->never()) + ->method('consume') + ; + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->never()) + ->method('createQueue') + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Client "not-defined" is not supported.'); + $tester->execute([ + '--client' => 'not-defined', + ]); + } + + public function testShouldBindDefaultQueueIfRouteUseDifferentQueue() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor'), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('default', true) + ->willReturn($queue) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldBindCustomExecuteConsumptionAndUseCustomClientDestinationName() + { + $defaultQueue = new NullQueue(''); + $customQueue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'custom']), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->at(3)) + ->method('createQueue') + ->with('default', true) + ->willReturn($defaultQueue) + ; + $driver + ->expects($this->at(4)) + ->method('createQueue') + ->with('custom', true) + ->willReturn($customQueue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->at(0)) + ->method('bind') + ->with($this->identicalTo($defaultQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('bind') + ->with($this->identicalTo($customQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(2)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldBindUserProvidedQueues() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'custom']), + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'non-default-queue']), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('non-default-queue', true) + ->willReturn($queue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'client-queue-names' => ['non-default-queue'], + ]); + } + + public function testShouldBindNotPrefixedQueue() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('topic', Route::TOPIC, 'processor', ['queue' => 'non-prefixed-queue', 'prefix_queue' => false]), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('non-prefixed-queue', false) + ->willReturn($queue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'client-queue-names' => ['non-prefixed-queue'], + ]); + } + + public function testShouldBindQueuesOnlyOnce() + { + $defaultQueue = new NullQueue(''); + $customQueue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('fooTopic', Route::TOPIC, 'processor', ['queue' => 'custom']), + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'custom']), + new Route('ololoTopic', Route::TOPIC, 'processor', []), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->at(3)) + ->method('createQueue') + ->with('default', true) + ->willReturn($defaultQueue) + ; + $driver + ->expects($this->at(4)) + ->method('createQueue', true) + ->with('custom') + ->willReturn($customQueue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->at(0)) + ->method('bind') + ->with($this->identicalTo($defaultQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('bind') + ->with($this->identicalTo($customQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(2)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldNotBindExternalRoutes() + { + $defaultQueue = new NullQueue(''); + + $routeCollection = new RouteCollection([ + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => null]), + new Route('fooTopic', Route::TOPIC, 'processor', ['queue' => 'external_queue', 'external' => true]), + ]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->exactly(1)) + ->method('createQueue') + ->with('default', true) + ->willReturn($defaultQueue) + ; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->exactly(1)) + ->method('bind') + ->with($this->identicalTo($defaultQueue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldSkipQueueConsumptionAndUseCustomClientDestinationName() + { + $queue = new NullQueue(''); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->exactly(3)) + ->method('bind') + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $routeCollection = new RouteCollection([ + new Route('fooTopic', Route::TOPIC, 'processor', ['queue' => 'fooQueue']), + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'barQueue']), + new Route('ololoTopic', Route::TOPIC, 'processor', ['queue' => 'ololoQueue']), + ]); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->at(3)) + ->method('createQueue', true) + ->with('default') + ->willReturn($queue) + ; + $driver + ->expects($this->at(4)) + ->method('createQueue', true) + ->with('fooQueue') + ->willReturn($queue) + ; + $driver + ->expects($this->at(5)) + ->method('createQueue', true) + ->with('ololoQueue') + ->willReturn($queue) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + '--skip' => ['barQueue'], + ]); + } + + public function testShouldReturnExitStatusIfSet() + { + $testExitCode = 678; + + $stubExtension = $this->createExtension(); + + $stubExtension + ->expects($this->once()) + ->method('onStart') + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($testExitCode) { + $context->interruptExecution($testExitCode); + }) + ; + + $defaultQueue = new NullQueue('default'); + + $routeCollection = new RouteCollection([]); + + $processor = $this->createDelegateProcessorMock(); + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->exactly(1)) + ->method('createQueue') + ->with('default', true) + ->willReturn($defaultQueue) + ; + + $consumer = new QueueConsumer($this->createContextStub(), $stubExtension); + + $command = new ConsumeCommand(new Container([ + 'enqueue.client.default.queue_consumer' => $consumer, + 'enqueue.client.default.driver' => $driver, + 'enqueue.client.default.delegate_processor' => $processor, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertEquals($testExitCode, $tester->getStatusCode()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DelegateProcessor + */ + private function createDelegateProcessorMock() + { + return $this->createMock(DelegateProcessor::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + private function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface + { + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection ?? new RouteCollection([])) + ; + + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn(Config::create('aPrefix', 'anApp')) + ; + + return $driverMock; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createContextWithoutSubscriptionConsumerMock(): InteropContext + { + $contextMock = $this->createMock(InteropContext::class); + $contextMock + ->expects($this->any()) + ->method('createSubscriptionConsumer') + ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt()) + ; + + return $contextMock; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|InteropContext + */ + private function createContextStub(?Consumer $consumer = null): InteropContext + { + $context = $this->createContextWithoutSubscriptionConsumerMock(); + $context + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) + ; + $context + ->expects($this->any()) + ->method('createConsumer') + ->willReturnCallback(function (Queue $queue) use ($consumer) { + return $consumer ?: $this->createConsumerStub($queue); + }) + ; + + return $context; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface + */ + private function createExtension() + { + return $this->createMock(ExtensionInterface::class); + } + + /** + * @param mixed|null $queue + * + * @return \PHPUnit\Framework\MockObject\MockObject|Consumer + */ + private function createConsumerStub($queue = null): Consumer + { + if (null === $queue) { + $queue = 'queue'; + } + if (is_string($queue)) { + $queue = new NullQueue($queue); + } + + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/ConsumeMessagesCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/ConsumeMessagesCommandTest.php deleted file mode 100644 index 396417e2b..000000000 --- a/pkg/enqueue/Tests/Symfony/Client/ConsumeMessagesCommandTest.php +++ /dev/null @@ -1,283 +0,0 @@ -createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - } - - public function testShouldHaveCommandName() - { - $command = new ConsumeMessagesCommand( - $this->createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - - $this->assertEquals('enqueue:consume', $command->getName()); - } - - public function testShouldHaveCommandAliases() - { - $command = new ConsumeMessagesCommand( - $this->createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - - $this->assertEquals(['enq:c'], $command->getAliases()); - } - - public function testShouldHaveExpectedOptions() - { - $command = new ConsumeMessagesCommand( - $this->createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - - $options = $command->getDefinition()->getOptions(); - - $this->assertCount(8, $options); - $this->assertArrayHasKey('memory-limit', $options); - $this->assertArrayHasKey('message-limit', $options); - $this->assertArrayHasKey('time-limit', $options); - $this->assertArrayHasKey('setup-broker', $options); - $this->assertArrayHasKey('idle-timeout', $options); - $this->assertArrayHasKey('receive-timeout', $options); - $this->assertArrayHasKey('skip', $options); - $this->assertArrayHasKey('niceness', $options); - } - - public function testShouldHaveExpectedArguments() - { - $command = new ConsumeMessagesCommand( - $this->createQueueConsumerMock(), - $this->createDelegateProcessorMock(), - $this->createQueueMetaRegistry([]), - $this->createDriverMock() - ); - - $arguments = $command->getDefinition()->getArguments(); - - $this->assertCount(1, $arguments); - $this->assertArrayHasKey('client-queue-names', $arguments); - } - - public function testShouldExecuteConsumptionAndUseDefaultQueueName() - { - $queue = new NullQueue(''); - - $processor = $this->createDelegateProcessorMock(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->once()) - ->method('bind') - ->with($this->identicalTo($queue), $this->identicalTo($processor)) - ; - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $queueMetaRegistry = $this->createQueueMetaRegistry([ - 'default' => [], - ]); - - $driver = $this->createDriverMock(); - $driver - ->expects($this->once()) - ->method('createQueue') - ->with('default') - ->willReturn($queue) - ; - - $command = new ConsumeMessagesCommand($consumer, $processor, $queueMetaRegistry, $driver); - - $tester = new CommandTester($command); - $tester->execute([]); - } - - public function testShouldExecuteConsumptionAndUseCustomClientDestinationName() - { - $queue = new NullQueue(''); - - $processor = $this->createDelegateProcessorMock(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->once()) - ->method('bind') - ->with($this->identicalTo($queue), $this->identicalTo($processor)) - ; - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $queueMetaRegistry = $this->createQueueMetaRegistry([ - 'non-default-queue' => [], - ]); - - $driver = $this->createDriverMock(); - $driver - ->expects($this->once()) - ->method('createQueue') - ->with('non-default-queue') - ->willReturn($queue) - ; - - $command = new ConsumeMessagesCommand($consumer, $processor, $queueMetaRegistry, $driver); - - $tester = new CommandTester($command); - $tester->execute([ - 'client-queue-names' => ['non-default-queue'], - ]); - } - - public function testShouldSkipQueueConsumptionAndUseCustomClientDestinationName() - { - $queue = new NullQueue(''); - - $processor = $this->createDelegateProcessorMock(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->exactly(2)) - ->method('bind') - ; - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $queueMetaRegistry = $this->createQueueMetaRegistry([ - 'fooQueue' => [ - 'transportName' => 'fooTransportQueue', - ], - 'barQueue' => [ - 'transportName' => 'barTransportQueue', - ], - 'ololoQueue' => [ - 'transportName' => 'ololoTransportQueue', - ], - ]); - - $driver = $this->createDriverMock(); - $driver - ->expects($this->at(0)) - ->method('createQueue') - ->with('fooQueue') - ->willReturn($queue) - ; - $driver - ->expects($this->at(1)) - ->method('createQueue') - ->with('ololoQueue') - ->willReturn($queue) - ; - - $command = new ConsumeMessagesCommand($consumer, $processor, $queueMetaRegistry, $driver); - - $tester = new CommandTester($command); - $tester->execute([ - '--skip' => ['barQueue'], - ]); - } - - /** - * @param array $destinationNames - * - * @return QueueMetaRegistry - */ - private function createQueueMetaRegistry(array $destinationNames) - { - $config = new Config( - 'aPrefix', - 'anApp', - 'aRouterTopicName', - 'aRouterQueueName', - 'aDefaultQueueName', - 'aRouterProcessorName' - ); - - return new QueueMetaRegistry($config, $destinationNames); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - private function createPsrContextMock() - { - return $this->createMock(PsrContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|DelegateProcessor - */ - private function createDelegateProcessorMock() - { - return $this->createMock(DelegateProcessor::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|QueueConsumer - */ - private function createQueueConsumerMock() - { - return $this->createMock(QueueConsumer::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface - */ - private function createDriverMock() - { - return $this->createMock(DriverInterface::class); - } -} diff --git a/pkg/enqueue/Tests/Symfony/Client/ContainerAwareProcessorRegistryTest.php b/pkg/enqueue/Tests/Symfony/Client/ContainerAwareProcessorRegistryTest.php deleted file mode 100644 index 1d641d429..000000000 --- a/pkg/enqueue/Tests/Symfony/Client/ContainerAwareProcessorRegistryTest.php +++ /dev/null @@ -1,84 +0,0 @@ -assertClassImplements(ProcessorRegistryInterface::class, ContainerAwareProcessorRegistry::class); - } - - public function testCouldBeConstructedWithoutAnyArgument() - { - new ContainerAwareProcessorRegistry(); - } - - public function testShouldThrowExceptionIfProcessorIsNotSet() - { - $this->setExpectedException( - \LogicException::class, - 'Processor was not found. processorName: "processor-name"' - ); - - $registry = new ContainerAwareProcessorRegistry(); - $registry->get('processor-name'); - } - - public function testShouldThrowExceptionIfContainerIsNotSet() - { - $this->setExpectedException(\LogicException::class, 'Container was not set'); - - $registry = new ContainerAwareProcessorRegistry(); - $registry->set('processor-name', 'processor-id'); - - $registry->get('processor-name'); - } - - public function testShouldThrowExceptionIfInstanceOfProcessorIsInvalid() - { - $this->setExpectedException(\LogicException::class, 'Container was not set'); - - $processor = new \stdClass(); - - $container = new Container(); - $container->set('processor-id', $processor); - - $registry = new ContainerAwareProcessorRegistry(); - $registry->set('processor-name', 'processor-id'); - - $registry->get('processor-name'); - } - - public function testShouldReturnInstanceOfProcessor() - { - $this->setExpectedException(\LogicException::class, 'Container was not set'); - - $processor = $this->createProcessorMock(); - - $container = new Container(); - $container->set('processor-id', $processor); - - $registry = new ContainerAwareProcessorRegistry(); - $registry->set('processor-name', 'processor-id'); - - $this->assertSame($processor, $registry->get('processor-name')); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor - */ - protected function createProcessorMock() - { - return $this->createMock(PsrProcessor::class); - } -} diff --git a/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPassTest.php b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPassTest.php new file mode 100644 index 000000000..568de6488 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/AnalyzeRouteCollectionPassTest.php @@ -0,0 +1,167 @@ +assertClassImplements(CompilerPassInterface::class, AnalyzeRouteCollectionPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(AnalyzeRouteCollectionPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new AnalyzeRouteCollectionPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + + $pass = new AnalyzeRouteCollectionPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowIfExclusiveCommandProcessorOnDefaultQueue() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aCommand', + Route::COMMAND, + 'aBarProcessor', + ['exclusive' => true] + ))->toArray(), + ]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The command "aCommand" processor "aBarProcessor" is exclusive but queue is not specified. Exclusive processors could not be run on a default queue.'); + $pass = new AnalyzeRouteCollectionPass(); + + $pass->process($container); + } + + public function testThrowIfTwoExclusiveCommandProcessorsWorkOnSamePrefixedQueue() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aFooCommand', + Route::COMMAND, + 'aFooProcessor', + ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => true] + ))->toArray(), + + (new Route( + 'aBarCommand', + Route::COMMAND, + 'aBarProcessor', + ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => true] + ))->toArray(), + ]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The command "aBarCommand" processor "aBarProcessor" is exclusive. The queue "aQueue" already has another exclusive command processor "aFooProcessor" bound to it.'); + $pass = new AnalyzeRouteCollectionPass(); + + $pass->process($container); + } + + public function testThrowIfTwoExclusiveCommandProcessorsWorkOnSameQueue() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aFooCommand', + Route::COMMAND, + 'aFooProcessor', + ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => false] + ))->toArray(), + + (new Route( + 'aBarCommand', + Route::COMMAND, + 'aBarProcessor', + ['exclusive' => true, 'queue' => 'aQueue', 'prefix_queue' => false] + ))->toArray(), + ]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The command "aBarCommand" processor "aBarProcessor" is exclusive. The queue "aQueue" already has another exclusive command processor "aFooProcessor" bound to it.'); + $pass = new AnalyzeRouteCollectionPass(); + + $pass->process($container); + } + + public function testThrowIfThereAreTwoQueuesWithSameNameAndOneNotPrefixed() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aFooCommand', + Route::COMMAND, + 'aFooProcessor', + ['queue' => 'foo', 'prefix_queue' => false] + ))->toArray(), + + (new Route( + 'aBarCommand', + Route::COMMAND, + 'aBarProcessor', + ['queue' => 'foo', 'prefix_queue' => true] + ))->toArray(), + ]); + + $pass = new AnalyzeRouteCollectionPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('There are prefixed and not prefixed queue with the same name "foo". This is not allowed.'); + $pass->process($container); + } + + public function testThrowIfDefaultQueueNotPrefixed() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aFooCommand', + Route::COMMAND, + 'aFooProcessor', + ['queue' => null, 'prefix_queue' => false] + ))->toArray(), + ]); + + $pass = new AnalyzeRouteCollectionPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The default queue must be prefixed.'); + $pass->process($container); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildClientExtensionsPassTest.php b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildClientExtensionsPassTest.php new file mode 100644 index 000000000..753790369 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildClientExtensionsPassTest.php @@ -0,0 +1,283 @@ +assertClassImplements(CompilerPassInterface::class, BuildClientExtensionsPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildClientExtensionsPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildClientExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoClientExtensionsServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'foo'); + + $pass = new BuildClientExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.client_extensions" not found'); + $pass->process($container); + } + + public function testShouldRegisterClientExtension() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.aName.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'aName']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'aName']) + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldIgnoreOtherClientExtensions() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.aName.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'aName']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'anotherName']) + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldAddExtensionIfClientAll() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.aName.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'all']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'anotherName']) + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldTreatTagsWithoutClientAsDefaultClient() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension') + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldOrderExtensionsByPriority() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.client.foo.client_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => 6]); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => -5]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => 2]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]); + } + + public function testShouldAssumePriorityZeroIfPriorityIsNotSet() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.client.foo.client_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension'); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => 1]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.client_extension', ['priority' => -1]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]); + } + + public function testShouldMergeWithAddedPreviously() + { + $extensions = new Definition(); + $extensions->addArgument([ + 'aBarExtension' => 'aBarServiceIdAddedPreviously', + 'aOloloExtension' => 'aOloloServiceIdAddedPreviously', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.client_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension') + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertCount(4, $extensions->getArgument(0)); + } + + public function testShouldRegisterProcessorWithMatchedNameToCorrespondingExtensions() + { + $fooExtensions = new Definition(); + $fooExtensions->addArgument([]); + + $barExtensions = new Definition(); + $barExtensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.client_extensions', $fooExtensions); + $container->setDefinition('enqueue.client.bar.client_extensions', $barExtensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.client_extension', ['client' => 'bar']) + ; + + $pass = new BuildClientExtensionsPass(); + $pass->process($container); + + self::assertIsArray($fooExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $fooExtensions->getArgument(0)); + + self::assertIsArray($barExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aBarExtension'), + ], $barExtensions->getArgument(0)); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPassTest.php b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPassTest.php new file mode 100644 index 000000000..e1ed297c6 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildCommandSubscriberRoutesPassTest.php @@ -0,0 +1,459 @@ +assertClassImplements(CompilerPassInterface::class, BuildCommandSubscriberRoutesPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildCommandSubscriberRoutesPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'baz'); + + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowIfTaggedProcessorIsBuiltByFactory() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->register('enqueue.client.aName.route_collection', RouteCollection::class) + ->addArgument([]) + ; + $container->register('aProcessor', Processor::class) + ->setFactory('foo') + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The command subscriber tag could not be applied to a service created by factory.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorWithMatchedName() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'foo']) + ; + $container->register('aProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorWithoutNameToDefaultClient() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber') + ; + $container->register('aProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfClientNameEqualsAll() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'all']) + ; + $container->register('aProcessor', get_class($this->createCommandSubscriberProcessor())) + ->addTag('enqueue.command_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfCommandsIsString() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor('fooCommand'); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testThrowIfCommandSubscriberReturnsNothing() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Command subscriber must return something.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorIfCommandsAreStrings() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor(['fooCommand', 'barCommand']); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + [ + 'source' => 'barCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterProcessorIfParamSingleCommandArray() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor([ + 'command' => 'fooCommand', + 'processor' => 'aCustomFooProcessorName', + 'anOption' => 'aFooVal', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aCustomFooProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterProcessorIfCommandsAreParamArrays() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor([ + ['command' => 'fooCommand', 'processor' => 'aCustomFooProcessorName', 'anOption' => 'aFooVal'], + ['command' => 'barCommand', 'processor' => 'aCustomBarProcessorName', 'anOption' => 'aBarVal'], + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aCustomFooProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + ], + [ + 'source' => 'barCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aCustomBarProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aBarVal', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testThrowIfCommandSubscriberParamsInvalid() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor(['fooBar', true]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Command subscriber configuration is invalid'); + $pass->process($container); + } + + public function testShouldMergeExtractedRoutesWithAlreadySetInCollection() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([ + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + ]); + + $processor = $this->createCommandSubscriberProcessor(['fooCommand']); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(3, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegister08CommandProcessor() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createCommandSubscriberProcessor([ + 'processorName' => 'fooCommand', + 'queueName' => 'a_client_queue_name', + 'queueNameHardcoded' => true, + 'exclusive' => true, + 'anOption' => 'aFooVal', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['default']); + $container->setParameter('enqueue.default_client', 'default'); + $container->setDefinition('enqueue.client.default.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.command_subscriber') + ; + + $pass = new BuildCommandSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + 'queue' => 'a_client_queue_name', + 'prefix_queue' => false, + ], + ], + $routeCollection->getArgument(0) + ); + } + + private function createCommandSubscriberProcessor($commandSubscriberReturns = ['aCommand']) + { + $processor = new class implements Processor, CommandSubscriberInterface { + public static $return; + + public function process(InteropMessage $message, Context $context) + { + return self::ACK; + } + + public static function getSubscribedCommand() + { + return static::$return; + } + }; + + $processor::$return = $commandSubscriberReturns; + + return $processor; + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPassTest.php b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPassTest.php new file mode 100644 index 000000000..c2975051b --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildConsumptionExtensionsPassTest.php @@ -0,0 +1,283 @@ +assertClassImplements(CompilerPassInterface::class, BuildConsumptionExtensionsPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildConsumptionExtensionsPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildConsumptionExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoConsumptionExtensionsServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'baz'); + + $pass = new BuildConsumptionExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.consumption_extensions" not found'); + $pass->process($container); + } + + public function testShouldRegisterClientExtension() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'foo']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldIgnoreOtherClientExtensions() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'anotherName']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldAddExtensionIfClientAll() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'all']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'anotherName']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldTreatTagsWithoutClientAsDefaultClient() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension') + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldOrderExtensionsByPriority() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => 6]); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => -5]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => 2]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]); + } + + public function testShouldAssumePriorityZeroIfPriorityIsNotSet() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension'); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => 1]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.consumption_extension', ['priority' => -1]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]); + } + + public function testShouldMergeWithAddedPreviously() + { + $extensions = new Definition(); + $extensions->addArgument([ + 'aBarExtension' => 'aBarServiceIdAddedPreviously', + 'aOloloExtension' => 'aOloloServiceIdAddedPreviously', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension') + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertCount(4, $extensions->getArgument(0)); + } + + public function testShouldRegisterProcessorWithMatchedNameToCorrespondingExtensions() + { + $fooExtensions = new Definition(); + $fooExtensions->addArgument([]); + + $barExtensions = new Definition(); + $barExtensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.consumption_extensions', $fooExtensions); + $container->setDefinition('enqueue.client.bar.consumption_extensions', $barExtensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.consumption_extension', ['client' => 'bar']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($fooExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $fooExtensions->getArgument(0)); + + self::assertIsArray($barExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aBarExtension'), + ], $barExtensions->getArgument(0)); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildProcessorRegistryPassTest.php b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildProcessorRegistryPassTest.php new file mode 100644 index 000000000..5c9ac4840 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildProcessorRegistryPassTest.php @@ -0,0 +1,151 @@ +assertClassImplements(CompilerPassInterface::class, BuildProcessorRegistryPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildProcessorRegistryPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoProcessorRegistryServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.processor_registry" not found'); + $pass->process($container); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->register('enqueue.client.foo.processor_registry'); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowsIfNoRouteProcessorServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->register('enqueue.client.foo.processor_registry'); + $container->register('enqueue.client.foo.route_collection'); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.router_processor" not found'); + $pass->process($container); + } + + public function testThrowIfProcessorServiceIdOptionNotSet() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + ]); + $container->register('enqueue.client.aName.processor_registry')->addArgument([]); + $container->register('enqueue.client.aName.router_processor'); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The route option "processor_service_id" is required'); + $pass->process($container); + } + + public function testShouldPassLocatorAsFirstArgument() + { + $registry = new Definition(); + $registry->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['aName']); + $container->register('enqueue.client.aName.route_collection')->addArgument([ + (new Route( + 'aCommand', + Route::COMMAND, + 'aBarProcessor', + ['processor_service_id' => 'aBarServiceId'] + ))->toArray(), + (new Route( + 'aTopic', + Route::TOPIC, + 'aFooProcessor', + ['processor_service_id' => 'aFooServiceId'] + ))->toArray(), + ]); + $container->setDefinition('enqueue.client.aName.processor_registry', $registry); + $container->register('enqueue.client.aName.router_processor'); + + $pass = new BuildProcessorRegistryPass(); + $pass->process($container); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + '%enqueue.client.aName.router_processor%' => 'enqueue.client.aName.router_processor', + 'aBarProcessor' => 'aBarServiceId', + 'aFooProcessor' => 'aFooServiceId', + ]); + } + + private function assertLocatorServices(ContainerBuilder $container, $locatorId, array $locatorServices) + { + $this->assertInstanceOf(Reference::class, $locatorId); + $locatorId = (string) $locatorId; + + $this->assertTrue($container->hasDefinition($locatorId)); + $this->assertMatchesRegularExpression('/\.?service_locator\..*?\.enqueue\./', $locatorId); + + $match = []; + if (false == preg_match('/(\.?service_locator\..*?)\.enqueue\./', $locatorId, $match)) { + $this->fail('preg_match should not failed'); + } + + $this->assertTrue($container->hasDefinition($match[1])); + $locator = $container->getDefinition($match[1]); + + $this->assertContainsOnly(ServiceClosureArgument::class, $locator->getArgument(0)); + $actualServices = array_map(function (ServiceClosureArgument $value) { + return (string) $value->getValues()[0]; + }, $locator->getArgument(0)); + + $this->assertEquals($locatorServices, $actualServices); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildProcessorRoutesPassTest.php b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildProcessorRoutesPassTest.php new file mode 100644 index 000000000..0351c45f5 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildProcessorRoutesPassTest.php @@ -0,0 +1,302 @@ +assertClassImplements(CompilerPassInterface::class, BuildProcessorRoutesPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildProcessorRoutesPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildProcessorRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'baz'); + + $pass = new BuildProcessorRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowIfBothTopicAndCommandAttributesAreSet() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['topic' => 'foo', 'command' => 'bar']) + ; + + $pass = new BuildProcessorRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either "topic" or "command" tag attribute must be set on service "aFooProcessor". Both are set.'); + $pass->process($container); + } + + public function testThrowIfNeitherTopicNorCommandAttributesAreSet() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', []) + ; + + $pass = new BuildProcessorRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either "topic" or "command" tag attribute must be set on service "aFooProcessor". None is set.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorWithMatchedName() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'bar'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'foo', 'topic' => 'foo']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'bar', 'command' => 'foo']) + ; + + $pass = new BuildProcessorRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorWithoutNameToDefaultClient() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['topic' => 'foo']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'bar', 'command' => 'foo']) + ; + + $pass = new BuildProcessorRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfClientNameEqualsAll() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'all', 'topic' => 'foo']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['client' => 'bar', 'command' => 'foo']) + ; + + $pass = new BuildProcessorRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterAsTopicProcessor() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['topic' => 'aTopic']) + ; + + $pass = new BuildProcessorRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterAsCommandProcessor() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['command' => 'aCommand']) + ; + + $pass = new BuildProcessorRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterWithCustomProcessorName() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['command' => 'aCommand', 'processor' => 'customProcessorName']) + ; + + $pass = new BuildProcessorRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'customProcessorName', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldMergeExtractedRoutesWithAlreadySetInCollection() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([ + (new Route('aTopic', Route::TOPIC, 'aProcessor'))->toArray(), + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.processor', ['command' => 'fooCommand']) + ; + + $pass = new BuildProcessorRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(3, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'fooCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPassTest.php b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPassTest.php new file mode 100644 index 000000000..a954d9a41 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/BuildTopicSubscriberRoutesPassTest.php @@ -0,0 +1,423 @@ +assertClassImplements(CompilerPassInterface::class, BuildTopicSubscriberRoutesPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildTopicSubscriberRoutesPass::class); + } + + public function testThrowIfEnqueueClientsParameterNotSet() + { + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.clients" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRouteCollectionServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo', 'bar']); + $container->setParameter('enqueue.default_client', 'baz'); + + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.client.foo.route_collection" not found'); + $pass->process($container); + } + + public function testThrowIfTaggedProcessorIsBuiltByFactory() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->register('enqueue.client.foo.route_collection', RouteCollection::class) + ->addArgument([]) + ; + $container->register('aProcessor', Processor::class) + ->setFactory('foo') + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The topic subscriber tag could not be applied to a service created by factory.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorWithMatchedName() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'bar'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'foo']) + ; + $container->register('aProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorWithoutNameToDefaultClient() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber') + ; + $container->register('aProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfClientNameEqualsAll() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'all']) + ; + $container->register('aProcessor', get_class($this->createTopicSubscriberProcessor())) + ->addTag('enqueue.topic_subscriber', ['client' => 'bar']) + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + } + + public function testShouldRegisterProcessorIfTopicsIsString() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor('fooTopic'); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(1, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testThrowIfTopicSubscriberReturnsNothing() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Topic subscriber must return something.'); + $pass->process($container); + } + + public function testShouldRegisterProcessorIfTopicsAreStrings() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor(['fooTopic', 'barTopic']); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + [ + 'source' => 'barTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegisterProcessorIfTopicsAreParamArrays() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor([ + ['topic' => 'fooTopic', 'processor' => 'aCustomFooProcessorName', 'anOption' => 'aFooVal'], + ['topic' => 'barTopic', 'processor' => 'aCustomBarProcessorName', 'anOption' => 'aBarVal'], + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aCustomFooProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + ], + [ + 'source' => 'barTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aCustomBarProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aBarVal', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testThrowIfTopicSubscriberParamsInvalid() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor(['fooBar', true]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Topic subscriber configuration is invalid'); + $pass->process($container); + } + + public function testShouldMergeExtractedRoutesWithAlreadySetInCollection() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([ + (new Route('aTopic', Route::TOPIC, 'aProcessor'))->toArray(), + (new Route('aCommand', Route::COMMAND, 'aProcessor'))->toArray(), + ]); + + $processor = $this->createTopicSubscriberProcessor(['fooTopic']); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['foo']); + $container->setParameter('enqueue.default_client', 'foo'); + $container->setDefinition('enqueue.client.foo.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(3, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'aTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'aCommand', + 'source_type' => 'enqueue.client.command_route', + 'processor' => 'aProcessor', + ], + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aFooProcessor', + 'processor_service_id' => 'aFooProcessor', + ], + ], + $routeCollection->getArgument(0) + ); + } + + public function testShouldRegister08TopicSubscriber() + { + $routeCollection = new Definition(RouteCollection::class); + $routeCollection->addArgument([]); + + $processor = $this->createTopicSubscriberProcessor([ + 'fooTopic' => ['processorName' => 'aCustomFooProcessorName', 'queueName' => 'fooQueue', 'queueNameHardcoded' => true, 'anOption' => 'aFooVal'], + 'barTopic' => ['processorName' => 'aCustomBarProcessorName', 'anOption' => 'aBarVal'], + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.clients', ['default']); + $container->setParameter('enqueue.default_client', 'default'); + $container->setDefinition('enqueue.client.default.route_collection', $routeCollection); + $container->register('aFooProcessor', $processor::class) + ->addTag('enqueue.topic_subscriber') + ; + + $pass = new BuildTopicSubscriberRoutesPass(); + $pass->process($container); + + self::assertIsArray($routeCollection->getArgument(0)); + $this->assertCount(2, $routeCollection->getArgument(0)); + + $this->assertEquals( + [ + [ + 'source' => 'fooTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aCustomFooProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aFooVal', + 'queue' => 'fooQueue', + 'prefix_queue' => false, + ], + [ + 'source' => 'barTopic', + 'source_type' => 'enqueue.client.topic_route', + 'processor' => 'aCustomBarProcessorName', + 'processor_service_id' => 'aFooProcessor', + 'anOption' => 'aBarVal', + ], + ], + $routeCollection->getArgument(0) + ); + } + + private function createTopicSubscriberProcessor($topicSubscriberReturns = ['aTopic']) + { + $processor = new class implements Processor, TopicSubscriberInterface { + public static $return; + + public function process(InteropMessage $message, Context $context) + { + return self::ACK; + } + + public static function getSubscribedTopics() + { + return static::$return; + } + }; + + $processor::$return = $topicSubscriberReturns; + + return $processor; + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/ClientFactoryTest.php b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/ClientFactoryTest.php new file mode 100644 index 000000000..9f37dff47 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/DependencyInjection/ClientFactoryTest.php @@ -0,0 +1,54 @@ +assertClassFinal(ClientFactory::class); + } + + public function testThrowIfEmptyNameGivenOnConstruction() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The name could not be empty.'); + + new ClientFactory(''); + } + + public function testShouldCreateDriverFromDsn() + { + $container = new ContainerBuilder(); + + $transport = new ClientFactory('default'); + + $serviceId = $transport->createDriver($container, ['dsn' => 'foo://bar/baz', 'foo' => 'fooVal']); + + $this->assertEquals('enqueue.client.default.driver', $serviceId); + + $this->assertTrue($container->hasDefinition('enqueue.client.default.driver')); + + $this->assertNotEmpty($container->getDefinition('enqueue.client.default.driver')->getFactory()); + $this->assertEquals( + [new Reference('enqueue.client.default.driver_factory'), 'create'], + $container->getDefinition('enqueue.client.default.driver')->getFactory()) + ; + $this->assertEquals( + [ + new Reference('enqueue.transport.default.connection_factory'), + new Reference('enqueue.client.default.config'), + new Reference('enqueue.client.default.route_collection'), + ], + $container->getDefinition('enqueue.client.default.driver')->getArguments()) + ; + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php b/pkg/enqueue/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php index a1fe06e7a..539d332ee 100644 --- a/pkg/enqueue/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php +++ b/pkg/enqueue/Tests/Symfony/Client/FlushSpoolProducerListenerTest.php @@ -23,7 +23,7 @@ public function testShouldSubscribeOnKernelTerminateEvent() { $events = FlushSpoolProducerListener::getSubscribedEvents(); - $this->assertInternalType('array', $events); + self::assertIsArray($events); $this->assertArrayHasKey(KernelEvents::TERMINATE, $events); $this->assertEquals('flushMessages', $events[KernelEvents::TERMINATE]); @@ -33,17 +33,12 @@ public function testShouldSubscribeOnConsoleTerminateEvent() { $events = FlushSpoolProducerListener::getSubscribedEvents(); - $this->assertInternalType('array', $events); + self::assertIsArray($events); $this->assertArrayHasKey(ConsoleEvents::TERMINATE, $events); $this->assertEquals('flushMessages', $events[ConsoleEvents::TERMINATE]); } - public function testCouldBeConstructedWithSpoolProducerAsFirstArgument() - { - new FlushSpoolProducerListener($this->createSpoolProducerMock()); - } - public function testShouldFlushSpoolProducerOnFlushMessagesCall() { $producerMock = $this->createSpoolProducerMock(); @@ -58,7 +53,7 @@ public function testShouldFlushSpoolProducerOnFlushMessagesCall() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SpoolProducer + * @return \PHPUnit\Framework\MockObject\MockObject|SpoolProducer */ private function createSpoolProducerMock() { diff --git a/pkg/enqueue/Tests/Symfony/Client/Meta/QueuesCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/Meta/QueuesCommandTest.php deleted file mode 100644 index f0720c7cb..000000000 --- a/pkg/enqueue/Tests/Symfony/Client/Meta/QueuesCommandTest.php +++ /dev/null @@ -1,109 +0,0 @@ -assertClassExtends(Command::class, QueuesCommand::class); - } - - public function testCouldBeConstructedWithQueueMetaRegistryAsFirstArgument() - { - new QueuesCommand($this->createQueueMetaRegistryStub()); - } - - public function testShouldHaveCommandName() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub()); - - $this->assertEquals('enqueue:queues', $command->getName()); - } - - public function testShouldHaveCommandAliases() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub()); - - $this->assertEquals(['enq:m:q', 'debug:enqueue:queues'], $command->getAliases()); - } - - public function testShouldShowMessageFoundZeroDestinationsIfAnythingInRegistry() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub()); - - $output = $this->executeCommand($command); - - $this->assertContains('Found 0 destinations', $output); - } - - public function testShouldShowMessageFoundTwoDestinations() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub([ - new QueueMeta('aClientName', 'aDestinationName'), - new QueueMeta('anotherClientName', 'anotherDestinationName'), - ])); - - $output = $this->executeCommand($command); - - $this->assertContains('Found 2 destinations', $output); - } - - public function testShouldShowInfoAboutDestinations() - { - $command = new QueuesCommand($this->createQueueMetaRegistryStub([ - new QueueMeta('aFooClientName', 'aFooDestinationName', ['fooSubscriber']), - new QueueMeta('aBarClientName', 'aBarDestinationName', ['barSubscriber']), - ])); - - $output = $this->executeCommand($command); - - $this->assertContains('aFooClientName', $output); - $this->assertContains('aFooDestinationName', $output); - $this->assertContains('fooSubscriber', $output); - $this->assertContains('aBarClientName', $output); - $this->assertContains('aBarDestinationName', $output); - $this->assertContains('barSubscriber', $output); - } - - /** - * @param Command $command - * @param string[] $arguments - * - * @return string - */ - protected function executeCommand(Command $command, array $arguments = []) - { - $tester = new CommandTester($command); - $tester->execute($arguments); - - return $tester->getDisplay(); - } - - /** - * @param mixed $destinations - * - * @return \PHPUnit_Framework_MockObject_MockObject|QueueMetaRegistry - */ - protected function createQueueMetaRegistryStub($destinations = []) - { - $registryMock = $this->createMock(QueueMetaRegistry::class); - $registryMock - ->expects($this->any()) - ->method('getQueuesMeta') - ->willReturn($destinations) - ; - - return $registryMock; - } -} diff --git a/pkg/enqueue/Tests/Symfony/Client/Meta/TopicsCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/Meta/TopicsCommandTest.php deleted file mode 100644 index 4efdd2f66..000000000 --- a/pkg/enqueue/Tests/Symfony/Client/Meta/TopicsCommandTest.php +++ /dev/null @@ -1,91 +0,0 @@ -assertClassExtends(Command::class, TopicsCommand::class); - } - - public function testCouldBeConstructedWithTopicMetaRegistryAsFirstArgument() - { - new TopicsCommand(new TopicMetaRegistry([])); - } - - public function testShouldHaveCommandName() - { - $command = new TopicsCommand(new TopicMetaRegistry([])); - - $this->assertEquals('enqueue:topics', $command->getName()); - } - - public function testShouldHaveCommandAliases() - { - $command = new TopicsCommand(new TopicMetaRegistry([])); - - $this->assertEquals(['enq:m:t', 'debug:enqueue:topics'], $command->getAliases()); - } - - public function testShouldShowMessageFoundZeroTopicsIfAnythingInRegistry() - { - $command = new TopicsCommand(new TopicMetaRegistry([])); - - $output = $this->executeCommand($command); - - $this->assertContains('Found 0 topics', $output); - } - - public function testShouldShowMessageFoundTwoTopics() - { - $command = new TopicsCommand(new TopicMetaRegistry([ - 'fooTopic' => [], - 'barTopic' => [], - ])); - - $output = $this->executeCommand($command); - - $this->assertContains('Found 2 topics', $output); - } - - public function testShouldShowInfoAboutTopics() - { - $command = new TopicsCommand(new TopicMetaRegistry([ - 'fooTopic' => ['description' => 'fooDescription', 'processors' => ['fooSubscriber']], - 'barTopic' => ['description' => 'barDescription', 'processors' => ['barSubscriber']], - ])); - - $output = $this->executeCommand($command); - - $this->assertContains('fooTopic', $output); - $this->assertContains('fooDescription', $output); - $this->assertContains('fooSubscriber', $output); - $this->assertContains('barTopic', $output); - $this->assertContains('barDescription', $output); - $this->assertContains('barSubscriber', $output); - } - - /** - * @param Command $command - * @param string[] $arguments - * - * @return string - */ - protected function executeCommand(Command $command, array $arguments = []) - { - $tester = new CommandTester($command); - $tester->execute($arguments); - - return $tester->getDisplay(); - } -} diff --git a/pkg/enqueue/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php b/pkg/enqueue/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php index fe7a352a5..c21750592 100644 --- a/pkg/enqueue/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php +++ b/pkg/enqueue/Tests/Symfony/Client/Mock/SetupBrokerExtensionCommand.php @@ -3,8 +3,8 @@ namespace Enqueue\Tests\Symfony\Client\Mock; use Enqueue\Client\Config; -use Enqueue\Client\Meta\QueueMetaRegistry; -use Enqueue\Null\Client\NullDriver; +use Enqueue\Client\Driver\GenericDriver; +use Enqueue\Client\RouteCollection; use Enqueue\Null\NullContext; use Enqueue\Symfony\Client\SetupBrokerExtensionCommandTrait; use Symfony\Component\Console\Command\Command; @@ -29,12 +29,14 @@ protected function configure() $this->configureSetupBrokerExtension(); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - $this->extension = $this->getSetupBrokerExtension($input, new NullDriver( + $this->extension = $this->getSetupBrokerExtension($input, new GenericDriver( new NullContext(), Config::create(), - new QueueMetaRegistry(Config::create(), []) + new RouteCollection([]) )); + + return 0; } } diff --git a/pkg/enqueue/Tests/Symfony/Client/ProduceCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/ProduceCommandTest.php new file mode 100644 index 000000000..daa909175 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/ProduceCommandTest.php @@ -0,0 +1,284 @@ +assertClassExtends(Command::class, ProduceCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(ProduceCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = ProduceCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:produce', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveExpectedOptions() + { + $command = new ProduceCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + $this->assertCount(4, $options); + $this->assertArrayHasKey('client', $options); + $this->assertArrayHasKey('topic', $options); + $this->assertArrayHasKey('command', $options); + $this->assertArrayHasKey('header', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new ProduceCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + $this->assertCount(1, $arguments); + + $this->assertArrayHasKey('message', $arguments); + } + + public function testThrowIfNeitherTopicNorCommandOptionsAreSet() + { + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $producerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $producerMock, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either topic or command option should be set, none is set.'); + $tester->execute([ + 'message' => 'theMessage', + ]); + } + + public function testThrowIfBothTopicAndCommandOptionsAreSet() + { + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $producerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $producerMock, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either topic or command option should be set, both are set.'); + $tester->execute([ + 'message' => 'theMessage', + '--topic' => 'theTopic', + '--command' => 'theCommand', + ]); + } + + public function testShouldSendEventToDefaultTransport() + { + $header = 'Content-Type: text/plain'; + $payload = 'theMessage'; + + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->once()) + ->method('sendEvent') + ->with('theTopic', new Message($payload, [], [$header])) + ; + $producerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $producerMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'message' => $payload, + '--header' => $header, + '--topic' => 'theTopic', + ]); + } + + public function testShouldSendCommandToDefaultTransport() + { + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->once()) + ->method('sendCommand') + ->with('theCommand', 'theMessage') + ; + $producerMock + ->expects($this->never()) + ->method('sendEvent') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $producerMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'message' => 'theMessage', + '--command' => 'theCommand', + ]); + } + + public function testShouldSendEventToFooTransport() + { + $header = 'Content-Type: text/plain'; + $payload = 'theMessage'; + + $defaultProducerMock = $this->createProducerMock(); + $defaultProducerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $defaultProducerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $fooProducerMock = $this->createProducerMock(); + $fooProducerMock + ->expects($this->once()) + ->method('sendEvent') + ->with('theTopic', new Message($payload, [], [$header])) + ; + $fooProducerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $defaultProducerMock, + 'enqueue.client.foo.producer' => $fooProducerMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'message' => $payload, + '--header' => $header, + '--topic' => 'theTopic', + '--client' => 'foo', + ]); + } + + public function testShouldSendCommandToFooTransport() + { + $defaultProducerMock = $this->createProducerMock(); + $defaultProducerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $defaultProducerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $fooProducerMock = $this->createProducerMock(); + $fooProducerMock + ->expects($this->once()) + ->method('sendCommand') + ->with('theCommand', 'theMessage') + ; + $fooProducerMock + ->expects($this->never()) + ->method('sendEvent') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $defaultProducerMock, + 'enqueue.client.foo.producer' => $fooProducerMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'message' => 'theMessage', + '--command' => 'theCommand', + '--client' => 'foo', + ]); + } + + public function testThrowIfClientNotFound() + { + $defaultProducerMock = $this->createProducerMock(); + $defaultProducerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $defaultProducerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new ProduceCommand(new Container([ + 'enqueue.client.default.producer' => $defaultProducerMock, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Client "bar" is not supported.'); + $tester->execute([ + 'message' => 'theMessage', + '--command' => 'theCommand', + '--client' => 'bar', + ]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface + */ + private function createProducerMock() + { + return $this->createMock(ProducerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/ProduceMessageCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/ProduceMessageCommandTest.php deleted file mode 100644 index 2cd80952e..000000000 --- a/pkg/enqueue/Tests/Symfony/Client/ProduceMessageCommandTest.php +++ /dev/null @@ -1,75 +0,0 @@ -createProducerMock()); - } - - public function testShouldHaveCommandName() - { - $command = new ProduceMessageCommand($this->createProducerMock()); - - $this->assertEquals('enqueue:produce', $command->getName()); - } - - public function testShouldHaveCommandAliases() - { - $command = new ProduceMessageCommand($this->createProducerMock()); - - $this->assertEquals(['enq:p'], $command->getAliases()); - } - - public function testShouldHaveExpectedOptions() - { - $command = new ProduceMessageCommand($this->createProducerMock()); - - $options = $command->getDefinition()->getOptions(); - $this->assertCount(0, $options); - } - - public function testShouldHaveExpectedAttributes() - { - $command = new ProduceMessageCommand($this->createProducerMock()); - - $arguments = $command->getDefinition()->getArguments(); - $this->assertCount(2, $arguments); - - $this->assertArrayHasKey('topic', $arguments); - $this->assertArrayHasKey('message', $arguments); - } - - public function testShouldExecuteConsumptionAndUseDefaultQueueName() - { - $producerMock = $this->createProducerMock(); - $producerMock - ->expects($this->once()) - ->method('sendEvent') - ->with('theTopic', 'theMessage') - ; - - $command = new ProduceMessageCommand($producerMock); - - $tester = new CommandTester($command); - $tester->execute([ - 'topic' => 'theTopic', - 'message' => 'theMessage', - ]); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface - */ - private function createProducerMock() - { - return $this->createMock(ProducerInterface::class); - } -} diff --git a/pkg/enqueue/Tests/Symfony/Client/RoutesCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/RoutesCommandTest.php new file mode 100644 index 000000000..89bd7f745 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/RoutesCommandTest.php @@ -0,0 +1,366 @@ +assertClassExtends(Command::class, RoutesCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(RoutesCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = RoutesCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:routes', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveCommandAliases() + { + $command = new RoutesCommand($this->createMock(ContainerInterface::class), 'default'); + + $this->assertEquals(['debug:enqueue:routes'], $command->getAliases()); + } + + public function testShouldHaveExpectedOptions() + { + $command = new RoutesCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + $this->assertCount(2, $options); + + $this->assertArrayHasKey('show-route-options', $options); + $this->assertArrayHasKey('client', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new RoutesCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + $this->assertCount(0, $arguments); + } + + public function testShouldOutputEmptyRouteCollection() + { + $routeCollection = new RouteCollection([]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 0 routes + + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldUseFooDriver() + { + $routeCollection = new RouteCollection([ + new Route('fooTopic', Route::TOPIC, 'processor'), + ]); + + $defaultDriverMock = $this->createMock(DriverInterface::class); + $defaultDriverMock + ->expects($this->never()) + ->method('getConfig') + ; + + $defaultDriverMock + ->expects($this->never()) + ->method('getRouteCollection') + ; + + $fooDriverMock = $this->createDriverStub(Config::create(), $routeCollection); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $defaultDriverMock, + 'enqueue.client.foo.driver' => $fooDriverMock, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + '--client' => 'foo', + ]); + + $this->assertStringContainsString('Found 1 routes', $tester->getDisplay()); + } + + public function testThrowIfClientNotFound() + { + $defaultDriverMock = $this->createMock(DriverInterface::class); + $defaultDriverMock + ->expects($this->never()) + ->method('getConfig') + ; + + $defaultDriverMock + ->expects($this->never()) + ->method('getRouteCollection') + ; + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $defaultDriverMock, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Client "foo" is not supported.'); + $tester->execute([ + '--client' => 'foo', + ]); + } + + public function testShouldOutputTopicRouteInfo() + { + $routeCollection = new RouteCollection([ + new Route('fooTopic', Route::TOPIC, 'processor'), + new Route('barTopic', Route::TOPIC, 'processor'), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++-------+----------+--------------------+-----------+----------+ +| Type | Source | Queue | Processor | Options | ++-------+----------+--------------------+-----------+----------+ +| topic | fooTopic | default (prefixed) | processor | (hidden) | +| topic | barTopic | default (prefixed) | processor | (hidden) | ++-------+----------+--------------------+-----------+----------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldOutputCommandRouteInfo() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['foo' => 'fooVal', 'bar' => 'barVal']), + new Route('barCommand', Route::COMMAND, 'processor', ['foo' => 'fooVal', 'bar' => 'barVal']), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++---------+------------+--------------------+-----------+----------+ +| Type | Source | Queue | Processor | Options | ++---------+------------+--------------------+-----------+----------+ +| command | fooCommand | default (prefixed) | processor | (hidden) | +| command | barCommand | default (prefixed) | processor | (hidden) | ++---------+------------+--------------------+-----------+----------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldCorrectlyOutputPrefixedCustomQueue() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['queue' => 'foo']), + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'bar']), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $exitCode = $tester->execute([]); + $this->assertSame(0, $exitCode); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++---------+------------+----------------+-----------+----------+ +| Type | Source | Queue | Processor | Options | ++---------+------------+----------------+-----------+----------+ +| topic | barTopic | bar (prefixed) | processor | (hidden) | +| command | fooCommand | foo (prefixed) | processor | (hidden) | ++---------+------------+----------------+-----------+----------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldCorrectlyOutputNotPrefixedCustomQueue() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['queue' => 'foo', 'prefix_queue' => false]), + new Route('barTopic', Route::TOPIC, 'processor', ['queue' => 'bar', 'prefix_queue' => false]), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++---------+------------+-------------+-----------+----------+ +| Type | Source | Queue | Processor | Options | ++---------+------------+-------------+-----------+----------+ +| topic | barTopic | bar (as is) | processor | (hidden) | +| command | fooCommand | foo (as is) | processor | (hidden) | ++---------+------------+-------------+-----------+----------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldCorrectlyOutputExternalRoute() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['external' => true]), + new Route('barTopic', Route::TOPIC, 'processor', ['external' => true]), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<assertCommandOutput($expectedOutput, $tester); + } + + public function testShouldOutputRouteOptions() + { + $routeCollection = new RouteCollection([ + new Route('fooCommand', Route::COMMAND, 'processor', ['foo' => 'fooVal']), + new Route('barTopic', Route::TOPIC, 'processor', ['bar' => 'barVal']), + ]); + + $command = new RoutesCommand(new Container([ + 'enqueue.client.default.driver' => $this->createDriverStub(Config::create(), $routeCollection), + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute(['--show-route-options' => true]); + + $expectedOutput = <<<'OUTPUT' +Found 2 routes + ++---------+------------+--------------------+-----------+----------------------+ +| Type | Source | Queue | Processor | Options | ++---------+------------+--------------------+-----------+----------------------+ +| topic | barTopic | default (prefixed) | processor | array ( | +| | | | | 'bar' => 'barVal', | +| | | | | ) | +| command | fooCommand | default (prefixed) | processor | array ( | +| | | | | 'foo' => 'fooVal', | +| | | | | ) | ++---------+------------+--------------------+-----------+----------------------+ + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createDriverStub(Config $config, RouteCollection $routeCollection): DriverInterface + { + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn($config) + ; + + $driverMock + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection) + ; + + return $driverMock; + } + + private function assertCommandOutput(string $expected, CommandTester $tester): void + { + $this->assertSame(0, $tester->getStatusCode()); + $this->assertSame($expected, $tester->getDisplay()); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/SetupBrokerCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/SetupBrokerCommandTest.php index 740d6d1c2..c81c4e1b6 100644 --- a/pkg/enqueue/Tests/Symfony/Client/SetupBrokerCommandTest.php +++ b/pkg/enqueue/Tests/Symfony/Client/SetupBrokerCommandTest.php @@ -3,32 +3,74 @@ namespace Enqueue\Tests\Symfony\Client; use Enqueue\Client\DriverInterface; +use Enqueue\Container\Container; use Enqueue\Symfony\Client\SetupBrokerCommand; +use Enqueue\Test\ClassExtensionTrait; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; class SetupBrokerCommandTest extends TestCase { - public function testCouldBeConstructedWithRequiredAttributes() + use ClassExtensionTrait; + + public function testShouldBeSubClassOfCommand() { - new \Enqueue\Symfony\Client\SetupBrokerCommand($this->createClientDriverMock()); + $this->assertClassExtends(Command::class, SetupBrokerCommand::class); } - public function testShouldHaveCommandName() + public function testShouldNotBeFinal() { - $command = new SetupBrokerCommand($this->createClientDriverMock()); + $this->assertClassNotFinal(SetupBrokerCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = SetupBrokerCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); - $this->assertEquals('enqueue:setup-broker', $command->getName()); + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:setup-broker', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); } public function testShouldHaveCommandAliases() { - $command = new SetupBrokerCommand($this->createClientDriverMock()); + $command = new SetupBrokerCommand($this->createMock(ContainerInterface::class), 'default'); $this->assertEquals(['enq:sb'], $command->getAliases()); } - public function testShouldCreateQueues() + public function testShouldHaveExpectedOptions() + { + $command = new SetupBrokerCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(1, $options); + $this->assertArrayHasKey('client', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SetupBrokerCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(0, $arguments); + } + + public function testShouldCallDriverSetupBrokerMethod() { $driver = $this->createClientDriverMock(); $driver @@ -36,16 +78,66 @@ public function testShouldCreateQueues() ->method('setupBroker') ; - $command = new SetupBrokerCommand($driver); + $command = new SetupBrokerCommand(new Container([ + 'enqueue.client.default.driver' => $driver, + ]), 'default'); $tester = new CommandTester($command); $tester->execute([]); - $this->assertContains('Setup Broker', $tester->getDisplay()); + $this->assertStringContainsString('Broker set up', $tester->getDisplay()); + } + + public function testShouldCallRequestedClientDriverSetupBrokerMethod() + { + $defaultDriver = $this->createClientDriverMock(); + $defaultDriver + ->expects($this->never()) + ->method('setupBroker') + ; + + $fooDriver = $this->createClientDriverMock(); + $fooDriver + ->expects($this->once()) + ->method('setupBroker') + ; + + $command = new SetupBrokerCommand(new Container([ + 'enqueue.client.default.driver' => $defaultDriver, + 'enqueue.client.foo.driver' => $fooDriver, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + '--client' => 'foo', + ]); + + $this->assertStringContainsString('Broker set up', $tester->getDisplay()); + } + + public function testShouldThrowIfClientNotFound() + { + $defaultDriver = $this->createClientDriverMock(); + $defaultDriver + ->expects($this->never()) + ->method('setupBroker') + ; + + $command = new SetupBrokerCommand(new Container([ + 'enqueue.client.default.driver' => $defaultDriver, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Client "foo" is not supported.'); + $tester->execute([ + '--client' => 'foo', + ]); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|DriverInterface + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface */ private function createClientDriverMock() { diff --git a/pkg/enqueue/Tests/Symfony/Client/SimpleConsumeCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/SimpleConsumeCommandTest.php new file mode 100644 index 000000000..21c491eb5 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/SimpleConsumeCommandTest.php @@ -0,0 +1,130 @@ +assertClassExtends(ConsumeCommand::class, SimpleConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleConsumeCommand::class); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleConsumeCommand($this->createQueueConsumerMock(), $this->createDriverStub(), $this->createDelegateProcessorMock()); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(9, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('client', $options); + $this->assertArrayHasKey('logger', $options); + $this->assertArrayHasKey('skip', $options); + $this->assertArrayHasKey('setup-broker', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleConsumeCommand($this->createQueueConsumerMock(), $this->createDriverStub(), $this->createDelegateProcessorMock()); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(1, $arguments); + $this->assertArrayHasKey('client-queue-names', $arguments); + } + + public function testShouldBindDefaultQueueOnly() + { + $queue = new NullQueue(''); + + $routeCollection = new RouteCollection([]); + + $processor = $this->createDelegateProcessorMock(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with($this->identicalTo($queue), $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $driver = $this->createDriverStub($routeCollection); + $driver + ->expects($this->once()) + ->method('createQueue') + ->with('default', true) + ->willReturn($queue) + ; + + $command = new SimpleConsumeCommand($consumer, $driver, $processor); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DelegateProcessor + */ + private function createDelegateProcessorMock() + { + return $this->createMock(DelegateProcessor::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + private function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createDriverStub(?RouteCollection $routeCollection = null): DriverInterface + { + $driverMock = $this->createMock(DriverInterface::class); + $driverMock + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection ?? new RouteCollection([])) + ; + + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn(Config::create('aPrefix', 'anApp')) + ; + + return $driverMock; + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/SimpleProduceCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/SimpleProduceCommandTest.php new file mode 100644 index 000000000..3ff81bfd5 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/SimpleProduceCommandTest.php @@ -0,0 +1,78 @@ +assertClassExtends(ProduceCommand::class, SimpleProduceCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleProduceCommand::class); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleProduceCommand($this->createProducerMock()); + + $options = $command->getDefinition()->getOptions(); + $this->assertCount(4, $options); + $this->assertArrayHasKey('client', $options); + $this->assertArrayHasKey('topic', $options); + $this->assertArrayHasKey('command', $options); + $this->assertArrayHasKey('header', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleProduceCommand($this->createProducerMock()); + + $arguments = $command->getDefinition()->getArguments(); + $this->assertCount(1, $arguments); + + $this->assertArrayHasKey('message', $arguments); + } + + public function testThrowIfNeitherTopicNorCommandOptionsAreSet() + { + $producerMock = $this->createProducerMock(); + $producerMock + ->expects($this->never()) + ->method('sendEvent') + ; + $producerMock + ->expects($this->never()) + ->method('sendCommand') + ; + + $command = new SimpleProduceCommand($producerMock); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Either topic or command option should be set, none is set.'); + $tester->execute([ + 'message' => 'theMessage', + ]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface + */ + private function createProducerMock() + { + return $this->createMock(ProducerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/SimpleRoutesCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/SimpleRoutesCommandTest.php new file mode 100644 index 000000000..20ee454cc --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/SimpleRoutesCommandTest.php @@ -0,0 +1,107 @@ +assertClassExtends(RoutesCommand::class, SimpleRoutesCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleRoutesCommand::class); + } + + public function testShouldHaveCommandAliases() + { + $command = new SimpleRoutesCommand($this->createDriverMock()); + + $this->assertEquals(['debug:enqueue:routes'], $command->getAliases()); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleRoutesCommand($this->createDriverMock()); + + $options = $command->getDefinition()->getOptions(); + $this->assertCount(2, $options); + + $this->assertArrayHasKey('show-route-options', $options); + $this->assertArrayHasKey('client', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleRoutesCommand($this->createDriverMock()); + + $arguments = $command->getDefinition()->getArguments(); + $this->assertCount(0, $arguments); + } + + public function testShouldOutputEmptyRouteCollection() + { + $routeCollection = new RouteCollection([]); + + $command = new SimpleRoutesCommand($this->createDriverStub(Config::create(), $routeCollection)); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $expectedOutput = <<<'OUTPUT' +Found 0 routes + + +OUTPUT; + + $this->assertCommandOutput($expectedOutput, $tester); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverMock(): DriverInterface + { + return $this->createMock(DriverInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createDriverStub(Config $config, RouteCollection $routeCollection): DriverInterface + { + $driverMock = $this->createDriverMock(); + $driverMock + ->expects($this->any()) + ->method('getConfig') + ->willReturn($config) + ; + + $driverMock + ->expects($this->any()) + ->method('getRouteCollection') + ->willReturn($routeCollection) + ; + + return $driverMock; + } + + private function assertCommandOutput(string $expected, CommandTester $tester): void + { + $this->assertSame(0, $tester->getStatusCode()); + $this->assertSame($expected, $tester->getDisplay()); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Client/SimpleSetupBrokerCommandTest.php b/pkg/enqueue/Tests/Symfony/Client/SimpleSetupBrokerCommandTest.php new file mode 100644 index 000000000..3702dbf18 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Client/SimpleSetupBrokerCommandTest.php @@ -0,0 +1,75 @@ +assertClassExtends(SetupBrokerCommand::class, SimpleSetupBrokerCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleSetupBrokerCommand::class); + } + + public function testShouldHaveCommandAliases() + { + $command = new SimpleSetupBrokerCommand($this->createClientDriverMock()); + + $this->assertEquals(['enq:sb'], $command->getAliases()); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleSetupBrokerCommand($this->createClientDriverMock()); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(1, $options); + $this->assertArrayHasKey('client', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleSetupBrokerCommand($this->createClientDriverMock()); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(0, $arguments); + } + + public function testShouldCallDriverSetupBrokerMethod() + { + $driver = $this->createClientDriverMock(); + $driver + ->expects($this->once()) + ->method('setupBroker') + ; + + $command = new SimpleSetupBrokerCommand($driver); + + $tester = new CommandTester($command); + $tester->execute([]); + + $this->assertStringContainsString('Broker set up', $tester->getDisplay()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|DriverInterface + */ + private function createClientDriverMock() + { + return $this->createMock(DriverInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Consumption/ConfigurableConsumeCommandTest.php b/pkg/enqueue/Tests/Symfony/Consumption/ConfigurableConsumeCommandTest.php new file mode 100644 index 000000000..251e264e2 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Consumption/ConfigurableConsumeCommandTest.php @@ -0,0 +1,306 @@ +assertClassExtends(Command::class, ConfigurableConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(ConfigurableConsumeCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = ConfigurableConsumeCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:transport:consume', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveExpectedOptions() + { + $command = new ConfigurableConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(7, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('transport', $options); + $this->assertArrayHasKey('logger', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new ConfigurableConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(2, $arguments); + $this->assertArrayHasKey('processor', $arguments); + $this->assertArrayHasKey('queues', $arguments); + } + + public function testThrowIfNeitherQueueOptionNorProcessorImplementsQueueSubscriberInterface() + { + $processor = $this->createProcessor(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->never()) + ->method('bind') + ; + $consumer + ->expects($this->never()) + ->method('consume') + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['aProcessor' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The queue is not provided. The processor must implement "Enqueue\Consumption\QueueSubscriberInterface" interface and it must return not empty array of queues or a queue set using as a second argument.'); + $tester->execute([ + 'processor' => 'aProcessor', + ]); + } + + public function testShouldExecuteConsumptionWithExplicitlySetQueue() + { + $processor = $this->createProcessor(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('bind') + ->with('queue-name', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'processor' => 'processor-service', + 'queues' => ['queue-name'], + ]); + } + + public function testThrowIfTransportNotDefined() + { + $processor = $this->createProcessor(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->never()) + ->method('bind') + ; + $consumer + ->expects($this->never()) + ->method('consume') + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Transport "not-defined" is not supported.'); + $tester->execute([ + 'processor' => 'processor-service', + 'queues' => ['queue-name'], + '--transport' => 'not-defined', + ]); + } + + public function testShouldExecuteConsumptionWithSeveralCustomQueues() + { + $processor = $this->createProcessor(); + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->at(0)) + ->method('bind') + ->with('queue-name', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('bind') + ->with('another-queue-name', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(2)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'processor' => 'processor-service', + 'queues' => ['queue-name', 'another-queue-name'], + ]); + } + + public function testShouldExecuteConsumptionWhenProcessorImplementsQueueSubscriberInterface() + { + $processor = new class implements Processor, QueueSubscriberInterface { + public function process(InteropMessage $message, Context $context): void + { + } + + public static function getSubscribedQueues() + { + return ['fooSubscribedQueues', 'barSubscribedQueues']; + } + }; + + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->at(0)) + ->method('bind') + ->with('fooSubscribedQueues', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(1)) + ->method('bind') + ->with('barSubscribedQueues', $this->identicalTo($processor)) + ; + $consumer + ->expects($this->at(2)) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + 'enqueue.transport.default.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'processor' => 'processor-service', + ]); + } + + public function testShouldExecuteConsumptionWithCustomTransportExplicitlySetQueue() + { + $processor = $this->createProcessor(); + + $fooConsumer = $this->createQueueConsumerMock(); + $fooConsumer + ->expects($this->never()) + ->method('bind') + ; + $fooConsumer + ->expects($this->never()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $barConsumer = $this->createQueueConsumerMock(); + $barConsumer + ->expects($this->once()) + ->method('bind') + ->with('queue-name', $this->identicalTo($processor)) + ; + $barConsumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConfigurableConsumeCommand(new Container([ + 'enqueue.transport.foo.queue_consumer' => $fooConsumer, + 'enqueue.transport.foo.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + 'enqueue.transport.bar.queue_consumer' => $barConsumer, + 'enqueue.transport.bar.processor_registry' => new ArrayProcessorRegistry(['processor-service' => $processor]), + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([ + 'processor' => 'processor-service', + 'queues' => ['queue-name'], + '--transport' => 'bar', + ]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|InteropQueue + */ + protected function createQueueMock() + { + return $this->createMock(InteropQueue::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|Processor + */ + protected function createProcessor() + { + return $this->createMock(Processor::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + protected function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Symfony/Consumption/ConsumeCommandTest.php b/pkg/enqueue/Tests/Symfony/Consumption/ConsumeCommandTest.php new file mode 100644 index 000000000..f07bef03b --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Consumption/ConsumeCommandTest.php @@ -0,0 +1,247 @@ +assertClassExtends(Command::class, ConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(ConsumeCommand::class); + } + + public function testShouldHaveAsCommandAttributeWithCommandName() + { + $commandClass = ConsumeCommand::class; + + $reflectionClass = new \ReflectionClass($commandClass); + + $attributes = $reflectionClass->getAttributes(AsCommand::class); + + $this->assertNotEmpty($attributes, 'The command does not have the AsCommand attribute.'); + + // Get the first attribute instance (assuming there is only one AsCommand attribute) + $asCommandAttribute = $attributes[0]; + + // Verify the 'name' parameter value + $attributeInstance = $asCommandAttribute->newInstance(); + $this->assertEquals('enqueue:transport:consume', $attributeInstance->name, 'The command name is not set correctly in the AsCommand attribute.'); + } + + public function testShouldHaveExpectedOptions() + { + $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(7, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('transport', $options); + $this->assertArrayHasKey('logger', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new ConsumeCommand($this->createMock(ContainerInterface::class), 'default'); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(0, $arguments); + } + + public function testShouldExecuteDefaultConsumption() + { + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + public function testShouldExecuteCustomConsumption() + { + $defaultConsumer = $this->createQueueConsumerMock(); + $defaultConsumer + ->expects($this->never()) + ->method('consume') + ; + + $customConsumer = $this->createQueueConsumerMock(); + $customConsumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $defaultConsumer, + 'enqueue.transport.custom.queue_consumer' => $customConsumer, + ]), 'default'); + + $tester = new CommandTester($command); + $tester->execute(['--transport' => 'custom']); + } + + public function testThrowIfNotDefinedTransportRequested() + { + $defaultConsumer = $this->createQueueConsumerMock(); + $defaultConsumer + ->expects($this->never()) + ->method('consume') + ; + + $command = new ConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $defaultConsumer, + ]), 'default'); + + $tester = new CommandTester($command); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Transport "not-defined" is not supported.'); + $tester->execute(['--transport' => 'not-defined']); + } + + public function testShouldReturnExitStatusIfSet() + { + $testExitCode = 678; + + $stubExtension = $this->createExtension(); + + $stubExtension + ->expects($this->once()) + ->method('onStart') + ->with($this->isInstanceOf(Start::class)) + ->willReturnCallback(function (Start $context) use ($testExitCode) { + $context->interruptExecution($testExitCode); + }) + ; + + $consumer = new QueueConsumer($this->createContextStub(), $stubExtension); + + $command = new ConsumeCommand(new Container([ + 'enqueue.transport.default.queue_consumer' => $consumer, + ]), 'default'); + + $tester = new CommandTester($command); + + $tester->execute([]); + + $this->assertEquals($testExitCode, $tester->getStatusCode()); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + private function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createContextWithoutSubscriptionConsumerMock(): InteropContext + { + $contextMock = $this->createMock(InteropContext::class); + $contextMock + ->expects($this->any()) + ->method('createSubscriptionConsumer') + ->willThrowException(SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt()) + ; + + return $contextMock; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|InteropContext + */ + private function createContextStub(?Consumer $consumer = null): InteropContext + { + $context = $this->createContextWithoutSubscriptionConsumerMock(); + $context + ->expects($this->any()) + ->method('createQueue') + ->willReturnCallback(function (string $queueName) { + return new NullQueue($queueName); + }) + ; + $context + ->expects($this->any()) + ->method('createConsumer') + ->willReturnCallback(function (Queue $queue) use ($consumer) { + return $consumer ?: $this->createConsumerStub($queue); + }) + ; + + return $context; + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ExtensionInterface + */ + private function createExtension() + { + return $this->createMock(ExtensionInterface::class); + } + + /** + * @param mixed|null $queue + * + * @return \PHPUnit\Framework\MockObject\MockObject|Consumer + */ + private function createConsumerStub($queue = null): Consumer + { + if (null === $queue) { + $queue = 'queue'; + } + if (is_string($queue)) { + $queue = new NullQueue($queue); + } + + $consumerMock = $this->createMock(Consumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queue) + ; + + return $consumerMock; + } +} diff --git a/pkg/enqueue/Tests/Symfony/Consumption/ConsumeMessagesCommandTest.php b/pkg/enqueue/Tests/Symfony/Consumption/ConsumeMessagesCommandTest.php deleted file mode 100644 index aad0a293e..000000000 --- a/pkg/enqueue/Tests/Symfony/Consumption/ConsumeMessagesCommandTest.php +++ /dev/null @@ -1,86 +0,0 @@ -createQueueConsumerMock()); - } - - public function testShouldHaveCommandName() - { - $command = new ConsumeMessagesCommand($this->createQueueConsumerMock()); - - $this->assertEquals('enqueue:transport:consume', $command->getName()); - } - - public function testShouldHaveExpectedOptions() - { - $command = new ConsumeMessagesCommand($this->createQueueConsumerMock()); - - $options = $command->getDefinition()->getOptions(); - - $this->assertCount(6, $options); - $this->assertArrayHasKey('memory-limit', $options); - $this->assertArrayHasKey('message-limit', $options); - $this->assertArrayHasKey('time-limit', $options); - $this->assertArrayHasKey('idle-timeout', $options); - $this->assertArrayHasKey('receive-timeout', $options); - $this->assertArrayHasKey('niceness', $options); - } - - public function testShouldHaveExpectedAttributes() - { - $command = new ConsumeMessagesCommand($this->createQueueConsumerMock()); - - $arguments = $command->getDefinition()->getArguments(); - - $this->assertCount(0, $arguments); - } - - public function testShouldExecuteConsumption() - { - $context = $this->createContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $command = new ConsumeMessagesCommand($consumer); - - $tester = new CommandTester($command); - $tester->execute([]); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - private function createContextMock() - { - return $this->createMock(PsrContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|QueueConsumer - */ - private function createQueueConsumerMock() - { - return $this->createMock(QueueConsumer::class); - } -} diff --git a/pkg/enqueue/Tests/Symfony/Consumption/ContainerAwareConsumeMessagesCommandTest.php b/pkg/enqueue/Tests/Symfony/Consumption/ContainerAwareConsumeMessagesCommandTest.php deleted file mode 100644 index 78929e04a..000000000 --- a/pkg/enqueue/Tests/Symfony/Consumption/ContainerAwareConsumeMessagesCommandTest.php +++ /dev/null @@ -1,208 +0,0 @@ -createQueueConsumerMock()); - } - - public function testShouldHaveCommandName() - { - $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock()); - - $this->assertEquals('enqueue:transport:consume', $command->getName()); - } - - public function testShouldHaveExpectedOptions() - { - $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock()); - - $options = $command->getDefinition()->getOptions(); - - $this->assertCount(7, $options); - $this->assertArrayHasKey('memory-limit', $options); - $this->assertArrayHasKey('message-limit', $options); - $this->assertArrayHasKey('time-limit', $options); - $this->assertArrayHasKey('queue', $options); - $this->assertArrayHasKey('idle-timeout', $options); - $this->assertArrayHasKey('receive-timeout', $options); - $this->assertArrayHasKey('niceness', $options); - } - - public function testShouldHaveExpectedAttributes() - { - $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock()); - - $arguments = $command->getDefinition()->getArguments(); - - $this->assertCount(1, $arguments); - $this->assertArrayHasKey('processor-service', $arguments); - } - - public function testShouldThrowExceptionIfProcessorInstanceHasWrongClass() - { - $container = new Container(); - $container->set('processor-service', new \stdClass()); - - $command = new ContainerAwareConsumeMessagesCommand($this->createQueueConsumerMock()); - $command->setContainer($container); - - $tester = new CommandTester($command); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Invalid message processor service given. It must be an instance of Interop\Queue\PsrProcessor but stdClass'); - $tester->execute([ - 'processor-service' => 'processor-service', - '--queue' => ['queue-name'], - ]); - } - - public function testThrowIfNeitherQueueOptionNorProcessorImplementsQueueSubscriberInterface() - { - $processor = $this->createProcessor(); - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->never()) - ->method('bind') - ; - $consumer - ->expects($this->never()) - ->method('consume') - ; - - $container = new Container(); - $container->set('processor-service', $processor); - - $command = new ContainerAwareConsumeMessagesCommand($consumer); - $command->setContainer($container); - - $tester = new CommandTester($command); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The queues are not provided. The processor must implement "Enqueue\Consumption\QueueSubscriberInterface" interface and it must return not empty array of queues or queues set using --queue option.'); - $tester->execute([ - 'processor-service' => 'processor-service', - ]); - } - - public function testShouldExecuteConsumptionWithExplicitlySetQueueViaQueueOption() - { - $processor = $this->createProcessor(); - - $context = $this->createContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->once()) - ->method('bind') - ->with('queue-name', $this->identicalTo($processor)) - ; - $consumer - ->expects($this->once()) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $container = new Container(); - $container->set('processor-service', $processor); - - $command = new ContainerAwareConsumeMessagesCommand($consumer); - $command->setContainer($container); - - $tester = new CommandTester($command); - $tester->execute([ - 'processor-service' => 'processor-service', - '--queue' => ['queue-name'], - ]); - } - - public function testShouldExecuteConsumptionWhenProcessorImplementsQueueSubscriberInterface() - { - $processor = new QueueSubscriberProcessor(); - - $context = $this->createContextMock(); - $context - ->expects($this->never()) - ->method('close') - ; - - $consumer = $this->createQueueConsumerMock(); - $consumer - ->expects($this->at(0)) - ->method('bind') - ->with('fooSubscribedQueues', $this->identicalTo($processor)) - ; - $consumer - ->expects($this->at(1)) - ->method('bind') - ->with('barSubscribedQueues', $this->identicalTo($processor)) - ; - $consumer - ->expects($this->at(2)) - ->method('consume') - ->with($this->isInstanceOf(ChainExtension::class)) - ; - - $container = new Container(); - $container->set('processor-service', $processor); - - $command = new ContainerAwareConsumeMessagesCommand($consumer); - $command->setContainer($container); - - $tester = new CommandTester($command); - $tester->execute([ - 'processor-service' => 'processor-service', - ]); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext - */ - protected function createContextMock() - { - return $this->createMock(PsrContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrQueue - */ - protected function createQueueMock() - { - return $this->createMock(PsrQueue::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProcessor - */ - protected function createProcessor() - { - return $this->createMock(PsrProcessor::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|QueueConsumer - */ - protected function createQueueConsumerMock() - { - return $this->createMock(QueueConsumer::class); - } -} diff --git a/pkg/enqueue/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php b/pkg/enqueue/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php index 54952749a..f47a32161 100644 --- a/pkg/enqueue/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php +++ b/pkg/enqueue/Tests/Symfony/Consumption/LimitsExtensionsCommandTraitTest.php @@ -108,17 +108,35 @@ public function testShouldAddThreeLimitExtensions() $this->assertInstanceOf(LimitConsumerMemoryExtension::class, $result[2]); } - public function testShouldAddNicenessExtension() + /** + * @dataProvider provideNicenessValues + */ + public function testShouldAddNicenessExtension($inputValue, bool $enabled) { $command = new LimitsExtensionsCommand('name'); $tester = new CommandTester($command); $tester->execute([ - '--niceness' => 1, + '--niceness' => $inputValue, ]); $result = $command->getExtensions(); - $this->assertCount(1, $result); - $this->assertInstanceOf(NicenessExtension::class, $result[0]); + if ($enabled) { + $this->assertCount(1, $result); + $this->assertInstanceOf(NicenessExtension::class, $result[0]); + } else { + $this->assertEmpty($result); + } + } + + public function provideNicenessValues(): \Generator + { + yield [1, true]; + yield ['1', true]; + yield [-1.0, true]; + yield ['100', true]; + yield ['', false]; + yield ['0', false]; + yield [0.0, false]; } } diff --git a/pkg/enqueue/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php b/pkg/enqueue/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php index 7b6722393..05e0c56ba 100644 --- a/pkg/enqueue/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php +++ b/pkg/enqueue/Tests/Symfony/Consumption/Mock/LimitsExtensionsCommand.php @@ -25,8 +25,10 @@ protected function configure() $this->configureLimitsExtensions(); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->extensions = $this->getLimitsExtensions($input, $output); + + return 0; } } diff --git a/pkg/enqueue/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php b/pkg/enqueue/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php index 88a0d8cf2..147a3b905 100644 --- a/pkg/enqueue/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php +++ b/pkg/enqueue/Tests/Symfony/Consumption/Mock/QueueConsumerOptionsCommand.php @@ -2,7 +2,7 @@ namespace Enqueue\Tests\Symfony\Consumption\Mock; -use Enqueue\Consumption\QueueConsumer; +use Enqueue\Consumption\QueueConsumerInterface; use Enqueue\Symfony\Consumption\QueueConsumerOptionsCommandTrait; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -13,11 +13,11 @@ class QueueConsumerOptionsCommand extends Command use QueueConsumerOptionsCommandTrait; /** - * @var QueueConsumer + * @var QueueConsumerInterface */ private $consumer; - public function __construct(QueueConsumer $consumer) + public function __construct(QueueConsumerInterface $consumer) { parent::__construct('queue-consumer-options'); @@ -31,8 +31,10 @@ protected function configure() $this->configureQueueConsumerOptions(); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->setQueueConsumerOptions($this->consumer, $input); + + return 0; } } diff --git a/pkg/enqueue/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php b/pkg/enqueue/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php index e71ad1634..a210b0e6b 100644 --- a/pkg/enqueue/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php +++ b/pkg/enqueue/Tests/Symfony/Consumption/Mock/QueueSubscriberProcessor.php @@ -3,14 +3,15 @@ namespace Enqueue\Tests\Symfony\Consumption\Mock; use Enqueue\Consumption\QueueSubscriberInterface; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message as InteropMessage; +use Interop\Queue\Processor; -class QueueSubscriberProcessor implements PsrProcessor, QueueSubscriberInterface +class QueueSubscriberProcessor implements Processor, QueueSubscriberInterface { - public function process(PsrMessage $message, PsrContext $context) + public function process(InteropMessage $message, Context $context) { + return self::ACK; } public static function getSubscribedQueues() diff --git a/pkg/enqueue/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php b/pkg/enqueue/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php index 57106aa24..b44c89af9 100644 --- a/pkg/enqueue/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php +++ b/pkg/enqueue/Tests/Symfony/Consumption/QueueConsumerOptionsCommandTraitTest.php @@ -2,7 +2,7 @@ namespace Enqueue\Tests\Symfony\Consumption; -use Enqueue\Consumption\QueueConsumer; +use Enqueue\Consumption\QueueConsumerInterface; use Enqueue\Tests\Symfony\Consumption\Mock\QueueConsumerOptionsCommand; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; @@ -15,19 +15,13 @@ public function testShouldAddExtensionsOptions() $options = $trait->getDefinition()->getOptions(); - $this->assertCount(2, $options); - $this->assertArrayHasKey('idle-timeout', $options); + $this->assertCount(1, $options); $this->assertArrayHasKey('receive-timeout', $options); } public function testShouldSetQueueConsumerOptions() { $consumer = $this->createQueueConsumer(); - $consumer - ->expects($this->once()) - ->method('setIdleTimeout') - ->with($this->identicalTo(123)) - ; $consumer ->expects($this->once()) ->method('setReceiveTimeout') @@ -38,16 +32,15 @@ public function testShouldSetQueueConsumerOptions() $tester = new CommandTester($trait); $tester->execute([ - '--idle-timeout' => '123', '--receive-timeout' => '456', ]); } /** - * @return QueueConsumer|\PHPUnit_Framework_MockObject_MockObject|QueueConsumer + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface */ private function createQueueConsumer() { - return $this->createMock(QueueConsumer::class); + return $this->createMock(QueueConsumerInterface::class); } } diff --git a/pkg/enqueue/Tests/Symfony/Consumption/SimpleConsumeCommandTest.php b/pkg/enqueue/Tests/Symfony/Consumption/SimpleConsumeCommandTest.php new file mode 100644 index 000000000..eeb38bf19 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/Consumption/SimpleConsumeCommandTest.php @@ -0,0 +1,74 @@ +assertClassExtends(ConsumeCommand::class, SimpleConsumeCommand::class); + } + + public function testShouldNotBeFinal() + { + $this->assertClassNotFinal(SimpleConsumeCommand::class); + } + + public function testShouldHaveExpectedOptions() + { + $command = new SimpleConsumeCommand($this->createQueueConsumerMock()); + + $options = $command->getDefinition()->getOptions(); + + $this->assertCount(7, $options); + $this->assertArrayHasKey('memory-limit', $options); + $this->assertArrayHasKey('message-limit', $options); + $this->assertArrayHasKey('time-limit', $options); + $this->assertArrayHasKey('receive-timeout', $options); + $this->assertArrayHasKey('niceness', $options); + $this->assertArrayHasKey('transport', $options); + $this->assertArrayHasKey('logger', $options); + } + + public function testShouldHaveExpectedAttributes() + { + $command = new SimpleConsumeCommand($this->createQueueConsumerMock()); + + $arguments = $command->getDefinition()->getArguments(); + + $this->assertCount(0, $arguments); + } + + public function testShouldExecuteDefaultConsumption() + { + $consumer = $this->createQueueConsumerMock(); + $consumer + ->expects($this->once()) + ->method('consume') + ->with($this->isInstanceOf(ChainExtension::class)) + ; + + $command = new SimpleConsumeCommand($consumer); + + $tester = new CommandTester($command); + $tester->execute([]); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|QueueConsumerInterface + */ + private function createQueueConsumerMock() + { + return $this->createMock(QueueConsumerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Symfony/ContainerProcessorRegistryTest.php b/pkg/enqueue/Tests/Symfony/ContainerProcessorRegistryTest.php new file mode 100644 index 000000000..5504e8ef6 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/ContainerProcessorRegistryTest.php @@ -0,0 +1,107 @@ +assertClassImplements(ProcessorRegistryInterface::class, ContainerProcessorRegistry::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(ContainerProcessorRegistry::class); + } + + public function testShouldAllowGetProcessor() + { + $processorMock = $this->createProcessorMock(); + + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('has') + ->with('processor-name') + ->willReturn(true) + ; + $containerMock + ->expects($this->once()) + ->method('get') + ->with('processor-name') + ->willReturn($processorMock) + ; + + $registry = new ContainerProcessorRegistry($containerMock); + $this->assertSame($processorMock, $registry->get('processor-name')); + } + + public function testThrowErrorIfServiceDoesNotImplementProcessorReturnType() + { + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('has') + ->with('processor-name') + ->willReturn(true) + ; + $containerMock + ->expects($this->once()) + ->method('get') + ->with('processor-name') + ->willReturn(new \stdClass()) + ; + + $registry = new ContainerProcessorRegistry($containerMock); + + $this->expectException(\TypeError::class); + // Exception messages vary slightly between versions + $this->expectExceptionMessageMatches( + '/Enqueue\\\\Symfony\\\\ContainerProcessorRegistry::get\(\).+ Interop\\\\Queue\\\\Processor,.*stdClass returned/' + ); + + $registry->get('processor-name'); + } + + public function testShouldThrowExceptionIfProcessorIsNotSet() + { + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('has') + ->with('processor-name') + ->willReturn(false) + ; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service locator does not have a processor with name "processor-name".'); + + $registry = new ContainerProcessorRegistry($containerMock); + $registry->get('processor-name'); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createProcessorMock(): Processor + { + return $this->createMock(Processor::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function createContainerMock(): ContainerInterface + { + return $this->createMock(ContainerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php b/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php deleted file mode 100644 index a97fb86b8..000000000 --- a/pkg/enqueue/Tests/Symfony/DefaultTransportFactoryTest.php +++ /dev/null @@ -1,297 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, DefaultTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new DefaultTransportFactory(); - - $this->assertEquals('default', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new DefaultTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfigurationAsAliasAsString() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['the_alias']); - - $this->assertEquals(['alias' => 'the_alias'], $config); - } - - public function testShouldAllowAddConfigurationAsAliasAsOption() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [['alias' => 'the_alias']]); - - $this->assertEquals(['alias' => 'the_alias'], $config); - } - - public function testShouldAllowAddConfigurationAsDsn() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['dsn://']); - - $this->assertEquals(['dsn' => 'dsn://'], $config); - } - - /** - * @see https://github.com/php-enqueue/enqueue-dev/issues/356 - * - * @group bug - */ - public function testShouldAllowAddConfigurationAsDsnWithoutSlashes() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['dsn:']); - - $this->assertEquals(['dsn' => 'dsn:'], $config); - } - - public function testShouldSetNullTransportByDefault() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $config = $processor->process($tb->buildTree(), [null]); - $this->assertEquals(['dsn' => 'null:'], $config); - - $config = $processor->process($tb->buildTree(), ['']); - $this->assertEquals(['dsn' => 'null:'], $config); - } - - public function testThrowIfNeitherDsnNorAliasConfigured() - { - $transport = new DefaultTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Either dsn or alias option must be set'); - $processor->process($tb->buildTree(), [[]]); - } - - public function testShouldCreateConnectionFactoryFromAlias() - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, ['alias' => 'foo']); - - $this->assertEquals('enqueue.transport.default.connection_factory', $serviceId); - - $this->assertTrue($container->hasAlias('enqueue.transport.default.connection_factory')); - $this->assertEquals( - 'enqueue.transport.foo.connection_factory', - (string) $container->getAlias('enqueue.transport.default.connection_factory') - ); - - $this->assertTrue($container->hasAlias('enqueue.transport.connection_factory')); - $this->assertEquals( - 'enqueue.transport.default.connection_factory', - (string) $container->getAlias('enqueue.transport.connection_factory') - ); - } - - public function testShouldCreateContextFromAlias() - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $serviceId = $transport->createContext($container, ['alias' => 'the_alias']); - - $this->assertEquals('enqueue.transport.default.context', $serviceId); - - $this->assertTrue($container->hasAlias($serviceId)); - $context = $container->getAlias($serviceId); - $this->assertEquals('enqueue.transport.the_alias.context', (string) $context); - - $this->assertTrue($container->hasAlias('enqueue.transport.context')); - $context = $container->getAlias('enqueue.transport.context'); - $this->assertEquals($serviceId, (string) $context); - } - - public function testShouldCreateDriverFromAlias() - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $driverId = $transport->createDriver($container, ['alias' => 'the_alias']); - - $this->assertEquals('enqueue.client.default.driver', $driverId); - - $this->assertTrue($container->hasAlias($driverId)); - $context = $container->getAlias($driverId); - $this->assertEquals('enqueue.client.the_alias.driver', (string) $context); - - $this->assertTrue($container->hasAlias('enqueue.client.driver')); - $context = $container->getAlias('enqueue.client.driver'); - $this->assertEquals($driverId, (string) $context); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedName - */ - public function testShouldCreateConnectionFactoryFromDSN($dsn, $expectedName) - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, ['dsn' => $dsn]); - - $this->assertEquals('enqueue.transport.default.connection_factory', $serviceId); - - $this->assertTrue($container->hasAlias('enqueue.transport.default.connection_factory')); - $this->assertEquals( - sprintf('enqueue.transport.%s.connection_factory', $expectedName), - (string) $container->getAlias('enqueue.transport.default.connection_factory') - ); - - $this->assertTrue($container->hasAlias('enqueue.transport.connection_factory')); - $this->assertEquals( - 'enqueue.transport.default.connection_factory', - (string) $container->getAlias('enqueue.transport.connection_factory') - ); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedName - */ - public function testShouldCreateContextFromDsn($dsn, $expectedName) - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $serviceId = $transport->createContext($container, ['dsn' => $dsn]); - - $this->assertEquals('enqueue.transport.default.context', $serviceId); - - $this->assertTrue($container->hasAlias($serviceId)); - $context = $container->getAlias($serviceId); - $this->assertEquals( - sprintf('enqueue.transport.%s.context', $expectedName), - (string) $context - ); - - $this->assertTrue($container->hasAlias('enqueue.transport.context')); - $context = $container->getAlias('enqueue.transport.context'); - $this->assertEquals($serviceId, (string) $context); - } - - /** - * @dataProvider provideDSNs - * - * @param mixed $dsn - * @param mixed $expectedName - */ - public function testShouldCreateDriverFromDsn($dsn, $expectedName) - { - $container = new ContainerBuilder(); - - $transport = new DefaultTransportFactory(); - - $driverId = $transport->createDriver($container, ['dsn' => $dsn]); - - $this->assertEquals('enqueue.client.default.driver', $driverId); - - $this->assertTrue($container->hasAlias($driverId)); - $context = $container->getAlias($driverId); - $this->assertEquals( - sprintf('enqueue.client.%s.driver', $expectedName), - (string) $context - ); - - $this->assertTrue($container->hasAlias('enqueue.client.driver')); - $context = $container->getAlias('enqueue.client.driver'); - $this->assertEquals($driverId, (string) $context); - } - - public static function provideDSNs() - { - yield ['amqp+ext:', 'default_amqp']; - - yield ['amqp+lib:', 'default_amqp']; - - yield ['amqp+bunny:', 'default_amqp']; - - yield ['null:', 'default_null']; - - yield ['file:', 'default_fs']; - - yield ['mysql:', 'default_dbal']; - - yield ['pgsql:', 'default_dbal']; - - yield ['gps:', 'default_gps']; - - yield ['sqs:', 'default_sqs']; - - yield ['redis:', 'default_redis']; - - yield ['stomp:', 'default_stomp']; - - yield ['kafka:', 'default_kafka']; - - yield ['mongodb:', 'default_mongodb']; - } -} diff --git a/pkg/enqueue/Tests/Symfony/DependencyInjection/BuildConsumptionExtensionsPassTest.php b/pkg/enqueue/Tests/Symfony/DependencyInjection/BuildConsumptionExtensionsPassTest.php new file mode 100644 index 000000000..bdccd338c --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/DependencyInjection/BuildConsumptionExtensionsPassTest.php @@ -0,0 +1,283 @@ +assertClassImplements(CompilerPassInterface::class, BuildConsumptionExtensionsPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildConsumptionExtensionsPass::class); + } + + public function testThrowIfEnqueueTransportsParameterNotSet() + { + $pass = new BuildConsumptionExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.transports" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoConsumptionExtensionsServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo', 'bar']); + $container->setParameter('enqueue.default_transport', 'foo'); + + $pass = new BuildConsumptionExtensionsPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.transport.foo.consumption_extensions" not found'); + $pass->process($container); + } + + public function testShouldRegisterTransportExtension() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldIgnoreOtherTransportExtensions() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'anotherName']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldAddExtensionIfTransportAll() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'all']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'anotherName']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldTreatTagsWithoutTransportAsDefaultTransport() + { + $extensions = new Definition(); + $extensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension') + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + new Reference('aBarExtension'), + ], $extensions->getArgument(0)); + } + + public function testShouldOrderExtensionsByPriority() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => 6]); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => -5]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => 2]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[2]); + } + + public function testShouldAssumePriorityZeroIfPriorityIsNotSet() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + + $extensions = new Definition(); + $extensions->addArgument([]); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension'); + $container->setDefinition('foo_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => 1]); + $container->setDefinition('bar_extension', $extension); + + $extension = new Definition(); + $extension->addTag('enqueue.transport.consumption_extension', ['priority' => -1]); + $container->setDefinition('baz_extension', $extension); + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + $orderedExtensions = $extensions->getArgument(0); + + $this->assertCount(3, $orderedExtensions); + $this->assertEquals(new Reference('bar_extension'), $orderedExtensions[0]); + $this->assertEquals(new Reference('foo_extension'), $orderedExtensions[1]); + $this->assertEquals(new Reference('baz_extension'), $orderedExtensions[2]); + } + + public function testShouldMergeWithAddedPreviously() + { + $extensions = new Definition(); + $extensions->addArgument([ + 'aBarExtension' => 'aBarServiceIdAddedPreviously', + 'aOloloExtension' => 'aOloloServiceIdAddedPreviously', + ]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $extensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension') + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension') + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($extensions->getArgument(0)); + $this->assertCount(4, $extensions->getArgument(0)); + } + + public function testShouldRegisterProcessorWithMatchedNameToCorrespondingRegistries() + { + $fooExtensions = new Definition(); + $fooExtensions->addArgument([]); + + $barExtensions = new Definition(); + $barExtensions->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo', 'bar']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.consumption_extensions', $fooExtensions); + $container->setDefinition('enqueue.transport.bar.consumption_extensions', $barExtensions); + + $container->register('aFooExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'foo']) + ; + $container->register('aBarExtension', ExtensionInterface::class) + ->addTag('enqueue.transport.consumption_extension', ['transport' => 'bar']) + ; + + $pass = new BuildConsumptionExtensionsPass(); + $pass->process($container); + + self::assertIsArray($fooExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aFooExtension'), + ], $fooExtensions->getArgument(0)); + + self::assertIsArray($barExtensions->getArgument(0)); + $this->assertEquals([ + new Reference('aBarExtension'), + ], $barExtensions->getArgument(0)); + } +} diff --git a/pkg/enqueue/Tests/Symfony/DependencyInjection/BuildProcessorRegistryPassTest.php b/pkg/enqueue/Tests/Symfony/DependencyInjection/BuildProcessorRegistryPassTest.php new file mode 100644 index 000000000..134c216dc --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/DependencyInjection/BuildProcessorRegistryPassTest.php @@ -0,0 +1,214 @@ +assertClassImplements(CompilerPassInterface::class, BuildProcessorRegistryPass::class); + } + + public function testShouldBeFinal() + { + $this->assertClassFinal(BuildProcessorRegistryPass::class); + } + + public function testThrowIfEnqueueTransportsParameterNotSet() + { + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The "enqueue.transports" parameter must be set.'); + $pass->process(new ContainerBuilder()); + } + + public function testThrowsIfNoRegistryServiceFoundForConfiguredTransport() + { + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo', 'bar']); + $container->setParameter('enqueue.default_transport', 'baz'); + + $pass = new BuildProcessorRegistryPass(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service "enqueue.transport.foo.processor_registry" not found'); + $pass->process($container); + } + + public function testShouldRegisterProcessorWithMatchedName() + { + $registry = new Definition(ProcessorRegistryInterface::class); + $registry->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $registry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'foo']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'bar']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $registry->getArgument(0)); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + 'aFooProcessor' => 'aFooProcessor', + ]); + } + + public function testShouldRegisterProcessorWithMatchedNameToCorrespondingRegistries() + { + $fooRegistry = new Definition(ProcessorRegistryInterface::class); + $fooRegistry->addArgument([]); + + $barRegistry = new Definition(ProcessorRegistryInterface::class); + $barRegistry->addArgument([]); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo', 'bar']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $fooRegistry); + $container->setDefinition('enqueue.transport.bar.processor_registry', $barRegistry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'foo']) + ; + $container->register('aBarProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'bar']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $fooRegistry->getArgument(0)); + $this->assertLocatorServices($container, $fooRegistry->getArgument(0), [ + 'aFooProcessor' => 'aFooProcessor', + ]); + + $this->assertInstanceOf(Reference::class, $barRegistry->getArgument(0)); + $this->assertLocatorServices($container, $barRegistry->getArgument(0), [ + 'aBarProcessor' => 'aBarProcessor', + ]); + } + + public function testShouldRegisterProcessorWithoutNameToDefaultTransport() + { + $registry = new Definition(ProcessorRegistryInterface::class); + $registry->addArgument(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $registry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', []) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'bar']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $registry->getArgument(0)); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + 'aFooProcessor' => 'aFooProcessor', + ]); + } + + public function testShouldRegisterProcessorIfTransportNameEqualsAll() + { + $registry = new Definition(ProcessorRegistryInterface::class); + $registry->addArgument(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $registry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'all']) + ; + $container->register('aProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['transport' => 'bar']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $registry->getArgument(0)); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + 'aFooProcessor' => 'aFooProcessor', + ]); + } + + public function testShouldRegisterWithCustomProcessorName() + { + $registry = new Definition(ProcessorRegistryInterface::class); + $registry->addArgument(null); + + $container = new ContainerBuilder(); + $container->setParameter('enqueue.transports', ['foo']); + $container->setParameter('enqueue.default_transport', 'foo'); + $container->setDefinition('enqueue.transport.foo.processor_registry', $registry); + $container->register('aFooProcessor', 'aProcessorClass') + ->addTag('enqueue.transport.processor', ['processor' => 'customProcessorName']) + ; + + $pass = new BuildProcessorRegistryPass(); + + $pass->process($container); + + $this->assertInstanceOf(Reference::class, $registry->getArgument(0)); + + $this->assertLocatorServices($container, $registry->getArgument(0), [ + 'customProcessorName' => 'aFooProcessor', + ]); + } + + private function assertLocatorServices(ContainerBuilder $container, $locatorId, array $locatorServices) + { + $this->assertInstanceOf(Reference::class, $locatorId); + $locatorId = (string) $locatorId; + + $this->assertTrue($container->hasDefinition($locatorId)); + $this->assertMatchesRegularExpression('/\.?service_locator\..*?\.enqueue\./', $locatorId); + + $match = []; + if (false == preg_match('/(\.?service_locator\..*?)\.enqueue\./', $locatorId, $match)) { + $this->fail('preg_match should not failed'); + } + + $this->assertTrue($container->hasDefinition($match[1])); + $locator = $container->getDefinition($match[1]); + + $this->assertContainsOnly(ServiceClosureArgument::class, $locator->getArgument(0)); + $actualServices = array_map(function (ServiceClosureArgument $value) { + return (string) $value->getValues()[0]; + }, $locator->getArgument(0)); + + $this->assertEquals($locatorServices, $actualServices); + } +} diff --git a/pkg/enqueue/Tests/Symfony/DependencyInjection/TransportFactoryTest.php b/pkg/enqueue/Tests/Symfony/DependencyInjection/TransportFactoryTest.php new file mode 100644 index 000000000..909407452 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/DependencyInjection/TransportFactoryTest.php @@ -0,0 +1,478 @@ +assertClassFinal(TransportFactory::class); + } + + public function testThrowIfEmptyNameGivenOnConstruction() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The name could not be empty.'); + + new TransportFactory(''); + } + + public function testShouldAllowAddConfigurationAsStringDsn() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => 'dsn://']]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'dsn://', + ], + ], $config); + } + + /** + * @see https://github.com/php-enqueue/enqueue-dev/issues/356 + * + * @group bug + */ + public function testShouldAllowAddConfigurationAsDsnWithoutSlashes() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => 'dsn:']]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'dsn:', + ], + ], $config); + } + + public function testShouldSetNullTransportIfNullGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => null]]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'null:', + ], + ], $config); + } + + public function testShouldSetNullTransportIfEmptyStringGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => '']]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'null:', + ], + ], $config); + } + + public function testShouldSetNullTransportIfEmptyArrayGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $config = $processor->process($tb->buildTree(), [['transport' => []]]); + + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'null:', + ], + ], $config); + } + + public function testThrowIfEmptyDsnGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The path "foo.transport.dsn" cannot contain an empty value, but got "".'); + $processor->process($tb->buildTree(), [['transport' => ['dsn' => '']]]); + } + + public function testThrowIfFactoryClassAndFactoryServiceSetAtTheSameTime() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Both options factory_class and factory_service are set. Please choose one.'); + $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'factory_class' => 'aFactoryClass', + 'factory_service' => 'aFactoryService', + ], ]]); + } + + public function testThrowIfConnectionFactoryClassUsedWithFactoryClassAtTheSameTime() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + + $processor = new Processor(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The option connection_factory_class must not be used with factory_class or factory_service at the same time. Please choose one.'); + $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'connection_factory_class' => 'aFactoryClass', + 'factory_service' => 'aFactoryService', + ], ]]); + } + + public function testThrowIfConnectionFactoryClassUsedWithFactoryServiceAtTheSameTime() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The option connection_factory_class must not be used with factory_class or factory_service at the same time. Please choose one.'); + $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'connection_factory_class' => 'aFactoryClass', + 'factory_service' => 'aFactoryService', + ], ]]); + } + + public function testShouldAllowSetFactoryClass() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $config = $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'factory_class' => 'theFactoryClass', + ], ]]); + + $this->assertArrayHasKey('factory_class', $config['transport']); + $this->assertSame('theFactoryClass', $config['transport']['factory_class']); + } + + public function testShouldAllowSetFactoryService() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $config = $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'factory_service' => 'theFactoryService', + ], ]]); + + $this->assertArrayHasKey('factory_service', $config['transport']); + $this->assertSame('theFactoryService', $config['transport']['factory_service']); + } + + public function testShouldAllowSetConnectionFactoryClass() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $config = $processor->process($tb->buildTree(), [[ + 'transport' => [ + 'dsn' => 'foo:', + 'connection_factory_class' => 'theFactoryClass', + ], ]]); + + $this->assertArrayHasKey('connection_factory_class', $config['transport']); + $this->assertSame('theFactoryClass', $config['transport']['connection_factory_class']); + } + + public function testThrowIfExtraOptionGiven() + { + list($tb, $rootNode) = $this->getRootNode(); + + $rootNode->append(TransportFactory::getConfiguration()); + $processor = new Processor(); + + $config = $processor->process($tb->buildTree(), [['transport' => ['dsn' => 'foo:', 'extraOption' => 'aVal']]]); + $this->assertEquals([ + 'transport' => [ + 'dsn' => 'foo:', + 'extraOption' => 'aVal', + ], ], $config + ); + } + + public function testShouldBuildConnectionFactoryFromDSN() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $config = [ + 'dsn' => 'foo://bar/baz', + 'connection_factory_class' => null, + 'factory_service' => null, + 'factory_class' => null, + ]; + + $transport->buildConnectionFactory($container, $config); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory')); + + $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()); + $this->assertEquals( + [new Reference('enqueue.transport.default.connection_factory_factory'), 'create'], + $container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()) + ; + $this->assertSame( + [['dsn' => 'foo://bar/baz']], + $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments()) + ; + } + + public function testShouldBuildConnectionFactoryUsingCustomFactoryClass() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $transport->buildConnectionFactory($container, ['dsn' => 'foo:', 'factory_class' => 'theFactoryClass']); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory_factory')); + $this->assertSame( + 'theFactoryClass', + $container->getDefinition('enqueue.transport.default.connection_factory_factory')->getClass() + ); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory')); + + $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()); + $this->assertEquals( + [new Reference('enqueue.transport.default.connection_factory_factory'), 'create'], + $container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()) + ; + $this->assertSame( + [['dsn' => 'foo:']], + $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments()) + ; + } + + public function testShouldBuildConnectionFactoryUsingCustomFactoryService() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $transport->buildConnectionFactory($container, ['dsn' => 'foo:', 'factory_service' => 'theFactoryService']); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory')); + + $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()); + $this->assertEquals( + [new Reference('theFactoryService'), 'create'], + $container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()) + ; + $this->assertSame( + [['dsn' => 'foo:']], + $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments()) + ; + } + + public function testShouldBuildConnectionFactoryUsingConnectionFactoryClassWithoutFactory() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $transport->buildConnectionFactory($container, ['dsn' => 'foo:', 'connection_factory_class' => 'theFactoryClass']); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.connection_factory')); + + $this->assertEmpty($container->getDefinition('enqueue.transport.default.connection_factory')->getFactory()); + $this->assertSame('theFactoryClass', $container->getDefinition('enqueue.transport.default.connection_factory')->getClass()); + $this->assertSame( + [['dsn' => 'foo:']], + $container->getDefinition('enqueue.transport.default.connection_factory')->getArguments()) + ; + } + + public function testShouldBuildContext() + { + $container = new ContainerBuilder(); + $container->register('enqueue.transport.default.connection_factory', ConnectionFactory::class); + + $transport = new TransportFactory('default'); + + $transport->buildContext($container, []); + + $this->assertNotEmpty($container->getDefinition('enqueue.transport.default.context')->getFactory()); + $this->assertEquals( + [new Reference('enqueue.transport.default.connection_factory'), 'createContext'], + $container->getDefinition('enqueue.transport.default.context')->getFactory()) + ; + $this->assertSame( + [], + $container->getDefinition('enqueue.transport.default.context')->getArguments()) + ; + } + + public function testThrowIfBuildContextCalledButConnectionFactoryServiceDoesNotExist() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The service "enqueue.transport.default.connection_factory" does not exist.'); + $transport->buildContext($container, []); + } + + public function testShouldBuildQueueConsumerWithDefaultOptions() + { + $container = new ContainerBuilder(); + $container->register('enqueue.transport.default.context', Context::class); + + $transport = new TransportFactory('default'); + + $transport->buildQueueConsumer($container, []); + + $this->assertSame(10000, $container->getParameter('enqueue.transport.default.receive_timeout')); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.consumption_extensions')); + $this->assertSame(ChainExtension::class, $container->getDefinition('enqueue.transport.default.consumption_extensions')->getClass()); + $this->assertSame([[]], $container->getDefinition('enqueue.transport.default.consumption_extensions')->getArguments()); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.queue_consumer')); + $this->assertSame(QueueConsumer::class, $container->getDefinition('enqueue.transport.default.queue_consumer')->getClass()); + $this->assertEquals([ + new Reference('enqueue.transport.default.context'), + new Reference('enqueue.transport.default.consumption_extensions'), + [], + new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), + '%enqueue.transport.default.receive_timeout%', + ], $container->getDefinition('enqueue.transport.default.queue_consumer')->getArguments()); + } + + public function testShouldBuildQueueConsumerWithCustomOptions() + { + $container = new ContainerBuilder(); + $container->register('enqueue.transport.default.context', Context::class); + + $transport = new TransportFactory('default'); + + $transport->buildQueueConsumer($container, [ + 'receive_timeout' => 567, + ]); + + $this->assertSame(567, $container->getParameter('enqueue.transport.default.receive_timeout')); + } + + public function testThrowIfBuildQueueConsumerCalledButContextServiceDoesNotExist() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The service "enqueue.transport.default.context" does not exist.'); + $transport->buildQueueConsumer($container, []); + } + + public function testShouldBuildRpcClientWithDefaultOptions() + { + $container = new ContainerBuilder(); + $container->register('enqueue.transport.default.context', Context::class); + + $transport = new TransportFactory('default'); + + $transport->buildRpcClient($container, []); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.rpc_factory')); + $this->assertSame(RpcFactory::class, $container->getDefinition('enqueue.transport.default.rpc_factory')->getClass()); + + $this->assertTrue($container->hasDefinition('enqueue.transport.default.rpc_client')); + $this->assertSame(RpcClient::class, $container->getDefinition('enqueue.transport.default.rpc_client')->getClass()); + $this->assertEquals([ + new Reference('enqueue.transport.default.context'), + new Reference('enqueue.transport.default.rpc_factory'), + ], $container->getDefinition('enqueue.transport.default.rpc_client')->getArguments()); + } + + public function testThrowIfBuildRpcClientCalledButContextServiceDoesNotExist() + { + $container = new ContainerBuilder(); + + $transport = new TransportFactory('default'); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The service "enqueue.transport.default.context" does not exist.'); + $transport->buildRpcClient($container, []); + } + + /** + * @return [TreeBuilder, NodeDefinition] + */ + private function getRootNode(): array + { + if (method_exists(TreeBuilder::class, 'getRootNode')) { + $tb = new TreeBuilder('foo'); + + return [$tb, $tb->getRootNode()]; + } + + $tb = new TreeBuilder(); + + return [$tb, $tb->root('foo')]; + } +} diff --git a/pkg/enqueue/Tests/Symfony/LazyProducerTest.php b/pkg/enqueue/Tests/Symfony/LazyProducerTest.php new file mode 100644 index 000000000..c8ba596a8 --- /dev/null +++ b/pkg/enqueue/Tests/Symfony/LazyProducerTest.php @@ -0,0 +1,128 @@ +assertClassImplements(ProducerInterface::class, LazyProducer::class); + } + + public function testShouldNotCallRealProducerInConstructor() + { + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->never()) + ->method('get') + ; + + new LazyProducer($containerMock, 'realProducerId'); + } + + public function testShouldProxyAllArgumentOnSendEvent() + { + $topic = 'theTopic'; + $message = 'theMessage'; + + $realProducerMock = $this->createProducerMock(); + $realProducerMock + ->expects($this->once()) + ->method('sendEvent') + ->with($topic, $message) + ; + + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('get') + ->with('realProducerId') + ->willReturn($realProducerMock) + ; + + $lazyProducer = new LazyProducer($containerMock, 'realProducerId'); + + $lazyProducer->sendEvent($topic, $message); + } + + public function testShouldProxyAllArgumentOnSendCommand() + { + $command = 'theCommand'; + $message = 'theMessage'; + $needReply = false; + + $realProducerMock = $this->createProducerMock(); + $realProducerMock + ->expects($this->once()) + ->method('sendCommand') + ->with($command, $message, $needReply) + ; + + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('get') + ->with('realProducerId') + ->willReturn($realProducerMock) + ; + + $lazyProducer = new LazyProducer($containerMock, 'realProducerId'); + + $result = $lazyProducer->sendCommand($command, $message, $needReply); + + $this->assertNull($result); + } + + public function testShouldProxyReturnedPromiseBackOnSendCommand() + { + $expectedPromise = $this->createMock(Promise::class); + + $realProducerMock = $this->createProducerMock(); + $realProducerMock + ->expects($this->once()) + ->method('sendCommand') + ->willReturn($expectedPromise) + ; + + $containerMock = $this->createContainerMock(); + $containerMock + ->expects($this->once()) + ->method('get') + ->with('realProducerId') + ->willReturn($realProducerMock) + ; + + $lazyProducer = new LazyProducer($containerMock, 'realProducerId'); + + $actualPromise = $lazyProducer->sendCommand('aCommand', 'aMessage', true); + + $this->assertSame($expectedPromise, $actualPromise); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerInterface + */ + private function createProducerMock(): ProducerInterface + { + return $this->createMock(ProducerInterface::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|ContainerInterface + */ + private function createContainerMock(): ContainerInterface + { + return $this->createMock(ContainerInterface::class); + } +} diff --git a/pkg/enqueue/Tests/Symfony/MissingTransportFactoryTest.php b/pkg/enqueue/Tests/Symfony/MissingTransportFactoryTest.php deleted file mode 100644 index b466a5b63..000000000 --- a/pkg/enqueue/Tests/Symfony/MissingTransportFactoryTest.php +++ /dev/null @@ -1,73 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, MissingTransportFactory::class); - } - - public function testCouldBeConstructedWithNameAndPackages() - { - $transport = new MissingTransportFactory('aMissingTransportName', ['aPackage', 'anotherPackage']); - - $this->assertEquals('aMissingTransportName', $transport->getName()); - } - - public function testThrowOnProcessForOnePackageToInstall() - { - $transport = new MissingTransportFactory('aMissingTransportName', ['aFooPackage']); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Invalid configuration for path "foo": In order to use the transport "aMissingTransportName" install a package "aFooPackage"'); - $processor->process($tb->buildTree(), [[]]); - } - - public function testThrowOnProcessForSeveralPackagesToInstall() - { - $transport = new MissingTransportFactory('aMissingTransportName', ['aFooPackage', 'aBarPackage']); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Invalid configuration for path "foo": In order to use the transport "aMissingTransportName" install one of the packages "aFooPackage", "aBarPackage"'); - $processor->process($tb->buildTree(), [[]]); - } - - public function testThrowEvenIfThereAreSomeOptionsPassed() - { - $transport = new MissingTransportFactory('aMissingTransportName', ['aFooPackage', 'aBarPackage']); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('In order to use the transport "aMissingTransportName"'); - $processor->process($tb->buildTree(), [[ - 'foo' => 'fooVal', - 'bar' => 'barVal', - ]]); - } -} diff --git a/pkg/enqueue/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php b/pkg/enqueue/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php deleted file mode 100644 index cfe77e6d4..000000000 --- a/pkg/enqueue/Tests/Symfony/RabbitMqAmqpTransportFactoryTest.php +++ /dev/null @@ -1,129 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, RabbitMqAmqpTransportFactory::class); - } - - public function testShouldExtendAmqpTransportFactoryClass() - { - $this->assertClassExtends(AmqpTransportFactory::class, RabbitMqAmqpTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new RabbitMqAmqpTransportFactory(); - - $this->assertEquals('rabbitmq_amqp', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new RabbitMqAmqpTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new RabbitMqAmqpTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); - - $this->assertEquals([ - 'delay_strategy' => 'dlx', - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(AmqpConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'host' => 'localhost', - 'port' => 5672, - 'user' => 'guest', - 'pass' => 'guest', - 'vhost' => '/', - 'persisted' => false, - 'delay_strategy' => null, - ]); - - $this->assertEquals('enqueue.transport.rabbitmq_amqp.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.rabbitmq_amqp.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.rabbitmq_amqp.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqAmqpTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.rabbitmq_amqp.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(RabbitMqDriver::class, $driver->getClass()); - } -} diff --git a/pkg/enqueue/Tests/Util/Fixtures/JsonSerializableClass.php b/pkg/enqueue/Tests/Util/Fixtures/JsonSerializableClass.php index b612978e7..1a77ce0cf 100644 --- a/pkg/enqueue/Tests/Util/Fixtures/JsonSerializableClass.php +++ b/pkg/enqueue/Tests/Util/Fixtures/JsonSerializableClass.php @@ -6,7 +6,8 @@ class JsonSerializableClass implements \JsonSerializable { public $keyPublic = 'public'; - public function jsonSerialize() + #[\ReturnTypeWillChange] + public function jsonSerialize(): array { return [ 'key' => 'value', diff --git a/pkg/enqueue/Tests/Util/JSONTest.php b/pkg/enqueue/Tests/Util/JSONTest.php index c37862eb0..1a3df4211 100644 --- a/pkg/enqueue/Tests/Util/JSONTest.php +++ b/pkg/enqueue/Tests/Util/JSONTest.php @@ -16,7 +16,8 @@ public function testShouldDecodeString() public function testThrowIfMalformedJson() { - $this->setExpectedException(\InvalidArgumentException::class, 'The malformed json given. '); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The malformed json given. '); $this->assertSame(['foo' => 'fooVal'], JSON::decode('{]')); } @@ -38,15 +39,11 @@ public function nonStringDataProvider() /** * @dataProvider nonStringDataProvider - * - * @param mixed $value */ public function testShouldThrowExceptionIfInputIsNotString($value) { - $this->setExpectedException( - \InvalidArgumentException::class, - 'Accept only string argument but got:' - ); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Accept only string argument but got:'); $this->assertSame(0, JSON::decode($value)); } @@ -96,10 +93,8 @@ public function testShouldEncodeObjectOfJsonSerializableClass() public function testThrowIfValueIsResource() { - $this->setExpectedException( - \InvalidArgumentException::class, - 'Could not encode value into json. Error 8 and message Type is not supported' - ); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Could not encode value into json. Error 8 and message Type is not supported'); $resource = fopen('php://memory', 'r'); fclose($resource); diff --git a/pkg/enqueue/Tests/Util/UUIDTest.php b/pkg/enqueue/Tests/Util/UUIDTest.php index ac3090315..f21693e78 100644 --- a/pkg/enqueue/Tests/Util/UUIDTest.php +++ b/pkg/enqueue/Tests/Util/UUIDTest.php @@ -11,7 +11,7 @@ public function testShouldGenerateUniqueId() { $uuid = UUID::generate(); - $this->assertInternalType('string', $uuid); + $this->assertIsString($uuid); $this->assertEquals(36, strlen($uuid)); } diff --git a/pkg/enqueue/Tests/Util/VarExportTest.php b/pkg/enqueue/Tests/Util/VarExportTest.php index 1d2384ac9..b71e78a65 100644 --- a/pkg/enqueue/Tests/Util/VarExportTest.php +++ b/pkg/enqueue/Tests/Util/VarExportTest.php @@ -7,16 +7,8 @@ class VarExportTest extends TestCase { - public function testCouldBeConstructedWithValueAsArgument() - { - new VarExport('aVal'); - } - /** * @dataProvider provideValues - * - * @param mixed $value - * @param mixed $expected */ public function testShouldConvertValueToStringUsingVarExportFunction($value, $expected) { diff --git a/pkg/enqueue/Tests/fix_composer_json.php b/pkg/enqueue/Tests/fix_composer_json.php index bce1ebb75..324f1840b 100644 --- a/pkg/enqueue/Tests/fix_composer_json.php +++ b/pkg/enqueue/Tests/fix_composer_json.php @@ -4,8 +4,8 @@ $composerJson = json_decode(file_get_contents(__DIR__.'/../composer.json'), true); -$composerJson['config']['platform']['ext-amqp'] = '1.7'; +$composerJson['config']['platform']['ext-amqp'] = '1.9.3'; $composerJson['config']['platform']['ext-rdkafka'] = '3.3'; -$composerJson['config']['platform']['ext-gearman'] = '1.1'; +$composerJson['config']['platform']['ext-gearman'] = '2'; -file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, JSON_PRETTY_PRINT)); +file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, \JSON_PRETTY_PRINT)); diff --git a/pkg/enqueue/Util/JSON.php b/pkg/enqueue/Util/JSON.php index f85738e1d..67411af16 100644 --- a/pkg/enqueue/Util/JSON.php +++ b/pkg/enqueue/Util/JSON.php @@ -14,10 +14,7 @@ class JSON public static function decode($string) { if (!is_string($string)) { - throw new \InvalidArgumentException(sprintf( - 'Accept only string argument but got: "%s"', - is_object($string) ? get_class($string) : gettype($string) - )); + throw new \InvalidArgumentException(sprintf('Accept only string argument but got: "%s"', is_object($string) ? $string::class : gettype($string))); } // PHP7 fix - empty string and null cause syntax error @@ -26,32 +23,22 @@ public static function decode($string) } $decoded = json_decode($string, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return $decoded; } /** - * @param mixed $value - * * @return string */ public static function encode($value) { - $encoded = json_encode($value, JSON_UNESCAPED_UNICODE); - - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'Could not encode value into json. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + $encoded = json_encode($value, \JSON_UNESCAPED_UNICODE); + + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('Could not encode value into json. Error %s and message %s', json_last_error(), json_last_error_msg())); } return $encoded; diff --git a/pkg/enqueue/Util/Stringify.php b/pkg/enqueue/Util/Stringify.php new file mode 100644 index 000000000..d8a48a8d6 --- /dev/null +++ b/pkg/enqueue/Util/Stringify.php @@ -0,0 +1,30 @@ +value = $value; + } + + public function __toString(): string + { + if (is_string($this->value) || is_scalar($this->value)) { + return $this->value; + } + + return json_encode($this->value, \JSON_UNESCAPED_SLASHES); + } + + public static function that($value): self + { + return new self($value); + } +} diff --git a/pkg/enqueue/Util/VarExport.php b/pkg/enqueue/Util/VarExport.php index 4a48afadd..9a914706d 100644 --- a/pkg/enqueue/Util/VarExport.php +++ b/pkg/enqueue/Util/VarExport.php @@ -7,14 +7,8 @@ */ class VarExport { - /** - * @var mixed - */ private $value; - /** - * @param mixed $value - */ public function __construct($value) { $this->value = $value; diff --git a/pkg/enqueue/composer.json b/pkg/enqueue/composer.json index 4f3632d1f..c336c4bad 100644 --- a/pkg/enqueue/composer.json +++ b/pkg/enqueue/composer.json @@ -6,40 +6,45 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "queue-interop/queue-interop": "^0.6.2@dev", - "enqueue/null": "^0.8@dev", - "ramsey/uuid": "^2|^3.5", - "psr/log": "^1" + "php": "^8.1", + "queue-interop/amqp-interop": "^0.8.2", + "queue-interop/queue-interop": "^0.8", + "enqueue/null": "^0.10", + "enqueue/dsn": "^0.10", + "ramsey/uuid": "^3.5|^4", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/container": "^1.1 || ^2.0" }, "require-dev": { - "phpunit/phpunit": "~5.5", - "symfony/console": "^2.8|^3|^4", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4", - "symfony/event-dispatcher": "^2.8|^3|^4", - "symfony/http-kernel": "^2.8|^3|^4", - "enqueue/amqp-ext": "^0.8@dev", - "enqueue/amqp-lib": "^0.8@dev", - "enqueue/amqp-bunny": "^0.8@dev", - "enqueue/pheanstalk": "^0.8@dev", - "enqueue/gearman": "^0.8@dev", - "enqueue/rdkafka": "^0.8@dev", - "enqueue/dbal": "^0.8@dev", - "enqueue/fs": "^0.8@dev", - "enqueue/gps": "^0.8@dev", - "enqueue/redis": "^0.8@dev", - "enqueue/sqs": "^0.8@dev", - "enqueue/stomp": "^0.8@dev", - "enqueue/test": "^0.8@dev", - "enqueue/simple-client": "^0.8@dev", - "enqueue/mongodb": "^0.8@dev", - "empi89/php-amqp-stubs": "*@dev" + "phpunit/phpunit": "^9.5", + "symfony/console": "^5.41|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "enqueue/amqp-ext": "0.10.x-dev", + "enqueue/amqp-lib": "0.10.x-dev", + "enqueue/amqp-bunny": "0.10.x-dev", + "enqueue/pheanstalk": "0.10.x-dev", + "enqueue/gearman": "0.10.x-dev", + "enqueue/rdkafka": "0.10.x-dev", + "enqueue/dbal": "0.10.x-dev", + "enqueue/fs": "0.10.x-dev", + "enqueue/gps": "0.10.x-dev", + "enqueue/redis": "0.10.x-dev", + "enqueue/sqs": "0.10.x-dev", + "enqueue/stomp": "0.10.x-dev", + "enqueue/test": "0.10.x-dev", + "enqueue/simple-client": "0.10.x-dev", + "enqueue/mongodb": "0.10.x-dev", + "empi89/php-amqp-stubs": "*@dev", + "enqueue/dsn": "0.10.x-dev" }, "suggest": { - "symfony/console": "^2.8|^3|^4 If you want to use li commands", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4", + "symfony/console": "^5.4|^6.0 If you want to use cli commands", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", "enqueue/amqp-ext": "AMQP transport (based on php extension)", "enqueue/stomp": "STOMP transport", "enqueue/fs": "Filesystem transport", @@ -56,7 +61,6 @@ }, "autoload": { "psr-4": { "Enqueue\\": "" }, - "files": ["functions_include.php"], "exclude-from-classmap": [ "/Tests/" ] @@ -69,7 +73,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/enqueue/functions.php b/pkg/enqueue/functions.php deleted file mode 100644 index 537f9964a..000000000 --- a/pkg/enqueue/functions.php +++ /dev/null @@ -1,181 +0,0 @@ -createContext(); -} - -/** - * @param PsrContext $c - * @param string $topic - * @param string $body - */ -function send_topic(PsrContext $c, $topic, $body) -{ - $topic = $c->createTopic($topic); - $message = $c->createMessage($body); - - $c->createProducer()->send($topic, $message); -} - -/** - * @param PsrContext $c - * @param string $queue - * @param string $body - */ -function send_queue(PsrContext $c, $queue, $body) -{ - $queue = $c->createQueue($queue); - $message = $c->createMessage($body); - - $c->createProducer()->send($queue, $message); -} - -/** - * @param PsrContext $c - * @param string $queue - * @param callable $callback - */ -function consume(PsrContext $c, $queue, callable $callback) -{ - $queueConsumer = new QueueConsumer($c); - $queueConsumer->bind($queue, $callback); - - $queueConsumer->consume(); -} diff --git a/pkg/enqueue/functions_include.php b/pkg/enqueue/functions_include.php deleted file mode 100644 index cf5502ab1..000000000 --- a/pkg/enqueue/functions_include.php +++ /dev/null @@ -1,6 +0,0 @@ - - + diff --git a/pkg/fs/.github/workflows/ci.yml b/pkg/fs/.github/workflows/ci.yml new file mode 100644 index 000000000..65cfbbb2d --- /dev/null +++ b/pkg/fs/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: SYMFONY_DEPRECATIONS_HELPER=weak vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/fs/.travis.yml b/pkg/fs/.travis.yml deleted file mode 100644 index 1a44d0c7e..000000000 --- a/pkg/fs/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - SYMFONY_DEPRECATIONS_HELPER=weak vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/fs/CannotObtainLockException.php b/pkg/fs/CannotObtainLockException.php index 854d726f8..2788dede8 100644 --- a/pkg/fs/CannotObtainLockException.php +++ b/pkg/fs/CannotObtainLockException.php @@ -1,8 +1,10 @@ context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $topic = $this->createRouterTopic(); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($topic, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $log = function ($text, ...$args) use ($logger) { - $logger->debug(sprintf('[FsDriver] '.$text, ...$args)); - }; - - // setup router - $routerTopic = $this->createRouterTopic(); - $routerQueue = $this->createQueue($this->config->getRouterQueueName()); - - $log('Declare router exchange "%s" file: %s', $routerTopic->getTopicName(), $routerTopic->getFileInfo()); - $this->context->declareDestination($routerTopic); - - $log('Declare router queue "%s" file: %s', $routerQueue->getQueueName(), $routerTopic->getFileInfo()); - $this->context->declareDestination($routerQueue); - - // setup queues - foreach ($this->queueMetaRegistry->getQueuesMeta() as $meta) { - $queue = $this->createQueue($meta->getClientName()); - - $log('Declare processor queue "%s" file: %s', $queue->getQueueName(), $queue->getFileInfo()); - $this->context->declareDestination($queue); - } - } - - /** - * {@inheritdoc} - * - * @return FsDestination - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - return $this->context->createQueue($transportName); - } - - /** - * {@inheritdoc} - * - * @return FsMessage - */ - public function createTransportMessage(Message $message) - { - $properties = $message->getProperties(); - - $headers = $message->getHeaders(); - $headers['content_type'] = $message->getContentType(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($properties); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - - return $transportMessage; - } - - /** - * @param FsMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - - $clientMessage->setContentType($message->getHeader('content_type')); - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setPriority(MessagePriority::NORMAL); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - return $clientMessage; - } - - /** - * @return Config - */ - public function getConfig() - { - return $this->config; - } - - /** - * @return FsDestination - */ - private function createRouterTopic() - { - return $this->context->createTopic( - $this->config->createTransportQueueName($this->config->getRouterTopicName()) - ); - } -} diff --git a/pkg/fs/FsConnectionFactory.php b/pkg/fs/FsConnectionFactory.php index 8627edb95..9c4ba17e5 100644 --- a/pkg/fs/FsConnectionFactory.php +++ b/pkg/fs/FsConnectionFactory.php @@ -1,10 +1,14 @@ sys_get_temp_dir().'/enqueue']; + $config = $this->parseDsn('file://'.sys_get_temp_dir().'/enqueue'); } elseif (is_string($config)) { - $config = $this->parseDsn($config); + if ('/' === $config[0]) { + $config = $this->parseDsn('file://'.$config); + } else { + $config = $this->parseDsn($config); + } } elseif (is_array($config)) { } else { throw new \LogicException('The config must be either an array of options, a DSN string or null'); } $this->config = array_replace($this->defaultConfig(), $config); + + if (empty($this->config['path'])) { + throw new \LogicException('The path option must be set.'); + } } /** - * {@inheritdoc} - * * @return FsContext */ - public function createContext() + public function createContext(): Context { return new FsContext( $this->config['path'], @@ -58,49 +68,24 @@ public function createContext() ); } - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) + private function parseDsn(string $dsn): array { - if ($dsn && '/' === $dsn[0]) { - return ['path' => $dsn]; - } + $dsn = Dsn::parseFirst($dsn); - if (false === strpos($dsn, 'file:')) { - throw new \LogicException(sprintf('The given DSN "%s" is not supported. Must start with "file:".', $dsn)); + $supportedSchemes = ['file']; + if (false == in_array($dsn->getSchemeProtocol(), $supportedSchemes, true)) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be one of "%s"', $dsn->getSchemeProtocol(), implode('", "', $supportedSchemes))); } - $dsn = substr($dsn, 7); - - $path = parse_url(http://github.com/$dsn,%20PHP_URL_PATH); - $query = parse_url(http://github.com/$dsn,%20PHP_URL_QUERY); - - if ('/' != $path[0]) { - throw new \LogicException(sprintf('Failed to parse DSN path "%s". The path must start with "/"', $path)); - } - - if ($query) { - $config = []; - parse_str($query, $config); - } - - if (isset($config['pre_fetch_count'])) { - $config['pre_fetch_count'] = (int) $config['pre_fetch_count']; - } - - if (isset($config['chmod'])) { - $config['chmod'] = intval($config['chmod'], 8); - } - - $config['path'] = $path; - - return $config; + return array_filter(array_replace($dsn->getQuery(), [ + 'path' => $dsn->getPath(), + 'pre_fetch_count' => $dsn->getDecimal('pre_fetch_count'), + 'chmod' => $dsn->getOctal('chmod'), + 'polling_interval' => $dsn->getDecimal('polling_interval'), + ]), function ($value) { return null !== $value; }); } - private function defaultConfig() + private function defaultConfig(): array { return [ 'path' => null, diff --git a/pkg/fs/FsConsumer.php b/pkg/fs/FsConsumer.php index f0d8463fe..614461eb2 100644 --- a/pkg/fs/FsConsumer.php +++ b/pkg/fs/FsConsumer.php @@ -1,12 +1,15 @@ context = $context; $this->destination = $destination; @@ -49,40 +49,32 @@ public function __construct(FsContext $context, FsDestination $destination, $pre /** * Set polling interval in milliseconds. - * - * @param int $msec */ - public function setPollingInterval($msec) + public function setPollingInterval(int $msec): void { - $this->pollingInterval = $msec * 1000; + $this->pollingInterval = $msec; } /** * Get polling interval in milliseconds. - * - * @return int */ - public function getPollingInterval() + public function getPollingInterval(): int { - return (int) $this->pollingInterval / 1000; + return $this->pollingInterval; } /** - * {@inheritdoc} - * * @return FsDestination */ - public function getQueue() + public function getQueue(): Queue { return $this->destination; } /** - * {@inheritdoc} - * - * @return FsMessage|null + * @return FsMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { $timeout /= 1000; $startAt = microtime(true); @@ -95,21 +87,21 @@ public function receive($timeout = 0) } if ($timeout && (microtime(true) - $startAt) >= $timeout) { - return; + return null; } - usleep($this->pollingInterval); + usleep($this->pollingInterval * 1000); if ($timeout && (microtime(true) - $startAt) >= $timeout) { - return; + return null; } } } /** - * {@inheritdoc} + * @return FsMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { if ($this->preFetchedMessages) { return array_shift($this->preFetchedMessages); @@ -120,7 +112,7 @@ public function receiveNoWait() while ($count) { $frame = $this->readFrame($file, 1); - //guards + // guards if ($frame && false == ('|' == $frame[0] || ' ' == $frame[0])) { throw new \LogicException(sprintf('The frame could start from either " " or "|". The malformed frame starts with "%s".', $frame[0])); } @@ -140,15 +132,15 @@ public function receiveNoWait() $expireAt = $fetchedMessage->getHeader('x-expire-at'); if ($expireAt && $expireAt - microtime(true) < 0) { // message has expired, just drop it. - return; + return null; } $this->preFetchedMessages[] = $fetchedMessage; } catch (\Exception $e) { - throw new \LogicException(sprintf("Cannot decode json message '%s'", $rawMessage), null, $e); + throw new \LogicException(sprintf("Cannot decode json message '%s'", $rawMessage), 0, $e); } } else { - return; + return null; } --$count; @@ -158,20 +150,16 @@ public function receiveNoWait() if ($this->preFetchedMessages) { return array_shift($this->preFetchedMessages); } + + return null; } - /** - * {@inheritdoc} - */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { // do nothing. fs transport always works in auto ack mode } - /** - * {@inheritdoc} - */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { InvalidMessageException::assertMessageInstanceOf($message, FsMessage::class); @@ -182,40 +170,31 @@ public function reject(PsrMessage $message, $requeue = false) } } - /** - * @return int - */ - public function getPreFetchCount() + public function getPreFetchCount(): int { return $this->preFetchCount; } - /** - * @param int $preFetchCount - */ - public function setPreFetchCount($preFetchCount) + public function setPreFetchCount(int $preFetchCount): void { $this->preFetchCount = $preFetchCount; } /** * @param resource $file - * @param int $frameNumber - * - * @return string */ - private function readFrame($file, $frameNumber) + private function readFrame($file, int $frameNumber): string { $frameSize = 64; $offset = $frameNumber * $frameSize; - fseek($file, -$offset, SEEK_END); + fseek($file, -$offset, \SEEK_END); $frame = fread($file, $frameSize); if ('' == $frame) { return ''; } - if (false !== strpos($frame, '|{')) { + if (str_contains($frame, '|{')) { return $frame; } diff --git a/pkg/fs/FsContext.php b/pkg/fs/FsContext.php index 5a0e00339..c735e13aa 100644 --- a/pkg/fs/FsContext.php +++ b/pkg/fs/FsContext.php @@ -1,15 +1,23 @@ mkdir($storeDir); @@ -56,66 +58,57 @@ public function __construct($storeDir, $preFetchCount, $chmod, $pollingInterval } /** - * {@inheritdoc} - * * @return FsMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new FsMessage($body, $properties, $headers); } /** - * {@inheritdoc} - * * @return FsDestination */ - public function createTopic($topicName) + public function createTopic(string $topicName): Topic { return $this->createQueue($topicName); } /** - * {@inheritdoc} - * * @return FsDestination */ - public function createQueue($queueName) + public function createQueue(string $queueName): Queue { return new FsDestination(new \SplFileInfo($this->getStoreDir().'/'.$queueName)); } - /** - * @param PsrDestination|FsDestination $destination - */ - public function declareDestination(PsrDestination $destination) + public function declareDestination(FsDestination $destination): void { - InvalidDestinationException::assertDestinationInstanceOf($destination, FsDestination::class); + // InvalidDestinationException::assertDestinationInstanceOf($destination, FsDestination::class); set_error_handler(function ($severity, $message, $file, $line) { throw new \ErrorException($message, 0, $severity, $file, $line); }); try { - if (false == file_exists($destination->getFileInfo())) { - touch($destination->getFileInfo()); - chmod($destination->getFileInfo(), $this->chmod); + if (false == file_exists((string) $destination->getFileInfo())) { + touch((string) $destination->getFileInfo()); + chmod((string) $destination->getFileInfo(), $this->chmod); } } finally { restore_error_handler(); } } - public function workWithFile(FsDestination $destination, $mode, callable $callback) + public function workWithFile(FsDestination $destination, string $mode, callable $callback) { $this->declareDestination($destination); set_error_handler(function ($severity, $message, $file, $line) { throw new \ErrorException($message, 0, $severity, $file, $line); - }); + }, \E_ALL & ~\E_USER_DEPRECATED); try { - $file = fopen($destination->getFileInfo(), $mode); + $file = fopen((string) $destination->getFileInfo(), $mode); $this->lock->lock($destination); return call_user_func($callback, $destination, $file); @@ -130,33 +123,29 @@ public function workWithFile(FsDestination $destination, $mode, callable $callba } /** - * {@inheritdoc} - * * @return FsDestination */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { - return new FsDestination(new TempFile($this->getStoreDir().'/'.uniqid('tmp-q-', true))); + return new FsDestination( + new TempFile($this->getStoreDir().'/'.uniqid('tmp-q-', true)) + ); } /** - * {@inheritdoc} - * * @return FsProducer */ - public function createProducer() + public function createProducer(): Producer { return new FsProducer($this); } /** - * {@inheritdoc} - * - * @param FsDestination|PsrDestination $destination + * @param FsDestination $destination * * @return FsConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, FsDestination::class); @@ -169,15 +158,20 @@ public function createConsumer(PsrDestination $destination) return $consumer; } - public function close() + public function close(): void { $this->lock->releaseAll(); } + public function createSubscriptionConsumer(): SubscriptionConsumer + { + throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); + } + /** - * @param PsrQueue|FsDestination $queue + * @param FsDestination $queue */ - public function purge(PsrQueue $queue) + public function purgeQueue(Queue $queue): void { InvalidDestinationException::assertDestinationInstanceOf($queue, FsDestination::class); @@ -186,26 +180,17 @@ public function purge(PsrQueue $queue) }); } - /** - * @return int - */ - public function getPreFetchCount() + public function getPreFetchCount(): int { return $this->preFetchCount; } - /** - * @param int $preFetchCount - */ - public function setPreFetchCount($preFetchCount) + public function setPreFetchCount(int $preFetchCount): void { $this->preFetchCount = $preFetchCount; } - /** - * @return string - */ - private function getStoreDir() + private function getStoreDir(): string { if (false == is_dir($this->storeDir)) { throw new \LogicException(sprintf('The directory %s does not exist', $this->storeDir)); diff --git a/pkg/fs/FsDestination.php b/pkg/fs/FsDestination.php index 47fd47f8f..559391785 100644 --- a/pkg/fs/FsDestination.php +++ b/pkg/fs/FsDestination.php @@ -1,54 +1,41 @@ file = $file; } - /** - * @return \SplFileInfo - */ - public function getFileInfo() + public function getFileInfo(): \SplFileInfo { return $this->file; } - /** - * @return string - */ - public function getName() + public function getName(): string { return $this->file->getFilename(); } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { - return $this->getName(); + return $this->file->getFilename(); } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getTopicName(): string { - return $this->getName(); + return $this->file->getFilename(); } } diff --git a/pkg/fs/FsMessage.php b/pkg/fs/FsMessage.php index afb8f838b..45312e52c 100644 --- a/pkg/fs/FsMessage.php +++ b/pkg/fs/FsMessage.php @@ -1,10 +1,12 @@ body = $body; $this->properties = $properties; @@ -39,172 +36,109 @@ public function __construct($body = '', array $properties = [], array $headers = $this->redelivered = false; } - /** - * @param string $body - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * @param array $properties - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * @param array $headers - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * @return bool - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * @param bool $redelivered - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = $redelivered; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', (string) $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id'); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', (string) $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id'); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } - /** - * @param string|null $replyTo - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * @return string|null - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } - /** - * {@inheritdoc} - */ - public function jsonSerialize() + public function jsonSerialize(): array { return [ 'body' => $this->getBody(), @@ -213,20 +147,11 @@ public function jsonSerialize() ]; } - /** - * @param string $json - * - * @return FsMessage - */ - public static function jsonUnserialize($json) + public static function jsonUnserialize(string $json): self { $data = json_decode($json, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return new self($data['body'], $data['properties'], $data['headers']); diff --git a/pkg/fs/FsProducer.php b/pkg/fs/FsProducer.php index 21f890575..067e54b36 100644 --- a/pkg/fs/FsProducer.php +++ b/pkg/fs/FsProducer.php @@ -1,17 +1,19 @@ context = $context; } /** - * {@inheritdoc} - * * @param FsDestination $destination * @param FsMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, FsDestination::class); InvalidMessageException::assertMessageInstanceOf($message, FsMessage::class); $this->context->workWithFile($destination, 'a+', function (FsDestination $destination, $file) use ($message) { $fileInfo = $destination->getFileInfo(); - if ($fileInfo instanceof TempFile && false == file_exists($fileInfo)) { + if ($fileInfo instanceof TempFile && false == file_exists((string) $fileInfo)) { return; } @@ -56,12 +53,8 @@ public function send(PsrDestination $destination, PsrMessage $message) $rawMessage = str_replace('|{', '\|\{', $rawMessage); $rawMessage = '|'.$rawMessage; - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'Could not encode value into json. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('Could not encode value into json. Error %s and message %s', json_last_error(), json_last_error_msg())); } $rawMessage = str_repeat(' ', 64 - (strlen($rawMessage) % 64)).$rawMessage; @@ -70,60 +63,42 @@ public function send(PsrDestination $destination, PsrMessage $message) }); } - /** - * {@inheritdoc} - */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { if (null === $deliveryDelay) { - return; + return $this; } throw DeliveryDelayNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return null; } - /** - * {@inheritdoc} - */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { if (null === $priority) { - return; + return $this; } throw PriorityNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return null; } - /** - * {@inheritdoc} - */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { $this->timeToLive = $timeToLive; return $this; } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return null; } diff --git a/pkg/fs/LegacyFilesystemLock.php b/pkg/fs/LegacyFilesystemLock.php index a765d2ced..328fb7098 100644 --- a/pkg/fs/LegacyFilesystemLock.php +++ b/pkg/fs/LegacyFilesystemLock.php @@ -19,9 +19,6 @@ public function __construct() $this->lockHandlers = []; } - /** - * {@inheritdoc} - */ public function lock(FsDestination $destination) { $lockHandler = $this->getLockHandler($destination); @@ -31,9 +28,6 @@ public function lock(FsDestination $destination) } } - /** - * {@inheritdoc} - */ public function release(FsDestination $destination) { $lockHandler = $this->getLockHandler($destination); @@ -51,8 +45,6 @@ public function releaseAll() } /** - * @param FsDestination $destination - * * @return LockHandler */ private function getLockHandler(FsDestination $destination) @@ -161,7 +153,7 @@ public function lock($blocking = false) // On Windows, even if PHP doc says the contrary, LOCK_NB works, see // https://bugs.php.net/54129 - if (!flock($this->handle, LOCK_EX | ($blocking ? 0 : LOCK_NB))) { + if (!flock($this->handle, \LOCK_EX | ($blocking ? 0 : \LOCK_NB))) { fclose($this->handle); $this->handle = null; @@ -177,7 +169,7 @@ public function lock($blocking = false) public function release() { if ($this->handle) { - flock($this->handle, LOCK_UN | LOCK_NB); + flock($this->handle, \LOCK_UN | \LOCK_NB); fclose($this->handle); $this->handle = null; } diff --git a/pkg/fs/Lock.php b/pkg/fs/Lock.php index 02c2fbb76..16349f22c 100644 --- a/pkg/fs/Lock.php +++ b/pkg/fs/Lock.php @@ -1,5 +1,7 @@ Supporting Enqueue + +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Enqueue Filesystem Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/fs.png?branch=master)](https://travis-ci.org/php-enqueue/fs) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/fs/ci.yml?branch=master)](https://github.com/php-enqueue/fs/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/fs/d/total.png)](https://packagist.org/packages/enqueue/fs) [![Latest Stable Version](https://poser.pugx.org/enqueue/fs/version.png)](https://packagist.org/packages/enqueue/fs) - -This is an implementation of PSR queue specification. It allows you to send and consume message stored locally in files. + +This is an implementation of Queue Interop specification. It allows you to send and consume message stored locally in files. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/filesystem/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com diff --git a/pkg/fs/Symfony/FsTransportFactory.php b/pkg/fs/Symfony/FsTransportFactory.php deleted file mode 100644 index da4921305..000000000 --- a/pkg/fs/Symfony/FsTransportFactory.php +++ /dev/null @@ -1,124 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifString() - ->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('dsn') - ->info('The path to a directory where to store messages given as DSN. For example file://tmp/foo') - ->end() - ->scalarNode('path') - ->cannotBeEmpty() - ->info('The store directory where all queue\topics files will be created and messages are stored') - ->end() - ->integerNode('pre_fetch_count') - ->min(1) - ->defaultValue(1) - ->info('The option tells how many messages should be read from file at once. The feature save resources but could lead to bigger messages lose.') - ->end() - ->integerNode('chmod') - ->defaultValue(0600) - ->info('The queue files are created with this given permissions if not exist.') - ->end() - ->integerNode('polling_interval') - ->defaultValue(100) - ->min(50) - ->info('How often query for new messages.') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factory = new Definition(FsConnectionFactory::class); - $factory->setArguments(isset($config['dsn']) ? [$config['dsn']] : [$config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(FsContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(FsDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/fs/Tests/Client/FsDriverTest.php b/pkg/fs/Tests/Client/FsDriverTest.php deleted file mode 100644 index 14dfcf076..000000000 --- a/pkg/fs/Tests/Client/FsDriverTest.php +++ /dev/null @@ -1,392 +0,0 @@ -assertClassImplements(DriverInterface::class, FsDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new FsDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = $this->createDummyConfig(); - - $driver = new FsDriver($this->createPsrContextMock(), $config, $this->createDummyQueueMetaRegistry()); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new FsDestination(new TempFile(sys_get_temp_dir().'/queue-name')); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new FsDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new FsDestination(new TempFile(sys_get_temp_dir().'/queue-name')); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new FsDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new FsMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new FsDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - - $this->assertNull($clientMessage->getExpire()); - $this->assertSame(MessagePriority::NORMAL, $clientMessage->getPriority()); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setPriority(MessagePriority::VERY_HIGH); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new FsMessage()) - ; - - $driver = new FsDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(FsMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testShouldSendMessageToRouter() - { - $topic = new FsDestination(TempFile::generate()); - $transportMessage = new FsMessage(); - $config = $this->createDummyConfig(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->with('aprefix.router') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new FsDriver( - $context, - $config, - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new FsDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new FsDestination(TempFile::generate()); - $transportMessage = new FsMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new FsDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new FsDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new FsDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBroker() - { - $routerTopic = new FsDestination(TempFile::generate()); - $routerQueue = new FsDestination(TempFile::generate()); - - $processorQueue = new FsDestination(TempFile::generate()); - - $context = $this->createPsrContextMock(); - // setup router - $context - ->expects($this->at(0)) - ->method('createTopic') - ->willReturn($routerTopic) - ; - $context - ->expects($this->at(1)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(2)) - ->method('declareDestination') - ->with($this->identicalTo($routerTopic)) - ; - $context - ->expects($this->at(3)) - ->method('declareDestination') - ->with($this->identicalTo($routerQueue)) - ; - // setup processor queue - $context - ->expects($this->at(4)) - ->method('createQueue') - ->willReturn($processorQueue) - ; - $context - ->expects($this->at(5)) - ->method('declareDestination') - ->with($this->identicalTo($processorQueue)) - ; - - $meta = new QueueMetaRegistry($this->createDummyConfig(), [ - 'default' => [], - ]); - - $driver = new FsDriver( - $context, - $this->createDummyConfig(), - $meta - ); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|FsContext - */ - private function createPsrContextMock() - { - return $this->createMock(FsContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer - */ - private function createPsrProducerMock() - { - return $this->createMock(PsrProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/fs/Tests/FsConnectionFactoryConfigTest.php b/pkg/fs/Tests/FsConnectionFactoryConfigTest.php index c79581c51..0b3411f2c 100644 --- a/pkg/fs/Tests/FsConnectionFactoryConfigTest.php +++ b/pkg/fs/Tests/FsConnectionFactoryConfigTest.php @@ -4,6 +4,7 @@ use Enqueue\Fs\FsConnectionFactory; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; /** @@ -12,6 +13,7 @@ class FsConnectionFactoryConfigTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testThrowNeitherArrayStringNorNullGivenAsConfig() { @@ -21,10 +23,10 @@ public function testThrowNeitherArrayStringNorNullGivenAsConfig() new FsConnectionFactory(new \stdClass()); } - public function testThrowIfSchemeIsNotAmqp() + public function testThrowIfSchemeIsNotFileScheme() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN "http://example.com/" is not supported. Must start with "file:'); + $this->expectExceptionMessage('The given scheme protocol "http" is not supported. It must be one of "file"'); new FsConnectionFactory('http://example.com/'); } @@ -32,16 +34,23 @@ public function testThrowIfSchemeIsNotAmqp() public function testThrowIfDsnCouldNotBeParsed() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Failed to parse DSN path ":@/". The path must start with "/"'); + $this->expectExceptionMessage('The DSN is invalid.'); - new FsConnectionFactory('file://:@/'); + new FsConnectionFactory('foo'); + } + + public function testThrowIfArrayConfigGivenWithEmptyPath() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The path option must be set.'); + + new FsConnectionFactory([ + 'path' => null, + ]); } /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { diff --git a/pkg/fs/Tests/FsConnectionFactoryTest.php b/pkg/fs/Tests/FsConnectionFactoryTest.php index 30cfc6f69..2df442342 100644 --- a/pkg/fs/Tests/FsConnectionFactoryTest.php +++ b/pkg/fs/Tests/FsConnectionFactoryTest.php @@ -5,15 +5,17 @@ use Enqueue\Fs\FsConnectionFactory; use Enqueue\Fs\FsContext; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\ConnectionFactory; class FsConnectionFactoryTest extends \PHPUnit\Framework\TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, FsConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, FsConnectionFactory::class); } public function testShouldCreateContext() diff --git a/pkg/fs/Tests/FsConsumerTest.php b/pkg/fs/Tests/FsConsumerTest.php index b3f14266c..67f03ae98 100644 --- a/pkg/fs/Tests/FsConsumerTest.php +++ b/pkg/fs/Tests/FsConsumerTest.php @@ -8,7 +8,7 @@ use Enqueue\Fs\FsMessage; use Enqueue\Fs\FsProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConsumer; +use Interop\Queue\Consumer; use Makasim\File\TempFile; class FsConsumerTest extends \PHPUnit\Framework\TestCase @@ -17,12 +17,7 @@ class FsConsumerTest extends \PHPUnit\Framework\TestCase public function testShouldImplementConsumerInterface() { - $this->assertClassImplements(PsrConsumer::class, FsConsumer::class); - } - - public function testCouldBeConstructedWithContextAndDestinationAndPreFetchCountAsArguments() - { - new FsConsumer($this->createContextMock(), new FsDestination(TempFile::generate()), 1); + $this->assertClassImplements(Consumer::class, FsConsumer::class); } public function testShouldReturnDestinationSetInConstructorOnGetQueue() @@ -50,6 +45,9 @@ public function testShouldAllowGetPreviouslySetPreFetchCount() $this->assertSame(456, $consumer->getPreFetchCount()); } + /** + * @doesNotPerformAssertions + */ public function testShouldDoNothingOnAcknowledge() { $consumer = new FsConsumer($this->createContextMock(), new FsDestination(TempFile::generate()), 123); @@ -57,6 +55,9 @@ public function testShouldDoNothingOnAcknowledge() $consumer->acknowledge(new FsMessage()); } + /** + * @doesNotPerformAssertions + */ public function testShouldDoNothingOnReject() { $consumer = new FsConsumer($this->createContextMock(), new FsDestination(TempFile::generate()), 123); @@ -134,7 +135,7 @@ public function testShouldWaitTwoSecondsForMessageAndExitOnReceive() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|FsProducer + * @return \PHPUnit\Framework\MockObject\MockObject|FsProducer */ private function createProducerMock() { @@ -142,7 +143,7 @@ private function createProducerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|FsContext + * @return \PHPUnit\Framework\MockObject\MockObject|FsContext */ private function createContextMock() { diff --git a/pkg/fs/Tests/FsContextTest.php b/pkg/fs/Tests/FsContextTest.php index 0d1ea4c63..9d5a5f1fc 100644 --- a/pkg/fs/Tests/FsContextTest.php +++ b/pkg/fs/Tests/FsContextTest.php @@ -9,27 +9,24 @@ use Enqueue\Fs\FsProducer; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrContext; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\Context; +use Interop\Queue\Exception\InvalidDestinationException; use Makasim\File\TempFile; class FsContextTest extends \PHPUnit\Framework\TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementContextInterface() { - $this->assertClassImplements(PsrContext::class, FsContext::class); - } - - public function testCouldBeConstructedWithExpectedArguments() - { - new FsContext(sys_get_temp_dir(), 1, 0666); + $this->assertClassImplements(Context::class, FsContext::class); } public function testShouldAllowCreateEmptyMessage() { - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $message = $context->createMessage(); @@ -42,7 +39,7 @@ public function testShouldAllowCreateEmptyMessage() public function testShouldAllowCreateCustomMessage() { - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $message = $context->createMessage('theBody', ['aProp' => 'aPropVal'], ['aHeader' => 'aHeaderVal']); @@ -57,7 +54,7 @@ public function testShouldCreateQueue() { $tmpFile = new TempFile(sys_get_temp_dir().'/foo'); - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $queue = $context->createQueue($tmpFile->getFilename()); @@ -72,7 +69,7 @@ public function testShouldAllowCreateTopic() { $tmpFile = new TempFile(sys_get_temp_dir().'/foo'); - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $topic = $context->createTopic($tmpFile->getFilename()); @@ -87,7 +84,7 @@ public function testShouldAllowCreateTmpQueue() { $tmpFile = new TempFile(sys_get_temp_dir().'/foo'); - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $queue = $context->createTemporaryQueue(); @@ -100,7 +97,7 @@ public function testShouldCreateProducer() { $tmpFile = new TempFile(sys_get_temp_dir().'/foo'); - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $producer = $context->createProducer(); @@ -111,7 +108,7 @@ public function testShouldThrowIfNotFsDestinationGivenOnCreateConsumer() { $tmpFile = new TempFile(sys_get_temp_dir().'/foo'); - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $this->expectException(InvalidDestinationException::class); $this->expectExceptionMessage('The destination must be an instance of Enqueue\Fs\FsDestination but got Enqueue\Null\NullQueue.'); @@ -120,11 +117,14 @@ public function testShouldThrowIfNotFsDestinationGivenOnCreateConsumer() $this->assertInstanceOf(FsConsumer::class, $consumer); } + /** + * @doesNotPerformAssertions + */ public function testShouldCreateConsumer() { $tmpFile = new TempFile(sys_get_temp_dir().'/foo'); - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $queue = $context->createQueue($tmpFile->getFilename()); @@ -135,7 +135,7 @@ public function testShouldPropagatePreFetchCountToCreatedConsumer() { $tmpFile = new TempFile(sys_get_temp_dir().'/foo'); - $context = new FsContext(sys_get_temp_dir(), 123, 0666); + $context = new FsContext(sys_get_temp_dir(), 123, 0666, 100); $queue = $context->createQueue($tmpFile->getFilename()); @@ -151,7 +151,7 @@ public function testShouldAllowGetPreFetchCountSetInConstructor() { $tmpFile = new TempFile(sys_get_temp_dir().'/foo'); - $context = new FsContext(sys_get_temp_dir(), 123, 0666); + $context = new FsContext(sys_get_temp_dir(), 123, 0666, 100); $this->assertSame(123, $context->getPreFetchCount()); } @@ -160,7 +160,7 @@ public function testShouldAllowGetPreviouslySetPreFetchCount() { $tmpFile = new TempFile(sys_get_temp_dir().'/foo'); - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $context->setPreFetchCount(456); @@ -173,11 +173,11 @@ public function testShouldAllowPurgeMessagesFromQueue() file_put_contents($tmpFile, 'foo'); - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $queue = $context->createQueue($tmpFile->getFilename()); - $context->purge($queue); + $context->purgeQueue($queue); $this->assertEmpty(file_get_contents($tmpFile)); } @@ -186,11 +186,11 @@ public function testShouldCreateFileOnFilesystemIfNotExistOnDeclareDestination() { $tmpFile = new TempFile(sys_get_temp_dir().'/'.uniqid()); - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $queue = $context->createQueue($tmpFile->getFilename()); - $this->assertFileNotExists((string) $tmpFile); + $this->assertFileDoesNotExist((string) $tmpFile); $context->declareDestination($queue); diff --git a/pkg/fs/Tests/FsDestinationTest.php b/pkg/fs/Tests/FsDestinationTest.php index cfe9e1aec..6e5753f6f 100644 --- a/pkg/fs/Tests/FsDestinationTest.php +++ b/pkg/fs/Tests/FsDestinationTest.php @@ -4,8 +4,8 @@ use Enqueue\Fs\FsDestination; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrQueue; -use Interop\Queue\PsrTopic; +use Interop\Queue\Queue; +use Interop\Queue\Topic; use Makasim\File\TempFile; class FsDestinationTest extends \PHPUnit\Framework\TestCase @@ -14,8 +14,8 @@ class FsDestinationTest extends \PHPUnit\Framework\TestCase public function testShouldImplementsTopicAndQueueInterfaces() { - $this->assertClassImplements(PsrTopic::class, FsDestination::class); - $this->assertClassImplements(PsrQueue::class, FsDestination::class); + $this->assertClassImplements(Topic::class, FsDestination::class); + $this->assertClassImplements(Queue::class, FsDestination::class); } public function testCouldBeConstructedWithSplFileAsFirstArgument() diff --git a/pkg/fs/Tests/FsMessageTest.php b/pkg/fs/Tests/FsMessageTest.php index c2f788d04..90655b620 100644 --- a/pkg/fs/Tests/FsMessageTest.php +++ b/pkg/fs/Tests/FsMessageTest.php @@ -89,7 +89,7 @@ public function testCouldBeUnserializedFromJson() $json = json_encode($message); - //guard + // guard $this->assertNotEmpty($json); $unserializedMessage = FsMessage::jsonUnserialize($json); diff --git a/pkg/fs/Tests/FsProducerTest.php b/pkg/fs/Tests/FsProducerTest.php index 201cac30a..266854c7b 100644 --- a/pkg/fs/Tests/FsProducerTest.php +++ b/pkg/fs/Tests/FsProducerTest.php @@ -9,9 +9,9 @@ use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrProducer; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Producer; use Makasim\File\TempFile; class FsProducerTest extends \PHPUnit\Framework\TestCase @@ -20,12 +20,7 @@ class FsProducerTest extends \PHPUnit\Framework\TestCase public function testShouldImplementProducerInterface() { - $this->assertClassImplements(PsrProducer::class, FsProducer::class); - } - - public function testCouldBeConstructedWithContextAsFirstArgument() - { - new FsProducer($this->createContextMock()); + $this->assertClassImplements(Producer::class, FsProducer::class); } public function testThrowIfDestinationNotFsOnSend() @@ -63,7 +58,7 @@ public function testShouldCallContextWorkWithFileAndCallbackToItOnSend() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|FsContext + * @return \PHPUnit\Framework\MockObject\MockObject|FsContext */ private function createContextMock() { diff --git a/pkg/fs/Tests/Functional/FsCommonUseCasesTest.php b/pkg/fs/Tests/Functional/FsCommonUseCasesTest.php index ee7e9e4ee..b96091e7f 100644 --- a/pkg/fs/Tests/Functional/FsCommonUseCasesTest.php +++ b/pkg/fs/Tests/Functional/FsCommonUseCasesTest.php @@ -17,14 +17,14 @@ class FsCommonUseCasesTest extends \PHPUnit\Framework\TestCase */ private $fsContext; - public function setUp() + protected function setUp(): void { $this->fsContext = (new FsConnectionFactory(['path' => sys_get_temp_dir()]))->createContext(); new TempFile(sys_get_temp_dir().'/fs_test_queue'); } - public function tearDown() + protected function tearDown(): void { $this->fsContext->close(); } @@ -111,7 +111,7 @@ public function testConsumerReceiveMessageWithZeroTimeout() $topic = $this->fsContext->createTopic('fs_test_queue_exchange'); $consumer = $this->fsContext->createConsumer($topic); - //guard + // guard $this->assertNull($consumer->receive(1000)); $message = $this->fsContext->createMessage(__METHOD__); @@ -139,7 +139,7 @@ public function testPurgeMessagesFromQueue() $producer->send($queue, $message); $producer->send($queue, $message); - $this->fsContext->purge($queue); + $this->fsContext->purgeQueue($queue); $this->assertNull($consumer->receive(1)); } diff --git a/pkg/fs/Tests/Functional/FsConsumerTest.php b/pkg/fs/Tests/Functional/FsConsumerTest.php index d04090d18..3be009b02 100644 --- a/pkg/fs/Tests/Functional/FsConsumerTest.php +++ b/pkg/fs/Tests/Functional/FsConsumerTest.php @@ -15,14 +15,14 @@ class FsConsumerTest extends TestCase */ private $fsContext; - public function setUp() + protected function setUp(): void { $this->fsContext = (new FsConnectionFactory(['path' => sys_get_temp_dir()]))->createContext(); - $this->fsContext->purge($this->fsContext->createQueue('fs_test_queue')); + $this->fsContext->purgeQueue($this->fsContext->createQueue('fs_test_queue')); } - public function tearDown() + protected function tearDown(): void { $this->fsContext->close(); } @@ -76,7 +76,7 @@ public function testShouldNotFailOnSpecificMessageSize() { $context = $this->fsContext; $queue = $context->createQueue('fs_test_queue'); - $context->purge($queue); + $context->purgeQueue($queue); $consumer = $context->createConsumer($queue); $producer = $context->createProducer(); @@ -102,7 +102,7 @@ public function testShouldNotCorruptFrameSize() { $context = $this->fsContext; $queue = $context->createQueue('fs_test_queue'); - $context->purge($queue); + $context->purgeQueue($queue); $consumer = $context->createConsumer($queue); $producer = $context->createProducer(); @@ -134,7 +134,7 @@ public function testShouldThrowExceptionForTheCorruptedQueueFile() { $context = $this->fsContext; $queue = $context->createQueue('fs_test_queue'); - $context->purge($queue); + $context->purgeQueue($queue); $context->workWithFile($queue, 'a+', function (FsDestination $destination, $file) { fwrite($file, '|{"body":"{\"path\":\"\\\/p\\\/r\\\/pr_swoppad_6_4910_red_1.jpg\",\"filters\":null,\"force\":false}","properties":{"enqueue.topic_name":"liip_imagine_resolve_cache"},"headers":{"content_type":"application\/json","message_id":"46fdc345-5d0c-426e-95ac-227c7e657839","timestamp":1505379216,"reply_to":null,"correlation_id":""}} |{"body":"{\"path\":\"\\\/p\\\/r\\\/pr_swoppad_6_4910_black_1.jpg\",\"filters\":null,\"force\":false}","properties":{"enqueue.topic_name":"liip_imagine_resolve_cache"},"headers":{"content_type":"application\/json","message_id":"c4d60e39-3a8c-42df-b536-c8b7c13e006d","timestamp":1505379216,"reply_to":null,"correlation_id":""}} |{"body":"{\"path\":\"\\\/p\\\/r\\\/pr_swoppad_6_4910_green_1.jpg\",\"filters\":null,\"force\":false}","properties":{"enqueue.topic_name":"liip_imagine_resolve_cache"},"headers":{"content_type":"application\/json","message_id":"3a6aa176-c879-4435-9626-c48e0643defa","timestamp":1505379216,"reply_to":null,"correlation_id":""}}'); @@ -155,11 +155,11 @@ public function testShouldThrowExceptionWhenFrameSizeNotDivideExactly() { $context = $this->fsContext; $queue = $context->createQueue('fs_test_queue'); - $context->purge($queue); + $context->purgeQueue($queue); $context->workWithFile($queue, 'a+', function (FsDestination $destination, $file) { $msg = '|{"body":""}'; - //guard + // guard $this->assertNotSame(0, strlen($msg) % 64); fwrite($file, $msg); @@ -180,7 +180,7 @@ public function testShouldUnEscapeDelimiterSymbolsInMessageBody() { $context = $this->fsContext; $queue = $context->createQueue('fs_test_queue'); - $context->purge($queue); + $context->purgeQueue($queue); $message = $this->fsContext->createMessage(' |{"body":"aMessageData","properties":{"enqueue.topic_name":"user_updated"},"headers":{"content_type":"text\/plain","message_id":"90979b6c-d9ff-4b39-9938-878b83a95360","timestamp":1519899428,"reply_to":null,"correlation_id":""}}'); diff --git a/pkg/fs/Tests/Functional/FsConsumptionUseCasesTest.php b/pkg/fs/Tests/Functional/FsConsumptionUseCasesTest.php index 6662f2b16..334a8fe7d 100644 --- a/pkg/fs/Tests/Functional/FsConsumptionUseCasesTest.php +++ b/pkg/fs/Tests/Functional/FsConsumptionUseCasesTest.php @@ -10,9 +10,9 @@ use Enqueue\Consumption\Result; use Enqueue\Fs\FsConnectionFactory; use Enqueue\Fs\FsContext; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; use Makasim\File\TempFile; /** @@ -25,14 +25,14 @@ class FsConsumptionUseCasesTest extends \PHPUnit\Framework\TestCase */ private $fsContext; - public function setUp() + protected function setUp(): void { $this->fsContext = (new FsConnectionFactory(['path' => sys_get_temp_dir()]))->createContext(); new TempFile(sys_get_temp_dir().'/fs_test_queue'); } - public function tearDown() + protected function tearDown(): void { $this->fsContext->close(); } @@ -54,7 +54,7 @@ public function testConsumeOneMessageAndExit() $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); } @@ -85,22 +85,22 @@ public function testConsumeOneMessageAndSendReplyExit() $queueConsumer->bind($replyQueue, $replyProcessor); $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); - $this->assertInstanceOf(PsrMessage::class, $replyProcessor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $replyProcessor->lastProcessedMessage); $this->assertEquals(__METHOD__.'.reply', $replyProcessor->lastProcessedMessage->getBody()); } } -class StubProcessor implements PsrProcessor +class StubProcessor implements Processor { public $result = self::ACK; - /** @var PsrMessage */ + /** @var Message */ public $lastProcessedMessage; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $this->lastProcessedMessage = $message; diff --git a/pkg/fs/Tests/Functional/FsContextTest.php b/pkg/fs/Tests/Functional/FsContextTest.php index bdcfb341a..806b9f56a 100644 --- a/pkg/fs/Tests/Functional/FsContextTest.php +++ b/pkg/fs/Tests/Functional/FsContextTest.php @@ -14,7 +14,7 @@ class FsContextTest extends TestCase */ private $fsContext; - public function tearDown() + protected function tearDown(): void { $fs = new Filesystem(); $fs->remove(sys_get_temp_dir().'/enqueue'); diff --git a/pkg/fs/Tests/Functional/FsProducerTest.php b/pkg/fs/Tests/Functional/FsProducerTest.php index 634e09882..75625cfdd 100644 --- a/pkg/fs/Tests/Functional/FsProducerTest.php +++ b/pkg/fs/Tests/Functional/FsProducerTest.php @@ -14,7 +14,7 @@ class FsProducerTest extends TestCase */ private $fsContext; - public function setUp() + protected function setUp(): void { $this->fsContext = (new FsConnectionFactory(['path' => sys_get_temp_dir()]))->createContext(); @@ -22,7 +22,7 @@ public function setUp() file_put_contents(sys_get_temp_dir().'/fs_test_queue', ''); } - public function tearDown() + protected function tearDown(): void { $this->fsContext->close(); } diff --git a/pkg/fs/Tests/Functional/FsRpcUseCasesTest.php b/pkg/fs/Tests/Functional/FsRpcUseCasesTest.php index 91c40f450..3a0327d7c 100644 --- a/pkg/fs/Tests/Functional/FsRpcUseCasesTest.php +++ b/pkg/fs/Tests/Functional/FsRpcUseCasesTest.php @@ -20,7 +20,7 @@ class FsRpcUseCasesTest extends TestCase */ private $fsContext; - public function setUp() + protected function setUp(): void { $this->fsContext = (new FsConnectionFactory(['path' => sys_get_temp_dir()]))->createContext(); @@ -28,7 +28,7 @@ public function setUp() new TempFile(sys_get_temp_dir().'/fs_reply_queue'); } - public function tearDown() + protected function tearDown(): void { $this->fsContext->close(); } diff --git a/pkg/fs/Tests/LegacyFilesystemLockTest.php b/pkg/fs/Tests/LegacyFilesystemLockTest.php index c797bf41a..519712881 100644 --- a/pkg/fs/Tests/LegacyFilesystemLockTest.php +++ b/pkg/fs/Tests/LegacyFilesystemLockTest.php @@ -6,12 +6,14 @@ use Enqueue\Fs\LegacyFilesystemLock; use Enqueue\Fs\Lock; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use Makasim\File\TempFile; use PHPUnit\Framework\TestCase; class LegacyFilesystemLockTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementLockInterface() { @@ -20,7 +22,7 @@ public function testShouldImplementLockInterface() public function testShouldReleaseAllLocksOnClose() { - $context = new FsContext(sys_get_temp_dir(), 1, 0666); + $context = new FsContext(sys_get_temp_dir(), 1, 0666, 100); $fooQueue = $context->createQueue('foo'); $barQueue = $context->createTopic('bar'); diff --git a/pkg/fs/Tests/Spec/FsMessageTest.php b/pkg/fs/Tests/Spec/FsMessageTest.php index da34ab66a..f1ece8ecb 100644 --- a/pkg/fs/Tests/Spec/FsMessageTest.php +++ b/pkg/fs/Tests/Spec/FsMessageTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Fs\Tests\Spec; use Enqueue\Fs\FsMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class FsMessageTest extends PsrMessageSpec +class FsMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new FsMessage(); diff --git a/pkg/fs/Tests/Spec/FsSendAndReceiveTimeToLiveMessagesFromQueueTest.php b/pkg/fs/Tests/Spec/FsSendAndReceiveTimeToLiveMessagesFromQueueTest.php index 3dd1697c7..d6a76ca94 100644 --- a/pkg/fs/Tests/Spec/FsSendAndReceiveTimeToLiveMessagesFromQueueTest.php +++ b/pkg/fs/Tests/Spec/FsSendAndReceiveTimeToLiveMessagesFromQueueTest.php @@ -9,8 +9,6 @@ class FsSendAndReceiveTimeToLiveMessagesFromQueueTest extends SendAndReceiveTimeToLiveMessagesFromQueueSpec { /** - * {@inheritdoc} - * * @return FsContext */ protected function createContext() diff --git a/pkg/fs/Tests/Symfony/FsTransportFactoryTest.php b/pkg/fs/Tests/Symfony/FsTransportFactoryTest.php deleted file mode 100644 index 1fccf154e..000000000 --- a/pkg/fs/Tests/Symfony/FsTransportFactoryTest.php +++ /dev/null @@ -1,162 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, FsTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new FsTransportFactory(); - - $this->assertEquals('fs', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new FsTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new FsTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'path' => sys_get_temp_dir(), - ]]); - - $this->assertEquals([ - 'path' => sys_get_temp_dir(), - 'pre_fetch_count' => 1, - 'chmod' => 0600, - 'polling_interval' => 100, - ], $config); - } - - public function testShouldAllowAddConfigurationAsString() - { - $transport = new FsTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['fileDSN']); - - $this->assertEquals([ - 'dsn' => 'fileDSN', - 'pre_fetch_count' => 1, - 'chmod' => 0600, - 'polling_interval' => 100, - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new FsTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'path' => sys_get_temp_dir(), - 'pre_fetch_count' => 1, - 'chmod' => 0600, - 'polling_interval' => 100, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(FsConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'path' => sys_get_temp_dir(), - 'pre_fetch_count' => 1, - 'chmod' => 0600, - 'polling_interval' => 100, - ]], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryFromDsnString() - { - $container = new ContainerBuilder(); - - $transport = new FsTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theFileDSN', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(FsConnectionFactory::class, $factory->getClass()); - $this->assertSame(['theFileDSN'], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new FsTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'path' => sys_get_temp_dir(), - 'pre_fetch_count' => 1, - 'chmod' => 0600, - 'polling_interval' => 100, - ]); - - $this->assertEquals('enqueue.transport.fs.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.fs.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.fs.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new FsTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.fs.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(FsDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.fs.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/fs/composer.json b/pkg/fs/composer.json index c2456cac7..4dd2ff806 100644 --- a/pkg/fs/composer.json +++ b/pkg/fs/composer.json @@ -6,20 +6,19 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "queue-interop/queue-interop": "^0.6|^1.0.0-alpha1", - "symfony/filesystem": "^2.8|^3|^4", + "php": "^8.1", + "queue-interop/queue-interop": "^0.8", + "enqueue/dsn": "^0.10", + "symfony/filesystem": "^5.4|^6.0", "makasim/temp-file": "^0.2@stable" }, "require-dev": { - "phpunit/phpunit": "~5.5", - "enqueue/enqueue": "^0.7", - "enqueue/null": "^0.7", - "enqueue/test": "^0.7", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4", - "symfony/phpunit-bridge": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/null": "0.10.x-dev", + "enqueue/test": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" }, "support": { "email": "opensource@forma-pro.com", @@ -37,7 +36,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/fs/phpunit.xml.dist b/pkg/fs/phpunit.xml.dist index 9754bd41f..79088ae1d 100644 --- a/pkg/fs/phpunit.xml.dist +++ b/pkg/fs/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/gearman/.github/workflows/ci.yml b/pkg/gearman/.github/workflows/ci.yml new file mode 100644 index 000000000..28ae81b0f --- /dev/null +++ b/pkg/gearman/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: gearman + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/gearman/.travis.yml b/pkg/gearman/.travis.yml deleted file mode 100644 index b1a480247..000000000 --- a/pkg/gearman/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -git: - depth: 10 - -language: php - -php: - - '5.6' - -cache: - directories: - - $HOME/.composer/cache - -install: - - sudo apt-get update - - sudo apt-get install libgearman-dev -y --no-install-recommends --no-install-suggests - - pecl install gearman - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/gearman/GearmanConnectionFactory.php b/pkg/gearman/GearmanConnectionFactory.php index 77f8dfd96..c938a1025 100644 --- a/pkg/gearman/GearmanConnectionFactory.php +++ b/pkg/gearman/GearmanConnectionFactory.php @@ -1,10 +1,13 @@ config); } - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) + private function parseDsn(string $dsn): array { $dsnConfig = parse_url(http://github.com/$dsn); if (false === $dsnConfig) { @@ -81,10 +77,7 @@ private function parseDsn($dsn) ]; } - /** - * @return array - */ - private function defaultConfig() + private function defaultConfig(): array { return [ 'host' => \GEARMAN_DEFAULT_TCP_HOST, diff --git a/pkg/gearman/GearmanConsumer.php b/pkg/gearman/GearmanConsumer.php index e5afe27c9..e834fe469 100644 --- a/pkg/gearman/GearmanConsumer.php +++ b/pkg/gearman/GearmanConsumer.php @@ -1,11 +1,14 @@ context = $context; $this->destination = $destination; $this->worker = $context->createWorker(); + + $this->worker->addFunction($this->destination->getName(), function (\GearmanJob $job) { + $this->message = GearmanMessage::jsonUnserialize($job->workload()); + }); } /** - * {@inheritdoc} - * * @return GearmanDestination */ - public function getQueue() + public function getQueue(): Queue { return $this->destination; } /** - * {@inheritdoc} - * * @return GearmanMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { set_error_handler(function ($severity, $message, $file, $line) { throw new \ErrorException($message, 0, $severity, $file, $line); @@ -60,49 +62,42 @@ public function receive($timeout = 0) $this->worker->setTimeout($timeout); try { - $message = null; + $this->message = null; - $this->worker->addFunction($this->destination->getName(), function (\GearmanJob $job) use (&$message) { - $message = GearmanMessage::jsonUnserialize($job->workload()); - }); - - while ($this->worker->work()); + $this->worker->work(); } finally { restore_error_handler(); } - return $message; + return $this->message; } /** - * {@inheritdoc} + * @return GearmanMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { return $this->receive(100); } /** - * {@inheritdoc} + * @param GearmanMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { } /** - * {@inheritdoc} + * @param GearmanMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { if ($requeue) { $this->context->createProducer()->send($this->destination, $message); } } - /** - * @return \GearmanWorker - */ - public function getWorker() + public function getWorker(): \GearmanWorker { return $this->worker; } diff --git a/pkg/gearman/GearmanContext.php b/pkg/gearman/GearmanContext.php index a396eb11b..80a93882e 100644 --- a/pkg/gearman/GearmanContext.php +++ b/pkg/gearman/GearmanContext.php @@ -1,12 +1,23 @@ config = $config; } /** - * {@inheritdoc} - * * @return GearmanMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new GearmanMessage($body, $properties, $headers); } /** - * {@inheritdoc} - * * @return GearmanDestination */ - public function createTopic($topicName) + public function createTopic(string $topicName): Topic { return new GearmanDestination($topicName); } /** - * {@inheritdoc} + * @return GearmanDestination */ - public function createQueue($queueName) + public function createQueue(string $queueName): Queue { return new GearmanDestination($queueName); } - /** - * {@inheritdoc} - */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { - throw new \LogicException('Not implemented'); + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); } /** - * {@inheritdoc} - * * @return GearmanProducer */ - public function createProducer() + public function createProducer(): Producer { return new GearmanProducer($this->getClient()); } /** - * {@inheritdoc} - * * @param GearmanDestination $destination * * @return GearmanConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, GearmanDestination::class); @@ -93,7 +90,7 @@ public function createConsumer(PsrDestination $destination) return $consumer; } - public function close() + public function close(): void { $this->getClient()->clearCallbacks(); @@ -102,10 +99,17 @@ public function close() } } - /** - * @return \GearmanClient - */ - public function getClient() + public function createSubscriptionConsumer(): SubscriptionConsumer + { + throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); + } + + public function purgeQueue(Queue $queue): void + { + throw PurgeQueueNotSupportedException::providerDoestNotSupportIt(); + } + + public function getClient(): \GearmanClient { if (false == $this->client) { $this->client = new \GearmanClient(); @@ -115,10 +119,7 @@ public function getClient() return $this->client; } - /** - * @return \GearmanWorker - */ - public function createWorker() + public function createWorker(): \GearmanWorker { $worker = new \GearmanWorker(); $worker->addServer($this->config['host'], $this->config['port']); diff --git a/pkg/gearman/GearmanDestination.php b/pkg/gearman/GearmanDestination.php index 6f779d7f7..c559d6126 100644 --- a/pkg/gearman/GearmanDestination.php +++ b/pkg/gearman/GearmanDestination.php @@ -1,46 +1,36 @@ destinationName = $destinationName; } - /** - * @return string - */ - public function getName() + public function getName(): string { return $this->destinationName; } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { - return $this->getName(); + return $this->destinationName; } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getTopicName(): string { - return $this->getName(); + return $this->destinationName; } } diff --git a/pkg/gearman/GearmanMessage.php b/pkg/gearman/GearmanMessage.php index 1943c8162..ee93a78dd 100644 --- a/pkg/gearman/GearmanMessage.php +++ b/pkg/gearman/GearmanMessage.php @@ -1,11 +1,12 @@ body = $body; $this->properties = $properties; @@ -45,220 +41,109 @@ public function __construct($body = '', array $properties = [], array $headers = $this->redelivered = false; } - /** - * @param string $body - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * @param array $properties - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * @param array $headers - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * @return bool - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * @param bool $redelivered - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = $redelivered; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', (string) $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id'); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', (string) $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id'); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } - /** - * @param string|null $replyTo - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * @return string|null - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } - /** - * @param int $time - */ - public function setTimeToRun($time) - { - $this->setHeader('ttr', $time); - } - - /** - * @return int - */ - public function getTimeToRun() - { - return $this->getHeader('ttr', Pheanstalk::DEFAULT_TTR); - } - - /** - * @param int $priority - */ - public function setPriority($priority) - { - $this->setHeader('priority', $priority); - } - - /** - * @return int - */ - public function getPriority() - { - return $this->getHeader('priority', Pheanstalk::DEFAULT_PRIORITY); - } - - /** - * @param int $delay - */ - public function setDelay($delay) - { - $this->setHeader('delay', $delay); - } - - /** - * @return int - */ - public function getDelay() - { - return $this->getHeader('delay', Pheanstalk::DEFAULT_DELAY); - } - - /** - * {@inheritdoc} - */ - public function jsonSerialize() + public function jsonSerialize(): array { return [ 'body' => $this->getBody(), @@ -267,37 +152,22 @@ public function jsonSerialize() ]; } - /** - * @param string $json - * - * @return self - */ - public static function jsonUnserialize($json) + public static function jsonUnserialize(string $json): self { $data = json_decode($json, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return new self($data['body'], $data['properties'], $data['headers']); } - /** - * @return \GearmanJob - */ - public function getJob() + public function getJob(): ?\GearmanJob { return $this->job; } - /** - * @param \GearmanJob $job - */ - public function setJob(\GearmanJob $job) + public function setJob(?\GearmanJob $job = null): void { $this->job = $job; } diff --git a/pkg/gearman/GearmanProducer.php b/pkg/gearman/GearmanProducer.php index 73a2c073b..870bdcb03 100644 --- a/pkg/gearman/GearmanProducer.php +++ b/pkg/gearman/GearmanProducer.php @@ -1,35 +1,33 @@ client = $client; } /** - * {@inheritdoc} - * * @param GearmanDestination $destination * @param GearmanMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, GearmanDestination::class); InvalidMessageException::assertMessageInstanceOf($message, GearmanMessage::class); @@ -42,62 +40,44 @@ public function send(PsrDestination $destination, PsrMessage $message) } } - /** - * {@inheritdoc} - */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { if (null === $deliveryDelay) { - return; + return $this; } throw new \LogicException('Not implemented'); } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return null; } - /** - * {@inheritdoc} - */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { if (null === $priority) { - return; + return $this; } - throw new \LogicException('Not implemented'); + throw PriorityNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return null; } - /** - * {@inheritdoc} - */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { if (null === $timeToLive) { - return; + return $this; } throw new \LogicException('Not implemented'); } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return null; } diff --git a/pkg/gearman/README.md b/pkg/gearman/README.md index d973ac841..4aedb72d2 100644 --- a/pkg/gearman/README.md +++ b/pkg/gearman/README.md @@ -1,27 +1,36 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Gearman Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/gearman.png?branch=master)](https://travis-ci.org/php-enqueue/gearman) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/gearman/ci.yml?branch=master)](https://github.com/php-enqueue/gearman/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/gearman/d/total.png)](https://packagist.org/packages/enqueue/gearman) [![Latest Stable Version](https://poser.pugx.org/enqueue/gearman/version.png)](https://packagist.org/packages/enqueue/gearman) - -This is an implementation of the queue specification. It allows you to send and consume message from Gearman broker. + +This is an implementation of the queue specification. It allows you to send and consume message from Gearman broker. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/gearman/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/gearman/Tests/GearmanConnectionFactoryConfigTest.php b/pkg/gearman/Tests/GearmanConnectionFactoryConfigTest.php index bb8ab7ad6..8fc7a6b1e 100644 --- a/pkg/gearman/Tests/GearmanConnectionFactoryConfigTest.php +++ b/pkg/gearman/Tests/GearmanConnectionFactoryConfigTest.php @@ -4,6 +4,7 @@ use Enqueue\Gearman\GearmanConnectionFactory; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; /** @@ -12,6 +13,7 @@ class GearmanConnectionFactoryConfigTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; use SkipIfGearmanExtensionIsNotInstalledTrait; public function testThrowNeitherArrayStringNorNullGivenAsConfig() @@ -40,9 +42,6 @@ public function testThrowIfDsnCouldNotBeParsed() /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { diff --git a/pkg/gearman/Tests/GearmanContextTest.php b/pkg/gearman/Tests/GearmanContextTest.php index 98bc5c6d3..8a36ad80b 100644 --- a/pkg/gearman/Tests/GearmanContextTest.php +++ b/pkg/gearman/Tests/GearmanContextTest.php @@ -5,34 +5,31 @@ use Enqueue\Gearman\GearmanContext; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\TemporaryQueueNotSupportedException; use PHPUnit\Framework\TestCase; /** * @group functional + * @group gearman */ class GearmanContextTest extends TestCase { use ClassExtensionTrait; use SkipIfGearmanExtensionIsNotInstalledTrait; - public function testShouldImplementPsrContextInterface() + public function testShouldImplementContextInterface() { - $this->assertClassImplements(PsrContext::class, GearmanContext::class); - } - - public function testCouldBeConstructedWithConnectionConfigAsFirstArgument() - { - new GearmanContext(['host' => 'aHost', 'port' => 'aPort']); + $this->assertClassImplements(Context::class, GearmanContext::class); } public function testThrowNotImplementedOnCreateTemporaryQueue() { $context = new GearmanContext(['host' => 'aHost', 'port' => 'aPort']); - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Not implemented'); + $this->expectException(TemporaryQueueNotSupportedException::class); + $context->createTemporaryQueue(); } diff --git a/pkg/gearman/Tests/GearmanDestinationTest.php b/pkg/gearman/Tests/GearmanDestinationTest.php index 09b29999f..b98f09fee 100644 --- a/pkg/gearman/Tests/GearmanDestinationTest.php +++ b/pkg/gearman/Tests/GearmanDestinationTest.php @@ -4,8 +4,8 @@ use Enqueue\Gearman\GearmanDestination; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrQueue; -use Interop\Queue\PsrTopic; +use Interop\Queue\Queue; +use Interop\Queue\Topic; use PHPUnit\Framework\TestCase; class GearmanDestinationTest extends TestCase @@ -13,14 +13,14 @@ class GearmanDestinationTest extends TestCase use ClassExtensionTrait; use SkipIfGearmanExtensionIsNotInstalledTrait; - public function testShouldImplementPsrQueueInterface() + public function testShouldImplementQueueInterface() { - $this->assertClassImplements(PsrQueue::class, GearmanDestination::class); + $this->assertClassImplements(Queue::class, GearmanDestination::class); } - public function testShouldImplementPsrTopicInterface() + public function testShouldImplementTopicInterface() { - $this->assertClassImplements(PsrTopic::class, GearmanDestination::class); + $this->assertClassImplements(Topic::class, GearmanDestination::class); } public function testShouldAllowGetNameSetInConstructor() diff --git a/pkg/gearman/Tests/GearmanProducerTest.php b/pkg/gearman/Tests/GearmanProducerTest.php index d87b2aaf7..2a7baa4de 100644 --- a/pkg/gearman/Tests/GearmanProducerTest.php +++ b/pkg/gearman/Tests/GearmanProducerTest.php @@ -8,8 +8,9 @@ use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class GearmanProducerTest extends TestCase @@ -17,11 +18,6 @@ class GearmanProducerTest extends TestCase use ClassExtensionTrait; use SkipIfGearmanExtensionIsNotInstalledTrait; - public function testCouldBeConstructedWithGearmanClientAsFirstArgument() - { - new GearmanProducer($this->createGearmanClientMock()); - } - public function testThrowIfDestinationInvalid() { $producer = new GearmanProducer($this->createGearmanClientMock()); @@ -68,7 +64,7 @@ public function testShouldJsonEncodeMessageAndPutToExpectedTube() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|\GearmanClient + * @return MockObject|\GearmanClient */ private function createGearmanClientMock() { diff --git a/pkg/gearman/Tests/SkipIfGearmanExtensionIsNotInstalledTrait.php b/pkg/gearman/Tests/SkipIfGearmanExtensionIsNotInstalledTrait.php index 99419eb15..9c680bb67 100644 --- a/pkg/gearman/Tests/SkipIfGearmanExtensionIsNotInstalledTrait.php +++ b/pkg/gearman/Tests/SkipIfGearmanExtensionIsNotInstalledTrait.php @@ -4,7 +4,7 @@ trait SkipIfGearmanExtensionIsNotInstalledTrait { - public function setUp() + public function setUp(): void { if (false == class_exists(\GearmanClient::class)) { $this->markTestSkipped('The gearman extension is not installed'); diff --git a/pkg/gearman/Tests/Spec/GearmanConnectionFactoryTest.php b/pkg/gearman/Tests/Spec/GearmanConnectionFactoryTest.php index 83cb4306b..05418febc 100644 --- a/pkg/gearman/Tests/Spec/GearmanConnectionFactoryTest.php +++ b/pkg/gearman/Tests/Spec/GearmanConnectionFactoryTest.php @@ -4,15 +4,12 @@ use Enqueue\Gearman\GearmanConnectionFactory; use Enqueue\Gearman\Tests\SkipIfGearmanExtensionIsNotInstalledTrait; -use Interop\Queue\Spec\PsrConnectionFactorySpec; +use Interop\Queue\Spec\ConnectionFactorySpec; -class GearmanConnectionFactoryTest extends PsrConnectionFactorySpec +class GearmanConnectionFactoryTest extends ConnectionFactorySpec { use SkipIfGearmanExtensionIsNotInstalledTrait; - /** - * {@inheritdoc} - */ protected function createConnectionFactory() { return new GearmanConnectionFactory(); diff --git a/pkg/gearman/Tests/Spec/GearmanContextTest.php b/pkg/gearman/Tests/Spec/GearmanContextTest.php index 65cdf118c..d5f879f12 100644 --- a/pkg/gearman/Tests/Spec/GearmanContextTest.php +++ b/pkg/gearman/Tests/Spec/GearmanContextTest.php @@ -3,16 +3,14 @@ namespace Enqueue\Gearman\Tests\Spec; use Enqueue\Gearman\GearmanConnectionFactory; -use Interop\Queue\Spec\PsrContextSpec; +use Interop\Queue\Spec\ContextSpec; /** * @group functional + * @group gearman */ -class GearmanContextTest extends PsrContextSpec +class GearmanContextTest extends ContextSpec { - /** - * {@inheritdoc} - */ protected function createContext() { return (new GearmanConnectionFactory(getenv('GEARMAN_DSN')))->createContext(); diff --git a/pkg/gearman/Tests/Spec/GearmanMessageTest.php b/pkg/gearman/Tests/Spec/GearmanMessageTest.php index faed23e65..37aa71e62 100644 --- a/pkg/gearman/Tests/Spec/GearmanMessageTest.php +++ b/pkg/gearman/Tests/Spec/GearmanMessageTest.php @@ -4,15 +4,12 @@ use Enqueue\Gearman\GearmanMessage; use Enqueue\Gearman\Tests\SkipIfGearmanExtensionIsNotInstalledTrait; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class GearmanMessageTest extends PsrMessageSpec +class GearmanMessageTest extends MessageSpec { use SkipIfGearmanExtensionIsNotInstalledTrait; - /** - * {@inheritdoc} - */ protected function createMessage() { return new GearmanMessage(); diff --git a/pkg/gearman/Tests/Spec/GearmanQueueTest.php b/pkg/gearman/Tests/Spec/GearmanQueueTest.php index 8151c8804..abf6be603 100644 --- a/pkg/gearman/Tests/Spec/GearmanQueueTest.php +++ b/pkg/gearman/Tests/Spec/GearmanQueueTest.php @@ -4,15 +4,12 @@ use Enqueue\Gearman\GearmanDestination; use Enqueue\Gearman\Tests\SkipIfGearmanExtensionIsNotInstalledTrait; -use Interop\Queue\Spec\PsrQueueSpec; +use Interop\Queue\Spec\QueueSpec; -class GearmanQueueTest extends PsrQueueSpec +class GearmanQueueTest extends QueueSpec { use SkipIfGearmanExtensionIsNotInstalledTrait; - /** - * {@inheritdoc} - */ protected function createQueue() { return new GearmanDestination(self::EXPECTED_QUEUE_NAME); diff --git a/pkg/gearman/Tests/Spec/GearmanSendToAndReceiveFromQueueTest.php b/pkg/gearman/Tests/Spec/GearmanSendToAndReceiveFromQueueTest.php index 2affc25b3..10a284987 100644 --- a/pkg/gearman/Tests/Spec/GearmanSendToAndReceiveFromQueueTest.php +++ b/pkg/gearman/Tests/Spec/GearmanSendToAndReceiveFromQueueTest.php @@ -3,18 +3,16 @@ namespace Enqueue\Gearman\Tests\Spec; use Enqueue\Gearman\GearmanConnectionFactory; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrQueue; +use Interop\Queue\Context; +use Interop\Queue\Queue; use Interop\Queue\Spec\SendToAndReceiveFromQueueSpec; /** * @group functional + * @group gearman */ class GearmanSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new GearmanConnectionFactory(getenv('GEARMAN_DSN')); @@ -23,12 +21,11 @@ protected function createContext() } /** - * @param PsrContext $context - * @param string $queueName + * @param string $queueName * - * @return PsrQueue + * @return Queue */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { return $context->createQueue($queueName.time()); } diff --git a/pkg/gearman/Tests/Spec/GearmanSendToAndReceiveNoWaitFromQueueTest.php b/pkg/gearman/Tests/Spec/GearmanSendToAndReceiveNoWaitFromQueueTest.php index 2ec97cdbb..e2164ea7a 100644 --- a/pkg/gearman/Tests/Spec/GearmanSendToAndReceiveNoWaitFromQueueTest.php +++ b/pkg/gearman/Tests/Spec/GearmanSendToAndReceiveNoWaitFromQueueTest.php @@ -3,17 +3,15 @@ namespace Enqueue\Gearman\Tests\Spec; use Enqueue\Gearman\GearmanConnectionFactory; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromQueueSpec; /** * @group functional + * @group gearman */ class GearmanSendToAndReceiveNoWaitFromQueueTest extends SendToAndReceiveNoWaitFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new GearmanConnectionFactory(getenv('GEARMAN_DSN')); @@ -21,10 +19,7 @@ protected function createContext() return $factory->createContext(); } - /** - * {@inheritdoc} - */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { return $context->createQueue($queueName.time()); } diff --git a/pkg/gearman/Tests/Spec/GearmanSendToTopicAndReceiveFromQueueTest.php b/pkg/gearman/Tests/Spec/GearmanSendToTopicAndReceiveFromQueueTest.php index 2b6454765..32463cce1 100644 --- a/pkg/gearman/Tests/Spec/GearmanSendToTopicAndReceiveFromQueueTest.php +++ b/pkg/gearman/Tests/Spec/GearmanSendToTopicAndReceiveFromQueueTest.php @@ -3,24 +3,22 @@ namespace Enqueue\Gearman\Tests\Spec; use Enqueue\Gearman\GearmanConnectionFactory; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveFromQueueSpec; /** * @group functional + * @group gearman */ class GearmanSendToTopicAndReceiveFromQueueTest extends SendToTopicAndReceiveFromQueueSpec { private $time; - public function setUp() + protected function setUp(): void { $this->time = time(); } - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new GearmanConnectionFactory(getenv('GEARMAN_DSN')); @@ -28,18 +26,12 @@ protected function createContext() return $factory->createContext(); } - /** - * {@inheritdoc} - */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { return $context->createQueue($queueName.$this->time); } - /** - * {@inheritdoc} - */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { return $context->createTopic($topicName.$this->time); } diff --git a/pkg/gearman/Tests/Spec/GearmanSendToTopicAndReceiveNoWaitFromQueueTest.php b/pkg/gearman/Tests/Spec/GearmanSendToTopicAndReceiveNoWaitFromQueueTest.php index 3b120afda..993dc3f25 100644 --- a/pkg/gearman/Tests/Spec/GearmanSendToTopicAndReceiveNoWaitFromQueueTest.php +++ b/pkg/gearman/Tests/Spec/GearmanSendToTopicAndReceiveNoWaitFromQueueTest.php @@ -3,24 +3,22 @@ namespace Enqueue\Gearman\Tests\Spec; use Enqueue\Gearman\GearmanConnectionFactory; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveNoWaitFromQueueSpec; /** * @group functional + * @group gearman */ class GearmanSendToTopicAndReceiveNoWaitFromQueueTest extends SendToTopicAndReceiveNoWaitFromQueueSpec { private $time; - public function setUp() + protected function setUp(): void { $this->time = time(); } - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new GearmanConnectionFactory(getenv('GEARMAN_DSN')); @@ -28,18 +26,12 @@ protected function createContext() return $factory->createContext(); } - /** - * {@inheritdoc} - */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { return $context->createQueue($queueName.$this->time); } - /** - * {@inheritdoc} - */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { return $context->createTopic($topicName.$this->time); } diff --git a/pkg/gearman/Tests/Spec/GearmanTopicTest.php b/pkg/gearman/Tests/Spec/GearmanTopicTest.php index 17d933de2..826344e8b 100644 --- a/pkg/gearman/Tests/Spec/GearmanTopicTest.php +++ b/pkg/gearman/Tests/Spec/GearmanTopicTest.php @@ -4,15 +4,12 @@ use Enqueue\Gearman\GearmanDestination; use Enqueue\Gearman\Tests\SkipIfGearmanExtensionIsNotInstalledTrait; -use Interop\Queue\Spec\PsrTopicSpec; +use Interop\Queue\Spec\TopicSpec; -class GearmanTopicTest extends PsrTopicSpec +class GearmanTopicTest extends TopicSpec { use SkipIfGearmanExtensionIsNotInstalledTrait; - /** - * {@inheritdoc} - */ protected function createTopic() { return new GearmanDestination(self::EXPECTED_TOPIC_NAME); diff --git a/pkg/gearman/composer.json b/pkg/gearman/composer.json index 1a1d0331d..e8805849f 100644 --- a/pkg/gearman/composer.json +++ b/pkg/gearman/composer.json @@ -6,18 +6,15 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "ext-gearman": "^1.1", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1" + "php": "^8.1", + "ext-gearman": "^2.0", + "queue-interop/queue-interop": "^0.8" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -32,13 +29,10 @@ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/gearman/phpunit.xml.dist b/pkg/gearman/phpunit.xml.dist index 626570c00..6b750813c 100644 --- a/pkg/gearman/phpunit.xml.dist +++ b/pkg/gearman/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/gps/.github/workflows/ci.yml b/pkg/gps/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/gps/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/gps/.travis.yml b/pkg/gps/.travis.yml deleted file mode 100644 index b9cf57fc9..000000000 --- a/pkg/gps/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/gps/Client/GpsDriver.php b/pkg/gps/Client/GpsDriver.php deleted file mode 100644 index db3c0c7f6..000000000 --- a/pkg/gps/Client/GpsDriver.php +++ /dev/null @@ -1,182 +0,0 @@ -context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $topic = $this->createRouterTopic(); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($topic, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->context->createTopic( - $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName()) - ; - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $log = function ($text, ...$args) use ($logger) { - $logger->debug(sprintf('[GpsDriver] '.$text, ...$args)); - }; - - // setup router - $routerTopic = $this->createRouterTopic(); - $routerQueue = $this->createQueue($this->config->getRouterQueueName()); - - $log('Subscribe router topic to queue: %s -> %s', $routerTopic->getTopicName(), $routerQueue->getQueueName()); - $this->context->subscribe($routerTopic, $routerQueue); - - // setup queues - foreach ($this->queueMetaRegistry->getQueuesMeta() as $meta) { - $topic = $this->context->createTopic($meta->getTransportName()); - $queue = $this->context->createQueue($meta->getTransportName()); - - $log('Subscribe processor topic to queue: %s -> %s', $topic->getTopicName(), $queue->getQueueName()); - $this->context->subscribe($topic, $queue); - } - } - - /** - * {@inheritdoc} - * - * @return GpsQueue - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - return $this->context->createQueue($transportName); - } - - /** - * {@inheritdoc} - * - * @return GpsMessage - */ - public function createTransportMessage(Message $message) - { - $headers = $message->getHeaders(); - $properties = $message->getProperties(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($properties); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - - return $transportMessage; - } - - /** - * @param GpsMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - return $clientMessage; - } - - /** - * @return Config - */ - public function getConfig() - { - return $this->config; - } - - /** - * @return GpsTopic - */ - private function createRouterTopic() - { - $topic = $this->context->createTopic( - $this->config->createTransportRouterTopicName($this->config->getRouterTopicName()) - ); - - return $topic; - } -} diff --git a/pkg/gps/GpsConnectionFactory.php b/pkg/gps/GpsConnectionFactory.php index 6d1090dd0..e45f8cbe3 100644 --- a/pkg/gps/GpsConnectionFactory.php +++ b/pkg/gps/GpsConnectionFactory.php @@ -1,17 +1,26 @@ The full path to your service account credentials.json file retrieved from the Google Developers Console. * 'retries' => Number of retries for a failed request. **Defaults to** `3`. * 'scopes' => Scopes to be used for the request. + * 'emulatorHost' => The endpoint used to emulate communication with GooglePubSub. * 'lazy' => 'the connection will be performed as later as possible, if the option set to true' * ] * @@ -29,28 +39,40 @@ class GpsConnectionFactory implements PsrConnectionFactory * gps: * gps:?projectId=projectName * - * @param array|string|null $config + * or instance of Google\Cloud\PubSub\PubSubClient + * + * @param array|string|PubSubClient|null $config */ public function __construct($config = 'gps:') { - if (empty($config) || 'gps:' === $config) { + if ($config instanceof PubSubClient) { + $this->client = $config; + $this->config = ['lazy' => false] + $this->defaultConfig(); + + return; + } + + if (empty($config)) { $config = []; } elseif (is_string($config)) { $config = $this->parseDsn($config); } elseif (is_array($config)) { + if (array_key_exists('dsn', $config)) { + $config = array_replace_recursive($config, $this->parseDsn($config['dsn'])); + + unset($config['dsn']); + } } else { - throw new \LogicException('The config must be either an array of options, a DSN string or null'); + throw new \LogicException(sprintf('The config must be either an array of options, a DSN string, null or instance of %s', PubSubClient::class)); } $this->config = array_replace($this->defaultConfig(), $config); } /** - * {@inheritdoc} - * * @return GpsContext */ - public function createContext() + public function createContext(): Context { if ($this->config['lazy']) { return new GpsContext(function () { @@ -61,38 +83,38 @@ public function createContext() return new GpsContext($this->establishConnection()); } - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) + private function parseDsn(string $dsn): array { - if (false === strpos($dsn, 'gps:')) { - throw new \LogicException(sprintf('The given DSN "%s" is not supported. Must start with "gps:".', $dsn)); - } + $dsn = Dsn::parseFirst($dsn); - $config = []; - - if ($query = parse_url(http://github.com/$dsn,%20PHP_URL_QUERY)) { - parse_str($query, $config); + if ('gps' !== $dsn->getSchemeProtocol()) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "gps"', $dsn->getSchemeProtocol())); } - return $config; + $emulatorHost = $dsn->getString('emulatorHost'); + $hasEmulator = $emulatorHost ? true : null; + + return array_filter(array_replace($dsn->getQuery(), [ + 'projectId' => $dsn->getString('projectId'), + 'keyFilePath' => $dsn->getString('keyFilePath'), + 'retries' => $dsn->getDecimal('retries'), + 'scopes' => $dsn->getString('scopes'), + 'emulatorHost' => $emulatorHost, + 'hasEmulator' => $hasEmulator, + 'lazy' => $dsn->getBool('lazy'), + ]), function ($value) { return null !== $value; }); } - /** - * @return PubSubClient - */ - private function establishConnection() + private function establishConnection(): PubSubClient { - return new PubSubClient($this->config); + if (false == $this->client) { + $this->client = new PubSubClient($this->config); + } + + return $this->client; } - /** - * @return array - */ - private function defaultConfig() + private function defaultConfig(): array { return [ 'lazy' => true, diff --git a/pkg/gps/GpsConsumer.php b/pkg/gps/GpsConsumer.php index 4dc2997d5..e2a1be272 100644 --- a/pkg/gps/GpsConsumer.php +++ b/pkg/gps/GpsConsumer.php @@ -1,14 +1,17 @@ context = $context; @@ -36,17 +35,17 @@ public function __construct(GpsContext $context, GpsQueue $queue) } /** - * {@inheritdoc} + * @return GpsQueue */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } /** - * {@inheritdoc} + * @return GpsMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { if (0 === $timeout) { while (true) { @@ -60,9 +59,9 @@ public function receive($timeout = 0) } /** - * {@inheritdoc} + * @return GpsMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { $messages = $this->getSubscription()->pull([ 'maxMessages' => 1, @@ -72,12 +71,14 @@ public function receiveNoWait() if ($messages) { return $this->convertMessage(current($messages)); } + + return null; } /** - * {@inheritdoc} + * @param GpsMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { if (false == $message->getNativeMessage()) { throw new \LogicException('Native google pub/sub message required but it is empty'); @@ -87,9 +88,9 @@ public function acknowledge(PsrMessage $message) } /** - * {@inheritdoc} + * @param GpsMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { if (false == $message->getNativeMessage()) { throw new \LogicException('Native google pub/sub message required but it is empty'); @@ -98,10 +99,7 @@ public function reject(PsrMessage $message, $requeue = false) $this->getSubscription()->acknowledge($message->getNativeMessage()); } - /** - * @return Subscription - */ - private function getSubscription() + private function getSubscription(): Subscription { if (null === $this->subscription) { $this->subscription = $this->context->getClient()->subscription($this->queue->getQueueName()); @@ -110,12 +108,7 @@ private function getSubscription() return $this->subscription; } - /** - * @param Message $message - * - * @return GpsMessage - */ - private function convertMessage(Message $message) + private function convertMessage(GoogleMessage $message): GpsMessage { $gpsMessage = GpsMessage::jsonUnserialize($message->data()); $gpsMessage->setNativeMessage($message); @@ -123,12 +116,7 @@ private function convertMessage(Message $message) return $gpsMessage; } - /** - * @param int $timeout - * - * @return GpsMessage|null - */ - private function receiveMessage($timeout) + private function receiveMessage(int $timeout): ?GpsMessage { $timeout /= 1000; @@ -143,5 +131,7 @@ private function receiveMessage($timeout) } } catch (ServiceException $e) { } // timeout + + return null; } } diff --git a/pkg/gps/GpsContext.php b/pkg/gps/GpsContext.php index 8c98fdbdf..27625992a 100644 --- a/pkg/gps/GpsContext.php +++ b/pkg/gps/GpsContext.php @@ -1,14 +1,25 @@ clientFactory = $client; } else { - throw new \InvalidArgumentException(sprintf( - 'The $client argument must be either %s or callable that returns %s once called.', - PubSubClient::class, - PubSubClient::class - )); + throw new \InvalidArgumentException(sprintf('The $client argument must be either %s or callable that returns %s once called.', PubSubClient::class, PubSubClient::class)); } } /** - * {@inheritdoc} + * @return GpsMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new GpsMessage($body, $properties, $headers); } /** - * {@inheritdoc} + * @return GpsTopic */ - public function createTopic($topicName) + public function createTopic(string $topicName): Topic { return new GpsTopic($topicName); } /** - * {@inheritdoc} + * @return GpsQueue */ - public function createQueue($queueName) + public function createQueue(string $queueName): Queue { return new GpsQueue($queueName); } - /** - * {@inheritdoc} - */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { - throw new \LogicException('Not implemented'); + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); } /** - * {@inheritdoc} + * @return GpsProducer */ - public function createProducer() + public function createProducer(): Producer { return new GpsProducer($this); } /** - * {@inheritdoc} + * @param GpsQueue|GpsTopic $destination + * + * @return GpsConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, GpsQueue::class); return new GpsConsumer($this, $destination); } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { } - /** - * @param GpsTopic $topic - */ - public function declareTopic(GpsTopic $topic) + public function createSubscriptionConsumer(): SubscriptionConsumer + { + throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); + } + + public function purgeQueue(Queue $queue): void + { + throw PurgeQueueNotSupportedException::providerDoestNotSupportIt(); + } + + public function declareTopic(GpsTopic $topic): void { try { $this->getClient()->createTopic($topic->getTopicName()); @@ -117,11 +127,7 @@ public function declareTopic(GpsTopic $topic) } } - /** - * @param GpsTopic $topic - * @param GpsQueue $queue - */ - public function subscribe(GpsTopic $topic, GpsQueue $queue) + public function subscribe(GpsTopic $topic, GpsQueue $queue): void { $this->declareTopic($topic); @@ -133,19 +139,12 @@ public function subscribe(GpsTopic $topic, GpsQueue $queue) } } - /** - * @return PubSubClient - */ - public function getClient() + public function getClient(): PubSubClient { if (false == $this->client) { $client = call_user_func($this->clientFactory); if (false == $client instanceof PubSubClient) { - throw new \LogicException(sprintf( - 'The factory must return instance of %s. It returned %s', - PubSubClient::class, - is_object($client) ? get_class($client) : gettype($client) - )); + throw new \LogicException(sprintf('The factory must return instance of %s. It returned %s', PubSubClient::class, is_object($client) ? $client::class : gettype($client))); } $this->client = $client; diff --git a/pkg/gps/GpsMessage.php b/pkg/gps/GpsMessage.php index 7782a4d8e..b7e2bf484 100644 --- a/pkg/gps/GpsMessage.php +++ b/pkg/gps/GpsMessage.php @@ -1,11 +1,13 @@ body = $body; $this->properties = $properties; @@ -46,172 +43,109 @@ public function __construct($body = '', array $properties = [], array $headers = $this->redelivered = false; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * {@inheritdoc} - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * {@inheritdoc} - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * {@inheritdoc} - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = (bool) $redelivered; } - /** - * {@inheritdoc} - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id'); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id'); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } - /** - * {@inheritdoc} - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * {@inheritdoc} - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } - /** - * {@inheritdoc} - */ - public function jsonSerialize() + public function jsonSerialize(): array { return [ 'body' => $this->getBody(), @@ -220,37 +154,22 @@ public function jsonSerialize() ]; } - /** - * @param string $json - * - * @return GpsMessage - */ - public static function jsonUnserialize($json) + public static function jsonUnserialize(string $json): self { $data = json_decode($json, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } - return new self($data['body'], $data['properties'], $data['headers']); + return new self($data['body'] ?? $json, $data['properties'] ?? [], $data['headers'] ?? []); } - /** - * @return Message - */ - public function getNativeMessage() + public function getNativeMessage(): ?GoogleMessage { return $this->nativeMessage; } - /** - * @param Message $message - */ - public function setNativeMessage(Message $message) + public function setNativeMessage(?GoogleMessage $message = null): void { $this->nativeMessage = $message; } diff --git a/pkg/gps/GpsProducer.php b/pkg/gps/GpsProducer.php index e19f227a8..7e307636f 100644 --- a/pkg/gps/GpsProducer.php +++ b/pkg/gps/GpsProducer.php @@ -1,101 +1,91 @@ context = $context; } /** - * {@inheritdoc} + * @param GpsTopic $destination + * @param GpsMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, GpsTopic::class); InvalidMessageException::assertMessageInstanceOf($message, GpsMessage::class); /** @var Topic $topic */ $topic = $this->context->getClient()->topic($destination->getTopicName()); - $topic->publish([ - 'data' => json_encode($message), - ]); + + $params = ['data' => json_encode($message)]; + + if (count($message->getHeaders()) > 0) { + $params['attributes'] = $message->getHeaders(); + } + + $topic->publish($params); } - /** - * {@inheritdoc} - */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { if (null === $deliveryDelay) { - return; + return $this; } throw DeliveryDelayNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { + return null; } - /** - * {@inheritdoc} - */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { if (null === $priority) { - return; + return $this; } throw PriorityNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { + return null; } - /** - * {@inheritdoc} - */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { if (null === $timeToLive) { - return; + return $this; } throw TimeToLiveNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { + return null; } } diff --git a/pkg/gps/GpsQueue.php b/pkg/gps/GpsQueue.php index c1f84b00e..1c4f1c785 100644 --- a/pkg/gps/GpsQueue.php +++ b/pkg/gps/GpsQueue.php @@ -1,28 +1,24 @@ name = $name; } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { return $this->name; } diff --git a/pkg/gps/GpsTopic.php b/pkg/gps/GpsTopic.php index a3617f44a..73646c46a 100644 --- a/pkg/gps/GpsTopic.php +++ b/pkg/gps/GpsTopic.php @@ -1,28 +1,24 @@ name = $name; } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getTopicName(): string { return $this->name; } diff --git a/pkg/gps/README.md b/pkg/gps/README.md index 5f03e8e3f..4f2a0e6ac 100644 --- a/pkg/gps/README.md +++ b/pkg/gps/README.md @@ -1,19 +1,28 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Google Pub/Sub Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/gps.png?branch=master)](https://travis-ci.org/php-enqueue/gps) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/gps/ci.yml?branch=master)](https://github.com/php-enqueue/gps/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/gps/d/total.png)](https://packagist.org/packages/enqueue/gps) [![Latest Stable Version](https://poser.pugx.org/enqueue/gps/version.png)](https://packagist.org/packages/enqueue/gps) - -This is an implementation of PSR specification. It allows you to send and consume message through Google Pub/Sub library. + +This is an implementation of Queue Interop specification. It allows you to send and consume message through Google Pub/Sub library. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/gps/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/gps/Symfony/GpsTransportFactory.php b/pkg/gps/Symfony/GpsTransportFactory.php deleted file mode 100644 index 36c15e39b..000000000 --- a/pkg/gps/Symfony/GpsTransportFactory.php +++ /dev/null @@ -1,132 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifString() - ->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('dsn') - ->info('The connection to Google Pub/Sub broker set as a string. Other parameters are ignored if set') - ->end() - ->scalarNode('projectId') - ->info('The project ID from the Google Developer\'s Console.') - ->end() - ->scalarNode('keyFilePath') - ->info('The full path to your service account credentials.json file retrieved from the Google Developers Console.') - ->end() - ->integerNode('retries') - ->defaultValue(3) - ->info('Number of retries for a failed request.') - ->end() - ->arrayNode('scopes') - ->prototype('scalar')->end() - ->info('Scopes to be used for the request.') - ->end() - ->booleanNode('lazy') - ->defaultTrue() - ->info('The connection will be performed as later as possible, if the option set to true') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - foreach ($config as $key => $value) { - if (null === $value) { - unset($config[$key]); - } elseif (is_array($value) && empty($value)) { - unset($config[$key]); - } - } - - $factory = new Definition(GpsConnectionFactory::class); - $factory->setArguments(isset($config['dsn']) ? [$config['dsn']] : [$config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(GpsContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(GpsDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/gps/Tests/Client/GpsDriverTest.php b/pkg/gps/Tests/Client/GpsDriverTest.php deleted file mode 100644 index e5c76300e..000000000 --- a/pkg/gps/Tests/Client/GpsDriverTest.php +++ /dev/null @@ -1,385 +0,0 @@ -assertClassImplements(DriverInterface::class, GpsDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new GpsDriver( - $this->createGpsContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = $this->createDummyConfig(); - - $driver = new GpsDriver($this->createGpsContextMock(), $config, $this->createDummyQueueMetaRegistry()); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new GpsQueue('aName'); - - $context = $this->createGpsContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new GpsDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new GpsQueue('aName'); - - $context = $this->createGpsContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new GpsDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new GpsMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new GpsDriver( - $this->createGpsContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertNull($clientMessage->getExpire()); - $this->assertNull($clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createGpsContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new GpsMessage()) - ; - - $driver = new GpsDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(GpsMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testShouldSendMessageToRouter() - { - $topic = new GpsTopic(''); - $transportMessage = new GpsMessage(); - - $producer = $this->createGpsProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createGpsContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new GpsDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new GpsDriver( - $this->createGpsContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $topic = new GpsTopic(''); - $transportMessage = new GpsMessage(); - - $producer = $this->createGpsProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createGpsContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new GpsDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new GpsDriver( - $this->createGpsContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new GpsDriver( - $this->createGpsContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBroker() - { - $routerTopic = new GpsTopic(''); - $routerQueue = new GpsQueue(''); - - $processorTopic = new GpsTopic(''); - $processorQueue = new GpsQueue(''); - - $context = $this->createGpsContextMock(); - // setup router - $context - ->expects($this->at(0)) - ->method('createTopic') - ->willReturn($routerTopic) - ; - $context - ->expects($this->at(1)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(2)) - ->method('subscribe') - ->with($this->identicalTo($routerTopic), $this->identicalTo($routerQueue)) - ; - // setup processor queue - $context - ->expects($this->at(3)) - ->method('createTopic') - ->willReturn($processorTopic) - ; - $context - ->expects($this->at(4)) - ->method('createQueue') - ->willReturn($processorQueue) - ; - $context - ->expects($this->at(5)) - ->method('subscribe') - ->with($this->identicalTo($processorTopic), $this->identicalTo($processorQueue)) - ; - - $meta = new QueueMetaRegistry($this->createDummyConfig(), [ - 'default' => [], - ]); - - $driver = new GpsDriver( - $context, - $this->createDummyConfig(), - $meta - ); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|GpsContext - */ - private function createGpsContextMock() - { - return $this->createMock(GpsContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|GpsProducer - */ - private function createGpsProducerMock() - { - return $this->createMock(GpsProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/gps/Tests/GpsConnectionFactoryConfigTest.php b/pkg/gps/Tests/GpsConnectionFactoryConfigTest.php new file mode 100644 index 000000000..474149497 --- /dev/null +++ b/pkg/gps/Tests/GpsConnectionFactoryConfigTest.php @@ -0,0 +1,107 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage('The config must be either an array of options, a DSN string, null or instance of Google\Cloud\PubSub\PubSubClient'); + + new GpsConnectionFactory(new \stdClass()); + } + + public function testThrowIfSchemeIsNotAmqp() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The given scheme protocol "http" is not supported. It must be "gps"'); + + new GpsConnectionFactory('http://example.com/'); + } + + public function testThrowIfDsnCouldNotBeParsed() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The DSN is invalid.'); + + new GpsConnectionFactory('foo'); + } + + /** + * @dataProvider provideConfigs + */ + public function testShouldParseConfigurationAsExpected($config, $expectedConfig) + { + $factory = new GpsConnectionFactory($config); + + $this->assertAttributeEquals($expectedConfig, 'config', $factory); + } + + public static function provideConfigs() + { + yield [ + null, + [ + 'lazy' => true, + ], + ]; + + yield [ + 'gps:', + [ + 'lazy' => true, + ], + ]; + + yield [ + [], + [ + 'lazy' => true, + ], + ]; + + yield [ + 'gps:?foo=fooVal&projectId=mqdev&emulatorHost=http%3A%2F%2Fgoogle-pubsub%3A8085', + [ + 'foo' => 'fooVal', + 'projectId' => 'mqdev', + 'emulatorHost' => 'http://google-pubsub:8085/', + 'hasEmulator' => true, + 'lazy' => true, + ], + ]; + + yield [ + ['dsn' => 'gps:?foo=fooVal&projectId=mqdev&emulatorHost=http%3A%2F%2Fgoogle-pubsub%3A8085'], + [ + 'foo' => 'fooVal', + 'projectId' => 'mqdev', + 'emulatorHost' => 'http://google-pubsub:8085/', + 'hasEmulator' => true, + 'lazy' => true, + ], + ]; + + yield [ + ['foo' => 'fooVal', 'projectId' => 'mqdev', 'emulatorHost' => 'http://fgoogle-pubsub:8085/', 'lazy' => false], + [ + 'foo' => 'fooVal', + 'projectId' => 'mqdev', + 'emulatorHost' => 'http://fgoogle-pubsub:8085/', + 'lazy' => false, + ], + ]; + } +} diff --git a/pkg/gps/Tests/GpsConsumerTest.php b/pkg/gps/Tests/GpsConsumerTest.php index 24bed7e3b..0b4b925ce 100644 --- a/pkg/gps/Tests/GpsConsumerTest.php +++ b/pkg/gps/Tests/GpsConsumerTest.php @@ -180,7 +180,7 @@ public function testShouldReceiveMessage() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|GpsContext + * @return \PHPUnit\Framework\MockObject\MockObject|GpsContext */ private function createContextMock() { @@ -188,7 +188,7 @@ private function createContextMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PubSubClient + * @return \PHPUnit\Framework\MockObject\MockObject|PubSubClient */ private function createPubSubClientMock() { @@ -196,7 +196,7 @@ private function createPubSubClientMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Subscription + * @return \PHPUnit\Framework\MockObject\MockObject|Subscription */ private function createSubscriptionMock() { diff --git a/pkg/gps/Tests/GpsContextTest.php b/pkg/gps/Tests/GpsContextTest.php index 0ca5b437b..658e2659e 100644 --- a/pkg/gps/Tests/GpsContextTest.php +++ b/pkg/gps/Tests/GpsContextTest.php @@ -7,7 +7,7 @@ use Enqueue\Gps\GpsTopic; use Google\Cloud\Core\Exception\ConflictException; use Google\Cloud\PubSub\PubSubClient; -use Interop\Queue\InvalidDestinationException; +use Interop\Queue\Exception\InvalidDestinationException; use PHPUnit\Framework\TestCase; class GpsContextTest extends TestCase @@ -87,7 +87,7 @@ public function testCreateConsumerShouldThrowExceptionIfInvalidDestination() } /** - * @return PubSubClient|\PHPUnit_Framework_MockObject_MockObject|PubSubClient + * @return PubSubClient|\PHPUnit\Framework\MockObject\MockObject|PubSubClient */ private function createPubSubClientMock() { diff --git a/pkg/gps/Tests/GpsMessageTest.php b/pkg/gps/Tests/GpsMessageTest.php index d84cd99d9..8182333cc 100644 --- a/pkg/gps/Tests/GpsMessageTest.php +++ b/pkg/gps/Tests/GpsMessageTest.php @@ -29,7 +29,7 @@ public function testCouldBeUnserializedFromJson() $json = json_encode($message); - //guard + // guard $this->assertNotEmpty($json); $unserializedMessage = GpsMessage::jsonUnserialize($json); @@ -38,6 +38,31 @@ public function testCouldBeUnserializedFromJson() $this->assertEquals($message, $unserializedMessage); } + public function testMessageEntityCouldBeUnserializedFromJson() + { + $json = '{"body":"theBody","properties":{"thePropFoo":"thePropFooVal"},"headers":{"theHeaderFoo":"theHeaderFooVal"}}'; + + $unserializedMessage = GpsMessage::jsonUnserialize($json); + + $this->assertInstanceOf(GpsMessage::class, $unserializedMessage); + $decoded = json_decode($json, true); + $this->assertEquals($decoded['body'], $unserializedMessage->getBody()); + $this->assertEquals($decoded['properties'], $unserializedMessage->getProperties()); + $this->assertEquals($decoded['headers'], $unserializedMessage->getHeaders()); + } + + public function testMessagePayloadCouldBeUnserializedFromJson() + { + $json = '{"theBodyPropFoo":"theBodyPropVal"}'; + + $unserializedMessage = GpsMessage::jsonUnserialize($json); + + $this->assertInstanceOf(GpsMessage::class, $unserializedMessage); + $this->assertEquals($json, $unserializedMessage->getBody()); + $this->assertEquals([], $unserializedMessage->getProperties()); + $this->assertEquals([], $unserializedMessage->getHeaders()); + } + public function testThrowIfMalformedJsonGivenOnUnsterilizedFromJson() { $this->expectException(\InvalidArgumentException::class); diff --git a/pkg/gps/Tests/GpsProducerTest.php b/pkg/gps/Tests/GpsProducerTest.php index 117b93298..3079d3c7c 100644 --- a/pkg/gps/Tests/GpsProducerTest.php +++ b/pkg/gps/Tests/GpsProducerTest.php @@ -9,7 +9,7 @@ use Enqueue\Gps\GpsTopic; use Google\Cloud\PubSub\PubSubClient; use Google\Cloud\PubSub\Topic; -use Interop\Queue\InvalidDestinationException; +use Interop\Queue\Exception\InvalidDestinationException; use PHPUnit\Framework\TestCase; class GpsProducerTest extends TestCase @@ -33,7 +33,39 @@ public function testShouldSendMessage() $gtopic ->expects($this->once()) ->method('publish') - ->with($this->identicalTo(['data' => '{"body":"","properties":[],"headers":[]}'])) + ->with($this->identicalTo([ + 'data' => '{"body":"","properties":[],"headers":[]}', + ])); + + $client = $this->createPubSubClientMock(); + $client + ->expects($this->once()) + ->method('topic') + ->with('topic-name') + ->willReturn($gtopic) + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getClient') + ->willReturn($client) + ; + + $producer = new GpsProducer($context); + $producer->send($topic, $message); + } + + public function testShouldSendMessageWithHeaders() + { + $topic = new GpsTopic('topic-name'); + $message = new GpsMessage('', [], ['key1' => 'value1']); + + $gtopic = $this->createGTopicMock(); + $gtopic + ->expects($this->once()) + ->method('publish') + ->with($this->identicalTo(['data' => '{"body":"","properties":[],"headers":{"key1":"value1"}}', 'attributes' => ['key1' => 'value1']])) ; $client = $this->createPubSubClientMock(); @@ -56,7 +88,7 @@ public function testShouldSendMessage() } /** - * @return GpsContext|\PHPUnit_Framework_MockObject_MockObject|GpsContext + * @return GpsContext|\PHPUnit\Framework\MockObject\MockObject|GpsContext */ private function createContextMock() { @@ -64,7 +96,7 @@ private function createContextMock() } /** - * @return PubSubClient|\PHPUnit_Framework_MockObject_MockObject|PubSubClient + * @return PubSubClient|\PHPUnit\Framework\MockObject\MockObject|PubSubClient */ private function createPubSubClientMock() { @@ -72,7 +104,7 @@ private function createPubSubClientMock() } /** - * @return Topic|\PHPUnit_Framework_MockObject_MockObject|Topic + * @return Topic|\PHPUnit\Framework\MockObject\MockObject|Topic */ private function createGTopicMock() { diff --git a/pkg/gps/Tests/Spec/GpsConnectionFactoryTest.php b/pkg/gps/Tests/Spec/GpsConnectionFactoryTest.php index 5a12d7a02..3166d24e7 100644 --- a/pkg/gps/Tests/Spec/GpsConnectionFactoryTest.php +++ b/pkg/gps/Tests/Spec/GpsConnectionFactoryTest.php @@ -3,9 +3,9 @@ namespace Enqueue\Gps\Tests\Spec; use Enqueue\Gps\GpsConnectionFactory; -use Interop\Queue\Spec\PsrConnectionFactorySpec; +use Interop\Queue\Spec\ConnectionFactorySpec; -class GpsConnectionFactoryTest extends PsrConnectionFactorySpec +class GpsConnectionFactoryTest extends ConnectionFactorySpec { protected function createConnectionFactory() { diff --git a/pkg/gps/Tests/Spec/GpsContextTest.php b/pkg/gps/Tests/Spec/GpsContextTest.php index fe31877b6..5aaacaeb0 100644 --- a/pkg/gps/Tests/Spec/GpsContextTest.php +++ b/pkg/gps/Tests/Spec/GpsContextTest.php @@ -4,9 +4,9 @@ use Enqueue\Gps\GpsContext; use Google\Cloud\PubSub\PubSubClient; -use Interop\Queue\Spec\PsrContextSpec; +use Interop\Queue\Spec\ContextSpec; -class GpsContextTest extends PsrContextSpec +class GpsContextTest extends ContextSpec { protected function createContext() { diff --git a/pkg/gps/Tests/Spec/GpsMessageTest.php b/pkg/gps/Tests/Spec/GpsMessageTest.php index 107240923..7d7fe7191 100644 --- a/pkg/gps/Tests/Spec/GpsMessageTest.php +++ b/pkg/gps/Tests/Spec/GpsMessageTest.php @@ -3,9 +3,9 @@ namespace Enqueue\Gps\Tests\Spec; use Enqueue\Gps\GpsMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class GpsMessageTest extends PsrMessageSpec +class GpsMessageTest extends MessageSpec { protected function createMessage() { diff --git a/pkg/gps/Tests/Spec/GpsProducerTest.php b/pkg/gps/Tests/Spec/GpsProducerTest.php index a496c8205..7f973e5d3 100644 --- a/pkg/gps/Tests/Spec/GpsProducerTest.php +++ b/pkg/gps/Tests/Spec/GpsProducerTest.php @@ -4,9 +4,9 @@ use Enqueue\Gps\GpsContext; use Enqueue\Gps\GpsProducer; -use Interop\Queue\Spec\PsrProducerSpec; +use Interop\Queue\Spec\ProducerSpec; -class GpsProducerTest extends PsrProducerSpec +class GpsProducerTest extends ProducerSpec { protected function createProducer() { diff --git a/pkg/gps/Tests/Spec/GpsQueueTest.php b/pkg/gps/Tests/Spec/GpsQueueTest.php index c2eccb4fe..f4e401f6d 100644 --- a/pkg/gps/Tests/Spec/GpsQueueTest.php +++ b/pkg/gps/Tests/Spec/GpsQueueTest.php @@ -3,9 +3,9 @@ namespace Enqueue\Gps\Tests\Spec; use Enqueue\Gps\GpsQueue; -use Interop\Queue\Spec\PsrQueueSpec; +use Interop\Queue\Spec\QueueSpec; -class GpsQueueTest extends PsrQueueSpec +class GpsQueueTest extends QueueSpec { protected function createQueue() { diff --git a/pkg/gps/Tests/Spec/GpsSendToTopicAndReceiveFromQueueTest.php b/pkg/gps/Tests/Spec/GpsSendToTopicAndReceiveFromQueueTest.php index b702c5dea..4bcaf357b 100644 --- a/pkg/gps/Tests/Spec/GpsSendToTopicAndReceiveFromQueueTest.php +++ b/pkg/gps/Tests/Spec/GpsSendToTopicAndReceiveFromQueueTest.php @@ -2,9 +2,9 @@ namespace Enqueue\Gps\Tests\Spec; -use Enqueue\Gps\GpsConnectionFactory; use Enqueue\Gps\GpsContext; -use Interop\Queue\PsrContext; +use Enqueue\Test\GpsExtension; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveNoWaitFromQueueSpec; /** @@ -12,18 +12,19 @@ */ class GpsSendToTopicAndReceiveFromQueueTest extends SendToTopicAndReceiveNoWaitFromQueueSpec { + use GpsExtension; + private $topic; protected function createContext() { - return (new GpsConnectionFactory())->createContext(); + return $this->buildGpsContext(); } /** * @param GpsContext $context - * @param mixed $queueName */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = parent::createQueue($context, $queueName); @@ -32,7 +33,7 @@ protected function createQueue(PsrContext $context, $queueName) return $queue; } - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { return $this->topic = parent::createTopic($context, $topicName); } diff --git a/pkg/gps/Tests/Spec/GpsSendToTopicAndReceiveNoWaitFromQueueTest.php b/pkg/gps/Tests/Spec/GpsSendToTopicAndReceiveNoWaitFromQueueTest.php index 08c42a79b..3a96bb533 100644 --- a/pkg/gps/Tests/Spec/GpsSendToTopicAndReceiveNoWaitFromQueueTest.php +++ b/pkg/gps/Tests/Spec/GpsSendToTopicAndReceiveNoWaitFromQueueTest.php @@ -2,9 +2,9 @@ namespace Enqueue\Gps\Tests\Spec; -use Enqueue\Gps\GpsConnectionFactory; use Enqueue\Gps\GpsContext; -use Interop\Queue\PsrContext; +use Enqueue\Test\GpsExtension; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveNoWaitFromQueueSpec; /** @@ -12,18 +12,19 @@ */ class GpsSendToTopicAndReceiveNoWaitFromQueueTest extends SendToTopicAndReceiveNoWaitFromQueueSpec { + use GpsExtension; + private $topic; protected function createContext() { - return (new GpsConnectionFactory())->createContext(); + return $this->buildGpsContext(); } /** * @param GpsContext $context - * @param mixed $queueName */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { $queue = parent::createQueue($context, $queueName); @@ -32,7 +33,7 @@ protected function createQueue(PsrContext $context, $queueName) return $queue; } - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { return $this->topic = parent::createTopic($context, $topicName); } diff --git a/pkg/gps/Tests/Spec/GpsTopicTest.php b/pkg/gps/Tests/Spec/GpsTopicTest.php index cd08ae580..3cbb95ad0 100644 --- a/pkg/gps/Tests/Spec/GpsTopicTest.php +++ b/pkg/gps/Tests/Spec/GpsTopicTest.php @@ -3,9 +3,9 @@ namespace Enqueue\Gps\Tests\Spec; use Enqueue\Gps\GpsTopic; -use Interop\Queue\Spec\PsrTopicSpec; +use Interop\Queue\Spec\TopicSpec; -class GpsTopicTest extends PsrTopicSpec +class GpsTopicTest extends TopicSpec { protected function createTopic() { diff --git a/pkg/gps/Tests/Symfony/GpsTransportFactoryTest.php b/pkg/gps/Tests/Symfony/GpsTransportFactoryTest.php deleted file mode 100644 index f9f7c7f89..000000000 --- a/pkg/gps/Tests/Symfony/GpsTransportFactoryTest.php +++ /dev/null @@ -1,157 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, GpsTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new GpsTransportFactory(); - - $this->assertEquals('gps', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new GpsTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new GpsTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); - - $this->assertEquals([ - 'retries' => 3, - 'scopes' => [], - 'lazy' => true, - ], $config); - } - - public function testShouldAllowAddConfigurationAsString() - { - $transport = new GpsTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['gpsDSN']); - - $this->assertEquals([ - 'dsn' => 'gpsDSN', - 'lazy' => true, - 'retries' => 3, - 'scopes' => [], - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new GpsTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'projectId' => null, - 'lazy' => false, - 'retries' => 3, - 'scopes' => [], - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(GpsConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'lazy' => false, - 'retries' => 3, - ]], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryFromDsnString() - { - $container = new ContainerBuilder(); - - $transport = new GpsTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theConnectionDSN', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(GpsConnectionFactory::class, $factory->getClass()); - $this->assertSame(['theConnectionDSN'], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new GpsTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'projectId' => null, - 'lazy' => false, - 'retries' => 3, - 'scopes' => [], - ]); - - $this->assertEquals('enqueue.transport.gps.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.gps.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.gps.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new GpsTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.gps.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(GpsDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.gps.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/gps/composer.json b/pkg/gps/composer.json index 798fc8656..e7654be8d 100644 --- a/pkg/gps/composer.json +++ b/pkg/gps/composer.json @@ -6,17 +6,15 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1", - "google/cloud-pubsub": "^0.6.1|^1.0" + "php": "^8.1", + "queue-interop/queue-interop": "^0.8", + "google/cloud-pubsub": "^1.4.3", + "enqueue/dsn": "^0.10" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -31,13 +29,10 @@ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/gps/phpunit.xml.dist b/pkg/gps/phpunit.xml.dist index 57e46d2f2..77f02571a 100644 --- a/pkg/gps/phpunit.xml.dist +++ b/pkg/gps/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/job-queue/.github/workflows/ci.yml b/pkg/job-queue/.github/workflows/ci.yml new file mode 100644 index 000000000..28a9a9c02 --- /dev/null +++ b/pkg/job-queue/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-dist" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/job-queue/.travis.yml b/pkg/job-queue/.travis.yml deleted file mode 100644 index a478056d5..000000000 --- a/pkg/job-queue/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - travis_retry composer install --prefer-dist - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/job-queue/CalculateRootJobStatusProcessor.php b/pkg/job-queue/CalculateRootJobStatusProcessor.php index 8ccac2cae..96e7db2e8 100644 --- a/pkg/job-queue/CalculateRootJobStatusProcessor.php +++ b/pkg/job-queue/CalculateRootJobStatusProcessor.php @@ -2,17 +2,17 @@ namespace Enqueue\JobQueue; +use Enqueue\Client\CommandSubscriberInterface; use Enqueue\Client\ProducerInterface; -use Enqueue\Client\TopicSubscriberInterface; use Enqueue\Consumption\Result; use Enqueue\JobQueue\Doctrine\JobStorage; use Enqueue\Util\JSON; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; use Psr\Log\LoggerInterface; -class CalculateRootJobStatusProcessor implements PsrProcessor, TopicSubscriberInterface +class CalculateRootJobStatusProcessor implements Processor, CommandSubscriberInterface { /** * @var JobStorage @@ -34,17 +34,11 @@ class CalculateRootJobStatusProcessor implements PsrProcessor, TopicSubscriberIn */ private $logger; - /** - * @param JobStorage $jobStorage - * @param CalculateRootJobStatusService $calculateRootJobStatusCase - * @param ProducerInterface $producer - * @param LoggerInterface $logger - */ public function __construct( JobStorage $jobStorage, CalculateRootJobStatusService $calculateRootJobStatusCase, ProducerInterface $producer, - LoggerInterface $logger + LoggerInterface $logger, ) { $this->jobStorage = $jobStorage; $this->calculateRootJobStatusService = $calculateRootJobStatusCase; @@ -52,10 +46,7 @@ public function __construct( $this->logger = $logger; } - /** - * {@inheritdoc} - */ - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $data = JSON::decode($message->getBody()); @@ -83,11 +74,8 @@ public function process(PsrMessage $message, PsrContext $context) return Result::ACK; } - /** - * {@inheritdoc} - */ - public static function getSubscribedTopics() + public static function getSubscribedCommand() { - return [Topics::CALCULATE_ROOT_JOB_STATUS]; + return Commands::CALCULATE_ROOT_JOB_STATUS; } } diff --git a/pkg/job-queue/CalculateRootJobStatusService.php b/pkg/job-queue/CalculateRootJobStatusService.php index af4631e2a..41dd350b9 100644 --- a/pkg/job-queue/CalculateRootJobStatusService.php +++ b/pkg/job-queue/CalculateRootJobStatusService.php @@ -12,17 +12,12 @@ class CalculateRootJobStatusService */ private $jobStorage; - /** - * @param JobStorage $jobStorage - */ public function __construct(JobStorage $jobStorage) { $this->jobStorage = $jobStorage; } /** - * @param Job $job - * * @return bool true if root job was stopped */ public function calculate(Job $job) @@ -74,6 +69,7 @@ protected function calculateRootJobStatus(array $jobs) $success = 0; foreach ($jobs as $job) { + $this->jobStorage->refreshJobEntity($job); switch ($job->getStatus()) { case Job::STATUS_NEW: $new++; @@ -91,11 +87,7 @@ protected function calculateRootJobStatus(array $jobs) $success++; break; default: - throw new \LogicException(sprintf( - 'Got unsupported job status: id: "%s" status: "%s"', - $job->getId(), - $job->getStatus() - )); + throw new \LogicException(sprintf('Got unsupported job status: id: "%s" status: "%s"', $job->getId(), $job->getStatus())); } } diff --git a/pkg/job-queue/Commands.php b/pkg/job-queue/Commands.php new file mode 100644 index 000000000..ae744c991 --- /dev/null +++ b/pkg/job-queue/Commands.php @@ -0,0 +1,8 @@ +job = $job; diff --git a/pkg/job-queue/DependentJobProcessor.php b/pkg/job-queue/DependentJobProcessor.php index fb4ab1d87..ac3055b5f 100644 --- a/pkg/job-queue/DependentJobProcessor.php +++ b/pkg/job-queue/DependentJobProcessor.php @@ -2,18 +2,18 @@ namespace Enqueue\JobQueue; -use Enqueue\Client\Message; +use Enqueue\Client\Message as ClientMessage; use Enqueue\Client\ProducerInterface; use Enqueue\Client\TopicSubscriberInterface; use Enqueue\Consumption\Result; use Enqueue\JobQueue\Doctrine\JobStorage; use Enqueue\Util\JSON; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; use Psr\Log\LoggerInterface; -class DependentJobProcessor implements PsrProcessor, TopicSubscriberInterface +class DependentJobProcessor implements Processor, TopicSubscriberInterface { /** * @var JobStorage @@ -30,11 +30,6 @@ class DependentJobProcessor implements PsrProcessor, TopicSubscriberInterface */ private $logger; - /** - * @param JobStorage $jobStorage - * @param ProducerInterface $producer - * @param LoggerInterface $logger - */ public function __construct(JobStorage $jobStorage, ProducerInterface $producer, LoggerInterface $logger) { $this->jobStorage = $jobStorage; @@ -42,10 +37,7 @@ public function __construct(JobStorage $jobStorage, ProducerInterface $producer, $this->logger = $logger; } - /** - * {@inheritdoc} - */ - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $data = JSON::decode($message->getBody()); @@ -98,7 +90,7 @@ public function process(PsrMessage $message, PsrContext $context) } foreach ($dependentJobs as $dependentJob) { - $message = new Message(); + $message = new ClientMessage(); $message->setBody($dependentJob['message']); if (isset($dependentJob['priority'])) { @@ -111,11 +103,8 @@ public function process(PsrMessage $message, PsrContext $context) return Result::ACK; } - /** - * {@inheritdoc} - */ public static function getSubscribedTopics() { - return [Topics::ROOT_JOB_STOPPED]; + return Topics::ROOT_JOB_STOPPED; } } diff --git a/pkg/job-queue/DependentJobService.php b/pkg/job-queue/DependentJobService.php index b6066d669..9ab500ba8 100644 --- a/pkg/job-queue/DependentJobService.php +++ b/pkg/job-queue/DependentJobService.php @@ -20,8 +20,6 @@ public function __construct(JobStorage $jobStorage) } /** - * @param Job $job - * * @return DependentJobContext */ public function createDependentJobContext(Job $job) @@ -29,16 +27,10 @@ public function createDependentJobContext(Job $job) return new DependentJobContext($job); } - /** - * @param DependentJobContext $context - */ public function saveDependentJob(DependentJobContext $context) { if (!$context->getJob()->isRoot()) { - throw new \LogicException(sprintf( - 'Only root jobs allowed but got child. jobId: "%s"', - $context->getJob()->getId() - )); + throw new \LogicException(sprintf('Only root jobs allowed but got child. jobId: "%s"', $context->getJob()->getId())); } $this->jobStorage->saveJob($context->getJob(), function (Job $job) use ($context) { diff --git a/pkg/job-queue/Doctrine/JobStorage.php b/pkg/job-queue/Doctrine/JobStorage.php index 32e20fe8d..4db585696 100644 --- a/pkg/job-queue/Doctrine/JobStorage.php +++ b/pkg/job-queue/Doctrine/JobStorage.php @@ -2,12 +2,12 @@ namespace Enqueue\JobQueue\Doctrine; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\LockMode; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; +use Doctrine\Persistence\ManagerRegistry; use Enqueue\JobQueue\DuplicateJobException; use Enqueue\JobQueue\Job; @@ -39,9 +39,8 @@ class JobStorage private $uniqueTableName; /** - * @param ManagerRegistry $doctrine - * @param string $entityClass - * @param string $uniqueTableName + * @param string $entityClass + * @param string $uniqueTableName */ public function __construct(ManagerRegistry $doctrine, $entityClass, $uniqueTableName) { @@ -59,13 +58,18 @@ public function findJobById($id) { $qb = $this->getEntityRepository()->createQueryBuilder('job'); - return $qb + $job = $qb ->addSelect('rootJob') ->leftJoin('job.rootJob', 'rootJob') ->where('job = :id') ->setParameter('id', $id) ->getQuery()->getOneOrNullResult() ; + if ($job) { + $this->refreshJobEntity($job); + } + + return $job; } /** @@ -90,7 +94,6 @@ public function findRootJobByOwnerIdAndJobName($ownerId, $jobName) /** * @param string $name - * @param Job $rootJob * * @return Job */ @@ -119,20 +122,13 @@ public function createJob() } /** - * @param Job $job - * @param \Closure|null $lockCallback - * * @throws DuplicateJobException */ - public function saveJob(Job $job, \Closure $lockCallback = null) + public function saveJob(Job $job, ?\Closure $lockCallback = null) { $class = $this->getEntityRepository()->getClassName(); if (!$job instanceof $class) { - throw new \LogicException(sprintf( - 'Got unexpected job instance: expected: "%s", actual" "%s"', - $class, - get_class($job) - )); + throw new \LogicException(sprintf('Got unexpected job instance: expected: "%s", actual" "%s"', $class, $job::class)); } if ($lockCallback) { @@ -175,11 +171,7 @@ public function saveJob(Job $job, \Closure $lockCallback = null) ]); } } catch (UniqueConstraintViolationException $e) { - throw new DuplicateJobException(sprintf( - 'Duplicate job. ownerId:"%s", name:"%s"', - $job->getOwnerId(), - $job->getName() - )); + throw new DuplicateJobException(sprintf('Duplicate job. ownerId:"%s", name:"%s"', $job->getOwnerId(), $job->getName())); } $this->getEntityManager()->persist($job); @@ -192,6 +184,14 @@ public function saveJob(Job $job, \Closure $lockCallback = null) } } + /** + * @param Job $job + */ + public function refreshJobEntity($job) + { + $this->getEntityManager()->refresh($job); + } + /** * @return EntityRepository */ diff --git a/pkg/job-queue/Doctrine/mapping/Job.orm.xml b/pkg/job-queue/Doctrine/mapping/Job.orm.xml index e6fcbdde5..d6f481562 100644 --- a/pkg/job-queue/Doctrine/mapping/Job.orm.xml +++ b/pkg/job-queue/Doctrine/mapping/Job.orm.xml @@ -12,7 +12,7 @@ - + diff --git a/pkg/job-queue/Job.php b/pkg/job-queue/Job.php index 0468d7fdb..ddf53c2e3 100644 --- a/pkg/job-queue/Job.php +++ b/pkg/job-queue/Job.php @@ -4,11 +4,11 @@ class Job { - const STATUS_NEW = 'enqueue.job_queue.status.new'; - const STATUS_RUNNING = 'enqueue.job_queue.status.running'; - const STATUS_SUCCESS = 'enqueue.job_queue.status.success'; - const STATUS_FAILED = 'enqueue.job_queue.status.failed'; - const STATUS_CANCELLED = 'enqueue.job_queue.status.cancelled'; + public const STATUS_NEW = 'enqueue.job_queue.status.new'; + public const STATUS_RUNNING = 'enqueue.job_queue.status.running'; + public const STATUS_SUCCESS = 'enqueue.job_queue.status.success'; + public const STATUS_FAILED = 'enqueue.job_queue.status.failed'; + public const STATUS_CANCELLED = 'enqueue.job_queue.status.cancelled'; /** * @var int @@ -216,8 +216,6 @@ public function getRootJob() * Do not call from the outside. * * @internal - * - * @param Job $rootJob */ public function setRootJob(self $rootJob) { @@ -237,8 +235,6 @@ public function getCreatedAt() * Do not call from the outside. * * @internal - * - * @param \DateTime $createdAt */ public function setCreatedAt(\DateTime $createdAt) { @@ -258,8 +254,6 @@ public function getStartedAt() * Do not call from the outside. * * @internal - * - * @param \DateTime $startedAt */ public function setStartedAt(\DateTime $startedAt) { @@ -279,8 +273,6 @@ public function getStoppedAt() * Do not call from the outside. * * @internal - * - * @param \DateTime $stoppedAt */ public function setStoppedAt(\DateTime $stoppedAt) { @@ -324,9 +316,6 @@ public function getData() return $this->data; } - /** - * @param array $data - */ public function setData(array $data) { $this->data = $data; diff --git a/pkg/job-queue/JobProcessor.php b/pkg/job-queue/JobProcessor.php index 7acd84063..06698f45c 100644 --- a/pkg/job-queue/JobProcessor.php +++ b/pkg/job-queue/JobProcessor.php @@ -17,10 +17,6 @@ class JobProcessor */ private $producer; - /** - * @param JobStorage $jobStorage - * @param ProducerInterface $producer - */ public function __construct(JobStorage $jobStorage, ProducerInterface $producer) { $this->jobStorage = $jobStorage; @@ -74,7 +70,6 @@ public function findOrCreateRootJob($ownerId, $jobName, $unique = false) /** * @param string $jobName - * @param Job $rootJob * * @return Job */ @@ -104,9 +99,6 @@ public function findOrCreateChildJob($jobName, Job $rootJob) return $job; } - /** - * @param Job $job - */ public function startChildJob(Job $job) { if ($job->isRoot()) { @@ -116,11 +108,7 @@ public function startChildJob(Job $job) $job = $this->jobStorage->findJobById($job->getId()); if (Job::STATUS_NEW !== $job->getStatus()) { - throw new \LogicException(sprintf( - 'Can start only new jobs: id: "%s", status: "%s"', - $job->getId(), - $job->getStatus() - )); + throw new \LogicException(sprintf('Can start only new jobs: id: "%s", status: "%s"', $job->getId(), $job->getStatus())); } $job->setStatus(Job::STATUS_RUNNING); @@ -131,9 +119,6 @@ public function startChildJob(Job $job) $this->sendCalculateRootJobStatusEvent($job); } - /** - * @param Job $job - */ public function successChildJob(Job $job) { if ($job->isRoot()) { @@ -143,11 +128,7 @@ public function successChildJob(Job $job) $job = $this->jobStorage->findJobById($job->getId()); if (Job::STATUS_RUNNING !== $job->getStatus()) { - throw new \LogicException(sprintf( - 'Can success only running jobs. id: "%s", status: "%s"', - $job->getId(), - $job->getStatus() - )); + throw new \LogicException(sprintf('Can success only running jobs. id: "%s", status: "%s"', $job->getId(), $job->getStatus())); } $job->setStatus(Job::STATUS_SUCCESS); @@ -158,9 +139,6 @@ public function successChildJob(Job $job) $this->sendCalculateRootJobStatusEvent($job); } - /** - * @param Job $job - */ public function failChildJob(Job $job) { if ($job->isRoot()) { @@ -170,11 +148,7 @@ public function failChildJob(Job $job) $job = $this->jobStorage->findJobById($job->getId()); if (Job::STATUS_RUNNING !== $job->getStatus()) { - throw new \LogicException(sprintf( - 'Can fail only running jobs. id: "%s", status: "%s"', - $job->getId(), - $job->getStatus() - )); + throw new \LogicException(sprintf('Can fail only running jobs. id: "%s", status: "%s"', $job->getId(), $job->getStatus())); } $job->setStatus(Job::STATUS_FAILED); @@ -185,9 +159,6 @@ public function failChildJob(Job $job) $this->sendCalculateRootJobStatusEvent($job); } - /** - * @param Job $job - */ public function cancelChildJob(Job $job) { if ($job->isRoot()) { @@ -197,11 +168,7 @@ public function cancelChildJob(Job $job) $job = $this->jobStorage->findJobById($job->getId()); if (!in_array($job->getStatus(), [Job::STATUS_NEW, Job::STATUS_RUNNING], true)) { - throw new \LogicException(sprintf( - 'Can cancel only new or running jobs. id: "%s", status: "%s"', - $job->getId(), - $job->getStatus() - )); + throw new \LogicException(sprintf('Can cancel only new or running jobs. id: "%s", status: "%s"', $job->getId(), $job->getStatus())); } $job->setStatus(Job::STATUS_CANCELLED); @@ -217,7 +184,6 @@ public function cancelChildJob(Job $job) } /** - * @param Job $job * @param bool $force */ public function interruptRootJob(Job $job, $force = false) @@ -245,8 +211,6 @@ public function interruptRootJob(Job $job, $force = false) /** * @see https://github.com/php-enqueue/enqueue-dev/pull/222#issuecomment-336102749 See for rationale - * - * @param Job $job */ protected function saveJob(Job $job) { @@ -255,12 +219,10 @@ protected function saveJob(Job $job) /** * @see https://github.com/php-enqueue/enqueue-dev/pull/222#issuecomment-336102749 See for rationale - * - * @param Job $job */ protected function sendCalculateRootJobStatusEvent(Job $job) { - $this->producer->sendEvent(Topics::CALCULATE_ROOT_JOB_STATUS, [ + $this->producer->sendCommand(Commands::CALCULATE_ROOT_JOB_STATUS, [ 'jobId' => $job->getId(), ]); } diff --git a/pkg/job-queue/JobRunner.php b/pkg/job-queue/JobRunner.php index 3f7c3f966..e21e524d6 100644 --- a/pkg/job-queue/JobRunner.php +++ b/pkg/job-queue/JobRunner.php @@ -14,24 +14,17 @@ class JobRunner */ private $rootJob; - /** - * @param JobProcessor $jobProcessor - * @param Job $rootJob - */ - public function __construct(JobProcessor $jobProcessor, Job $rootJob = null) + public function __construct(JobProcessor $jobProcessor, ?Job $rootJob = null) { $this->jobProcessor = $jobProcessor; $this->rootJob = $rootJob; } /** - * @param string $ownerId - * @param string $name - * @param callable $runCallback + * @param string $ownerId + * @param string $name * * @throws \Throwable|\Exception if $runCallback triggers an exception - * - * @return mixed */ public function runUnique($ownerId, $name, callable $runCallback) { @@ -51,10 +44,12 @@ public function runUnique($ownerId, $name, callable $runCallback) try { $result = call_user_func($runCallback, $jobRunner, $childJob); } catch (\Throwable $e) { - $this->jobProcessor->failChildJob($childJob); - throw $e; - } catch (\Exception $e) { // needed to support PHP 5.6 - $this->jobProcessor->failChildJob($childJob); + try { + $this->jobProcessor->failChildJob($childJob); + } catch (\Throwable $t) { + throw new OrphanJobException(sprintf('Job cleanup failed. ID: "%s" Name: "%s"', $childJob->getId(), $childJob->getName()), 0, $e); + } + throw $e; } @@ -68,10 +63,7 @@ public function runUnique($ownerId, $name, callable $runCallback) } /** - * @param string $name - * @param callable $startCallback - * - * @return mixed + * @param string $name */ public function createDelayed($name, callable $startCallback) { @@ -83,10 +75,7 @@ public function createDelayed($name, callable $startCallback) } /** - * @param string $jobId - * @param callable $runCallback - * - * @return mixed + * @param string $jobId */ public function runDelayed($jobId, callable $runCallback) { diff --git a/pkg/job-queue/OrphanJobException.php b/pkg/job-queue/OrphanJobException.php new file mode 100644 index 000000000..37eac5ca7 --- /dev/null +++ b/pkg/job-queue/OrphanJobException.php @@ -0,0 +1,7 @@ +Supporting Enqueue + +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Job Queue. [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/job-queue.png?branch=master)](https://travis-ci.org/php-enqueue/job-queue) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/job-queue/ci.yml?branch=master)](https://github.com/php-enqueue/job-queue/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/job-queue/d/total.png)](https://packagist.org/packages/enqueue/job-queue) [![Latest Stable Version](https://poser.pugx.org/enqueue/job-queue/version.png)](https://packagist.org/packages/enqueue/job-queue) - -There is job queue component build on top of a transport. -It provides some additional features like: unique job, sub jobs, dependent job and so. + +There is job queue component build on top of a transport. +It provides some additional features like: unique job, sub jobs, dependent job and so. Read more about it in documentation ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/job-queue/Schema.php b/pkg/job-queue/Schema.php deleted file mode 100644 index 71d6985b7..000000000 --- a/pkg/job-queue/Schema.php +++ /dev/null @@ -1,55 +0,0 @@ -uniqueTableName = $uniqueTableName; - - $schemaConfig = $connection->getSchemaManager()->createSchemaConfig(); - - parent::__construct([], [], $schemaConfig); - - $this->addUniqueJobTable(); - } - - /** - * Merges ACL schema with the given schema. - * - * @param BaseSchema $schema - */ - public function addToSchema(BaseSchema $schema) - { - foreach ($this->getTables() as $table) { - $schema->_addTable($table); - } - - foreach ($this->getSequences() as $sequence) { - $schema->_addSequence($sequence); - } - } - - private function addUniqueJobTable() - { - $table = $this->createTable($this->uniqueTableName); - $table->addColumn('name', 'string', ['length' => 255]); - $table->addUniqueIndex(['name']); - } -} diff --git a/pkg/job-queue/Test/DbalPersistedConnection.php b/pkg/job-queue/Test/DbalPersistedConnection.php index 7c06f5ab0..470a65176 100644 --- a/pkg/job-queue/Test/DbalPersistedConnection.php +++ b/pkg/job-queue/Test/DbalPersistedConnection.php @@ -22,9 +22,6 @@ class DbalPersistedConnection extends Connection */ protected static $persistedTransactionNestingLevels; - /** - * {@inheritdoc} - */ public function connect() { if ($this->isConnected()) { @@ -33,7 +30,6 @@ public function connect() if ($this->hasPersistedConnection()) { $this->_conn = $this->getPersistedConnection(); - $this->setConnected(true); } else { parent::connect(); $this->persistConnection($this->_conn); @@ -42,39 +38,25 @@ public function connect() return true; } - /** - * {@inheritdoc} - */ public function beginTransaction() { $this->wrapTransactionNestingLevel('beginTransaction'); + + return true; } - /** - * {@inheritdoc} - */ public function commit() { $this->wrapTransactionNestingLevel('commit'); + + return true; } - /** - * {@inheritdoc} - */ public function rollBack() { $this->wrapTransactionNestingLevel('rollBack'); - } - /** - * @param bool $connected - */ - protected function setConnected($connected) - { - $isConnected = new \ReflectionProperty('Doctrine\DBAL\Connection', '_isConnected'); - $isConnected->setAccessible(true); - $isConnected->setValue($this, $connected); - $isConnected->setAccessible(false); + return true; } /** @@ -97,9 +79,6 @@ protected function persistTransactionNestingLevel($level) static::$persistedTransactionNestingLevels[$this->getConnectionId()] = $level; } - /** - * @param DriverConnection $connection - */ protected function persistConnection(DriverConnection $connection) { static::$persistedConnections[$this->getConnectionId()] = $connection; @@ -134,10 +113,15 @@ protected function getConnectionId() */ private function setTransactionNestingLevel($level) { - $prop = new \ReflectionProperty('Doctrine\DBAL\Connection', '_transactionNestingLevel'); - $prop->setAccessible(true); - - return $prop->setValue($this, $level); + $rc = new \ReflectionClass(Connection::class); + $rp = $rc->hasProperty('transactionNestingLevel') ? + $rc->getProperty('transactionNestingLevel') : + $rc->getProperty('_transactionNestingLevel') + ; + + $rp->setAccessible(true); + $rp->setValue($this, $level); + $rp->setAccessible(false); } /** diff --git a/pkg/job-queue/Test/JobRunner.php b/pkg/job-queue/Test/JobRunner.php index 62d9f9a52..6194fbcee 100644 --- a/pkg/job-queue/Test/JobRunner.php +++ b/pkg/job-queue/Test/JobRunner.php @@ -26,9 +26,6 @@ public function __construct() { } - /** - * {@inheritdoc} - */ public function runUnique($ownerId, $jobName, \Closure $runCallback) { $this->runUniqueJobs[] = ['ownerId' => $ownerId, 'jobName' => $jobName, 'runCallback' => $runCallback]; @@ -36,11 +33,6 @@ public function runUnique($ownerId, $jobName, \Closure $runCallback) return call_user_func($runCallback, $this, new Job()); } - /** - * {@inheritdoc} - * - * @return mixed - */ public function createDelayed($jobName, \Closure $startCallback) { $this->createDelayedJobs[] = ['jobName' => $jobName, 'runCallback' => $startCallback]; @@ -48,11 +40,6 @@ public function createDelayed($jobName, \Closure $startCallback) return call_user_func($startCallback, $this, new Job()); } - /** - * {@inheritdoc} - * - * @return mixed - */ public function runDelayed($jobId, \Closure $runCallback) { $this->runDelayedJobs[] = ['jobId' => $jobId, 'runCallback' => $runCallback]; diff --git a/pkg/job-queue/Tests/CalculateRootJobStatusProcessorTest.php b/pkg/job-queue/Tests/CalculateRootJobStatusProcessorTest.php index 2c86a01db..8509f0544 100644 --- a/pkg/job-queue/Tests/CalculateRootJobStatusProcessorTest.php +++ b/pkg/job-queue/Tests/CalculateRootJobStatusProcessorTest.php @@ -6,30 +6,22 @@ use Enqueue\Consumption\Result; use Enqueue\JobQueue\CalculateRootJobStatusProcessor; use Enqueue\JobQueue\CalculateRootJobStatusService; +use Enqueue\JobQueue\Commands; use Enqueue\JobQueue\Doctrine\JobStorage; use Enqueue\JobQueue\Job; use Enqueue\JobQueue\Topics; use Enqueue\Null\NullMessage; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; class CalculateRootJobStatusProcessorTest extends \PHPUnit\Framework\TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new CalculateRootJobStatusProcessor( - $this->createJobStorageMock(), - $this->createCalculateRootJobStatusCaseMock(), - $this->createProducerMock(), - $this->createLoggerMock() - ); - } - public function testShouldReturnSubscribedTopicNames() { $this->assertEquals( - [Topics::CALCULATE_ROOT_JOB_STATUS], - CalculateRootJobStatusProcessor::getSubscribedTopics() + Commands::CALCULATE_ROOT_JOB_STATUS, + CalculateRootJobStatusProcessor::getSubscribedCommand() ); } @@ -102,7 +94,7 @@ public function testShouldCallCalculateJobRootStatusAndACKMessage() ->expects($this->once()) ->method('findJobById') ->with('12345') - ->will($this->returnValue($job)) + ->willReturn($job) ; $logger = $this->createLoggerMock(); @@ -145,7 +137,7 @@ public function testShouldSendRootJobStoppedMessageIfJobHasStopped() ->expects($this->once()) ->method('findJobById') ->with('12345') - ->will($this->returnValue($job)) + ->willReturn($job) ; $logger = $this->createLoggerMock(); @@ -155,7 +147,7 @@ public function testShouldSendRootJobStoppedMessageIfJobHasStopped() ->expects($this->once()) ->method('calculate') ->with($this->identicalTo($job)) - ->will($this->returnValue(true)) + ->willReturn(true) ; $producer = $this->createProducerMock(); @@ -175,7 +167,7 @@ public function testShouldSendRootJobStoppedMessageIfJobHasStopped() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface + * @return MockObject|ProducerInterface */ private function createProducerMock() { @@ -183,15 +175,15 @@ private function createProducerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|Context */ private function createContextMock() { - return $this->createMock(PsrContext::class); + return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + * @return MockObject|LoggerInterface */ private function createLoggerMock() { @@ -199,7 +191,7 @@ private function createLoggerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|CalculateRootJobStatusService + * @return MockObject|CalculateRootJobStatusService */ private function createCalculateRootJobStatusCaseMock() { @@ -207,7 +199,7 @@ private function createCalculateRootJobStatusCaseMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|JobStorage + * @return MockObject|JobStorage */ private function createJobStorageMock() { diff --git a/pkg/job-queue/Tests/CalculateRootJobStatusServiceTest.php b/pkg/job-queue/Tests/CalculateRootJobStatusServiceTest.php index 01f0d10ce..a0f1b4c86 100644 --- a/pkg/job-queue/Tests/CalculateRootJobStatusServiceTest.php +++ b/pkg/job-queue/Tests/CalculateRootJobStatusServiceTest.php @@ -5,14 +5,10 @@ use Enqueue\JobQueue\CalculateRootJobStatusService; use Enqueue\JobQueue\Doctrine\JobStorage; use Enqueue\JobQueue\Job; +use PHPUnit\Framework\MockObject\MockObject; class CalculateRootJobStatusServiceTest extends \PHPUnit\Framework\TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new CalculateRootJobStatusService($this->createJobStorageMock()); - } - public function stopStatusProvider() { return [ @@ -24,8 +20,6 @@ public function stopStatusProvider() /** * @dataProvider stopStatusProvider - * - * @param mixed $status */ public function testShouldDoNothingIfRootJobHasStopState($status) { @@ -60,9 +54,9 @@ public function testShouldCalculateRootJobStatus() $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($storage); @@ -74,8 +68,6 @@ public function testShouldCalculateRootJobStatus() /** * @dataProvider stopStatusProvider - * - * @param mixed $stopStatus */ public function testShouldCalculateRootJobStatusAndSetStoppedAtTimeIfGotStopStatus($stopStatus) { @@ -92,16 +84,19 @@ public function testShouldCalculateRootJobStatusAndSetStoppedAtTimeIfGotStopStat $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($storage); $case->calculate($childJob); $this->assertEquals($stopStatus, $rootJob->getStatus()); - $this->assertEquals(new \DateTime(), $rootJob->getStoppedAt(), '', 1); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $rootJob->getStoppedAt()->getTimestamp() + ); } public function testShouldSetStoppedAtOnlyIfWasNotSet() @@ -120,15 +115,18 @@ public function testShouldSetStoppedAtOnlyIfWasNotSet() $em ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($em); $case->calculate($childJob); - $this->assertEquals(new \DateTime('2012-12-12 12:12:12'), $rootJob->getStoppedAt()); + $this->assertEquals( + (new \DateTime('2012-12-12 12:12:12'))->getTimestamp(), + $rootJob->getStoppedAt()->getTimestamp() + ); } public function testShouldThrowIfInvalidStatus() @@ -146,17 +144,15 @@ public function testShouldThrowIfInvalidStatus() $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($storage); - $this->setExpectedException( - \LogicException::class, - 'Got unsupported job status: id: "12345" status: "invalid-status"' - ); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Got unsupported job status: id: "12345" status: "invalid-status"'); $case->calculate($childJob); } @@ -179,9 +175,9 @@ public function testShouldSetStatusNewIfAllChildAreNew() $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($storage); @@ -212,9 +208,9 @@ public function testShouldSetStatusRunningIfAnyOneIsRunning() $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($storage); @@ -245,9 +241,9 @@ public function testShouldSetStatusRunningIfThereIsNoRunningButNewAndAnyOfStopSt $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($storage); @@ -278,9 +274,9 @@ public function testShouldSetStatusCancelledIfAllIsStopButOneIsCancelled() $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($storage); @@ -311,9 +307,9 @@ public function testShouldSetStatusFailedIfThereIsAnyOneIsFailedButIsNotCancelle $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($storage); @@ -344,9 +340,9 @@ public function testShouldSetStatusSuccessIfAllAreSuccess() $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $case = new CalculateRootJobStatusService($storage); @@ -356,7 +352,7 @@ public function testShouldSetStatusSuccessIfAllAreSuccess() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|\Enqueue\JobQueue\Doctrine\JobStorage + * @return MockObject|JobStorage */ private function createJobStorageMock() { diff --git a/pkg/job-queue/Tests/DependentJobContextTest.php b/pkg/job-queue/Tests/DependentJobContextTest.php index 32340d687..35942a974 100644 --- a/pkg/job-queue/Tests/DependentJobContextTest.php +++ b/pkg/job-queue/Tests/DependentJobContextTest.php @@ -7,11 +7,6 @@ class DependentJobContextTest extends \PHPUnit\Framework\TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new DependentJobContext(new Job()); - } - public function testShouldReturnJob() { $job = new Job(); diff --git a/pkg/job-queue/Tests/DependentJobProcessorTest.php b/pkg/job-queue/Tests/DependentJobProcessorTest.php index a61918c19..dcebf0d49 100644 --- a/pkg/job-queue/Tests/DependentJobProcessorTest.php +++ b/pkg/job-queue/Tests/DependentJobProcessorTest.php @@ -10,7 +10,8 @@ use Enqueue\JobQueue\Job; use Enqueue\JobQueue\Topics; use Enqueue\Null\NullMessage; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; class DependentJobProcessorTest extends \PHPUnit\Framework\TestCase @@ -18,7 +19,7 @@ class DependentJobProcessorTest extends \PHPUnit\Framework\TestCase public function testShouldReturnSubscribedTopicNames() { $this->assertEquals( - [Topics::ROOT_JOB_STOPPED], + Topics::ROOT_JOB_STOPPED, DependentJobProcessor::getSubscribedTopics() ); } @@ -84,7 +85,7 @@ public function testShouldLogCriticalAndRejectMessageIfJobIsNotRoot() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $producer = $this->createProducerMock(); @@ -115,7 +116,7 @@ public function testShouldDoNothingIfDependentJobsAreMissing() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $producer = $this->createProducerMock(); @@ -151,7 +152,7 @@ public function testShouldLogCriticalAndRejectMessageIfDependentJobTopicIsMissin ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $producer = $this->createProducerMock(); @@ -194,7 +195,7 @@ public function testShouldLogCriticalAndRejectMessageIfDependentJobMessageIsMiss ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $producer = $this->createProducerMock(); @@ -239,7 +240,7 @@ public function testShouldPublishDependentMessage() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $expectedMessage = null; @@ -248,9 +249,9 @@ public function testShouldPublishDependentMessage() ->expects($this->once()) ->method('sendEvent') ->with('topic-name', $this->isInstanceOf(Message::class)) - ->will($this->returnCallback(function ($topic, Message $message) use (&$expectedMessage) { + ->willReturnCallback(function ($topic, Message $message) use (&$expectedMessage) { $expectedMessage = $message; - })) + }) ; $logger = $this->createLoggerMock(); @@ -287,7 +288,7 @@ public function testShouldPublishDependentMessageWithPriority() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $expectedMessage = null; @@ -296,9 +297,9 @@ public function testShouldPublishDependentMessageWithPriority() ->expects($this->once()) ->method('sendEvent') ->with('topic-name', $this->isInstanceOf(Message::class)) - ->will($this->returnCallback(function ($topic, Message $message) use (&$expectedMessage) { + ->willReturnCallback(function ($topic, Message $message) use (&$expectedMessage) { $expectedMessage = $message; - })) + }) ; $logger = $this->createLoggerMock(); @@ -317,15 +318,15 @@ public function testShouldPublishDependentMessageWithPriority() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrContext + * @return MockObject|Context */ private function createContextMock() { - return $this->createMock(PsrContext::class); + return $this->createMock(Context::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|\Enqueue\JobQueue\Doctrine\JobStorage + * @return MockObject|JobStorage */ private function createJobStorageMock() { @@ -333,7 +334,7 @@ private function createJobStorageMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerInterface + * @return MockObject|ProducerInterface */ private function createProducerMock() { @@ -341,7 +342,7 @@ private function createProducerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface + * @return MockObject|LoggerInterface */ private function createLoggerMock() { diff --git a/pkg/job-queue/Tests/DependentJobServiceTest.php b/pkg/job-queue/Tests/DependentJobServiceTest.php index 7469b9fb8..c2a1c7b1a 100644 --- a/pkg/job-queue/Tests/DependentJobServiceTest.php +++ b/pkg/job-queue/Tests/DependentJobServiceTest.php @@ -6,14 +6,10 @@ use Enqueue\JobQueue\DependentJobService; use Enqueue\JobQueue\Doctrine\JobStorage; use Enqueue\JobQueue\Job; +use PHPUnit\Framework\MockObject\MockObject; class DependentJobServiceTest extends \PHPUnit\Framework\TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new DependentJobService($this->createJobStorageMock()); - } - public function testShouldThrowIfJobIsNotRootJob() { $job = new Job(); @@ -24,7 +20,8 @@ public function testShouldThrowIfJobIsNotRootJob() $service = new DependentJobService($this->createJobStorageMock()); - $this->setExpectedException(\LogicException::class, 'Only root jobs allowed but got child. jobId: "12345"'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Only root jobs allowed but got child. jobId: "12345"'); $service->saveDependentJob($context); } @@ -38,11 +35,11 @@ public function testShouldSaveDependentJobs() $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); return true; - })) + }) ; $context = new DependentJobContext($job); @@ -66,7 +63,7 @@ public function testShouldSaveDependentJobs() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|\Enqueue\JobQueue\Doctrine\JobStorage + * @return MockObject|JobStorage */ private function createJobStorageMock() { diff --git a/pkg/job-queue/Tests/Doctrine/JobStorageTest.php b/pkg/job-queue/Tests/Doctrine/JobStorageTest.php index 97fbc750c..73f130d52 100644 --- a/pkg/job-queue/Tests/Doctrine/JobStorageTest.php +++ b/pkg/job-queue/Tests/Doctrine/JobStorageTest.php @@ -2,30 +2,26 @@ namespace Enqueue\JobQueue\Tests\Doctrine; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\LockMode; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; +use Doctrine\Persistence\ManagerRegistry; use Enqueue\JobQueue\Doctrine\JobStorage; use Enqueue\JobQueue\DuplicateJobException; use Enqueue\JobQueue\Job; +use PHPUnit\Framework\MockObject\MockObject; class JobStorageTest extends \PHPUnit\Framework\TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new JobStorage($this->createDoctrineMock(), 'entity-class', 'unique_table'); - } - public function testShouldCreateJobObject() { $repository = $this->createRepositoryMock(); $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue(Job::class)) + ->willReturn(Job::class) ; $em = $this->createEntityManagerMock(); @@ -33,12 +29,12 @@ public function testShouldCreateJobObject() ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(true)) + ->willReturn(true) ; $doctrine = $this->createDoctrineMock(); @@ -46,7 +42,7 @@ public function testShouldCreateJobObject() ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->never()) @@ -65,7 +61,7 @@ public function testShouldResetManagerAndCreateJobObject() $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue(Job::class)) + ->willReturn(Job::class) ; $em = $this->createEntityManagerMock(); @@ -73,12 +69,12 @@ public function testShouldResetManagerAndCreateJobObject() ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(false)) + ->willReturn(false) ; $doctrine = $this->createDoctrineMock(); @@ -86,12 +82,12 @@ public function testShouldResetManagerAndCreateJobObject() ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->any()) ->method('resetManager') - ->will($this->returnValue($em)) + ->willReturn($em) ; $storage = new JobStorage($doctrine, 'entity-class', 'unique_table'); @@ -106,7 +102,7 @@ public function testShouldThrowIfGotUnexpectedJobInstance() $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue('expected\class\name')) + ->willReturn('expected\class\name') ; $em = $this->createEntityManagerMock(); @@ -114,12 +110,12 @@ public function testShouldThrowIfGotUnexpectedJobInstance() ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(true)) + ->willReturn(true) ; $doctrine = $this->createDoctrineMock(); @@ -127,7 +123,7 @@ public function testShouldThrowIfGotUnexpectedJobInstance() ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->never()) @@ -136,11 +132,8 @@ public function testShouldThrowIfGotUnexpectedJobInstance() $storage = new JobStorage($doctrine, 'entity-class', 'unique_table'); - $this->setExpectedException( - \LogicException::class, - 'Got unexpected job instance: expected: "expected\class\name", '. - 'actual" "Enqueue\JobQueue\Job"' - ); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Got unexpected job instance: expected: "expected\class\name", actual" "Enqueue\JobQueue\Job"'); $storage->saveJob(new Job()); } @@ -156,7 +149,7 @@ public function testShouldSaveJobWithoutLockIfThereIsNoCallbackAndChildJob() $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue(Job::class)) + ->willReturn(Job::class) ; $em = $this->createEntityManagerMock(); @@ -164,7 +157,7 @@ public function testShouldSaveJobWithoutLockIfThereIsNoCallbackAndChildJob() ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->once()) @@ -182,7 +175,7 @@ public function testShouldSaveJobWithoutLockIfThereIsNoCallbackAndChildJob() $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(true)) + ->willReturn(true) ; $doctrine = $this->createDoctrineMock(); @@ -190,7 +183,7 @@ public function testShouldSaveJobWithoutLockIfThereIsNoCallbackAndChildJob() ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->never()) @@ -210,7 +203,7 @@ public function testShouldSaveJobWithLockIfWithCallback() $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue(Job::class)) + ->willReturn(Job::class) ; $em = $this->createEntityManagerMock(); @@ -218,7 +211,7 @@ public function testShouldSaveJobWithLockIfWithCallback() ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->never()) @@ -236,7 +229,7 @@ public function testShouldSaveJobWithLockIfWithCallback() $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(true)) + ->willReturn(true) ; $doctrine = $this->createDoctrineMock(); @@ -244,7 +237,7 @@ public function testShouldSaveJobWithLockIfWithCallback() ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->never()) @@ -267,16 +260,16 @@ public function testShouldCatchUniqueConstraintViolationExceptionAndThrowDuplica $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue(Job::class)) + ->willReturn(Job::class) ; $connection = $this->createConnectionMock(); $connection ->expects($this->once()) ->method('transactional') - ->will($this->returnCallback(function ($callback) use ($connection) { + ->willReturnCallback(function ($callback) use ($connection) { $callback($connection); - })) + }) ; $connection ->expects($this->once()) @@ -289,17 +282,17 @@ public function testShouldCatchUniqueConstraintViolationExceptionAndThrowDuplica ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->once()) ->method('getConnection') - ->will($this->returnValue($connection)) + ->willReturn($connection) ; $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(true)) + ->willReturn(true) ; $doctrine = $this->createDoctrineMock(); @@ -307,7 +300,7 @@ public function testShouldCatchUniqueConstraintViolationExceptionAndThrowDuplica ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->never()) @@ -316,7 +309,8 @@ public function testShouldCatchUniqueConstraintViolationExceptionAndThrowDuplica $storage = new JobStorage($doctrine, 'entity-class', 'unique_table'); - $this->setExpectedException(DuplicateJobException::class, 'Duplicate job. ownerId:"owner-id", name:"job-name"'); + $this->expectException(DuplicateJobException::class); + $this->expectExceptionMessage('Duplicate job. ownerId:"owner-id", name:"job-name"'); $storage->saveJob($job); } @@ -329,7 +323,7 @@ public function testShouldThrowIfTryToSaveNewEntityWithLock() $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue(Job::class)) + ->willReturn(Job::class) ; $em = $this->createEntityManagerMock(); @@ -337,12 +331,12 @@ public function testShouldThrowIfTryToSaveNewEntityWithLock() ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(true)) + ->willReturn(true) ; $doctrine = $this->createDoctrineMock(); @@ -350,7 +344,7 @@ public function testShouldThrowIfTryToSaveNewEntityWithLock() ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->never()) @@ -359,11 +353,8 @@ public function testShouldThrowIfTryToSaveNewEntityWithLock() $storage = new JobStorage($doctrine, 'entity-class', 'unique_table'); - $this->setExpectedException( - \LogicException::class, - 'Is not possible to create new job with lock, only update is allowed' - ); - + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Is not possible to create new job with lock, only update is allowed'); $storage->saveJob($job, function () { }); } @@ -378,13 +369,13 @@ public function testShouldLockEntityAndPassNewInstanceIntoCallback() $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue(Job::class)) + ->willReturn(Job::class) ; $repository ->expects($this->once()) ->method('find') ->with(12345, LockMode::PESSIMISTIC_WRITE) - ->will($this->returnValue($lockedJob)) + ->willReturn($lockedJob) ; $em = $this->createEntityManagerMock(); @@ -392,19 +383,19 @@ public function testShouldLockEntityAndPassNewInstanceIntoCallback() ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->once()) ->method('transactional') - ->will($this->returnCallback(function ($callback) use ($em) { + ->willReturnCallback(function ($callback) use ($em) { $callback($em); - })) + }) ; $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(true)) + ->willReturn(true) ; $doctrine = $this->createDoctrineMock(); @@ -412,7 +403,7 @@ public function testShouldLockEntityAndPassNewInstanceIntoCallback() ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->never()) @@ -439,9 +430,9 @@ public function testShouldInsertIntoUniqueTableIfJobIsUniqueAndNew() $connection ->expects($this->once()) ->method('transactional') - ->will($this->returnCallback(function ($callback) use ($connection) { + ->willReturnCallback(function ($callback) use ($connection) { $callback($connection); - })) + }) ; $connection ->expects($this->at(0)) @@ -458,7 +449,7 @@ public function testShouldInsertIntoUniqueTableIfJobIsUniqueAndNew() $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue(Job::class)) + ->willReturn(Job::class) ; $em = $this->createEntityManagerMock(); @@ -466,12 +457,12 @@ public function testShouldInsertIntoUniqueTableIfJobIsUniqueAndNew() ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->once()) ->method('getConnection') - ->will($this->returnValue($connection)) + ->willReturn($connection) ; $em ->expects($this->once()) @@ -484,7 +475,7 @@ public function testShouldInsertIntoUniqueTableIfJobIsUniqueAndNew() $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(true)) + ->willReturn(true) ; $doctrine = $this->createDoctrineMock(); @@ -492,7 +483,7 @@ public function testShouldInsertIntoUniqueTableIfJobIsUniqueAndNew() ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->never()) @@ -528,12 +519,12 @@ public function testShouldDeleteRecordFromUniqueTableIfJobIsUniqueAndStoppedAtIs $repository ->expects($this->once()) ->method('getClassName') - ->will($this->returnValue(Job::class)) + ->willReturn(Job::class) ; $repository ->expects($this->once()) ->method('find') - ->will($this->returnValue($job)) + ->willReturn($job) ; $em = $this->createEntityManagerMock(); @@ -541,24 +532,24 @@ public function testShouldDeleteRecordFromUniqueTableIfJobIsUniqueAndStoppedAtIs ->expects($this->once()) ->method('getRepository') ->with('entity-class') - ->will($this->returnValue($repository)) + ->willReturn($repository) ; $em ->expects($this->once()) ->method('transactional') - ->will($this->returnCallback(function ($callback) use ($em) { + ->willReturnCallback(function ($callback) use ($em) { $callback($em); - })) + }) ; $em ->expects($this->exactly(2)) ->method('getConnection') - ->will($this->returnValue($connection)) + ->willReturn($connection) ; $em ->expects($this->any()) ->method('isOpen') - ->will($this->returnValue(true)) + ->willReturn(true) ; $doctrine = $this->createDoctrineMock(); @@ -566,7 +557,7 @@ public function testShouldDeleteRecordFromUniqueTableIfJobIsUniqueAndStoppedAtIs ->expects($this->once()) ->method('getManagerForClass') ->with('entity-class') - ->will($this->returnValue($em)) + ->willReturn($em) ; $doctrine ->expects($this->never()) @@ -579,7 +570,7 @@ public function testShouldDeleteRecordFromUniqueTableIfJobIsUniqueAndStoppedAtIs } /** - * @return \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry + * @return MockObject|ManagerRegistry */ private function createDoctrineMock() { @@ -587,7 +578,7 @@ private function createDoctrineMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Connection + * @return MockObject|Connection */ private function createConnectionMock() { @@ -595,7 +586,7 @@ private function createConnectionMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|EntityManager + * @return MockObject|EntityManager */ private function createEntityManagerMock() { @@ -603,7 +594,7 @@ private function createEntityManagerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|EntityRepository + * @return MockObject|EntityRepository */ private function createRepositoryMock() { @@ -611,7 +602,7 @@ private function createRepositoryMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|UniqueConstraintViolationException + * @return MockObject|UniqueConstraintViolationException */ private function createUniqueConstraintViolationExceptionMock() { diff --git a/pkg/job-queue/Tests/Functional/Entity/Job.php b/pkg/job-queue/Tests/Functional/Entity/Job.php index b6e0308a4..ad90e58a0 100644 --- a/pkg/job-queue/Tests/Functional/Entity/Job.php +++ b/pkg/job-queue/Tests/Functional/Entity/Job.php @@ -6,97 +6,47 @@ use Doctrine\ORM\Mapping as ORM; use Enqueue\JobQueue\Job as BaseJob; -/** - * @ORM\Entity - * @ORM\Table(name="enqueue_job_queue") - */ +#[ORM\Entity] +#[ORM\Table(name: 'enqueue_job_queue')] class Job extends BaseJob { - /** - * @var int - * - * @ORM\Column(name="id", type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - */ + #[ORM\Column(name: 'id', type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] protected $id; - /** - * @var string - * - * @ORM\Column(name="owner_id", type="string", nullable=true) - */ + #[ORM\Column(name: 'owner_id', type: 'string', nullable: true)] protected $ownerId; - /** - * @var string - * - * @ORM\Column(name="name", type="string", nullable=false) - */ + #[ORM\Column(name: 'name', type: 'string', nullable: false)] protected $name; - /** - * @var string - * - * @ORM\Column(name="status", type="string", nullable=false) - */ + #[ORM\Column(name: 'status', type: 'string', nullable: false)] protected $status; - /** - * @var bool - * - * @ORM\Column(name="interrupted", type="boolean") - */ + #[ORM\Column(name: 'interrupted', type: 'boolean')] protected $interrupted; - /** - * @var bool; - * - * @ORM\Column(name="`unique`", type="boolean") - */ + #[ORM\Column(name: '`unique`', type: 'boolean')] protected $unique; - /** - * @var Job - * - * @ORM\ManyToOne(targetEntity="Job", inversedBy="childJobs") - * @ORM\JoinColumn(name="root_job_id", referencedColumnName="id", onDelete="CASCADE") - */ + #[ORM\ManyToOne(targetEntity: 'Job', inversedBy: 'childJobs')] + #[ORM\JoinColumn(name: 'root_job_id', referencedColumnName: 'id', onDelete: 'CASCADE')] protected $rootJob; - /** - * @var Job[] - * - * @ORM\OneToMany(targetEntity="Job", mappedBy="rootJob") - */ + #[ORM\OneToMany(mappedBy: 'rootJob', targetEntity: 'Job')] protected $childJobs; - /** - * @var \DateTime - * - * @ORM\Column(name="created_at", type="datetime", nullable=false) - */ + #[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)] protected $createdAt; - /** - * @var \DateTime - * - * @ORM\Column(name="started_at", type="datetime", nullable=true) - */ + #[ORM\Column(name: 'started_at', type: 'datetime', nullable: true)] protected $startedAt; - /** - * @var \DateTime - * - * @ORM\Column(name="stopped_at", type="datetime", nullable=true) - */ + #[ORM\Column(name: 'stopped_at', type: 'datetime', nullable: true)] protected $stoppedAt; - /** - * @var array - * - * @ORM\Column(name="data", type="json_array", nullable=true) - */ + #[ORM\Column(name: 'data', type: 'json', nullable: true)] protected $data; public function __construct() diff --git a/pkg/job-queue/Tests/Functional/Entity/JobUnique.php b/pkg/job-queue/Tests/Functional/Entity/JobUnique.php index 4d8a33745..6d10ea2a5 100644 --- a/pkg/job-queue/Tests/Functional/Entity/JobUnique.php +++ b/pkg/job-queue/Tests/Functional/Entity/JobUnique.php @@ -4,15 +4,11 @@ use Doctrine\ORM\Mapping as ORM; -/** - * @ORM\Entity - * @ORM\Table(name="enqueue_job_queue_unique") - */ +#[ORM\Entity] +#[ORM\Table(name: 'enqueue_job_queue_unique')] class JobUnique { - /** - * @ORM\Id - * @ORM\Column(name="name", type="string", nullable=false) - */ + #[ORM\Id] + #[ORM\Column(name: 'name', type: 'string', nullable: false)] protected $name; } diff --git a/pkg/job-queue/Tests/Functional/Job/JobStorageTest.php b/pkg/job-queue/Tests/Functional/Job/JobStorageTest.php index 76cf28ee8..e884c31e8 100644 --- a/pkg/job-queue/Tests/Functional/Job/JobStorageTest.php +++ b/pkg/job-queue/Tests/Functional/Job/JobStorageTest.php @@ -128,7 +128,7 @@ public function testShouldThrowIfDuplicateJob() $job2->setStatus(Job::STATUS_NEW); $job2->setCreatedAt(new \DateTime()); - $this->setExpectedException(DuplicateJobException::class); + $this->expectException(DuplicateJobException::class); $this->getJobStorage()->saveJob($job2); } @@ -138,14 +138,14 @@ public function testShouldThrowIfDuplicateJob() */ private function getEntityManager() { - return $this->container->get('doctrine.orm.default_entity_manager'); + return static::$container->get('doctrine.orm.default_entity_manager'); } /** - * @return \Enqueue\JobQueue\Doctrine\JobStorage + * @return JobStorage */ private function getJobStorage() { - return new JobStorage($this->container->get('doctrine'), Job::class, 'enqueue_job_queue_unique'); + return new JobStorage(static::$container->get('doctrine'), Job::class, 'enqueue_job_queue_unique'); } } diff --git a/pkg/job-queue/Tests/Functional/WebTestCase.php b/pkg/job-queue/Tests/Functional/WebTestCase.php index da4913ee9..781bb5da4 100644 --- a/pkg/job-queue/Tests/Functional/WebTestCase.php +++ b/pkg/job-queue/Tests/Functional/WebTestCase.php @@ -11,36 +11,36 @@ abstract class WebTestCase extends BaseWebTestCase /** * @var Client */ - protected $client; + protected static $client; /** * @var ContainerInterface */ - protected $container; + protected static $container; - protected function setUp() + protected function setUp(): void { parent::setUp(); static::$class = null; - $this->client = static::createClient(); - $this->container = static::$kernel->getContainer(); + static::$client = static::createClient(); + + if (false == static::$container) { + static::$container = static::$kernel->getContainer(); + } $this->startTransaction(); } - protected function tearDown() + protected function tearDown(): void { $this->rollbackTransaction(); - parent::tearDown(); + static::ensureKernelShutdown(); } - /** - * @return string - */ - public static function getKernelClass() + public static function getKernelClass(): string { require_once __DIR__.'/app/AppKernel.php'; @@ -50,7 +50,7 @@ public static function getKernelClass() protected function startTransaction() { /** @var $em \Doctrine\ORM\EntityManager */ - foreach ($this->container->get('doctrine')->getManagers() as $em) { + foreach (static::$container->get('doctrine')->getManagers() as $em) { $em->clear(); $em->getConnection()->beginTransaction(); } @@ -58,15 +58,15 @@ protected function startTransaction() protected function rollbackTransaction() { - //the error can be thrown during setUp - //It would be caught by phpunit and tearDown called. - //In this case we could not rollback since container may not exist. - if (false == $this->container) { + // the error can be thrown during setUp + // It would be caught by phpunit and tearDown called. + // In this case we could not rollback since container may not exist. + if (false == static::$container) { return; } /** @var $em \Doctrine\ORM\EntityManager */ - foreach ($this->container->get('doctrine')->getManagers() as $em) { + foreach (static::$container->get('doctrine')->getManagers() as $em) { $connection = $em->getConnection(); while ($connection->isTransactionActive()) { diff --git a/pkg/job-queue/Tests/Functional/app/AppKernel.php b/pkg/job-queue/Tests/Functional/app/AppKernel.php index 0d60c665f..b51969f68 100644 --- a/pkg/job-queue/Tests/Functional/app/AppKernel.php +++ b/pkg/job-queue/Tests/Functional/app/AppKernel.php @@ -1,65 +1,42 @@ load(__DIR__.'/config/config.yml'); - } - - protected function getKernelParameters() - { - $parameters = parent::getKernelParameters(); + if (self::VERSION_ID < 60000) { + $loader->load(__DIR__.'/config/config-sf5.yml'); - // it works in all Symfony version, 2.8, 3.x, 4.x - $parameters['db.driver'] = getenv('DOCTRINE_DRIVER'); - $parameters['db.host'] = getenv('DOCTRINE_HOST'); - $parameters['db.port'] = getenv('DOCTRINE_PORT'); - $parameters['db.name'] = getenv('DOCTRINE_DB_NAME'); - $parameters['db.user'] = getenv('DOCTRINE_USER'); - $parameters['db.password'] = getenv('DOCTRINE_PASSWORD'); + return; + } - return $parameters; + $loader->load(__DIR__.'/config/config.yml'); } - protected function getContainerClass() + protected function getContainerClass(): string { return parent::getContainerClass().'JobQueue'; } diff --git a/pkg/job-queue/Tests/Functional/app/config/config-sf5.yml b/pkg/job-queue/Tests/Functional/app/config/config-sf5.yml new file mode 100644 index 000000000..dd3467e11 --- /dev/null +++ b/pkg/job-queue/Tests/Functional/app/config/config-sf5.yml @@ -0,0 +1,33 @@ +parameters: + locale: 'en' + secret: 'ThisTokenIsNotSoSecretChangeIt' + +framework: + #esi: ~ + #translator: { fallback: "%locale%" } + test: ~ + assets: false + session: + # the only option incompatible with Symfony 6 + storage_id: session.storage.mock_file + secret: "%secret%" + router: { resource: "%kernel.project_dir%/config/routing.yml" } + default_locale: "%locale%" + +doctrine: + dbal: + url: "%env(DOCTRINE_DSN)%" + driver: pdo_mysql + charset: UTF8 + wrapper_class: "Enqueue\\JobQueue\\Test\\DbalPersistedConnection" + orm: + auto_generate_proxy_classes: true + auto_mapping: true + mappings: + TestEntity: + mapping: true + type: annotation + dir: '%kernel.project_dir%/Tests/Functional/Entity' + alias: 'EnqueueJobQueue' + prefix: 'Enqueue\JobQueue\Tests\Functional\Entity' + is_bundle: false diff --git a/pkg/job-queue/Tests/Functional/app/config/config.yml b/pkg/job-queue/Tests/Functional/app/config/config.yml index 122fdef96..0121acdbf 100644 --- a/pkg/job-queue/Tests/Functional/app/config/config.yml +++ b/pkg/job-queue/Tests/Functional/app/config/config.yml @@ -7,21 +7,16 @@ framework: #translator: { fallback: "%locale%" } test: ~ assets: false - templating: false session: - storage_id: session.storage.mock_file + storage_factory_id: session.storage.factory.mock_file secret: "%secret%" - router: { resource: "%kernel.root_dir%/config/routing.yml" } + router: { resource: "%kernel.project_dir%/config/routing.yml" } default_locale: "%locale%" doctrine: dbal: - driver: "%db.driver%" - host: "%db.host%" - port: "%db.port%" - dbname: "%db.name%" - user: "%db.user%" - password: "%db.password%" + url: "%env(DOCTRINE_DSN)%" + driver: pdo_mysql charset: UTF8 wrapper_class: "Enqueue\\JobQueue\\Test\\DbalPersistedConnection" orm: @@ -30,8 +25,8 @@ doctrine: mappings: TestEntity: mapping: true - type: annotation - dir: '%kernel.root_dir%/../Entity' + type: attribute + dir: '%kernel.project_dir%/Tests/Functional/Entity' alias: 'EnqueueJobQueue' prefix: 'Enqueue\JobQueue\Tests\Functional\Entity' is_bundle: false diff --git a/pkg/job-queue/Tests/JobProcessorTest.php b/pkg/job-queue/Tests/JobProcessorTest.php index bb2386895..9f1c7b2fd 100644 --- a/pkg/job-queue/Tests/JobProcessorTest.php +++ b/pkg/job-queue/Tests/JobProcessorTest.php @@ -2,26 +2,23 @@ namespace Enqueue\JobQueue\Tests; -use Enqueue\Client\Producer; +use Enqueue\Client\ProducerInterface; +use Enqueue\JobQueue\Commands; use Enqueue\JobQueue\Doctrine\JobStorage; use Enqueue\JobQueue\DuplicateJobException; use Enqueue\JobQueue\Job; use Enqueue\JobQueue\JobProcessor; -use Enqueue\JobQueue\Topics; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class JobProcessorTest extends TestCase { - public function testCouldBeCreatedWithRequiredArguments() - { - new JobProcessor($this->createJobStorage(), $this->createProducerMock()); - } - public function testCreateRootJobShouldThrowIfOwnerIdIsEmpty() { $processor = new JobProcessor($this->createJobStorage(), $this->createProducerMock()); - $this->setExpectedException(\LogicException::class, 'OwnerId must not be empty'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('OwnerId must not be empty'); $processor->findOrCreateRootJob(null, 'job-name', true); } @@ -30,7 +27,8 @@ public function testCreateRootJobShouldThrowIfNameIsEmpty() { $processor = new JobProcessor($this->createJobStorage(), $this->createProducerMock()); - $this->setExpectedException(\LogicException::class, 'Job name must not be empty'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Job name must not be empty'); $processor->findOrCreateRootJob('owner-id', null, true); } @@ -43,7 +41,7 @@ public function testShouldCreateRootJobAndReturnIt() $storage ->expects($this->once()) ->method('createJob') - ->will($this->returnValue($job)) + ->willReturn($job) ; $storage ->expects($this->once()) @@ -57,8 +55,14 @@ public function testShouldCreateRootJobAndReturnIt() $this->assertSame($job, $result); $this->assertEquals(Job::STATUS_NEW, $job->getStatus()); - $this->assertEquals(new \DateTime(), $job->getCreatedAt(), '', 1); - $this->assertEquals(new \DateTime(), $job->getStartedAt(), '', 1); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $job->getCreatedAt()->getTimestamp() + ); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $job->getStartedAt()->getTimestamp() + ); $this->assertNull($job->getStoppedAt()); $this->assertEquals('job-name', $job->getName()); $this->assertEquals('owner-id', $job->getOwnerId()); @@ -72,7 +76,7 @@ public function testShouldCatchDuplicateJobAndTryToFindJobByOwnerId() $storage ->expects($this->once()) ->method('createJob') - ->will($this->returnValue($job)) + ->willReturn($job) ; $storage ->expects($this->once()) @@ -84,7 +88,7 @@ public function testShouldCatchDuplicateJobAndTryToFindJobByOwnerId() ->expects($this->once()) ->method('findRootJobByOwnerIdAndJobName') ->with('owner-id', 'job-name') - ->will($this->returnValue($job)) + ->willReturn($job) ; $processor = new JobProcessor($storage, $this->createProducerMock()); @@ -98,7 +102,8 @@ public function testCreateChildJobShouldThrowIfNameIsEmpty() { $processor = new JobProcessor($this->createJobStorage(), $this->createProducerMock()); - $this->setExpectedException(\LogicException::class, 'Job name must not be empty'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Job name must not be empty'); $processor->findOrCreateChildJob(null, new Job()); } @@ -121,13 +126,13 @@ public function testCreateChildJobShouldFindAndReturnAlreadyCreatedJob() ->expects($this->once()) ->method('findChildJobByName') ->with('job-name', $this->identicalTo($job)) - ->will($this->returnValue($job)) + ->willReturn($job) ; $storage ->expects($this->once()) ->method('findJobById') ->with(123) - ->will($this->returnValue($job)) + ->willReturn($job) ; $processor = new JobProcessor($storage, $this->createProducerMock()); @@ -146,7 +151,7 @@ public function testCreateChildJobShouldCreateAndSaveJobAndPublishRecalculateRoo $storage ->expects($this->once()) ->method('createJob') - ->will($this->returnValue($job)) + ->willReturn($job) ; $storage ->expects($this->once()) @@ -157,20 +162,20 @@ public function testCreateChildJobShouldCreateAndSaveJobAndPublishRecalculateRoo ->expects($this->once()) ->method('findChildJobByName') ->with('job-name', $this->identicalTo($job)) - ->will($this->returnValue(null)) + ->willReturn(null) ; $storage ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $producer = $this->createProducerMock(); $producer ->expects($this->once()) - ->method('sendEvent') - ->with(Topics::CALCULATE_ROOT_JOB_STATUS, ['jobId' => 12345]) + ->method('sendCommand') + ->with(Commands::CALCULATE_ROOT_JOB_STATUS, ['jobId' => 12345]) ; $processor = new JobProcessor($storage, $producer); @@ -179,7 +184,10 @@ public function testCreateChildJobShouldCreateAndSaveJobAndPublishRecalculateRoo $this->assertSame($job, $result); $this->assertEquals(Job::STATUS_NEW, $job->getStatus()); - $this->assertEquals(new \DateTime(), $job->getCreatedAt(), '', 1); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $job->getCreatedAt()->getTimestamp() + ); $this->assertNull($job->getStartedAt()); $this->assertNull($job->getStoppedAt()); $this->assertEquals('job-name', $job->getName()); @@ -193,7 +201,8 @@ public function testStartChildJobShouldThrowIfRootJob() $rootJob = new Job(); $rootJob->setId(12345); - $this->setExpectedException(\LogicException::class, 'Can\'t start root jobs. id: "12345"'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Can\'t start root jobs. id: "12345"'); $processor->startChildJob($rootJob); } @@ -210,15 +219,13 @@ public function testStartChildJobShouldThrowIfJobHasNotNewStatus() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $processor = new JobProcessor($storage, $this->createProducerMock()); - $this->setExpectedException( - \LogicException::class, - 'Can start only new jobs: id: "12345", status: "enqueue.job_queue.status.cancelled"' - ); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Can start only new jobs: id: "12345", status: "enqueue.job_queue.status.cancelled"'); $processor->startChildJob($job); } @@ -240,20 +247,23 @@ public function testStartJobShouldUpdateJobWithRunningStatusAndStartAtTime() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $producer = $this->createProducerMock(); $producer ->expects($this->once()) - ->method('sendEvent') + ->method('sendCommand') ; $processor = new JobProcessor($storage, $producer); $processor->startChildJob($job); $this->assertEquals(Job::STATUS_RUNNING, $job->getStatus()); - $this->assertEquals(new \DateTime(), $job->getStartedAt(), '', 1); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $job->getStartedAt()->getTimestamp() + ); } public function testSuccessChildJobShouldThrowIfRootJob() @@ -263,7 +273,8 @@ public function testSuccessChildJobShouldThrowIfRootJob() $rootJob = new Job(); $rootJob->setId(12345); - $this->setExpectedException(\LogicException::class, 'Can\'t success root jobs. id: "12345"'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Can\'t success root jobs. id: "12345"'); $processor->successChildJob($rootJob); } @@ -280,15 +291,13 @@ public function testSuccessChildJobShouldThrowIfJobHasNotRunningStatus() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $processor = new JobProcessor($storage, $this->createProducerMock()); - $this->setExpectedException( - \LogicException::class, - 'Can success only running jobs. id: "12345", status: "enqueue.job_queue.status.cancelled"' - ); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Can success only running jobs. id: "12345", status: "enqueue.job_queue.status.cancelled"'); $processor->successChildJob($job); } @@ -310,20 +319,23 @@ public function testSuccessJobShouldUpdateJobWithSuccessStatusAndStopAtTime() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $producer = $this->createProducerMock(); $producer ->expects($this->once()) - ->method('sendEvent') + ->method('sendCommand') ; $processor = new JobProcessor($storage, $producer); $processor->successChildJob($job); $this->assertEquals(Job::STATUS_SUCCESS, $job->getStatus()); - $this->assertEquals(new \DateTime(), $job->getStoppedAt(), '', 1); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $job->getStoppedAt()->getTimestamp() + ); } public function testFailChildJobShouldThrowIfRootJob() @@ -333,7 +345,8 @@ public function testFailChildJobShouldThrowIfRootJob() $rootJob = new Job(); $rootJob->setId(12345); - $this->setExpectedException(\LogicException::class, 'Can\'t fail root jobs. id: "12345"'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Can\'t fail root jobs. id: "12345"'); $processor->failChildJob($rootJob); } @@ -350,15 +363,13 @@ public function testFailChildJobShouldThrowIfJobHasNotRunningStatus() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $processor = new JobProcessor($storage, $this->createProducerMock()); - $this->setExpectedException( - \LogicException::class, - 'Can fail only running jobs. id: "12345", status: "enqueue.job_queue.status.cancelled"' - ); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Can fail only running jobs. id: "12345", status: "enqueue.job_queue.status.cancelled"'); $processor->failChildJob($job); } @@ -380,20 +391,23 @@ public function testFailJobShouldUpdateJobWithFailStatusAndStopAtTime() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $producer = $this->createProducerMock(); $producer ->expects($this->once()) - ->method('sendEvent') + ->method('sendCommand') ; $processor = new JobProcessor($storage, $producer); $processor->failChildJob($job); $this->assertEquals(Job::STATUS_FAILED, $job->getStatus()); - $this->assertEquals(new \DateTime(), $job->getStoppedAt(), '', 1); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $job->getStoppedAt()->getTimestamp() + ); } public function testCancelChildJobShouldThrowIfRootJob() @@ -403,7 +417,8 @@ public function testCancelChildJobShouldThrowIfRootJob() $rootJob = new Job(); $rootJob->setId(12345); - $this->setExpectedException(\LogicException::class, 'Can\'t cancel root jobs. id: "12345"'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Can\'t cancel root jobs. id: "12345"'); $processor->cancelChildJob($rootJob); } @@ -420,15 +435,13 @@ public function testCancelChildJobShouldThrowIfJobHasNotNewOrRunningStatus() ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $processor = new JobProcessor($storage, $this->createProducerMock()); - $this->setExpectedException( - \LogicException::class, - 'Can cancel only new or running jobs. id: "12345", status: "enqueue.job_queue.status.cancelled"' - ); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Can cancel only new or running jobs. id: "12345", status: "enqueue.job_queue.status.cancelled"'); $processor->cancelChildJob($job); } @@ -450,21 +463,27 @@ public function testCancelJobShouldUpdateJobWithCancelStatusAndStoppedAtTimeAndS ->expects($this->once()) ->method('findJobById') ->with(12345) - ->will($this->returnValue($job)) + ->willReturn($job) ; $producer = $this->createProducerMock(); $producer ->expects($this->once()) - ->method('sendEvent') + ->method('sendCommand') ; $processor = new JobProcessor($storage, $producer); $processor->cancelChildJob($job); $this->assertEquals(Job::STATUS_CANCELLED, $job->getStatus()); - $this->assertEquals(new \DateTime(), $job->getStoppedAt(), '', 1); - $this->assertEquals(new \DateTime(), $job->getStartedAt(), '', 1); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $job->getStoppedAt()->getTimestamp() + ); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $job->getStartedAt()->getTimestamp() + ); } public function testInterruptRootJobShouldThrowIfNotRootJob() @@ -475,7 +494,8 @@ public function testInterruptRootJobShouldThrowIfNotRootJob() $processor = new JobProcessor($this->createJobStorage(), $this->createProducerMock()); - $this->setExpectedException(\LogicException::class, 'Can interrupt only root jobs. id: "123"'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Can interrupt only root jobs. id: "123"'); $processor->interruptRootJob($notRootJob); } @@ -505,9 +525,9 @@ public function testInterruptRootJobShouldUpdateJobAndSetInterruptedTrue() $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $processor = new JobProcessor($storage, $this->createProducerMock()); @@ -526,31 +546,34 @@ public function testInterruptRootJobShouldUpdateJobAndSetInterruptedTrueAndStopp $storage ->expects($this->once()) ->method('saveJob') - ->will($this->returnCallback(function (Job $job, $callback) { + ->willReturnCallback(function (Job $job, $callback) { $callback($job); - })) + }) ; $processor = new JobProcessor($storage, $this->createProducerMock()); $processor->interruptRootJob($rootJob, true); $this->assertTrue($rootJob->isInterrupted()); - $this->assertEquals(new \DateTime(), $rootJob->getStoppedAt(), '', 1); + $this->assertEquals( + (new \DateTime())->getTimestamp(), + $rootJob->getStoppedAt()->getTimestamp() + ); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|JobStorage + * @return MockObject */ - private function createJobStorage() + private function createJobStorage(): JobStorage { return $this->createMock(JobStorage::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Producer + * @return MockObject */ - private function createProducerMock() + private function createProducerMock(): ProducerInterface { - return $this->createMock(Producer::class); + return $this->createMock(ProducerInterface::class); } } diff --git a/pkg/job-queue/Tests/JobRunnerTest.php b/pkg/job-queue/Tests/JobRunnerTest.php index 608742491..e43440fe3 100644 --- a/pkg/job-queue/Tests/JobRunnerTest.php +++ b/pkg/job-queue/Tests/JobRunnerTest.php @@ -5,6 +5,8 @@ use Enqueue\JobQueue\Job; use Enqueue\JobQueue\JobProcessor; use Enqueue\JobQueue\JobRunner; +use Enqueue\JobQueue\OrphanJobException; +use PHPUnit\Framework\MockObject\MockObject; class JobRunnerTest extends \PHPUnit\Framework\TestCase { @@ -18,13 +20,13 @@ public function testRunUniqueShouldCreateRootAndChildJobAndCallCallback() ->expects($this->once()) ->method('findOrCreateRootJob') ->with('owner-id', 'job-name', true) - ->will($this->returnValue($root)) + ->willReturn($root) ; $jobProcessor ->expects($this->once()) ->method('findOrCreateChildJob') ->with('job-name') - ->will($this->returnValue($child)) + ->willReturn($child) ; $expChild = null; @@ -56,12 +58,12 @@ public function testRunUniqueShouldStartChildJobIfNotStarted() $jobProcessor ->expects($this->once()) ->method('findOrCreateRootJob') - ->will($this->returnValue($root)) + ->willReturn($root) ; $jobProcessor ->expects($this->once()) ->method('findOrCreateChildJob') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->once()) @@ -84,12 +86,12 @@ public function testRunUniqueShouldNotStartChildJobIfAlreadyStarted() $jobProcessor ->expects($this->once()) ->method('findOrCreateRootJob') - ->will($this->returnValue($root)) + ->willReturn($root) ; $jobProcessor ->expects($this->once()) ->method('findOrCreateChildJob') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->never()) @@ -110,12 +112,12 @@ public function testRunUniqueShouldSuccessJobIfCallbackReturnValueIsTrue() $jobProcessor ->expects($this->once()) ->method('findOrCreateRootJob') - ->will($this->returnValue($root)) + ->willReturn($root) ; $jobProcessor ->expects($this->once()) ->method('findOrCreateChildJob') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->once()) @@ -141,12 +143,12 @@ public function testRunUniqueShouldFailJobIfCallbackReturnValueIsFalse() $jobProcessor ->expects($this->once()) ->method('findOrCreateRootJob') - ->will($this->returnValue($root)) + ->willReturn($root) ; $jobProcessor ->expects($this->once()) ->method('findOrCreateChildJob') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->never()) @@ -172,12 +174,12 @@ public function testRunUniqueShouldFailJobIfCallbackThrowsException() $jobProcessor ->expects($this->once()) ->method('findOrCreateRootJob') - ->will($this->returnValue($root)) + ->willReturn($root) ; $jobProcessor ->expects($this->once()) ->method('findOrCreateChildJob') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->never()) @@ -195,6 +197,39 @@ public function testRunUniqueShouldFailJobIfCallbackThrowsException() }); } + public function testRunUniqueShouldThrowOrphanJobExceptionIfChildCleanupFails() + { + $root = new Job(); + $child = new Job(); + + $jobProcessor = $this->createJobProcessorMock(); + $jobProcessor + ->expects($this->once()) + ->method('findOrCreateRootJob') + ->willReturn($root) + ; + $jobProcessor + ->expects($this->once()) + ->method('findOrCreateChildJob') + ->willReturn($child) + ; + $jobProcessor + ->expects($this->never()) + ->method('successChildJob') + ; + $jobProcessor + ->expects($this->once()) + ->method('failChildJob') + ->willThrowException(new \Exception()) + ; + + $jobRunner = new JobRunner($jobProcessor); + $this->expectException(OrphanJobException::class); + $jobRunner->runUnique('owner-id', 'job-name', function () { + throw new \Exception(); + }); + } + public function testRunUniqueShouldNotSuccessJobIfJobIsAlreadyStopped() { $root = new Job(); @@ -205,12 +240,12 @@ public function testRunUniqueShouldNotSuccessJobIfJobIsAlreadyStopped() $jobProcessor ->expects($this->once()) ->method('findOrCreateRootJob') - ->will($this->returnValue($root)) + ->willReturn($root) ; $jobProcessor ->expects($this->once()) ->method('findOrCreateChildJob') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->never()) @@ -237,7 +272,7 @@ public function testCreateDelayedShouldCreateChildJobAndCallCallback() ->expects($this->once()) ->method('findOrCreateChildJob') ->with('job-name', $this->identicalTo($root)) - ->will($this->returnValue($child)) + ->willReturn($child) ; $expRunner = null; @@ -262,12 +297,13 @@ public function testRunDelayedShouldThrowExceptionIfJobWasNotFoundById() ->expects($this->once()) ->method('findJobById') ->with('job-id') - ->will($this->returnValue(null)) + ->willReturn(null) ; $jobRunner = new JobRunner($jobProcessor); - $this->setExpectedException(\LogicException::class, 'Job was not found. id: "job-id"'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Job was not found. id: "job-id"'); $jobRunner->runDelayed('job-id', function () { }); @@ -284,7 +320,7 @@ public function testRunDelayedShouldFindJobAndCallCallback() ->expects($this->once()) ->method('findJobById') ->with('job-id') - ->will($this->returnValue($child)) + ->willReturn($child) ; $expRunner = null; @@ -314,7 +350,7 @@ public function testRunDelayedShouldCancelJobIfRootJobIsInterrupted() ->expects($this->once()) ->method('findJobById') ->with('job-id') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->once()) @@ -339,7 +375,7 @@ public function testRunDelayedShouldSuccessJobIfCallbackReturnValueIsTrue() ->expects($this->once()) ->method('findJobById') ->with('job-id') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->once()) @@ -368,7 +404,7 @@ public function testRunDelayedShouldFailJobIfCallbackReturnValueIsFalse() ->expects($this->once()) ->method('findJobById') ->with('job-id') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->never()) @@ -398,7 +434,7 @@ public function testRunDelayedShouldNotSuccessJobIfAlreadyStopped() ->expects($this->once()) ->method('findJobById') ->with('job-id') - ->will($this->returnValue($child)) + ->willReturn($child) ; $jobProcessor ->expects($this->never()) @@ -416,7 +452,7 @@ public function testRunDelayedShouldNotSuccessJobIfAlreadyStopped() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|JobProcessor + * @return MockObject|JobProcessor */ private function createJobProcessorMock() { diff --git a/pkg/job-queue/Topics.php b/pkg/job-queue/Topics.php index 664fa0b1c..891ea26f7 100644 --- a/pkg/job-queue/Topics.php +++ b/pkg/job-queue/Topics.php @@ -4,6 +4,5 @@ class Topics { - const CALCULATE_ROOT_JOB_STATUS = 'enqueue.message_queue.job.calculate_root_job_status'; - const ROOT_JOB_STOPPED = 'enqueue.message_queue.job.root_job_stopped'; + public const ROOT_JOB_STOPPED = 'enqueue.message_queue.job.root_job_stopped'; } diff --git a/pkg/job-queue/composer.json b/pkg/job-queue/composer.json index 2361a308e..6616069b3 100644 --- a/pkg/job-queue/composer.json +++ b/pkg/job-queue/composer.json @@ -6,18 +6,21 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "doctrine/orm": "~2.4" + "php": "^8.1", + "enqueue/enqueue": "^0.10", + "enqueue/null": "^0.10", + "queue-interop/queue-interop": "^0.8", + "doctrine/orm": "^2.12", + "doctrine/dbal": "^2.12 | ^3.0" }, "require-dev": { - "phpunit/phpunit": "~5.5", - "enqueue/test": "^0.8@dev", - "doctrine/doctrine-bundle": "~1.2", - "symfony/browser-kit": "^2.8|^3|^4", - "symfony/expression-language": "^2.8|^3|^4", - "symfony/framework-bundle": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "doctrine/doctrine-bundle": "^2.3.2", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/framework-bundle": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" }, "support": { "email": "opensource@forma-pro.com", @@ -35,7 +38,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/job-queue/phpunit.xml.dist b/pkg/job-queue/phpunit.xml.dist index 29dc33404..3665922c4 100644 --- a/pkg/job-queue/phpunit.xml.dist +++ b/pkg/job-queue/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/mongodb/.github/workflows/ci.yml b/pkg/mongodb/.github/workflows/ci.yml new file mode 100644 index 000000000..415baf634 --- /dev/null +++ b/pkg/mongodb/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: mongodb + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/mongodb/.travis.yml b/pkg/mongodb/.travis.yml deleted file mode 100644 index 6415d29e8..000000000 --- a/pkg/mongodb/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -sudo: false - -language: php - -php: - - '7.1' - -git: - depth: 10 - -cache: - directories: - - $HOME/.composer/cache - -services: - - mongodb - -before_install: - - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional - diff --git a/pkg/mongodb/Client/MongodbDriver.php b/pkg/mongodb/Client/MongodbDriver.php deleted file mode 100644 index aa0d4f999..000000000 --- a/pkg/mongodb/Client/MongodbDriver.php +++ /dev/null @@ -1,186 +0,0 @@ - 0, - MessagePriority::LOW => 1, - MessagePriority::NORMAL => 2, - MessagePriority::HIGH => 3, - MessagePriority::VERY_HIGH => 4, - ]; - - /** - * @param MongodbContext $context - * @param Config $config - * @param QueueMetaRegistry $queueMetaRegistry - */ - public function __construct(MongodbContext $context, Config $config, QueueMetaRegistry $queueMetaRegistry) - { - $this->context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - * - * @return MongodbMessage - */ - public function createTransportMessage(Message $message) - { - $properties = $message->getProperties(); - - $headers = $message->getHeaders(); - $headers['content_type'] = $message->getContentType(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($properties); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setDeliveryDelay($message->getDelay()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - if (array_key_exists($message->getPriority(), self::$priorityMap)) { - $transportMessage->setPriority(self::$priorityMap[$message->getPriority()]); - } - - return $transportMessage; - } - - /** - * @param MongodbMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - - $clientMessage->setContentType($message->getHeader('content_type')); - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setDelay($message->getDeliveryDelay()); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - $priorityMap = array_flip(self::$priorityMap); - $priority = array_key_exists($message->getPriority(), $priorityMap) ? - $priorityMap[$message->getPriority()] : - MessagePriority::NORMAL; - $clientMessage->setPriority($priority); - - return $clientMessage; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $queue = $this->createQueue($this->config->getRouterQueueName()); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($queue, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - return $this->context->createQueue($transportName); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $log = function ($text, ...$args) use ($logger) { - $logger->debug(sprintf('[MongodbDriver] '.$text, ...$args)); - }; - $contextConfig = $this->context->getConfig(); - $log('Creating database and collection: "%s" "%s"', $contextConfig['dbname'], $contextConfig['collection_name']); - $this->context->createCollection(); - } - - /** - * {@inheritdoc} - */ - public function getConfig() - { - return $this->config; - } - - /** - * @return array - */ - public static function getPriorityMap() - { - return self::$priorityMap; - } -} diff --git a/pkg/mongodb/JSON.php b/pkg/mongodb/JSON.php index 84cac50da..481b7f9ff 100644 --- a/pkg/mongodb/JSON.php +++ b/pkg/mongodb/JSON.php @@ -14,10 +14,7 @@ class JSON public static function decode($string) { if (!is_string($string)) { - throw new \InvalidArgumentException(sprintf( - 'Accept only string argument but got: "%s"', - is_object($string) ? get_class($string) : gettype($string) - )); + throw new \InvalidArgumentException(sprintf('Accept only string argument but got: "%s"', is_object($string) ? $string::class : gettype($string))); } // PHP7 fix - empty string and null cause syntax error @@ -26,32 +23,22 @@ public static function decode($string) } $decoded = json_decode($string, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return $decoded; } /** - * @param mixed $value - * * @return string */ public static function encode($value) { - $encoded = json_encode($value, JSON_UNESCAPED_UNICODE); - - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'Could not encode value into json. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + $encoded = json_encode($value, \JSON_UNESCAPED_UNICODE); + + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('Could not encode value into json. Error %s and message %s', json_last_error(), json_last_error_msg())); } return $encoded; diff --git a/pkg/mongodb/MongodbConnectionFactory.php b/pkg/mongodb/MongodbConnectionFactory.php index 1bc2c2db1..3d34f7369 100644 --- a/pkg/mongodb/MongodbConnectionFactory.php +++ b/pkg/mongodb/MongodbConnectionFactory.php @@ -1,11 +1,14 @@ parseDsn($config); } elseif (is_array($config)) { - $config = $this->parseDsn(empty($config['dsn']) ? 'mongodb:' : $config['dsn']); + $config = array_replace( + $config, + $this->parseDsn(empty($config['dsn']) ? 'mongodb:' : $config['dsn']) + ); } else { throw new \LogicException('The config must be either an array of options, a DSN string or null'); } @@ -48,14 +54,17 @@ public function __construct($config = 'mongodb:') $this->config = $config; } - public function createContext() + /** + * @return MongodbContext + */ + public function createContext(): Context { $client = new Client($this->config['dsn']); return new MongodbContext($client, $this->config); } - public static function parseDsn($dsn) + public static function parseDsn(string $dsn): array { $parsedUrl = parse_url(http://github.com/$dsn); if (false === $parsedUrl) { @@ -68,11 +77,7 @@ public static function parseDsn($dsn) 'mongodb' => true, ]; if (false == isset($parsedUrl['scheme'])) { - throw new \LogicException(sprintf( - 'The given DSN schema "%s" is not supported. There are supported schemes: "%s".', - $parsedUrl['scheme'], - implode('", "', array_keys($supported)) - )); + throw new \LogicException(sprintf('The given DSN schema "%s" is not supported. There are supported schemes: "%s".', $parsedUrl['scheme'], implode('", "', array_keys($supported)))); } if ('mongodb:' === $dsn) { return [ @@ -80,9 +85,11 @@ public static function parseDsn($dsn) ]; } $config['dsn'] = $dsn; + // FIXME this is NOT a dbname but rather authdb. But removing this would be a BC break. + // see: https://github.com/php-enqueue/enqueue-dev/issues/1027 if (isset($parsedUrl['path']) && '/' !== $parsedUrl['path']) { $pathParts = explode('/', $parsedUrl['path']); - //DB name + // DB name if ($pathParts[1]) { $config['dbname'] = $pathParts[1]; } @@ -90,13 +97,16 @@ public static function parseDsn($dsn) if (isset($parsedUrl['query'])) { $queryParts = null; parse_str($parsedUrl['query'], $queryParts); - //get enqueue attributes values + // get enqueue attributes values if (!empty($queryParts['polling_interval'])) { - $config['polling_interval'] = $queryParts['polling_interval']; + $config['polling_interval'] = (int) $queryParts['polling_interval']; } if (!empty($queryParts['enqueue_collection'])) { $config['collection_name'] = $queryParts['enqueue_collection']; } + if (!empty($queryParts['enqueue_database'])) { + $config['dbname'] = $queryParts['enqueue_database']; + } } return $config; diff --git a/pkg/mongodb/MongodbConsumer.php b/pkg/mongodb/MongodbConsumer.php index e3ef36895..37ef12530 100644 --- a/pkg/mongodb/MongodbConsumer.php +++ b/pkg/mongodb/MongodbConsumer.php @@ -1,12 +1,15 @@ context = $context; $this->queue = $queue; + + $this->pollingInterval = 1000; } /** * Set polling interval in milliseconds. - * - * @param int $msec */ - public function setPollingInterval($msec) + public function setPollingInterval(int $msec): void { - $this->pollingInterval = $msec * 1000; + $this->pollingInterval = $msec; } /** * Get polling interval in milliseconds. - * - * @return int */ - public function getPollingInterval() + public function getPollingInterval(): int { - return (int) $this->pollingInterval / 1000; + return $this->pollingInterval; } /** - * {@inheritdoc} - * * @return MongodbDestination */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } /** - * {@inheritdoc} - * - * @return MongodbMessage|null + * @return MongodbMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { $timeout /= 1000; $startAt = microtime(true); @@ -81,57 +74,49 @@ public function receive($timeout = 0) } if ($timeout && (microtime(true) - $startAt) >= $timeout) { - return; + return null; } - usleep($this->pollingInterval); + usleep($this->pollingInterval * 1000); if ($timeout && (microtime(true) - $startAt) >= $timeout) { - return; + return null; } } } /** - * {@inheritdoc} - * - * @return MongodbMessage|null + * @return MongodbMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { return $this->receiveMessage(); } /** - * {@inheritdoc} - * * @param MongodbMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { // does nothing } /** - * {@inheritdoc} - * * @param MongodbMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { InvalidMessageException::assertMessageInstanceOf($message, MongodbMessage::class); if ($requeue) { + $message->setRedelivered(true); $this->context->createProducer()->send($this->queue, $message); return; } } - /** - * @return MongodbMessage|null - */ - protected function receiveMessage() + private function receiveMessage(): ?MongodbMessage { $now = time(); $collection = $this->context->getCollection(); @@ -153,26 +138,9 @@ protected function receiveMessage() return null; } if (empty($message['time_to_live']) || $message['time_to_live'] > time()) { - return $this->convertMessage($message); + return $this->context->convertMessage($message); } - } - - /** - * @param array $dbalMessage - * - * @return MongodbMessage - */ - protected function convertMessage(array $mongodbMessage) - { - $properties = JSON::decode($mongodbMessage['properties']); - $headers = JSON::decode($mongodbMessage['headers']); - - $message = $this->context->createMessage($mongodbMessage['body'], $properties, $headers); - $message->setId((string) $mongodbMessage['_id']); - $message->setPriority((int) $mongodbMessage['priority']); - $message->setRedelivered((bool) $mongodbMessage['redelivered']); - $message->setPublishedAt((int) $mongodbMessage['published_at']); - return $message; + return null; } } diff --git a/pkg/mongodb/MongodbContext.php b/pkg/mongodb/MongodbContext.php index ce8945e53..2e52ebdb2 100644 --- a/pkg/mongodb/MongodbContext.php +++ b/pkg/mongodb/MongodbContext.php @@ -1,13 +1,23 @@ client = $client; } - public function createMessage($body = '', array $properties = [], array $headers = []) + /** + * @return MongodbMessage + */ + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { $message = new MongodbMessage(); $message->setBody($body); @@ -40,27 +53,41 @@ public function createMessage($body = '', array $properties = [], array $headers return $message; } - public function createTopic($name) + /** + * @return MongodbDestination + */ + public function createTopic(string $name): Topic { return new MongodbDestination($name); } - public function createQueue($queueName) + /** + * @return MongodbDestination + */ + public function createQueue(string $queueName): Queue { return new MongodbDestination($queueName); } - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { - throw new \BadMethodCallException('Mongodb transport does not support temporary queues'); + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); } - public function createProducer() + /** + * @return MongodbProducer + */ + public function createProducer(): Producer { return new MongodbProducer($this); } - public function createConsumer(PsrDestination $destination) + /** + * @param MongodbDestination $destination + * + * @return MongodbConsumer + */ + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, MongodbDestination::class); @@ -73,38 +100,67 @@ public function createConsumer(PsrDestination $destination) return $consumer; } - public function close() + public function close(): void { - // TODO: Implement close() method. } - public function getCollection() + public function createSubscriptionConsumer(): SubscriptionConsumer { - return $this->client - ->selectDatabase($this->config['dbname']) - ->selectCollection($this->config['collection_name']); + return new MongodbSubscriptionConsumer($this); } /** - * @return Client + * @internal It must be used here and in the consumer only */ - public function getClient() + public function convertMessage(array $mongodbMessage): MongodbMessage { - return $this->client; + $mongodbMessageObj = $this->createMessage( + $mongodbMessage['body'], + JSON::decode($mongodbMessage['properties']), + JSON::decode($mongodbMessage['headers']) + ); + + $mongodbMessageObj->setId((string) $mongodbMessage['_id']); + $mongodbMessageObj->setPriority((int) $mongodbMessage['priority']); + $mongodbMessageObj->setRedelivered((bool) $mongodbMessage['redelivered']); + $mongodbMessageObj->setPublishedAt((int) $mongodbMessage['published_at']); + + return $mongodbMessageObj; } /** - * @return array + * @param MongodbDestination $queue */ - public function getConfig() + public function purgeQueue(Queue $queue): void + { + $this->getCollection()->deleteMany([ + 'queue' => $queue->getQueueName(), + ]); + } + + public function getCollection(): Collection + { + return $this->client + ->selectDatabase($this->config['dbname']) + ->selectCollection($this->config['collection_name']); + } + + public function getClient(): Client + { + return $this->client; + } + + public function getConfig(): array { return $this->config; } - public function createCollection() + public function createCollection(): void { $collection = $this->getCollection(); + $collection->createIndex(['queue' => 1], ['name' => 'enqueue_queue']); $collection->createIndex(['priority' => -1, 'published_at' => 1], ['name' => 'enqueue_priority']); $collection->createIndex(['delayed_until' => 1], ['name' => 'enqueue_delayed']); + $collection->createIndex(['queue' => 1, 'priority' => -1, 'published_at' => 1, 'delayed_until' => 1], ['name' => 'enqueue_combined']); } } diff --git a/pkg/mongodb/MongodbDestination.php b/pkg/mongodb/MongodbDestination.php index 360f58a25..06653b4b9 100644 --- a/pkg/mongodb/MongodbDestination.php +++ b/pkg/mongodb/MongodbDestination.php @@ -1,46 +1,35 @@ destinationName = $name; } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { return $this->destinationName; } - /** - * Alias for getQueueName() - * {@inheritdoc} - */ - public function getName() + public function getTopicName(): string { - return $this->getQueueName(); + return $this->destinationName; } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getName(): string { return $this->destinationName; } diff --git a/pkg/mongodb/MongodbMessage.php b/pkg/mongodb/MongodbMessage.php index 9a10e5f7e..fadc5dd4e 100644 --- a/pkg/mongodb/MongodbMessage.php +++ b/pkg/mongodb/MongodbMessage.php @@ -1,10 +1,12 @@ body = $body; $this->properties = $properties; @@ -68,248 +65,163 @@ public function __construct($body = '', array $properties = [], array $headers = $this->redelivered = false; } - /** - * @param string $id - */ - public function setId($id) + public function setId(?string $id = null): void { $this->id = $id; } - /** - * @return string $id - */ - public function getId() + public function getId(): ?string { return $this->id; } - /** - * @param string $body - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * {@inheritdoc} - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * @param array $headers - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * {@inheritdoc} - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * {@inheritdoc} - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = $redelivered; } - /** - * {@inheritdoc} - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * {@inheritdoc} - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } - /** - * @return int - */ - public function getPriority() + public function getPriority(): ?int { return $this->priority; } - /** - * @param int $priority - */ - public function setPriority($priority) + public function setPriority(?int $priority = null): void { $this->priority = $priority; } - /** - * @return int - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return $this->deliveryDelay; } /** - * Set delay in milliseconds. - * - * @param int $deliveryDelay + * In milliseconds. */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): void { $this->deliveryDelay = $deliveryDelay; } - /** - * @return int|float|null - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return $this->timeToLive; } /** - * Set time to live in milliseconds. - * - * @param int|float|null $timeToLive + * In milliseconds. */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): void { $this->timeToLive = $timeToLive; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id', null); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id', null); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } - /** - * @return int - */ - public function getPublishedAt() + public function getPublishedAt(): ?int { return $this->publishedAt; } /** - * @param int $publishedAt + * In milliseconds. */ - public function setPublishedAt($publishedAt) + public function setPublishedAt(?int $publishedAt = null): void { $this->publishedAt = $publishedAt; } diff --git a/pkg/mongodb/MongodbProducer.php b/pkg/mongodb/MongodbProducer.php index c5132b62c..ed28a6681 100644 --- a/pkg/mongodb/MongodbProducer.php +++ b/pkg/mongodb/MongodbProducer.php @@ -1,15 +1,17 @@ context = $context; } /** - * {@inheritdoc} - * * @param MongodbDestination $destination * @param MongodbMessage $message - * - * @throws Exception */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, MongodbDestination::class); InvalidMessageException::assertMessageInstanceOf($message, MongodbMessage::class); @@ -63,14 +58,6 @@ public function send(PsrDestination $destination, PsrMessage $message) } $body = $message->getBody(); - if (is_scalar($body) || null === $body) { - $body = (string) $body; - } else { - throw new InvalidMessageException(sprintf( - 'The message body must be a scalar or null. Got: %s', - is_object($body) ? get_class($body) : gettype($body) - )); - } $publishedAt = null !== $message->getPublishedAt() ? $message->getPublishedAt() : @@ -90,10 +77,7 @@ public function send(PsrDestination $destination, PsrMessage $message) $delay = $message->getDeliveryDelay(); if ($delay) { if (!is_int($delay)) { - throw new \LogicException(sprintf( - 'Delay must be integer but got: "%s"', - is_object($delay) ? get_class($delay) : gettype($delay) - )); + throw new \LogicException(sprintf('Delay must be integer but got: "%s"', is_object($delay) ? $delay::class : gettype($delay))); } if ($delay <= 0) { @@ -106,10 +90,7 @@ public function send(PsrDestination $destination, PsrMessage $message) $timeToLive = $message->getTimeToLive(); if ($timeToLive) { if (!is_int($timeToLive)) { - throw new \LogicException(sprintf( - 'TimeToLive must be integer but got: "%s"', - is_object($timeToLive) ? get_class($timeToLive) : gettype($timeToLive) - )); + throw new \LogicException(sprintf('TimeToLive must be integer but got: "%s"', is_object($timeToLive) ? $timeToLive::class : gettype($timeToLive))); } if ($timeToLive <= 0) { @@ -123,58 +104,51 @@ public function send(PsrDestination $destination, PsrMessage $message) $collection = $this->context->getCollection(); $collection->insertOne($mongoMessage); } catch (\Exception $e) { - throw new Exception('The transport has failed to send the message due to some internal error.', null, $e); + throw new Exception('The transport has failed to send the message due to some internal error.', $e->getCode(), $e); } } /** - * {@inheritdoc} + * @return self */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { $this->deliveryDelay = $deliveryDelay; return $this; } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return $this->deliveryDelay; } /** - * {@inheritdoc} + * @return self */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { $this->priority = $priority; return $this; } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return $this->priority; } /** - * {@inheritdoc} + * @return self */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { $this->timeToLive = $timeToLive; + + return $this; } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return $this->timeToLive; } diff --git a/pkg/mongodb/MongodbSubscriptionConsumer.php b/pkg/mongodb/MongodbSubscriptionConsumer.php new file mode 100644 index 000000000..9fa6245f4 --- /dev/null +++ b/pkg/mongodb/MongodbSubscriptionConsumer.php @@ -0,0 +1,133 @@ +context = $context; + $this->subscribers = []; + } + + public function consume(int $timeout = 0): void + { + if (empty($this->subscribers)) { + throw new \LogicException('No subscribers'); + } + + $timeout = (int) ceil($timeout / 1000); + $endAt = time() + $timeout; + + $queueNames = []; + foreach (array_keys($this->subscribers) as $queueName) { + $queueNames[$queueName] = $queueName; + } + + $currentQueueNames = []; + while (true) { + if (empty($currentQueueNames)) { + $currentQueueNames = $queueNames; + } + + $result = $this->context->getCollection()->findOneAndDelete( + [ + 'queue' => ['$in' => array_keys($currentQueueNames)], + '$or' => [ + ['delayed_until' => ['$exists' => false]], + ['delayed_until' => ['$lte' => time()]], + ], + ], + [ + 'sort' => ['priority' => -1, 'published_at' => 1], + 'typeMap' => ['root' => 'array', 'document' => 'array'], + ] + ); + + if ($result) { + list($consumer, $callback) = $this->subscribers[$result['queue']]; + + $message = $this->context->convertMessage($result); + + if (false === call_user_func($callback, $message, $consumer)) { + return; + } + + unset($currentQueueNames[$result['queue']]); + } else { + $currentQueueNames = []; + + usleep(200000); // 200ms + } + + if ($timeout && microtime(true) >= $endAt) { + return; + } + } + } + + /** + * @param MongodbConsumer $consumer + */ + public function subscribe(Consumer $consumer, callable $callback): void + { + if (false == $consumer instanceof MongodbConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', MongodbConsumer::class, $consumer::class)); + } + + $queueName = $consumer->getQueue()->getQueueName(); + if (array_key_exists($queueName, $this->subscribers)) { + if ($this->subscribers[$queueName][0] === $consumer && $this->subscribers[$queueName][1] === $callback) { + return; + } + + throw new \InvalidArgumentException(sprintf('There is a consumer subscribed to queue: "%s"', $queueName)); + } + + $this->subscribers[$queueName] = [$consumer, $callback]; + } + + /** + * @param MongodbConsumer $consumer + */ + public function unsubscribe(Consumer $consumer): void + { + if (false == $consumer instanceof MongodbConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', MongodbConsumer::class, $consumer::class)); + } + + $queueName = $consumer->getQueue()->getQueueName(); + + if (false == array_key_exists($queueName, $this->subscribers)) { + return; + } + + if ($this->subscribers[$queueName][0] !== $consumer) { + return; + } + + unset($this->subscribers[$queueName]); + } + + public function unsubscribeAll(): void + { + $this->subscribers = []; + } +} diff --git a/pkg/mongodb/README.md b/pkg/mongodb/README.md index d29cde0f2..2e9bbd1fc 100644 --- a/pkg/mongodb/README.md +++ b/pkg/mongodb/README.md @@ -1,27 +1,36 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Mongodb Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/mongodb.png?branch=master)](https://travis-ci.org/php-enqueue/mongodb) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/mongodb/ci.yml?branch=master)](https://github.com/php-enqueue/mongodb/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/mongodb/d/total.png)](https://packagist.org/packages/enqueue/mongodb) [![Latest Stable Version](https://poser.pugx.org/enqueue/mongodb/version.png)](https://packagist.org/packages/enqueue/mongodb) - -This is an implementation of the queue specification. It allows you to use MongoDB database as a message broker. + +This is an implementation of the queue specification. It allows you to use MongoDB database as a message broker. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/mongodb/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/mongodb/Symfony/MongodbTransportFactory.php b/pkg/mongodb/Symfony/MongodbTransportFactory.php deleted file mode 100644 index 3486d8a8e..000000000 --- a/pkg/mongodb/Symfony/MongodbTransportFactory.php +++ /dev/null @@ -1,119 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifString() - ->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('dsn') - ->info('The Mongodb DSN. Other parameters are ignored if set') - ->isRequired() - ->end() - ->scalarNode('dbname') - ->defaultValue('enqueue') - ->info('Database name.') - ->end() - ->scalarNode('collection_name') - ->defaultValue('enqueue') - ->info('Collection') - ->end() - ->integerNode('polling_interval') - ->defaultValue(1000) - ->min(100) - ->info('How often query for new messages.') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factory = new Definition(MongodbConnectionFactory::class, [$config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(MongodbContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(MongodbDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/mongodb/Tests/Client/MongodbDriverTest.php b/pkg/mongodb/Tests/Client/MongodbDriverTest.php deleted file mode 100644 index 1d4bb60ad..000000000 --- a/pkg/mongodb/Tests/Client/MongodbDriverTest.php +++ /dev/null @@ -1,351 +0,0 @@ -assertClassImplements(DriverInterface::class, MongodbDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new MongodbDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = $this->createDummyConfig(); - - $driver = new MongodbDriver( - $this->createPsrContextMock(), - $config, - $this->createDummyQueueMetaRegistry() - ); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new MongodbDestination('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new MongodbDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new MongodbDestination('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new MongodbDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new MongodbMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setPriority(2); - $transportMessage->setDeliveryDelay(12345); - - $driver = new MongodbDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame(12345, $clientMessage->getDelay()); - - $this->assertNull($clientMessage->getExpire()); - $this->assertSame(MessagePriority::NORMAL, $clientMessage->getPriority()); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setPriority(MessagePriority::VERY_HIGH); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new MongodbMessage()) - ; - - $driver = new MongodbDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(MongodbMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => null, - 'correlation_id' => null, - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - } - - public function testShouldSendMessageToRouter() - { - $topic = new MongodbDestination('queue-name'); - $transportMessage = new MongodbMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.default') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new MongodbDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new MongodbDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new MongodbDestination('queue-name'); - $transportMessage = new MongodbMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new MongodbDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new MongodbDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new MongodbDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBroker() - { - $context = $this->createPsrContextMock(); - - $context - ->expects($this->once()) - ->method('createCollection') - ; - - $driver = new MongodbDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|MongodbContext - */ - private function createPsrContextMock() - { - return $this->createMock(MongodbContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer - */ - private function createPsrProducerMock() - { - return $this->createMock(PsrProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/mongodb/Tests/Functional/MongodbConsumerTest.php b/pkg/mongodb/Tests/Functional/MongodbConsumerTest.php index 609a22e16..b6b644beb 100644 --- a/pkg/mongodb/Tests/Functional/MongodbConsumerTest.php +++ b/pkg/mongodb/Tests/Functional/MongodbConsumerTest.php @@ -19,12 +19,12 @@ class MongodbConsumerTest extends TestCase */ private $context; - public function setUp() + protected function setUp(): void { $this->context = $this->buildMongodbContext(); } - protected function tearDown() + protected function tearDown(): void { if ($this->context) { $this->context->close(); diff --git a/pkg/mongodb/Tests/MongodbConnectionFactoryTest.php b/pkg/mongodb/Tests/MongodbConnectionFactoryTest.php index fa89b3424..d5dd9ca45 100644 --- a/pkg/mongodb/Tests/MongodbConnectionFactoryTest.php +++ b/pkg/mongodb/Tests/MongodbConnectionFactoryTest.php @@ -5,18 +5,21 @@ use Enqueue\Mongodb\MongodbConnectionFactory; use Enqueue\Mongodb\MongodbContext; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\ConnectionFactory; +use PHPUnit\Framework\TestCase; /** * @group mongodb */ -class MongodbConnectionFactoryTest extends \PHPUnit_Framework_TestCase +class MongodbConnectionFactoryTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, MongodbConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, MongodbConnectionFactory::class); } public function testCouldBeConstructedWithEmptyConfiguration() @@ -44,6 +47,20 @@ public function testCouldBeConstructedWithCustomConfiguration() $this->assertAttributeEquals($params, 'config', $factory); } + public function testCouldBeConstructedWithCustomConfigurationFromDsn() + { + $params = [ + 'dsn' => 'mongodb://127.0.0.3/test-db-name?enqueue_collection=collection-name&polling_interval=3000', + 'dbname' => 'test-db-name', + 'collection_name' => 'collection-name', + 'polling_interval' => 3000, + ]; + + $factory = new MongodbConnectionFactory($params['dsn']); + + $this->assertAttributeEquals($params, 'config', $factory); + } + public function testShouldCreateContext() { $factory = new MongodbConnectionFactory(); diff --git a/pkg/mongodb/Tests/MongodbConsumerTest.php b/pkg/mongodb/Tests/MongodbConsumerTest.php index 0c2e5c664..6cd597514 100644 --- a/pkg/mongodb/Tests/MongodbConsumerTest.php +++ b/pkg/mongodb/Tests/MongodbConsumerTest.php @@ -8,25 +8,21 @@ use Enqueue\Mongodb\MongodbMessage; use Enqueue\Mongodb\MongodbProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrMessage; +use Interop\Queue\Consumer; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Message; +use PHPUnit\Framework\TestCase; /** * @group mongodb */ -class MongodbConsumerTest extends \PHPUnit_Framework_TestCase +class MongodbConsumerTest extends TestCase { use ClassExtensionTrait; public function testShouldImplementConsumerInterface() { - $this->assertClassImplements(PsrConsumer::class, MongodbConsumer::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new MongodbConsumer($this->createContextMock(), new MongodbDestination('queue')); + $this->assertClassImplements(Consumer::class, MongodbConsumer::class); } public function testShouldReturnInstanceOfDestination() @@ -38,6 +34,9 @@ public function testShouldReturnInstanceOfDestination() $this->assertSame($destination, $consumer->getQueue()); } + /** + * @doesNotPerformAssertions + */ public function testCouldCallAcknowledgedMethod() { $consumer = new MongodbConsumer($this->createContextMock(), new MongodbDestination('queue')); @@ -103,7 +102,7 @@ public function testRejectShouldReSendMessageToSameQueueOnRequeue() $context ->expects($this->once()) ->method('createProducer') - ->will($this->returnValue($producerMock)) + ->willReturn($producerMock) ; $consumer = new MongodbConsumer($context, $queue); @@ -112,7 +111,7 @@ public function testRejectShouldReSendMessageToSameQueueOnRequeue() } /** - * @return MongodbProducer|\PHPUnit_Framework_MockObject_MockObject + * @return MongodbProducer|\PHPUnit\Framework\MockObject\MockObject */ private function createProducerMock() { @@ -120,7 +119,7 @@ private function createProducerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|MongodbContext + * @return \PHPUnit\Framework\MockObject\MockObject|MongodbContext */ private function createContextMock() { @@ -128,85 +127,93 @@ private function createContextMock() } } -class InvalidMessage implements PsrMessage +class InvalidMessage implements Message { - public function getBody() + public function getBody(): string { + throw new \BadMethodCallException('This should not be called directly'); } - public function setBody($body) + public function setBody(string $body): void { } - public function setProperties(array $properties) + public function setProperties(array $properties): void { } - public function getProperties() + public function getProperties(): array { + throw new \BadMethodCallException('This should not be called directly'); } - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { } - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { } - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { } - public function getHeaders() + public function getHeaders(): array { + throw new \BadMethodCallException('This should not be called directly'); } - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { } - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { } - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { } - public function isRedelivered() + public function isRedelivered(): bool { + throw new \BadMethodCallException('This should not be called directly'); } - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { } - public function getCorrelationId() + public function getCorrelationId(): ?string { + throw new \BadMethodCallException('This should not be called directly'); } - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { } - public function getMessageId() + public function getMessageId(): ?string { + throw new \BadMethodCallException('This should not be called directly'); } - public function getTimestamp() + public function getTimestamp(): ?int { + throw new \BadMethodCallException('This should not be called directly'); } - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { } - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { } - public function getReplyTo() + public function getReplyTo(): ?string { + throw new \BadMethodCallException('This should not be called directly'); } } diff --git a/pkg/mongodb/Tests/MongodbContextTest.php b/pkg/mongodb/Tests/MongodbContextTest.php index 0c06397d9..8cdef79ff 100644 --- a/pkg/mongodb/Tests/MongodbContextTest.php +++ b/pkg/mongodb/Tests/MongodbContextTest.php @@ -8,26 +8,25 @@ use Enqueue\Mongodb\MongodbMessage; use Enqueue\Mongodb\MongodbProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrDestination; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\Context; +use Interop\Queue\Destination; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\TemporaryQueueNotSupportedException; use MongoDB\Client; +use PHPUnit\Framework\TestCase; /** * @group mongodb */ -class MongodbContextTest extends \PHPUnit_Framework_TestCase +class MongodbContextTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementContextInterface() { - $this->assertClassImplements(PsrContext::class, MongodbContext::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new MongodbContext($this->createClientMock()); + $this->assertClassImplements(Context::class, MongodbContext::class); } public function testCouldBeConstructedWithEmptyConfiguration() @@ -69,6 +68,32 @@ public function testShouldCreateMessage() $this->assertFalse($message->isRedelivered()); } + public function testShouldConvertFromArrayToMongodbMessage() + { + $arrayData = [ + '_id' => 'stringId', + 'body' => 'theBody', + 'properties' => json_encode(['barProp' => 'barPropVal']), + 'headers' => json_encode(['fooHeader' => 'fooHeaderVal']), + 'priority' => '12', + 'published_at' => 1525935820, + 'redelivered' => false, + ]; + + $context = new MongodbContext($this->createClientMock()); + $message = $context->convertMessage($arrayData); + + $this->assertInstanceOf(MongodbMessage::class, $message); + + $this->assertEquals('stringId', $message->getId()); + $this->assertEquals('theBody', $message->getBody()); + $this->assertEquals(['barProp' => 'barPropVal'], $message->getProperties()); + $this->assertEquals(['fooHeader' => 'fooHeaderVal'], $message->getHeaders()); + $this->assertEquals(12, $message->getPriority()); + $this->assertEquals(1525935820, $message->getPublishedAt()); + $this->assertFalse($message->isRedelivered()); + } + public function testShouldCreateTopic() { $context = new MongodbContext($this->createClientMock()); @@ -145,18 +170,17 @@ public function testShouldReturnConfig() ], $context->getConfig()); } - public function testShouldThrowBadMethodCallExceptionOncreateTemporaryQueueCall() + public function testShouldThrowNotSupportedOnCreateTemporaryQueueCall() { $context = new MongodbContext($this->createClientMock()); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Mongodb transport does not support temporary queues'); + $this->expectException(TemporaryQueueNotSupportedException::class); $context->createTemporaryQueue(); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Client + * @return \PHPUnit\Framework\MockObject\MockObject|Client */ private function createClientMock() { @@ -164,6 +188,6 @@ private function createClientMock() } } -class NotSupportedDestination2 implements PsrDestination +class NotSupportedDestination2 implements Destination { } diff --git a/pkg/mongodb/Tests/MongodbDestinationTest.php b/pkg/mongodb/Tests/MongodbDestinationTest.php index ef81e8fc9..4e94ef018 100644 --- a/pkg/mongodb/Tests/MongodbDestinationTest.php +++ b/pkg/mongodb/Tests/MongodbDestinationTest.php @@ -1,33 +1,34 @@ assertClassImplements(PsrDestination::class, MongodbDestination::class); + $this->assertClassImplements(Destination::class, MongodbDestination::class); } public function testShouldImplementTopicInterface() { - $this->assertClassImplements(PsrTopic::class, MongodbDestination::class); + $this->assertClassImplements(Topic::class, MongodbDestination::class); } public function testShouldImplementQueueInterface() { - $this->assertClassImplements(PsrQueue::class, MongodbDestination::class); + $this->assertClassImplements(Queue::class, MongodbDestination::class); } public function testShouldReturnTopicAndQueuePreviouslySetInConstructor() diff --git a/pkg/mongodb/Tests/MongodbMessageTest.php b/pkg/mongodb/Tests/MongodbMessageTest.php index b9e5e7501..391d8f7a2 100644 --- a/pkg/mongodb/Tests/MongodbMessageTest.php +++ b/pkg/mongodb/Tests/MongodbMessageTest.php @@ -4,11 +4,12 @@ use Enqueue\Mongodb\MongodbMessage; use Enqueue\Test\ClassExtensionTrait; +use PHPUnit\Framework\TestCase; /** * @group mongodb */ -class MongodbMessageTest extends \PHPUnit_Framework_TestCase +class MongodbMessageTest extends TestCase { use ClassExtensionTrait; diff --git a/pkg/mongodb/Tests/MongodbProducerTest.php b/pkg/mongodb/Tests/MongodbProducerTest.php index ca59d5520..6987b1a76 100644 --- a/pkg/mongodb/Tests/MongodbProducerTest.php +++ b/pkg/mongodb/Tests/MongodbProducerTest.php @@ -3,42 +3,24 @@ namespace Enqueue\Mongodb\Tests; use Enqueue\Mongodb\MongodbContext; -use Enqueue\Mongodb\MongodbDestination; use Enqueue\Mongodb\MongodbMessage; use Enqueue\Mongodb\MongodbProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrProducer; +use Interop\Queue\Destination; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Producer; +use PHPUnit\Framework\TestCase; /** * @group mongodb */ -class MongodbProducerTest extends \PHPUnit_Framework_TestCase +class MongodbProducerTest extends TestCase { use ClassExtensionTrait; public function testShouldImplementProducerInterface() { - $this->assertClassImplements(PsrProducer::class, MongodbProducer::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new MongodbProducer($this->createContextMock()); - } - - public function testShouldThrowIfBodyOfInvalidType() - { - $this->expectException(InvalidMessageException::class); - $this->expectExceptionMessage('The message body must be a scalar or null. Got: stdClass'); - - $producer = new MongodbProducer($this->createContextMock()); - - $message = new MongodbMessage(new \stdClass()); - - $producer->send(new MongodbDestination(''), $message); + $this->assertClassImplements(Producer::class, MongodbProducer::class); } public function testShouldThrowIfDestinationOfInvalidType() @@ -56,7 +38,7 @@ public function testShouldThrowIfDestinationOfInvalidType() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|MongodbContext + * @return \PHPUnit\Framework\MockObject\MockObject|MongodbContext */ private function createContextMock() { @@ -64,6 +46,6 @@ private function createContextMock() } } -class NotSupportedDestination1 implements PsrDestination +class NotSupportedDestination1 implements Destination { } diff --git a/pkg/mongodb/Tests/MongodbSubscriptionConsumerTest.php b/pkg/mongodb/Tests/MongodbSubscriptionConsumerTest.php new file mode 100644 index 000000000..d982e0418 --- /dev/null +++ b/pkg/mongodb/Tests/MongodbSubscriptionConsumerTest.php @@ -0,0 +1,178 @@ +assertTrue($rc->implementsInterface(SubscriptionConsumer::class)); + } + + public function testShouldAddConsumerAndCallbackToSubscribersPropertyOnSubscribe() + { + $subscriptionConsumer = new MongodbSubscriptionConsumer($this->createMongodbContextMock()); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $barCallback = function () {}; + $barConsumer = $this->createConsumerStub('bar_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + $subscriptionConsumer->subscribe($barConsumer, $barCallback); + + $this->assertAttributeSame([ + 'foo_queue' => [$fooConsumer, $fooCallback], + 'bar_queue' => [$barConsumer, $barCallback], + ], 'subscribers', $subscriptionConsumer); + } + + public function testThrowsIfTrySubscribeAnotherConsumerToAlreadySubscribedQueue() + { + $subscriptionConsumer = new MongodbSubscriptionConsumer($this->createMongodbContextMock()); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $barCallback = function () {}; + $barConsumer = $this->createConsumerStub('foo_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('There is a consumer subscribed to queue: "foo_queue"'); + $subscriptionConsumer->subscribe($barConsumer, $barCallback); + } + + /** + * @doesNotPerformAssertions + */ + public function testShouldAllowSubscribeSameConsumerAndCallbackSecondTime() + { + $subscriptionConsumer = new MongodbSubscriptionConsumer($this->createMongodbContextMock()); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + } + + public function testShouldRemoveSubscribedConsumerOnUnsubscribeCall() + { + $subscriptionConsumer = new MongodbSubscriptionConsumer($this->createMongodbContextMock()); + + $fooConsumer = $this->createConsumerStub('foo_queue'); + $barConsumer = $this->createConsumerStub('bar_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, function () {}); + $subscriptionConsumer->subscribe($barConsumer, function () {}); + + // guard + $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($fooConsumer); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldDoNothingIfTryUnsubscribeNotSubscribedQueueName() + { + $subscriptionConsumer = new MongodbSubscriptionConsumer($this->createMongodbContextMock()); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + + // guard + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($this->createConsumerStub('bar_queue')); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldDoNothingIfTryUnsubscribeNotSubscribedConsumer() + { + $subscriptionConsumer = new MongodbSubscriptionConsumer($this->createMongodbContextMock()); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + + // guard + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($this->createConsumerStub('foo_queue')); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldRemoveAllSubscriberOnUnsubscribeAllCall() + { + $subscriptionConsumer = new MongodbSubscriptionConsumer($this->createMongodbContextMock()); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + $subscriptionConsumer->subscribe($this->createConsumerStub('bar_queue'), function () {}); + + // guard + $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribeAll(); + + $this->assertAttributeCount(0, 'subscribers', $subscriptionConsumer); + } + + public function testThrowsIfTryConsumeWithoutSubscribers() + { + $subscriptionConsumer = new MongodbSubscriptionConsumer($this->createMongodbContextMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('No subscribers'); + + $subscriptionConsumer->consume(); + } + + /** + * @return MongodbContext|\PHPUnit\Framework\MockObject\MockObject + */ + private function createMongodbContextMock() + { + return $this->createMock(MongodbContext::class); + } + + /** + * @param mixed|null $queueName + * + * @return Consumer|\PHPUnit\Framework\MockObject\MockObject + */ + private function createConsumerStub($queueName = null) + { + $queueMock = $this->createMock(Queue::class); + $queueMock + ->expects($this->any()) + ->method('getQueueName') + ->willReturn($queueName); + + $consumerMock = $this->createMock(MongodbConsumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queueMock) + ; + + return $consumerMock; + } +} diff --git a/pkg/mongodb/Tests/Spec/MongodbConnectionFactoryTest.php b/pkg/mongodb/Tests/Spec/MongodbConnectionFactoryTest.php index 324fe52a6..9f0d195ba 100644 --- a/pkg/mongodb/Tests/Spec/MongodbConnectionFactoryTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbConnectionFactoryTest.php @@ -3,16 +3,13 @@ namespace Enqueue\Mongodb\Tests\Spec; use Enqueue\Mongodb\MongodbConnectionFactory; -use Interop\Queue\Spec\PsrConnectionFactorySpec; +use Interop\Queue\Spec\ConnectionFactorySpec; /** * @group mongodb */ -class MongodbConnectionFactoryTest extends PsrConnectionFactorySpec +class MongodbConnectionFactoryTest extends ConnectionFactorySpec { - /** - * {@inheritdoc} - */ protected function createConnectionFactory() { return new MongodbConnectionFactory(); diff --git a/pkg/mongodb/Tests/Spec/MongodbContextTest.php b/pkg/mongodb/Tests/Spec/MongodbContextTest.php index 51d4c4b88..dfd5de3cb 100644 --- a/pkg/mongodb/Tests/Spec/MongodbContextTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbContextTest.php @@ -3,19 +3,16 @@ namespace Enqueue\Mongodb\Tests\Spec; use Enqueue\Test\MongodbExtensionTrait; -use Interop\Queue\Spec\PsrContextSpec; +use Interop\Queue\Spec\ContextSpec; /** * @group functional * @group mongodb */ -class MongodbContextTest extends PsrContextSpec +class MongodbContextTest extends ContextSpec { use MongodbExtensionTrait; - /** - * {@inheritdoc} - */ protected function createContext() { return $this->buildMongodbContext(); diff --git a/pkg/mongodb/Tests/Spec/MongodbMessageTest.php b/pkg/mongodb/Tests/Spec/MongodbMessageTest.php index 51ab55ac5..92983d430 100644 --- a/pkg/mongodb/Tests/Spec/MongodbMessageTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbMessageTest.php @@ -3,16 +3,13 @@ namespace Enqueue\Mongodb\Tests\Spec; use Enqueue\Mongodb\MongodbMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; /** * @group mongodb */ -class MongodbMessageTest extends PsrMessageSpec +class MongodbMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new MongodbMessage(); diff --git a/pkg/mongodb/Tests/Spec/MongodbProducerTest.php b/pkg/mongodb/Tests/Spec/MongodbProducerTest.php index 54eb096d0..68b6007ec 100644 --- a/pkg/mongodb/Tests/Spec/MongodbProducerTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbProducerTest.php @@ -3,19 +3,16 @@ namespace Enqueue\Mongodb\Tests\Spec; use Enqueue\Test\MongodbExtensionTrait; -use Interop\Queue\Spec\PsrProducerSpec; +use Interop\Queue\Spec\ProducerSpec; /** * @group functional * @group mongodb */ -class MongodbProducerTest extends PsrProducerSpec +class MongodbProducerTest extends ProducerSpec { use MongodbExtensionTrait; - /** - * {@inheritdoc} - */ protected function createProducer() { return $this->buildMongodbContext()->createProducer(); diff --git a/pkg/mongodb/Tests/Spec/MongodbQueueTest.php b/pkg/mongodb/Tests/Spec/MongodbQueueTest.php index a555461a7..25e437ba6 100644 --- a/pkg/mongodb/Tests/Spec/MongodbQueueTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbQueueTest.php @@ -3,16 +3,13 @@ namespace Enqueue\Mongodb\Tests\Spec; use Enqueue\Mongodb\MongodbDestination; -use Interop\Queue\Spec\PsrQueueSpec; +use Interop\Queue\Spec\QueueSpec; /** * @group mongodb */ -class MongodbQueueTest extends PsrQueueSpec +class MongodbQueueTest extends QueueSpec { - /** - * {@inheritdoc} - */ protected function createQueue() { return new MongodbDestination(self::EXPECTED_QUEUE_NAME); diff --git a/pkg/mongodb/Tests/Spec/MongodbRequeueMessageTest.php b/pkg/mongodb/Tests/Spec/MongodbRequeueMessageTest.php index 454d357ad..8a9072470 100644 --- a/pkg/mongodb/Tests/Spec/MongodbRequeueMessageTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbRequeueMessageTest.php @@ -13,9 +13,6 @@ class MongodbRequeueMessageTest extends RequeueMessageSpec { use MongodbExtensionTrait; - /** - * {@inheritdoc} - */ protected function createContext() { return $this->buildMongodbContext(); diff --git a/pkg/mongodb/Tests/Spec/MongodbSendAndReceiveDelayedMessageFromQueueTest.php b/pkg/mongodb/Tests/Spec/MongodbSendAndReceiveDelayedMessageFromQueueTest.php index a5eb3511d..f54513fae 100644 --- a/pkg/mongodb/Tests/Spec/MongodbSendAndReceiveDelayedMessageFromQueueTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbSendAndReceiveDelayedMessageFromQueueTest.php @@ -13,9 +13,6 @@ class MongodbSendAndReceiveDelayedMessageFromQueueTest extends SendAndReceiveDel { use MongodbExtensionTrait; - /** - * {@inheritdoc} - */ protected function createContext() { return $this->buildMongodbContext(); diff --git a/pkg/mongodb/Tests/Spec/MongodbSendAndReceivePriorityMessagesFromQueueTest.php b/pkg/mongodb/Tests/Spec/MongodbSendAndReceivePriorityMessagesFromQueueTest.php index 5400820b8..6aadef7ba 100644 --- a/pkg/mongodb/Tests/Spec/MongodbSendAndReceivePriorityMessagesFromQueueTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbSendAndReceivePriorityMessagesFromQueueTest.php @@ -5,7 +5,7 @@ use Enqueue\Mongodb\MongodbContext; use Enqueue\Mongodb\MongodbMessage; use Enqueue\Test\MongodbExtensionTrait; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceivePriorityMessagesFromQueueSpec; /** @@ -18,7 +18,7 @@ class MongodbSendAndReceivePriorityMessagesFromQueueTest extends SendAndReceiveP private $publishedAt; - public function setUp() + protected function setUp(): void { parent::setUp(); @@ -26,7 +26,7 @@ public function setUp() } /** - * @return PsrContext + * @return Context */ protected function createContext() { @@ -34,13 +34,11 @@ protected function createContext() } /** - * {@inheritdoc} - * * @param MongodbContext $context * * @return MongodbMessage */ - protected function createMessage(PsrContext $context, $body) + protected function createMessage(Context $context, $body) { /** @var MongodbMessage $message */ $message = parent::createMessage($context, $body); diff --git a/pkg/mongodb/Tests/Spec/MongodbSendAndReceiveTimeToLiveMessagesFromQueueTest.php b/pkg/mongodb/Tests/Spec/MongodbSendAndReceiveTimeToLiveMessagesFromQueueTest.php index d87ac10e9..f16e80b60 100644 --- a/pkg/mongodb/Tests/Spec/MongodbSendAndReceiveTimeToLiveMessagesFromQueueTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbSendAndReceiveTimeToLiveMessagesFromQueueTest.php @@ -13,9 +13,6 @@ class MongodbSendAndReceiveTimeToLiveMessagesFromQueueTest extends SendAndReceiv { use MongodbExtensionTrait; - /** - * {@inheritdoc} - */ protected function createContext() { return $this->buildMongodbContext(); diff --git a/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveFromQueueTest.php b/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveFromQueueTest.php index 992c0626e..c9b9cb2d1 100644 --- a/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveFromQueueTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveFromQueueTest.php @@ -13,9 +13,6 @@ class MongodbSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec { use MongodbExtensionTrait; - /** - * {@inheritdoc} - */ protected function createContext() { return $this->buildMongodbContext(); diff --git a/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveFromTopicTest.php b/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveFromTopicTest.php index c539386f7..a416d3c11 100644 --- a/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveFromTopicTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveFromTopicTest.php @@ -13,9 +13,6 @@ class MongodbSendToAndReceiveFromTopicTest extends SendToAndReceiveFromTopicSpec { use MongodbExtensionTrait; - /** - * {@inheritdoc} - */ protected function createContext() { return $this->buildMongodbContext(); diff --git a/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveNoWaitFromQueueTest.php b/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveNoWaitFromQueueTest.php index ea4febcc2..43ae34c6b 100644 --- a/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveNoWaitFromQueueTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveNoWaitFromQueueTest.php @@ -13,9 +13,6 @@ class MongodbSendToAndReceiveNoWaitFromQueueTest extends SendToAndReceiveNoWaitF { use MongodbExtensionTrait; - /** - * {@inheritdoc} - */ protected function createContext() { return $this->buildMongodbContext(); diff --git a/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveNoWaitFromTopicTest.php b/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveNoWaitFromTopicTest.php index 1e1be32c1..0fe9f0e56 100644 --- a/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveNoWaitFromTopicTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbSendToAndReceiveNoWaitFromTopicTest.php @@ -13,9 +13,6 @@ class MongodbSendToAndReceiveNoWaitFromTopicTest extends SendToAndReceiveNoWaitF { use MongodbExtensionTrait; - /** - * {@inheritdoc} - */ protected function createContext() { return $this->buildMongodbContext(); diff --git a/pkg/mongodb/Tests/Spec/MongodbSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php b/pkg/mongodb/Tests/Spec/MongodbSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..2fe16e860 --- /dev/null +++ b/pkg/mongodb/Tests/Spec/MongodbSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,40 @@ +buildMongodbContext(); + } + + /** + * @param MongodbContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var MongodbDestination $queue */ + $queue = parent::createQueue($context, $queueName); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/mongodb/Tests/Spec/MongodbSubscriptionConsumerConsumeUntilUnsubscribedTest.php b/pkg/mongodb/Tests/Spec/MongodbSubscriptionConsumerConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..b18e0bf0d --- /dev/null +++ b/pkg/mongodb/Tests/Spec/MongodbSubscriptionConsumerConsumeUntilUnsubscribedTest.php @@ -0,0 +1,40 @@ +buildMongodbContext(); + } + + /** + * @param MongodbContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var MongodbDestination $queue */ + $queue = parent::createQueue($context, $queueName); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/mongodb/Tests/Spec/MongodbSubscriptionConsumerStopOnFalseTest.php b/pkg/mongodb/Tests/Spec/MongodbSubscriptionConsumerStopOnFalseTest.php new file mode 100644 index 000000000..3acfa94ed --- /dev/null +++ b/pkg/mongodb/Tests/Spec/MongodbSubscriptionConsumerStopOnFalseTest.php @@ -0,0 +1,40 @@ +buildMongodbContext(); + } + + /** + * @param MongodbContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var MongodbDestination $queue */ + $queue = parent::createQueue($context, $queueName); + $context->getClient()->dropDatabase($queueName); + + return $queue; + } +} diff --git a/pkg/mongodb/Tests/Spec/MongodbTopicTest.php b/pkg/mongodb/Tests/Spec/MongodbTopicTest.php index 14a79f5a5..ab5c025a2 100644 --- a/pkg/mongodb/Tests/Spec/MongodbTopicTest.php +++ b/pkg/mongodb/Tests/Spec/MongodbTopicTest.php @@ -3,16 +3,13 @@ namespace Enqueue\Mongodb\Tests\Spec; use Enqueue\Mongodb\MongodbDestination; -use Interop\Queue\Spec\PsrTopicSpec; +use Interop\Queue\Spec\TopicSpec; /** * @group mongodb */ -class MongodbTopicTest extends PsrTopicSpec +class MongodbTopicTest extends TopicSpec { - /** - * {@inheritdoc} - */ protected function createTopic() { return new MongodbDestination(self::EXPECTED_TOPIC_NAME); diff --git a/pkg/mongodb/Tests/Symfony/MongodbTransportFactoryTest.php b/pkg/mongodb/Tests/Symfony/MongodbTransportFactoryTest.php deleted file mode 100644 index 3621acafe..000000000 --- a/pkg/mongodb/Tests/Symfony/MongodbTransportFactoryTest.php +++ /dev/null @@ -1,164 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, MongodbTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new MongodbTransportFactory(); - - $this->assertEquals('mongodb', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new MongodbTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new MongodbTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'dsn' => 'mongodb://127.0.0.1/', - ]]); - - $this->assertEquals([ - 'dsn' => 'mongodb://127.0.0.1/', - 'dbname' => 'enqueue', - 'collection_name' => 'enqueue', - 'polling_interval' => 1000, - ], $config); - } - - public function testShouldAllowAddConfigurationAsString() - { - $transport = new MongodbTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['mysqlDSN']); - - $this->assertEquals([ - 'dsn' => 'mysqlDSN', - 'dbname' => 'enqueue', - 'collection_name' => 'enqueue', - 'polling_interval' => 1000, - ], $config); - } - - public function testShouldCreateMongodbConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new MongodbTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'mysqlDSN', - 'dbname' => 'enqueue', - 'collection_name' => 'enqueue', - 'polling_interval' => 1000, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(MongodbConnectionFactory::class, $factory->getClass()); - - $this->assertSame([ - 'dsn' => 'mysqlDSN', - 'dbname' => 'enqueue', - 'collection_name' => 'enqueue', - 'polling_interval' => 1000, - ], $factory->getArgument(0)); - } - - public function testShouldCreateConnectionFactoryFromDsnString() - { - $container = new ContainerBuilder(); - - $transport = new MongodbTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theDSN', - 'connection' => [], - 'lazy' => true, - 'table_name' => 'enqueue', - 'polling_interval' => 1000, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(MongodbConnectionFactory::class, $factory->getClass()); - $this->assertSame('theDSN', $factory->getArgument(0)['dsn']); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new MongodbTransportFactory(); - - $serviceId = $transport->createContext($container, []); - - $this->assertEquals('enqueue.transport.mongodb.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.mongodb.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.mongodb.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new MongodbTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.mongodb.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(MongodbDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.mongodb.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/mongodb/composer.json b/pkg/mongodb/composer.json index 9642adb77..f64d53ef3 100644 --- a/pkg/mongodb/composer.json +++ b/pkg/mongodb/composer.json @@ -10,19 +10,16 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": "^7.0", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1", + "php": "^8.1", + "queue-interop/queue-interop": "^0.8", "mongodb/mongodb": "^1.2", - "ext-mongodb": "^1.3" + "ext-mongodb": "^1.5" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "symfony/dependency-injection": "^4", - "symfony/config": "^4", - "queue-interop/queue-spec": "^0.5.5@dev", - "enqueue/test": "^0.8.25@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev" + "phpunit/phpunit": "^9.5", + "queue-interop/queue-spec": "^0.6.2", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev" }, "support": { "email": "opensource@forma-pro.com", @@ -32,20 +29,15 @@ "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" }, "autoload": { - "psr-4": { - "Enqueue\\Mongodb\\": "" - }, + "psr-4": { "Enqueue\\Mongodb\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/mongodb/phpunit.xml.dist b/pkg/mongodb/phpunit.xml.dist index 1f34af01d..6b9960935 100644 --- a/pkg/mongodb/phpunit.xml.dist +++ b/pkg/mongodb/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/monitoring/.gitattributes b/pkg/monitoring/.gitattributes new file mode 100644 index 000000000..bdf2dcb14 --- /dev/null +++ b/pkg/monitoring/.gitattributes @@ -0,0 +1,5 @@ +/Tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +phpunit.xml.dist export-ignore diff --git a/pkg/monitoring/.github/workflows/ci.yml b/pkg/monitoring/.github/workflows/ci.yml new file mode 100644 index 000000000..5448d7b1a --- /dev/null +++ b/pkg/monitoring/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/monitoring/.gitignore b/pkg/monitoring/.gitignore new file mode 100644 index 000000000..a770439e5 --- /dev/null +++ b/pkg/monitoring/.gitignore @@ -0,0 +1,6 @@ +*~ +/composer.lock +/composer.phar +/phpunit.xml +/vendor/ +/.idea/ diff --git a/pkg/monitoring/ClientMonitoringExtension.php b/pkg/monitoring/ClientMonitoringExtension.php new file mode 100644 index 000000000..11e7054f7 --- /dev/null +++ b/pkg/monitoring/ClientMonitoringExtension.php @@ -0,0 +1,64 @@ +storage = $storage; + $this->logger = $logger; + } + + public function onPostSend(PostSend $context): void + { + $timestampMs = (int) (microtime(true) * 1000); + + $destination = $context->getTransportDestination() instanceof Topic + ? $context->getTransportDestination()->getTopicName() + : $context->getTransportDestination()->getQueueName() + ; + + $stats = new SentMessageStats( + $timestampMs, + $destination, + $context->getTransportDestination() instanceof Topic, + $context->getTransportMessage()->getMessageId(), + $context->getTransportMessage()->getCorrelationId(), + $context->getTransportMessage()->getHeaders(), + $context->getTransportMessage()->getProperties() + ); + + $this->safeCall(function () use ($stats) { + $this->storage->pushSentMessageStats($stats); + }); + } + + private function safeCall(callable $fun) + { + try { + return call_user_func($fun); + } catch (\Throwable $e) { + $this->logger->error(sprintf('[ClientMonitoringExtension] Push to storage failed: %s', $e->getMessage())); + } + + return null; + } +} diff --git a/pkg/monitoring/ConsumedMessageStats.php b/pkg/monitoring/ConsumedMessageStats.php new file mode 100644 index 000000000..077ceebdf --- /dev/null +++ b/pkg/monitoring/ConsumedMessageStats.php @@ -0,0 +1,210 @@ +consumerId = $consumerId; + $this->timestampMs = $timestampMs; + $this->receivedAtMs = $receivedAtMs; + $this->queue = $queue; + $this->messageId = $messageId; + $this->correlationId = $correlationId; + $this->headers = $headers; + $this->properties = $properties; + $this->redelivered = $redelivered; + $this->status = $status; + + $this->errorClass = $errorClass; + $this->errorMessage = $errorMessage; + $this->errorCode = $errorCode; + $this->errorFile = $errorFile; + $this->errorLine = $errorLine; + $this->trance = $trace; + } + + public function getConsumerId(): string + { + return $this->consumerId; + } + + public function getTimestampMs(): int + { + return $this->timestampMs; + } + + public function getReceivedAtMs(): int + { + return $this->receivedAtMs; + } + + public function getQueue(): string + { + return $this->queue; + } + + public function getMessageId(): ?string + { + return $this->messageId; + } + + public function getCorrelationId(): ?string + { + return $this->correlationId; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function getProperties(): array + { + return $this->properties; + } + + public function isRedelivered(): bool + { + return $this->redelivered; + } + + public function getStatus(): string + { + return $this->status; + } + + public function getErrorClass(): ?string + { + return $this->errorClass; + } + + public function getErrorMessage(): ?string + { + return $this->errorMessage; + } + + public function getErrorCode(): ?int + { + return $this->errorCode; + } + + public function getErrorFile(): ?string + { + return $this->errorFile; + } + + public function getErrorLine(): ?int + { + return $this->errorLine; + } + + public function getTrance(): ?string + { + return $this->trance; + } +} diff --git a/pkg/monitoring/ConsumerMonitoringExtension.php b/pkg/monitoring/ConsumerMonitoringExtension.php new file mode 100644 index 000000000..31e97697d --- /dev/null +++ b/pkg/monitoring/ConsumerMonitoringExtension.php @@ -0,0 +1,320 @@ +storage = $storage; + $this->updateStatsPeriod = 60; + } + + public function onStart(Start $context): void + { + $this->consumerId = UUID::generate(); + + $this->queues = []; + + $this->startedAtMs = 0; + $this->lastStatsAt = 0; + + $this->received = 0; + $this->acknowledged = 0; + $this->rejected = 0; + $this->requeued = 0; + } + + public function onPreSubscribe(PreSubscribe $context): void + { + $this->queues[] = $context->getConsumer()->getQueue()->getQueueName(); + } + + public function onPreConsume(PreConsume $context): void + { + // send started only once + $isStarted = false; + if (0 === $this->startedAtMs) { + $isStarted = true; + $this->startedAtMs = $context->getStartTime(); + } + + // send stats event only once per period + $time = time(); + if (($time - $this->lastStatsAt) > $this->updateStatsPeriod) { + $this->lastStatsAt = $time; + + $event = new ConsumerStats( + $this->consumerId, + $this->getNowMs(), + $this->startedAtMs, + null, + $isStarted, + false, + false, + $this->queues, + $this->received, + $this->acknowledged, + $this->rejected, + $this->requeued, + $this->getMemoryUsage(), + $this->getSystemLoad() + ); + + $this->safeCall(function () use ($event) { + $this->storage->pushConsumerStats($event); + }, $context->getLogger()); + } + } + + public function onEnd(End $context): void + { + $event = new ConsumerStats( + $this->consumerId, + $this->getNowMs(), + $this->startedAtMs, + $context->getEndTime(), + false, + true, + false, + $this->queues, + $this->received, + $this->acknowledged, + $this->rejected, + $this->requeued, + $this->getMemoryUsage(), + $this->getSystemLoad() + ); + + $this->safeCall(function () use ($event) { + $this->storage->pushConsumerStats($event); + }, $context->getLogger()); + } + + public function onProcessorException(ProcessorException $context): void + { + $timeMs = $this->getNowMs(); + + $event = new ConsumedMessageStats( + $this->consumerId, + $timeMs, + $context->getReceivedAt(), + $context->getConsumer()->getQueue()->getQueueName(), + $context->getMessage()->getMessageId(), + $context->getMessage()->getCorrelationId(), + $context->getMessage()->getHeaders(), + $context->getMessage()->getProperties(), + $context->getMessage()->isRedelivered(), + ConsumedMessageStats::STATUS_FAILED, + get_class($context->getException()), + $context->getException()->getMessage(), + $context->getException()->getCode(), + $context->getException()->getFile(), + $context->getException()->getLine(), + $context->getException()->getTraceAsString() + ); + + $this->safeCall(function () use ($event) { + $this->storage->pushConsumedMessageStats($event); + }, $context->getLogger()); + + // priority of this extension must be the lowest and + // if result is null we emit consumer stopped event here + if (null === $context->getResult()) { + $event = new ConsumerStats( + $this->consumerId, + $timeMs, + $this->startedAtMs, + $timeMs, + false, + true, + true, + $this->queues, + $this->received, + $this->acknowledged, + $this->rejected, + $this->requeued, + $this->getMemoryUsage(), + $this->getSystemLoad(), + get_class($context->getException()), + $context->getException()->getMessage(), + $context->getException()->getCode(), + $context->getException()->getFile(), + $context->getException()->getLine(), + $context->getException()->getTraceAsString() + ); + + $this->safeCall(function () use ($event) { + $this->storage->pushConsumerStats($event); + }, $context->getLogger()); + } + } + + public function onMessageReceived(MessageReceived $context): void + { + ++$this->received; + } + + public function onResult(MessageResult $context): void + { + $timeMs = $this->getNowMs(); + + switch ($context->getResult()) { + case Result::ACK: + case Result::ALREADY_ACKNOWLEDGED: + $this->acknowledged++; + $status = ConsumedMessageStats::STATUS_ACK; + break; + case Result::REJECT: + $this->rejected++; + $status = ConsumedMessageStats::STATUS_REJECTED; + break; + case Result::REQUEUE: + $this->requeued++; + $status = ConsumedMessageStats::STATUS_REQUEUED; + break; + default: + throw new \LogicException(); + } + + $event = new ConsumedMessageStats( + $this->consumerId, + $timeMs, + $context->getReceivedAt(), + $context->getConsumer()->getQueue()->getQueueName(), + $context->getMessage()->getMessageId(), + $context->getMessage()->getCorrelationId(), + $context->getMessage()->getHeaders(), + $context->getMessage()->getProperties(), + $context->getMessage()->isRedelivered(), + $status + ); + + $this->safeCall(function () use ($event) { + $this->storage->pushConsumedMessageStats($event); + }, $context->getLogger()); + + // send stats event only once per period + $time = time(); + if (($time - $this->lastStatsAt) > $this->updateStatsPeriod) { + $this->lastStatsAt = $time; + + $event = new ConsumerStats( + $this->consumerId, + $timeMs, + $this->startedAtMs, + null, + false, + false, + false, + $this->queues, + $this->received, + $this->acknowledged, + $this->rejected, + $this->requeued, + $this->getMemoryUsage(), + $this->getSystemLoad() + ); + + $this->safeCall(function () use ($event) { + $this->storage->pushConsumerStats($event); + }, $context->getLogger()); + } + } + + private function getNowMs(): int + { + return (int) (microtime(true) * 1000); + } + + private function getMemoryUsage(): int + { + return memory_get_usage(true); + } + + private function getSystemLoad(): float + { + return sys_getloadavg()[0]; + } + + private function safeCall(callable $fun, LoggerInterface $logger) + { + try { + return call_user_func($fun); + } catch (\Throwable $e) { + $logger->error(sprintf('[ConsumerMonitoringExtension] Push to storage failed: %s', $e->getMessage())); + } + + return null; + } +} diff --git a/pkg/monitoring/ConsumerStats.php b/pkg/monitoring/ConsumerStats.php new file mode 100644 index 000000000..d281b532d --- /dev/null +++ b/pkg/monitoring/ConsumerStats.php @@ -0,0 +1,257 @@ +consumerId = $consumerId; + $this->timestampMs = $timestampMs; + $this->startedAtMs = $startedAtMs; + $this->finishedAtMs = $finishedAtMs; + + $this->started = $started; + $this->finished = $finished; + $this->failed = $failed; + + $this->queues = $queues; + $this->startedAtMs = $startedAtMs; + $this->received = $received; + $this->acknowledged = $acknowledged; + $this->rejected = $rejected; + $this->requeued = $requeued; + + $this->memoryUsage = $memoryUsage; + $this->systemLoad = $systemLoad; + + $this->errorClass = $errorClass; + $this->errorMessage = $errorMessage; + $this->errorCode = $errorCode; + $this->errorFile = $errorFile; + $this->errorLine = $errorLine; + $this->trance = $trace; + } + + public function getConsumerId(): string + { + return $this->consumerId; + } + + public function getTimestampMs(): int + { + return $this->timestampMs; + } + + public function getStartedAtMs(): int + { + return $this->startedAtMs; + } + + public function getFinishedAtMs(): ?int + { + return $this->finishedAtMs; + } + + public function isStarted(): bool + { + return $this->started; + } + + public function isFinished(): bool + { + return $this->finished; + } + + public function isFailed(): bool + { + return $this->failed; + } + + public function getQueues(): array + { + return $this->queues; + } + + public function getReceived(): int + { + return $this->received; + } + + public function getAcknowledged(): int + { + return $this->acknowledged; + } + + public function getRejected(): int + { + return $this->rejected; + } + + public function getRequeued(): int + { + return $this->requeued; + } + + public function getMemoryUsage(): int + { + return $this->memoryUsage; + } + + public function getSystemLoad(): float + { + return $this->systemLoad; + } + + public function getErrorClass(): ?string + { + return $this->errorClass; + } + + public function getErrorMessage(): ?string + { + return $this->errorMessage; + } + + public function getErrorCode(): ?int + { + return $this->errorCode; + } + + public function getErrorFile(): ?string + { + return $this->errorFile; + } + + public function getErrorLine(): ?int + { + return $this->errorLine; + } + + public function getTrance(): ?string + { + return $this->trance; + } +} diff --git a/pkg/monitoring/DatadogStorage.php b/pkg/monitoring/DatadogStorage.php new file mode 100644 index 000000000..c10cbc671 --- /dev/null +++ b/pkg/monitoring/DatadogStorage.php @@ -0,0 +1,165 @@ +config = $this->prepareConfig($config); + + if (null === $this->datadog) { + if (true === filter_var($this->config['batched'], \FILTER_VALIDATE_BOOLEAN)) { + $this->datadog = new BatchedDogStatsd($this->config); + } else { + $this->datadog = new DogStatsd($this->config); + } + } + } + + public function pushConsumerStats(ConsumerStats $stats): void + { + $queues = $stats->getQueues(); + array_walk($queues, function (string $queue) use ($stats) { + $tags = [ + 'queue' => $queue, + 'consumerId' => $stats->getConsumerId(), + ]; + + if ($stats->getFinishedAtMs()) { + $values['finishedAtMs'] = $stats->getFinishedAtMs(); + } + + $this->datadog->gauge($this->config['metric.consumers.started'], (int) $stats->isStarted(), 1, $tags); + $this->datadog->gauge($this->config['metric.consumers.finished'], (int) $stats->isFinished(), 1, $tags); + $this->datadog->gauge($this->config['metric.consumers.failed'], (int) $stats->isFailed(), 1, $tags); + $this->datadog->gauge($this->config['metric.consumers.received'], $stats->getReceived(), 1, $tags); + $this->datadog->gauge($this->config['metric.consumers.acknowledged'], $stats->getAcknowledged(), 1, $tags); + $this->datadog->gauge($this->config['metric.consumers.rejected'], $stats->getRejected(), 1, $tags); + $this->datadog->gauge($this->config['metric.consumers.requeued'], $stats->getRejected(), 1, $tags); + $this->datadog->gauge($this->config['metric.consumers.memoryUsage'], $stats->getMemoryUsage(), 1, $tags); + }); + } + + public function pushSentMessageStats(SentMessageStats $stats): void + { + $tags = [ + 'destination' => $stats->getDestination(), + ]; + + $properties = $stats->getProperties(); + if (false === empty($properties[Config::TOPIC])) { + $tags['topic'] = $properties[Config::TOPIC]; + } + + if (false === empty($properties[Config::COMMAND])) { + $tags['command'] = $properties[Config::COMMAND]; + } + + $this->datadog->increment($this->config['metric.messages.sent'], 1, $tags); + } + + public function pushConsumedMessageStats(ConsumedMessageStats $stats): void + { + $tags = [ + 'queue' => $stats->getQueue(), + 'status' => $stats->getStatus(), + ]; + + if (ConsumedMessageStats::STATUS_FAILED === $stats->getStatus()) { + $this->datadog->increment($this->config['metric.messages.failed'], 1, $tags); + } + + if ($stats->isRedelivered()) { + $this->datadog->increment($this->config['metric.messages.redelivered'], 1, $tags); + } + + $runtime = $stats->getTimestampMs() - $stats->getReceivedAtMs(); + $this->datadog->histogram($this->config['metric.messages.consumed'], $runtime, 1, $tags); + } + + private function parseDsn(string $dsn): array + { + $dsn = Dsn::parseFirst($dsn); + + if ('datadog' !== $dsn->getSchemeProtocol()) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "datadog"', $dsn->getSchemeProtocol())); + } + + return array_filter(array_replace($dsn->getQuery(), [ + 'host' => $dsn->getHost(), + 'port' => $dsn->getPort(), + 'global_tags' => $dsn->getString('global_tags'), + 'batched' => $dsn->getString('batched'), + 'metric.messages.sent' => $dsn->getString('metric.messages.sent'), + 'metric.messages.consumed' => $dsn->getString('metric.messages.consumed'), + 'metric.messages.redelivered' => $dsn->getString('metric.messages.redelivered'), + 'metric.messages.failed' => $dsn->getString('metric.messages.failed'), + 'metric.consumers.started' => $dsn->getString('metric.consumers.started'), + 'metric.consumers.finished' => $dsn->getString('metric.consumers.finished'), + 'metric.consumers.failed' => $dsn->getString('metric.consumers.failed'), + 'metric.consumers.received' => $dsn->getString('metric.consumers.received'), + 'metric.consumers.acknowledged' => $dsn->getString('metric.consumers.acknowledged'), + 'metric.consumers.rejected' => $dsn->getString('metric.consumers.rejected'), + 'metric.consumers.requeued' => $dsn->getString('metric.consumers.requeued'), + 'metric.consumers.memoryUsage' => $dsn->getString('metric.consumers.memoryUsage'), + ]), function ($value) { + return null !== $value; + }); + } + + private function prepareConfig($config): array + { + if (empty($config)) { + $config = $this->parseDsn('datadog:'); + } elseif (\is_string($config)) { + $config = $this->parseDsn($config); + } elseif (\is_array($config)) { + $config = empty($config['dsn']) ? $config : $this->parseDsn($config['dsn']); + } elseif ($config instanceof DogStatsd) { + $this->datadog = $config; + $config = []; + } else { + throw new \LogicException('The config must be either an array of options, a DSN string or null'); + } + + return array_replace([ + 'host' => 'localhost', + 'port' => 8125, + 'batched' => true, + 'metric.messages.sent' => 'enqueue.messages.sent', + 'metric.messages.consumed' => 'enqueue.messages.consumed', + 'metric.messages.redelivered' => 'enqueue.messages.redelivered', + 'metric.messages.failed' => 'enqueue.messages.failed', + 'metric.consumers.started' => 'enqueue.consumers.started', + 'metric.consumers.finished' => 'enqueue.consumers.finished', + 'metric.consumers.failed' => 'enqueue.consumers.failed', + 'metric.consumers.received' => 'enqueue.consumers.received', + 'metric.consumers.acknowledged' => 'enqueue.consumers.acknowledged', + 'metric.consumers.rejected' => 'enqueue.consumers.rejected', + 'metric.consumers.requeued' => 'enqueue.consumers.requeued', + 'metric.consumers.memoryUsage' => 'enqueue.consumers.memoryUsage', + ], $config); + } +} diff --git a/pkg/monitoring/GenericStatsStorageFactory.php b/pkg/monitoring/GenericStatsStorageFactory.php new file mode 100644 index 000000000..55475f953 --- /dev/null +++ b/pkg/monitoring/GenericStatsStorageFactory.php @@ -0,0 +1,65 @@ + $config]; + } + + if (false === \is_array($config)) { + throw new \InvalidArgumentException('The config must be either array or DSN string.'); + } + + if (false === array_key_exists('dsn', $config)) { + throw new \InvalidArgumentException('The config must have dsn key set.'); + } + + $dsn = Dsn::parseFirst($config['dsn']); + + if ($storageClass = $this->findStorageClass($dsn, Resources::getKnownStorages())) { + return new $storageClass(1 === \count($config) ? $config['dsn'] : $config); + } + + throw new \LogicException(sprintf('A given scheme "%s" is not supported.', $dsn->getScheme())); + } + + private function findStorageClass(Dsn $dsn, array $factories): ?string + { + $protocol = $dsn->getSchemeProtocol(); + + if ($dsn->getSchemeExtensions()) { + foreach ($factories as $storageClass => $info) { + if (empty($info['supportedSchemeExtensions'])) { + continue; + } + + if (false === \in_array($protocol, $info['schemes'], true)) { + continue; + } + + $diff = array_diff($info['supportedSchemeExtensions'], $dsn->getSchemeExtensions()); + if (empty($diff)) { + return $storageClass; + } + } + } + + foreach ($factories as $storageClass => $info) { + if (false === \in_array($protocol, $info['schemes'], true)) { + continue; + } + + return $storageClass; + } + + return null; + } +} diff --git a/pkg/monitoring/InfluxDbStorage.php b/pkg/monitoring/InfluxDbStorage.php new file mode 100644 index 000000000..e39cccfd2 --- /dev/null +++ b/pkg/monitoring/InfluxDbStorage.php @@ -0,0 +1,264 @@ + 'influxdb://127.0.0.1:8086', + * 'host' => '127.0.0.1', + * 'port' => '8086', + * 'user' => '', + * 'password' => '', + * 'db' => 'enqueue', + * 'measurementSentMessages' => 'sent-messages', + * 'measurementConsumedMessages' => 'consumed-messages', + * 'measurementConsumers' => 'consumers', + * 'client' => null, # Client instance. Null by default. + * 'retentionPolicy' => null, + * ] + * + * or + * + * influxdb://127.0.0.1:8086?user=Jon&password=secret + * + * @param array|string|null $config + */ + public function __construct($config = 'influxdb:') + { + if (false == class_exists(Client::class)) { + throw new \LogicException('Seems client library is not installed. Please install "influxdb/influxdb-php"'); + } + + if (empty($config)) { + $config = []; + } elseif (is_string($config)) { + $config = self::parseDsn($config); + } elseif (is_array($config)) { + $config = empty($config['dsn']) ? $config : self::parseDsn($config['dsn']); + } elseif ($config instanceof Client) { + // Passing Client instead of array config is deprecated because it prevents setting any configuration values + // and causes library to use defaults. + @trigger_error( + sprintf('Passing %s as %s argument is deprecated. Pass it as "client" array property or use createWithClient instead', + Client::class, + __METHOD__ + ), \E_USER_DEPRECATED); + $this->client = $config; + $config = []; + } else { + throw new \LogicException('The config must be either an array of options, a DSN string or null'); + } + + $config = array_replace([ + 'host' => '127.0.0.1', + 'port' => '8086', + 'user' => '', + 'password' => '', + 'db' => 'enqueue', + 'measurementSentMessages' => 'sent-messages', + 'measurementConsumedMessages' => 'consumed-messages', + 'measurementConsumers' => 'consumers', + 'client' => null, + 'retentionPolicy' => null, + ], $config); + + if (null !== $config['client']) { + if (!$config['client'] instanceof Client) { + throw new \InvalidArgumentException(sprintf('%s configuration property is expected to be an instance of %s class. %s was passed instead.', 'client', Client::class, gettype($config['client']))); + } + $this->client = $config['client']; + } + + $this->config = $config; + } + + /** + * @param string $config + */ + public static function createWithClient(Client $client, $config = 'influxdb:'): self + { + if (is_string($config)) { + $config = self::parseDsn($config); + } + $config['client'] = $client; + + return new self($config); + } + + public function pushConsumerStats(ConsumerStats $stats): void + { + $points = []; + + foreach ($stats->getQueues() as $queue) { + $tags = [ + 'queue' => $queue, + 'consumerId' => $stats->getConsumerId(), + ]; + + $values = [ + 'startedAtMs' => $stats->getStartedAtMs(), + 'started' => $stats->isStarted(), + 'finished' => $stats->isFinished(), + 'failed' => $stats->isFailed(), + 'received' => $stats->getReceived(), + 'acknowledged' => $stats->getAcknowledged(), + 'rejected' => $stats->getRejected(), + 'requeued' => $stats->getRequeued(), + 'memoryUsage' => $stats->getMemoryUsage(), + 'systemLoad' => $stats->getSystemLoad(), + ]; + + if ($stats->getFinishedAtMs()) { + $values['finishedAtMs'] = $stats->getFinishedAtMs(); + } + + $points[] = new Point($this->config['measurementConsumers'], null, $tags, $values, $stats->getTimestampMs()); + } + + $this->doWrite($points); + } + + public function pushConsumedMessageStats(ConsumedMessageStats $stats): void + { + $tags = [ + 'queue' => $stats->getQueue(), + 'status' => $stats->getStatus(), + ]; + + $properties = $stats->getProperties(); + + if (false === empty($properties[Config::TOPIC])) { + $tags['topic'] = $properties[Config::TOPIC]; + } + + if (false === empty($properties[Config::COMMAND])) { + $tags['command'] = $properties[Config::COMMAND]; + } + + $values = [ + 'receivedAt' => $stats->getReceivedAtMs(), + 'processedAt' => $stats->getTimestampMs(), + 'redelivered' => $stats->isRedelivered(), + ]; + + if (ConsumedMessageStats::STATUS_FAILED === $stats->getStatus()) { + $values['failed'] = 1; + } + + $runtime = $stats->getTimestampMs() - $stats->getReceivedAtMs(); + + $points = [ + new Point($this->config['measurementConsumedMessages'], $runtime, $tags, $values, $stats->getTimestampMs()), + ]; + + $this->doWrite($points); + } + + public function pushSentMessageStats(SentMessageStats $stats): void + { + $tags = [ + 'destination' => $stats->getDestination(), + ]; + + $properties = $stats->getProperties(); + + if (false === empty($properties[Config::TOPIC])) { + $tags['topic'] = $properties[Config::TOPIC]; + } + + if (false === empty($properties[Config::COMMAND])) { + $tags['command'] = $properties[Config::COMMAND]; + } + + $points = [ + new Point($this->config['measurementSentMessages'], 1, $tags, [], $stats->getTimestampMs()), + ]; + + $this->doWrite($points); + } + + private function doWrite(array $points): void + { + if (null === $this->client) { + $this->client = new Client( + $this->config['host'], + $this->config['port'], + $this->config['user'], + $this->config['password'] + ); + } + + if ($this->client->getDriver() instanceof QueryDriverInterface) { + if (null === $this->database) { + $this->database = $this->client->selectDB($this->config['db']); + $this->database->create(); + } + + $this->database->writePoints($points, Database::PRECISION_MILLISECONDS, $this->config['retentionPolicy']); + } else { + // Code below mirrors what `writePoints` method of Database does. + try { + $parameters = [ + 'url' => sprintf('write?db=%s&precision=%s', $this->config['db'], Database::PRECISION_MILLISECONDS), + 'database' => $this->config['db'], + 'method' => 'post', + ]; + if (null !== $this->config['retentionPolicy']) { + $parameters['url'] .= sprintf('&rp=%s', $this->config['retentionPolicy']); + } + + $this->client->write($parameters, $points); + } catch (\Exception $e) { + throw new InfluxDBException($e->getMessage(), $e->getCode()); + } + } + } + + private static function parseDsn(string $dsn): array + { + $dsn = Dsn::parseFirst($dsn); + + if (false === in_array($dsn->getSchemeProtocol(), ['influxdb'], true)) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "influxdb"', $dsn->getSchemeProtocol())); + } + + return array_filter(array_replace($dsn->getQuery(), [ + 'host' => $dsn->getHost(), + 'port' => $dsn->getPort(), + 'user' => $dsn->getUser(), + 'password' => $dsn->getPassword(), + 'db' => $dsn->getString('db'), + 'measurementSentMessages' => $dsn->getString('measurementSentMessages'), + 'measurementConsumedMessages' => $dsn->getString('measurementConsumedMessages'), + 'measurementConsumers' => $dsn->getString('measurementConsumers'), + 'retentionPolicy' => $dsn->getString('retentionPolicy'), + ]), function ($value) { return null !== $value; }); + } +} diff --git a/pkg/monitoring/JsonSerializer.php b/pkg/monitoring/JsonSerializer.php new file mode 100644 index 000000000..8d046092a --- /dev/null +++ b/pkg/monitoring/JsonSerializer.php @@ -0,0 +1,31 @@ + $rfClass->getShortName(), + ]; + + foreach ($rfClass->getProperties() as $rfProperty) { + $rfProperty->setAccessible(true); + $data[$rfProperty->getName()] = $rfProperty->getValue($stats); + $rfProperty->setAccessible(false); + } + + $json = json_encode($data); + + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); + } + + return $json; + } +} diff --git a/pkg/monitoring/LICENSE b/pkg/monitoring/LICENSE new file mode 100644 index 000000000..7afbaa1ff --- /dev/null +++ b/pkg/monitoring/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright (c) 2018 Forma-Pro + +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/pkg/monitoring/README.md b/pkg/monitoring/README.md new file mode 100644 index 000000000..dfd33f056 --- /dev/null +++ b/pkg/monitoring/README.md @@ -0,0 +1,42 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + +# Enqueue Monitoring + +Queue Monitoring tool. Track sent, consumed messages. Consumers performances. + +* Could be used with any message queue library. +* Could be integrated to any PHP framework +* Could send stats to any analytical platform +* Supports Datadog, InfluxDb, Grafana and WAMP out of the box. +* Provides integration for Enqueue + +[![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/monitoring/ci.yml?branch=master)](https://github.com/php-enqueue/monitoring/actions?query=workflow%3ACI) +[![Total Downloads](https://poser.pugx.org/enqueue/monitoring/d/total.png)](https://packagist.org/packages/enqueue/monitoring) +[![Latest Stable Version](https://poser.pugx.org/enqueue/monitoring/version.png)](https://packagist.org/packages/enqueue/monitoring) + +## Resources + +* [Site](https://enqueue.forma-pro.com/) +* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/monitoring.md) +* [Questions](https://gitter.im/php-enqueue/Lobby) +* [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) + +## Developed by Forma-Pro + +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. + +If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com + +## License + +It is released under the [MIT License](LICENSE). diff --git a/pkg/monitoring/Resources.php b/pkg/monitoring/Resources.php new file mode 100644 index 000000000..409d9861f --- /dev/null +++ b/pkg/monitoring/Resources.php @@ -0,0 +1,55 @@ + $item) { + foreach ($item['schemes'] as $scheme) { + $schemes[$scheme] = $storageClass; + } + } + + return $schemes; + } + + public static function getKnownStorages(): array + { + if (null === self::$knownStorages) { + $map = []; + + $map[WampStorage::class] = [ + 'schemes' => ['wamp', 'ws'], + 'supportedSchemeExtensions' => [], + ]; + + $map[InfluxDbStorage::class] = [ + 'schemes' => ['influxdb'], + 'supportedSchemeExtensions' => [], + ]; + + $map[DatadogStorage::class] = [ + 'schemes' => ['datadog'], + 'supportedSchemeExtensions' => [], + ]; + + self::$knownStorages = $map; + } + + return self::$knownStorages; + } +} diff --git a/pkg/monitoring/SentMessageStats.php b/pkg/monitoring/SentMessageStats.php new file mode 100644 index 000000000..f8ddc73be --- /dev/null +++ b/pkg/monitoring/SentMessageStats.php @@ -0,0 +1,96 @@ +timestampMs = $timestampMs; + $this->destination = $destination; + $this->isTopic = $isTopic; + $this->messageId = $messageId; + $this->correlationId = $correlationId; + $this->headers = $headers; + $this->properties = $properties; + } + + public function getTimestampMs(): int + { + return $this->timestampMs; + } + + public function getDestination(): string + { + return $this->destination; + } + + public function isTopic(): bool + { + return $this->isTopic; + } + + public function getMessageId(): ?string + { + return $this->messageId; + } + + public function getCorrelationId(): ?string + { + return $this->correlationId; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function getProperties(): array + { + return $this->properties; + } +} diff --git a/pkg/monitoring/Serializer.php b/pkg/monitoring/Serializer.php new file mode 100644 index 000000000..227dbc602 --- /dev/null +++ b/pkg/monitoring/Serializer.php @@ -0,0 +1,10 @@ +diUtils = DiUtils::create(self::MODULE, $name); + } + + public static function getConfiguration(string $name = 'monitoring'): ArrayNodeDefinition + { + $builder = new ArrayNodeDefinition($name); + + $builder + ->info(sprintf('The "%s" option could accept a string DSN, an array with DSN key, or null. It accept extra options. To find out what option you can set, look at stats storage constructor doc block.', $name)) + ->beforeNormalization() + ->always(function ($v) { + if (\is_array($v)) { + if (isset($v['storage_factory_class'], $v['storage_factory_service'])) { + throw new \LogicException('Both options storage_factory_class and storage_factory_service are set. Please choose one.'); + } + + return $v; + } + + if (is_string($v)) { + return ['dsn' => $v]; + } + + return $v; + }) + ->end() + ->ignoreExtraKeys(false) + ->children() + ->scalarNode('dsn') + ->cannotBeEmpty() + ->isRequired() + ->info(sprintf('The stats storage DSN. These schemes are supported: "%s".', implode('", "', array_keys(Resources::getKnownSchemes())))) + ->end() + ->scalarNode('storage_factory_service') + ->info(sprintf('The factory class should implement "%s" interface', StatsStorageFactory::class)) + ->end() + ->scalarNode('storage_factory_class') + ->info(sprintf('The factory service should be a class that implements "%s" interface', StatsStorageFactory::class)) + ->end() + ->end() + ; + + return $builder; + } + + public function buildStorage(ContainerBuilder $container, array $config): void + { + $storageId = $this->diUtils->format('storage'); + $storageFactoryId = $this->diUtils->format('storage.factory'); + + if (isset($config['storage_factory_service'])) { + $container->setAlias($storageFactoryId, $config['storage_factory_service']); + } elseif (isset($config['storage_factory_class'])) { + $container->register($storageFactoryId, $config['storage_factory_class']); + } else { + $container->register($storageFactoryId, GenericStatsStorageFactory::class); + } + + unset($config['storage_factory_service'], $config['storage_factory_class']); + + $container->register($storageId, StatsStorage::class) + ->setFactory([new Reference($storageFactoryId), 'create']) + ->addArgument($config) + ; + } + + public function buildClientExtension(ContainerBuilder $container, array $config): void + { + $container->register($this->diUtils->format('client_extension'), ClientMonitoringExtension::class) + ->addArgument($this->diUtils->reference('storage')) + ->addArgument(new Reference('logger')) + ->addTag('enqueue.client_extension', ['client' => $this->diUtils->getConfigName()]) + ; + } + + public function buildConsumerExtension(ContainerBuilder $container, array $config): void + { + $container->register($this->diUtils->format('consumer_extension'), ConsumerMonitoringExtension::class) + ->addArgument($this->diUtils->reference('storage')) + ->addTag('enqueue.consumption_extension', ['client' => $this->diUtils->getConfigName()]) + ->addTag('enqueue.transport.consumption_extension', ['transport' => $this->diUtils->getConfigName()]) + ; + } +} diff --git a/pkg/monitoring/Tests/GenericStatsStorageFactoryTest.php b/pkg/monitoring/Tests/GenericStatsStorageFactoryTest.php new file mode 100644 index 000000000..fe7c2c759 --- /dev/null +++ b/pkg/monitoring/Tests/GenericStatsStorageFactoryTest.php @@ -0,0 +1,52 @@ +assertClassImplements(StatsStorageFactory::class, GenericStatsStorageFactory::class); + } + + public function testShouldCreateInfluxDbStorage(): void + { + $storage = (new GenericStatsStorageFactory())->create('influxdb:'); + + $this->assertInstanceOf(InfluxDbStorage::class, $storage); + } + + public function testShouldCreateWampStorage(): void + { + $storage = (new GenericStatsStorageFactory())->create('wamp:'); + + $this->assertInstanceOf(WampStorage::class, $storage); + } + + public function testShouldCreateDatadogStorage(): void + { + $storage = (new GenericStatsStorageFactory())->create('datadog:'); + + $this->assertInstanceOf(DatadogStorage::class, $storage); + } + + public function testShouldThrowIfStorageIsNotSupported(): void + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('A given scheme "unsupported" is not supported.'); + + (new GenericStatsStorageFactory())->create('unsupported:'); + } +} diff --git a/pkg/monitoring/WampStorage.php b/pkg/monitoring/WampStorage.php new file mode 100644 index 000000000..0d5ba1801 --- /dev/null +++ b/pkg/monitoring/WampStorage.php @@ -0,0 +1,211 @@ + 'wamp://127.0.0.1:9090', + * 'host' => '127.0.0.1', + * 'port' => '9090', + * 'topic' => 'stats', + * 'max_retries' => 15, + * 'initial_retry_delay' => 1.5, + * 'max_retry_delay' => 300, + * 'retry_delay_growth' => 1.5, + * ] + * + * or + * + * wamp://127.0.0.1:9090?max_retries=10 + * + * @param array|string|null $config + */ + public function __construct($config = 'wamp:') + { + if (false == class_exists(Client::class) || false == class_exists(PawlTransportProvider::class)) { + throw new \LogicException('Seems client libraries are not installed. Please install "thruway/client" and "thruway/pawl-transport"'); + } + + if (empty($config)) { + $config = $this->parseDsn('wamp:'); + } elseif (is_string($config)) { + $config = $this->parseDsn($config); + } elseif (is_array($config)) { + $config = empty($config['dsn']) ? $config : $this->parseDsn($config['dsn']); + } else { + throw new \LogicException('The config must be either an array of options, a DSN string or null'); + } + + $config = array_replace([ + 'host' => '127.0.0.1', + 'port' => '9090', + 'topic' => 'stats', + 'max_retries' => 15, + 'initial_retry_delay' => 1.5, + 'max_retry_delay' => 300, + 'retry_delay_growth' => 1.5, + ], $config); + + $this->config = $config; + + $this->serialiser = new JsonSerializer(); + } + + public function pushConsumerStats(ConsumerStats $stats): void + { + $this->push($stats); + } + + public function pushConsumedMessageStats(ConsumedMessageStats $stats): void + { + $this->push($stats); + } + + public function pushSentMessageStats(SentMessageStats $stats): void + { + $this->push($stats); + } + + private function push(Stats $stats) + { + $init = false; + $this->stats = $stats; + + if (null === $this->client) { + $init = true; + + $this->client = $this->createClient(); + $this->client->setAttemptRetry(true); + $this->client->on('open', function (ClientSession $session) { + $this->session = $session; + + $this->doSendMessageIfPossible(); + }); + + $this->client->on('close', function () { + if ($this->session === $this->client->getSession()) { + $this->session = null; + } + }); + + $this->client->on('error', function () { + if ($this->session === $this->client->getSession()) { + $this->session = null; + } + }); + + $this->client->on('do-send', function (Stats $stats) { + $onFinish = function () { + $this->client->emit('do-stop'); + }; + + $payload = $this->serialiser->toString($stats); + + $this->session->publish('stats', [$payload], [], ['acknowledge' => true]) + ->then($onFinish, $onFinish); + }); + + $this->client->on('do-stop', function () { + $this->client->getLoop()->stop(); + }); + } + + $this->client->getLoop()->futureTick(function () { + $this->doSendMessageIfPossible(); + }); + + if ($init) { + $this->client->start(false); + } + + $this->client->getLoop()->run(); + } + + private function doSendMessageIfPossible() + { + if (null === $this->session) { + return; + } + + if (null === $this->stats) { + return; + } + + $stats = $this->stats; + + $this->stats = null; + + $this->client->emit('do-send', [$stats]); + } + + private function createClient(): Client + { + $uri = sprintf('ws://%s:%s', $this->config['host'], $this->config['port']); + + $client = new Client('realm1'); + $client->addTransportProvider(new PawlTransportProvider($uri)); + $client->setReconnectOptions([ + 'max_retries' => $this->config['max_retries'], + 'initial_retry_delay' => $this->config['initial_retry_delay'], + 'max_retry_delay' => $this->config['max_retry_delay'], + 'retry_delay_growth' => $this->config['retry_delay_growth'], + ]); + + return $client; + } + + private function parseDsn(string $dsn): array + { + $dsn = Dsn::parseFirst($dsn); + + if (false === in_array($dsn->getSchemeProtocol(), ['wamp', 'ws'], true)) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "wamp"', $dsn->getSchemeProtocol())); + } + + return array_filter(array_replace($dsn->getQuery(), [ + 'host' => $dsn->getHost(), + 'port' => $dsn->getPort(), + 'topic' => $dsn->getString('topic'), + 'max_retries' => $dsn->getDecimal('max_retries'), + 'initial_retry_delay' => $dsn->getFloat('initial_retry_delay'), + 'max_retry_delay' => $dsn->getDecimal('max_retry_delay'), + 'retry_delay_growth' => $dsn->getFloat('retry_delay_growth'), + ]), function ($value) { return null !== $value; }); + } +} diff --git a/pkg/monitoring/composer.json b/pkg/monitoring/composer.json new file mode 100644 index 000000000..13b57a5f2 --- /dev/null +++ b/pkg/monitoring/composer.json @@ -0,0 +1,47 @@ +{ + "name": "enqueue/monitoring", + "type": "library", + "description": "Enqueue Monitoring", + "keywords": ["messaging", "queue", "monitoring", "grafana"], + "homepage": "https://enqueue.forma-pro.com/", + "license": "MIT", + "require": { + "php": "^8.1", + "enqueue/enqueue": "^0.10", + "enqueue/dsn": "^0.10" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "influxdb/influxdb-php": "^1.14", + "datadog/php-datadogstatsd": "^1.3", + "thruway/client": "^0.5.5", + "thruway/pawl-transport": "^0.5", + "voryx/thruway-common": "^1.0.1" + }, + "suggest": { + "thruway/client": "Client for Thruway and the WAMP (Web Application Messaging Protocol).", + "thruway/pawl-transport": "Pawl WebSocket Transport for Thruway Client", + "influxdb/influxdb-php": "A PHP Client for InfluxDB, a time series database", + "datadog/php-datadogstatsd": "Datadog monitoring tool PHP integration" + }, + "support": { + "email": "opensource@forma-pro.com", + "issues": "https://github.com/php-enqueue/enqueue-dev/issues", + "forum": "https://gitter.im/php-enqueue/Lobby", + "source": "https://github.com/php-enqueue/enqueue-dev", + "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" + }, + "autoload": { + "psr-4": { "Enqueue\\Monitoring\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "0.10.x-dev" + } + } +} diff --git a/pkg/monitoring/phpunit.xml.dist b/pkg/monitoring/phpunit.xml.dist new file mode 100644 index 000000000..254ab22d6 --- /dev/null +++ b/pkg/monitoring/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + + ./Tests + + + + + + . + + ./vendor + ./Tests + + + + diff --git a/pkg/null/.github/workflows/ci.yml b/pkg/null/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/null/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/null/.travis.yml b/pkg/null/.travis.yml deleted file mode 100644 index b9cf57fc9..000000000 --- a/pkg/null/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/null/Client/NullDriver.php b/pkg/null/Client/NullDriver.php deleted file mode 100644 index 156cea241..000000000 --- a/pkg/null/Client/NullDriver.php +++ /dev/null @@ -1,160 +0,0 @@ -context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - * - * @return NullMessage - */ - public function createTransportMessage(Message $message) - { - $headers = $message->getHeaders(); - $headers['content_type'] = $message->getContentType(); - $headers['expiration'] = $message->getExpire(); - $headers['delay'] = $message->getDelay(); - $headers['priority'] = $message->getPriority(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($message->getProperties()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - - return $transportMessage; - } - - /** - * {@inheritdoc} - * - * @param NullMessage $message - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - if ($contentType = $message->getHeader('content_type')) { - $clientMessage->setContentType($contentType); - } - - if ($expiration = $message->getHeader('expiration')) { - $clientMessage->setExpire($expiration); - } - - if ($delay = $message->getHeader('delay')) { - $clientMessage->setDelay($delay); - } - - if ($priority = $message->getHeader('priority')) { - $clientMessage->setPriority($priority); - } - - return $clientMessage; - } - - /** - * {@inheritdoc} - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - return $this->context->createQueue($transportName); - } - - /** - * {@inheritdoc} - */ - public function getConfig() - { - return $this->config; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - $transportMessage = $this->createTransportMessage($message); - $topic = $this->context->createTopic( - $this->config->createTransportRouterTopicName( - $this->config->getRouterTopicName() - ) - ); - - $this->context->createProducer()->send($topic, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - $transportMessage = $this->createTransportMessage($message); - $queue = $this->context->createQueue( - $this->config->createTransportQueueName( - $this->config->getRouterQueueName() - ) - ); - - $this->context->createProducer()->send($queue, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger ?: new NullLogger(); - $logger->debug('[NullDriver] setup broker'); - } -} diff --git a/pkg/null/NullConnectionFactory.php b/pkg/null/NullConnectionFactory.php index 3e58fea98..b89cd5089 100644 --- a/pkg/null/NullConnectionFactory.php +++ b/pkg/null/NullConnectionFactory.php @@ -1,15 +1,18 @@ queue = $queue; } - /** - * {@inheritdoc} - */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } /** - * {@inheritdoc} + * @return NullMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { return null; } /** - * {@inheritdoc} + * @return NullMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { return null; } - /** - * {@inheritdoc} - */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { } - /** - * {@inheritdoc} - */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { } } diff --git a/pkg/null/NullContext.php b/pkg/null/NullContext.php index 2d2f48cb3..5f6001cab 100644 --- a/pkg/null/NullContext.php +++ b/pkg/null/NullContext.php @@ -1,18 +1,24 @@ setBody($body); @@ -23,55 +29,58 @@ public function createMessage($body = null, array $properties = [], array $heade } /** - * {@inheritdoc} - * * @return NullQueue */ - public function createQueue($name) + public function createQueue(string $name): Queue { return new NullQueue($name); } /** - * {@inheritdoc} + * @return NullQueue */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { return $this->createQueue(uniqid('', true)); } /** - * {@inheritdoc} - * * @return NullTopic */ - public function createTopic($name) + public function createTopic(string $name): Topic { return new NullTopic($name); } /** - * {@inheritdoc} - * * @return NullConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { return new NullConsumer($destination); } /** - * {@inheritdoc} + * @return NullProducer */ - public function createProducer() + public function createProducer(): Producer { return new NullProducer(); } /** - * {@inheritdoc} + * @return NullSubscriptionConsumer */ - public function close() + public function createSubscriptionConsumer(): SubscriptionConsumer + { + return new NullSubscriptionConsumer(); + } + + public function purgeQueue(Queue $queue): void + { + } + + public function close(): void { } } diff --git a/pkg/null/NullMessage.php b/pkg/null/NullMessage.php index 4e6a69210..93fc57c3d 100644 --- a/pkg/null/NullMessage.php +++ b/pkg/null/NullMessage.php @@ -1,10 +1,12 @@ body = $body; $this->properties = $properties; @@ -35,106 +37,67 @@ public function __construct($body = '', array $properties = [], array $headers = $this->redelivered = false; } - /** - * {@inheritdoc} - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * {@inheritdoc} - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * {@inheritdoc} - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * {@inheritdoc} - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * {@inheritdoc} - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = $redelivered; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $headers = $this->getHeaders(); $headers['correlation_id'] = (string) $correlationId; @@ -142,18 +105,12 @@ public function setCorrelationId($correlationId) $this->setHeaders($headers); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id'); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $headers = $this->getHeaders(); $headers['message_id'] = (string) $messageId; @@ -161,28 +118,19 @@ public function setMessageId($messageId) $this->setHeaders($headers); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id'); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $headers = $this->getHeaders(); $headers['timestamp'] = (int) $timestamp; @@ -190,18 +138,12 @@ public function setTimestamp($timestamp) $this->setHeaders($headers); } - /** - * @param string|null $replyTo - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * @return string|null - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } diff --git a/pkg/null/NullProducer.php b/pkg/null/NullProducer.php index 47169a635..1349de9ba 100644 --- a/pkg/null/NullProducer.php +++ b/pkg/null/NullProducer.php @@ -1,12 +1,14 @@ deliveryDelay = $deliveryDelay; return $this; } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return $this->deliveryDelay; } /** - * {@inheritdoc} + * @return NullProducer */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { $this->priority = $priority; return $this; } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return $this->priority; } /** - * {@inheritdoc} + * @return NullProducer */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { $this->timeToLive = $timeToLive; return $this; } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return $this->timeToLive; } diff --git a/pkg/null/NullQueue.php b/pkg/null/NullQueue.php index d99e010e8..543111e67 100644 --- a/pkg/null/NullQueue.php +++ b/pkg/null/NullQueue.php @@ -1,28 +1,24 @@ name = $name; } - /** - * @return string - */ - public function getQueueName() + public function getQueueName(): string { return $this->name; } diff --git a/pkg/null/NullSubscriptionConsumer.php b/pkg/null/NullSubscriptionConsumer.php new file mode 100644 index 000000000..1e9591666 --- /dev/null +++ b/pkg/null/NullSubscriptionConsumer.php @@ -0,0 +1,27 @@ +name = $name; } - /** - * @return string - */ - public function getTopicName() + public function getTopicName(): string { return $this->name; } diff --git a/pkg/null/README.md b/pkg/null/README.md index bebd31257..7d78ae0d6 100644 --- a/pkg/null/README.md +++ b/pkg/null/README.md @@ -1,23 +1,32 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Enqueue Null Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/null.png?branch=master)](https://travis-ci.org/php-enqueue/null) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/null/ci.yml?branch=master)](https://github.com/php-enqueue/null/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/null/d/total.png)](https://packagist.org/packages/enqueue/null) [![Latest Stable Version](https://poser.pugx.org/enqueue/null/version.png)](https://packagist.org/packages/enqueue/null) - -This is an implementation of PSR queue specification. It does not send messages any where and could be used as mock. Suitable in tests. + +This is an implementation of Queue Interop specification. It does not send messages any where and could be used as mock. Suitable in tests. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/null/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com diff --git a/pkg/null/Symfony/NullTransportFactory.php b/pkg/null/Symfony/NullTransportFactory.php deleted file mode 100644 index 7d70b8426..000000000 --- a/pkg/null/Symfony/NullTransportFactory.php +++ /dev/null @@ -1,105 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifString()->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('dsn')->end() - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $factory = new Definition(NullConnectionFactory::class); - - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - - $context = new Definition(NullContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(NullDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/null/Tests/Client/NullDriverTest.php b/pkg/null/Tests/Client/NullDriverTest.php deleted file mode 100644 index c233b1a2b..000000000 --- a/pkg/null/Tests/Client/NullDriverTest.php +++ /dev/null @@ -1,277 +0,0 @@ -createDummyQueueMetaRegistry()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new NullQueue('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new NullDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new NullQueue('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new NullDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldSendMessageToRouter() - { - $config = Config::create(); - $topic = new NullTopic('topic'); - - $transportMessage = new NullMessage(); - - $producer = $this->createMessageProducer(); - $producer - ->expects(self::once()) - ->method('send') - ->with(self::identicalTo($topic), self::identicalTo($transportMessage)) - ; - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - - $driver = new NullDriver($context, $config, $this->createDummyQueueMetaRegistry()); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $config = Config::create(); - $queue = new NullQueue(''); - - $transportMessage = new NullMessage(); - - $producer = $this->createMessageProducer(); - $producer - ->expects(self::once()) - ->method('send') - ->with(self::identicalTo($queue), self::identicalTo($transportMessage)) - ; - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - - $driver = new NullDriver($context, $config, $this->createDummyQueueMetaRegistry()); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $config = Config::create(); - - $clientMessage = new Message(); - $clientMessage->setBody('theBody'); - $clientMessage->setContentType('theContentType'); - $clientMessage->setMessageId('theMessageId'); - $clientMessage->setTimestamp(12345); - $clientMessage->setDelay(123); - $clientMessage->setExpire(345); - $clientMessage->setPriority(MessagePriority::LOW); - $clientMessage->setHeaders(['theHeaderFoo' => 'theFoo']); - $clientMessage->setProperties(['thePropertyBar' => 'theBar']); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $transportMessage = new NullMessage(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new NullDriver($context, $config, $this->createDummyQueueMetaRegistry()); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - self::assertSame('theBody', $transportMessage->getBody()); - self::assertSame([ - 'theHeaderFoo' => 'theFoo', - 'content_type' => 'theContentType', - 'expiration' => 345, - 'delay' => 123, - 'priority' => MessagePriority::LOW, - 'timestamp' => 12345, - 'message_id' => 'theMessageId', - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $transportMessage->getHeaders()); - self::assertSame([ - 'thePropertyBar' => 'theBar', - ], $transportMessage->getProperties()); - - $this->assertSame('theMessageId', $transportMessage->getMessageId()); - $this->assertSame(12345, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $config = Config::create(); - - $transportMessage = new NullMessage(); - $transportMessage->setBody('theBody'); - $transportMessage->setHeaders(['theHeaderFoo' => 'theFoo']); - $transportMessage->setTimestamp(12345); - $transportMessage->setMessageId('theMessageId'); - $transportMessage->setHeader('priority', MessagePriority::LOW); - $transportMessage->setHeader('content_type', 'theContentType'); - $transportMessage->setHeader('delay', 123); - $transportMessage->setHeader('expiration', 345); - $transportMessage->setProperties(['thePropertyBar' => 'theBar']); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new NullDriver($this->createPsrContextMock(), $config, $this->createDummyQueueMetaRegistry()); - - $clientMessage = $driver->createClientMessage($transportMessage); - - self::assertSame('theBody', $clientMessage->getBody()); - self::assertSame(MessagePriority::LOW, $clientMessage->getPriority()); - self::assertSame('theContentType', $clientMessage->getContentType()); - self::assertSame(123, $clientMessage->getDelay()); - self::assertSame(345, $clientMessage->getExpire()); - self::assertEquals([ - 'theHeaderFoo' => 'theFoo', - 'content_type' => 'theContentType', - 'expiration' => 345, - 'delay' => 123, - 'priority' => MessagePriority::LOW, - 'timestamp' => 12345, - 'message_id' => 'theMessageId', - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $clientMessage->getHeaders()); - self::assertSame([ - 'thePropertyBar' => 'theBar', - ], $clientMessage->getProperties()); - - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - } - - public function testShouldReturnConfigInstance() - { - $config = Config::create(); - - $driver = new NullDriver($this->createPsrContextMock(), $config, $this->createDummyQueueMetaRegistry()); - $result = $driver->getConfig(); - - self::assertSame($config, $result); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|NullContext - */ - private function createPsrContextMock() - { - return $this->createMock(NullContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|NullProducer - */ - private function createMessageProducer() - { - return $this->createMock(NullProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/null/Tests/NullConnectionFactoryTest.php b/pkg/null/Tests/NullConnectionFactoryTest.php index a1e6e90bb..bbf377e85 100644 --- a/pkg/null/Tests/NullConnectionFactoryTest.php +++ b/pkg/null/Tests/NullConnectionFactoryTest.php @@ -5,7 +5,7 @@ use Enqueue\Null\NullConnectionFactory; use Enqueue\Null\NullContext; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Interop\Queue\ConnectionFactory; use PHPUnit\Framework\TestCase; class NullConnectionFactoryTest extends TestCase @@ -14,12 +14,7 @@ class NullConnectionFactoryTest extends TestCase public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, NullConnectionFactory::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new NullConnectionFactory(); + $this->assertClassImplements(ConnectionFactory::class, NullConnectionFactory::class); } public function testShouldReturnNullContextOnCreateContextCall() diff --git a/pkg/null/Tests/NullConsumerTest.php b/pkg/null/Tests/NullConsumerTest.php index e7e6ae6f9..f4de53311 100644 --- a/pkg/null/Tests/NullConsumerTest.php +++ b/pkg/null/Tests/NullConsumerTest.php @@ -6,7 +6,7 @@ use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConsumer; +use Interop\Queue\Consumer; use PHPUnit\Framework\TestCase; class NullConsumerTest extends TestCase @@ -15,12 +15,7 @@ class NullConsumerTest extends TestCase public function testShouldImplementMessageConsumerInterface() { - $this->assertClassImplements(PsrConsumer::class, NullConsumer::class); - } - - public function testCouldBeConstructedWithQueueAsArgument() - { - new NullConsumer(new NullQueue('aName')); + $this->assertClassImplements(Consumer::class, NullConsumer::class); } public function testShouldAlwaysReturnNullOnReceive() @@ -41,6 +36,9 @@ public function testShouldAlwaysReturnNullOnReceiveNoWait() $this->assertNull($consumer->receiveNoWait()); } + /** + * @doesNotPerformAssertions + */ public function testShouldDoNothingOnAcknowledge() { $consumer = new NullConsumer(new NullQueue('theQueueName')); @@ -48,6 +46,9 @@ public function testShouldDoNothingOnAcknowledge() $consumer->acknowledge(new NullMessage()); } + /** + * @doesNotPerformAssertions + */ public function testShouldDoNothingOnReject() { $consumer = new NullConsumer(new NullQueue('theQueueName')); diff --git a/pkg/null/Tests/NullContextTest.php b/pkg/null/Tests/NullContextTest.php index a17f42aa9..f0da566d2 100644 --- a/pkg/null/Tests/NullContextTest.php +++ b/pkg/null/Tests/NullContextTest.php @@ -9,7 +9,7 @@ use Enqueue\Null\NullQueue; use Enqueue\Null\NullTopic; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use PHPUnit\Framework\TestCase; class NullContextTest extends TestCase @@ -18,12 +18,7 @@ class NullContextTest extends TestCase public function testShouldImplementSessionInterface() { - $this->assertClassImplements(PsrContext::class, NullContext::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new NullContext(); + $this->assertClassImplements(Context::class, NullContext::class); } public function testShouldAllowCreateMessageWithoutAnyArguments() @@ -34,7 +29,7 @@ public function testShouldAllowCreateMessageWithoutAnyArguments() $this->assertInstanceOf(NullMessage::class, $message); - $this->assertNull($message->getBody()); + $this->assertSame('', $message->getBody()); $this->assertSame([], $message->getHeaders()); $this->assertSame([], $message->getProperties()); } diff --git a/pkg/null/Tests/NullMessageTest.php b/pkg/null/Tests/NullMessageTest.php index 9870c7c21..f8a7090bb 100644 --- a/pkg/null/Tests/NullMessageTest.php +++ b/pkg/null/Tests/NullMessageTest.php @@ -4,7 +4,7 @@ use Enqueue\Null\NullMessage; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrMessage; +use Interop\Queue\Message; use PHPUnit\Framework\TestCase; class NullMessageTest extends TestCase @@ -13,7 +13,7 @@ class NullMessageTest extends TestCase public function testShouldImplementMessageInterface() { - $this->assertClassImplements(PsrMessage::class, NullMessage::class); + $this->assertClassImplements(Message::class, NullMessage::class); } public function testCouldBeConstructedWithoutAnyArguments() diff --git a/pkg/null/Tests/NullProducerTest.php b/pkg/null/Tests/NullProducerTest.php index c9f89b6e3..140d683ba 100644 --- a/pkg/null/Tests/NullProducerTest.php +++ b/pkg/null/Tests/NullProducerTest.php @@ -6,7 +6,7 @@ use Enqueue\Null\NullProducer; use Enqueue\Null\NullTopic; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrProducer; +use Interop\Queue\Producer; use PHPUnit\Framework\TestCase; class NullProducerTest extends TestCase @@ -15,14 +15,12 @@ class NullProducerTest extends TestCase public function testShouldImplementProducerInterface() { - $this->assertClassImplements(PsrProducer::class, NullProducer::class); - } - - public function testCouldBeConstructedWithoutAnyArguments() - { - new NullProducer(); + $this->assertClassImplements(Producer::class, NullProducer::class); } + /** + * @doesNotPerformAssertions + */ public function testShouldDoNothingOnSend() { $producer = new NullProducer(); diff --git a/pkg/null/Tests/NullQueueTest.php b/pkg/null/Tests/NullQueueTest.php index ff2c9bd74..cb29ca180 100644 --- a/pkg/null/Tests/NullQueueTest.php +++ b/pkg/null/Tests/NullQueueTest.php @@ -4,7 +4,7 @@ use Enqueue\Null\NullQueue; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrQueue; +use Interop\Queue\Queue; use PHPUnit\Framework\TestCase; class NullQueueTest extends TestCase @@ -13,12 +13,7 @@ class NullQueueTest extends TestCase public function testShouldImplementQueueInterface() { - $this->assertClassImplements(PsrQueue::class, NullQueue::class); - } - - public function testCouldBeConstructedWithNameAsArgument() - { - new NullQueue('aName'); + $this->assertClassImplements(Queue::class, NullQueue::class); } public function testShouldAllowGetNameSetInConstructor() diff --git a/pkg/null/Tests/NullTopicTest.php b/pkg/null/Tests/NullTopicTest.php index b07641e2f..27c4b58de 100644 --- a/pkg/null/Tests/NullTopicTest.php +++ b/pkg/null/Tests/NullTopicTest.php @@ -4,7 +4,7 @@ use Enqueue\Null\NullTopic; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrTopic; +use Interop\Queue\Topic; use PHPUnit\Framework\TestCase; class NullTopicTest extends TestCase @@ -13,12 +13,7 @@ class NullTopicTest extends TestCase public function testShouldImplementTopicInterface() { - $this->assertClassImplements(PsrTopic::class, NullTopic::class); - } - - public function testCouldBeConstructedWithNameAsArgument() - { - new NullTopic('aName'); + $this->assertClassImplements(Topic::class, NullTopic::class); } public function testShouldAllowGetNameSetInConstructor() diff --git a/pkg/null/Tests/Spec/NullMessageTest.php b/pkg/null/Tests/Spec/NullMessageTest.php index 02b5cc2be..6bacc9294 100644 --- a/pkg/null/Tests/Spec/NullMessageTest.php +++ b/pkg/null/Tests/Spec/NullMessageTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Null\Tests\Spec; use Enqueue\Null\NullMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class NullMessageTest extends PsrMessageSpec +class NullMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new NullMessage(); diff --git a/pkg/null/Tests/Symfony/NullTransportFactoryTest.php b/pkg/null/Tests/Symfony/NullTransportFactoryTest.php deleted file mode 100644 index b6c80f154..000000000 --- a/pkg/null/Tests/Symfony/NullTransportFactoryTest.php +++ /dev/null @@ -1,136 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, NullTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new NullTransportFactory(); - - $this->assertEquals('null', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new NullTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new NullTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [true]); - - $this->assertEquals([], $config); - } - - public function testShouldAllowAddConfigurationWithDsnString() - { - $transport = new NullTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['aStringDSN']); - - $this->assertEquals(['dsn' => 'aStringDSN'], $config); - } - - public function testShouldAllowAddConfigurationWithDsnStringOption() - { - $transport = new NullTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [['dsn' => 'aStringDSN']]); - - $this->assertEquals(['dsn' => 'aStringDSN'], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new NullTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, []); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(NullConnectionFactory::class, $factory->getClass()); - $this->assertSame([], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new NullTransportFactory(); - - $serviceId = $transport->createContext($container, []); - - $this->assertEquals('enqueue.transport.null.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition($serviceId); - $this->assertEquals(NullContext::class, $context->getClass()); - $this->assertEquals( - [new Reference('enqueue.transport.null.connection_factory'), 'createContext'], - $context->getFactory() - ); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new NullTransportFactory(); - - $driverId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.null.driver', $driverId); - $this->assertTrue($container->hasDefinition($driverId)); - - $driver = $container->getDefinition($driverId); - $this->assertEquals(NullDriver::class, $driver->getClass()); - $this->assertNull($driver->getFactory()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.null.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/null/composer.json b/pkg/null/composer.json index d02614971..a09910a47 100644 --- a/pkg/null/composer.json +++ b/pkg/null/composer.json @@ -6,16 +6,13 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1" + "php": "^8.1", + "queue-interop/queue-interop": "^0.8" }, "require-dev": { - "phpunit/phpunit": "~5.5", - "enqueue/enqueue": "^0.8@dev", - "enqueue/test": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -33,7 +30,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/null/phpunit.xml.dist b/pkg/null/phpunit.xml.dist index 2fd4e20c3..07729246d 100644 --- a/pkg/null/phpunit.xml.dist +++ b/pkg/null/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/pheanstalk/.github/workflows/ci.yml b/pkg/pheanstalk/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/pheanstalk/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/pheanstalk/.travis.yml b/pkg/pheanstalk/.travis.yml deleted file mode 100644 index b9cf57fc9..000000000 --- a/pkg/pheanstalk/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/pheanstalk/PheanstalkConnectionFactory.php b/pkg/pheanstalk/PheanstalkConnectionFactory.php index 40999ccef..d3cd5f0c0 100644 --- a/pkg/pheanstalk/PheanstalkConnectionFactory.php +++ b/pkg/pheanstalk/PheanstalkConnectionFactory.php @@ -1,11 +1,14 @@ establishConnection()); } - /** - * @return Pheanstalk - */ - private function establishConnection() + private function establishConnection(): Pheanstalk { if (false == $this->connection) { $this->connection = new Pheanstalk( @@ -75,12 +73,7 @@ private function establishConnection() return $this->connection; } - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) + private function parseDsn(string $dsn): array { $dsnConfig = parse_url(http://github.com/$dsn); if (false === $dsnConfig) { @@ -112,10 +105,7 @@ private function parseDsn($dsn) ]); } - /** - * @return array - */ - private function defaultConfig() + private function defaultConfig(): array { return [ 'host' => 'localhost', diff --git a/pkg/pheanstalk/PheanstalkConsumer.php b/pkg/pheanstalk/PheanstalkConsumer.php index 56f0f9b18..3fb217683 100644 --- a/pkg/pheanstalk/PheanstalkConsumer.php +++ b/pkg/pheanstalk/PheanstalkConsumer.php @@ -1,14 +1,17 @@ destination = $destination; @@ -31,21 +30,17 @@ public function __construct(PheanstalkDestination $destination, Pheanstalk $phea } /** - * {@inheritdoc} - * * @return PheanstalkDestination */ - public function getQueue() + public function getQueue(): Queue { return $this->destination; } /** - * {@inheritdoc} - * - * @return PheanstalkMessage|null + * @return PheanstalkMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { if (0 === $timeout) { while (true) { @@ -58,26 +53,26 @@ public function receive($timeout = 0) return $this->convertJobToMessage($job); } } + + return null; } /** - * {@inheritdoc} - * - * @return PheanstalkMessage|null + * @return PheanstalkMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { if ($job = $this->pheanstalk->reserveFromTube($this->destination->getName(), 0)) { return $this->convertJobToMessage($job); } + + return null; } /** - * {@inheritdoc} - * * @param PheanstalkMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { InvalidMessageException::assertMessageInstanceOf($message, PheanstalkMessage::class); @@ -89,30 +84,34 @@ public function acknowledge(PsrMessage $message) } /** - * {@inheritdoc} - * * @param PheanstalkMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { - $this->acknowledge($message); + InvalidMessageException::assertMessageInstanceOf($message, PheanstalkMessage::class); + + if (false == $message->getJob()) { + $state = $requeue ? 'requeued' : 'rejected'; + throw new \LogicException(sprintf('The message could not be %s because it does not have job set.', $state)); + } if ($requeue) { $this->pheanstalk->release($message->getJob(), $message->getPriority(), $message->getDelay()); + + return; } + + $this->acknowledge($message); } - /** - * @param Job $job - * - * @return PheanstalkMessage - */ - private function convertJobToMessage(Job $job) + private function convertJobToMessage(Job $job): PheanstalkMessage { $stats = $this->pheanstalk->statsJob($job); $message = PheanstalkMessage::jsonUnserialize($job->getData()); - $message->setRedelivered($stats['reserves'] > 1); + if (isset($stats['reserves'])) { + $message->setRedelivered($stats['reserves'] > 1); + } $message->setJob($job); return $message; diff --git a/pkg/pheanstalk/PheanstalkContext.php b/pkg/pheanstalk/PheanstalkContext.php index 100bb9f07..3e2fe834d 100644 --- a/pkg/pheanstalk/PheanstalkContext.php +++ b/pkg/pheanstalk/PheanstalkContext.php @@ -1,92 +1,100 @@ pheanstalk = $pheanstalk; } /** - * {@inheritdoc} + * @return PheanstalkMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new PheanstalkMessage($body, $properties, $headers); } /** - * {@inheritdoc} + * @return PheanstalkDestination */ - public function createTopic($topicName) + public function createTopic(string $topicName): Topic { return new PheanstalkDestination($topicName); } /** - * {@inheritdoc} + * @return PheanstalkDestination */ - public function createQueue($queueName) + public function createQueue(string $queueName): Queue { return new PheanstalkDestination($queueName); } - /** - * {@inheritdoc} - */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { - throw new \LogicException('Not implemented'); + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); } /** - * {@inheritdoc} - * * @return PheanstalkProducer */ - public function createProducer() + public function createProducer(): Producer { return new PheanstalkProducer($this->pheanstalk); } /** - * {@inheritdoc} - * * @param PheanstalkDestination $destination * * @return PheanstalkConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, PheanstalkDestination::class); return new PheanstalkConsumer($destination, $this->pheanstalk); } - public function close() + public function close(): void { $this->pheanstalk->getConnection()->disconnect(); } - /** - * @return Pheanstalk - */ - public function getPheanstalk() + public function createSubscriptionConsumer(): SubscriptionConsumer + { + throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); + } + + public function purgeQueue(Queue $queue): void + { + throw PurgeQueueNotSupportedException::providerDoestNotSupportIt(); + } + + public function getPheanstalk(): Pheanstalk { return $this->pheanstalk; } diff --git a/pkg/pheanstalk/PheanstalkDestination.php b/pkg/pheanstalk/PheanstalkDestination.php index 5f1412f91..76565b1ae 100644 --- a/pkg/pheanstalk/PheanstalkDestination.php +++ b/pkg/pheanstalk/PheanstalkDestination.php @@ -1,46 +1,36 @@ destinationName = $destinationName; } - /** - * @return string - */ - public function getName() + public function getName(): string { return $this->destinationName; } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { - return $this->getName(); + return $this->destinationName; } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getTopicName(): string { - return $this->getName(); + return $this->destinationName; } } diff --git a/pkg/pheanstalk/PheanstalkMessage.php b/pkg/pheanstalk/PheanstalkMessage.php index 9a1e80065..5bff1a7a6 100644 --- a/pkg/pheanstalk/PheanstalkMessage.php +++ b/pkg/pheanstalk/PheanstalkMessage.php @@ -1,12 +1,14 @@ body = $body; $this->properties = $properties; @@ -46,220 +43,139 @@ public function __construct($body = '', array $properties = [], array $headers = $this->redelivered = false; } - /** - * @param string $body - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * @param array $properties - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * @param array $headers - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * @return bool - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * @param bool $redelivered - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = $redelivered; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', (string) $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id'); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', (string) $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id'); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } - /** - * @param string|null $replyTo - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * @return string|null - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } - /** - * @param int $time - */ - public function setTimeToRun($time) + public function setTimeToRun(int $time) { $this->setHeader('ttr', $time); } - /** - * @return int - */ - public function getTimeToRun() + public function getTimeToRun(): int { return $this->getHeader('ttr', Pheanstalk::DEFAULT_TTR); } - /** - * @param int $priority - */ - public function setPriority($priority) + public function setPriority(int $priority): void { $this->setHeader('priority', $priority); } - /** - * @return int - */ - public function getPriority() + public function getPriority(): int { return $this->getHeader('priority', Pheanstalk::DEFAULT_PRIORITY); } - /** - * @param int $delay - */ - public function setDelay($delay) + public function setDelay(int $delay): void { $this->setHeader('delay', $delay); } - /** - * @return int - */ - public function getDelay() + public function getDelay(): int { return $this->getHeader('delay', Pheanstalk::DEFAULT_DELAY); } - /** - * {@inheritdoc} - */ - public function jsonSerialize() + public function jsonSerialize(): array { return [ 'body' => $this->getBody(), @@ -268,37 +184,22 @@ public function jsonSerialize() ]; } - /** - * @param string $json - * - * @return self - */ - public static function jsonUnserialize($json) + public static function jsonUnserialize(string $json): self { $data = json_decode($json, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return new self($data['body'], $data['properties'], $data['headers']); } - /** - * @return Job - */ - public function getJob() + public function getJob(): ?Job { return $this->job; } - /** - * @param Job $job - */ - public function setJob(Job $job) + public function setJob(?Job $job = null): void { $this->job = $job; } diff --git a/pkg/pheanstalk/PheanstalkProducer.php b/pkg/pheanstalk/PheanstalkProducer.php index 08255e170..0c8ffd8ff 100644 --- a/pkg/pheanstalk/PheanstalkProducer.php +++ b/pkg/pheanstalk/PheanstalkProducer.php @@ -1,15 +1,17 @@ pheanstalk = $pheanstalk; } /** - * {@inheritdoc} - * * @param PheanstalkDestination $destination * @param PheanstalkMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, PheanstalkDestination::class); InvalidMessageException::assertMessageInstanceOf($message, PheanstalkMessage::class); $rawMessage = json_encode($message); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'Could not encode value into json. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('Could not encode value into json. Error %s and message %s', json_last_error(), json_last_error_msg())); + } + + if (null !== $this->priority && null === $message->getHeader('priority')) { + $message->setPriority($this->priority); + } + if (null !== $this->deliveryDelay && null === $message->getHeader('delay')) { + $message->setDelay($this->deliveryDelay / 1000); + } + if (null !== $this->timeToLive && null === $message->getHeader('ttr')) { + $message->setTimeToRun($this->timeToLive / 1000); } $this->pheanstalk->useTube($destination->getName())->put( @@ -53,62 +71,47 @@ public function send(PsrDestination $destination, PsrMessage $message) } /** - * {@inheritdoc} + * @return PheanstalkProducer */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { - if (null === $deliveryDelay) { - return; - } + $this->deliveryDelay = $deliveryDelay; - throw new \LogicException('Not implemented'); + return $this; } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { - return null; + return $this->deliveryDelay; } /** - * {@inheritdoc} + * @return PheanstalkProducer */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { - if (null === $priority) { - return; - } + $this->priority = $priority; - throw new \LogicException('Not implemented'); + return $this; } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { - return null; + return $this->priority; } /** - * {@inheritdoc} + * @return PheanstalkProducer */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { - if (null === $timeToLive) { - return; - } + $this->timeToLive = $timeToLive; - throw new \LogicException('Not implemented'); + return $this; } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { - return null; + return $this->timeToLive; } } diff --git a/pkg/pheanstalk/README.md b/pkg/pheanstalk/README.md index c1941485f..6461741cf 100644 --- a/pkg/pheanstalk/README.md +++ b/pkg/pheanstalk/README.md @@ -1,27 +1,36 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Beanstalk Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/pheanstalk.png?branch=master)](https://travis-ci.org/php-enqueue/pheanstalk) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/pheanstalk/ci.yml?branch=master)](https://github.com/php-enqueue/pheanstalk/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/pheanstalk/d/total.png)](https://packagist.org/packages/enqueue/pheanstalk) [![Latest Stable Version](https://poser.pugx.org/enqueue/pheanstalk/version.png)](https://packagist.org/packages/enqueue/pheanstalk) - -This is an implementation of the queue specification. It allows you to send and consume message from Beanstalkd broker. + +This is an implementation of the queue specification. It allows you to send and consume message from Beanstalkd broker. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/pheanstalk/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/pheanstalk/Tests/PheanstalkConnectionFactoryConfigTest.php b/pkg/pheanstalk/Tests/PheanstalkConnectionFactoryConfigTest.php index 231ec0d77..a7bc7fc34 100644 --- a/pkg/pheanstalk/Tests/PheanstalkConnectionFactoryConfigTest.php +++ b/pkg/pheanstalk/Tests/PheanstalkConnectionFactoryConfigTest.php @@ -4,6 +4,7 @@ use Enqueue\Pheanstalk\PheanstalkConnectionFactory; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; /** @@ -12,6 +13,7 @@ class PheanstalkConnectionFactoryConfigTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testThrowNeitherArrayStringNorNullGivenAsConfig() { @@ -39,9 +41,6 @@ public function testThrowIfDsnCouldNotBeParsed() /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { diff --git a/pkg/pheanstalk/Tests/PheanstalkConsumerTest.php b/pkg/pheanstalk/Tests/PheanstalkConsumerTest.php index 60015dc99..c79b20bbd 100644 --- a/pkg/pheanstalk/Tests/PheanstalkConsumerTest.php +++ b/pkg/pheanstalk/Tests/PheanstalkConsumerTest.php @@ -14,14 +14,6 @@ class PheanstalkConsumerTest extends TestCase { use ClassExtensionTrait; - public function testCouldBeConstructedWithDestinationAndPheanstalkAsArguments() - { - new PheanstalkConsumer( - new PheanstalkDestination('aQueueName'), - $this->createPheanstalkMock() - ); - } - public function testShouldReturnQueueSetInConstructor() { $destination = new PheanstalkDestination('aQueueName'); @@ -54,7 +46,7 @@ public function testShouldReceiveFromQueueAndReturnNullIfNoMessageInQueue() public function testShouldReceiveFromQueueAndReturnMessageIfMessageInQueue() { $destination = new PheanstalkDestination('theQueueName'); - $message = new PheanstalkMessage('theBody', ['foo' => 'fooVal'], ['bar' => 'barVal']); + $message = new PheanstalkMessage('theBody', ['foo' => 'fooVal'], ['bar' => 'barVal']); $job = new Job('theJobId', json_encode($message)); @@ -96,7 +88,7 @@ public function testShouldReceiveNoWaitFromQueueAndReturnNullIfNoMessageInQueue( public function testShouldReceiveNoWaitFromQueueAndReturnMessageIfMessageInQueue() { $destination = new PheanstalkDestination('theQueueName'); - $message = new PheanstalkMessage('theBody', ['foo' => 'fooVal'], ['bar' => 'barVal']); + $message = new PheanstalkMessage('theBody', ['foo' => 'fooVal'], ['bar' => 'barVal']); $job = new Job('theJobId', json_encode($message)); @@ -118,8 +110,111 @@ public function testShouldReceiveNoWaitFromQueueAndReturnMessageIfMessageInQueue $this->assertSame($job, $actualMessage->getJob()); } + public function testShouldAcknowledgeMessage() + { + $destination = new PheanstalkDestination('theQueueName'); + $message = new PheanstalkMessage(); + + $job = new Job('theJobId', json_encode($message)); + $message->setJob($job); + + $pheanstalk = $this->createPheanstalkMock(); + $pheanstalk + ->expects($this->once()) + ->method('delete') + ->with($job) + ; + + $consumer = new PheanstalkConsumer($destination, $pheanstalk); + + $consumer->acknowledge($message); + } + + public function testAcknowledgeShouldThrowExceptionIfMessageHasNoJob() + { + $destination = new PheanstalkDestination('theQueueName'); + $pheanstalk = $this->createPheanstalkMock(); + + $consumer = new PheanstalkConsumer($destination, $pheanstalk); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message could not be acknowledged because it does not have job set.'); + + $consumer->acknowledge(new PheanstalkMessage()); + } + + public function testShouldRejectMessage() + { + $destination = new PheanstalkDestination('theQueueName'); + $message = new PheanstalkMessage(); + + $job = new Job('theJobId', json_encode($message)); + $message->setJob($job); + + $pheanstalk = $this->createPheanstalkMock(); + $pheanstalk + ->expects($this->once()) + ->method('delete') + ->with($job) + ; + + $consumer = new PheanstalkConsumer($destination, $pheanstalk); + + $consumer->reject($message); + } + + public function testRejectShouldThrowExceptionIfMessageHasNoJob() + { + $destination = new PheanstalkDestination('theQueueName'); + $pheanstalk = $this->createPheanstalkMock(); + + $consumer = new PheanstalkConsumer($destination, $pheanstalk); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message could not be rejected because it does not have job set.'); + + $consumer->reject(new PheanstalkMessage()); + } + + public function testShouldRequeueMessage() + { + $destination = new PheanstalkDestination('theQueueName'); + $message = new PheanstalkMessage(); + + $job = new Job('theJobId', json_encode($message)); + $message->setJob($job); + + $pheanstalk = $this->createPheanstalkMock(); + $pheanstalk + ->expects($this->once()) + ->method('release') + ->with($job, Pheanstalk::DEFAULT_PRIORITY, Pheanstalk::DEFAULT_DELAY) + ; + $pheanstalk + ->expects($this->never()) + ->method('delete') + ; + + $consumer = new PheanstalkConsumer($destination, $pheanstalk); + + $consumer->reject($message, true); + } + + public function testRequeueShouldThrowExceptionIfMessageHasNoJob() + { + $destination = new PheanstalkDestination('theQueueName'); + $pheanstalk = $this->createPheanstalkMock(); + + $consumer = new PheanstalkConsumer($destination, $pheanstalk); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The message could not be requeued because it does not have job set.'); + + $consumer->reject(new PheanstalkMessage(), true); + } + /** - * @return \PHPUnit_Framework_MockObject_MockObject|Pheanstalk + * @return \PHPUnit\Framework\MockObject\MockObject|Pheanstalk */ private function createPheanstalkMock() { diff --git a/pkg/pheanstalk/Tests/PheanstalkContextTest.php b/pkg/pheanstalk/Tests/PheanstalkContextTest.php index ecc5fc932..3b7bfbeb7 100644 --- a/pkg/pheanstalk/Tests/PheanstalkContextTest.php +++ b/pkg/pheanstalk/Tests/PheanstalkContextTest.php @@ -5,8 +5,9 @@ use Enqueue\Null\NullQueue; use Enqueue\Pheanstalk\PheanstalkContext; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\TemporaryQueueNotSupportedException; use Pheanstalk\Connection; use Pheanstalk\Pheanstalk; use PHPUnit\Framework\TestCase; @@ -15,22 +16,17 @@ class PheanstalkContextTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementPsrContextInterface() + public function testShouldImplementContextInterface() { - $this->assertClassImplements(PsrContext::class, PheanstalkContext::class); - } - - public function testCouldBeConstructedWithPheanstalkAsFirstArgument() - { - new PheanstalkContext($this->createPheanstalkMock()); + $this->assertClassImplements(Context::class, PheanstalkContext::class); } public function testThrowNotImplementedOnCreateTemporaryQueue() { $context = new PheanstalkContext($this->createPheanstalkMock()); - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Not implemented'); + $this->expectException(TemporaryQueueNotSupportedException::class); + $context->createTemporaryQueue(); } @@ -72,7 +68,7 @@ public function testShouldDoConnectionDisconnectOnContextClose() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Pheanstalk + * @return \PHPUnit\Framework\MockObject\MockObject|Pheanstalk */ private function createPheanstalkMock() { diff --git a/pkg/pheanstalk/Tests/PheanstalkDestinationTest.php b/pkg/pheanstalk/Tests/PheanstalkDestinationTest.php index 31ea1fd8e..d390f4715 100644 --- a/pkg/pheanstalk/Tests/PheanstalkDestinationTest.php +++ b/pkg/pheanstalk/Tests/PheanstalkDestinationTest.php @@ -4,22 +4,22 @@ use Enqueue\Pheanstalk\PheanstalkDestination; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrQueue; -use Interop\Queue\PsrTopic; +use Interop\Queue\Queue; +use Interop\Queue\Topic; use PHPUnit\Framework\TestCase; class PheanstalkDestinationTest extends TestCase { use ClassExtensionTrait; - public function testShouldImplementPsrQueueInterface() + public function testShouldImplementQueueInterface() { - $this->assertClassImplements(PsrQueue::class, PheanstalkDestination::class); + $this->assertClassImplements(Queue::class, PheanstalkDestination::class); } - public function testShouldImplementPsrTopicInterface() + public function testShouldImplementTopicInterface() { - $this->assertClassImplements(PsrTopic::class, PheanstalkDestination::class); + $this->assertClassImplements(Topic::class, PheanstalkDestination::class); } public function testShouldAllowGetNameSetInConstructor() diff --git a/pkg/pheanstalk/Tests/PheanstalkProducerTest.php b/pkg/pheanstalk/Tests/PheanstalkProducerTest.php index 1ce0fe668..b9a69176c 100644 --- a/pkg/pheanstalk/Tests/PheanstalkProducerTest.php +++ b/pkg/pheanstalk/Tests/PheanstalkProducerTest.php @@ -8,20 +8,16 @@ use Enqueue\Pheanstalk\PheanstalkMessage; use Enqueue\Pheanstalk\PheanstalkProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; use Pheanstalk\Pheanstalk; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class PheanstalkProducerTest extends TestCase { use ClassExtensionTrait; - public function testCouldBeConstructedWithPheanstalkAsFirstArgument() - { - new PheanstalkProducer($this->createPheanstalkMock()); - } - public function testThrowIfDestinationInvalid() { $producer = new PheanstalkProducer($this->createPheanstalkMock()); @@ -65,8 +61,157 @@ public function testShouldJsonEncodeMessageAndPutToExpectedTube() ); } + public function testMessagePriorityPrecedesPriority() + { + $message = new PheanstalkMessage('theBody'); + $message->setPriority(100); + + $pheanstalk = $this->createPheanstalkMock(); + $pheanstalk + ->expects($this->once()) + ->method('useTube') + ->with('theQueueName') + ->willReturnSelf() + ; + $pheanstalk + ->expects($this->once()) + ->method('put') + ->with('{"body":"theBody","properties":[],"headers":{"priority":100}}', 100, Pheanstalk::DEFAULT_DELAY, Pheanstalk::DEFAULT_TTR) + ; + + $producer = new PheanstalkProducer($pheanstalk); + $producer->setPriority(50); + + $producer->send( + new PheanstalkDestination('theQueueName'), + $message + ); + } + + public function testAccessDeliveryDelayAsMilliseconds() + { + $producer = new PheanstalkProducer($this->createPheanstalkMock()); + $producer->setDeliveryDelay(5000); + + $this->assertEquals(5000, $producer->getDeliveryDelay()); + } + + public function testDeliveryDelayResolvesToSeconds() + { + $message = new PheanstalkMessage('theBody'); + + $pheanstalk = $this->createPheanstalkMock(); + $pheanstalk + ->expects($this->once()) + ->method('useTube') + ->with('theQueueName') + ->willReturnSelf() + ; + $pheanstalk + ->expects($this->once()) + ->method('put') + ->with('{"body":"theBody","properties":[],"headers":[]}', Pheanstalk::DEFAULT_PRIORITY, 5, Pheanstalk::DEFAULT_TTR) + ; + + $producer = new PheanstalkProducer($pheanstalk); + $producer->setDeliveryDelay(5000); + + $producer->send( + new PheanstalkDestination('theQueueName'), + $message + ); + } + + public function testMessageDelayPrecedesDeliveryDelay() + { + $message = new PheanstalkMessage('theBody'); + $message->setDelay(25); + + $pheanstalk = $this->createPheanstalkMock(); + $pheanstalk + ->expects($this->once()) + ->method('useTube') + ->with('theQueueName') + ->willReturnSelf() + ; + $pheanstalk + ->expects($this->once()) + ->method('put') + ->with('{"body":"theBody","properties":[],"headers":{"delay":25}}', Pheanstalk::DEFAULT_PRIORITY, 25, Pheanstalk::DEFAULT_TTR) + ; + + $producer = new PheanstalkProducer($pheanstalk); + $producer->setDeliveryDelay(1000); + + $producer->send( + new PheanstalkDestination('theQueueName'), + $message + ); + } + + public function testAccessTimeToLiveAsMilliseconds() + { + $producer = new PheanstalkProducer($this->createPheanstalkMock()); + $producer->setTimeToLive(5000); + + $this->assertEquals(5000, $producer->getTimeToLive()); + } + + public function testTimeToLiveResolvesToSeconds() + { + $message = new PheanstalkMessage('theBody'); + + $pheanstalk = $this->createPheanstalkMock(); + $pheanstalk + ->expects($this->once()) + ->method('useTube') + ->with('theQueueName') + ->willReturnSelf() + ; + $pheanstalk + ->expects($this->once()) + ->method('put') + ->with('{"body":"theBody","properties":[],"headers":[]}', Pheanstalk::DEFAULT_PRIORITY, Pheanstalk::DEFAULT_DELAY, 5) + ; + + $producer = new PheanstalkProducer($pheanstalk); + $producer->setTimeToLive(5000); + + $producer->send( + new PheanstalkDestination('theQueueName'), + $message + ); + } + + public function testMessageTimeToRunPrecedesTimeToLive() + { + $message = new PheanstalkMessage('theBody'); + $message->setTimeToRun(25); + + $pheanstalk = $this->createPheanstalkMock(); + $pheanstalk + ->expects($this->once()) + ->method('useTube') + ->with('theQueueName') + ->willReturnSelf() + ; + $pheanstalk + ->expects($this->once()) + ->method('put') + ->with('{"body":"theBody","properties":[],"headers":{"ttr":25}}', Pheanstalk::DEFAULT_PRIORITY, Pheanstalk::DEFAULT_DELAY, 25) + ; + + $producer = new PheanstalkProducer($pheanstalk); + $producer->setTimeToLive(1000); + + $producer->send( + new PheanstalkDestination('theQueueName'), + $message + ); + } + /** - * @return \PHPUnit_Framework_MockObject_MockObject|Pheanstalk + * @return MockObject|Pheanstalk */ private function createPheanstalkMock() { diff --git a/pkg/pheanstalk/Tests/Spec/PheanstalkConnectionFactoryTest.php b/pkg/pheanstalk/Tests/Spec/PheanstalkConnectionFactoryTest.php index d9bdebff4..4d9148447 100644 --- a/pkg/pheanstalk/Tests/Spec/PheanstalkConnectionFactoryTest.php +++ b/pkg/pheanstalk/Tests/Spec/PheanstalkConnectionFactoryTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Pheanstalk\Tests\Spec; use Enqueue\Pheanstalk\PheanstalkConnectionFactory; -use Interop\Queue\Spec\PsrConnectionFactorySpec; +use Interop\Queue\Spec\ConnectionFactorySpec; -class PheanstalkConnectionFactoryTest extends PsrConnectionFactorySpec +class PheanstalkConnectionFactoryTest extends ConnectionFactorySpec { - /** - * {@inheritdoc} - */ protected function createConnectionFactory() { return new PheanstalkConnectionFactory(); diff --git a/pkg/pheanstalk/Tests/Spec/PheanstalkContextTest.php b/pkg/pheanstalk/Tests/Spec/PheanstalkContextTest.php index 16a52a2e1..d6ea514f4 100644 --- a/pkg/pheanstalk/Tests/Spec/PheanstalkContextTest.php +++ b/pkg/pheanstalk/Tests/Spec/PheanstalkContextTest.php @@ -3,14 +3,11 @@ namespace Enqueue\Pheanstalk\Tests\Spec; use Enqueue\Pheanstalk\PheanstalkContext; -use Interop\Queue\Spec\PsrContextSpec; +use Interop\Queue\Spec\ContextSpec; use Pheanstalk\Pheanstalk; -class PheanstalkContextTest extends PsrContextSpec +class PheanstalkContextTest extends ContextSpec { - /** - * {@inheritdoc} - */ protected function createContext() { return new PheanstalkContext($this->createMock(Pheanstalk::class)); diff --git a/pkg/pheanstalk/Tests/Spec/PheanstalkMessageTest.php b/pkg/pheanstalk/Tests/Spec/PheanstalkMessageTest.php index e62c44828..692e0db54 100644 --- a/pkg/pheanstalk/Tests/Spec/PheanstalkMessageTest.php +++ b/pkg/pheanstalk/Tests/Spec/PheanstalkMessageTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Pheanstalk\Tests\Spec; use Enqueue\Pheanstalk\PheanstalkMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class PheanstalkMessageTest extends PsrMessageSpec +class PheanstalkMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new PheanstalkMessage(); diff --git a/pkg/pheanstalk/Tests/Spec/PheanstalkQueueTest.php b/pkg/pheanstalk/Tests/Spec/PheanstalkQueueTest.php index 03cec7e3b..0a770b2e2 100644 --- a/pkg/pheanstalk/Tests/Spec/PheanstalkQueueTest.php +++ b/pkg/pheanstalk/Tests/Spec/PheanstalkQueueTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Pheanstalk\Tests\Spec; use Enqueue\Pheanstalk\PheanstalkDestination; -use Interop\Queue\Spec\PsrQueueSpec; +use Interop\Queue\Spec\QueueSpec; -class PheanstalkQueueTest extends PsrQueueSpec +class PheanstalkQueueTest extends QueueSpec { - /** - * {@inheritdoc} - */ protected function createQueue() { return new PheanstalkDestination(self::EXPECTED_QUEUE_NAME); diff --git a/pkg/pheanstalk/Tests/Spec/PheanstalkSendToAndReceiveFromQueueTest.php b/pkg/pheanstalk/Tests/Spec/PheanstalkSendToAndReceiveFromQueueTest.php index cb2d21ba1..282ac364c 100644 --- a/pkg/pheanstalk/Tests/Spec/PheanstalkSendToAndReceiveFromQueueTest.php +++ b/pkg/pheanstalk/Tests/Spec/PheanstalkSendToAndReceiveFromQueueTest.php @@ -3,8 +3,8 @@ namespace Enqueue\Pheanstalk\Tests\Spec; use Enqueue\Pheanstalk\PheanstalkConnectionFactory; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrQueue; +use Interop\Queue\Context; +use Interop\Queue\Queue; use Interop\Queue\Spec\SendToAndReceiveFromQueueSpec; /** @@ -12,9 +12,6 @@ */ class PheanstalkSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new PheanstalkConnectionFactory(getenv('BEANSTALKD_DSN')); @@ -23,12 +20,11 @@ protected function createContext() } /** - * @param PsrContext $context - * @param string $queueName + * @param string $queueName * - * @return PsrQueue + * @return Queue */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { return $context->createQueue($queueName.time()); } diff --git a/pkg/pheanstalk/Tests/Spec/PheanstalkSendToAndReceiveNoWaitFromQueueTest.php b/pkg/pheanstalk/Tests/Spec/PheanstalkSendToAndReceiveNoWaitFromQueueTest.php index b416dd1a4..de464e5bf 100644 --- a/pkg/pheanstalk/Tests/Spec/PheanstalkSendToAndReceiveNoWaitFromQueueTest.php +++ b/pkg/pheanstalk/Tests/Spec/PheanstalkSendToAndReceiveNoWaitFromQueueTest.php @@ -3,7 +3,7 @@ namespace Enqueue\Pheanstalk\Tests\Spec; use Enqueue\Pheanstalk\PheanstalkConnectionFactory; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromQueueSpec; /** @@ -11,9 +11,6 @@ */ class PheanstalkSendToAndReceiveNoWaitFromQueueTest extends SendToAndReceiveNoWaitFromQueueSpec { - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new PheanstalkConnectionFactory(getenv('BEANSTALKD_DSN')); @@ -21,10 +18,7 @@ protected function createContext() return $factory->createContext(); } - /** - * {@inheritdoc} - */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { return $context->createQueue($queueName.time()); } diff --git a/pkg/pheanstalk/Tests/Spec/PheanstalkSendToTopicAndReceiveFromQueueTest.php b/pkg/pheanstalk/Tests/Spec/PheanstalkSendToTopicAndReceiveFromQueueTest.php index 2f001aba1..4c30d7796 100644 --- a/pkg/pheanstalk/Tests/Spec/PheanstalkSendToTopicAndReceiveFromQueueTest.php +++ b/pkg/pheanstalk/Tests/Spec/PheanstalkSendToTopicAndReceiveFromQueueTest.php @@ -3,7 +3,7 @@ namespace Enqueue\Pheanstalk\Tests\Spec; use Enqueue\Pheanstalk\PheanstalkConnectionFactory; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveFromQueueSpec; /** @@ -13,14 +13,11 @@ class PheanstalkSendToTopicAndReceiveFromQueueTest extends SendToTopicAndReceive { private $time; - public function setUp() + protected function setUp(): void { $this->time = time(); } - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new PheanstalkConnectionFactory(getenv('BEANSTALKD_DSN')); @@ -28,18 +25,12 @@ protected function createContext() return $factory->createContext(); } - /** - * {@inheritdoc} - */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { return $context->createQueue($queueName.$this->time); } - /** - * {@inheritdoc} - */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { return $context->createTopic($topicName.$this->time); } diff --git a/pkg/pheanstalk/Tests/Spec/PheanstalkSendToTopicAndReceiveNoWaitFromQueueTest.php b/pkg/pheanstalk/Tests/Spec/PheanstalkSendToTopicAndReceiveNoWaitFromQueueTest.php index b5cea72d5..58e8b71f4 100644 --- a/pkg/pheanstalk/Tests/Spec/PheanstalkSendToTopicAndReceiveNoWaitFromQueueTest.php +++ b/pkg/pheanstalk/Tests/Spec/PheanstalkSendToTopicAndReceiveNoWaitFromQueueTest.php @@ -3,7 +3,7 @@ namespace Enqueue\Pheanstalk\Tests\Spec; use Enqueue\Pheanstalk\PheanstalkConnectionFactory; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveNoWaitFromQueueSpec; /** @@ -13,14 +13,11 @@ class PheanstalkSendToTopicAndReceiveNoWaitFromQueueTest extends SendToTopicAndR { private $time; - public function setUp() + protected function setUp(): void { $this->time = time(); } - /** - * {@inheritdoc} - */ protected function createContext() { $factory = new PheanstalkConnectionFactory(getenv('BEANSTALKD_DSN')); @@ -28,18 +25,12 @@ protected function createContext() return $factory->createContext(); } - /** - * {@inheritdoc} - */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName) { return $context->createQueue($queueName.$this->time); } - /** - * {@inheritdoc} - */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $topicName) { return $context->createTopic($topicName.$this->time); } diff --git a/pkg/pheanstalk/Tests/Spec/PheanstalkTopicTest.php b/pkg/pheanstalk/Tests/Spec/PheanstalkTopicTest.php index 0b04b89b3..4b0028261 100644 --- a/pkg/pheanstalk/Tests/Spec/PheanstalkTopicTest.php +++ b/pkg/pheanstalk/Tests/Spec/PheanstalkTopicTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Pheanstalk\Tests\Spec; use Enqueue\Pheanstalk\PheanstalkDestination; -use Interop\Queue\Spec\PsrTopicSpec; +use Interop\Queue\Spec\TopicSpec; -class PheanstalkTopicTest extends PsrTopicSpec +class PheanstalkTopicTest extends TopicSpec { - /** - * {@inheritdoc} - */ protected function createTopic() { return new PheanstalkDestination(self::EXPECTED_TOPIC_NAME); diff --git a/pkg/pheanstalk/composer.json b/pkg/pheanstalk/composer.json index 4aed7e5cb..c810971c8 100644 --- a/pkg/pheanstalk/composer.json +++ b/pkg/pheanstalk/composer.json @@ -6,18 +6,15 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "pda/pheanstalk": "^3", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1" + "php": "^8.1", + "pda/pheanstalk": "^3.1", + "queue-interop/queue-interop": "^0.8" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -32,13 +29,10 @@ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/pheanstalk/phpunit.xml.dist b/pkg/pheanstalk/phpunit.xml.dist index 4dca142e1..1e72c01a2 100644 --- a/pkg/pheanstalk/phpunit.xml.dist +++ b/pkg/pheanstalk/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/rdkafka/.github/workflows/ci.yml b/pkg/rdkafka/.github/workflows/ci.yml new file mode 100644 index 000000000..9e0ceb121 --- /dev/null +++ b/pkg/rdkafka/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: # ext-rdkafka not needed for tests, and a pain to install on CI; + composer-options: "--ignore-platform-req=ext-rdkafka" + + - run: sed -i 's/525568/16777471/' vendor/kwn/php-rdkafka-stubs/stubs/constants.php + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/rdkafka/.travis.yml b/pkg/rdkafka/.travis.yml deleted file mode 100644 index 658dcabad..000000000 --- a/pkg/rdkafka/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - php Tests/fix_composer_json.php - - composer self-update - - composer install - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/rdkafka/Client/RdKafkaDriver.php b/pkg/rdkafka/Client/RdKafkaDriver.php deleted file mode 100644 index 416725366..000000000 --- a/pkg/rdkafka/Client/RdKafkaDriver.php +++ /dev/null @@ -1,167 +0,0 @@ -context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - */ - public function createTransportMessage(Message $message) - { - $headers = $message->getHeaders(); - $headers['content_type'] = $message->getContentType(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($message->getProperties()); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - - return $transportMessage; - } - - /** - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - - $clientMessage->setContentType($message->getHeader('content_type')); - - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - return $clientMessage; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $topic = $this->createRouterTopic(); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($topic, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - return $this->context->createQueue($transportName); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $logger->debug('[RdKafkaDriver] setup broker'); - $log = function ($text, ...$args) use ($logger) { - $logger->debug(sprintf('[RdKafkaDriver] '.$text, ...$args)); - }; - - // setup router - $routerQueue = $this->createQueue($this->config->getRouterQueueName()); - $log('Create router queue: %s', $routerQueue->getQueueName()); - $this->context->createConsumer($routerQueue); - - // setup queues - foreach ($this->queueMetaRegistry->getQueuesMeta() as $meta) { - $queue = $this->createQueue($meta->getClientName()); - $log('Create processor queue: %s', $queue->getQueueName()); - $this->context->createConsumer($queue); - } - } - - /** - * {@inheritdoc} - */ - public function getConfig() - { - return $this->config; - } - - private function createRouterTopic() - { - $topic = $this->context->createTopic( - $this->config->createTransportRouterTopicName($this->config->getRouterTopicName()) - ); - - return $topic; - } -} diff --git a/pkg/rdkafka/JsonSerializer.php b/pkg/rdkafka/JsonSerializer.php index 763c432a9..1d25ea55e 100644 --- a/pkg/rdkafka/JsonSerializer.php +++ b/pkg/rdkafka/JsonSerializer.php @@ -1,13 +1,12 @@ $message->getBody(), @@ -15,29 +14,18 @@ public function toString(RdKafkaMessage $message) 'headers' => $message->getHeaders(), ]); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return $json; } - /** - * {@inheritdoc} - */ - public function toMessage($string) + public function toMessage(string $string): RdKafkaMessage { $data = json_decode($string, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return new RdKafkaMessage($data['body'], $data['properties'], $data['headers']); diff --git a/pkg/rdkafka/README.md b/pkg/rdkafka/README.md index 7ea76aeac..94f24e510 100644 --- a/pkg/rdkafka/README.md +++ b/pkg/rdkafka/README.md @@ -1,23 +1,32 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # RdKafka Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/rdkafka.png?branch=master)](https://travis-ci.org/php-enqueue/rdkafka) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/rdkafka/ci.yml?branch=master)](https://github.com/php-enqueue/rdkafka/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/rdkafka/d/total.png)](https://packagist.org/packages/enqueue/rdkafka) [![Latest Stable Version](https://poser.pugx.org/enqueue/rdkafka/version.png)](https://packagist.org/packages/enqueue/rdkafka) - -This is an implementation of PSR specification. It allows you to send and consume message via Kafka protocol. + +This is an implementation of Queue Interop specification. It allows you to send and consume message via Kafka protocol. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/kafka/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com diff --git a/pkg/rdkafka/RdKafkaConnectionFactory.php b/pkg/rdkafka/RdKafkaConnectionFactory.php index 214552d2a..24d60c9b6 100644 --- a/pkg/rdkafka/RdKafkaConnectionFactory.php +++ b/pkg/rdkafka/RdKafkaConnectionFactory.php @@ -1,10 +1,13 @@ null, // https://arnaud-lb.github.io/php-rdkafka/phpdoc/rdkafka-topicconf.setpartitioner.html * 'log_level' => null, * 'commit_async' => false, + * 'shutdown_timeout' => -1, // https://github.com/arnaud-lb/php-rdkafka#proper-shutdown * ] * * or @@ -35,6 +39,10 @@ class RdKafkaConnectionFactory implements PsrConnectionFactory */ public function __construct($config = 'kafka:') { + if (version_compare(RdKafkaContext::getLibrdKafkaVersion(), '1.0.0', '<')) { + throw new \RuntimeException('You must install librdkafka:1.0.0 or higher'); + } + if (empty($config) || 'kafka:' === $config) { $config = []; } elseif (is_string($config)) { @@ -48,21 +56,14 @@ public function __construct($config = 'kafka:') } /** - * {@inheritdoc} - * * @return RdKafkaContext */ - public function createContext() + public function createContext(): Context { return new RdKafkaContext($this->config); } - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) + private function parseDsn(string $dsn): array { $dsnConfig = parse_url(http://github.com/$dsn); if (false === $dsnConfig) { @@ -98,10 +99,7 @@ private function parseDsn($dsn) return array_replace_recursive($this->defaultConfig(), $config); } - /** - * @return array - */ - private function defaultConfig() + private function defaultConfig(): array { return [ 'global' => [ diff --git a/pkg/rdkafka/RdKafkaConsumer.php b/pkg/rdkafka/RdKafkaConsumer.php index 37bdb8d97..8b6cf12c6 100644 --- a/pkg/rdkafka/RdKafkaConsumer.php +++ b/pkg/rdkafka/RdKafkaConsumer.php @@ -1,14 +1,17 @@ consumer = $consumer; $this->context = $context; $this->topic = $topic; $this->subscribed = false; - $this->commitAsync = false; - $this->offset = null; + $this->commitAsync = true; $this->setSerializer($serializer); } - /** - * @return bool - */ - public function isCommitAsync() + public function isCommitAsync(): bool { return $this->commitAsync; } - /** - * @param bool $async - */ - public function setCommitAsync($async) + public function setCommitAsync(bool $async): void { - $this->commitAsync = (bool) $async; + $this->commitAsync = $async; } - public function setOffset($offset) + public function getOffset(): ?int + { + return $this->offset; + } + + public function setOffset(?int $offset = null): void { if ($this->subscribed) { throw new \LogicException('The consumer has already subscribed.'); @@ -86,19 +81,19 @@ public function setOffset($offset) } /** - * {@inheritdoc} + * @return RdKafkaTopic */ - public function getQueue() + public function getQueue(): Queue { return $this->topic; } /** - * {@inheritdoc} + * @return RdKafkaMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { - if (false == $this->subscribed) { + if (false === $this->subscribed) { if (null === $this->offset) { $this->consumer->subscribe([$this->getQueue()->getQueueName()]); } else { @@ -112,34 +107,31 @@ public function receive($timeout = 0) $this->subscribed = true; } - $message = null; if ($timeout > 0) { - $message = $this->doReceive($timeout); - } else { - while (true) { - if ($message = $this->doReceive(500)) { - break; - } + return $this->doReceive($timeout); + } + + while (true) { + if ($message = $this->doReceive(500)) { + return $message; } } - return $message; + return null; } /** - * {@inheritdoc} + * @return RdKafkaMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { throw new \LogicException('Not implemented'); } /** - * {@inheritdoc} - * * @param RdKafkaMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { InvalidMessageException::assertMessageInstanceOf($message, RdKafkaMessage::class); @@ -155,11 +147,9 @@ public function acknowledge(PsrMessage $message) } /** - * {@inheritdoc} - * * @param RdKafkaMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { $this->acknowledge($message); @@ -168,25 +158,31 @@ public function reject(PsrMessage $message, $requeue = false) } } - /** - * @param int $timeout - * - * @return RdKafkaMessage|null - */ - private function doReceive($timeout) + private function doReceive(int $timeout): ?RdKafkaMessage { $kafkaMessage = $this->consumer->consume($timeout); + if (null === $kafkaMessage) { + return null; + } + switch ($kafkaMessage->err) { - case RD_KAFKA_RESP_ERR__PARTITION_EOF: - case RD_KAFKA_RESP_ERR__TIMED_OUT: - break; - case RD_KAFKA_RESP_ERR_NO_ERROR: + case \RD_KAFKA_RESP_ERR__PARTITION_EOF: + case \RD_KAFKA_RESP_ERR__TIMED_OUT: + case \RD_KAFKA_RESP_ERR__TRANSPORT: + return null; + case \RD_KAFKA_RESP_ERR_NO_ERROR: $message = $this->serializer->toMessage($kafkaMessage->payload); $message->setKey($kafkaMessage->key); $message->setPartition($kafkaMessage->partition); $message->setKafkaMessage($kafkaMessage); + // Merge headers passed from Kafka with possible earlier serialized payload headers. Prefer Kafka's. + // Note: Requires phprdkafka >= 3.1.0 + if (isset($kafkaMessage->headers)) { + $message->setHeaders(array_merge($message->getHeaders(), $kafkaMessage->headers)); + } + return $message; default: throw new \LogicException($kafkaMessage->errstr(), $kafkaMessage->err); diff --git a/pkg/rdkafka/RdKafkaContext.php b/pkg/rdkafka/RdKafkaContext.php index 0a67e5935..a252fcfd5 100644 --- a/pkg/rdkafka/RdKafkaContext.php +++ b/pkg/rdkafka/RdKafkaContext.php @@ -1,16 +1,26 @@ config = $config; $this->kafkaConsumers = []; + $this->rdKafkaConsumers = []; $this->setSerializer(new JsonSerializer()); } /** - * {@inheritdoc} + * @return RdKafkaMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new RdKafkaMessage($body, $properties, $headers); } /** - * {@inheritdoc} - * * @return RdKafkaTopic */ - public function createTopic($topicName) + public function createTopic(string $topicName): Topic { return new RdKafkaTopic($topicName); } /** - * {@inheritdoc} - * * @return RdKafkaTopic */ - public function createQueue($queueName) + public function createQueue(string $queueName): Queue { return new RdKafkaTopic($queueName); } - /** - * {@inheritdoc} - */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { - throw new \LogicException('Not implemented'); + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); } /** - * {@inheritdoc} - * * @return RdKafkaProducer */ - public function createProducer() + public function createProducer(): Producer { - return new RdKafkaProducer($this->getProducer(), $this->getSerializer()); + if (!isset($this->producer)) { + $producer = new VendorProducer($this->getConf()); + + if (isset($this->config['log_level'])) { + $producer->setLogLevel($this->config['log_level']); + } + + $this->producer = new RdKafkaProducer($producer, $this->getSerializer()); + + // Once created RdKafkaProducer can store messages internally that need to be delivered before PHP shuts + // down. Otherwise, we are bound to lose messages in transit. + // Note that it is generally preferable to call "close" method explicitly before shutdown starts, since + // otherwise we might not have access to some objects, like database connections. + register_shutdown_function([$this->producer, 'flush'], $this->config['shutdown_timeout'] ?? -1); + } + + return $this->producer; } /** - * {@inheritdoc} - * * @param RdKafkaTopic $destination + * + * @return RdKafkaConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, RdKafkaTopic::class); - $this->kafkaConsumers[] = $kafkaConsumer = new KafkaConsumer($this->getConf()); + $queueName = $destination->getQueueName(); + + if (!isset($this->rdKafkaConsumers[$queueName])) { + $this->kafkaConsumers[] = $kafkaConsumer = new KafkaConsumer($this->getConf()); - $consumer = new RdKafkaConsumer( - $kafkaConsumer, - $this, - $destination, - $this->getSerializer() - ); + $consumer = new RdKafkaConsumer( + $kafkaConsumer, + $this, + $destination, + $this->getSerializer() + ); - if (isset($this->config['commit_async'])) { - $consumer->setCommitAsync($this->config['commit_async']); + if (isset($this->config['commit_async'])) { + $consumer->setCommitAsync($this->config['commit_async']); + } + + $this->rdKafkaConsumers[$queueName] = $consumer; } - return $consumer; + return $this->rdKafkaConsumers[$queueName]; } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { $kafkaConsumers = $this->kafkaConsumers; $this->kafkaConsumers = []; + $this->rdKafkaConsumers = []; foreach ($kafkaConsumers as $kafkaConsumer) { $kafkaConsumer->unsubscribe(); } + + // Compatibility with phprdkafka 4.0. + if (isset($this->producer)) { + $this->producer->flush($this->config['shutdown_timeout'] ?? -1); + } } - /** - * @return Producer - */ - private function getProducer() + public function createSubscriptionConsumer(): SubscriptionConsumer { - if (null === $this->producer) { - $this->producer = new Producer($this->getConf()); + throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); + } - if (isset($this->config['log_level'])) { - $this->producer->setLogLevel($this->config['log_level']); - } + public function purgeQueue(Queue $queue): void + { + throw PurgeQueueNotSupportedException::providerDoestNotSupportIt(); + } + + public static function getLibrdKafkaVersion(): string + { + if (!defined('RD_KAFKA_VERSION')) { + throw new \RuntimeException('RD_KAFKA_VERSION constant is not defined. Phprdkafka is probably not installed'); } + $major = (\RD_KAFKA_VERSION & 0xFF000000) >> 24; + $minor = (\RD_KAFKA_VERSION & 0x00FF0000) >> 16; + $patch = (\RD_KAFKA_VERSION & 0x0000FF00) >> 8; - return $this->producer; + return "$major.$minor.$patch"; } - /** - * @return Conf - */ - private function getConf() + private function getConf(): Conf { if (null === $this->conf) { - $topicConf = new TopicConf(); + $this->conf = new Conf(); if (isset($this->config['topic']) && is_array($this->config['topic'])) { foreach ($this->config['topic'] as $key => $value) { - $topicConf->set($key, $value); + $this->conf->set($key, $value); } } if (isset($this->config['partitioner'])) { - $topicConf->setPartitioner($this->config['partitioner']); + $this->conf->set('partitioner', $this->config['partitioner']); } - $this->conf = new Conf(); - if (isset($this->config['global']) && is_array($this->config['global'])) { foreach ($this->config['global'] as $key => $value) { $this->conf->set($key, $value); @@ -183,7 +213,9 @@ private function getConf() $this->conf->setRebalanceCb($this->config['rebalance_cb']); } - $this->conf->setDefaultTopicConf($topicConf); + if (isset($this->config['stats_cb'])) { + $this->conf->setStatsCb($this->config['stats_cb']); + } } return $this->conf; diff --git a/pkg/rdkafka/RdKafkaMessage.php b/pkg/rdkafka/RdKafkaMessage.php index 7b5137e6e..7c6d0d005 100644 --- a/pkg/rdkafka/RdKafkaMessage.php +++ b/pkg/rdkafka/RdKafkaMessage.php @@ -1,15 +1,13 @@ body = $body; $this->properties = $properties; @@ -59,231 +52,135 @@ public function __construct($body = '', array $properties = [], array $headers = $this->redelivered = false; } - /** - * @param string $body - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * @param array $properties - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * @param array $headers - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * @return bool - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * @param bool $redelivered - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = $redelivered; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', (string) $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id'); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', (string) $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id'); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } - /** - * @param string|null $replyTo - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * @return string|null - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } - /** - * @return int - */ - public function getPartition() + public function getPartition(): ?int { return $this->partition; } - /** - * @param int $partition - */ - public function setPartition($partition) + public function setPartition(?int $partition = null): void { $this->partition = $partition; } - /** - * @return string|null - */ - public function getKey() + public function getKey(): ?string { return $this->key; } - /** - * @param string|null $key - */ - public function setKey($key) + public function setKey(?string $key = null): void { $this->key = $key; } - /** - * @return Message - */ - public function getKafkaMessage() + public function getKafkaMessage(): ?VendorMessage { return $this->kafkaMessage; } - /** - * @param Message $message - */ - public function setKafkaMessage(Message $message) + public function setKafkaMessage(?VendorMessage $message = null): void { $this->kafkaMessage = $message; } - - /** - * {@inheritdoc} - */ - public function jsonSerialize() - { - return (new JsonSerializer())->toString($this); - } - - /** - * @param string $json - * - * @return self - */ - public static function jsonUnserialize($json) - { - return (new JsonSerializer())->toMessage($json); - } } diff --git a/pkg/rdkafka/RdKafkaProducer.php b/pkg/rdkafka/RdKafkaProducer.php index 337ce7b37..24589b3e7 100644 --- a/pkg/rdkafka/RdKafkaProducer.php +++ b/pkg/rdkafka/RdKafkaProducer.php @@ -1,28 +1,27 @@ producer = $producer; @@ -30,81 +29,99 @@ public function __construct(Producer $producer, Serializer $serializer) } /** - * {@inheritdoc} - * * @param RdKafkaTopic $destination * @param RdKafkaMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, RdKafkaTopic::class); InvalidMessageException::assertMessageInstanceOf($message, RdKafkaMessage::class); - $partition = $message->getPartition() ?: $destination->getPartition() ?: RD_KAFKA_PARTITION_UA; + $partition = $message->getPartition() ?? $destination->getPartition() ?? \RD_KAFKA_PARTITION_UA; $payload = $this->serializer->toString($message); - $key = $message->getKey() ?: $destination->getKey() ?: null; + $key = $message->getKey() ?? $destination->getKey() ?? null; $topic = $this->producer->newTopic($destination->getTopicName(), $destination->getConf()); - $topic->produce($partition, 0 /* must be 0 */, $payload, $key); + + // Note: Topic::producev method exists in phprdkafka > 3.1.0 + // Headers in payload are maintained for backwards compatibility with apps that might run on lower phprdkafka version + if (method_exists($topic, 'producev')) { + // Phprdkafka <= 3.1.0 will fail calling `producev` on librdkafka >= 1.0.0 causing segfault + // Since we are forcing to use at least librdkafka:1.0.0, no need to check the lib version anymore + if (false !== phpversion('rdkafka') + && version_compare(phpversion('rdkafka'), '3.1.0', '<=')) { + trigger_error( + 'Phprdkafka <= 3.1.0 is incompatible with librdkafka 1.0.0 when calling `producev`. '. + 'Falling back to `produce` (without message headers) instead.', + \E_USER_WARNING + ); + } else { + $topic->producev($partition, 0 /* must be 0 */ , $payload, $key, $message->getHeaders()); + $this->producer->poll(0); + + return; + } + } + + $topic->produce($partition, 0 /* must be 0 */ , $payload, $key); + $this->producer->poll(0); } /** - * {@inheritdoc} + * @return RdKafkaProducer */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { if (null === $deliveryDelay) { - return; + return $this; } throw new \LogicException('Not implemented'); } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return null; } /** - * {@inheritdoc} + * @return RdKafkaProducer */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { if (null === $priority) { - return; + return $this; } - throw new \LogicException('Not implemented'); + throw PriorityNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return null; } - /** - * {@inheritdoc} - */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { if (null === $timeToLive) { - return; + return $this; } throw new \LogicException('Not implemented'); } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int + { + return null; + } + + public function flush(int $timeout): ?int { + // Flush method is exposed in phprdkafka 4.0 + if (method_exists($this->producer, 'flush')) { + return $this->producer->flush($timeout); + } + return null; } } diff --git a/pkg/rdkafka/RdKafkaTopic.php b/pkg/rdkafka/RdKafkaTopic.php index 6faf12f8c..572f4d024 100644 --- a/pkg/rdkafka/RdKafkaTopic.php +++ b/pkg/rdkafka/RdKafkaTopic.php @@ -1,12 +1,14 @@ name = $name; } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getTopicName(): string { return $this->name; } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { return $this->name; } - /** - * @return TopicConf|null - */ - public function getConf() + public function getConf(): ?TopicConf { return $this->conf; } - /** - * @param TopicConf|null $conf - */ - public function setConf(TopicConf $conf = null) + public function setConf(?TopicConf $conf = null): void { $this->conf = $conf; } - /** - * @return int - */ - public function getPartition() + public function getPartition(): ?int { return $this->partition; } - /** - * @param int $partition - */ - public function setPartition($partition) + public function setPartition(?int $partition = null): void { $this->partition = $partition; } - /** - * @return string|null - */ - public function getKey() + public function getKey(): ?string { return $this->key; } - /** - * @param string|null $key - */ - public function setKey($key) + public function setKey(?string $key = null): void { $this->key = $key; } diff --git a/pkg/rdkafka/Serializer.php b/pkg/rdkafka/Serializer.php index e1414150f..7e2a116ed 100644 --- a/pkg/rdkafka/Serializer.php +++ b/pkg/rdkafka/Serializer.php @@ -1,20 +1,12 @@ serializer = $serializer; diff --git a/pkg/rdkafka/Symfony/RdKafkaTransportFactory.php b/pkg/rdkafka/Symfony/RdKafkaTransportFactory.php deleted file mode 100644 index f03971fba..000000000 --- a/pkg/rdkafka/Symfony/RdKafkaTransportFactory.php +++ /dev/null @@ -1,139 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifString() - ->then(function ($v) { - return ['dsn' => $v]; - }) - ->end() - ->children() - ->scalarNode('dsn') - ->info('The kafka DSN. Other parameters are ignored if set') - ->end() - ->variableNode('global') - ->defaultValue([]) - ->info('The kafka global configuration properties') - ->end() - ->variableNode('topic') - ->defaultValue([]) - ->info('The kafka topic configuration properties') - ->end() - ->scalarNode('dr_msg_cb') - ->info('Delivery report callback') - ->end() - ->scalarNode('error_cb') - ->info('Error callback') - ->end() - ->scalarNode('rebalance_cb') - ->info('Called after consumer group has been rebalanced') - ->end() - ->enumNode('partitioner') - ->values(['RD_KAFKA_MSG_PARTITIONER_RANDOM', 'RD_KAFKA_MSG_PARTITIONER_CONSISTENT']) - ->info('Which partitioner to use') - ->end() - ->integerNode('log_level') - ->info('Logging level (syslog(3) levels)') - ->min(0)->max(7) - ->end() - ->booleanNode('commit_async') - ->defaultFalse() - ->info('Commit asynchronous') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - if (false == empty($config['rdkafka'])) { - $config['rdkafka'] = new Reference($config['rdkafka']); - } - - $factory = new Definition(RdKafkaConnectionFactory::class); - $factory->setArguments([isset($config['dsn']) ? $config['dsn'] : $config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(RdKafkaContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(RdKafkaDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/rdkafka/Tests/Client/RdKafkaDriverTest.php b/pkg/rdkafka/Tests/Client/RdKafkaDriverTest.php deleted file mode 100644 index af9de4110..000000000 --- a/pkg/rdkafka/Tests/Client/RdKafkaDriverTest.php +++ /dev/null @@ -1,368 +0,0 @@ -assertClassImplements(DriverInterface::class, RdKafkaDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new RdKafkaDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = $this->createDummyConfig(); - - $driver = new RdKafkaDriver( - $this->createPsrContextMock(), - $config, - $this->createDummyQueueMetaRegistry() - ); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new RdKafkaTopic('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new RdKafkaDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new RdKafkaTopic('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new RdKafkaDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new RdKafkaMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - - $driver = new RdKafkaDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - - $this->assertNull($clientMessage->getExpire()); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new RdKafkaMessage()) - ; - - $driver = new RdKafkaDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(RdKafkaMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => null, - 'correlation_id' => '', - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - } - - public function testShouldSendMessageToRouter() - { - $topic = new RdKafkaTopic('queue-name'); - $transportMessage = new RdKafkaMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->with('aprefix.router') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RdKafkaDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new RdKafkaDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new RdKafkaTopic('queue-name'); - $transportMessage = new RdKafkaMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RdKafkaDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new RdKafkaDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new RdKafkaDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBroker() - { - $routerTopic = new RdKafkaTopic(''); - $routerQueue = new RdKafkaTopic(''); - - $processorTopic = new RdKafkaTopic(''); - - $context = $this->createPsrContextMock(); - - $context - ->expects($this->at(0)) - ->method('createQueue') - ->willReturn($routerTopic) - ; - $context - ->expects($this->at(1)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(2)) - ->method('createQueue') - ->willReturn($processorTopic) - ; - - $meta = new QueueMetaRegistry($this->createDummyConfig(), [ - 'default' => [], - ]); - - $driver = new RdKafkaDriver( - $context, - $this->createDummyConfig(), - $meta - ); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|RdKafkaContext - */ - private function createPsrContextMock() - { - return $this->createMock(RdKafkaContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer - */ - private function createPsrProducerMock() - { - return $this->createMock(PsrProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/rdkafka/Tests/JsonSerializerTest.php b/pkg/rdkafka/Tests/JsonSerializerTest.php index 6513a2257..6c9bbef84 100644 --- a/pkg/rdkafka/Tests/JsonSerializerTest.php +++ b/pkg/rdkafka/Tests/JsonSerializerTest.php @@ -8,9 +8,6 @@ use Enqueue\Test\ClassExtensionTrait; use PHPUnit\Framework\TestCase; -/** - * @group rdkafka - */ class JsonSerializerTest extends TestCase { use ClassExtensionTrait; @@ -20,11 +17,6 @@ public function testShouldImplementSerializerInterface() $this->assertClassImplements(Serializer::class, JsonSerializer::class); } - public function testCouldBeConstructedWithoutAnyArguments() - { - new JsonSerializer(); - } - public function testShouldConvertMessageToJsonString() { $serializer = new JsonSerializer(); @@ -42,8 +34,8 @@ public function testThrowIfFailedToEncodeMessageToJson() $resource = fopen(__FILE__, 'r'); - //guard - $this->assertInternalType('resource', $resource); + // guard + $this->assertIsResource($resource); $message = new RdKafkaMessage('theBody', ['aProp' => $resource]); diff --git a/pkg/rdkafka/Tests/RdKafkaConnectionFactoryConfigTest.php b/pkg/rdkafka/Tests/RdKafkaConnectionFactoryConfigTest.php index d3a6a5dab..7ecb1bd7f 100644 --- a/pkg/rdkafka/Tests/RdKafkaConnectionFactoryConfigTest.php +++ b/pkg/rdkafka/Tests/RdKafkaConnectionFactoryConfigTest.php @@ -4,6 +4,7 @@ use Enqueue\RdKafka\RdKafkaConnectionFactory; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; /** @@ -12,6 +13,7 @@ class RdKafkaConnectionFactoryConfigTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testThrowNeitherArrayStringNorNullGivenAsConfig() { @@ -31,15 +33,12 @@ public function testThrowIfSchemeIsNotSupported() /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { $factory = new RdKafkaConnectionFactory($config); - $config = $this->getObjectAttribute($factory, 'config'); + $config = $this->readAttribute($factory, 'config'); $this->assertNotEmpty($config['global']['group.id']); diff --git a/pkg/rdkafka/Tests/RdKafkaConnectionFactoryTest.php b/pkg/rdkafka/Tests/RdKafkaConnectionFactoryTest.php index 47fdc1715..d7121da65 100644 --- a/pkg/rdkafka/Tests/RdKafkaConnectionFactoryTest.php +++ b/pkg/rdkafka/Tests/RdKafkaConnectionFactoryTest.php @@ -3,13 +3,13 @@ namespace Enqueue\RdKafka\Tests; use Enqueue\RdKafka\RdKafkaConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; -/** - * @group rdkafka - */ class RdKafkaConnectionFactoryTest extends TestCase { + use ReadAttributeTrait; + public function testThrowNeitherArrayStringNorNullGivenAsConfig() { $this->expectException(\LogicException::class); @@ -38,7 +38,7 @@ public function testShouldBeExpectedDefaultConfig() { $factory = new RdKafkaConnectionFactory(null); - $config = $this->getObjectAttribute($factory, 'config'); + $config = $this->readAttribute($factory, 'config'); $this->assertNotEmpty($config['global']['group.id']); @@ -55,7 +55,7 @@ public function testShouldBeExpectedDefaultDsnConfig() { $factory = new RdKafkaConnectionFactory('kafka:'); - $config = $this->getObjectAttribute($factory, 'config'); + $config = $this->readAttribute($factory, 'config'); $this->assertNotEmpty($config['global']['group.id']); @@ -70,9 +70,6 @@ public function testShouldBeExpectedDefaultDsnConfig() /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { diff --git a/pkg/rdkafka/Tests/RdKafkaConsumerTest.php b/pkg/rdkafka/Tests/RdKafkaConsumerTest.php index e79a51df0..a0544da6c 100644 --- a/pkg/rdkafka/Tests/RdKafkaConsumerTest.php +++ b/pkg/rdkafka/Tests/RdKafkaConsumerTest.php @@ -11,21 +11,8 @@ use RdKafka\KafkaConsumer; use RdKafka\Message; -/** - * @group rdkafka - */ class RdKafkaConsumerTest extends TestCase { - public function testCouldBeConstructedWithRequiredArguments() - { - new RdKafkaConsumer( - $this->createKafkaConsumerMock(), - $this->createContextMock(), - new RdKafkaTopic(''), - $this->createSerializerMock() - ); - } - public function testShouldReturnQueueSetInConstructor() { $destination = new RdKafkaTopic(''); @@ -45,7 +32,7 @@ public function testShouldReceiveFromQueueAndReturnNullIfNoMessageInQueue() $destination = new RdKafkaTopic('dest'); $kafkaMessage = new Message(); - $kafkaMessage->err = RD_KAFKA_RESP_ERR__TIMED_OUT; + $kafkaMessage->err = \RD_KAFKA_RESP_ERR__TIMED_OUT; $kafkaConsumer = $this->createKafkaConsumerMock(); $kafkaConsumer @@ -74,12 +61,12 @@ public function testShouldPassProperlyConfiguredTopicPartitionOnAssign() $destination = new RdKafkaTopic('dest'); $kafkaMessage = new Message(); - $kafkaMessage->err = RD_KAFKA_RESP_ERR__TIMED_OUT; + $kafkaMessage->err = \RD_KAFKA_RESP_ERR__TIMED_OUT; $kafkaConsumer = $this->createKafkaConsumerMock(); $kafkaConsumer ->expects($this->once()) - ->method('assign') + ->method('subscribe') ; $kafkaConsumer ->expects($this->any()) @@ -94,8 +81,6 @@ public function testShouldPassProperlyConfiguredTopicPartitionOnAssign() $this->createSerializerMock() ); - $consumer->setOffset(12345); - $consumer->receive(1000); $consumer->receive(1000); $consumer->receive(1000); @@ -106,7 +91,7 @@ public function testShouldSubscribeOnFirstReceiveOnly() $destination = new RdKafkaTopic('dest'); $kafkaMessage = new Message(); - $kafkaMessage->err = RD_KAFKA_RESP_ERR__TIMED_OUT; + $kafkaMessage->err = \RD_KAFKA_RESP_ERR__TIMED_OUT; $kafkaConsumer = $this->createKafkaConsumerMock(); $kafkaConsumer @@ -131,12 +116,45 @@ public function testShouldSubscribeOnFirstReceiveOnly() $consumer->receive(1000); } + public function testShouldAssignWhenOffsetIsSet() + { + $destination = new RdKafkaTopic('dest'); + $destination->setPartition(1); + + $kafkaMessage = new Message(); + $kafkaMessage->err = \RD_KAFKA_RESP_ERR__TIMED_OUT; + + $kafkaConsumer = $this->createKafkaConsumerMock(); + $kafkaConsumer + ->expects($this->once()) + ->method('assign') + ; + $kafkaConsumer + ->expects($this->any()) + ->method('consume') + ->willReturn($kafkaMessage) + ; + + $consumer = new RdKafkaConsumer( + $kafkaConsumer, + $this->createContextMock(), + $destination, + $this->createSerializerMock() + ); + + $consumer->setOffset(123); + + $consumer->receive(1000); + $consumer->receive(1000); + $consumer->receive(1000); + } + public function testThrowOnOffsetChangeAfterSubscribing() { $destination = new RdKafkaTopic('dest'); $kafkaMessage = new Message(); - $kafkaMessage->err = RD_KAFKA_RESP_ERR__TIMED_OUT; + $kafkaMessage->err = \RD_KAFKA_RESP_ERR__TIMED_OUT; $kafkaConsumer = $this->createKafkaConsumerMock(); $kafkaConsumer @@ -167,10 +185,10 @@ public function testShouldReceiveFromQueueAndReturnMessageIfMessageInQueue() { $destination = new RdKafkaTopic('dest'); - $expectedMessage = new RdKafkaMessage('theBody', ['foo' => 'fooVal'], ['bar' => 'barVal']); + $expectedMessage = new RdKafkaMessage('theBody', ['foo' => 'fooVal'], ['bar' => 'barVal']); $kafkaMessage = new Message(); - $kafkaMessage->err = RD_KAFKA_RESP_ERR_NO_ERROR; + $kafkaMessage->err = \RD_KAFKA_RESP_ERR_NO_ERROR; $kafkaMessage->payload = 'theSerializedMessage'; $kafkaConsumer = $this->createKafkaConsumerMock(); @@ -232,7 +250,7 @@ public function testShouldAllowGetPreviouslySetSerializer() $expectedSerializer = $this->createSerializerMock(); - //guard + // guard $this->assertNotSame($consumer->getSerializer(), $expectedSerializer); $consumer->setSerializer($expectedSerializer); @@ -241,7 +259,7 @@ public function testShouldAllowGetPreviouslySetSerializer() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|KafkaConsumer + * @return \PHPUnit\Framework\MockObject\MockObject|KafkaConsumer */ private function createKafkaConsumerMock() { @@ -249,7 +267,7 @@ private function createKafkaConsumerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|RdKafkaContext + * @return \PHPUnit\Framework\MockObject\MockObject|RdKafkaContext */ private function createContextMock() { @@ -257,7 +275,7 @@ private function createContextMock() } /** - * @return Serializer|\PHPUnit_Framework_MockObject_MockObject|Serializer + * @return Serializer|\PHPUnit\Framework\MockObject\MockObject|Serializer */ private function createSerializerMock() { diff --git a/pkg/rdkafka/Tests/RdKafkaContextTest.php b/pkg/rdkafka/Tests/RdKafkaContextTest.php index f09c5be1e..dc1b597de 100644 --- a/pkg/rdkafka/Tests/RdKafkaContextTest.php +++ b/pkg/rdkafka/Tests/RdKafkaContextTest.php @@ -6,20 +6,18 @@ use Enqueue\RdKafka\JsonSerializer; use Enqueue\RdKafka\RdKafkaContext; use Enqueue\RdKafka\Serializer; -use Interop\Queue\InvalidDestinationException; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\TemporaryQueueNotSupportedException; use PHPUnit\Framework\TestCase; -/** - * @group rdkafka - */ class RdKafkaContextTest extends TestCase { public function testThrowNotImplementedOnCreateTemporaryQueue() { $context = new RdKafkaContext([]); - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Not implemented'); + $this->expectException(TemporaryQueueNotSupportedException::class); + $context->createTemporaryQueue(); } @@ -68,4 +66,31 @@ public function testShouldInjectItsSerializerToConsumer() $this->assertSame($context->getSerializer(), $producer->getSerializer()); } + + public function testShouldNotCreateConsumerTwice() + { + $context = new RdKafkaContext(['global' => [ + 'group.id' => uniqid('', true), + ]]); + $queue = $context->createQueue('aQueue'); + + $consumer = $context->createConsumer($queue); + $consumer2 = $context->createConsumer($queue); + + $this->assertSame($consumer, $consumer2); + } + + public function testShouldCreateTwoConsumers() + { + $context = new RdKafkaContext(['global' => [ + 'group.id' => uniqid('', true), + ]]); + $queueA = $context->createQueue('aQueue'); + $queueB = $context->createQueue('bQueue'); + + $consumer = $context->createConsumer($queueA); + $consumer2 = $context->createConsumer($queueB); + + $this->assertNotSame($consumer, $consumer2); + } } diff --git a/pkg/rdkafka/Tests/RdKafkaMessageTest.php b/pkg/rdkafka/Tests/RdKafkaMessageTest.php index c2e5c224a..9bcc34642 100644 --- a/pkg/rdkafka/Tests/RdKafkaMessageTest.php +++ b/pkg/rdkafka/Tests/RdKafkaMessageTest.php @@ -6,9 +6,6 @@ use PHPUnit\Framework\TestCase; use RdKafka\Message; -/** - * @group rdkafka - */ class RdKafkaMessageTest extends TestCase { public function testCouldSetGetPartition() diff --git a/pkg/rdkafka/Tests/RdKafkaProducerTest.php b/pkg/rdkafka/Tests/RdKafkaProducerTest.php index 1c076e498..6295fbc1b 100644 --- a/pkg/rdkafka/Tests/RdKafkaProducerTest.php +++ b/pkg/rdkafka/Tests/RdKafkaProducerTest.php @@ -8,23 +8,15 @@ use Enqueue\RdKafka\RdKafkaProducer; use Enqueue\RdKafka\RdKafkaTopic; use Enqueue\RdKafka\Serializer; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; use PHPUnit\Framework\TestCase; use RdKafka\Producer; use RdKafka\ProducerTopic; use RdKafka\TopicConf; -/** - * @group rdkafka - */ class RdKafkaProducerTest extends TestCase { - public function testCouldBeConstructedWithKafkaProducerAndSerializerAsArguments() - { - new RdKafkaProducer($this->createKafkaProducerMock(), $this->createSerializerMock()); - } - public function testThrowIfDestinationInvalid() { $producer = new RdKafkaProducer($this->createKafkaProducerMock(), $this->createSerializerMock()); @@ -45,18 +37,20 @@ public function testThrowIfMessageInvalid() public function testShouldUseSerializerToEncodeMessageAndPutToExpectedTube() { - $message = new RdKafkaMessage('theBody', ['foo' => 'fooVal'], ['bar' => 'barVal']); + $messageHeaders = ['bar' => 'barVal']; + $message = new RdKafkaMessage('theBody', ['foo' => 'fooVal'], $messageHeaders); $message->setKey('key'); $kafkaTopic = $this->createKafkaTopicMock(); $kafkaTopic ->expects($this->once()) - ->method('produce') + ->method('producev') ->with( - RD_KAFKA_PARTITION_UA, + \RD_KAFKA_PARTITION_UA, 0, 'theSerializedMessage', - 'key' + 'key', + $messageHeaders ) ; @@ -67,6 +61,11 @@ public function testShouldUseSerializerToEncodeMessageAndPutToExpectedTube() ->with('theQueueName') ->willReturn($kafkaTopic) ; + $kafkaProducer + ->expects($this->once()) + ->method('poll') + ->with(0) + ; $serializer = $this->createSerializerMock(); $serializer @@ -87,7 +86,7 @@ public function testShouldPassNullAsTopicConfigIfNotSetOnTopic() $kafkaTopic = $this->createKafkaTopicMock(); $kafkaTopic ->expects($this->once()) - ->method('produce') + ->method('producev') ; $kafkaProducer = $this->createKafkaProducerMock(); @@ -97,6 +96,11 @@ public function testShouldPassNullAsTopicConfigIfNotSetOnTopic() ->with('theQueueName', null) ->willReturn($kafkaTopic) ; + $kafkaProducer + ->expects($this->once()) + ->method('poll') + ->with(0) + ; $serializer = $this->createSerializerMock(); $serializer @@ -123,7 +127,7 @@ public function testShouldPassCustomConfAsTopicConfigIfSetOnTopic() $kafkaTopic = $this->createKafkaTopicMock(); $kafkaTopic ->expects($this->once()) - ->method('produce') + ->method('producev') ; $kafkaProducer = $this->createKafkaProducerMock(); @@ -133,6 +137,11 @@ public function testShouldPassCustomConfAsTopicConfigIfSetOnTopic() ->with('theQueueName', $this->identicalTo($conf)) ->willReturn($kafkaTopic) ; + $kafkaProducer + ->expects($this->once()) + ->method('poll') + ->with(0) + ; $serializer = $this->createSerializerMock(); $serializer @@ -155,7 +164,7 @@ public function testShouldAllowGetPreviouslySetSerializer() $expectedSerializer = $this->createSerializerMock(); - //guard + // guard $this->assertNotSame($producer->getSerializer(), $expectedSerializer); $producer->setSerializer($expectedSerializer); @@ -165,15 +174,16 @@ public function testShouldAllowGetPreviouslySetSerializer() public function testShouldAllowSerializersToSerializeKeys() { - $message = new RdKafkaMessage('theBody', ['foo' => 'fooVal'], ['bar' => 'barVal']); + $messageHeaders = ['bar' => 'barVal']; + $message = new RdKafkaMessage('theBody', ['foo' => 'fooVal'], $messageHeaders); $message->setKey('key'); $kafkaTopic = $this->createKafkaTopicMock(); $kafkaTopic ->expects($this->once()) - ->method('produce') + ->method('producev') ->with( - RD_KAFKA_PARTITION_UA, + \RD_KAFKA_PARTITION_UA, 0, 'theSerializedMessage', 'theSerializedKey' @@ -186,6 +196,11 @@ public function testShouldAllowSerializersToSerializeKeys() ->method('newTopic') ->willReturn($kafkaTopic) ; + $kafkaProducer + ->expects($this->once()) + ->method('poll') + ->with(0) + ; $serializer = $this->createSerializerMock(); $serializer @@ -202,8 +217,166 @@ public function testShouldAllowSerializersToSerializeKeys() $producer->send(new RdKafkaTopic('theQueueName'), $message); } + public function testShouldGetPartitionFromMessage(): void + { + $partition = 1; + + $kafkaTopic = $this->createKafkaTopicMock(); + $kafkaTopic + ->expects($this->once()) + ->method('producev') + ->with( + $partition, + 0, + 'theSerializedMessage', + 'theSerializedKey' + ) + ; + + $kafkaProducer = $this->createKafkaProducerMock(); + $kafkaProducer + ->expects($this->once()) + ->method('newTopic') + ->willReturn($kafkaTopic) + ; + $kafkaProducer + ->expects($this->once()) + ->method('poll') + ->with(0) + ; + $messageHeaders = ['bar' => 'barVal']; + $message = new RdKafkaMessage('theBody', ['foo' => 'fooVal'], $messageHeaders); + $message->setKey('key'); + $message->setPartition($partition); + + $serializer = $this->createSerializerMock(); + $serializer + ->expects($this->once()) + ->method('toString') + ->willReturnCallback(function () use ($message) { + $message->setKey('theSerializedKey'); + + return 'theSerializedMessage'; + }) + ; + + $destination = new RdKafkaTopic('theQueueName'); + + $producer = new RdKafkaProducer($kafkaProducer, $serializer); + $producer->send($destination, $message); + } + + public function testShouldGetPartitionFromDestination(): void + { + $partition = 2; + + $kafkaTopic = $this->createKafkaTopicMock(); + $kafkaTopic + ->expects($this->once()) + ->method('producev') + ->with( + $partition, + 0, + 'theSerializedMessage', + 'theSerializedKey' + ) + ; + + $kafkaProducer = $this->createKafkaProducerMock(); + $kafkaProducer + ->expects($this->once()) + ->method('newTopic') + ->willReturn($kafkaTopic) + ; + $kafkaProducer + ->expects($this->once()) + ->method('poll') + ->with(0) + ; + $messageHeaders = ['bar' => 'barVal']; + $message = new RdKafkaMessage('theBody', ['foo' => 'fooVal'], $messageHeaders); + $message->setKey('key'); + + $serializer = $this->createSerializerMock(); + $serializer + ->expects($this->once()) + ->method('toString') + ->willReturnCallback(function () use ($message) { + $message->setKey('theSerializedKey'); + + return 'theSerializedMessage'; + }) + ; + + $destination = new RdKafkaTopic('theQueueName'); + $destination->setPartition($partition); + + $producer = new RdKafkaProducer($kafkaProducer, $serializer); + $producer->send($destination, $message); + } + + public function testShouldAllowFalsyKeyFromMessage(): void + { + $key = 0; + + $kafkaTopic = $this->createKafkaTopicMock(); + $kafkaTopic + ->expects($this->once()) + ->method('producev') + ->with( + \RD_KAFKA_PARTITION_UA, + 0, + '', + $key + ) + ; + + $kafkaProducer = $this->createKafkaProducerMock(); + $kafkaProducer + ->expects($this->once()) + ->method('newTopic') + ->willReturn($kafkaTopic) + ; + + $message = new RdKafkaMessage(); + $message->setKey($key); + + $producer = new RdKafkaProducer($kafkaProducer, $this->createSerializerMock()); + $producer->send(new RdKafkaTopic(''), $message); + } + + public function testShouldAllowFalsyKeyFromDestination(): void + { + $key = 0; + + $kafkaTopic = $this->createKafkaTopicMock(); + $kafkaTopic + ->expects($this->once()) + ->method('producev') + ->with( + \RD_KAFKA_PARTITION_UA, + 0, + '', + $key + ) + ; + + $kafkaProducer = $this->createKafkaProducerMock(); + $kafkaProducer + ->expects($this->once()) + ->method('newTopic') + ->willReturn($kafkaTopic) + ; + + $destination = new RdKafkaTopic(''); + $destination->setKey($key); + + $producer = new RdKafkaProducer($kafkaProducer, $this->createSerializerMock()); + $producer->send($destination, new RdKafkaMessage()); + } + /** - * @return \PHPUnit_Framework_MockObject_MockObject|ProducerTopic + * @return \PHPUnit\Framework\MockObject\MockObject|ProducerTopic */ private function createKafkaTopicMock() { @@ -211,7 +384,7 @@ private function createKafkaTopicMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Producer + * @return \PHPUnit\Framework\MockObject\MockObject|Producer */ private function createKafkaProducerMock() { @@ -219,7 +392,7 @@ private function createKafkaProducerMock() } /** - * @return Serializer|\PHPUnit_Framework_MockObject_MockObject|Serializer + * @return Serializer|\PHPUnit\Framework\MockObject\MockObject|Serializer */ private function createSerializerMock() { diff --git a/pkg/rdkafka/Tests/RdKafkaTopicTest.php b/pkg/rdkafka/Tests/RdKafkaTopicTest.php index 5ed22885a..d0bc8cc13 100644 --- a/pkg/rdkafka/Tests/RdKafkaTopicTest.php +++ b/pkg/rdkafka/Tests/RdKafkaTopicTest.php @@ -6,9 +6,6 @@ use PHPUnit\Framework\TestCase; use RdKafka\TopicConf; -/** - * @group rdkafka - */ class RdKafkaTopicTest extends TestCase { public function testCouldSetGetPartition() diff --git a/pkg/rdkafka/Tests/Spec/RdKafkaConnectionFactoryTest.php b/pkg/rdkafka/Tests/Spec/RdKafkaConnectionFactoryTest.php index 1132e3930..a582aadca 100644 --- a/pkg/rdkafka/Tests/Spec/RdKafkaConnectionFactoryTest.php +++ b/pkg/rdkafka/Tests/Spec/RdKafkaConnectionFactoryTest.php @@ -3,12 +3,9 @@ namespace Enqueue\RdKafka\Tests\Spec; use Enqueue\RdKafka\RdKafkaConnectionFactory; -use Interop\Queue\Spec\PsrConnectionFactorySpec; +use Interop\Queue\Spec\ConnectionFactorySpec; -/** - * @group rdkafka - */ -class RdKafkaConnectionFactoryTest extends PsrConnectionFactorySpec +class RdKafkaConnectionFactoryTest extends ConnectionFactorySpec { protected function createConnectionFactory() { diff --git a/pkg/rdkafka/Tests/Spec/RdKafkaContextTest.php b/pkg/rdkafka/Tests/Spec/RdKafkaContextTest.php index c99c9f051..d049ca74f 100644 --- a/pkg/rdkafka/Tests/Spec/RdKafkaContextTest.php +++ b/pkg/rdkafka/Tests/Spec/RdKafkaContextTest.php @@ -3,12 +3,9 @@ namespace Enqueue\RdKafka\Tests\Spec; use Enqueue\RdKafka\RdKafkaContext; -use Interop\Queue\Spec\PsrContextSpec; +use Interop\Queue\Spec\ContextSpec; -/** - * @group rdkafka - */ -class RdKafkaContextTest extends PsrContextSpec +class RdKafkaContextTest extends ContextSpec { protected function createContext() { diff --git a/pkg/rdkafka/Tests/Spec/RdKafkaMessageTest.php b/pkg/rdkafka/Tests/Spec/RdKafkaMessageTest.php index 6cff6667c..9e230d1b6 100644 --- a/pkg/rdkafka/Tests/Spec/RdKafkaMessageTest.php +++ b/pkg/rdkafka/Tests/Spec/RdKafkaMessageTest.php @@ -3,16 +3,10 @@ namespace Enqueue\RdKafka\Tests\Spec; use Enqueue\RdKafka\RdKafkaMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -/** - * @group rdkafka - */ -class RdKafkaMessageTest extends PsrMessageSpec +class RdKafkaMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new RdKafkaMessage(); diff --git a/pkg/rdkafka/Tests/Spec/RdKafkaQueueTest.php b/pkg/rdkafka/Tests/Spec/RdKafkaQueueTest.php index e6bf40156..863f3e3c5 100644 --- a/pkg/rdkafka/Tests/Spec/RdKafkaQueueTest.php +++ b/pkg/rdkafka/Tests/Spec/RdKafkaQueueTest.php @@ -3,12 +3,9 @@ namespace Enqueue\RdKafka\Tests\Spec; use Enqueue\RdKafka\RdKafkaTopic; -use Interop\Queue\Spec\PsrQueueSpec; +use Interop\Queue\Spec\QueueSpec; -/** - * @group rdkafka - */ -class RdKafkaQueueTest extends PsrQueueSpec +class RdKafkaQueueTest extends QueueSpec { protected function createQueue() { diff --git a/pkg/rdkafka/Tests/Spec/RdKafkaSendToAndReceiveFromTopicTest.php b/pkg/rdkafka/Tests/Spec/RdKafkaSendToAndReceiveFromTopicTest.php index 05b5abfe2..9a969d420 100644 --- a/pkg/rdkafka/Tests/Spec/RdKafkaSendToAndReceiveFromTopicTest.php +++ b/pkg/rdkafka/Tests/Spec/RdKafkaSendToAndReceiveFromTopicTest.php @@ -3,12 +3,12 @@ namespace Enqueue\RdKafka\Tests\Spec; use Enqueue\RdKafka\RdKafkaConnectionFactory; -use Interop\Queue\PsrMessage; +use Interop\Queue\Message; use Interop\Queue\Spec\SendToAndReceiveFromTopicSpec; /** - * @group rdkafka * @group functional + * * @retry 5 */ class RdKafkaSendToAndReceiveFromTopicTest extends SendToAndReceiveFromTopicSpec @@ -19,15 +19,21 @@ public function test() $topic = $this->createTopic($context, uniqid('', true)); - $consumer = $context->createConsumer($topic); - $expectedBody = __CLASS__.time(); + $producer = $context->createProducer(); + $producer->send($topic, $context->createMessage($expectedBody)); + + // Calling close causes Producer to flush (wait for messages to be delivered to Kafka) + $context->close(); + + $consumer = $context->createConsumer($topic); $context->createProducer()->send($topic, $context->createMessage($expectedBody)); - $message = $consumer->receive(10000); // 10 sec + // Initial balancing can take some time, so we want to make sure the timeout is high enough + $message = $consumer->receive(15000); // 15 sec - $this->assertInstanceOf(PsrMessage::class, $message); + $this->assertInstanceOf(Message::class, $message); $consumer->acknowledge($message); $this->assertSame($expectedBody, $message->getBody()); @@ -42,14 +48,12 @@ protected function createContext() 'enable.auto.commit' => 'false', ], 'topic' => [ - 'auto.offset.reset' => 'beginning', + 'auto.offset.reset' => 'earliest', ], ]; $context = (new RdKafkaConnectionFactory($config))->createContext(); - sleep(3); - return $context; } } diff --git a/pkg/rdkafka/Tests/Spec/RdKafkaTopicTest.php b/pkg/rdkafka/Tests/Spec/RdKafkaTopicTest.php index a5c61ade9..08d427883 100644 --- a/pkg/rdkafka/Tests/Spec/RdKafkaTopicTest.php +++ b/pkg/rdkafka/Tests/Spec/RdKafkaTopicTest.php @@ -3,16 +3,10 @@ namespace Enqueue\RdKafka\Tests\Spec; use Enqueue\RdKafka\RdKafkaTopic; -use Interop\Queue\Spec\PsrTopicSpec; +use Interop\Queue\Spec\TopicSpec; -/** - * @group rdkafka - */ -class RdKafkaTopicTest extends PsrTopicSpec +class RdKafkaTopicTest extends TopicSpec { - /** - * {@inheritdoc} - */ protected function createTopic() { return new RdKafkaTopic(self::EXPECTED_TOPIC_NAME); diff --git a/pkg/rdkafka/Tests/Symfony/RdKafkaTransportFactoryTest.php b/pkg/rdkafka/Tests/Symfony/RdKafkaTransportFactoryTest.php deleted file mode 100644 index 6816e1690..000000000 --- a/pkg/rdkafka/Tests/Symfony/RdKafkaTransportFactoryTest.php +++ /dev/null @@ -1,151 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, RdKafkaTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new RdKafkaTransportFactory(); - - $this->assertEquals('rdkafka', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new RdKafkaTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new RdKafkaTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - ]]); - - $this->assertEquals([ - 'topic' => [], - 'commit_async' => false, - 'global' => [], - ], $config); - } - - public function testShouldAllowAddConfigurationAsString() - { - $transport = new RdKafkaTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), ['kafkaDSN']); - - $this->assertEquals([ - 'dsn' => 'kafkaDSN', - 'topic' => [], - 'commit_async' => false, - 'global' => [], - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new RdKafkaTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(RdKafkaConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - ]], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryFromDsnString() - { - $container = new ContainerBuilder(); - - $transport = new RdKafkaTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'dsn' => 'theKafkaDSN', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(RdKafkaConnectionFactory::class, $factory->getClass()); - $this->assertSame(['theKafkaDSN'], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new RdKafkaTransportFactory(); - - $serviceId = $transport->createContext($container, [ - ]); - - $this->assertEquals('enqueue.transport.rdkafka.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.rdkafka.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.rdkafka.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new RdKafkaTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.rdkafka.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(RdKafkaDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.rdkafka.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/rdkafka/Tests/bootstrap.php b/pkg/rdkafka/Tests/bootstrap.php index bf112623e..60f8101ef 100644 --- a/pkg/rdkafka/Tests/bootstrap.php +++ b/pkg/rdkafka/Tests/bootstrap.php @@ -8,7 +8,7 @@ if (false == file_exists($kafkaStubsDir)) { $kafkaStubsDir = __DIR__.'/../../../vendor/kwn/php-rdkafka-stubs'; if (false == file_exists($kafkaStubsDir)) { - throw new \LogicException('The kafka extension is not loaded and stubs could not be found as well'); + throw new LogicException('The kafka extension is not loaded and stubs could not be found as well'); } } diff --git a/pkg/rdkafka/Tests/fix_composer_json.php b/pkg/rdkafka/Tests/fix_composer_json.php deleted file mode 100644 index b778f6f69..000000000 --- a/pkg/rdkafka/Tests/fix_composer_json.php +++ /dev/null @@ -1,9 +0,0 @@ -=5.6", - "ext-rdkafka": "^3.0.3", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1" + "php": "^8.1", + "ext-rdkafka": "^4.0|^5.0|^6.0", + "queue-interop/queue-interop": "^0.8.1" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "kwn/php-rdkafka-stubs": "^1.0.2", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2", + "kwn/php-rdkafka-stubs": "^2.0.3" }, "support": { "email": "opensource@forma-pro.com", @@ -44,13 +41,10 @@ "RdKafka\\": "vendor/kwn/php-rdkafka-stubs/stubs/RdKafka" } }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/rdkafka/phpunit.xml.dist b/pkg/rdkafka/phpunit.xml.dist index d899fd655..a20efef51 100644 --- a/pkg/rdkafka/phpunit.xml.dist +++ b/pkg/rdkafka/phpunit.xml.dist @@ -1,16 +1,19 @@ - + diff --git a/pkg/redis/.github/workflows/ci.yml b/pkg/redis/.github/workflows/ci.yml new file mode 100644 index 000000000..57d501bee --- /dev/null +++ b/pkg/redis/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: redis + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/redis/.travis.yml b/pkg/redis/.travis.yml deleted file mode 100644 index b9cf57fc9..000000000 --- a/pkg/redis/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/redis/Client/RedisDriver.php b/pkg/redis/Client/RedisDriver.php deleted file mode 100644 index 5265c771c..000000000 --- a/pkg/redis/Client/RedisDriver.php +++ /dev/null @@ -1,152 +0,0 @@ -context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $queue = $this->createQueue($this->config->getRouterQueueName()); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($queue, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - } - - /** - * {@inheritdoc} - * - * @return RedisDestination - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - return $this->context->createQueue($transportName); - } - - /** - * {@inheritdoc} - * - * @return RedisMessage - */ - public function createTransportMessage(Message $message) - { - $properties = $message->getProperties(); - - $headers = $message->getHeaders(); - $headers['content_type'] = $message->getContentType(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($properties); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - - return $transportMessage; - } - - /** - * @param RedisMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - - $clientMessage->setContentType($message->getHeader('content_type')); - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setPriority(MessagePriority::NORMAL); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - return $clientMessage; - } - - /** - * @return Config - */ - public function getConfig() - { - return $this->config; - } -} diff --git a/pkg/redis/JsonSerializer.php b/pkg/redis/JsonSerializer.php new file mode 100644 index 000000000..ff67ed880 --- /dev/null +++ b/pkg/redis/JsonSerializer.php @@ -0,0 +1,33 @@ + $message->getBody(), + 'properties' => $message->getProperties(), + 'headers' => $message->getHeaders(), + ]); + + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); + } + + return $json; + } + + public function toMessage(string $string): RedisMessage + { + $data = json_decode($string, true); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); + } + + return new RedisMessage($data['body'], $data['properties'], $data['headers']); + } +} diff --git a/pkg/redis/LuaScripts.php b/pkg/redis/LuaScripts.php new file mode 100644 index 000000000..4a40d2447 --- /dev/null +++ b/pkg/redis/LuaScripts.php @@ -0,0 +1,38 @@ +redis = $redis; + if (false == class_exists(Client::class)) { + throw new \LogicException('The package "predis/predis" must be installed. Please run "composer req predis/predis:^1.1" to install it'); + } + + $this->options = $config['predis_options']; + + $this->parameters = [ + 'scheme' => $config['scheme'], + 'host' => $config['host'], + 'port' => $config['port'], + 'password' => $config['password'], + 'database' => $config['database'], + 'path' => $config['path'], + 'async' => $config['async'], + 'persistent' => $config['persistent'], + 'timeout' => $config['timeout'], + 'read_write_timeout' => $config['read_write_timeout'], + ]; + + if ($config['ssl']) { + $this->parameters['ssl'] = $config['ssl']; + } } - /** - * {@inheritdoc} - */ - public function lpush($key, $value) + public function eval(string $script, array $keys = [], array $args = []) { try { - $this->redis->lpush($key, [$value]); + // mixed eval($script, $numkeys, $keyOrArg1 = null, $keyOrArgN = null) + return call_user_func_array([$this->redis, 'eval'], array_merge([$script, count($keys)], $keys, $args)); } catch (PRedisServerException $e) { - throw new ServerException('lpush command has failed', null, $e); + throw new ServerException('eval command has failed', 0, $e); } } - /** - * {@inheritdoc} - */ - public function brpop($key, $timeout) + public function zadd(string $key, string $value, float $score): int + { + try { + return $this->redis->zadd($key, [$value => $score]); + } catch (PRedisServerException $e) { + throw new ServerException('zadd command has failed', 0, $e); + } + } + + public function zrem(string $key, string $value): int { try { - if ($result = $this->redis->brpop([$key], $timeout)) { - return $result[1]; + return $this->redis->zrem($key, [$value]); + } catch (PRedisServerException $e) { + throw new ServerException('zrem command has failed', 0, $e); + } + } + + public function lpush(string $key, string $value): int + { + try { + return $this->redis->lpush($key, [$value]); + } catch (PRedisServerException $e) { + throw new ServerException('lpush command has failed', 0, $e); + } + } + + public function brpop(array $keys, int $timeout): ?RedisResult + { + try { + if ($result = $this->redis->brpop($keys, $timeout)) { + return new RedisResult($result[0], $result[1]); } + + return null; } catch (PRedisServerException $e) { - throw new ServerException('brpop command has failed', null, $e); + throw new ServerException('brpop command has failed', 0, $e); } } - /** - * {@inheritdoc} - */ - public function rpop($key) + public function rpop(string $key): ?RedisResult { try { - return $this->redis->rpop($key); + if ($message = $this->redis->rpop($key)) { + return new RedisResult($key, $message); + } + + return null; } catch (PRedisServerException $e) { - throw new ServerException('rpop command has failed', null, $e); + throw new ServerException('rpop command has failed', 0, $e); } } - /** - * {@inheritdoc} - */ - public function connect() + public function connect(): void { + if ($this->redis) { + return; + } + + $this->redis = new Client($this->parameters, $this->options); + + // No need to pass "auth" here because Predis already handles this internally + $this->redis->connect(); } - /** - * {@inheritdoc} - */ - public function disconnect() + public function disconnect(): void { $this->redis->disconnect(); } - /** - * {@inheritdoc} - */ - public function del($key) + public function del(string $key): void { $this->redis->del([$key]); } diff --git a/pkg/redis/PhpRedis.php b/pkg/redis/PhpRedis.php index 71ead40d4..1a229e3c9 100644 --- a/pkg/redis/PhpRedis.php +++ b/pkg/redis/PhpRedis.php @@ -15,93 +15,127 @@ class PhpRedis implements Redis private $config; /** - * @param array $config + * @see https://github.com/phpredis/phpredis#parameters */ public function __construct(array $config) { - $this->config = array_replace([ - 'host' => null, - 'port' => null, - 'timeout' => null, - 'reserved' => null, - 'retry_interval' => null, - 'persisted' => false, - 'database' => 0, - ], $config); + if (false == class_exists(\Redis::class)) { + throw new \LogicException('You must install the redis extension to use phpredis'); + } + + $this->config = $config; } - /** - * {@inheritdoc} - */ - public function lpush($key, $value) + public function eval(string $script, array $keys = [], array $args = []) { - if (false == $this->redis->lPush($key, $value)) { - throw new ServerException($this->redis->getLastError()); + try { + return $this->redis->eval($script, array_merge($keys, $args), count($keys)); + } catch (\RedisException $e) { + throw new ServerException('eval command has failed', 0, $e); } } - /** - * {@inheritdoc} - */ - public function brpop($key, $timeout) + public function zadd(string $key, string $value, float $score): int { - if ($result = $this->redis->brPop([$key], $timeout)) { - return $result[1]; + try { + return $this->redis->zAdd($key, $score, $value); + } catch (\RedisException $e) { + throw new ServerException('zadd command has failed', 0, $e); } } - /** - * {@inheritdoc} - */ - public function rpop($key) + public function zrem(string $key, string $value): int { - return $this->redis->rPop($key); + try { + return $this->redis->zRem($key, $value); + } catch (\RedisException $e) { + throw new ServerException('zrem command has failed', 0, $e); + } } - /** - * {@inheritdoc} - */ - public function connect() + public function lpush(string $key, string $value): int { - if (false == $this->redis) { - $this->redis = new \Redis(); - - if ($this->config['persisted']) { - $this->redis->pconnect( - $this->config['host'], - $this->config['port'], - $this->config['timeout'] - ); - } else { - $this->redis->connect( - $this->config['host'], - $this->config['port'], - $this->config['timeout'], - $this->config['reserved'], - $this->config['retry_interval'] - ); + try { + return $this->redis->lPush($key, $value); + } catch (\RedisException $e) { + throw new ServerException('lpush command has failed', 0, $e); + } + } + + public function brpop(array $keys, int $timeout): ?RedisResult + { + try { + if ($result = $this->redis->brPop($keys, $timeout)) { + return new RedisResult($result[0], $result[1]); } - $this->redis->select($this->config['database']); + return null; + } catch (\RedisException $e) { + throw new ServerException('brpop command has failed', 0, $e); } + } - return $this->redis; + public function rpop(string $key): ?RedisResult + { + try { + if ($message = $this->redis->rPop($key)) { + return new RedisResult($key, $message); + } + + return null; + } catch (\RedisException $e) { + throw new ServerException('rpop command has failed', 0, $e); + } } - /** - * {@inheritdoc} - */ - public function disconnect() + public function connect(): void + { + if ($this->redis) { + return; + } + + $supportedSchemes = ['redis', 'rediss', 'tcp', 'unix']; + if (false == in_array($this->config['scheme'], $supportedSchemes, true)) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported by php extension. It must be one of "%s"', $this->config['scheme'], implode('", "', $supportedSchemes))); + } + + $this->redis = new \Redis(); + + $connectionMethod = $this->config['persistent'] ? 'pconnect' : 'connect'; + + $host = 'rediss' === $this->config['scheme'] ? 'tls://'.$this->config['host'] : $this->config['host']; + + $result = call_user_func( + [$this->redis, $connectionMethod], + 'unix' === $this->config['scheme'] ? $this->config['path'] : $host, + $this->config['port'], + $this->config['timeout'], + $this->config['persistent'] ? ($this->config['phpredis_persistent_id'] ?? null) : null, + $this->config['phpredis_retry_interval'] ?? null, + $this->config['read_write_timeout'] + ); + + if (false == $result) { + throw new ServerException('Failed to connect.'); + } + + if ($this->config['password']) { + $this->redis->auth($this->config['password']); + } + + if (null !== $this->config['database']) { + $this->redis->select($this->config['database']); + } + } + + public function disconnect(): void { if ($this->redis) { $this->redis->close(); } } - /** - * {@inheritdoc} - */ - public function del($key) + public function del(string $key): void { $this->redis->del($key); } diff --git a/pkg/redis/README.md b/pkg/redis/README.md index b77b1df65..7b368bb35 100644 --- a/pkg/redis/README.md +++ b/pkg/redis/README.md @@ -1,27 +1,36 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Redis Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/redis.png?branch=master)](https://travis-ci.org/php-enqueue/redis) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/redis/ci.yml?branch=master)](https://github.com/php-enqueue/redis/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/redis/d/total.png)](https://packagist.org/packages/enqueue/redis) [![Latest Stable Version](https://poser.pugx.org/enqueue/redis/version.png)](https://packagist.org/packages/enqueue/redis) - -This is an implementation of PSR specification. It allows you to send and consume message with Redis store as a broker. + +This is an implementation of Queue Interop specification. It allows you to send and consume message with Redis store as a broker. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/redis/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/redis/Redis.php b/pkg/redis/Redis.php index 796081775..04165af9d 100644 --- a/pkg/redis/Redis.php +++ b/pkg/redis/Redis.php @@ -1,38 +1,55 @@ can be a host, or the path to a unix domain socket - * 'port' => optional - * 'timeout' => value in seconds (optional, default is 0.0 meaning unlimited) - * 'reserved' => should be null if $retry_interval is specified - * 'retry_interval' => retry interval in milliseconds. - * 'vendor' => 'The library used internally to interact with Redis server - * 'redis' => 'Used only if vendor is custom, should contain an instance of \Enqueue\Redis\Redis interface. - * 'persisted' => bool, Whether it use single persisted connection or open a new one for every context - * 'lazy' => the connection will be performed as later as possible, if the option set to true - * 'database' => Database index to select when connected (default value: 0) + * 'dsn' => A redis DSN string. + * 'scheme' => Specifies the protocol used to communicate with an instance of Redis. + * 'host' => IP or hostname of the target server. + * 'port' => TCP/IP port of the target server. + * 'path' => Path of the UNIX domain socket file used when connecting to Redis using UNIX domain sockets. + * 'database' => Accepts a numeric value that is used by Predis to automatically select a logical database with the SELECT command. + * 'password' => Accepts a value used to authenticate with a Redis server protected by password with the AUTH command. + * 'async' => Specifies if connections to the server is estabilished in a non-blocking way (that is, the client is not blocked while the underlying resource performs the actual connection). + * 'persistent' => Specifies if the underlying connection resource should be left open when a script ends its lifecycle. + * 'lazy' => The connection will be performed as later as possible, if the option set to true + * 'timeout' => Timeout (expressed in seconds) used to connect to a Redis server after which an exception is thrown. + * 'read_write_timeout' => Timeout (expressed in seconds) used when performing read or write operations on the underlying network resource after which an exception is thrown. + * 'predis_options' => An array of predis specific options. + * 'ssl' => could be any of http://fi2.php.net/manual/en/context.ssl.php#refsect1-context.ssl-options + * 'redelivery_delay' => Default 300 sec. Returns back message into the queue if message was not acknowledged or rejected after this delay. + * It could happen if consumer has failed with fatal error or even if message processing is slow and takes more than this time. * ]. * * or * - * redis: - * redis:?vendor=predis + * redis://h:asdfqwer1234asdf@ec2-111-1-1-1.compute-1.amazonaws.com:111 + * tls://127.0.0.1?ssl[cafile]=private.pem&ssl[verify_peer]=1 + * + * or + * + * instance of Enqueue\Redis * - * @param array|string|null $config + * @param array|string|Redis|null $config */ public function __construct($config = 'redis:') { - if (empty($config) || 'redis:' === $config) { + if ($config instanceof Redis) { + $this->redis = $config; + $this->config = $this->defaultConfig(); + + return; + } + + if (empty($config)) { $config = []; } elseif (is_string($config)) { $config = $this->parseDsn($config); } elseif (is_array($config)) { + if (array_key_exists('dsn', $config)) { + $config = array_replace_recursive($config, $this->parseDsn($config['dsn'])); + + unset($config['dsn']); + } } else { - throw new \LogicException('The config must be either an array of options, a DSN string or null'); + throw new \LogicException(sprintf('The config must be either an array of options, a DSN string, null or instance of %s', Redis::class)); } $this->config = array_replace($this->defaultConfig(), $config); - - $supportedVendors = ['predis', 'phpredis', 'custom']; - if (false == in_array($this->config['vendor'], $supportedVendors, true)) { - throw new \LogicException(sprintf( - 'Unsupported redis vendor given. It must be either "%s". Got "%s"', - implode('", "', $supportedVendors), - $this->config['vendor'] - )); - } } /** - * {@inheritdoc} - * * @return RedisContext */ - public function createContext() + public function createContext(): Context { if ($this->config['lazy']) { return new RedisContext(function () { return $this->createRedis(); - }); + }, (int) $this->config['redelivery_delay']); } - return new RedisContext($this->createRedis()); + return new RedisContext($this->createRedis(), (int) $this->config['redelivery_delay']); } - /** - * @return Redis - */ - private function createRedis() + private function createRedis(): Redis { if (false == $this->redis) { - if ('phpredis' == $this->config['vendor'] && false == $this->redis) { + if (in_array('phpredis', $this->config['scheme_extensions'], true)) { $this->redis = new PhpRedis($this->config); - } - - if ('predis' == $this->config['vendor'] && false == $this->redis) { - $this->redis = new PRedis(new Client($this->config, ['exceptions' => true])); - } - - if ('custom' == $this->config['vendor'] && false == $this->redis) { - if (empty($this->config['redis'])) { - throw new \LogicException('The redis option should be set if vendor is custom.'); - } - - if (false == $this->config['redis'] instanceof Redis) { - throw new \LogicException(sprintf('The redis option should be instance of "%s".', Redis::class)); - } - - $this->redis = $this->config['redis']; + } else { + $this->redis = new PRedis($this->config); } $this->redis->connect(); @@ -109,52 +108,55 @@ private function createRedis() return $this->redis; } - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) + private function parseDsn(string $dsn): array { - if (false === strpos($dsn, 'redis:')) { - throw new \LogicException(sprintf('The given DSN "%s" is not supported. Must start with "redis:".', $dsn)); - } + $dsn = Dsn::parseFirst($dsn); - if (false === $config = parse_url(http://github.com/$dsn)) { - throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); + $supportedSchemes = ['redis', 'rediss', 'tcp', 'tls', 'unix']; + if (false == in_array($dsn->getSchemeProtocol(), $supportedSchemes, true)) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be one of "%s"', $dsn->getSchemeProtocol(), implode('", "', $supportedSchemes))); } - if ($query = parse_url(http://github.com/$dsn,%20PHP_URL_QUERY)) { - $queryConfig = []; - parse_str($query, $queryConfig); + $database = $dsn->getDecimal('database'); - $config = array_replace($queryConfig, $config); + // try use path as database name if not set. + if (null === $database && 'unix' !== $dsn->getSchemeProtocol() && null !== $dsn->getPath()) { + $database = (int) ltrim($dsn->getPath(), '/'); } - unset($config['query'], $config['scheme']); - - $config['lazy'] = empty($config['lazy']) ? false : true; - $config['persisted'] = empty($config['persisted']) ? false : true; - - return $config; + return array_filter(array_replace($dsn->getQuery(), [ + 'scheme' => $dsn->getSchemeProtocol(), + 'scheme_extensions' => $dsn->getSchemeExtensions(), + 'host' => $dsn->getHost(), + 'port' => $dsn->getPort(), + 'path' => $dsn->getPath(), + 'database' => $database, + 'password' => $dsn->getPassword() ?: $dsn->getUser() ?: $dsn->getString('password'), + 'async' => $dsn->getBool('async'), + 'persistent' => $dsn->getBool('persistent'), + 'timeout' => $dsn->getFloat('timeout'), + 'read_write_timeout' => $dsn->getFloat('read_write_timeout'), + ]), function ($value) { return null !== $value; }); } - /** - * @return array - */ - private function defaultConfig() + private function defaultConfig(): array { return [ - 'host' => 'localhost', + 'scheme' => 'redis', + 'scheme_extensions' => [], + 'host' => '127.0.0.1', 'port' => 6379, - 'timeout' => null, - 'reserved' => null, - 'retry_interval' => null, - 'vendor' => 'phpredis', - 'redis' => null, - 'persisted' => false, + 'path' => null, + 'database' => null, + 'password' => null, + 'async' => false, + 'persistent' => false, 'lazy' => true, - 'database' => 0, + 'timeout' => 5.0, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'redelivery_delay' => 300, ]; } } diff --git a/pkg/redis/RedisConsumer.php b/pkg/redis/RedisConsumer.php index 6c57f798c..ca3733d4b 100644 --- a/pkg/redis/RedisConsumer.php +++ b/pkg/redis/RedisConsumer.php @@ -1,13 +1,18 @@ context = $context; $this->queue = $queue; } + public function getRedeliveryDelay(): ?int + { + return $this->redeliveryDelay; + } + + public function setRedeliveryDelay(int $delay): void + { + $this->redeliveryDelay = $delay; + } + /** - * {@inheritdoc} - * * @return RedisDestination */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } /** - * {@inheritdoc} - * - * @return RedisMessage|null + * @return RedisMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { - $timeout = (int) ($timeout / 1000); - if (empty($timeout)) { - // Caused by - // Predis\Response\ServerException: ERR timeout is not an integer or out of range - // /mqdev/vendor/predis/predis/src/Client.php:370 + $timeout = (int) ceil($timeout / 1000); - return $this->receiveNoWait(); + if ($timeout <= 0) { + while (true) { + if ($message = $this->receive(5000)) { + return $message; + } + } } - if ($message = $this->getRedis()->brpop($this->queue->getName(), $timeout)) { - return RedisMessage::jsonUnserialize($message); - } + return $this->receiveMessage([$this->queue], $timeout, $this->redeliveryDelay); } /** - * {@inheritdoc} - * - * @return RedisMessage|null + * @return RedisMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { - if ($message = $this->getRedis()->rpop($this->queue->getName())) { - return RedisMessage::jsonUnserialize($message); - } + return $this->receiveMessageNoWait($this->queue, $this->redeliveryDelay); } /** - * {@inheritdoc} - * * @param RedisMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { - // do nothing. redis transport always works in auto ack mode + $this->getRedis()->zrem($this->queue->getName().':reserved', $message->getReservedKey()); } /** - * {@inheritdoc} - * * @param RedisMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { InvalidMessageException::assertMessageInstanceOf($message, RedisMessage::class); - // do nothing on reject. redis transport always works in auto ack mode + $this->acknowledge($message); if ($requeue) { - $this->context->createProducer()->send($this->queue, $message); + $message = $this->getContext()->getSerializer()->toMessage($message->getReservedKey()); + $message->setRedelivered(true); + + if ($message->getTimeToLive()) { + $message->setHeader('expires_at', time() + $message->getTimeToLive()); + } + + $payload = $this->getContext()->getSerializer()->toString($message); + + $this->getRedis()->lpush($this->queue->getName(), $payload); } } - /** - * @return Redis - */ - private function getRedis() + private function getContext(): RedisContext + { + return $this->context; + } + + private function getRedis(): Redis { return $this->context->getRedis(); } diff --git a/pkg/redis/RedisConsumerHelperTrait.php b/pkg/redis/RedisConsumerHelperTrait.php new file mode 100644 index 000000000..063ff1fbd --- /dev/null +++ b/pkg/redis/RedisConsumerHelperTrait.php @@ -0,0 +1,114 @@ +queueNames) { + $this->queueNames = []; + foreach ($queues as $queue) { + $this->queueNames[] = $queue->getName(); + } + } + + while ($thisTimeout > 0) { + $this->migrateExpiredMessages($this->queueNames); + + if (false == $result = $this->getContext()->getRedis()->brpop($this->queueNames, $thisTimeout)) { + return null; + } + + $this->pushQueueNameBack($result->getKey()); + + if ($message = $this->processResult($result, $redeliveryDelay)) { + return $message; + } + + $thisTimeout -= time() - $startAt; + } + + return null; + } + + protected function receiveMessageNoWait(RedisDestination $destination, int $redeliveryDelay): ?RedisMessage + { + $this->migrateExpiredMessages([$destination->getName()]); + + if ($result = $this->getContext()->getRedis()->rpop($destination->getName())) { + return $this->processResult($result, $redeliveryDelay); + } + + return null; + } + + protected function processResult(RedisResult $result, int $redeliveryDelay): ?RedisMessage + { + $message = $this->getContext()->getSerializer()->toMessage($result->getMessage()); + + $now = time(); + + if (0 === $message->getAttempts() && $expiresAt = $message->getHeader('expires_at')) { + if ($now > $expiresAt) { + return null; + } + } + + $message->setHeader('attempts', $message->getAttempts() + 1); + $message->setRedelivered($message->getAttempts() > 1); + $message->setKey($result->getKey()); + $message->setReservedKey($this->getContext()->getSerializer()->toString($message)); + + $reservedQueue = $result->getKey().':reserved'; + $redeliveryAt = $now + $redeliveryDelay; + + $this->getContext()->getRedis()->zadd($reservedQueue, $message->getReservedKey(), $redeliveryAt); + + return $message; + } + + protected function pushQueueNameBack(string $queueName): void + { + if (count($this->queueNames) <= 1) { + return; + } + + if (false === $from = array_search($queueName, $this->queueNames, true)) { + throw new \LogicException(sprintf('Queue name was not found: "%s"', $queueName)); + } + + $to = count($this->queueNames) - 1; + + $out = array_splice($this->queueNames, $from, 1); + array_splice($this->queueNames, $to, 0, $out); + } + + protected function migrateExpiredMessages(array $queueNames): void + { + $now = time(); + + foreach ($queueNames as $queueName) { + $this->getContext()->getRedis() + ->eval(LuaScripts::migrateExpired(), [$queueName.':delayed', $queueName], [$now]); + + $this->getContext()->getRedis() + ->eval(LuaScripts::migrateExpired(), [$queueName.':reserved', $queueName], [$now]); + } + } +} diff --git a/pkg/redis/RedisContext.php b/pkg/redis/RedisContext.php index b87cc5e5f..346375f8d 100644 --- a/pkg/redis/RedisContext.php +++ b/pkg/redis/RedisContext.php @@ -1,15 +1,24 @@ redis = $redis; } elseif (is_callable($redis)) { $this->redisFactory = $redis; } else { - throw new \InvalidArgumentException(sprintf( - 'The $redis argument must be either %s or callable that returns %s once called.', - Redis::class, - Redis::class - )); + throw new \InvalidArgumentException(sprintf('The $redis argument must be either %s or callable that returns %s once called.', Redis::class, Redis::class)); } + + $this->redeliveryDelay = $redeliveryDelay; + $this->setSerializer(new JsonSerializer()); } /** - * {@inheritdoc} - * * @return RedisMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new RedisMessage($body, $properties, $headers); } /** - * {@inheritdoc} - * * @return RedisDestination */ - public function createTopic($topicName) + public function createTopic(string $topicName): Topic { return new RedisDestination($topicName); } /** - * {@inheritdoc} - * * @return RedisDestination */ - public function createQueue($queueName) + public function createQueue(string $queueName): Queue { return new RedisDestination($queueName); } /** - * @param RedisDestination|PsrQueue $queue + * @param RedisDestination $queue */ - public function deleteQueue(PsrQueue $queue) + public function deleteQueue(Queue $queue): void { InvalidDestinationException::assertDestinationInstanceOf($queue, RedisDestination::class); - $this->getRedis()->del($queue->getName()); + $this->deleteDestination($queue); } /** - * @param RedisDestination|PsrTopic $topic + * @param RedisDestination $topic */ - public function deleteTopic(PsrTopic $topic) + public function deleteTopic(Topic $topic): void { InvalidDestinationException::assertDestinationInstanceOf($topic, RedisDestination::class); - $this->getRedis()->del($topic->getName()); + $this->deleteDestination($topic); } - /** - * {@inheritdoc} - */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { - throw new \LogicException('Not implemented'); + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); } /** - * {@inheritdoc} - * * @return RedisProducer */ - public function createProducer() + public function createProducer(): Producer { - return new RedisProducer($this->getRedis()); + return new RedisProducer($this); } /** - * {@inheritdoc} - * * @param RedisDestination $destination * * @return RedisConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, RedisDestination::class); - return new RedisConsumer($this, $destination); + $consumer = new RedisConsumer($this, $destination); + $consumer->setRedeliveryDelay($this->redeliveryDelay); + + return $consumer; } - public function close() + /** + * @return RedisSubscriptionConsumer + */ + public function createSubscriptionConsumer(): SubscriptionConsumer { - $this->getRedis()->disconnect(); + $consumer = new RedisSubscriptionConsumer($this); + $consumer->setRedeliveryDelay($this->redeliveryDelay); + + return $consumer; } /** - * @return Redis + * @param RedisDestination $queue */ - public function getRedis() + public function purgeQueue(Queue $queue): void + { + $this->deleteDestination($queue); + } + + public function close(): void + { + $this->getRedis()->disconnect(); + } + + public function getRedis(): Redis { if (false == $this->redis) { $redis = call_user_func($this->redisFactory); if (false == $redis instanceof Redis) { - throw new \LogicException(sprintf( - 'The factory must return instance of %s. It returned %s', - Redis::class, - is_object($redis) ? get_class($redis) : gettype($redis) - )); + throw new \LogicException(sprintf('The factory must return instance of %s. It returned %s', Redis::class, is_object($redis) ? $redis::class : gettype($redis))); } $this->redis = $redis; @@ -147,4 +162,11 @@ public function getRedis() return $this->redis; } + + private function deleteDestination(RedisDestination $destination): void + { + $this->getRedis()->del($destination->getName()); + $this->getRedis()->del($destination->getName().':delayed'); + $this->getRedis()->del($destination->getName().':reserved'); + } } diff --git a/pkg/redis/RedisDestination.php b/pkg/redis/RedisDestination.php index c06433635..72e61e5a1 100644 --- a/pkg/redis/RedisDestination.php +++ b/pkg/redis/RedisDestination.php @@ -1,45 +1,35 @@ name = $name; } - /** - * @return string - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { return $this->getName(); } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getTopicName(): string { return $this->getName(); } diff --git a/pkg/redis/RedisMessage.php b/pkg/redis/RedisMessage.php index 754de075f..708bdbc97 100644 --- a/pkg/redis/RedisMessage.php +++ b/pkg/redis/RedisMessage.php @@ -1,10 +1,12 @@ body = $body; $this->properties = $properties; @@ -40,196 +47,156 @@ public function __construct($body = '', array $properties = [], array $headers = $this->redelivered = false; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * {@inheritdoc} - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * {@inheritdoc} - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * {@inheritdoc} - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = (bool) $redelivered; } - /** - * {@inheritdoc} - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id'); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id'); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } - /** - * {@inheritdoc} - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * {@inheritdoc} - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } + public function getAttempts(): int + { + return (int) $this->getHeader('attempts', 0); + } + + public function getTimeToLive(): ?int + { + return $this->getHeader('time_to_live'); + } + /** - * {@inheritdoc} + * Set time to live in milliseconds. */ - public function jsonSerialize() + public function setTimeToLive(?int $timeToLive = null): void { - return [ - 'body' => $this->getBody(), - 'properties' => $this->getProperties(), - 'headers' => $this->getHeaders(), - ]; + $this->setHeader('time_to_live', $timeToLive); + } + + public function getDeliveryDelay(): ?int + { + return $this->getHeader('delivery_delay'); } /** - * @param string $json - * - * @return RedisMessage + * Set delay in milliseconds. */ - public static function jsonUnserialize($json) - { - $data = json_decode($json, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); - } - - return new self($data['body'], $data['properties'], $data['headers']); + public function setDeliveryDelay(?int $deliveryDelay = null): void + { + $this->setHeader('delivery_delay', $deliveryDelay); + } + + public function getReservedKey(): ?string + { + return $this->reservedKey; + } + + public function setReservedKey(string $reservedKey) + { + $this->reservedKey = $reservedKey; + } + + public function getKey(): ?string + { + return $this->key; + } + + public function setKey(string $key): void + { + $this->key = $key; } } diff --git a/pkg/redis/RedisProducer.php b/pkg/redis/RedisProducer.php index e34506b3a..3ad3e5bb2 100644 --- a/pkg/redis/RedisProducer.php +++ b/pkg/redis/RedisProducer.php @@ -1,99 +1,117 @@ redis = $redis; + $this->context = $context; } /** - * {@inheritdoc} - * * @param RedisDestination $destination * @param RedisMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, RedisDestination::class); InvalidMessageException::assertMessageInstanceOf($message, RedisMessage::class); - $this->redis->lpush($destination->getName(), json_encode($message)); + $message->setMessageId(Uuid::uuid4()->toString()); + $message->setHeader('attempts', 0); + + if (null !== $this->timeToLive && null === $message->getTimeToLive()) { + $message->setTimeToLive($this->timeToLive); + } + + if (null !== $this->deliveryDelay && null === $message->getDeliveryDelay()) { + $message->setDeliveryDelay($this->deliveryDelay); + } + + if ($message->getTimeToLive()) { + $message->setHeader('expires_at', time() + $message->getTimeToLive()); + } + + $payload = $this->context->getSerializer()->toString($message); + + if ($message->getDeliveryDelay()) { + $deliveryAt = time() + $message->getDeliveryDelay() / 1000; + $this->context->getRedis()->zadd($destination->getName().':delayed', $payload, $deliveryAt); + } else { + $this->context->getRedis()->lpush($destination->getName(), $payload); + } } /** - * {@inheritdoc} + * @return self */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { - if (null === $deliveryDelay) { - return; - } + $this->deliveryDelay = $deliveryDelay; - throw new \LogicException('Not implemented'); + return $this; } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { - return null; + return $this->deliveryDelay; } /** - * {@inheritdoc} + * @return RedisProducer */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { if (null === $priority) { - return; + return $this; } - throw new \LogicException('Not implemented'); + throw PriorityNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return null; } /** - * {@inheritdoc} + * @return self */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { - if (null === $timeToLive) { - return; - } + $this->timeToLive = $timeToLive; - throw new \LogicException('Not implemented'); + return $this; } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { - return null; + return $this->timeToLive; } } diff --git a/pkg/redis/RedisResult.php b/pkg/redis/RedisResult.php new file mode 100644 index 000000000..83a90f576 --- /dev/null +++ b/pkg/redis/RedisResult.php @@ -0,0 +1,34 @@ +key = $key; + $this->message = $message; + } + + public function getKey(): string + { + return $this->key; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/pkg/redis/RedisSubscriptionConsumer.php b/pkg/redis/RedisSubscriptionConsumer.php new file mode 100644 index 000000000..d0b34634d --- /dev/null +++ b/pkg/redis/RedisSubscriptionConsumer.php @@ -0,0 +1,132 @@ +context = $context; + $this->subscribers = []; + } + + public function getRedeliveryDelay(): ?int + { + return $this->redeliveryDelay; + } + + public function setRedeliveryDelay(int $delay): void + { + $this->redeliveryDelay = $delay; + } + + public function consume(int $timeout = 0): void + { + if (empty($this->subscribers)) { + throw new \LogicException('No subscribers'); + } + + $timeout = (int) ceil($timeout / 1000); + $endAt = time() + $timeout; + + $queues = []; + /** @var Consumer $consumer */ + foreach ($this->subscribers as list($consumer)) { + $queues[] = $consumer->getQueue(); + } + + while (true) { + if ($message = $this->receiveMessage($queues, $timeout ?: 5, $this->redeliveryDelay)) { + list($consumer, $callback) = $this->subscribers[$message->getKey()]; + + if (false === call_user_func($callback, $message, $consumer)) { + return; + } + } + + if ($timeout && microtime(true) >= $endAt) { + return; + } + } + } + + /** + * @param RedisConsumer $consumer + */ + public function subscribe(Consumer $consumer, callable $callback): void + { + if (false == $consumer instanceof RedisConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', RedisConsumer::class, $consumer::class)); + } + + $queueName = $consumer->getQueue()->getQueueName(); + if (array_key_exists($queueName, $this->subscribers)) { + if ($this->subscribers[$queueName][0] === $consumer && $this->subscribers[$queueName][1] === $callback) { + return; + } + + throw new \InvalidArgumentException(sprintf('There is a consumer subscribed to queue: "%s"', $queueName)); + } + + $this->subscribers[$queueName] = [$consumer, $callback]; + $this->queueNames = null; + } + + /** + * @param RedisConsumer $consumer + */ + public function unsubscribe(Consumer $consumer): void + { + if (false == $consumer instanceof RedisConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', RedisConsumer::class, $consumer::class)); + } + + $queueName = $consumer->getQueue()->getQueueName(); + + if (false == array_key_exists($queueName, $this->subscribers)) { + return; + } + + if ($this->subscribers[$queueName][0] !== $consumer) { + return; + } + + unset($this->subscribers[$queueName]); + $this->queueNames = null; + } + + public function unsubscribeAll(): void + { + $this->subscribers = []; + $this->queueNames = null; + } + + private function getContext(): RedisContext + { + return $this->context; + } +} diff --git a/pkg/redis/Serializer.php b/pkg/redis/Serializer.php new file mode 100644 index 000000000..a936a9328 --- /dev/null +++ b/pkg/redis/Serializer.php @@ -0,0 +1,12 @@ +serializer = $serializer; + } + + /** + * @return Serializer + */ + public function getSerializer() + { + return $this->serializer; + } +} diff --git a/pkg/redis/ServerException.php b/pkg/redis/ServerException.php index 98273adf5..f7503f57b 100644 --- a/pkg/redis/ServerException.php +++ b/pkg/redis/ServerException.php @@ -1,8 +1,10 @@ name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->beforeNormalization() - ->ifTrue(function ($node) { - return empty($node['dsn']) && (empty($node['host']) || empty($node['vendor'])); - }) - ->thenInvalid('Invalid configuration %s') - ->end() - ->children() - ->scalarNode('dsn') - ->info('The redis connection given as DSN. For example redis://host:port?vendor=predis') - ->end() - ->scalarNode('host') - ->cannotBeEmpty() - ->info('can be a host, or the path to a unix domain socket') - ->end() - ->integerNode('port')->end() - ->enumNode('vendor') - ->values(['phpredis', 'predis', 'custom']) - ->cannotBeEmpty() - ->info('The library used internally to interact with Redis server') - ->end() - ->scalarNode('redis') - ->cannotBeEmpty() - ->info('A custom redis service id, used with vendor true only') - ->end() - ->booleanNode('persisted') - ->defaultFalse() - ->info('bool, Whether it use single persisted connection or open a new one for every context') - ->end() - ->booleanNode('lazy') - ->defaultTrue() - ->info('the connection will be performed as later as possible, if the option set to true') - ->end() - ->integerNode('database') - ->defaultValue(0) - ->info('Database index to select when connected.') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - if (false == empty($config['redis'])) { - $config['redis'] = new Reference($config['redis']); - } - - $factory = new Definition(RedisConnectionFactory::class); - $factory->setArguments([isset($config['dsn']) ? $config['dsn'] : $config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(RedisContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(RedisDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/redis/Tests/Client/RedisDriverTest.php b/pkg/redis/Tests/Client/RedisDriverTest.php deleted file mode 100644 index 419f2f27c..000000000 --- a/pkg/redis/Tests/Client/RedisDriverTest.php +++ /dev/null @@ -1,366 +0,0 @@ -assertClassImplements(DriverInterface::class, RedisDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new RedisDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - } - - public function testShouldReturnConfigObject() - { - $config = Config::create(); - - $driver = new RedisDriver($this->createPsrContextMock(), $config, $this->createDummyQueueMetaRegistry()); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new RedisDestination('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new RedisDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new RedisDestination('aName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new RedisDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new RedisMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new RedisDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - - $this->assertNull($clientMessage->getExpire()); - $this->assertSame(MessagePriority::NORMAL, $clientMessage->getPriority()); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setPriority(MessagePriority::VERY_HIGH); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new RedisMessage()) - ; - - $driver = new RedisDriver( - $context, - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(RedisMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testShouldSendMessageToRouterQueue() - { - $topic = new RedisDestination('aDestinationName'); - $transportMessage = new RedisMessage(); - $config = $this->createDummyConfig(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.default') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RedisDriver( - $context, - $config, - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new RedisDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new RedisDestination('aDestinationName'); - $transportMessage = new RedisMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RedisDriver( - $context, - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new RedisDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new RedisDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldDoNothingOnSetupBroker() - { - $context = $this->createPsrContextMock(); - // setup router - $context - ->expects($this->never()) - ->method('createTopic') - ; - $context - ->expects($this->never()) - ->method('createQueue') - ; - - $meta = new QueueMetaRegistry(Config::create(), [ - 'default' => [], - ]); - - $driver = new RedisDriver( - $context, - Config::create(), - $meta - ); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|RedisContext - */ - private function createPsrContextMock() - { - return $this->createMock(RedisContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer - */ - private function createPsrProducerMock() - { - return $this->createMock(PsrProducer::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/redis/Tests/Functional/CommonUseCasesTrait.php b/pkg/redis/Tests/Functional/CommonUseCasesTrait.php index ac12adb09..bdab87b31 100644 --- a/pkg/redis/Tests/Functional/CommonUseCasesTrait.php +++ b/pkg/redis/Tests/Functional/CommonUseCasesTrait.php @@ -61,7 +61,10 @@ public function testProduceAndReceiveOneMessageSentDirectlyToQueue() $this->assertEquals(__METHOD__, $message->getBody()); $this->assertEquals(['FooProperty' => 'FooVal'], $message->getProperties()); - $this->assertEquals(['BarHeader' => 'BarVal'], $message->getHeaders()); + $this->assertCount(3, $message->getHeaders()); + $this->assertSame(1, $message->getHeader('attempts')); + $this->assertSame('BarVal', $message->getHeader('BarHeader')); + $this->assertNotEmpty('BarVal', $message->getHeader('message_id')); } public function testProduceAndReceiveOneMessageSentDirectlyToTopic() @@ -88,7 +91,7 @@ public function testConsumerReceiveMessageWithZeroTimeout() $consumer = $this->getContext()->createConsumer($topic); - //guard + // guard $this->assertNull($consumer->receive(1000)); $message = $this->getContext()->createMessage(__METHOD__); @@ -99,7 +102,7 @@ public function testConsumerReceiveMessageWithZeroTimeout() $actualMessage = $consumer->receive(0); $this->assertInstanceOf(RedisMessage::class, $actualMessage); - $consumer->acknowledge($message); + $consumer->acknowledge($actualMessage); $this->assertEquals(__METHOD__, $message->getBody()); } @@ -109,15 +112,15 @@ public function testShouldReceiveMessagesInExpectedOrder() $queue = $this->getContext()->createQueue('enqueue.test_queue'); $producer = $this->getContext()->createProducer(); - $producer->send($queue, $this->getContext()->createMessage(1)); - $producer->send($queue, $this->getContext()->createMessage(2)); - $producer->send($queue, $this->getContext()->createMessage(3)); + $producer->send($queue, $this->getContext()->createMessage('1')); + $producer->send($queue, $this->getContext()->createMessage('2')); + $producer->send($queue, $this->getContext()->createMessage('3')); $consumer = $this->getContext()->createConsumer($queue); - $this->assertSame(1, $consumer->receiveNoWait()->getBody()); - $this->assertSame(2, $consumer->receiveNoWait()->getBody()); - $this->assertSame(3, $consumer->receiveNoWait()->getBody()); + $this->assertSame('1', $consumer->receiveNoWait()->getBody()); + $this->assertSame('2', $consumer->receiveNoWait()->getBody()); + $this->assertSame('3', $consumer->receiveNoWait()->getBody()); } /** diff --git a/pkg/redis/Tests/Functional/ConsumptionUseCasesTrait.php b/pkg/redis/Tests/Functional/ConsumptionUseCasesTrait.php index d582bb3ba..0f69070f6 100644 --- a/pkg/redis/Tests/Functional/ConsumptionUseCasesTrait.php +++ b/pkg/redis/Tests/Functional/ConsumptionUseCasesTrait.php @@ -9,7 +9,7 @@ use Enqueue\Consumption\QueueConsumer; use Enqueue\Consumption\Result; use Enqueue\Redis\RedisContext; -use Interop\Queue\PsrMessage; +use Interop\Queue\Message; trait ConsumptionUseCasesTrait { @@ -30,7 +30,7 @@ public function testConsumeOneMessageAndExit() $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); } @@ -61,10 +61,10 @@ public function testConsumeOneMessageAndSendReplyExit() $queueConsumer->bind($replyQueue, $replyProcessor); $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); - $this->assertInstanceOf(PsrMessage::class, $replyProcessor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $replyProcessor->lastProcessedMessage); $this->assertEquals(__METHOD__.'.reply', $replyProcessor->lastProcessedMessage->getBody()); } diff --git a/pkg/redis/Tests/Functional/PRedisCommonUseCasesTest.php b/pkg/redis/Tests/Functional/PRedisCommonUseCasesTest.php index 8c95e47de..9ac2a037b 100644 --- a/pkg/redis/Tests/Functional/PRedisCommonUseCasesTest.php +++ b/pkg/redis/Tests/Functional/PRedisCommonUseCasesTest.php @@ -11,15 +11,15 @@ */ class PRedisCommonUseCasesTest extends TestCase { - use RedisExtension; use CommonUseCasesTrait; + use RedisExtension; /** * @var RedisContext */ private $context; - public function setUp() + protected function setUp(): void { $this->context = $this->buildPRedisContext(); @@ -27,14 +27,11 @@ public function setUp() $this->context->deleteTopic($this->context->createTopic('enqueue.test_topic')); } - public function tearDown() + protected function tearDown(): void { $this->context->close(); } - /** - * {@inheritdoc} - */ protected function getContext() { return $this->context; diff --git a/pkg/redis/Tests/Functional/PRedisConsumptionUseCasesTest.php b/pkg/redis/Tests/Functional/PRedisConsumptionUseCasesTest.php index 87fca37fd..e61cd1f0f 100644 --- a/pkg/redis/Tests/Functional/PRedisConsumptionUseCasesTest.php +++ b/pkg/redis/Tests/Functional/PRedisConsumptionUseCasesTest.php @@ -11,15 +11,15 @@ */ class PRedisConsumptionUseCasesTest extends TestCase { - use RedisExtension; use ConsumptionUseCasesTrait; + use RedisExtension; /** * @var RedisContext */ private $context; - public function setUp() + protected function setUp(): void { $this->context = $this->buildPRedisContext(); @@ -27,14 +27,11 @@ public function setUp() $this->context->deleteQueue($this->context->createQueue('enqueue.test_queue_reply')); } - public function tearDown() + protected function tearDown(): void { $this->context->close(); } - /** - * {@inheritdoc} - */ protected function getContext() { return $this->context; diff --git a/pkg/redis/Tests/Functional/PhpRedisCommonUseCasesTest.php b/pkg/redis/Tests/Functional/PhpRedisCommonUseCasesTest.php index 2d5559d34..f36843ec9 100644 --- a/pkg/redis/Tests/Functional/PhpRedisCommonUseCasesTest.php +++ b/pkg/redis/Tests/Functional/PhpRedisCommonUseCasesTest.php @@ -11,15 +11,15 @@ */ class PhpRedisCommonUseCasesTest extends TestCase { - use RedisExtension; use CommonUseCasesTrait; + use RedisExtension; /** * @var RedisContext */ private $context; - public function setUp() + protected function setUp(): void { $this->context = $this->buildPhpRedisContext(); @@ -27,14 +27,11 @@ public function setUp() $this->context->deleteTopic($this->context->createTopic('enqueue.test_topic')); } - public function tearDown() + protected function tearDown(): void { $this->context->close(); } - /** - * {@inheritdoc} - */ protected function getContext() { return $this->context; diff --git a/pkg/redis/Tests/Functional/PhpRedisConsumptionUseCasesTest.php b/pkg/redis/Tests/Functional/PhpRedisConsumptionUseCasesTest.php index 50c639f92..073c1aff9 100644 --- a/pkg/redis/Tests/Functional/PhpRedisConsumptionUseCasesTest.php +++ b/pkg/redis/Tests/Functional/PhpRedisConsumptionUseCasesTest.php @@ -11,15 +11,15 @@ */ class PhpRedisConsumptionUseCasesTest extends TestCase { - use RedisExtension; use ConsumptionUseCasesTrait; + use RedisExtension; /** * @var RedisContext */ private $context; - public function setUp() + protected function setUp(): void { $this->context = $this->buildPhpRedisContext(); @@ -27,14 +27,11 @@ public function setUp() $this->context->deleteQueue($this->context->createQueue('enqueue.test_queue_reply')); } - public function tearDown() + protected function tearDown(): void { $this->context->close(); } - /** - * {@inheritdoc} - */ protected function getContext() { return $this->context; diff --git a/pkg/redis/Tests/Functional/StubProcessor.php b/pkg/redis/Tests/Functional/StubProcessor.php index 1aeef3c39..b7c15ada2 100644 --- a/pkg/redis/Tests/Functional/StubProcessor.php +++ b/pkg/redis/Tests/Functional/StubProcessor.php @@ -2,18 +2,18 @@ namespace Enqueue\Redis\Tests\Functional; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; -class StubProcessor implements PsrProcessor +class StubProcessor implements Processor { public $result = self::ACK; - /** @var PsrMessage */ + /** @var Message */ public $lastProcessedMessage; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $this->lastProcessedMessage = $message; diff --git a/pkg/redis/Tests/RedisConnectionFactoryConfigTest.php b/pkg/redis/Tests/RedisConnectionFactoryConfigTest.php index cfcdf959b..37e831823 100644 --- a/pkg/redis/Tests/RedisConnectionFactoryConfigTest.php +++ b/pkg/redis/Tests/RedisConnectionFactoryConfigTest.php @@ -2,8 +2,10 @@ namespace Enqueue\Redis\Tests; +use Enqueue\Redis\Redis; use Enqueue\Redis\RedisConnectionFactory; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; /** @@ -12,19 +14,20 @@ class RedisConnectionFactoryConfigTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testThrowNeitherArrayStringNorNullGivenAsConfig() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The config must be either an array of options, a DSN string or null'); + $this->expectExceptionMessage('The config must be either an array of options, a DSN string, null or instance of Enqueue\Redis\Redis'); new RedisConnectionFactory(new \stdClass()); } - public function testThrowIfSchemeIsNotAmqp() + public function testThrowIfSchemeIsNotRedis() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN "http://example.com/" is not supported. Must start with "redis:".'); + $this->expectExceptionMessage('The given scheme protocol "http" is not supported. It must be one of "redis", "rediss", "tcp", "tls", "unix"'); new RedisConnectionFactory('http://example.com/'); } @@ -32,24 +35,24 @@ public function testThrowIfSchemeIsNotAmqp() public function testThrowIfDsnCouldNotBeParsed() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Failed to parse DSN "redis://:@/"'); + $this->expectExceptionMessage('The DSN is invalid.'); - new RedisConnectionFactory('redis://:@/'); + new RedisConnectionFactory('foo'); } - public function testThrowIfVendorIsInvalid() + public function testCouldBeCreatedWithRedisInstance() { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Unsupported redis vendor given. It must be either "predis", "phpredis", "custom". Got "invalidVendor"'); + $redisMock = $this->createMock(Redis::class); + + $factory = new RedisConnectionFactory($redisMock); + $this->assertAttributeSame($redisMock, 'redis', $factory); - new RedisConnectionFactory(['vendor' => 'invalidVendor']); + $context = $factory->createContext(); + $this->assertSame($redisMock, $context->getRedis()); } /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { @@ -63,65 +66,221 @@ public static function provideConfigs() yield [ null, [ - 'host' => 'localhost', + 'host' => '127.0.0.1', + 'scheme' => 'redis', 'port' => 6379, - 'timeout' => null, - 'reserved' => null, - 'retry_interval' => null, - 'vendor' => 'phpredis', - 'persisted' => false, + 'timeout' => 5., + 'database' => null, + 'password' => null, + 'scheme_extensions' => [], + 'path' => null, + 'async' => false, + 'persistent' => false, 'lazy' => true, - 'database' => 0, - 'redis' => null, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'redelivery_delay' => 300, ], ]; yield [ 'redis:', [ - 'host' => 'localhost', + 'host' => '127.0.0.1', + 'scheme' => 'redis', 'port' => 6379, - 'timeout' => null, - 'reserved' => null, - 'retry_interval' => null, - 'vendor' => 'phpredis', - 'persisted' => false, + 'timeout' => 5., + 'database' => null, + 'password' => null, + 'scheme_extensions' => [], + 'path' => null, + 'async' => false, + 'persistent' => false, 'lazy' => true, - 'database' => 0, - 'redis' => null, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'redelivery_delay' => 300, ], ]; yield [ [], [ - 'host' => 'localhost', + 'host' => '127.0.0.1', + 'scheme' => 'redis', 'port' => 6379, - 'timeout' => null, - 'reserved' => null, - 'retry_interval' => null, - 'vendor' => 'phpredis', - 'persisted' => false, + 'timeout' => 5., + 'database' => null, + 'password' => null, + 'scheme_extensions' => [], + 'path' => null, + 'async' => false, + 'persistent' => false, 'lazy' => true, - 'database' => 0, - 'redis' => null, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'redelivery_delay' => 300, ], ]; yield [ - 'redis://localhost:1234?foo=bar&lazy=0&persisted=true&database=5', + 'unix:/path/to/redis.sock?foo=bar&database=5', [ - 'host' => 'localhost', + 'host' => '127.0.0.1', + 'scheme' => 'unix', + 'port' => 6379, + 'timeout' => 5., + 'database' => 5, + 'password' => null, + 'scheme_extensions' => [], + 'path' => '/path/to/redis.sock', + 'async' => false, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'foo' => 'bar', + 'redelivery_delay' => 300, + ], + ]; + + yield [ + ['dsn' => 'redis://expectedHost:1234/5', 'host' => 'shouldBeOverwrittenHost', 'foo' => 'bar'], + [ + 'host' => 'expectedHost', + 'scheme' => 'redis', 'port' => 1234, - 'timeout' => null, - 'reserved' => null, - 'retry_interval' => null, - 'vendor' => 'phpredis', - 'persisted' => true, - 'lazy' => false, + 'timeout' => 5., + 'database' => 5, + 'password' => null, + 'scheme_extensions' => [], + 'path' => '/5', + 'async' => false, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, 'foo' => 'bar', + 'redelivery_delay' => 300, + ], + ]; + + yield [ + 'redis+predis://localhost:1234/5?foo=bar&persistent=true', + [ + 'host' => 'localhost', + 'scheme' => 'redis', + 'port' => 1234, + 'timeout' => 5., 'database' => 5, - 'redis' => null, + 'password' => null, + 'scheme_extensions' => ['predis'], + 'path' => '/5', + 'async' => false, + 'persistent' => true, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'foo' => 'bar', + 'redelivery_delay' => 300, + ], + ]; + + // check normal redis connection for php redis extension + yield [ + 'redis+phpredis://localhost:1234?foo=bar', + [ + 'host' => 'localhost', + 'scheme' => 'redis', + 'port' => 1234, + 'timeout' => 5., + 'database' => null, + 'password' => null, + 'scheme_extensions' => ['phpredis'], + 'path' => null, + 'async' => false, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'foo' => 'bar', + 'redelivery_delay' => 300, + ], + ]; + + // check normal redis connection for predis library + yield [ + 'redis+predis://localhost:1234?foo=bar', + [ + 'host' => 'localhost', + 'scheme' => 'redis', + 'port' => 1234, + 'timeout' => 5., + 'database' => null, + 'password' => null, + 'scheme_extensions' => ['predis'], + 'path' => null, + 'async' => false, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'foo' => 'bar', + 'redelivery_delay' => 300, + ], + ]; + + // check tls connection for predis library + yield [ + 'rediss+predis://localhost:1234?foo=bar&async=1', + [ + 'host' => 'localhost', + 'scheme' => 'rediss', + 'port' => 1234, + 'timeout' => 5., + 'database' => null, + 'password' => null, + 'scheme_extensions' => ['predis'], + 'path' => null, + 'async' => true, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'foo' => 'bar', + 'redelivery_delay' => 300, + ], + ]; + + // check tls connection for predis library + yield [ + 'rediss+phpredis://localhost:1234?foo=bar&async=1', + [ + 'host' => 'localhost', + 'scheme' => 'rediss', + 'port' => 1234, + 'timeout' => 5., + 'database' => null, + 'password' => null, + 'scheme_extensions' => ['phpredis'], + 'path' => null, + 'async' => true, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'foo' => 'bar', + 'redelivery_delay' => 300, ], ]; @@ -129,16 +288,112 @@ public static function provideConfigs() ['host' => 'localhost', 'port' => 1234, 'foo' => 'bar'], [ 'host' => 'localhost', + 'scheme' => 'redis', 'port' => 1234, - 'timeout' => null, - 'reserved' => null, - 'retry_interval' => null, - 'vendor' => 'phpredis', - 'persisted' => false, + 'timeout' => 5., + 'database' => null, + 'password' => null, + 'scheme_extensions' => [], + 'path' => null, + 'async' => false, + 'persistent' => false, 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, 'foo' => 'bar', - 'database' => 0, - 'redis' => null, + 'redelivery_delay' => 300, + ], + ]; + + // heroku redis + yield [ + 'redis://h:asdfqwer1234asdf@ec2-111-1-1-1.compute-1.amazonaws.com:111', + [ + 'host' => 'ec2-111-1-1-1.compute-1.amazonaws.com', + 'scheme' => 'redis', + 'port' => 111, + 'timeout' => 5., + 'database' => null, + 'password' => 'asdfqwer1234asdf', + 'scheme_extensions' => [], + 'path' => null, + 'async' => false, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'redelivery_delay' => 300, + ], + ]; + + // password as user + yield [ + 'redis://asdfqwer1234asdf@foo', + [ + 'host' => 'foo', + 'scheme' => 'redis', + 'port' => 6379, + 'timeout' => 5., + 'database' => null, + 'password' => 'asdfqwer1234asdf', + 'scheme_extensions' => [], + 'path' => null, + 'async' => false, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'redelivery_delay' => 300, + ], + ]; + + // password as query parameter + yield [ + 'redis:?password=asdfqwer1234asdf', + [ + 'host' => '127.0.0.1', + 'scheme' => 'redis', + 'port' => 6379, + 'timeout' => 5., + 'database' => null, + 'password' => 'asdfqwer1234asdf', + 'scheme_extensions' => [], + 'path' => null, + 'async' => false, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'ssl' => null, + 'redelivery_delay' => 300, + ], + ]; + + // from predis doc + yield [ + 'tls://127.0.0.1?ssl[cafile]=private.pem&ssl[verify_peer]=1', + [ + 'host' => '127.0.0.1', + 'scheme' => 'tls', + 'port' => 6379, + 'timeout' => 5., + 'database' => null, + 'scheme_extensions' => [], + 'path' => null, + 'async' => false, + 'persistent' => false, + 'lazy' => true, + 'read_write_timeout' => null, + 'predis_options' => null, + 'password' => null, + 'ssl' => [ + 'cafile' => 'private.pem', + 'verify_peer' => '1', + ], + 'redelivery_delay' => 300, ], ]; } diff --git a/pkg/redis/Tests/RedisConnectionFactoryTest.php b/pkg/redis/Tests/RedisConnectionFactoryTest.php index e6d740836..1f9b3a259 100644 --- a/pkg/redis/Tests/RedisConnectionFactoryTest.php +++ b/pkg/redis/Tests/RedisConnectionFactoryTest.php @@ -2,20 +2,21 @@ namespace Enqueue\Redis\Tests; -use Enqueue\Redis\Redis; use Enqueue\Redis\RedisConnectionFactory; use Enqueue\Redis\RedisContext; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\ConnectionFactory; use PHPUnit\Framework\TestCase; class RedisConnectionFactoryTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, RedisConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, RedisConnectionFactory::class); } public function testShouldCreateLazyContext() @@ -27,47 +28,6 @@ public function testShouldCreateLazyContext() $this->assertInstanceOf(RedisContext::class, $context); $this->assertAttributeEquals(null, 'redis', $context); - $this->assertInternalType('callable', $this->readAttribute($context, 'redisFactory')); - } - - public function testShouldThrowIfVendorIsCustomButRedisInstanceNotSet() - { - $factory = new RedisConnectionFactory([ - 'vendor' => 'custom', - 'redis' => null, - 'lazy' => false, - ]); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The redis option should be set if vendor is custom.'); - $factory->createContext(); - } - - public function testShouldThrowIfVendorIsCustomButRedisIsNotInstanceOfRedis() - { - $factory = new RedisConnectionFactory([ - 'vendor' => 'custom', - 'redis' => new \stdClass(), - 'lazy' => false, - ]); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The redis option should be instance of "Enqueue\Redis\Redis".'); - $factory->createContext(); - } - - public function testShouldUseCustomRedisInstance() - { - $redisMock = $this->createMock(Redis::class); - - $factory = new RedisConnectionFactory([ - 'vendor' => 'custom', - 'redis' => $redisMock, - 'lazy' => false, - ]); - - $context = $factory->createContext(); - - $this->assertAttributeSame($redisMock, 'redis', $context); + self::assertIsCallable($this->readAttribute($context, 'redisFactory')); } } diff --git a/pkg/redis/Tests/RedisConsumerTest.php b/pkg/redis/Tests/RedisConsumerTest.php index 55ab35346..56373c18a 100644 --- a/pkg/redis/Tests/RedisConsumerTest.php +++ b/pkg/redis/Tests/RedisConsumerTest.php @@ -2,14 +2,16 @@ namespace Enqueue\Redis\Tests; +use Enqueue\Redis\JsonSerializer; use Enqueue\Redis\Redis; use Enqueue\Redis\RedisConsumer; use Enqueue\Redis\RedisContext; use Enqueue\Redis\RedisDestination; use Enqueue\Redis\RedisMessage; use Enqueue\Redis\RedisProducer; +use Enqueue\Redis\RedisResult; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConsumer; +use Interop\Queue\Consumer; class RedisConsumerTest extends \PHPUnit\Framework\TestCase { @@ -17,12 +19,7 @@ class RedisConsumerTest extends \PHPUnit\Framework\TestCase public function testShouldImplementConsumerInterface() { - $this->assertClassImplements(PsrConsumer::class, RedisConsumer::class); - } - - public function testCouldBeConstructedWithContextAndDestinationAndPreFetchCountAsArguments() - { - new RedisConsumer($this->createContextMock(), new RedisDestination('aQueue')); + $this->assertClassImplements(Consumer::class, RedisConsumer::class); } public function testShouldReturnDestinationSetInConstructorOnGetQueue() @@ -34,41 +31,86 @@ public function testShouldReturnDestinationSetInConstructorOnGetQueue() $this->assertSame($destination, $consumer->getQueue()); } - public function testShouldDoNothingOnAcknowledge() + public function testShouldAcknowledgeMessage() { - $consumer = new RedisConsumer($this->createContextMock(), new RedisDestination('aQueue')); + $redisMock = $this->createRedisMock(); + $redisMock + ->expects($this->once()) + ->method('zrem') + ->with('aQueue:reserved', 'reserved-key') + ->willReturn(1) + ; - $consumer->acknowledge(new RedisMessage()); - } + $contextMock = $this->createContextMock(); + $contextMock + ->expects($this->once()) + ->method('getRedis') + ->willReturn($redisMock) + ; - public function testShouldDoNothingOnReject() - { - $consumer = new RedisConsumer($this->createContextMock(), new RedisDestination('aQueue')); + $message = new RedisMessage(); + $message->setReservedKey('reserved-key'); + + $consumer = new RedisConsumer($contextMock, new RedisDestination('aQueue')); - $consumer->reject(new RedisMessage()); + $consumer->acknowledge($message); } - public function testShouldSendSameMessageToDestinationOnReQueue() + public function testShouldRejectMessage() { + $redisMock = $this->createRedisMock(); + $redisMock + ->expects($this->once()) + ->method('zrem') + ->with('aQueue:reserved', 'reserved-key') + ->willReturn(1) + ; + + $contextMock = $this->createContextMock(); + $contextMock + ->expects($this->once()) + ->method('getRedis') + ->willReturn($redisMock) + ; + $message = new RedisMessage(); + $message->setReservedKey('reserved-key'); - $destination = new RedisDestination('aQueue'); + $consumer = new RedisConsumer($contextMock, new RedisDestination('aQueue')); + + $consumer->reject($message); + } - $producerMock = $this->createProducerMock(); - $producerMock + public function testShouldSendSameMessageToDestinationOnReQueue() + { + $redisMock = $this->createRedisMock(); + $redisMock ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($destination), $this->identicalTo($message)) + ->method('lpush') + ->with('aQueue', '{"body":"text","properties":[],"headers":{"attempts":0}}') + ->willReturn(1) ; + $serializer = new JsonSerializer(); + $contextMock = $this->createContextMock(); $contextMock - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producerMock) + ->expects($this->any()) + ->method('getRedis') + ->willReturn($redisMock) + ; + $contextMock + ->expects($this->any()) + ->method('getSerializer') + ->willReturn($serializer) ; - $consumer = new RedisConsumer($contextMock, $destination); + $message = new RedisMessage(); + $message->setBody('text'); + $message->setHeader('attempts', 0); + $message->setReservedKey($serializer->toString($message)); + + $consumer = new RedisConsumer($contextMock, new RedisDestination('aQueue')); $consumer->reject($message, true); } @@ -81,13 +123,13 @@ public function testShouldCallRedisBRPopAndReturnNullIfNothingInQueueOnReceive() $redisMock ->expects($this->once()) ->method('brpop') - ->with('aQueue', 2) + ->with(['aQueue'], 2) ->willReturn(null) ; $contextMock = $this->createContextMock(); $contextMock - ->expects($this->once()) + ->expects($this->any()) ->method('getRedis') ->willReturn($redisMock) ; @@ -101,20 +143,27 @@ public function testShouldCallRedisBRPopAndReturnMessageIfOneInQueueOnReceive() { $destination = new RedisDestination('aQueue'); + $serializer = new JsonSerializer(); + $redisMock = $this->createRedisMock(); $redisMock ->expects($this->once()) ->method('brpop') - ->with('aQueue', 2) - ->willReturn(json_encode(new RedisMessage('aBody'))) + ->with(['aQueue'], 2) + ->willReturn(new RedisResult('aQueue', $serializer->toString(new RedisMessage('aBody')))) ; $contextMock = $this->createContextMock(); $contextMock - ->expects($this->once()) + ->expects($this->any()) ->method('getRedis') ->willReturn($redisMock) ; + $contextMock + ->expects($this->any()) + ->method('getSerializer') + ->willReturn($serializer) + ; $consumer = new RedisConsumer($contextMock, $destination); @@ -124,10 +173,60 @@ public function testShouldCallRedisBRPopAndReturnMessageIfOneInQueueOnReceive() $this->assertSame('aBody', $message->getBody()); } + public function testShouldCallRedisBRPopSeveralTimesWithFiveSecondTimeoutIfZeroTimeoutIsPassed() + { + $destination = new RedisDestination('aQueue'); + + $expectedTimeout = 5; + + $serializer = new JsonSerializer(); + + $redisMock = $this->createRedisMock(); + $redisMock + ->expects($this->at(2)) + ->method('brpop') + ->with(['aQueue'], $expectedTimeout) + ->willReturn(null) + ; + $redisMock + ->expects($this->at(5)) + ->method('brpop') + ->with(['aQueue'], $expectedTimeout) + ->willReturn(null) + ; + $redisMock + ->expects($this->at(8)) + ->method('brpop') + ->with(['aQueue'], $expectedTimeout) + ->willReturn(new RedisResult('aQueue', $serializer->toString(new RedisMessage('aBody')))) + ; + + $contextMock = $this->createContextMock(); + $contextMock + ->expects($this->atLeastOnce()) + ->method('getRedis') + ->willReturn($redisMock) + ; + $contextMock + ->expects($this->atLeastOnce()) + ->method('getSerializer') + ->willReturn($serializer) + ; + + $consumer = new RedisConsumer($contextMock, $destination); + + $message = $consumer->receive(0); + + $this->assertInstanceOf(RedisMessage::class, $message); + $this->assertSame('aBody', $message->getBody()); + } + public function testShouldCallRedisRPopAndReturnNullIfNothingInQueueOnReceiveNoWait() { $destination = new RedisDestination('aQueue'); + $serializer = new JsonSerializer(); + $redisMock = $this->createRedisMock(); $redisMock ->expects($this->once()) @@ -138,10 +237,15 @@ public function testShouldCallRedisRPopAndReturnNullIfNothingInQueueOnReceiveNoW $contextMock = $this->createContextMock(); $contextMock - ->expects($this->once()) + ->expects($this->any()) ->method('getRedis') ->willReturn($redisMock) ; + $contextMock + ->expects($this->any()) + ->method('getSerializer') + ->willReturn($serializer) + ; $consumer = new RedisConsumer($contextMock, $destination); @@ -152,20 +256,27 @@ public function testShouldCallRedisRPopAndReturnMessageIfOneInQueueOnReceiveNoWa { $destination = new RedisDestination('aQueue'); + $serializer = new JsonSerializer(); + $redisMock = $this->createRedisMock(); $redisMock ->expects($this->once()) ->method('rpop') ->with('aQueue') - ->willReturn(json_encode(new RedisMessage('aBody'))) + ->willReturn(new RedisResult('aQueue', $serializer->toString(new RedisMessage('aBody')))) ; $contextMock = $this->createContextMock(); $contextMock - ->expects($this->once()) + ->expects($this->atLeastOnce()) ->method('getRedis') ->willReturn($redisMock) ; + $contextMock + ->expects($this->any()) + ->method('getSerializer') + ->willReturn($serializer) + ; $consumer = new RedisConsumer($contextMock, $destination); @@ -176,7 +287,7 @@ public function testShouldCallRedisRPopAndReturnMessageIfOneInQueueOnReceiveNoWa } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Redis + * @return \PHPUnit\Framework\MockObject\MockObject|Redis */ private function createRedisMock() { @@ -184,7 +295,7 @@ private function createRedisMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|RedisProducer + * @return \PHPUnit\Framework\MockObject\MockObject|RedisProducer */ private function createProducerMock() { @@ -192,7 +303,7 @@ private function createProducerMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|RedisContext + * @return \PHPUnit\Framework\MockObject\MockObject|RedisContext */ private function createContextMock() { diff --git a/pkg/redis/Tests/RedisContextTest.php b/pkg/redis/Tests/RedisContextTest.php index eedfaf29a..6395e954e 100644 --- a/pkg/redis/Tests/RedisContextTest.php +++ b/pkg/redis/Tests/RedisContextTest.php @@ -10,9 +10,11 @@ use Enqueue\Redis\RedisDestination; use Enqueue\Redis\RedisMessage; use Enqueue\Redis\RedisProducer; +use Enqueue\Redis\RedisSubscriptionConsumer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\TemporaryQueueNotSupportedException; class RedisContextTest extends \PHPUnit\Framework\TestCase { @@ -20,31 +22,19 @@ class RedisContextTest extends \PHPUnit\Framework\TestCase public function testShouldImplementContextInterface() { - $this->assertClassImplements(PsrContext::class, RedisContext::class); - } - - public function testCouldBeConstructedWithRedisAsFirstArgument() - { - new RedisContext($this->createRedisMock()); - } - - public function testCouldBeConstructedWithRedisFactoryAsFirstArgument() - { - new RedisContext(function () { - return $this->createRedisMock(); - }); + $this->assertClassImplements(Context::class, RedisContext::class); } public function testThrowIfNeitherRedisNorFactoryGiven() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The $redis argument must be either Enqueue\Redis\Redis or callable that returns Enqueue\Redis\Redis once called.'); - new RedisContext(new \stdClass()); + new RedisContext(new \stdClass(), 300); } public function testShouldAllowCreateEmptyMessage() { - $context = new RedisContext($this->createRedisMock()); + $context = new RedisContext($this->createRedisMock(), 300); $message = $context->createMessage(); @@ -57,7 +47,7 @@ public function testShouldAllowCreateEmptyMessage() public function testShouldAllowCreateCustomMessage() { - $context = new RedisContext($this->createRedisMock()); + $context = new RedisContext($this->createRedisMock(), 300); $message = $context->createMessage('theBody', ['aProp' => 'aPropVal'], ['aHeader' => 'aHeaderVal']); @@ -70,7 +60,7 @@ public function testShouldAllowCreateCustomMessage() public function testShouldCreateQueue() { - $context = new RedisContext($this->createRedisMock()); + $context = new RedisContext($this->createRedisMock(), 300); $queue = $context->createQueue('aQueue'); @@ -80,7 +70,7 @@ public function testShouldCreateQueue() public function testShouldAllowCreateTopic() { - $context = new RedisContext($this->createRedisMock()); + $context = new RedisContext($this->createRedisMock(), 300); $topic = $context->createTopic('aTopic'); @@ -90,16 +80,16 @@ public function testShouldAllowCreateTopic() public function testThrowNotImplementedOnCreateTmpQueueCall() { - $context = new RedisContext($this->createRedisMock()); + $context = new RedisContext($this->createRedisMock(), 300); + + $this->expectException(TemporaryQueueNotSupportedException::class); - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Not implemented'); $context->createTemporaryQueue(); } public function testShouldCreateProducer() { - $context = new RedisContext($this->createRedisMock()); + $context = new RedisContext($this->createRedisMock(), 300); $producer = $context->createProducer(); @@ -108,7 +98,7 @@ public function testShouldCreateProducer() public function testShouldThrowIfNotRedisDestinationGivenOnCreateConsumer() { - $context = new RedisContext($this->createRedisMock()); + $context = new RedisContext($this->createRedisMock(), 300); $this->expectException(InvalidDestinationException::class); $this->expectExceptionMessage('The destination must be an instance of Enqueue\Redis\RedisDestination but got Enqueue\Null\NullQueue.'); @@ -119,7 +109,7 @@ public function testShouldThrowIfNotRedisDestinationGivenOnCreateConsumer() public function testShouldCreateConsumer() { - $context = new RedisContext($this->createRedisMock()); + $context = new RedisContext($this->createRedisMock(), 300); $queue = $context->createQueue('aQueue'); @@ -136,7 +126,7 @@ public function testShouldCallRedisDisconnectOnClose() ->method('disconnect') ; - $context = new RedisContext($redisMock); + $context = new RedisContext($redisMock, 300); $context->close(); } @@ -149,7 +139,7 @@ public function testThrowIfNotRedisDestinationGivenOnDeleteQueue() ->method('del') ; - $context = new RedisContext($redisMock); + $context = new RedisContext($redisMock, 300); $this->expectException(InvalidDestinationException::class); $context->deleteQueue(new NullQueue('aQueue')); @@ -159,12 +149,22 @@ public function testShouldAllowDeleteQueue() { $redisMock = $this->createRedisMock(); $redisMock - ->expects($this->once()) + ->expects($this->at(0)) ->method('del') ->with('aQueueName') ; + $redisMock + ->expects($this->at(1)) + ->method('del') + ->with('aQueueName:delayed') + ; + $redisMock + ->expects($this->at(2)) + ->method('del') + ->with('aQueueName:reserved') + ; - $context = new RedisContext($redisMock); + $context = new RedisContext($redisMock, 300); $queue = $context->createQueue('aQueueName'); @@ -179,7 +179,7 @@ public function testThrowIfNotRedisDestinationGivenOnDeleteTopic() ->method('del') ; - $context = new RedisContext($redisMock); + $context = new RedisContext($redisMock, 300); $this->expectException(InvalidDestinationException::class); $context->deleteTopic(new NullTopic('aTopic')); @@ -189,20 +189,37 @@ public function testShouldAllowDeleteTopic() { $redisMock = $this->createRedisMock(); $redisMock - ->expects($this->once()) + ->expects($this->at(0)) ->method('del') ->with('aTopicName') ; + $redisMock + ->expects($this->at(1)) + ->method('del') + ->with('aTopicName:delayed') + ; + $redisMock + ->expects($this->at(2)) + ->method('del') + ->with('aTopicName:reserved') + ; - $context = new RedisContext($redisMock); + $context = new RedisContext($redisMock, 300); $topic = $context->createTopic('aTopicName'); $context->deleteQueue($topic); } + public function testShouldReturnExpectedSubscriptionConsumerInstance() + { + $context = new RedisContext($this->createRedisMock(), 300); + + $this->assertInstanceOf(RedisSubscriptionConsumer::class, $context->createSubscriptionConsumer()); + } + /** - * @return \PHPUnit_Framework_MockObject_MockObject|Redis + * @return \PHPUnit\Framework\MockObject\MockObject|Redis */ private function createRedisMock() { diff --git a/pkg/redis/Tests/RedisDestinationTest.php b/pkg/redis/Tests/RedisDestinationTest.php index 26d93cd05..4e73849fc 100644 --- a/pkg/redis/Tests/RedisDestinationTest.php +++ b/pkg/redis/Tests/RedisDestinationTest.php @@ -4,8 +4,8 @@ use Enqueue\Redis\RedisDestination; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrQueue; -use Interop\Queue\PsrTopic; +use Interop\Queue\Queue; +use Interop\Queue\Topic; class RedisDestinationTest extends \PHPUnit\Framework\TestCase { @@ -13,8 +13,8 @@ class RedisDestinationTest extends \PHPUnit\Framework\TestCase public function testShouldImplementsTopicAndQueueInterfaces() { - $this->assertClassImplements(PsrTopic::class, RedisDestination::class); - $this->assertClassImplements(PsrQueue::class, RedisDestination::class); + $this->assertClassImplements(Topic::class, RedisDestination::class); + $this->assertClassImplements(Queue::class, RedisDestination::class); } public function testShouldReturnNameSetInConstructor() diff --git a/pkg/redis/Tests/RedisMessageTest.php b/pkg/redis/Tests/RedisMessageTest.php index 2a81efa5a..5b1e42fe2 100644 --- a/pkg/redis/Tests/RedisMessageTest.php +++ b/pkg/redis/Tests/RedisMessageTest.php @@ -9,11 +9,6 @@ class RedisMessageTest extends \PHPUnit\Framework\TestCase { use ClassExtensionTrait; - public function testShouldImplementJsonSerializableInterface() - { - $this->assertClassImplements(\JsonSerializable::class, RedisMessage::class); - } - public function testCouldConstructMessageWithoutArguments() { $message = new RedisMessage(); @@ -63,34 +58,4 @@ public function testShouldSetReplyToAsHeader() $this->assertSame(['reply_to' => 'theQueueName'], $message->getHeaders()); } - - public function testColdBeSerializedToJson() - { - $message = new RedisMessage('theBody', ['thePropFoo' => 'thePropFooVal'], ['theHeaderFoo' => 'theHeaderFooVal']); - - $this->assertEquals('{"body":"theBody","properties":{"thePropFoo":"thePropFooVal"},"headers":{"theHeaderFoo":"theHeaderFooVal"}}', json_encode($message)); - } - - public function testCouldBeUnserializedFromJson() - { - $message = new RedisMessage('theBody', ['thePropFoo' => 'thePropFooVal'], ['theHeaderFoo' => 'theHeaderFooVal']); - - $json = json_encode($message); - - //guard - $this->assertNotEmpty($json); - - $unserializedMessage = RedisMessage::jsonUnserialize($json); - - $this->assertInstanceOf(RedisMessage::class, $unserializedMessage); - $this->assertEquals($message, $unserializedMessage); - } - - public function testThrowIfMalformedJsonGivenOnUnsterilizedFromJson() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The malformed json given.'); - - RedisMessage::jsonUnserialize('{]'); - } } diff --git a/pkg/redis/Tests/RedisProducerTest.php b/pkg/redis/Tests/RedisProducerTest.php index 7c2ac63ee..40e03bae2 100644 --- a/pkg/redis/Tests/RedisProducerTest.php +++ b/pkg/redis/Tests/RedisProducerTest.php @@ -4,14 +4,16 @@ use Enqueue\Null\NullMessage; use Enqueue\Null\NullQueue; +use Enqueue\Redis\JsonSerializer; use Enqueue\Redis\Redis; +use Enqueue\Redis\RedisContext; use Enqueue\Redis\RedisDestination; use Enqueue\Redis\RedisMessage; use Enqueue\Redis\RedisProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrProducer; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Producer; use PHPUnit\Framework\TestCase; class RedisProducerTest extends TestCase @@ -20,17 +22,12 @@ class RedisProducerTest extends TestCase public function testShouldImplementProducerInterface() { - $this->assertClassImplements(PsrProducer::class, RedisProducer::class); - } - - public function testCouldBeConstructedWithRedisAsFirstArgument() - { - new RedisProducer($this->createRedisMock()); + $this->assertClassImplements(Producer::class, RedisProducer::class); } public function testThrowIfDestinationNotRedisDestinationOnSend() { - $producer = new RedisProducer($this->createRedisMock()); + $producer = new RedisProducer($this->createContextMock()); $this->expectException(InvalidDestinationException::class); $this->expectExceptionMessage('The destination must be an instance of Enqueue\Redis\RedisDestination but got Enqueue\Null\NullQueue.'); @@ -39,7 +36,7 @@ public function testThrowIfDestinationNotRedisDestinationOnSend() public function testThrowIfMessageNotRedisMessageOnSend() { - $producer = new RedisProducer($this->createRedisMock()); + $producer = new RedisProducer($this->createContextMock()); $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of Enqueue\Redis\RedisMessage but it is Enqueue\Null\NullMessage.'); @@ -54,16 +51,87 @@ public function testShouldCallLPushOnSend() $redisMock ->expects($this->once()) ->method('lpush') - ->with('aDestination', '{"body":"","properties":[],"headers":[]}') + ->willReturnCallback(function (string $key, string $value) { + $this->assertSame('aDestination', $key); + + $message = json_decode($value, true); + + $this->assertArrayHasKey('body', $message); + $this->assertArrayHasKey('properties', $message); + $this->assertArrayHasKey('headers', $message); + $this->assertNotEmpty($message['headers']['message_id']); + $this->assertSame(0, $message['headers']['attempts']); + + return 1; + }) ; - $producer = new RedisProducer($redisMock); + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getRedis') + ->willReturn($redisMock) + ; + $context + ->expects($this->once()) + ->method('getSerializer') + ->willReturn(new JsonSerializer()) + ; + + $producer = new RedisProducer($context); $producer->send($destination, new RedisMessage()); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Redis + * Tests if Redis::zadd is called with the expected 'score' (used as delivery timestamp). + * + * @depends testShouldCallLPushOnSend + */ + public function testShouldCallZaddOnSendWithDeliveryDelay() + { + $destination = new RedisDestination('aDestination'); + + $redisMock = $this->createRedisMock(); + $redisMock + ->expects($this->once()) + ->method('zadd') + ->with( + 'aDestination:delayed', + $this->isJson(), + $this->equalTo(time() + 5) + ) + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getRedis') + ->willReturn($redisMock) + ; + $context + ->expects($this->once()) + ->method('getSerializer') + ->willReturn(new JsonSerializer()) + ; + + $message = new RedisMessage(); + $message->setDeliveryDelay(5000); // 5 seconds in milliseconds + + $producer = new RedisProducer($context); + $producer->send($destination, $message); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|RedisContext + */ + private function createContextMock() + { + return $this->createMock(RedisContext::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|Redis */ private function createRedisMock() { diff --git a/pkg/redis/Tests/RedisSubscriptionConsumerTest.php b/pkg/redis/Tests/RedisSubscriptionConsumerTest.php new file mode 100644 index 000000000..8d00fcc14 --- /dev/null +++ b/pkg/redis/Tests/RedisSubscriptionConsumerTest.php @@ -0,0 +1,175 @@ +assertTrue($rc->implementsInterface(SubscriptionConsumer::class)); + } + + public function testShouldAddConsumerAndCallbackToSubscribersPropertyOnSubscribe() + { + $subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock()); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $barCallback = function () {}; + $barConsumer = $this->createConsumerStub('bar_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + $subscriptionConsumer->subscribe($barConsumer, $barCallback); + + $this->assertAttributeSame([ + 'foo_queue' => [$fooConsumer, $fooCallback], + 'bar_queue' => [$barConsumer, $barCallback], + ], 'subscribers', $subscriptionConsumer); + } + + public function testThrowsIfTrySubscribeAnotherConsumerToAlreadySubscribedQueue() + { + $subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock()); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $barCallback = function () {}; + $barConsumer = $this->createConsumerStub('foo_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('There is a consumer subscribed to queue: "foo_queue"'); + $subscriptionConsumer->subscribe($barConsumer, $barCallback); + } + + /** + * @doesNotPerformAssertions + */ + public function testShouldAllowSubscribeSameConsumerAndCallbackSecondTime() + { + $subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock()); + + $fooCallback = function () {}; + $fooConsumer = $this->createConsumerStub('foo_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + $subscriptionConsumer->subscribe($fooConsumer, $fooCallback); + } + + public function testShouldRemoveSubscribedConsumerOnUnsubscribeCall() + { + $subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock()); + + $fooConsumer = $this->createConsumerStub('foo_queue'); + $barConsumer = $this->createConsumerStub('bar_queue'); + + $subscriptionConsumer->subscribe($fooConsumer, function () {}); + $subscriptionConsumer->subscribe($barConsumer, function () {}); + + // guard + $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($fooConsumer); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldDoNothingIfTryUnsubscribeNotSubscribedQueueName() + { + $subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock()); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + + // guard + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($this->createConsumerStub('bar_queue')); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldDoNothingIfTryUnsubscribeNotSubscribedConsumer() + { + $subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock()); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + + // guard + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribe($this->createConsumerStub('foo_queue')); + + $this->assertAttributeCount(1, 'subscribers', $subscriptionConsumer); + } + + public function testShouldRemoveAllSubscriberOnUnsubscribeAllCall() + { + $subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock()); + + $subscriptionConsumer->subscribe($this->createConsumerStub('foo_queue'), function () {}); + $subscriptionConsumer->subscribe($this->createConsumerStub('bar_queue'), function () {}); + + // guard + $this->assertAttributeCount(2, 'subscribers', $subscriptionConsumer); + + $subscriptionConsumer->unsubscribeAll(); + + $this->assertAttributeCount(0, 'subscribers', $subscriptionConsumer); + } + + public function testThrowsIfTryConsumeWithoutSubscribers() + { + $subscriptionConsumer = new RedisSubscriptionConsumer($this->createRedisContextMock()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('No subscribers'); + $subscriptionConsumer->consume(); + } + + /** + * @return RedisContext|\PHPUnit\Framework\MockObject\MockObject + */ + private function createRedisContextMock() + { + return $this->createMock(RedisContext::class); + } + + /** + * @param mixed|null $queueName + * + * @return Consumer|\PHPUnit\Framework\MockObject\MockObject + */ + private function createConsumerStub($queueName = null) + { + $queueMock = $this->createMock(Queue::class); + $queueMock + ->expects($this->any()) + ->method('getQueueName') + ->willReturn($queueName); + + $consumerMock = $this->createMock(RedisConsumer::class); + $consumerMock + ->expects($this->any()) + ->method('getQueue') + ->willReturn($queueMock) + ; + + return $consumerMock; + } +} diff --git a/pkg/redis/Tests/Spec/JsonSerializerTest.php b/pkg/redis/Tests/Spec/JsonSerializerTest.php new file mode 100644 index 000000000..a15859d35 --- /dev/null +++ b/pkg/redis/Tests/Spec/JsonSerializerTest.php @@ -0,0 +1,71 @@ +assertClassImplements(Serializer::class, JsonSerializer::class); + } + + public function testShouldConvertMessageToJsonString() + { + $serializer = new JsonSerializer(); + + $message = new RedisMessage('theBody', ['aProp' => 'aPropVal'], ['aHeader' => 'aHeaderVal']); + + $json = $serializer->toString($message); + + $this->assertSame('{"body":"theBody","properties":{"aProp":"aPropVal"},"headers":{"aHeader":"aHeaderVal"}}', $json); + } + + public function testThrowIfFailedToEncodeMessageToJson() + { + $serializer = new JsonSerializer(); + + $resource = fopen(__FILE__, 'r'); + + // guard + $this->assertIsResource($resource); + + $message = new RedisMessage('theBody', ['aProp' => $resource]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The malformed json given.'); + $serializer->toString($message); + } + + public function testShouldConvertJsonStringToMessage() + { + $serializer = new JsonSerializer(); + + $message = $serializer->toMessage('{"body":"theBody","properties":{"aProp":"aPropVal"},"headers":{"aHeader":"aHeaderVal"}}'); + + $this->assertInstanceOf(RedisMessage::class, $message); + + $this->assertSame('theBody', $message->getBody()); + $this->assertSame(['aProp' => 'aPropVal'], $message->getProperties()); + $this->assertSame(['aHeader' => 'aHeaderVal'], $message->getHeaders()); + } + + public function testThrowIfFailedToDecodeJsonToMessage() + { + $serializer = new JsonSerializer(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The malformed json given.'); + $serializer->toMessage('{]'); + } +} diff --git a/pkg/redis/Tests/Spec/RedisConnectionFactoryTest.php b/pkg/redis/Tests/Spec/RedisConnectionFactoryTest.php new file mode 100644 index 000000000..f15282f11 --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisConnectionFactoryTest.php @@ -0,0 +1,17 @@ +buildPhpRedisContext(); + } +} diff --git a/pkg/redis/Tests/Spec/RedisMessageTest.php b/pkg/redis/Tests/Spec/RedisMessageTest.php index 073f81322..b0cc828e9 100644 --- a/pkg/redis/Tests/Spec/RedisMessageTest.php +++ b/pkg/redis/Tests/Spec/RedisMessageTest.php @@ -3,13 +3,13 @@ namespace Enqueue\Redis\Tests\Spec; use Enqueue\Redis\RedisMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class RedisMessageTest extends PsrMessageSpec +/** + * @group Redis + */ +class RedisMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new RedisMessage(); diff --git a/pkg/redis/Tests/Spec/RedisProducerTest.php b/pkg/redis/Tests/Spec/RedisProducerTest.php new file mode 100644 index 000000000..3434820e9 --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisProducerTest.php @@ -0,0 +1,20 @@ +buildPhpRedisContext()->createProducer(); + } +} diff --git a/pkg/redis/Tests/Spec/RedisQueueTest.php b/pkg/redis/Tests/Spec/RedisQueueTest.php new file mode 100644 index 000000000..a8cd3b442 --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisQueueTest.php @@ -0,0 +1,17 @@ +buildPhpRedisContext(); + } +} diff --git a/pkg/redis/Tests/Spec/RedisSendToAndReceiveFromQueueTest.php b/pkg/redis/Tests/Spec/RedisSendToAndReceiveFromQueueTest.php new file mode 100644 index 000000000..7bb61ab5d --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisSendToAndReceiveFromQueueTest.php @@ -0,0 +1,20 @@ +buildPhpRedisContext(); + } +} diff --git a/pkg/redis/Tests/Spec/RedisSendToAndReceiveFromTopicTest.php b/pkg/redis/Tests/Spec/RedisSendToAndReceiveFromTopicTest.php new file mode 100644 index 000000000..37545f032 --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisSendToAndReceiveFromTopicTest.php @@ -0,0 +1,20 @@ +buildPhpRedisContext(); + } +} diff --git a/pkg/redis/Tests/Spec/RedisSendToAndReceiveNoWaitFromQueueTest.php b/pkg/redis/Tests/Spec/RedisSendToAndReceiveNoWaitFromQueueTest.php new file mode 100644 index 000000000..01aea7ff1 --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisSendToAndReceiveNoWaitFromQueueTest.php @@ -0,0 +1,20 @@ +buildPhpRedisContext(); + } +} diff --git a/pkg/redis/Tests/Spec/RedisSendToAndReceiveNoWaitFromTopicTest.php b/pkg/redis/Tests/Spec/RedisSendToAndReceiveNoWaitFromTopicTest.php new file mode 100644 index 000000000..2c8fbac7a --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisSendToAndReceiveNoWaitFromTopicTest.php @@ -0,0 +1,20 @@ +buildPhpRedisContext(); + } +} diff --git a/pkg/redis/Tests/Spec/RedisSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php b/pkg/redis/Tests/Spec/RedisSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php new file mode 100644 index 000000000..78d33045f --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisSubscriptionConsumerConsumeFromAllSubscribedQueuesTest.php @@ -0,0 +1,38 @@ +buildPhpRedisContext(); + } + + /** + * @param RedisContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var RedisDestination $queue */ + $queue = parent::createQueue($context, $queueName); + $context->deleteQueue($queue); + + return $queue; + } +} diff --git a/pkg/redis/Tests/Spec/RedisSubscriptionConsumerConsumeUntilUnsubscribedTest.php b/pkg/redis/Tests/Spec/RedisSubscriptionConsumerConsumeUntilUnsubscribedTest.php new file mode 100644 index 000000000..84872093b --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisSubscriptionConsumerConsumeUntilUnsubscribedTest.php @@ -0,0 +1,38 @@ +buildPhpRedisContext(); + } + + /** + * @param RedisContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var RedisDestination $queue */ + $queue = parent::createQueue($context, $queueName); + $context->purgeQueue($queue); + + return $queue; + } +} diff --git a/pkg/redis/Tests/Spec/RedisSubscriptionConsumerStopOnFalseTest.php b/pkg/redis/Tests/Spec/RedisSubscriptionConsumerStopOnFalseTest.php new file mode 100644 index 000000000..490d58eab --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisSubscriptionConsumerStopOnFalseTest.php @@ -0,0 +1,38 @@ +buildPhpRedisContext(); + } + + /** + * @param RedisContext $context + */ + protected function createQueue(Context $context, $queueName) + { + /** @var RedisDestination $queue */ + $queue = parent::createQueue($context, $queueName); + $context->getRedis()->del($queueName); + + return $queue; + } +} diff --git a/pkg/redis/Tests/Spec/RedisTopicTest.php b/pkg/redis/Tests/Spec/RedisTopicTest.php new file mode 100644 index 000000000..da94ffa1b --- /dev/null +++ b/pkg/redis/Tests/Spec/RedisTopicTest.php @@ -0,0 +1,17 @@ +assertClassImplements(TransportFactoryInterface::class, RedisTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new RedisTransportFactory(); - - $this->assertEquals('redis', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new RedisTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new RedisTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'host' => 'localhost', - 'port' => 123, - 'vendor' => 'phpredis', - 'persisted' => true, - 'lazy' => false, - ]]); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 123, - 'vendor' => 'phpredis', - 'persisted' => true, - 'lazy' => false, - 'database' => 0, - ], $config); - } - - public function testShouldAllowAddConfigurationFromDSN() - { - $transport = new RedisTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'dsn' => 'redis://localhost:8080?vendor=predis&persisted=false&lazy=true&database=5', - ]]); - - $this->assertEquals([ - 'persisted' => false, - 'lazy' => true, - 'database' => 0, - 'dsn' => 'redis://localhost:8080?vendor=predis&persisted=false&lazy=true&database=5', - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new RedisTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 123, - 'vendor' => 'phpredis', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(RedisConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'host' => 'localhost', - 'port' => 123, - 'vendor' => 'phpredis', - ]], $factory->getArguments()); - } - - public function testShouldCreateConnectionFactoryWithCustomRedisInstance() - { - $container = new ContainerBuilder(); - - $transport = new RedisTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'host' => 'localhost', - 'port' => 123, - 'vendor' => 'custom', - 'redis' => 'a.redis.service', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(RedisConnectionFactory::class, $factory->getClass()); - - $config = $factory->getArgument(0); - - $this->assertInternalType('array', $config); - - $this->assertArrayHasKey('vendor', $config); - $this->assertSame('custom', $config['vendor']); - - $this->assertArrayHasKey('redis', $config); - $this->assertInstanceOf(Reference::class, $config['redis']); - $this->assertSame('a.redis.service', (string) $config['redis']); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new RedisTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'host' => 'localhost', - 'port' => 123, - 'vendor' => 'predis', - ]); - - $this->assertEquals('enqueue.transport.redis.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.redis.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.redis.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new RedisTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.redis.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(RedisDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.redis.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/redis/composer.json b/pkg/redis/composer.json index 61699c3be..c48323201 100644 --- a/pkg/redis/composer.json +++ b/pkg/redis/composer.json @@ -6,18 +6,17 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1" + "php": "^8.1", + "queue-interop/queue-interop": "^0.8", + "enqueue/dsn": "^0.10", + "ramsey/uuid": "^3.5|^4" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", + "phpunit/phpunit": "^9.5", "predis/predis": "^1.1", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -33,14 +32,12 @@ ] }, "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features", - "predis/predis": "Either this PHP library or redis extension has to be installed.", - "ext-redis": "Either this PHP extension or predis/predis extension has to be installed." + "ext-redis": "If you'd like to use phpredis extension." }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/redis/phpunit.xml.dist b/pkg/redis/phpunit.xml.dist index 9c4467b56..22691000e 100644 --- a/pkg/redis/phpunit.xml.dist +++ b/pkg/redis/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/simple-client/.github/workflows/ci.yml b/pkg/simple-client/.github/workflows/ci.yml new file mode 100644 index 000000000..6b24b0f30 --- /dev/null +++ b/pkg/simple-client/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - run: php Tests/fix_composer_json.php + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/simple-client/.travis.yml b/pkg/simple-client/.travis.yml deleted file mode 100644 index 566e0af94..000000000 --- a/pkg/simple-client/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - php Tests/fix_composer_json.php - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/simple-client/README.md b/pkg/simple-client/README.md index 2368b31d7..8ecb67059 100644 --- a/pkg/simple-client/README.md +++ b/pkg/simple-client/README.md @@ -1,23 +1,32 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Message Queue. Simple client [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/simple-client.png?branch=master)](https://travis-ci.org/php-enqueue/simple-client) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/simple-client/ci.yml?branch=master)](https://github.com/php-enqueue/simple-client/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/simple-client/d/total.png)](https://packagist.org/packages/enqueue/simple-client) [![Latest Stable Version](https://poser.pugx.org/enqueue/simple-client/version.png)](https://packagist.org/packages/enqueue/simple-client) - -The simple client takes Enqueue client classes and Symfony components and combines it to easy to use facade called `SimpleCLient`. + +The simple client takes Enqueue client classes and Symfony components and combines it to easy to use facade called `SimpleClient`. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com diff --git a/pkg/simple-client/SimpleClient.php b/pkg/simple-client/SimpleClient.php index 8114de2e2..bbfd8b91b 100644 --- a/pkg/simple-client/SimpleClient.php +++ b/pkg/simple-client/SimpleClient.php @@ -2,423 +2,348 @@ namespace Enqueue\SimpleClient; -use Enqueue\AmqpBunny\AmqpConnectionFactory as AmqpBunnyConnectionFactory; -use Enqueue\AmqpExt\AmqpConnectionFactory as AmqpExtConnectionFactory; -use Enqueue\AmqpLib\AmqpConnectionFactory as AmqpLibConnectionFactory; -use Enqueue\Client\ArrayProcessorRegistry; +use Enqueue\ArrayProcessorRegistry; +use Enqueue\Client\ChainExtension as ClientChainExtensions; use Enqueue\Client\Config; +use Enqueue\Client\ConsumptionExtension\DelayRedeliveredMessageExtension; +use Enqueue\Client\ConsumptionExtension\LogExtension; +use Enqueue\Client\ConsumptionExtension\SetRouterPropertiesExtension; use Enqueue\Client\DelegateProcessor; +use Enqueue\Client\DriverFactory; use Enqueue\Client\DriverInterface; -use Enqueue\Client\Meta\QueueMetaRegistry; -use Enqueue\Client\Meta\TopicMetaRegistry; +use Enqueue\Client\Message; +use Enqueue\Client\Producer; use Enqueue\Client\ProducerInterface; +use Enqueue\Client\Route; +use Enqueue\Client\RouteCollection; use Enqueue\Client\RouterProcessor; +use Enqueue\ConnectionFactoryFactory; use Enqueue\Consumption\CallbackProcessor; +use Enqueue\Consumption\ChainExtension as ConsumptionChainExtension; +use Enqueue\Consumption\Extension\ReplyExtension; +use Enqueue\Consumption\Extension\SignalExtension; use Enqueue\Consumption\ExtensionInterface; use Enqueue\Consumption\QueueConsumer; -use Enqueue\Dbal\DbalConnectionFactory; -use Enqueue\Dbal\Symfony\DbalTransportFactory; -use Enqueue\Fs\FsConnectionFactory; -use Enqueue\Fs\Symfony\FsTransportFactory; -use Enqueue\Gps\GpsConnectionFactory; -use Enqueue\Gps\Symfony\GpsTransportFactory; -use Enqueue\Mongodb\MongodbConnectionFactory; -use Enqueue\Mongodb\Symfony\MongodbTransportFactory; -use Enqueue\Null\Symfony\NullTransportFactory; -use Enqueue\RdKafka\RdKafkaConnectionFactory; -use Enqueue\RdKafka\Symfony\RdKafkaTransportFactory; -use Enqueue\Redis\RedisConnectionFactory; -use Enqueue\Redis\Symfony\RedisTransportFactory; +use Enqueue\Consumption\QueueConsumerInterface; use Enqueue\Rpc\Promise; -use Enqueue\Sqs\SqsConnectionFactory; -use Enqueue\Sqs\Symfony\SqsTransportFactory; -use Enqueue\Stomp\StompConnectionFactory; -use Enqueue\Stomp\Symfony\RabbitMqStompTransportFactory; -use Enqueue\Stomp\Symfony\StompTransportFactory; -use Enqueue\Symfony\AmqpTransportFactory; -use Enqueue\Symfony\DefaultTransportFactory; -use Enqueue\Symfony\MissingTransportFactory; -use Enqueue\Symfony\RabbitMqAmqpTransportFactory; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrProcessor; -use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Enqueue\Rpc\RpcFactory; +use Enqueue\Symfony\Client\DependencyInjection\ClientFactory; +use Enqueue\Symfony\DependencyInjection\TransportFactory; +use Interop\Queue\Processor; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\NodeInterface; +use Symfony\Component\Config\Definition\Processor as ConfigProcessor; final class SimpleClient { /** - * @var ContainerInterface + * @var DriverInterface */ - private $container; + private $driver; /** - * @var array|string + * @var Producer */ - private $config; + private $producer; + + /** + * @var QueueConsumer + */ + private $queueConsumer; + + /** + * @var ArrayProcessorRegistry + */ + private $processorRegistry; + + /** + * @var DelegateProcessor + */ + private $delegateProcessor; + + /** + * @var LoggerInterface + */ + private $logger; /** * The config could be a transport DSN (string) or an array, here's an example of a few DSNs:. * - * amqp: - * amqp://guest:guest@localhost:5672/%2f?lazy=1&persisted=1 - * file://foo/bar/ - * null: + * $config = amqp: + * $config = amqp://guest:guest@localhost:5672/%2f?lazy=1&persisted=1 + * $config = file://foo/bar/ + * $config = null: * - * or an array, the most simple: + * or an array: * - *$config = [ + * $config = [ * 'transport' => [ - * 'default' => 'amqp', - * 'amqp' => [], // amqp options here - * ], + * 'dsn' => 'amqps://guest:guest@localhost:5672/%2f', + * 'ssl_cacert' => '/a/dir/cacert.pem', + * 'ssl_cert' => '/a/dir/cert.pem', + * 'ssl_key' => '/a/dir/key.pem', * ] * - * or a with all details: + * with custom connection factory class * * $config = [ * 'transport' => [ - * 'default' => 'amqp', - * 'amqp' => [], - * .... - * ], + * 'dsn' => 'amqps://guest:guest@localhost:5672/%2f', + * 'connection_factory_class' => 'aCustomConnectionFactory', + * // other options available options are factory_class and factory_service + * ] + * + * The client config + * + * $config = [ + * 'transport' => 'null:', * 'client' => [ * 'prefix' => 'enqueue', + * 'separator' => '.', * 'app_name' => 'app', * 'router_topic' => 'router', * 'router_queue' => 'default', - * 'default_processor_queue' => 'default', + * 'default_queue' => 'default', * 'redelivered_delay_time' => 0 * ], * 'extensions' => [ * 'signal_extension' => true, + * 'reply_extension' => true, * ] * ] * - * - * @param string|array $config - * @param ContainerBuilder|null $container + * @param string|array $config */ - public function __construct($config, ContainerBuilder $container = null) + public function __construct($config, ?LoggerInterface $logger = null) { - $this->container = $this->buildContainer($config, $container ?: new ContainerBuilder()); - $this->config = $config; + if (is_string($config)) { + $config = [ + 'transport' => $config, + 'client' => true, + ]; + } + + $this->logger = $logger ?: new NullLogger(); + + $this->build(['enqueue' => $config]); } /** - * @param string $topic - * @param string $processorName - * @param callable|PsrProcessor $processor + * @param callable|Processor $processor */ - public function bind($topic, $processorName, $processor) + public function bindTopic(string $topic, $processor, ?string $processorName = null): void { if (is_callable($processor)) { $processor = new CallbackProcessor($processor); } - if (false == $processor instanceof PsrProcessor) { - throw new \LogicException('The processor must be either callable or instance of PsrProcessor'); + if (false == $processor instanceof Processor) { + throw new \LogicException('The processor must be either callable or instance of Processor'); } - $queueName = $this->getConfig()->getDefaultProcessorQueueName(); + $processorName = $processorName ?: uniqid($processor::class); - $this->getTopicMetaRegistry()->addProcessor($topic, $processorName); - $this->getQueueMetaRegistry()->addProcessor($queueName, $processorName); - $this->getProcessorRegistry()->add($processorName, $processor); - $this->getRouterProcessor()->add($topic, $queueName, $processorName); + $this->driver->getRouteCollection()->add(new Route($topic, Route::TOPIC, $processorName)); + $this->processorRegistry->add($processorName, $processor); } /** - * @param string $command - * @param mixed $message - * @param bool $needReply - * - * @return Promise|null + * @param callable|Processor $processor */ - public function sendCommand($command, $message, $needReply = false) + public function bindCommand(string $command, $processor, ?string $processorName = null): void { - return $this->getProducer()->sendCommand($command, $message, $needReply); + if (is_callable($processor)) { + $processor = new CallbackProcessor($processor); + } + + if (false == $processor instanceof Processor) { + throw new \LogicException('The processor must be either callable or instance of Processor'); + } + + $processorName = $processorName ?: uniqid($processor::class); + + $this->driver->getRouteCollection()->add(new Route($command, Route::COMMAND, $processorName)); + $this->processorRegistry->add($processorName, $processor); } /** - * @param string $topic - * @param string|array $message + * @param string|array|\JsonSerializable|Message $message */ - public function sendEvent($topic, $message) + public function sendCommand(string $command, $message, bool $needReply = false): ?Promise { - $this->getProducer()->sendEvent($topic, $message); + return $this->producer->sendCommand($command, $message, $needReply); } /** - * @deprecated since 0.8.18 and will be removed in 0.9. Use sendEvent method instead - * - * @param string $topic - * @param string|array $message - * @param bool $setupBroker + * @param string|array|Message $message */ - public function send($topic, $message, $setupBroker = false) + public function sendEvent(string $topic, $message): void { - if ($setupBroker) { - $this->setupBroker(); - } - - $this->sendEvent($topic, $message); + $this->producer->sendEvent($topic, $message); } - /** - * @param ExtensionInterface|null $runtimeExtension - */ - public function consume(ExtensionInterface $runtimeExtension = null) + public function consume(?ExtensionInterface $runtimeExtension = null): void { $this->setupBroker(); - $processor = $this->getDelegateProcessor(); - $queueConsumer = $this->getQueueConsumer(); - $defaultQueueName = $this->getConfig()->getDefaultProcessorQueueName(); - $defaultTransportQueueName = $this->getConfig()->createTransportQueueName($defaultQueueName); + $boundQueues = []; - $queueConsumer->bind($defaultTransportQueueName, $processor); - if ($this->getConfig()->getRouterQueueName() != $defaultQueueName) { - $routerTransportQueueName = $this->getConfig()->createTransportQueueName($this->getConfig()->getRouterQueueName()); + $routerQueue = $this->getDriver()->createQueue($this->getDriver()->getConfig()->getRouterQueue()); + $this->queueConsumer->bind($routerQueue, $this->delegateProcessor); + $boundQueues[$routerQueue->getQueueName()] = true; - $queueConsumer->bind($routerTransportQueueName, $processor); - } + foreach ($this->driver->getRouteCollection()->all() as $route) { + $queue = $this->getDriver()->createRouteQueue($route); + if (array_key_exists($queue->getQueueName(), $boundQueues)) { + continue; + } - $queueConsumer->consume($runtimeExtension); - } + $this->queueConsumer->bind($queue, $this->delegateProcessor); - /** - * @return PsrContext - */ - public function getContext() - { - return $this->container->get('enqueue.transport.context'); - } + $boundQueues[$queue->getQueueName()] = true; + } - /** - * @return QueueConsumer - */ - public function getQueueConsumer() - { - return $this->container->get('enqueue.client.queue_consumer'); + $this->queueConsumer->consume($runtimeExtension); } - /** - * @return Config - */ - public function getConfig() + public function getQueueConsumer(): QueueConsumerInterface { - return $this->container->get('enqueue.client.config'); + return $this->queueConsumer; } - /** - * @return DriverInterface - */ - public function getDriver() + public function getDriver(): DriverInterface { - return $this->container->get('enqueue.client.driver'); + return $this->driver; } - /** - * @return TopicMetaRegistry - */ - public function getTopicMetaRegistry() + public function getProducer(bool $setupBroker = false): ProducerInterface { - return $this->container->get('enqueue.client.meta.topic_meta_registry'); - } + $setupBroker && $this->setupBroker(); - /** - * @return QueueMetaRegistry - */ - public function getQueueMetaRegistry() - { - return $this->container->get('enqueue.client.meta.queue_meta_registry'); + return $this->producer; } - /** - * @param bool $setupBroker - * - * @return ProducerInterface - */ - public function getProducer($setupBroker = false) + public function getDelegateProcessor(): DelegateProcessor { - $setupBroker && $this->setupBroker(); - - return $this->container->get('enqueue.client.producer'); + return $this->delegateProcessor; } - public function setupBroker() + public function setupBroker(): void { $this->getDriver()->setupBroker(); } - /** - * @return ArrayProcessorRegistry - */ - public function getProcessorRegistry() + public function build(array $configs): void { - return $this->container->get('enqueue.client.processor_registry'); - } + $configProcessor = new ConfigProcessor(); + $simpleClientConfig = $configProcessor->process($this->createConfiguration(), $configs); - /** - * @return DelegateProcessor - */ - public function getDelegateProcessor() - { - return $this->container->get('enqueue.client.delegate_processor'); - } + if (isset($simpleClientConfig['transport']['factory_service'])) { + throw new \LogicException('transport.factory_service option is not supported by simple client'); + } + if (isset($simpleClientConfig['transport']['factory_class'])) { + throw new \LogicException('transport.factory_class option is not supported by simple client'); + } + if (isset($simpleClientConfig['transport']['connection_factory_class'])) { + throw new \LogicException('transport.connection_factory_class option is not supported by simple client'); + } - /** - * @return RouterProcessor - */ - public function getRouterProcessor() - { - return $this->container->get('enqueue.client.router_processor'); - } + $connectionFactoryFactory = new ConnectionFactoryFactory(); + $connection = $connectionFactoryFactory->create($simpleClientConfig['transport']); - /** - * @param array|string $config - * @param ContainerBuilder $container - * - * @return ContainerInterface - */ - private function buildContainer($config, ContainerBuilder $container) - { - $config = $this->buildConfig($config); - $extension = $this->buildContainerExtension(); + $clientExtensions = new ClientChainExtensions([]); - $container->registerExtension($extension); - $container->loadFromExtension($extension->getAlias(), $config); + $config = new Config( + $simpleClientConfig['client']['prefix'], + $simpleClientConfig['client']['separator'], + $simpleClientConfig['client']['app_name'], + $simpleClientConfig['client']['router_topic'], + $simpleClientConfig['client']['router_queue'], + $simpleClientConfig['client']['default_queue'], + 'enqueue.client.router_processor', + $simpleClientConfig['transport'], + [] + ); - $container->compile(); + $routeCollection = new RouteCollection([]); + $driverFactory = new DriverFactory(); - return $container; - } + $driver = $driverFactory->create( + $connection, + $config, + $routeCollection + ); - /** - * @return SimpleClientContainerExtension - */ - private function buildContainerExtension() - { - $extension = new SimpleClientContainerExtension(); + $rpcFactory = new RpcFactory($driver->getContext()); - $extension->addTransportFactory(new DefaultTransportFactory('default')); - $extension->addTransportFactory(new NullTransportFactory('null')); + $producer = new Producer($driver, $rpcFactory, $clientExtensions); - if (class_exists(StompConnectionFactory::class)) { - $extension->addTransportFactory(new StompTransportFactory('stomp')); - $extension->addTransportFactory(new RabbitMqStompTransportFactory('rabbitmq_stomp')); - } else { - $extension->addTransportFactory(new MissingTransportFactory('stomp', ['enqueue/stomp'])); - $extension->addTransportFactory(new MissingTransportFactory('rabbitmq_stomp', ['enqueue/stomp'])); - } + $processorRegistry = new ArrayProcessorRegistry([]); - if ( - class_exists(AmqpBunnyConnectionFactory::class) || - class_exists(AmqpExtConnectionFactory::class) || - class_exists(AmqpLibConnectionFactory::class) - ) { - $extension->addTransportFactory(new AmqpTransportFactory('amqp')); - $extension->addTransportFactory(new RabbitMqAmqpTransportFactory('rabbitmq_amqp')); - } else { - $amppPackages = ['enqueue/amqp-ext', 'enqueue/amqp-bunny', 'enqueue/amqp-lib']; - $extension->addTransportFactory(new MissingTransportFactory('amqp', $amppPackages)); - $extension->addTransportFactory(new MissingTransportFactory('rabbitmq_amqp', $amppPackages)); - } + $delegateProcessor = new DelegateProcessor($processorRegistry); - if (class_exists(FsConnectionFactory::class)) { - $extension->addTransportFactory(new FsTransportFactory('fs')); - } else { - $extension->addTransportFactory(new MissingTransportFactory('fs', ['enqueue/fs'])); + // consumption extensions + $consumptionExtensions = []; + if ($simpleClientConfig['client']['redelivered_delay_time']) { + $consumptionExtensions[] = new DelayRedeliveredMessageExtension($driver, $simpleClientConfig['client']['redelivered_delay_time']); } - if (class_exists(RedisConnectionFactory::class)) { - $extension->addTransportFactory(new RedisTransportFactory('redis')); - } else { - $extension->addTransportFactory(new MissingTransportFactory('redis', ['enqueue/redis'])); + if ($simpleClientConfig['extensions']['signal_extension']) { + $consumptionExtensions[] = new SignalExtension(); } - if (class_exists(DbalConnectionFactory::class)) { - $extension->addTransportFactory(new DbalTransportFactory('dbal')); - } else { - $extension->addTransportFactory(new MissingTransportFactory('dbal', ['enqueue/dbal'])); + if ($simpleClientConfig['extensions']['reply_extension']) { + $consumptionExtensions[] = new ReplyExtension(); } - if (class_exists(SqsConnectionFactory::class)) { - $extension->addTransportFactory(new SqsTransportFactory('sqs')); - } else { - $extension->addTransportFactory(new MissingTransportFactory('sqs', ['enqueue/sqs'])); - } + $consumptionExtensions[] = new SetRouterPropertiesExtension($driver); + $consumptionExtensions[] = new LogExtension(); - if (class_exists(GpsConnectionFactory::class)) { - $extension->addTransportFactory(new GpsTransportFactory('gps')); - } else { - $extension->addTransportFactory(new MissingTransportFactory('gps', ['enqueue/gps'])); - } + $consumptionChainExtension = new ConsumptionChainExtension($consumptionExtensions); + $queueConsumer = new QueueConsumer($driver->getContext(), $consumptionChainExtension, [], $this->logger); - if (class_exists(RdKafkaConnectionFactory::class)) { - $extension->addTransportFactory(new RdKafkaTransportFactory('rdkafka')); - } else { - $extension->addTransportFactory(new MissingTransportFactory('rdkafka', ['enqueue/rdkafka'])); - } + $routerProcessor = new RouterProcessor($driver); - if (class_exists(MongodbConnectionFactory::class)) { - $extension->addTransportFactory(new MongodbTransportFactory('mongodb')); - } else { - $extension->addTransportFactory(new MissingTransportFactory('mongodb', ['enqueue/mongodb'])); - } + $processorRegistry->add($config->getRouterProcessor(), $routerProcessor); - return $extension; + $this->driver = $driver; + $this->producer = $producer; + $this->queueConsumer = $queueConsumer; + $this->delegateProcessor = $delegateProcessor; + $this->processorRegistry = $processorRegistry; } - /** - * @param array|string $config - * - * @return array - */ - private function buildConfig($config) + private function createConfiguration(): NodeInterface { - if (is_string($config) && false !== strpos($config, ':')) { - $extConfig = [ - 'client' => [], - 'transport' => [ - 'default' => $config, - ], - ]; - } elseif (is_string($config)) { - $extConfig = [ - 'client' => [], - 'transport' => [ - 'default' => $config, - $config => [], - ], - ]; - } elseif (is_array($config)) { - $extConfig = array_replace_recursive([ - 'client' => [], - 'transport' => [], - ], $config); + if (method_exists(TreeBuilder::class, 'getRootNode')) { + $tb = new TreeBuilder('enqueue'); + $rootNode = $tb->getRootNode(); } else { - throw new \LogicException('Expects config is string or array'); - } - - if (empty($extConfig['transport']['default'])) { - $defaultTransport = null; - foreach ($extConfig['transport'] as $transport => $config) { - if ('default' === $transport) { - continue; - } - - $defaultTransport = $transport; - break; - } - - if (false == $defaultTransport) { - throw new \LogicException('There is no transport configured'); - } - - $extConfig['transport']['default'] = $defaultTransport; + $tb = new TreeBuilder(); + $rootNode = $tb->root('enqueue'); } - return $extConfig; + $rootNode + ->beforeNormalization() + ->ifEmpty()->then(function () { + return ['transport' => ['dsn' => 'null:']]; + }); + + $rootNode + ->append(TransportFactory::getConfiguration()) + ->append(TransportFactory::getQueueConsumerConfiguration()) + ->append(ClientFactory::getConfiguration(false)) + ; + + $rootNode->children() + ->arrayNode('extensions')->addDefaultsIfNotSet()->children() + ->booleanNode('signal_extension')->defaultValue(function_exists('pcntl_signal_dispatch'))->end() + ->booleanNode('reply_extension')->defaultTrue()->end() + ->end() + ; + + return $tb->buildTree(); } } diff --git a/pkg/simple-client/SimpleClientContainerExtension.php b/pkg/simple-client/SimpleClientContainerExtension.php deleted file mode 100644 index 7bcb849cf..000000000 --- a/pkg/simple-client/SimpleClientContainerExtension.php +++ /dev/null @@ -1,198 +0,0 @@ -factories = []; - } - - /** - * {@inheritdoc} - */ - public function getAlias() - { - return 'enqueue'; - } - - /** - * @param TransportFactoryInterface $transportFactory - */ - public function addTransportFactory(TransportFactoryInterface $transportFactory) - { - $name = $transportFactory->getName(); - - if (empty($name)) { - throw new \LogicException('Transport factory name cannot be empty'); - } - if (array_key_exists($name, $this->factories)) { - throw new \LogicException(sprintf('Transport factory with such name already added. Name %s', $name)); - } - - $this->factories[$name] = $transportFactory; - } - - /** - * {@inheritdoc} - */ - public function load(array $configs, ContainerBuilder $container) - { - $configProcessor = new Processor(); - $config = $configProcessor->process($this->createConfiguration(), $configs); - - foreach ($config['transport'] as $name => $transportConfig) { - $this->factories[$name]->createConnectionFactory($container, $transportConfig); - $this->factories[$name]->createContext($container, $transportConfig); - $this->factories[$name]->createDriver($container, $transportConfig); - } - - $transportConfig = isset($config['transport']['default']['alias']) ? - $config['transport'][$config['transport']['default']['alias']] : - [] - ; - - $container->register('enqueue.client.config', Config::class) - ->setPublic(true) - ->setArguments([ - $config['client']['prefix'], - $config['client']['app_name'], - $config['client']['router_topic'], - $config['client']['router_queue'], - $config['client']['default_processor_queue'], - 'enqueue.client.router_processor', - $transportConfig, - ]); - - $container->register('enqueue.client.rpc_factory', RpcFactory::class) - ->setPublic(true) - ->setArguments([ - new Reference('enqueue.transport.context'), - ]); - - $container->register('enqueue.client.producer', Producer::class) - ->setPublic(true) - ->setArguments([ - new Reference('enqueue.client.driver'), - new Reference('enqueue.client.rpc_factory'), - ]); - - $container->setAlias('enqueue.client.producer_v2', new Alias('enqueue.client.producer', true)); - - $container->register('enqueue.client.meta.topic_meta_registry', TopicMetaRegistry::class) - ->setPublic(true) - ->setArguments([[]]); - - $container->register('enqueue.client.meta.queue_meta_registry', QueueMetaRegistry::class) - ->setPublic(true) - ->setArguments([new Reference('enqueue.client.config'), []]); - - $container->register('enqueue.client.processor_registry', ArrayProcessorRegistry::class) - ->setPublic(true) - ; - - $container->register('enqueue.client.delegate_processor', DelegateProcessor::class) - ->setPublic(true) - ->setArguments([new Reference('enqueue.client.processor_registry')]); - - $container->register('enqueue.client.queue_consumer', QueueConsumer::class) - ->setPublic(true) - ->setArguments([ - new Reference('enqueue.transport.context'), - new Reference('enqueue.consumption.extensions'), - ]); - - // router - $container->register('enqueue.client.router_processor', RouterProcessor::class) - ->setPublic(true) - ->setArguments([new Reference('enqueue.client.driver'), []]); - $container->getDefinition('enqueue.client.processor_registry') - ->addMethodCall('add', ['enqueue.client.router_processor', new Reference('enqueue.client.router_processor')]); - $container->getDefinition('enqueue.client.meta.queue_meta_registry') - ->addMethodCall('addProcessor', [$config['client']['router_queue'], 'enqueue.client.router_processor']); - - // extensions - $extensions = []; - if ($config['client']['redelivered_delay_time']) { - $container->register('enqueue.client.delay_redelivered_message_extension', DelayRedeliveredMessageExtension::class) - ->setPublic(true) - ->setArguments([ - new Reference('enqueue.client.driver'), - $config['client']['redelivered_delay_time'], - ]); - - $extensions[] = new Reference('enqueue.client.delay_redelivered_message_extension'); - } - - $container->register('enqueue.client.extension.set_router_properties', SetRouterPropertiesExtension::class) - ->setPublic(true) - ->setArguments([new Reference('enqueue.client.driver')]); - - $extensions[] = new Reference('enqueue.client.extension.set_router_properties'); - - $container->register('enqueue.consumption.extensions', ConsumptionChainExtension::class) - ->setPublic(true) - ->setArguments([$extensions]); - } - - /** - * @return NodeInterface - */ - private function createConfiguration() - { - $tb = new TreeBuilder(); - $rootNode = $tb->root('enqueue'); - - $transportChildren = $rootNode->children() - ->arrayNode('transport')->isRequired()->children(); - - foreach ($this->factories as $factory) { - $factory->addConfiguration( - $transportChildren->arrayNode($factory->getName()) - ); - } - - $rootNode->children() - ->arrayNode('client')->children() - ->scalarNode('prefix')->defaultValue('enqueue')->end() - ->scalarNode('app_name')->defaultValue('app')->end() - ->scalarNode('router_topic')->defaultValue(Config::DEFAULT_PROCESSOR_QUEUE_NAME)->cannotBeEmpty()->end() - ->scalarNode('router_queue')->defaultValue(Config::DEFAULT_PROCESSOR_QUEUE_NAME)->cannotBeEmpty()->end() - ->scalarNode('default_processor_queue')->defaultValue(Config::DEFAULT_PROCESSOR_QUEUE_NAME)->cannotBeEmpty()->end() - ->integerNode('redelivered_delay_time')->min(0)->defaultValue(0)->end() - ->end()->end() - ->arrayNode('extensions')->addDefaultsIfNotSet()->children() - ->booleanNode('signal_extension')->defaultValue(function_exists('pcntl_signal_dispatch'))->end() - ->end()->end() - ; - - return $tb->buildTree(); - } -} diff --git a/pkg/simple-client/Tests/Functional/SimpleClientTest.php b/pkg/simple-client/Tests/Functional/SimpleClientTest.php index 87d60034b..ce75457af 100644 --- a/pkg/simple-client/Tests/Functional/SimpleClientTest.php +++ b/pkg/simple-client/Tests/Functional/SimpleClientTest.php @@ -7,9 +7,8 @@ use Enqueue\Consumption\Extension\LimitConsumptionTimeExtension; use Enqueue\Consumption\Result; use Enqueue\SimpleClient\SimpleClient; -use Enqueue\Test\RabbitManagementExtensionTrait; -use Enqueue\Test\RabbitmqAmqpExtension; -use Interop\Queue\PsrMessage; +use Interop\Queue\Exception\PurgeQueueNotSupportedException; +use Interop\Queue\Message; use PHPUnit\Framework\TestCase; /** @@ -17,140 +16,197 @@ */ class SimpleClientTest extends TestCase { - use RabbitmqAmqpExtension; - use RabbitManagementExtensionTrait; - - public function setUp() - { - if (false == getenv('RABBITMQ_HOST')) { - throw new \PHPUnit_Framework_SkippedTestError('Functional tests are not allowed in this environment'); - } - - $this->removeQueue('enqueue.app.default'); - } - public function transportConfigDataProvider() { - yield 'amqp' => [[ - 'transport' => [ - 'default' => 'amqp', - 'amqp' => [ - 'driver' => 'ext', - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - ], - ], - ]]; + yield 'amqp_dsn' => [[ + 'transport' => getenv('AMQP_DSN'), + ], '+1sec']; - yield 'config_as_dsn_string' => [getenv('AMQP_DSN')]; + yield 'dbal_dsn' => [[ + 'transport' => getenv('DOCTRINE_DSN'), + ], '+1sec']; - yield 'amqp_dsn' => [[ + yield 'rabbitmq_stomp' => [[ 'transport' => [ - 'default' => 'amqp', - 'amqp' => getenv('AMQP_DSN'), + 'dsn' => getenv('RABITMQ_STOMP_DSN'), + 'lazy' => false, + 'management_plugin_installed' => true, ], - ]]; + ], '+1sec']; - yield 'default_amqp_as_dsn' => [[ + yield 'predis_dsn' => [[ 'transport' => [ - 'default' => getenv('AMQP_DSN'), + 'dsn' => getenv('PREDIS_DSN'), + 'lazy' => false, ], - ]]; + ], '+1sec']; - yield [[ - 'transport' => [ - 'default' => 'rabbitmq_amqp', - 'rabbitmq_amqp' => [ - 'driver' => 'ext', - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - ], - ], - ]]; + yield 'fs_dsn' => [[ + 'transport' => 'file://'.sys_get_temp_dir(), + ], '+1sec']; - yield [[ + yield 'sqs' => [[ 'transport' => [ - 'default' => 'rabbitmq_amqp', - 'rabbitmq_amqp' => [ - 'driver' => 'ext', - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - ], + 'dsn' => getenv('SQS_DSN'), ], - ]]; + ], '+1sec']; yield 'mongodb_dsn' => [[ - 'transport' => [ - 'default' => 'mongodb', - 'mongodb' => getenv('MONGO_DSN'), - ], - ]]; + 'transport' => getenv('MONGO_DSN'), + ], '+1sec']; + } + + public function testShouldWorkWithStringDsnConstructorArgument() + { + $actualMessage = null; + + $client = new SimpleClient(getenv('AMQP_DSN')); + + $client->bindTopic('foo_topic', function (Message $message) use (&$actualMessage) { + $actualMessage = $message; + + return Result::ACK; + }); + + $client->setupBroker(); + $this->purgeQueue($client); + + $client->sendEvent('foo_topic', 'Hello there!'); + + $client->getQueueConsumer()->setReceiveTimeout(200); + $client->consume(new ChainExtension([ + new LimitConsumptionTimeExtension(new \DateTime('+1sec')), + new LimitConsumedMessagesExtension(2), + ])); + + $this->assertInstanceOf(Message::class, $actualMessage); + $this->assertSame('Hello there!', $actualMessage->getBody()); } /** * @dataProvider transportConfigDataProvider - * - * @param mixed $config */ - public function testProduceAndConsumeOneMessage($config) + public function testSendEventWithOneSubscriber($config, string $timeLimit) { $actualMessage = null; + $config['client'] = [ + 'prefix' => str_replace('.', '', uniqid('enqueue', true)), + 'app_name' => 'simple_client', + 'router_topic' => 'test', + 'router_queue' => 'test', + 'default_queue' => 'test', + ]; + $client = new SimpleClient($config); - $client->bind('foo_topic', 'foo_processor', function (PsrMessage $message) use (&$actualMessage) { + + $client->bindTopic('foo_topic', function (Message $message) use (&$actualMessage) { $actualMessage = $message; return Result::ACK; }); - $client->send('foo_topic', 'Hello there!', true); + $client->setupBroker(); + $this->purgeQueue($client); + $client->sendEvent('foo_topic', 'Hello there!'); + + $client->getQueueConsumer()->setReceiveTimeout(200); $client->consume(new ChainExtension([ - new LimitConsumptionTimeExtension(new \DateTime('+5sec')), + new LimitConsumptionTimeExtension(new \DateTime($timeLimit)), new LimitConsumedMessagesExtension(2), ])); - $this->assertInstanceOf(PsrMessage::class, $actualMessage); + $this->assertInstanceOf(Message::class, $actualMessage); $this->assertSame('Hello there!', $actualMessage->getBody()); } /** * @dataProvider transportConfigDataProvider - * - * @param mixed $config */ - public function testProduceAndRouteToTwoConsumes($config) + public function testSendEventWithTwoSubscriber($config, string $timeLimit) { $received = 0; + $config['client'] = [ + 'prefix' => str_replace('.', '', uniqid('enqueue', true)), + 'app_name' => 'simple_client', + 'router_topic' => 'test', + 'router_queue' => 'test', + 'default_queue' => 'test', + ]; + $client = new SimpleClient($config); - $client->bind('foo_topic', 'foo_processor1', function () use (&$received) { + + $client->bindTopic('foo_topic', function () use (&$received) { ++$received; return Result::ACK; }); - $client->bind('foo_topic', 'foo_processor2', function () use (&$received) { + $client->bindTopic('foo_topic', function () use (&$received) { ++$received; return Result::ACK; }); - $client->send('foo_topic', 'Hello there!', true); + $client->setupBroker(); + $this->purgeQueue($client); + $client->sendEvent('foo_topic', 'Hello there!'); + $client->getQueueConsumer()->setReceiveTimeout(200); $client->consume(new ChainExtension([ - new LimitConsumptionTimeExtension(new \DateTime('+5sec')), + new LimitConsumptionTimeExtension(new \DateTime($timeLimit)), new LimitConsumedMessagesExtension(3), ])); $this->assertSame(2, $received); } + + /** + * @dataProvider transportConfigDataProvider + */ + public function testSendCommand($config, string $timeLimit) + { + $received = 0; + + $config['client'] = [ + 'prefix' => str_replace('.', '', uniqid('enqueue', true)), + 'app_name' => 'simple_client', + 'router_topic' => 'test', + 'router_queue' => 'test', + 'default_queue' => 'test', + ]; + + $client = new SimpleClient($config); + + $client->bindCommand('foo_command', function () use (&$received) { + ++$received; + + return Result::ACK; + }); + + $client->setupBroker(); + $this->purgeQueue($client); + + $client->sendCommand('foo_command', 'Hello there!'); + $client->getQueueConsumer()->setReceiveTimeout(200); + $client->consume(new ChainExtension([ + new LimitConsumptionTimeExtension(new \DateTime($timeLimit)), + new LimitConsumedMessagesExtension(1), + ])); + + $this->assertSame(1, $received); + } + + protected function purgeQueue(SimpleClient $client): void + { + $driver = $client->getDriver(); + + $queue = $driver->createQueue($driver->getConfig()->getDefaultQueue()); + + try { + $client->getDriver()->getContext()->purgeQueue($queue); + } catch (PurgeQueueNotSupportedException $e) { + } + } } diff --git a/pkg/simple-client/Tests/SimpleClientTest.php b/pkg/simple-client/Tests/SimpleClientTest.php deleted file mode 100644 index 3ff8badf9..000000000 --- a/pkg/simple-client/Tests/SimpleClientTest.php +++ /dev/null @@ -1,117 +0,0 @@ -removeQueue('enqueue.app.default'); - } - - public function transportConfigDataProvider() - { - yield 'amqp' => [[ - 'transport' => [ - 'default' => 'amqp', - 'amqp' => [ - 'driver' => 'ext', - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - ], - ], - ]]; - - yield 'config_as_dsn_string' => [getenv('AMQP_DSN')]; - - yield 'config_as_dsn_without_host' => ['amqp:?lazy=1']; - - yield 'amqp_dsn' => [[ - 'transport' => [ - 'default' => 'amqp', - 'amqp' => getenv('AMQP_DSN'), - ], - ]]; - - yield 'default_amqp_as_dsn' => [[ - 'transport' => [ - 'default' => getenv('AMQP_DSN'), - ], - ]]; - - yield [[ - 'transport' => [ - 'default' => 'rabbitmq_amqp', - 'rabbitmq_amqp' => [ - 'driver' => 'ext', - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - ], - ], - ]]; - - yield [[ - 'transport' => [ - 'default' => 'rabbitmq_amqp', - 'rabbitmq_amqp' => [ - 'driver' => 'ext', - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - ], - ], - ]]; - - yield 'mongodb_dsn' => [[ - 'transport' => [ - 'default' => 'mongodb', - 'mongodb' => getenv('MONGO_DSN'), - ], - ]]; - } - - /** - * @dataProvider transportConfigDataProvider - * - * @param mixed $config - */ - public function testProduceAndConsumeOneMessage($config) - { - $actualMessage = null; - - $client = new SimpleClient($config); - $client->bind('foo_topic', 'foo_processor', function (PsrMessage $message) use (&$actualMessage) { - $actualMessage = $message; - - return Result::ACK; - }); - - $this->assertInstanceOf(PsrContext::class, $client->getContext()); - } -} diff --git a/pkg/simple-client/Tests/fix_composer_json.php b/pkg/simple-client/Tests/fix_composer_json.php index fc430e276..01f73c95e 100644 --- a/pkg/simple-client/Tests/fix_composer_json.php +++ b/pkg/simple-client/Tests/fix_composer_json.php @@ -4,6 +4,6 @@ $composerJson = json_decode(file_get_contents(__DIR__.'/../composer.json'), true); -$composerJson['config']['platform']['ext-amqp'] = '1.7'; +$composerJson['config']['platform']['ext-amqp'] = '1.9.3'; -file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, JSON_PRETTY_PRINT)); +file_put_contents(__DIR__.'/../composer.json', json_encode($composerJson, \JSON_PRETTY_PRINT)); diff --git a/pkg/simple-client/composer.json b/pkg/simple-client/composer.json index 3986b0f8e..2d2bd3710 100644 --- a/pkg/simple-client/composer.json +++ b/pkg/simple-client/composer.json @@ -6,18 +6,19 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "enqueue/enqueue": "^0.8.21@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4", - "symfony/console": "^2.8|^3|^4" + "php": "^8.1", + "enqueue/enqueue": "^0.10", + "queue-interop/amqp-interop": "^0.8.2", + "queue-interop/queue-interop": "^0.8", + "symfony/config": "^5.4|^6.0" }, "require-dev": { - "phpunit/phpunit": "~5.5", - "enqueue/test": "^0.8@dev", - "enqueue/amqp-ext": "^0.8@dev", - "enqueue/fs": "^0.8@dev", - "enqueue/null": "^0.8@dev" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/amqp-ext": "0.10.x-dev", + "enqueue/fs": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "symfony/yaml": "^5.4|^6.0" }, "support": { "email": "opensource@forma-pro.com", @@ -35,7 +36,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/simple-client/phpunit.xml.dist b/pkg/simple-client/phpunit.xml.dist index e86476dec..81d59cfaf 100644 --- a/pkg/simple-client/phpunit.xml.dist +++ b/pkg/simple-client/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/sns/.gitattributes b/pkg/sns/.gitattributes new file mode 100644 index 000000000..bdf2dcb14 --- /dev/null +++ b/pkg/sns/.gitattributes @@ -0,0 +1,5 @@ +/Tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +phpunit.xml.dist export-ignore diff --git a/pkg/sns/.github/workflows/ci.yml b/pkg/sns/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/sns/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/sns/.gitignore b/pkg/sns/.gitignore new file mode 100644 index 000000000..a770439e5 --- /dev/null +++ b/pkg/sns/.gitignore @@ -0,0 +1,6 @@ +*~ +/composer.lock +/composer.phar +/phpunit.xml +/vendor/ +/.idea/ diff --git a/pkg/sns/LICENSE b/pkg/sns/LICENSE new file mode 100644 index 000000000..20211e5fd --- /dev/null +++ b/pkg/sns/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright (c) 2018 Max Kotliar + +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/pkg/sns/README.md b/pkg/sns/README.md new file mode 100644 index 000000000..bfc7a4012 --- /dev/null +++ b/pkg/sns/README.md @@ -0,0 +1,28 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + +# Amazon SNS Transport + +[![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/sns/ci.yml?branch=master)](https://github.com/php-enqueue/sns/actions?query=workflow%3ACI) +[![Total Downloads](https://poser.pugx.org/enqueue/sns/d/total.png)](https://packagist.org/packages/enqueue/sns) +[![Latest Stable Version](https://poser.pugx.org/enqueue/sns/version.png)](https://packagist.org/packages/enqueue/sns) + +This is an implementation of Queue Interop specification. It allows you to send and consume message using [Amazon SNS](https://aws.amazon.com/sns/) service. + +## Resources + +* [Site](https://enqueue.forma-pro.com/) +* [Documentation](https://php-enqueue.github.io/transport/sns/) +* [Questions](https://gitter.im/php-enqueue/Lobby) +* [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) + +## License + +It is released under the [MIT License](LICENSE). diff --git a/pkg/sns/SnsClient.php b/pkg/sns/SnsClient.php new file mode 100644 index 000000000..7c4a81693 --- /dev/null +++ b/pkg/sns/SnsClient.php @@ -0,0 +1,145 @@ +inputClient = $inputClient; + } + + public function createTopic(array $args): Result + { + return $this->callApi('createTopic', $args); + } + + public function deleteTopic(string $topicArn): Result + { + return $this->callApi('DeleteTopic', [ + 'TopicArn' => $topicArn, + ]); + } + + public function publish(array $args): Result + { + return $this->callApi('publish', $args); + } + + public function subscribe(array $args): Result + { + return $this->callApi('subscribe', $args); + } + + public function unsubscribe(array $args): Result + { + return $this->callApi('unsubscribe', $args); + } + + public function setSubscriptionAttributes(array $args): Result + { + return $this->callApi('setSubscriptionAttributes', $args); + } + + public function listSubscriptionsByTopic(array $args): Result + { + return $this->callApi('ListSubscriptionsByTopic', $args); + } + + public function getAWSClient(): AwsSnsClient + { + $this->resolveClient(); + + if ($this->singleClient) { + return $this->singleClient; + } + + if ($this->multiClient) { + $mr = new \ReflectionMethod($this->multiClient, 'getClientFromPool'); + $mr->setAccessible(true); + $singleClient = $mr->invoke($this->multiClient, $this->multiClient->getRegion()); + $mr->setAccessible(false); + + return $singleClient; + } + + throw new \LogicException('The multi or single client must be set'); + } + + private function callApi(string $name, array $args): Result + { + $this->resolveClient(); + + if ($this->singleClient) { + if (false == empty($args['@region'])) { + throw new \LogicException('Cannot send message to another region because transport is configured with single aws client'); + } + + unset($args['@region']); + + return call_user_func([$this->singleClient, $name], $args); + } + + if ($this->multiClient) { + return call_user_func([$this->multiClient, $name], $args); + } + + throw new \LogicException('The multi or single client must be set'); + } + + private function resolveClient(): void + { + if ($this->singleClient || $this->multiClient) { + return; + } + + $client = $this->inputClient; + if ($client instanceof MultiRegionClient) { + $this->multiClient = $client; + + return; + } elseif ($client instanceof AwsSnsClient) { + $this->singleClient = $client; + + return; + } elseif (is_callable($client)) { + $client = call_user_func($client); + if ($client instanceof MultiRegionClient) { + $this->multiClient = $client; + + return; + } + if ($client instanceof AwsSnsClient) { + $this->singleClient = $client; + + return; + } + } + + throw new \LogicException(sprintf('The input client must be an instance of "%s" or "%s" or a callable that returns one of those. Got "%s"', AwsSnsClient::class, MultiRegionClient::class, is_object($client) ? $client::class : gettype($client))); + } +} diff --git a/pkg/sns/SnsConnectionFactory.php b/pkg/sns/SnsConnectionFactory.php new file mode 100644 index 000000000..8a815abad --- /dev/null +++ b/pkg/sns/SnsConnectionFactory.php @@ -0,0 +1,159 @@ + null, AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. + * 'secret' => null, AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. + * 'token' => null, AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. + * 'region' => null, (string, required) Region to connect to. See http://docs.aws.amazon.com/general/latest/gr/rande.html for a list of available regions. + * 'version' => '2012-11-05', (string, required) The version of the webservice to utilize + * 'lazy' => true, Enable lazy connection (boolean) + * 'endpoint' => null, (string, default=null) The full URI of the webservice. This is only required when connecting to a custom endpoint e.g. localstack + * 'topic_arns' => [], (array) The list of existing topic arns: key - topic name; value - arn + * ]. + * + * or + * + * sns: + * sns::?key=aKey&secret=aSecret&token=aToken + * + * @param array|string|SnsClient|null $config + */ + public function __construct($config = 'sns:') + { + if ($config instanceof AwsSnsClient) { + $this->client = new SnsClient($config); + $this->config = ['lazy' => false] + $this->defaultConfig(); + + return; + } + + if (empty($config)) { + $config = []; + } elseif (\is_string($config)) { + $config = $this->parseDsn($config); + } elseif (\is_array($config)) { + if (\array_key_exists('dsn', $config)) { + $config = \array_replace_recursive($config, $this->parseDsn($config['dsn'])); + + unset($config['dsn']); + } + } else { + throw new \LogicException(\sprintf('The config must be either an array of options, a DSN string, null or instance of %s', AwsSnsClient::class)); + } + + $this->config = \array_replace($this->defaultConfig(), $config); + } + + /** + * @return SnsContext + */ + public function createContext(): Context + { + return new SnsContext($this->establishConnection(), $this->config); + } + + private function establishConnection(): SnsClient + { + if ($this->client) { + return $this->client; + } + + $config = [ + 'version' => $this->config['version'], + 'region' => $this->config['region'], + ]; + + if (isset($this->config['endpoint'])) { + $config['endpoint'] = $this->config['endpoint']; + } + + if (isset($this->config['profile'])) { + $config['profile'] = $this->config['profile']; + } + + if ($this->config['key'] && $this->config['secret']) { + $config['credentials'] = [ + 'key' => $this->config['key'], + 'secret' => $this->config['secret'], + ]; + + if ($this->config['token']) { + $config['credentials']['token'] = $this->config['token']; + } + } + + if (isset($this->config['http'])) { + $config['http'] = $this->config['http']; + } + + $establishConnection = function () use ($config) { + return (new Sdk(['Sns' => $config]))->createMultiRegionSns(); + }; + + $this->client = $this->config['lazy'] ? + new SnsClient($establishConnection) : + new SnsClient($establishConnection()) + ; + + return $this->client; + } + + private function parseDsn(string $dsn): array + { + $dsn = Dsn::parseFirst($dsn); + + if ('sns' !== $dsn->getSchemeProtocol()) { + throw new \LogicException(\sprintf('The given scheme protocol "%s" is not supported. It must be "sns"', $dsn->getSchemeProtocol())); + } + + return \array_filter(\array_replace($dsn->getQuery(), [ + 'key' => $dsn->getString('key'), + 'secret' => $dsn->getString('secret'), + 'token' => $dsn->getString('token'), + 'region' => $dsn->getString('region'), + 'version' => $dsn->getString('version'), + 'lazy' => $dsn->getBool('lazy'), + 'endpoint' => $dsn->getString('endpoint'), + 'topic_arns' => $dsn->getArray('topic_arns', [])->toArray(), + 'http' => $dsn->getArray('http', [])->toArray(), + ]), function ($value) { return null !== $value; }); + } + + private function defaultConfig(): array + { + return [ + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => true, + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [], + ]; + } +} diff --git a/pkg/sns/SnsContext.php b/pkg/sns/SnsContext.php new file mode 100644 index 000000000..2e19164d9 --- /dev/null +++ b/pkg/sns/SnsContext.php @@ -0,0 +1,214 @@ +client = $client; + $this->config = $config; + $this->topicArns = $config['topic_arns'] ?? []; + } + + /** + * @return SnsMessage + */ + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message + { + return new SnsMessage($body, $properties, $headers); + } + + /** + * @return SnsDestination + */ + public function createTopic(string $topicName): Topic + { + return new SnsDestination($topicName); + } + + /** + * @return SnsDestination + */ + public function createQueue(string $queueName): Queue + { + return new SnsDestination($queueName); + } + + public function declareTopic(SnsDestination $destination): void + { + $result = $this->client->createTopic([ + 'Attributes' => $destination->getAttributes(), + 'Name' => $destination->getQueueName(), + ]); + + if (false == $result->hasKey('TopicArn')) { + throw new \RuntimeException(sprintf('Cannot create topic. topicName: "%s"', $destination->getTopicName())); + } + + $this->topicArns[$destination->getTopicName()] = (string) $result->get('TopicArn'); + } + + public function setTopicArn(SnsDestination $destination, string $arn): void + { + $this->topicArns[$destination->getTopicName()] = $arn; + } + + public function deleteTopic(SnsDestination $destination): void + { + $this->client->deleteTopic($this->getTopicArn($destination)); + + unset($this->topicArns[$destination->getTopicName()]); + } + + public function subscribe(SnsSubscribe $subscribe): void + { + foreach ($this->getSubscriptions($subscribe->getTopic()) as $subscription) { + if ($subscription['Protocol'] === $subscribe->getProtocol() + && $subscription['Endpoint'] === $subscribe->getEndpoint()) { + return; + } + } + + $this->client->subscribe([ + 'Attributes' => $subscribe->getAttributes(), + 'Endpoint' => $subscribe->getEndpoint(), + 'Protocol' => $subscribe->getProtocol(), + 'ReturnSubscriptionArn' => $subscribe->isReturnSubscriptionArn(), + 'TopicArn' => $this->getTopicArn($subscribe->getTopic()), + ]); + } + + public function unsubscibe(SnsUnsubscribe $unsubscribe): void + { + foreach ($this->getSubscriptions($unsubscribe->getTopic()) as $subscription) { + if ($subscription['Protocol'] != $unsubscribe->getProtocol()) { + continue; + } + + if ($subscription['Endpoint'] != $unsubscribe->getEndpoint()) { + continue; + } + + $this->client->unsubscribe([ + 'SubscriptionArn' => $subscription['SubscriptionArn'], + ]); + } + } + + public function getSubscriptions(SnsDestination $destination): array + { + $args = [ + 'TopicArn' => $this->getTopicArn($destination), + ]; + + $subscriptions = []; + while (true) { + $result = $this->client->listSubscriptionsByTopic($args); + + $subscriptions = array_merge($subscriptions, $result->get('Subscriptions')); + + if (false == $result->hasKey('NextToken')) { + break; + } + + $args['NextToken'] = $result->get('NextToken'); + } + + return $subscriptions; + } + + public function setSubscriptionAttributes(SnsSubscribe $subscribe): void + { + foreach ($this->getSubscriptions($subscribe->getTopic()) as $subscription) { + $this->client->setSubscriptionAttributes(array_merge( + $subscribe->getAttributes(), + ['SubscriptionArn' => $subscription['SubscriptionArn']], + )); + } + } + + public function getTopicArn(SnsDestination $destination): string + { + if (false == array_key_exists($destination->getTopicName(), $this->topicArns)) { + $this->declareTopic($destination); + } + + return $this->topicArns[$destination->getTopicName()]; + } + + public function createTemporaryQueue(): Queue + { + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); + } + + /** + * @return SnsProducer + */ + public function createProducer(): Producer + { + return new SnsProducer($this); + } + + /** + * @param SnsDestination $destination + */ + public function createConsumer(Destination $destination): Consumer + { + throw new \LogicException('SNS transport does not support consumption. You should consider using SQS instead.'); + } + + public function close(): void + { + } + + /** + * @param SnsDestination $queue + */ + public function purgeQueue(Queue $queue): void + { + PurgeQueueNotSupportedException::providerDoestNotSupportIt(); + } + + public function createSubscriptionConsumer(): SubscriptionConsumer + { + throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); + } + + public function getAwsSnsClient(): AwsSnsClient + { + return $this->client->getAWSClient(); + } + + public function getSnsClient(): SnsClient + { + return $this->client; + } +} diff --git a/pkg/sns/SnsDestination.php b/pkg/sns/SnsDestination.php new file mode 100644 index 000000000..adcb08f43 --- /dev/null +++ b/pkg/sns/SnsDestination.php @@ -0,0 +1,119 @@ +name = $name; + $this->attributes = []; + } + + public function getQueueName(): string + { + return $this->name; + } + + public function getTopicName(): string + { + return $this->name; + } + + /** + * The policy that defines who can access your topic. By default, only the topic owner can publish or subscribe to the topic. + */ + public function setPolicy(?string $policy = null): void + { + $this->setAttribute('Policy', $policy); + } + + public function getPolicy(): ?string + { + return $this->getAttribute('Policy'); + } + + /** + * The display name to use for a topic with SMS subscriptions. + */ + public function setDisplayName(?string $displayName = null): void + { + $this->setAttribute('DisplayName', $displayName); + } + + public function getDisplayName(): ?string + { + return $this->getAttribute('DisplayName'); + } + + /** + * The display name to use for a topic with SMS subscriptions. + */ + public function setDeliveryPolicy(?int $deliveryPolicy = null): void + { + $this->setAttribute('DeliveryPolicy', $deliveryPolicy); + } + + public function getDeliveryPolicy(): ?int + { + return $this->getAttribute('DeliveryPolicy'); + } + + /** + * Only FIFO. + * + * Designates a topic as FIFO. You can provide this attribute only during queue creation. + * You can't change it for an existing topic. When you set this attribute, you must provide aMessageGroupId + * explicitly. + * For more information, see https://docs.aws.amazon.com/sns/latest/dg/sns-fifo-topics.html + */ + public function setFifoTopic(bool $enable): void + { + $value = $enable ? 'true' : null; + + $this->setAttribute('FifoTopic', $value); + } + + /** + * Only FIFO. + * + * Enables content-based deduplication. + * For more information, see: https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html + */ + public function setContentBasedDeduplication(bool $enable): void + { + $value = $enable ? 'true' : null; + + $this->setAttribute('ContentBasedDeduplication', $value); + } + + public function getAttributes(): array + { + return $this->attributes; + } + + private function getAttribute(string $name, $default = null) + { + return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; + } + + private function setAttribute(string $name, $value): void + { + if (null == $value) { + unset($this->attributes[$name]); + } else { + $this->attributes[$name] = $value; + } + } +} diff --git a/pkg/sns/SnsMessage.php b/pkg/sns/SnsMessage.php new file mode 100644 index 000000000..4122209e8 --- /dev/null +++ b/pkg/sns/SnsMessage.php @@ -0,0 +1,222 @@ +body = $body; + $this->properties = $properties; + $this->headers = $headers; + $this->messageAttributes = $messageAttributes; + $this->messageStructure = $messageStructure; + $this->phoneNumber = $phoneNumber; + $this->subject = $subject; + $this->targetArn = $targetArn; + $this->redelivered = false; + } + + public function getSnsMessageId(): ?string + { + return $this->snsMessageId; + } + + public function setSnsMessageId(?string $snsMessageId): void + { + $this->snsMessageId = $snsMessageId; + } + + public function getMessageStructure(): ?string + { + return $this->messageStructure; + } + + public function setMessageStructure(?string $messageStructure): void + { + $this->messageStructure = $messageStructure; + } + + public function getPhoneNumber(): ?string + { + return $this->phoneNumber; + } + + public function setPhoneNumber(?string $phoneNumber): void + { + $this->phoneNumber = $phoneNumber; + } + + public function getSubject(): ?string + { + return $this->subject; + } + + public function setSubject(?string $subject): void + { + $this->subject = $subject; + } + + public function getMessageAttributes(): ?array + { + return $this->messageAttributes; + } + + public function setMessageAttributes(?array $messageAttributes): void + { + $this->messageAttributes = $messageAttributes; + } + + /** + * @param null $default + * + * @return array|null + */ + public function getAttribute(string $name, $default = null) + { + return array_key_exists($name, $this->messageAttributes) ? $this->messageAttributes[$name] : $default; + } + + /** + * Attribute array format: + * [ + * 'BinaryValue' => , + * 'DataType' => '', // REQUIRED + * 'StringValue' => '', + * ]. + */ + public function setAttribute(string $name, ?array $attribute): void + { + if (null === $attribute) { + unset($this->messageAttributes[$name]); + } else { + $this->messageAttributes[$name] = $attribute; + } + } + + /** + * @param string $dataType String, String.Array, Number, or Binary + * @param string|resource|StreamInterface $value + */ + public function addAttribute(string $name, string $dataType, $value): void + { + $valueKey = 'Binary' === $dataType ? 'BinaryValue' : 'StringValue'; + + $this->messageAttributes[$name] = [ + 'DataType' => $dataType, + $valueKey => $value, + ]; + } + + public function getTargetArn(): ?string + { + return $this->targetArn; + } + + public function setTargetArn(?string $targetArn): void + { + $this->targetArn = $targetArn; + } + + /** + * Only FIFO. + * + * The tag that specifies that a message belongs to a specific message group. Messages that belong to the same + * message group are processed in a FIFO manner (however, messages in different message groups might be processed + * out of order). + * To interleave multiple ordered streams within a single queue, use MessageGroupId values (for example, session + * data for multiple users). In this scenario, multiple readers can process the queue, but the session data + * of each user is processed in a FIFO fashion. + * For more information, see: https://docs.aws.amazon.com/sns/latest/dg/fifo-message-grouping.html + */ + public function setMessageGroupId(?string $id = null): void + { + $this->messageGroupId = $id; + } + + public function getMessageGroupId(): ?string + { + return $this->messageGroupId; + } + + /** + * Only FIFO. + * + * The token used for deduplication of sent messages. If a message with a particular MessageDeduplicationId is + * sent successfully, any messages sent with the same MessageDeduplicationId are accepted successfully but + * aren't delivered during the 5-minute deduplication interval. + * For more information, see https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html + */ + public function setMessageDeduplicationId(?string $id = null): void + { + $this->messageDeduplicationId = $id; + } + + public function getMessageDeduplicationId(): ?string + { + return $this->messageDeduplicationId; + } +} diff --git a/pkg/sns/SnsProducer.php b/pkg/sns/SnsProducer.php new file mode 100644 index 000000000..ac7e38b5b --- /dev/null +++ b/pkg/sns/SnsProducer.php @@ -0,0 +1,153 @@ +context = $context; + } + + /** + * @param SnsDestination $destination + * @param SnsMessage $message + */ + public function send(Destination $destination, Message $message): void + { + InvalidDestinationException::assertDestinationInstanceOf($destination, SnsDestination::class); + InvalidMessageException::assertMessageInstanceOf($message, SnsMessage::class); + + $body = $message->getBody(); + if (empty($body)) { + throw new InvalidMessageException('The message body must be a non-empty string.'); + } + + $topicArn = $this->context->getTopicArn($destination); + + $arguments = [ + 'Message' => $message->getBody(), + 'MessageAttributes' => [ + 'Headers' => [ + 'DataType' => 'String', + 'StringValue' => json_encode([$message->getHeaders(), $message->getProperties()]), + ], + ], + 'TopicArn' => $topicArn, + ]; + + if (null !== $message->getMessageAttributes()) { + $arguments['MessageAttributes'] = array_merge( + $arguments['MessageAttributes'], + $message->getMessageAttributes() + ); + } + + if (null !== ($structure = $message->getMessageStructure())) { + $arguments['MessageStructure'] = $structure; + } + if (null !== ($phone = $message->getPhoneNumber())) { + $arguments['PhoneNumber'] = $phone; + } + if (null !== ($subject = $message->getSubject())) { + $arguments['Subject'] = $subject; + } + if (null !== ($targetArn = $message->getTargetArn())) { + $arguments['TargetArn'] = $targetArn; + } + + if ($messageGroupId = $message->getMessageGroupId()) { + $arguments['MessageGroupId'] = $messageGroupId; + } + + if ($messageDeduplicationId = $message->getMessageDeduplicationId()) { + $arguments['MessageDeduplicationId'] = $messageDeduplicationId; + } + + $result = $this->context->getSnsClient()->publish($arguments); + + if (false == $result->hasKey('MessageId')) { + throw new \RuntimeException('Message was not sent'); + } + + $message->setSnsMessageId((string) $result->get('MessageId')); + } + + /** + * @throws DeliveryDelayNotSupportedException + * + * @return SnsProducer + */ + public function setDeliveryDelay(?int $deliveryDelay = null): Producer + { + if (null === $deliveryDelay) { + return $this; + } + + throw DeliveryDelayNotSupportedException::providerDoestNotSupportIt(); + } + + public function getDeliveryDelay(): ?int + { + return $this->deliveryDelay; + } + + /** + * @throws PriorityNotSupportedException + * + * @return SnsProducer + */ + public function setPriority(?int $priority = null): Producer + { + if (null === $priority) { + return $this; + } + + throw PriorityNotSupportedException::providerDoestNotSupportIt(); + } + + public function getPriority(): ?int + { + return null; + } + + /** + * @throws TimeToLiveNotSupportedException + * + * @return SnsProducer + */ + public function setTimeToLive(?int $timeToLive = null): Producer + { + if (null === $timeToLive) { + return $this; + } + + throw TimeToLiveNotSupportedException::providerDoestNotSupportIt(); + } + + public function getTimeToLive(): ?int + { + return null; + } +} diff --git a/pkg/sns/SnsSubscribe.php b/pkg/sns/SnsSubscribe.php new file mode 100644 index 000000000..52991d81f --- /dev/null +++ b/pkg/sns/SnsSubscribe.php @@ -0,0 +1,68 @@ +topic = $topic; + $this->endpoint = $endpoint; + $this->protocol = $protocol; + $this->returnSubscriptionArn = $returnSubscriptionArn; + $this->attributes = $attributes; + } + + public function getTopic(): SnsDestination + { + return $this->topic; + } + + public function getEndpoint(): string + { + return $this->endpoint; + } + + public function getProtocol(): string + { + return $this->protocol; + } + + public function isReturnSubscriptionArn(): bool + { + return $this->returnSubscriptionArn; + } + + public function getAttributes(): array + { + return $this->attributes; + } +} diff --git a/pkg/sns/SnsUnsubscribe.php b/pkg/sns/SnsUnsubscribe.php new file mode 100644 index 000000000..ad6b93d45 --- /dev/null +++ b/pkg/sns/SnsUnsubscribe.php @@ -0,0 +1,48 @@ +topic = $topic; + $this->endpoint = $endpoint; + $this->protocol = $protocol; + } + + public function getTopic(): SnsDestination + { + return $this->topic; + } + + public function getEndpoint(): string + { + return $this->endpoint; + } + + public function getProtocol(): string + { + return $this->protocol; + } +} diff --git a/pkg/sns/Tests/SnsClientTest.php b/pkg/sns/Tests/SnsClientTest.php new file mode 100644 index 000000000..a029f4fd0 --- /dev/null +++ b/pkg/sns/Tests/SnsClientTest.php @@ -0,0 +1,227 @@ + [ + 'key' => '', + 'secret' => '', + 'region' => 'us-west-2', + 'version' => '2010-03-31', + 'endpoint' => 'http://localhost/', + ]]))->createSns(); + + $client = new SnsClient($awsClient); + + $this->assertSame($awsClient, $client->getAWSClient()); + } + + public function testShouldAllowGetAwsClientIfMultipleClientProvided() + { + $awsClient = (new Sdk(['Sns' => [ + 'key' => '', + 'secret' => '', + 'region' => 'us-west-2', + 'version' => '2010-03-31', + 'endpoint' => 'http://localhost/', + ]]))->createMultiRegionSns(); + + $client = new SnsClient($awsClient); + + $this->assertInstanceOf(AwsSnsClient::class, $client->getAWSClient()); + } + + /** + * @dataProvider provideApiCallsSingleClient + * @dataProvider provideApiCallsMultipleClient + */ + public function testApiCall(string $method, array $args, array $result, string $awsClientClass) + { + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->once()) + ->method($method) + ->with($this->identicalTo($args)) + ->willReturn(new Result($result)); + + $client = new SnsClient($awsClient); + + $actualResult = $client->{$method}($args); + + $this->assertInstanceOf(Result::class, $actualResult); + $this->assertSame($result, $actualResult->toArray()); + } + + /** + * @dataProvider provideApiCallsSingleClient + * @dataProvider provideApiCallsMultipleClient + */ + public function testLazyApiCall(string $method, array $args, array $result, string $awsClientClass) + { + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->once()) + ->method($method) + ->with($this->identicalTo($args)) + ->willReturn(new Result($result)); + + $client = new SnsClient(function () use ($awsClient) { + return $awsClient; + }); + + $actualResult = $client->{$method}($args); + + $this->assertInstanceOf(Result::class, $actualResult); + $this->assertSame($result, $actualResult->toArray()); + } + + /** + * @dataProvider provideApiCallsSingleClient + * @dataProvider provideApiCallsMultipleClient + */ + public function testThrowIfInvalidInputClientApiCall(string $method, array $args, array $result, string $awsClientClass) + { + $client = new SnsClient(new \stdClass()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The input client must be an instance of "Aws\Sns\SnsClient" or "Aws\MultiRegionClient" or a callable that returns one of those. Got "stdClass"'); + $client->{$method}($args); + } + + /** + * @dataProvider provideApiCallsSingleClient + * @dataProvider provideApiCallsMultipleClient + */ + public function testThrowIfInvalidLazyInputClientApiCall(string $method, array $args, array $result, string $awsClientClass) + { + $client = new SnsClient(function () { return new \stdClass(); }); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The input client must be an instance of "Aws\Sns\SnsClient" or "Aws\MultiRegionClient" or a callable that returns one of those. Got "stdClass"'); + $client->{$method}($args); + } + + /** + * @dataProvider provideApiCallsMultipleClient + */ + public function testApiCallWithMultiClientAndCustomRegion(string $method, array $args, array $result, string $awsClientClass) + { + $args['@region'] = 'theRegion'; + + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->once()) + ->method($method) + ->with($this->identicalTo($args)) + ->willReturn(new Result($result)); + + $client = new SnsClient($awsClient); + + $actualResult = $client->{$method}($args); + + $this->assertInstanceOf(Result::class, $actualResult); + $this->assertSame($result, $actualResult->toArray()); + } + + /** + * @dataProvider provideApiCallsSingleClient + */ + public function testApiCallWithSingleClientAndCustomRegion(string $method, array $args, array $result, string $awsClientClass) + { + $args['@region'] = 'theRegion'; + + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->never()) + ->method($method) + ; + + $client = new SnsClient($awsClient); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Cannot send message to another region because transport is configured with single aws client'); + $client->{$method}($args); + } + + /** + * @dataProvider provideApiCallsSingleClient + */ + public function testApiCallWithMultiClientAndEmptyCustomRegion(string $method, array $args, array $result, string $awsClientClass) + { + $expectedArgs = $args; + $args['@region'] = ''; + + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->once()) + ->method($method) + ->with($this->identicalTo($expectedArgs)) + ->willReturn(new Result($result)); + + $client = new SnsClient($awsClient); + + $actualResult = $client->{$method}($args); + + $this->assertInstanceOf(Result::class, $actualResult); + $this->assertSame($result, $actualResult->toArray()); + } + + public function provideApiCallsSingleClient() + { + yield [ + 'createTopic', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSnsClient::class, + ]; + + yield [ + 'publish', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSnsClient::class, + ]; + } + + public function provideApiCallsMultipleClient() + { + yield [ + 'createTopic', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + + yield [ + 'publish', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + } +} diff --git a/pkg/sns/Tests/SnsConnectionFactoryConfigTest.php b/pkg/sns/Tests/SnsConnectionFactoryConfigTest.php new file mode 100644 index 000000000..305a6518d --- /dev/null +++ b/pkg/sns/Tests/SnsConnectionFactoryConfigTest.php @@ -0,0 +1,201 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage('The config must be either an array of options, a DSN string, null or instance of Aws\Sns\SnsClient'); + + new SnsConnectionFactory(new \stdClass()); + } + + public function testThrowIfSchemeIsNotAmqp() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The given scheme protocol "http" is not supported. It must be "sns"'); + + new SnsConnectionFactory('http://example.com/'); + } + + public function testThrowIfDsnCouldNotBeParsed() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The DSN is invalid.'); + + new SnsConnectionFactory('foo'); + } + + /** + * @dataProvider provideConfigs + */ + public function testShouldParseConfigurationAsExpected($config, $expectedConfig) + { + $factory = new SnsConnectionFactory($config); + + $this->assertAttributeEquals($expectedConfig, 'config', $factory); + } + + public static function provideConfigs() + { + yield [ + null, + [ + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => true, + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [], + ], + ]; + + yield [ + 'sns:', + [ + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => true, + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [], + ], + ]; + + yield [ + [], + [ + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => true, + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [], + ], + ]; + + yield [ + 'sns:?key=theKey&secret=theSecret&token=theToken&lazy=0', + [ + 'key' => 'theKey', + 'secret' => 'theSecret', + 'token' => 'theToken', + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => false, + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [], + ], + ]; + + yield [ + ['dsn' => 'sns:?key=theKey&secret=theSecret&token=theToken&lazy=0'], + [ + 'key' => 'theKey', + 'secret' => 'theSecret', + 'token' => 'theToken', + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => false, + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [], + ], + ]; + + yield [ + ['key' => 'theKey', 'secret' => 'theSecret', 'token' => 'theToken', 'lazy' => false], + [ + 'key' => 'theKey', + 'secret' => 'theSecret', + 'token' => 'theToken', + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => false, + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [], + ], + ]; + + yield [ + [ + 'key' => 'theKey', + 'secret' => 'theSecret', + 'token' => 'theToken', + 'lazy' => false, + 'endpoint' => 'http://localstack:1111/', + ], + [ + 'key' => 'theKey', + 'secret' => 'theSecret', + 'token' => 'theToken', + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => false, + 'endpoint' => 'http://localstack:1111/', + 'topic_arns' => [], + 'http' => [], + ], + ]; + + yield [ + ['dsn' => 'sns:?topic_arns[topic1]=arn:aws:sns:us-east-1:123456789012:topic1&topic_arns[topic2]=arn:aws:sns:us-west-2:123456789012:topic2'], + [ + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => true, + 'endpoint' => null, + 'topic_arns' => [ + 'topic1' => 'arn:aws:sns:us-east-1:123456789012:topic1', + 'topic2' => 'arn:aws:sns:us-west-2:123456789012:topic2', + ], + 'http' => [], + ], + ]; + + yield [ + ['dsn' => 'sns:?http[timeout]=5&http[connect_timeout]=2'], + [ + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'version' => '2010-03-31', + 'lazy' => true, + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [ + 'timeout' => '5', + 'connect_timeout' => '2', + ], + ], + ]; + } +} diff --git a/pkg/sns/Tests/SnsConnectionFactoryTest.php b/pkg/sns/Tests/SnsConnectionFactoryTest.php new file mode 100644 index 000000000..4e9ad6ec9 --- /dev/null +++ b/pkg/sns/Tests/SnsConnectionFactoryTest.php @@ -0,0 +1,85 @@ +assertClassImplements(ConnectionFactory::class, SnsConnectionFactory::class); + } + + public function testCouldBeConstructedWithEmptyConfiguration() + { + $factory = new SnsConnectionFactory([]); + + $this->assertAttributeEquals([ + 'lazy' => true, + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'version' => '2010-03-31', + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [], + ], 'config', $factory); + } + + public function testCouldBeConstructedWithCustomConfiguration() + { + $factory = new SnsConnectionFactory(['key' => 'theKey']); + + $this->assertAttributeEquals([ + 'lazy' => true, + 'key' => 'theKey', + 'secret' => null, + 'token' => null, + 'region' => null, + 'version' => '2010-03-31', + 'endpoint' => null, + 'topic_arns' => [], + 'http' => [], + ], 'config', $factory); + } + + public function testCouldBeConstructedWithClient() + { + $awsClient = $this->createMock(AwsSnsClient::class); + + $factory = new SnsConnectionFactory($awsClient); + + $context = $factory->createContext(); + + $this->assertInstanceOf(SnsContext::class, $context); + + $client = $this->readAttribute($context, 'client'); + $this->assertInstanceOf(SnsClient::class, $client); + $this->assertAttributeSame($awsClient, 'inputClient', $client); + } + + public function testShouldCreateLazyContext() + { + $factory = new SnsConnectionFactory(['lazy' => true]); + + $context = $factory->createContext(); + + $this->assertInstanceOf(SnsContext::class, $context); + + $client = $this->readAttribute($context, 'client'); + $this->assertInstanceOf(SnsClient::class, $client); + $this->assertAttributeInstanceOf(\Closure::class, 'inputClient', $client); + } +} diff --git a/pkg/sns/Tests/SnsDestinationTest.php b/pkg/sns/Tests/SnsDestinationTest.php new file mode 100644 index 000000000..c9f9669e7 --- /dev/null +++ b/pkg/sns/Tests/SnsDestinationTest.php @@ -0,0 +1,52 @@ +assertClassImplements(Topic::class, SnsDestination::class); + $this->assertClassImplements(Queue::class, SnsDestination::class); + } + + public function testShouldReturnNameSetInConstructor() + { + $destination = new SnsDestination('aDestinationName'); + + $this->assertSame('aDestinationName', $destination->getQueueName()); + $this->assertSame('aDestinationName', $destination->getTopicName()); + } + + public function testCouldSetPolicyAttribute() + { + $destination = new SnsDestination('aDestinationName'); + $destination->setPolicy('thePolicy'); + + $this->assertSame(['Policy' => 'thePolicy'], $destination->getAttributes()); + } + + public function testCouldSetDisplayNameAttribute() + { + $destination = new SnsDestination('aDestinationName'); + $destination->setDisplayName('theDisplayName'); + + $this->assertSame(['DisplayName' => 'theDisplayName'], $destination->getAttributes()); + } + + public function testCouldSetDeliveryPolicyAttribute() + { + $destination = new SnsDestination('aDestinationName'); + $destination->setDeliveryPolicy(123); + + $this->assertSame(['DeliveryPolicy' => 123], $destination->getAttributes()); + } +} diff --git a/pkg/sns/Tests/SnsProducerTest.php b/pkg/sns/Tests/SnsProducerTest.php new file mode 100644 index 000000000..1c6be7f85 --- /dev/null +++ b/pkg/sns/Tests/SnsProducerTest.php @@ -0,0 +1,245 @@ +assertClassImplements(Producer::class, SnsProducer::class); + } + + public function testShouldThrowIfBodyOfInvalidType() + { + $this->expectException(InvalidMessageException::class); + $this->expectExceptionMessage('The message body must be a non-empty string.'); + + $producer = new SnsProducer($this->createSnsContextMock()); + + $message = new SnsMessage(''); + + $producer->send(new SnsDestination(''), $message); + } + + public function testShouldThrowIfDestinationOfInvalidType() + { + $this->expectException(InvalidDestinationException::class); + $this->expectExceptionMessage('The destination must be an instance of Enqueue\Sns\SnsDestination but got Mock_Destinat'); + + $producer = new SnsProducer($this->createSnsContextMock()); + + $producer->send($this->createMock(Destination::class), new SnsMessage()); + } + + public function testShouldThrowIfPublishFailed() + { + $destination = new SnsDestination('queue-name'); + + $client = $this->createSnsClientMock(); + $client + ->expects($this->once()) + ->method('publish') + ->willReturn(new Result()) + ; + + $context = $this->createSnsContextMock(); + $context + ->expects($this->once()) + ->method('getTopicArn') + ->with($this->identicalTo($destination)) + ->willReturn('theTopicArn') + ; + $context + ->expects($this->once()) + ->method('getSnsClient') + ->willReturn($client) + ; + + $message = new SnsMessage('foo'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Message was not sent'); + + $producer = new SnsProducer($context); + $producer->send($destination, $message); + } + + public function testShouldThrowIfsetTimeToLiveIsNotNull() + { + $this->expectException(TimeToLiveNotSupportedException::class); + + $producer = new SnsProducer($this->createSnsContextMock()); + $result = $producer->setTimeToLive(); + + $this->assertInstanceOf(SnsProducer::class, $result); + + $this->expectExceptionMessage('The provider does not support time to live feature'); + + $producer->setTimeToLive(200); + } + + public function testShouldThrowIfsetPriorityIsNotNull() + { + $this->expectException(PriorityNotSupportedException::class); + + $producer = new SnsProducer($this->createSnsContextMock()); + $result = $producer->setPriority(); + + $this->assertInstanceOf(SnsProducer::class, $result); + + $this->expectExceptionMessage('The provider does not support priority feature'); + + $producer->setPriority(200); + } + + public function testShouldThrowIfsetDeliveryDelayIsNotNull() + { + $this->expectException(DeliveryDelayNotSupportedException::class); + + $producer = new SnsProducer($this->createSnsContextMock()); + $result = $producer->setDeliveryDelay(); + + $this->assertInstanceOf(SnsProducer::class, $result); + + $this->expectExceptionMessage('The provider does not support delivery delay feature'); + + $producer->setDeliveryDelay(200); + } + + public function testShouldPublish() + { + $destination = new SnsDestination('queue-name'); + + $expectedArguments = [ + 'Message' => 'theBody', + 'MessageAttributes' => [ + 'Headers' => [ + 'DataType' => 'String', + 'StringValue' => '[{"hkey":"hvaleu"},{"key":"value"}]', + ], + ], + 'TopicArn' => 'theTopicArn', + ]; + + $client = $this->createSnsClientMock(); + $client + ->expects($this->once()) + ->method('publish') + ->with($this->identicalTo($expectedArguments)) + ->willReturn(new Result(['MessageId' => 'theMessageId'])) + ; + + $context = $this->createSnsContextMock(); + $context + ->expects($this->once()) + ->method('getTopicArn') + ->with($this->identicalTo($destination)) + ->willReturn('theTopicArn') + ; + $context + ->expects($this->once()) + ->method('getSnsClient') + ->willReturn($client) + ; + + $message = new SnsMessage('theBody', ['key' => 'value'], ['hkey' => 'hvaleu']); + + $producer = new SnsProducer($context); + $producer->send($destination, $message); + } + + /** + * @throws InvalidMessageException + */ + public function testShouldPublishWithMergedAttributes() + { + $context = $this->createSnsContextMock(); + $client = $this->createSnsClientMock(); + + $context + ->expects($this->once()) + ->method('getSnsClient') + ->willReturn($client); + + $expectedArgument = [ + 'Message' => 'message', + 'MessageAttributes' => [ + 'Headers' => [ + 'DataType' => 'String', + 'StringValue' => '[[],[]]', + ], + 'Foo' => [ + 'DataType' => 'String', + 'StringValue' => 'foo-value', + ], + 'Bar' => [ + 'DataType' => 'Binary', + 'BinaryValue' => 'bar-val', + ], + ], + 'TopicArn' => '', + 'MessageStructure' => 'structure', + 'PhoneNumber' => 'phone', + 'Subject' => 'subject', + 'TargetArn' => 'target_arn', + ]; + + $client + ->expects($this->once()) + ->method('publish') + ->with($this->identicalTo($expectedArgument)) + ->willReturn(new Result(['MessageId' => 'theMessageId'])); + + $attributes = [ + 'Foo' => [ + 'DataType' => 'String', + 'StringValue' => 'foo-value', + ], + ]; + + $message = new SnsMessage( + 'message', [], [], $attributes, 'structure', 'phone', + 'subject', 'target_arn' + ); + $message->addAttribute('Bar', 'Binary', 'bar-val'); + + $destination = new SnsDestination('queue-name'); + + $producer = new SnsProducer($context); + $producer->send($destination, $message); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|SnsContext + */ + private function createSnsContextMock(): SnsContext + { + return $this->createMock(SnsContext::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|SnsClient + */ + private function createSnsClientMock(): SnsClient + { + return $this->createMock(SnsClient::class); + } +} diff --git a/pkg/sns/Tests/Spec/SnsConnectionFactoryTest.php b/pkg/sns/Tests/Spec/SnsConnectionFactoryTest.php new file mode 100644 index 000000000..20008bc04 --- /dev/null +++ b/pkg/sns/Tests/Spec/SnsConnectionFactoryTest.php @@ -0,0 +1,18 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage('SNS transport does not support consumption. You should consider using SQS instead.'); + + parent::testShouldCreateConsumerOnCreateConsumerMethodCall(); + } + + public function testSetsSubscriptionAttributes(): void + { + $client = $this->createMock(SnsClient::class); + $client->expects($this->once()) + ->method('listSubscriptionsByTopic') + ->willReturn(new Result(['Subscriptions' => [ + ['SubscriptionArn' => 'arn1'], + ['SubscriptionArn' => 'arn2'], + ]])); + $client->expects($this->exactly(2)) + ->method('setSubscriptionAttributes') + ->withConsecutive( + [$this->equalTo(['attr1' => 'value1', 'SubscriptionArn' => 'arn1'])], + [$this->equalTo(['attr1' => 'value1', 'SubscriptionArn' => 'arn2'])], + ); + + $context = new SnsContext($client, ['topic_arns' => ['topic1' => 'topicArn1']]); + $context->setSubscriptionAttributes(new SnsSubscribe( + new SnsDestination('topic1'), + 'endpoint1', + 'protocol1', + false, + ['attr1' => 'value1'], + )); + } + + protected function createContext() + { + $client = $this->createMock(SnsClient::class); + + return new SnsContext($client, ['topic_arns' => []]); + } +} diff --git a/pkg/sns/Tests/Spec/SnsMessageTest.php b/pkg/sns/Tests/Spec/SnsMessageTest.php new file mode 100644 index 000000000..af24344e6 --- /dev/null +++ b/pkg/sns/Tests/Spec/SnsMessageTest.php @@ -0,0 +1,14 @@ +createMock(SnsContext::class)); + } +} diff --git a/pkg/sns/Tests/Spec/SnsQueueTest.php b/pkg/sns/Tests/Spec/SnsQueueTest.php new file mode 100644 index 000000000..39c0e5513 --- /dev/null +++ b/pkg/sns/Tests/Spec/SnsQueueTest.php @@ -0,0 +1,14 @@ +createContext(); + +$queue = $context->createQueue('enqueue'); +$consumer = $context->createConsumer($queue); + +while (true) { + if ($m = $consumer->receive(20000)) { + $consumer->acknowledge($m); + echo 'Received message: '.$m->getBody().\PHP_EOL; + } +} + +echo 'Done'."\n"; diff --git a/pkg/sns/examples/produce.php b/pkg/sns/examples/produce.php new file mode 100644 index 000000000..3e59c5232 --- /dev/null +++ b/pkg/sns/examples/produce.php @@ -0,0 +1,34 @@ + getenv('ENQUEUE_AWS__SQS__KEY'), + 'secret' => getenv('ENQUEUE_AWS__SQS__SECRET'), + 'region' => getenv('ENQUEUE_AWS__SQS__REGION'), +]); +$context = $factory->createContext(); + +$topic = $context->createTopic('test_enqueue'); +$context->declareTopic($topic); + +$message = $context->createMessage('a_body'); +$message->setProperty('aProp', 'aPropVal'); +$message->setHeader('aHeader', 'aHeaderVal'); + +$context->createProducer()->send($topic, $message); diff --git a/pkg/sns/phpunit.xml.dist b/pkg/sns/phpunit.xml.dist new file mode 100644 index 000000000..5f01f5897 --- /dev/null +++ b/pkg/sns/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + + ./Tests + + + + + + . + + ./vendor + ./Tests + + + + diff --git a/pkg/snsqs/.gitattributes b/pkg/snsqs/.gitattributes new file mode 100644 index 000000000..bdf2dcb14 --- /dev/null +++ b/pkg/snsqs/.gitattributes @@ -0,0 +1,5 @@ +/Tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +phpunit.xml.dist export-ignore diff --git a/pkg/snsqs/.github/workflows/ci.yml b/pkg/snsqs/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/snsqs/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/snsqs/.gitignore b/pkg/snsqs/.gitignore new file mode 100644 index 000000000..a770439e5 --- /dev/null +++ b/pkg/snsqs/.gitignore @@ -0,0 +1,6 @@ +*~ +/composer.lock +/composer.phar +/phpunit.xml +/vendor/ +/.idea/ diff --git a/pkg/snsqs/LICENSE b/pkg/snsqs/LICENSE new file mode 100644 index 000000000..20211e5fd --- /dev/null +++ b/pkg/snsqs/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright (c) 2018 Max Kotliar + +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/pkg/snsqs/README.md b/pkg/snsqs/README.md new file mode 100644 index 000000000..94a22776d --- /dev/null +++ b/pkg/snsqs/README.md @@ -0,0 +1,28 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + +# Amazon SNS-SQS Transport + +[![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/snsqs/ci.yml?branch=master)](https://github.com/php-enqueue/snsqs/actions?query=workflow%3ACI) +[![Total Downloads](https://poser.pugx.org/enqueue/snsqs/d/total.png)](https://packagist.org/packages/enqueue/snsqs) +[![Latest Stable Version](https://poser.pugx.org/enqueue/snsqs/version.png)](https://packagist.org/packages/enqueue/snsqs) + +This is an implementation of Queue Interop specification. It allows you to send and consume message using [Amazon SNS-SQS](https://aws.amazon.com/snsqs/) service. + +## Resources + +* [Site](https://enqueue.forma-pro.com/) +* [Documentation](https://php-enqueue.github.io/transport/snsqs/) +* [Questions](https://gitter.im/php-enqueue/Lobby) +* [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) + +## License + +It is released under the [MIT License](LICENSE). diff --git a/pkg/snsqs/SnsQsConnectionFactory.php b/pkg/snsqs/SnsQsConnectionFactory.php new file mode 100644 index 000000000..65812beb3 --- /dev/null +++ b/pkg/snsqs/SnsQsConnectionFactory.php @@ -0,0 +1,114 @@ + null AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. + * 'secret' => null, AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. + * 'token' => null, AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. + * 'region' => null, (string, required) Region to connect to. See http://docs.aws.amazon.com/general/latest/gr/rande.html for a list of available regions. + * 'version' => '2012-11-05', (string, required) The version of the webservice to utilize + * 'lazy' => true, Enable lazy connection (boolean) + * 'endpoint' => null (string, default=null) The full URI of the webservice. This is only required when connecting to a custom endpoint e.g. localstack + * ]. + * + * or + * + * $config = [ + * 'sns_key' => null, SNS option + * 'sqs_secret' => null, SQS option + * 'token' Option for both SNS and SQS + * ]. + * + * or + * + * snsqs: + * snsqs:?key=aKey&secret=aSecret&sns_token=aSnsToken&sqs_token=aSqsToken + * + * @param array|string|null $config + */ + public function __construct($config = 'snsqs:') + { + if (empty($config)) { + $this->snsConfig = []; + $this->sqsConfig = []; + } elseif (is_string($config)) { + $this->parseDsn($config); + } elseif (is_array($config)) { + if (array_key_exists('dsn', $config)) { + $this->parseDsn($config['dsn']); + } else { + $this->parseOptions($config); + } + } else { + throw new \LogicException('The config must be either an array of options, a DSN string or null'); + } + } + + /** + * @return SnsQsContext + */ + public function createContext(): Context + { + return new SnsQsContext(function () { + return (new SnsConnectionFactory($this->snsConfig))->createContext(); + }, function () { + return (new SqsConnectionFactory($this->sqsConfig))->createContext(); + }); + } + + private function parseDsn(string $dsn): void + { + $dsn = Dsn::parseFirst($dsn); + + if ('snsqs' !== $dsn->getSchemeProtocol()) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "snsqs"', $dsn->getSchemeProtocol())); + } + + $this->parseOptions($dsn->getQuery()); + } + + private function parseOptions(array $options): void + { + // set default options + foreach ($options as $key => $value) { + if (false === in_array(substr($key, 0, 4), ['sns_', 'sqs_'], true)) { + $this->snsConfig[$key] = $value; + $this->sqsConfig[$key] = $value; + } + } + + // set transport specific options + foreach ($options as $key => $value) { + switch (substr($key, 0, 4)) { + case 'sns_': + $this->snsConfig[substr($key, 4)] = $value; + break; + case 'sqs_': + $this->sqsConfig[substr($key, 4)] = $value; + break; + } + } + } +} diff --git a/pkg/snsqs/SnsQsConsumer.php b/pkg/snsqs/SnsQsConsumer.php new file mode 100644 index 000000000..45237d145 --- /dev/null +++ b/pkg/snsqs/SnsQsConsumer.php @@ -0,0 +1,143 @@ +context = $context; + $this->consumer = $consumer; + $this->queue = $queue; + } + + public function getVisibilityTimeout(): ?int + { + return $this->consumer->getVisibilityTimeout(); + } + + /** + * The duration (in seconds) that the received messages are hidden from subsequent retrieve + * requests after being retrieved by a ReceiveMessage request. + */ + public function setVisibilityTimeout(?int $visibilityTimeout = null): void + { + $this->consumer->setVisibilityTimeout($visibilityTimeout); + } + + public function getMaxNumberOfMessages(): int + { + return $this->consumer->getMaxNumberOfMessages(); + } + + /** + * The maximum number of messages to return. Amazon SQS never returns more messages than this value + * (however, fewer messages might be returned). Valid values are 1 to 10. Default is 1. + */ + public function setMaxNumberOfMessages(int $maxNumberOfMessages): void + { + $this->consumer->setMaxNumberOfMessages($maxNumberOfMessages); + } + + public function getQueue(): Queue + { + return $this->queue; + } + + public function receive(int $timeout = 0): ?Message + { + if ($sqsMessage = $this->consumer->receive($timeout)) { + return $this->convertMessage($sqsMessage); + } + + return null; + } + + public function receiveNoWait(): ?Message + { + if ($sqsMessage = $this->consumer->receiveNoWait()) { + return $this->convertMessage($sqsMessage); + } + + return null; + } + + /** + * @param SnsQsMessage $message + */ + public function acknowledge(Message $message): void + { + InvalidMessageException::assertMessageInstanceOf($message, SnsQsMessage::class); + + $this->consumer->acknowledge($message->getSqsMessage()); + } + + /** + * @param SnsQsMessage $message + */ + public function reject(Message $message, bool $requeue = false): void + { + InvalidMessageException::assertMessageInstanceOf($message, SnsQsMessage::class); + + $this->consumer->reject($message->getSqsMessage(), $requeue); + } + + private function convertMessage(SqsMessage $sqsMessage): SnsQsMessage + { + $message = $this->context->createMessage(); + $message->setRedelivered($sqsMessage->isRedelivered()); + $message->setSqsMessage($sqsMessage); + + $body = $sqsMessage->getBody(); + + if (isset($body[0]) && '{' === $body[0]) { + $data = json_decode($sqsMessage->getBody(), true); + + if (isset($data['TopicArn']) && isset($data['Type']) && 'Notification' === $data['Type']) { + // SNS message conversion + if (isset($data['Message'])) { + $message->setBody((string) $data['Message']); + } + + if (isset($data['MessageAttributes']['Headers'])) { + $headersData = json_decode($data['MessageAttributes']['Headers']['Value'], true); + + $message->setHeaders($headersData[0]); + $message->setProperties($headersData[1]); + } + + return $message; + } + } + + $message->setBody($sqsMessage->getBody()); + $message->setHeaders($sqsMessage->getHeaders()); + $message->setProperties($sqsMessage->getProperties()); + + return $message; + } +} diff --git a/pkg/snsqs/SnsQsContext.php b/pkg/snsqs/SnsQsContext.php new file mode 100644 index 000000000..d26a0fc6d --- /dev/null +++ b/pkg/snsqs/SnsQsContext.php @@ -0,0 +1,214 @@ +snsContext = $snsContext; + } elseif (is_callable($snsContext)) { + $this->snsContextFactory = $snsContext; + } else { + throw new \InvalidArgumentException(sprintf('The $snsContext argument must be either %s or callable that returns %s once called.', SnsContext::class, SnsContext::class)); + } + + if ($sqsContext instanceof SqsContext) { + $this->sqsContext = $sqsContext; + } elseif (is_callable($sqsContext)) { + $this->sqsContextFactory = $sqsContext; + } else { + throw new \InvalidArgumentException(sprintf('The $sqsContext argument must be either %s or callable that returns %s once called.', SqsContext::class, SqsContext::class)); + } + } + + /** + * @return SnsQsMessage + */ + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message + { + return new SnsQsMessage($body, $properties, $headers); + } + + /** + * @return SnsQsTopic + */ + public function createTopic(string $topicName): Topic + { + return new SnsQsTopic($topicName); + } + + /** + * @return SnsQsQueue + */ + public function createQueue(string $queueName): Queue + { + return new SnsQsQueue($queueName); + } + + public function createTemporaryQueue(): Queue + { + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); + } + + public function createProducer(): Producer + { + return new SnsQsProducer($this->getSnsContext(), $this->getSqsContext()); + } + + /** + * @param SnsQsQueue $destination + */ + public function createConsumer(Destination $destination): Consumer + { + InvalidDestinationException::assertDestinationInstanceOf($destination, SnsQsQueue::class); + + return new SnsQsConsumer($this, $this->getSqsContext()->createConsumer($destination), $destination); + } + + /** + * @param SnsQsQueue $queue + */ + public function purgeQueue(Queue $queue): void + { + InvalidDestinationException::assertDestinationInstanceOf($queue, SnsQsQueue::class); + + $this->getSqsContext()->purgeQueue($queue); + } + + public function createSubscriptionConsumer(): SubscriptionConsumer + { + throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); + } + + public function declareTopic(SnsQsTopic $topic): void + { + $this->getSnsContext()->declareTopic($topic); + } + + public function setTopicArn(SnsQsTopic $topic, string $arn): void + { + $this->getSnsContext()->setTopicArn($topic, $arn); + } + + public function deleteTopic(SnsQsTopic $topic): void + { + $this->getSnsContext()->deleteTopic($topic); + } + + public function declareQueue(SnsQsQueue $queue): void + { + $this->getSqsContext()->declareQueue($queue); + } + + public function deleteQueue(SnsQsQueue $queue): void + { + $this->getSqsContext()->deleteQueue($queue); + } + + public function bind(SnsQsTopic $topic, SnsQsQueue $queue): void + { + $this->getSnsContext()->subscribe(new SnsSubscribe( + $topic, + $this->getSqsContext()->getQueueArn($queue), + SnsSubscribe::PROTOCOL_SQS + )); + } + + public function unbind(SnsQsTopic $topic, SnsQsQueue $queue): void + { + $this->getSnsContext()->unsubscibe(new SnsUnsubscribe( + $topic, + $this->getSqsContext()->getQueueArn($queue), + SnsSubscribe::PROTOCOL_SQS + )); + } + + public function close(): void + { + $this->getSnsContext()->close(); + $this->getSqsContext()->close(); + } + + public function setSubscriptionAttributes(SnsQsTopic $topic, SnsQsQueue $queue, array $attributes): void + { + $this->getSnsContext()->setSubscriptionAttributes(new SnsSubscribe( + $topic, + $this->getSqsContext()->getQueueArn($queue), + SnsSubscribe::PROTOCOL_SQS, + false, + $attributes, + )); + } + + private function getSnsContext(): SnsContext + { + if (null === $this->snsContext) { + $context = call_user_func($this->snsContextFactory); + if (false == $context instanceof SnsContext) { + throw new \LogicException(sprintf('The factory must return instance of %s. It returned %s', SnsContext::class, is_object($context) ? $context::class : gettype($context))); + } + + $this->snsContext = $context; + } + + return $this->snsContext; + } + + private function getSqsContext(): SqsContext + { + if (null === $this->sqsContext) { + $context = call_user_func($this->sqsContextFactory); + if (false == $context instanceof SqsContext) { + throw new \LogicException(sprintf('The factory must return instance of %s. It returned %s', SqsContext::class, is_object($context) ? $context::class : gettype($context))); + } + + $this->sqsContext = $context; + } + + return $this->sqsContext; + } +} diff --git a/pkg/snsqs/SnsQsMessage.php b/pkg/snsqs/SnsQsMessage.php new file mode 100644 index 000000000..900ad9125 --- /dev/null +++ b/pkg/snsqs/SnsQsMessage.php @@ -0,0 +1,108 @@ +body = $body; + $this->properties = $properties; + $this->headers = $headers; + $this->redelivered = false; + $this->messageAttributes = $messageAttributes; + } + + public function setSqsMessage(SqsMessage $message): void + { + $this->sqsMessage = $message; + } + + public function getSqsMessage(): SqsMessage + { + return $this->sqsMessage; + } + + public function getMessageAttributes(): ?array + { + return $this->messageAttributes; + } + + public function setMessageAttributes(?array $messageAttributes): void + { + $this->messageAttributes = $messageAttributes; + } + + /** + * Only FIFO. + * + * The token used for deduplication of sent messages. If a message with a particular MessageDeduplicationId is sent successfully, + * any messages sent with the same MessageDeduplicationId are accepted successfully but aren't delivered during the 5-minute + * deduplication interval. For more information, see http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html#FIFO-queues-exactly-once-processing. + */ + public function setMessageDeduplicationId(?string $id = null): void + { + $this->messageDeduplicationId = $id; + } + + public function getMessageDeduplicationId(): ?string + { + return $this->messageDeduplicationId; + } + + /** + * Only FIFO. + * + * The tag that specifies that a message belongs to a specific message group. Messages that belong to the same message group + * are processed in a FIFO manner (however, messages in different message groups might be processed out of order). + * To interleave multiple ordered streams within a single queue, use MessageGroupId values (for example, session data + * for multiple users). In this scenario, multiple readers can process the queue, but the session data + * of each user is processed in a FIFO fashion. + */ + public function setMessageGroupId(?string $id = null): void + { + $this->messageGroupId = $id; + } + + public function getMessageGroupId(): ?string + { + return $this->messageGroupId; + } +} diff --git a/pkg/snsqs/SnsQsProducer.php b/pkg/snsqs/SnsQsProducer.php new file mode 100644 index 000000000..a80e1eb2b --- /dev/null +++ b/pkg/snsqs/SnsQsProducer.php @@ -0,0 +1,143 @@ +snsContext = $snsContext; + $this->sqsContext = $sqsContext; + } + + /** + * @param SnsQsTopic $destination + * @param SnsQsMessage $message + */ + public function send(Destination $destination, Message $message): void + { + InvalidMessageException::assertMessageInstanceOf($message, SnsQsMessage::class); + + if (false == $destination instanceof SnsQsTopic && false == $destination instanceof SnsQsQueue) { + throw new InvalidDestinationException(sprintf('The destination must be an instance of [%s|%s] but got %s.', SnsQsTopic::class, SnsQsQueue::class, is_object($destination) ? $destination::class : gettype($destination))); + } + + if ($destination instanceof SnsQsTopic) { + $snsMessage = $this->snsContext->createMessage( + $message->getBody(), + $message->getProperties(), + $message->getHeaders() + ); + $snsMessage->setMessageAttributes($message->getMessageAttributes()); + $snsMessage->setMessageGroupId($message->getMessageGroupId()); + $snsMessage->setMessageDeduplicationId($message->getMessageDeduplicationId()); + + $this->getSnsProducer()->send($destination, $snsMessage); + } else { + $sqsMessage = $this->sqsContext->createMessage( + $message->getBody(), + $message->getProperties(), + $message->getHeaders() + ); + + $sqsMessage->setMessageGroupId($message->getMessageGroupId()); + $sqsMessage->setMessageDeduplicationId($message->getMessageDeduplicationId()); + + $this->getSqsProducer()->send($destination, $sqsMessage); + } + } + + /** + * Delivery delay is supported by SQSProducer. + */ + public function setDeliveryDelay(?int $deliveryDelay = null): Producer + { + $this->getSqsProducer()->setDeliveryDelay($deliveryDelay); + + return $this; + } + + /** + * Delivery delay is supported by SQSProducer. + */ + public function getDeliveryDelay(): ?int + { + return $this->getSqsProducer()->getDeliveryDelay(); + } + + public function setPriority(?int $priority = null): Producer + { + $this->getSnsProducer()->setPriority($priority); + $this->getSqsProducer()->setPriority($priority); + + return $this; + } + + public function getPriority(): ?int + { + return $this->getSnsProducer()->getPriority(); + } + + public function setTimeToLive(?int $timeToLive = null): Producer + { + $this->getSnsProducer()->setTimeToLive($timeToLive); + $this->getSqsProducer()->setTimeToLive($timeToLive); + + return $this; + } + + public function getTimeToLive(): ?int + { + return $this->getSnsProducer()->getTimeToLive(); + } + + private function getSnsProducer(): SnsProducer + { + if (null === $this->snsProducer) { + $this->snsProducer = $this->snsContext->createProducer(); + } + + return $this->snsProducer; + } + + private function getSqsProducer(): SqsProducer + { + if (null === $this->sqsProducer) { + $this->sqsProducer = $this->sqsContext->createProducer(); + } + + return $this->sqsProducer; + } +} diff --git a/pkg/snsqs/SnsQsQueue.php b/pkg/snsqs/SnsQsQueue.php new file mode 100644 index 000000000..92c3a542b --- /dev/null +++ b/pkg/snsqs/SnsQsQueue.php @@ -0,0 +1,11 @@ +createMock(SnsQsContext::class); + $context->expects($this->once()) + ->method('createMessage') + ->willReturn(new SnsQsMessage()); + + $sqsConsumer = $this->createMock(SqsConsumer::class); + $sqsConsumer->expects($this->once()) + ->method('receive') + ->willReturn(new SqsMessage(json_encode([ + 'Type' => 'Notification', + 'TopicArn' => 'arn:aws:sns:us-east-2:12345:topic-name', + 'Message' => 'The Body', + 'MessageAttributes' => [ + 'Headers' => [ + 'Type' => 'String', + 'Value' => '[{"headerKey":"headerVal"},{"propKey": "propVal"}]', + ], + ], + ]))); + + $consumer = new SnsQsConsumer($context, $sqsConsumer, new SnsQsQueue('queue')); + $result = $consumer->receive(); + + $this->assertInstanceOf(SnsQsMessage::class, $result); + $this->assertSame('The Body', $result->getBody()); + $this->assertSame(['headerKey' => 'headerVal'], $result->getHeaders()); + $this->assertSame(['propKey' => 'propVal'], $result->getProperties()); + } + + public function testReceivesSqsMessage(): void + { + $context = $this->createMock(SnsQsContext::class); + $context->expects($this->once()) + ->method('createMessage') + ->willReturn(new SnsQsMessage()); + + $sqsConsumer = $this->createMock(SqsConsumer::class); + $sqsConsumer->expects($this->once()) + ->method('receive') + ->willReturn(new SqsMessage( + 'The Body', + ['propKey' => 'propVal'], + ['headerKey' => 'headerVal'], + )); + + $consumer = new SnsQsConsumer($context, $sqsConsumer, new SnsQsQueue('queue')); + $result = $consumer->receive(); + + $this->assertInstanceOf(SnsQsMessage::class, $result); + $this->assertSame('The Body', $result->getBody()); + $this->assertSame(['headerKey' => 'headerVal'], $result->getHeaders()); + $this->assertSame(['propKey' => 'propVal'], $result->getProperties()); + } +} diff --git a/pkg/snsqs/Tests/SnsQsProducerTest.php b/pkg/snsqs/Tests/SnsQsProducerTest.php new file mode 100644 index 000000000..59798dc11 --- /dev/null +++ b/pkg/snsqs/Tests/SnsQsProducerTest.php @@ -0,0 +1,203 @@ +assertClassImplements(Producer::class, SnsQsProducer::class); + } + + public function testShouldThrowIfMessageIsInvalidType() + { + $this->expectException(InvalidMessageException::class); + $this->expectExceptionMessage('The message must be an instance of Enqueue\SnsQs\SnsQsMessage but it is Double\Message\P4'); + + $producer = new SnsQsProducer($this->createSnsContextMock(), $this->createSqsContextMock()); + + $message = $this->prophesize(Message::class)->reveal(); + + $producer->send(new SnsQsTopic(''), $message); + } + + public function testShouldThrowIfDestinationOfInvalidType() + { + $this->expectException(InvalidDestinationException::class); + + $producer = new SnsQsProducer($this->createSnsContextMock(), $this->createSqsContextMock()); + + $destination = $this->prophesize(Destination::class)->reveal(); + + $producer->send($destination, new SnsQsMessage()); + } + + public function testShouldSetDeliveryDelayToSQSProducer() + { + $delay = 10; + + $sqsProducerStub = $this->prophesize(SqsProducer::class); + $sqsProducerStub->setDeliveryDelay(Argument::is($delay))->shouldBeCalledTimes(1); + + $sqsMock = $this->createSqsContextMock(); + $sqsMock->method('createProducer')->willReturn($sqsProducerStub->reveal()); + + $producer = new SnsQsProducer($this->createSnsContextMock(), $sqsMock); + + $producer->setDeliveryDelay($delay); + } + + public function testShouldGetDeliveryDelayFromSQSProducer() + { + $delay = 10; + + $sqsProducerStub = $this->prophesize(SqsProducer::class); + $sqsProducerStub->getDeliveryDelay()->willReturn($delay); + + $sqsMock = $this->createSqsContextMock(); + $sqsMock->method('createProducer')->willReturn($sqsProducerStub->reveal()); + + $producer = new SnsQsProducer($this->createSnsContextMock(), $sqsMock); + + $this->assertEquals($delay, $producer->getDeliveryDelay()); + } + + public function testShouldSendSnsTopicMessageToSnsProducer() + { + $snsMock = $this->createSnsContextMock(); + $snsMock->method('createMessage')->willReturn(new SnsMessage()); + $destination = new SnsQsTopic(''); + + $snsProducerStub = $this->prophesize(SnsProducer::class); + $snsProducerStub->send($destination, Argument::any())->shouldBeCalledOnce(); + + $snsMock->method('createProducer')->willReturn($snsProducerStub->reveal()); + + $producer = new SnsQsProducer($snsMock, $this->createSqsContextMock()); + $producer->send($destination, new SnsQsMessage()); + } + + public function testShouldSendSnsTopicMessageWithAttributesToSnsProducer() + { + $snsMock = $this->createSnsContextMock(); + $snsMock->method('createMessage')->willReturn(new SnsMessage()); + $destination = new SnsQsTopic(''); + + $snsProducerStub = $this->prophesize(SnsProducer::class); + $snsProducerStub->send( + $destination, + Argument::that(function (SnsMessage $snsMessage) { + return $snsMessage->getMessageAttributes() === ['foo' => 'bar']; + }) + )->shouldBeCalledOnce(); + + $snsMock->method('createProducer')->willReturn($snsProducerStub->reveal()); + + $producer = new SnsQsProducer($snsMock, $this->createSqsContextMock()); + $producer->send($destination, new SnsQsMessage('', [], [], ['foo' => 'bar'])); + } + + public function testShouldSendToSnsTopicMessageWithGroupIdAndDeduplicationId() + { + $snsMock = $this->createSnsContextMock(); + $snsMock->method('createMessage')->willReturn(new SnsMessage()); + $destination = new SnsQsTopic(''); + + $snsProducerStub = $this->prophesize(SnsProducer::class); + $snsProducerStub->send( + $destination, + Argument::that(function (SnsMessage $snsMessage) { + return 'group-id' === $snsMessage->getMessageGroupId() + && 'deduplication-id' === $snsMessage->getMessageDeduplicationId(); + }) + )->shouldBeCalledOnce(); + + $snsMock->method('createProducer')->willReturn($snsProducerStub->reveal()); + + $snsMessage = new SnsQsMessage(); + $snsMessage->setMessageGroupId('group-id'); + $snsMessage->setMessageDeduplicationId('deduplication-id'); + + $producer = new SnsQsProducer($snsMock, $this->createSqsContextMock()); + $producer->send($destination, $snsMessage); + } + + public function testShouldSendSqsMessageToSqsProducer() + { + $sqsMock = $this->createSqsContextMock(); + $sqsMock->method('createMessage')->willReturn(new SqsMessage()); + $destination = new SnsQsQueue(''); + + $sqsProducerStub = $this->prophesize(SqsProducer::class); + $sqsProducerStub->send($destination, Argument::any())->shouldBeCalledOnce(); + + $sqsMock->method('createProducer')->willReturn($sqsProducerStub->reveal()); + + $producer = new SnsQsProducer($this->createSnsContextMock(), $sqsMock); + $producer->send($destination, new SnsQsMessage()); + } + + public function testShouldSendToSqsProducerMessageWithGroupIdAndDeduplicationId() + { + $sqsMock = $this->createSqsContextMock(); + $sqsMock->method('createMessage')->willReturn(new SqsMessage()); + $destination = new SnsQsQueue(''); + + $sqsProducerStub = $this->prophesize(SqsProducer::class); + $sqsProducerStub->send( + $destination, + Argument::that(function (SqsMessage $sqsMessage) { + return 'group-id' === $sqsMessage->getMessageGroupId() + && 'deduplication-id' === $sqsMessage->getMessageDeduplicationId(); + }) + )->shouldBeCalledOnce(); + + $sqsMock->method('createProducer')->willReturn($sqsProducerStub->reveal()); + + $sqsMessage = new SnsQsMessage(); + $sqsMessage->setMessageGroupId('group-id'); + $sqsMessage->setMessageDeduplicationId('deduplication-id'); + + $producer = new SnsQsProducer($this->createSnsContextMock(), $sqsMock); + $producer->send($destination, $sqsMessage); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|SnsContext + */ + private function createSnsContextMock(): SnsContext + { + return $this->createMock(SnsContext::class); + } + + /** + * @return \PHPUnit\Framework\MockObject\MockObject|SqsContext + */ + private function createSqsContextMock(): SqsContext + { + return $this->createMock(SqsContext::class); + } +} diff --git a/pkg/snsqs/Tests/Spec/SnsQsConnectionFactoryTest.php b/pkg/snsqs/Tests/Spec/SnsQsConnectionFactoryTest.php new file mode 100644 index 000000000..f00c350da --- /dev/null +++ b/pkg/snsqs/Tests/Spec/SnsQsConnectionFactoryTest.php @@ -0,0 +1,18 @@ +createMock(SnsContext::class); + $snsContext->expects($this->once()) + ->method('setSubscriptionAttributes') + ->with($this->equalTo(new SnsSubscribe( + $topic, + 'queueArn1', + 'sqs', + false, + ['attr1' => 'value1'], + ))); + + $sqsContext = $this->createMock(SqsContext::class); + $sqsContext->expects($this->any()) + ->method('createConsumer') + ->willReturn($this->createMock(SqsConsumer::class)); + $sqsContext->expects($this->any()) + ->method('getQueueArn') + ->willReturn('queueArn1'); + + $context = new SnsQsContext($snsContext, $sqsContext); + $context->setSubscriptionAttributes( + $topic, + new SnsQsQueue('queue1'), + ['attr1' => 'value1'], + ); + } + + protected function createContext() + { + $sqsContext = $this->createMock(SqsContext::class); + $sqsContext + ->expects($this->any()) + ->method('createConsumer') + ->willReturn($this->createMock(SqsConsumer::class)) + ; + + return new SnsQsContext( + $this->createMock(SnsContext::class), + $sqsContext + ); + } +} diff --git a/pkg/snsqs/Tests/Spec/SnsQsFactoryTrait.php b/pkg/snsqs/Tests/Spec/SnsQsFactoryTrait.php new file mode 100644 index 000000000..e314c2667 --- /dev/null +++ b/pkg/snsqs/Tests/Spec/SnsQsFactoryTrait.php @@ -0,0 +1,68 @@ +snsQsContext = $this->buildSnsQsContext(); + } + + protected function createSnsQsQueue(string $queueName): SnsQsQueue + { + $queueName .= time(); + + $this->snsQsQueue = $this->snsQsContext->createQueue($queueName); + $this->snsQsContext->declareQueue($this->snsQsQueue); + + if ($this->snsQsTopic) { + $this->snsQsContext->bind($this->snsQsTopic, $this->snsQsQueue); + } + + return $this->snsQsQueue; + } + + protected function createSnsQsTopic(string $topicName): SnsQsTopic + { + $topicName .= time(); + + $this->snsQsTopic = $this->snsQsContext->createTopic($topicName); + $this->snsQsContext->declareTopic($this->snsQsTopic); + + return $this->snsQsTopic; + } + + protected function cleanUpSnsQs(): void + { + if ($this->snsQsTopic) { + $this->snsQsContext->deleteTopic($this->snsQsTopic); + } + + if ($this->snsQsQueue) { + $this->snsQsContext->deleteQueue($this->snsQsQueue); + } + } +} diff --git a/pkg/snsqs/Tests/Spec/SnsQsMessageTest.php b/pkg/snsqs/Tests/Spec/SnsQsMessageTest.php new file mode 100644 index 000000000..a2815cde5 --- /dev/null +++ b/pkg/snsqs/Tests/Spec/SnsQsMessageTest.php @@ -0,0 +1,14 @@ +createMock(SnsContext::class), + $this->createMock(SqsContext::class) + ); + } +} diff --git a/pkg/snsqs/Tests/Spec/SnsQsQueueTest.php b/pkg/snsqs/Tests/Spec/SnsQsQueueTest.php new file mode 100644 index 000000000..6a6bd4dfd --- /dev/null +++ b/pkg/snsqs/Tests/Spec/SnsQsQueueTest.php @@ -0,0 +1,14 @@ +cleanUpSnsQs(); + } + + protected function createContext() + { + return $this->createSnsQsContext(); + } + + protected function createQueue(Context $context, $queueName) + { + return $this->createSnsQsQueue($queueName); + } +} diff --git a/pkg/snsqs/Tests/Spec/SnsQsSendToAndReceiveNoWaitFromQueueTest.php b/pkg/snsqs/Tests/Spec/SnsQsSendToAndReceiveNoWaitFromQueueTest.php new file mode 100644 index 000000000..652766de4 --- /dev/null +++ b/pkg/snsqs/Tests/Spec/SnsQsSendToAndReceiveNoWaitFromQueueTest.php @@ -0,0 +1,35 @@ +cleanUpSnsQs(); + } + + protected function createContext() + { + return $this->createSnsQsContext(); + } + + protected function createQueue(Context $context, $queueName) + { + return $this->createSnsQsQueue($queueName); + } +} diff --git a/pkg/snsqs/Tests/Spec/SnsQsSendToTopicAndReceiveFromQueueSpec.php b/pkg/snsqs/Tests/Spec/SnsQsSendToTopicAndReceiveFromQueueSpec.php new file mode 100644 index 000000000..4a5869d63 --- /dev/null +++ b/pkg/snsqs/Tests/Spec/SnsQsSendToTopicAndReceiveFromQueueSpec.php @@ -0,0 +1,40 @@ +cleanUpSnsQs(); + } + + protected function createContext() + { + return $this->createSnsQsContext(); + } + + protected function createTopic(Context $context, $topicName) + { + return $this->createSnsQsTopic($topicName); + } + + protected function createQueue(Context $context, $queueName) + { + return $this->createSnsQsQueue($queueName); + } +} diff --git a/pkg/snsqs/Tests/Spec/SnsQsSendToTopicAndReceiveNoWaitFromQueueTest.php b/pkg/snsqs/Tests/Spec/SnsQsSendToTopicAndReceiveNoWaitFromQueueTest.php new file mode 100644 index 000000000..433fcf3a7 --- /dev/null +++ b/pkg/snsqs/Tests/Spec/SnsQsSendToTopicAndReceiveNoWaitFromQueueTest.php @@ -0,0 +1,40 @@ +cleanUpSnsQs(); + } + + protected function createContext() + { + return $this->createSnsQsContext(); + } + + protected function createTopic(Context $context, $topicName) + { + return $this->createSnsQsTopic($topicName); + } + + protected function createQueue(Context $context, $queueName) + { + return $this->createSnsQsQueue($queueName); + } +} diff --git a/pkg/snsqs/Tests/Spec/SnsQsTopicTest.php b/pkg/snsqs/Tests/Spec/SnsQsTopicTest.php new file mode 100644 index 000000000..94a455987 --- /dev/null +++ b/pkg/snsqs/Tests/Spec/SnsQsTopicTest.php @@ -0,0 +1,14 @@ + getenv('SNS_DSN'), + 'sqs' => getenv('SQS_DSN'), +]))->createContext(); + +$topic = $context->createTopic('topic'); +$queue = $context->createQueue('queue'); + +$context->declareTopic($topic); +$context->declareQueue($queue); +$context->bind($topic, $queue); + +$consumer = $context->createConsumer($queue); + +while (true) { + if ($m = $consumer->receive(20000)) { + $consumer->acknowledge($m); + echo 'Received message: '.$m->getBody().' '.json_encode($m->getHeaders()).' '.json_encode($m->getProperties()).\PHP_EOL; + } +} +echo 'Done'."\n"; diff --git a/pkg/snsqs/examples/produce.php b/pkg/snsqs/examples/produce.php new file mode 100644 index 000000000..53018d769 --- /dev/null +++ b/pkg/snsqs/examples/produce.php @@ -0,0 +1,40 @@ + getenv('SNS_DSN'), + 'sqs' => getenv('SQS_DSN'), +]))->createContext(); + +$topic = $context->createTopic('topic'); +$queue = $context->createQueue('queue'); + +$context->declareTopic($topic); +$context->declareQueue($queue); +$context->bind($topic, $queue); + +$message = $context->createMessage('Hello Bar!', ['key' => 'value'], ['key2' => 'value2']); + +while (true) { + $context->createProducer()->send($topic, $message); + echo 'Sent message: '.$message->getBody().\PHP_EOL; + sleep(1); +} + +echo 'Done'."\n"; diff --git a/pkg/snsqs/phpunit.xml.dist b/pkg/snsqs/phpunit.xml.dist new file mode 100644 index 000000000..9adb0b184 --- /dev/null +++ b/pkg/snsqs/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + + ./Tests + + + + + + . + + ./vendor + ./Tests + + + + diff --git a/pkg/sqs/.github/workflows/ci.yml b/pkg/sqs/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/sqs/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/sqs/.travis.yml b/pkg/sqs/.travis.yml deleted file mode 100644 index b9cf57fc9..000000000 --- a/pkg/sqs/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/sqs/Client/SqsDriver.php b/pkg/sqs/Client/SqsDriver.php deleted file mode 100755 index 59df35131..000000000 --- a/pkg/sqs/Client/SqsDriver.php +++ /dev/null @@ -1,173 +0,0 @@ -context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $queue = $this->createQueue($this->config->getRouterQueueName()); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($queue, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - * - * @return SqsDestination - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - $transportName = str_replace('.', '_dot_', $transportName); - - return $this->context->createQueue($transportName); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $log = function ($text, ...$args) use ($logger) { - $logger->debug(sprintf('[SqsDriver] '.$text, ...$args)); - }; - - // setup router - $routerQueue = $this->createQueue($this->config->getRouterQueueName()); - $log('Declare router queue: %s', $routerQueue->getQueueName()); - $this->context->declareQueue($routerQueue); - - // setup queues - foreach ($this->queueMetaRegistry->getQueuesMeta() as $meta) { - $queue = $this->createQueue($meta->getClientName()); - - $log('Declare processor queue: %s', $queue->getQueueName()); - $this->context->declareQueue($queue); - } - } - - /** - * {@inheritdoc} - * - * @return SqsMessage - */ - public function createTransportMessage(Message $message) - { - $properties = $message->getProperties(); - - $headers = $message->getHeaders(); - $headers['content_type'] = $message->getContentType(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setBody($message->getBody()); - $transportMessage->setHeaders($headers); - $transportMessage->setProperties($properties); - $transportMessage->setMessageId($message->getMessageId()); - $transportMessage->setTimestamp($message->getTimestamp()); - $transportMessage->setReplyTo($message->getReplyTo()); - $transportMessage->setCorrelationId($message->getCorrelationId()); - $transportMessage->setDelaySeconds($message->getDelay()); - - return $transportMessage; - } - - /** - * @param SqsMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - - $clientMessage->setBody($message->getBody()); - $clientMessage->setHeaders($message->getHeaders()); - $clientMessage->setProperties($message->getProperties()); - - $clientMessage->setContentType($message->getHeader('content_type')); - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setPriority(MessagePriority::NORMAL); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - $clientMessage->setDelay($message->getDelaySeconds() ?: null); - - return $clientMessage; - } - - /** - * @return Config - */ - public function getConfig() - { - return $this->config; - } -} diff --git a/pkg/sqs/README.md b/pkg/sqs/README.md index 55497f67b..7f4170bf2 100644 --- a/pkg/sqs/README.md +++ b/pkg/sqs/README.md @@ -1,19 +1,28 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Amazon SQS Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/sqs.png?branch=master)](https://travis-ci.org/php-enqueue/sqs) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/sqs/ci.yml?branch=master)](https://github.com/php-enqueue/sqs/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/sqs/d/total.png)](https://packagist.org/packages/enqueue/sqs) [![Latest Stable Version](https://poser.pugx.org/enqueue/sqs/version.png)](https://packagist.org/packages/enqueue/sqs) - -This is an implementation of PSR specification. It allows you to send and consume message through Amazon SQS library. + +This is an implementation of Queue Interop specification. It allows you to send and consume message using [Amazon SQS](https://aws.amazon.com/sqs/) service. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/sqs/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/sqs/SqsClient.php b/pkg/sqs/SqsClient.php new file mode 100644 index 000000000..bba2a5760 --- /dev/null +++ b/pkg/sqs/SqsClient.php @@ -0,0 +1,153 @@ +inputClient = $inputClient; + } + + public function deleteMessage(array $args): Result + { + return $this->callApi('deleteMessage', $args); + } + + public function receiveMessage(array $args): Result + { + return $this->callApi('receiveMessage', $args); + } + + public function changeMessageVisibility(array $args): Result + { + return $this->callApi('changeMessageVisibility', $args); + } + + public function purgeQueue(array $args): Result + { + return $this->callApi('purgeQueue', $args); + } + + public function getQueueUrl(array $args): Result + { + return $this->callApi('getQueueUrl', $args); + } + + public function getQueueAttributes(array $args): Result + { + return $this->callApi('getQueueAttributes', $args); + } + + public function createQueue(array $args): Result + { + return $this->callApi('createQueue', $args); + } + + public function deleteQueue(array $args): Result + { + return $this->callApi('deleteQueue', $args); + } + + public function sendMessage(array $args): Result + { + return $this->callApi('sendMessage', $args); + } + + public function getAWSClient(): AwsSqsClient + { + $this->resolveClient(); + + if ($this->singleClient) { + return $this->singleClient; + } + + if ($this->multiClient) { + $mr = new \ReflectionMethod($this->multiClient, 'getClientFromPool'); + $mr->setAccessible(true); + $singleClient = $mr->invoke($this->multiClient, $this->multiClient->getRegion()); + $mr->setAccessible(false); + + return $singleClient; + } + + throw new \LogicException('The multi or single client must be set'); + } + + private function callApi(string $name, array $args): Result + { + $this->resolveClient(); + + if ($this->singleClient) { + if (false == empty($args['@region'])) { + throw new \LogicException('Cannot send message to another region because transport is configured with single aws client'); + } + + unset($args['@region']); + + return call_user_func([$this->singleClient, $name], $args); + } + + if ($this->multiClient) { + return call_user_func([$this->multiClient, $name], $args); + } + + throw new \LogicException('The multi or single client must be set'); + } + + private function resolveClient(): void + { + if ($this->singleClient || $this->multiClient) { + return; + } + + $client = $this->inputClient; + if ($client instanceof MultiRegionClient) { + $this->multiClient = $client; + + return; + } elseif ($client instanceof AwsSqsClient) { + $this->singleClient = $client; + + return; + } elseif (is_callable($client)) { + $client = call_user_func($client); + if ($client instanceof MultiRegionClient) { + $this->multiClient = $client; + + return; + } + if ($client instanceof AwsSqsClient) { + $this->singleClient = $client; + + return; + } + } + + throw new \LogicException(sprintf('The input client must be an instance of "%s" or "%s" or a callable that returns one of those. Got "%s"', AwsSqsClient::class, MultiRegionClient::class, is_object($client) ? $client::class : gettype($client))); + } +} diff --git a/pkg/sqs/SqsConnectionFactory.php b/pkg/sqs/SqsConnectionFactory.php index 275752b6c..71e73b705 100644 --- a/pkg/sqs/SqsConnectionFactory.php +++ b/pkg/sqs/SqsConnectionFactory.php @@ -1,11 +1,16 @@ null - AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. - * 'secret' => null, - AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. - * 'token' => null, - AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. - * 'region' => null, - (string, required) Region to connect to. See http://docs.aws.amazon.com/general/latest/gr/rande.html for a list of available regions. - * 'retries' => 3, - (int, default=int(3)) Configures the maximum number of allowed retries for a client (pass 0 to disable retries). - * 'version' => '2012-11-05', - (string, required) The version of the webservice to utilize - * 'lazy' => true, - Enable lazy connection (boolean) - * 'endpoint' => null - (string, default=null) The full URI of the webservice. This is only required when connecting to a custom endpoint e.g. localstack + * 'key' => null AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. + * 'secret' => null, AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. + * 'token' => null, AWS credentials. If no credentials are provided, the SDK will attempt to load them from the environment. + * 'region' => null, (string, required) Region to connect to. See http://docs.aws.amazon.com/general/latest/gr/rande.html for a list of available regions. + * 'retries' => 3, (int, default=int(3)) Configures the maximum number of allowed retries for a client (pass 0 to disable retries). + * 'version' => '2012-11-05', (string, required) The version of the webservice to utilize + * 'lazy' => true, Enable lazy connection (boolean) + * 'endpoint' => null, (string, default=null) The full URI of the webservice. This is only required when connecting to a custom endpoint e.g. localstack + * 'profile' => null, (string, default=null) The name of an AWS profile to used, if provided the SDK will attempt to read associated credentials from the ~/.aws/credentials file. + * 'queue_owner_aws_account_id' The AWS account ID of the account that created the queue. * ]. * * or @@ -34,60 +41,43 @@ class SqsConnectionFactory implements PsrConnectionFactory * sqs: * sqs::?key=aKey&secret=aSecret&token=aToken * - * @param array|string|SqsClient|null $config + * @param array|string|AwsSqsClient|null $config */ public function __construct($config = 'sqs:') { - if ($config instanceof SqsClient) { - $this->client = $config; + if ($config instanceof AwsSqsClient) { + $this->client = new SqsClient($config); $this->config = ['lazy' => false] + $this->defaultConfig(); return; - } elseif (empty($config) || 'sqs:' === $config) { + } + + if (empty($config)) { $config = []; } elseif (is_string($config)) { $config = $this->parseDsn($config); } elseif (is_array($config)) { - $dsn = array_key_exists('dsn', $config) ? $config['dsn'] : null; - unset($config['dsn']); + if (array_key_exists('dsn', $config)) { + $config = array_replace_recursive($config, $this->parseDsn($config['dsn'])); - if ($dsn) { - $config = array_replace($config, $this->parseDsn($dsn)); + unset($config['dsn']); } } else { - throw new \LogicException('The config must be either an array of options, a DSN string or null'); + throw new \LogicException(sprintf('The config must be either an array of options, a DSN string, null or instance of %s', AwsSqsClient::class)); } $this->config = array_replace($this->defaultConfig(), $config); } /** - * {@inheritdoc} - * * @return SqsContext */ - public function createContext() + public function createContext(): Context { - if ($this->config['lazy']) { - return new SqsContext(function () { - return $this->establishConnection(); - }); - } - - return new SqsContext($this->establishConnection()); + return new SqsContext($this->establishConnection(), $this->config); } - /** - * {@inheritdoc} - */ - public function close() - { - } - - /** - * @return SqsClient - */ - private function establishConnection() + private function establishConnection(): SqsClient { if ($this->client) { return $this->client; @@ -103,6 +93,10 @@ private function establishConnection() $config['endpoint'] = $this->config['endpoint']; } + if (isset($this->config['profile'])) { + $config['profile'] = $this->config['profile']; + } + if ($this->config['key'] && $this->config['secret']) { $config['credentials'] = [ 'key' => $this->config['key'], @@ -114,44 +108,46 @@ private function establishConnection() } } - $this->client = new SqsClient($config); + if (isset($this->config['http'])) { + $config['http'] = $this->config['http']; + } + + $establishConnection = function () use ($config) { + return (new Sdk(['Sqs' => $config]))->createMultiRegionSqs(); + }; + + $this->client = $this->config['lazy'] ? + new SqsClient($establishConnection) : + new SqsClient($establishConnection()) + ; return $this->client; } - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) + private function parseDsn(string $dsn): array { - if (false === strpos($dsn, 'sqs:')) { - throw new \LogicException(sprintf('The given DSN "%s" is not supported. Must start with "sqs:".', $dsn)); - } + $dsn = Dsn::parseFirst($dsn); - if (false === $config = parse_url(http://github.com/$dsn)) { - throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); + if ('sqs' !== $dsn->getSchemeProtocol()) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "sqs"', $dsn->getSchemeProtocol())); } - if ($query = parse_url(http://github.com/$dsn,%20PHP_URL_QUERY)) { - $queryConfig = []; - parse_str($query, $queryConfig); - - $config = array_replace($queryConfig, $config); - } - - unset($config['query'], $config['scheme']); - - $config['lazy'] = empty($config['lazy']) ? false : true; - - return $config; + return array_filter(array_replace($dsn->getQuery(), [ + 'key' => $dsn->getString('key'), + 'secret' => $dsn->getString('secret'), + 'token' => $dsn->getString('token'), + 'region' => $dsn->getString('region'), + 'retries' => $dsn->getDecimal('retries'), + 'version' => $dsn->getString('version'), + 'lazy' => $dsn->getBool('lazy'), + 'endpoint' => $dsn->getString('endpoint'), + 'profile' => $dsn->getString('profile'), + 'queue_owner_aws_account_id' => $dsn->getString('queue_owner_aws_account_id'), + 'http' => $dsn->getArray('http', [])->toArray(), + ]), function ($value) { return null !== $value; }); } - /** - * @return array - */ - private function defaultConfig() + private function defaultConfig(): array { return [ 'key' => null, @@ -162,6 +158,9 @@ private function defaultConfig() 'version' => '2012-11-05', 'lazy' => true, 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], ]; } } diff --git a/pkg/sqs/SqsConsumer.php b/pkg/sqs/SqsConsumer.php index 29628c8d5..860bc648b 100644 --- a/pkg/sqs/SqsConsumer.php +++ b/pkg/sqs/SqsConsumer.php @@ -1,12 +1,15 @@ context = $context; @@ -45,10 +44,7 @@ public function __construct(SqsContext $context, SqsDestination $queue) $this->maxNumberOfMessages = 1; } - /** - * @return int|null - */ - public function getVisibilityTimeout() + public function getVisibilityTimeout(): ?int { return $this->visibilityTimeout; } @@ -56,18 +52,13 @@ public function getVisibilityTimeout() /** * The duration (in seconds) that the received messages are hidden from subsequent retrieve * requests after being retrieved by a ReceiveMessage request. - * - * @param int|null $visibilityTimeout */ - public function setVisibilityTimeout($visibilityTimeout) + public function setVisibilityTimeout(?int $visibilityTimeout = null): void { - $this->visibilityTimeout = null === $visibilityTimeout ? null : (int) $visibilityTimeout; + $this->visibilityTimeout = $visibilityTimeout; } - /** - * @return int - */ - public function getMaxNumberOfMessages() + public function getMaxNumberOfMessages(): int { return $this->maxNumberOfMessages; } @@ -75,28 +66,24 @@ public function getMaxNumberOfMessages() /** * The maximum number of messages to return. Amazon SQS never returns more messages than this value * (however, fewer messages might be returned). Valid values are 1 to 10. Default is 1. - * - * @param int $maxNumberOfMessages */ - public function setMaxNumberOfMessages($maxNumberOfMessages) + public function setMaxNumberOfMessages(int $maxNumberOfMessages): void { - $this->maxNumberOfMessages = (int) $maxNumberOfMessages; + $this->maxNumberOfMessages = $maxNumberOfMessages; } /** - * {@inheritdoc} - * * @return SqsDestination */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } /** - * {@inheritdoc} + * @return SqsMessage */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { $maxLongPollingTime = 20; // 20 is max allowed long polling value @@ -118,59 +105,58 @@ public function receive($timeout = 0) } /** - * {@inheritdoc} + * @return SqsMessage */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { return $this->receiveMessage(0); } /** - * {@inheritdoc} - * * @param SqsMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { InvalidMessageException::assertMessageInstanceOf($message, SqsMessage::class); - $this->context->getClient()->deleteMessage([ + $this->context->getSqsClient()->deleteMessage([ + '@region' => $this->queue->getRegion(), 'QueueUrl' => $this->context->getQueueUrl($this->queue), 'ReceiptHandle' => $message->getReceiptHandle(), ]); } /** - * {@inheritdoc} - * * @param SqsMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { InvalidMessageException::assertMessageInstanceOf($message, SqsMessage::class); - $this->context->getClient()->deleteMessage([ - 'QueueUrl' => $this->context->getQueueUrl($this->queue), - 'ReceiptHandle' => $message->getReceiptHandle(), - ]); - if ($requeue) { - $this->context->createProducer()->send($this->queue, $message); + $this->context->getSqsClient()->changeMessageVisibility([ + '@region' => $this->queue->getRegion(), + 'QueueUrl' => $this->context->getQueueUrl($this->queue), + 'ReceiptHandle' => $message->getReceiptHandle(), + 'VisibilityTimeout' => $message->getRequeueVisibilityTimeout(), + ]); + } else { + $this->context->getSqsClient()->deleteMessage([ + '@region' => $this->queue->getRegion(), + 'QueueUrl' => $this->context->getQueueUrl($this->queue), + 'ReceiptHandle' => $message->getReceiptHandle(), + ]); } } - /** - * @param int $timeoutSeconds - * - * @return SqsMessage|null - */ - protected function receiveMessage($timeoutSeconds) + protected function receiveMessage(int $timeoutSeconds): ?SqsMessage { if ($message = array_pop($this->messages)) { return $this->convertMessage($message); } $arguments = [ + '@region' => $this->queue->getRegion(), 'AttributeNames' => ['All'], 'MessageAttributeNames' => ['All'], 'MaxNumberOfMessages' => $this->maxNumberOfMessages, @@ -182,7 +168,7 @@ protected function receiveMessage($timeoutSeconds) $arguments['VisibilityTimeout'] = $this->visibilityTimeout; } - $result = $this->context->getClient()->receiveMessage($arguments); + $result = $this->context->getSqsClient()->receiveMessage($arguments); if ($result->hasKey('Messages')) { $this->messages = $result->get('Messages'); @@ -191,20 +177,21 @@ protected function receiveMessage($timeoutSeconds) if ($message = array_pop($this->messages)) { return $this->convertMessage($message); } + + return null; } - /** - * @param array $sqsMessage - * - * @return SqsMessage - */ - protected function convertMessage(array $sqsMessage) + protected function convertMessage(array $sqsMessage): SqsMessage { $message = $this->context->createMessage(); $message->setBody($sqsMessage['Body']); $message->setReceiptHandle($sqsMessage['ReceiptHandle']); + if (isset($sqsMessage['Attributes'])) { + $message->setAttributes($sqsMessage['Attributes']); + } + if (isset($sqsMessage['Attributes']['ApproximateReceiveCount'])) { $message->setRedelivered(((int) $sqsMessage['Attributes']['ApproximateReceiveCount']) > 1); } @@ -216,6 +203,8 @@ protected function convertMessage(array $sqsMessage) $message->setProperties($headers[1]); } + $message->setMessageId($sqsMessage['MessageId']); + return $message; } } diff --git a/pkg/sqs/SqsContext.php b/pkg/sqs/SqsContext.php index 57384daaf..65f12ae89 100644 --- a/pkg/sqs/SqsContext.php +++ b/pkg/sqs/SqsContext.php @@ -1,13 +1,23 @@ client = $client; - } elseif (is_callable($client)) { - $this->clientFactory = $client; - } else { - throw new \InvalidArgumentException(sprintf( - 'The $client argument must be either %s or callable that returns %s once called.', - SqsClient::class, - SqsClient::class - )); - } + private $config; + + public function __construct(SqsClient $client, array $config) + { + $this->client = $client; + $this->config = $config; + + $this->queueUrls = []; + $this->queueArns = []; } /** - * {@inheritdoc} - * * @return SqsMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new SqsMessage($body, $properties, $headers); } /** - * {@inheritdoc} - * * @return SqsDestination */ - public function createTopic($topicName) + public function createTopic(string $topicName): Topic { return new SqsDestination($topicName); } /** - * {@inheritdoc} - * * @return SqsDestination */ - public function createQueue($queueName) + public function createQueue(string $queueName): Queue { return new SqsDestination($queueName); } - /** - * {@inheritdoc} - */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { - throw new \BadMethodCallException('SQS transport does not support temporary queues'); + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); } /** - * {@inheritdoc} - * * @return SqsProducer */ - public function createProducer() + public function createProducer(): Producer { return new SqsProducer($this); } /** - * {@inheritdoc} - * * @param SqsDestination $destination * * @return SqsConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, SqsDestination::class); return new SqsConsumer($this, $destination); } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { } /** - * @return SqsClient + * @param SqsDestination $queue */ - public function getClient() - { - if (false == $this->client) { - $client = call_user_func($this->clientFactory); - if (false == $client instanceof SqsClient) { - throw new \LogicException(sprintf( - 'The factory must return instance of "%s". But it returns %s', - SqsClient::class, - is_object($client) ? get_class($client) : gettype($client) - )); - } - - $this->client = $client; - } + public function purgeQueue(Queue $queue): void + { + InvalidDestinationException::assertDestinationInstanceOf($queue, SqsDestination::class); + $this->client->purgeQueue([ + '@region' => $queue->getRegion(), + 'QueueUrl' => $this->getQueueUrl($queue), + ]); + } + + public function createSubscriptionConsumer(): SubscriptionConsumer + { + throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); + } + + public function getAwsSqsClient(): AwsSqsClient + { + return $this->client->getAWSClient(); + } + + public function getSqsClient(): SqsClient + { return $this->client; } /** - * @param SqsDestination $destination - * - * @return string + * @deprecated use getAwsSqsClient method */ - public function getQueueUrl(SqsDestination $destination) + public function getClient(): AwsSqsClient + { + @trigger_error('The method is deprecated since 0.9.2. SqsContext::getAwsSqsClient() method should be used.', \E_USER_DEPRECATED); + + return $this->getAwsSqsClient(); + } + + public function getQueueUrl(SqsDestination $destination): string { if (isset($this->queueUrls[$destination->getQueueName()])) { return $this->queueUrls[$destination->getQueueName()]; } - $result = $this->getClient()->getQueueUrl([ + $arguments = [ + '@region' => $destination->getRegion(), 'QueueName' => $destination->getQueueName(), - ]); + ]; + + if ($destination->getQueueOwnerAWSAccountId()) { + $arguments['QueueOwnerAWSAccountId'] = $destination->getQueueOwnerAWSAccountId(); + } elseif (false == empty($this->config['queue_owner_aws_account_id'])) { + $arguments['QueueOwnerAWSAccountId'] = $this->config['queue_owner_aws_account_id']; + } + + $result = $this->client->getQueueUrl($arguments); if (false == $result->hasKey('QueueUrl')) { throw new \RuntimeException(sprintf('QueueUrl cannot be resolved. queueName: "%s"', $destination->getQueueName())); } - return $this->queueUrls[$destination->getQueueName()] = $result->get('QueueUrl'); + return $this->queueUrls[$destination->getQueueName()] = (string) $result->get('QueueUrl'); } - /** - * @param SqsDestination $dest - */ - public function declareQueue(SqsDestination $dest) + public function getQueueArn(SqsDestination $destination): string { - $result = $this->getClient()->createQueue([ + if (isset($this->queueArns[$destination->getQueueName()])) { + return $this->queueArns[$destination->getQueueName()]; + } + + $arguments = [ + '@region' => $destination->getRegion(), + 'QueueUrl' => $this->getQueueUrl($destination), + 'AttributeNames' => ['QueueArn'], + ]; + + $result = $this->client->getQueueAttributes($arguments); + + if (false == $arn = $result->search('Attributes.QueueArn')) { + throw new \RuntimeException(sprintf('QueueArn cannot be resolved. queueName: "%s"', $destination->getQueueName())); + } + + return $this->queueArns[$destination->getQueueName()] = (string) $arn; + } + + public function declareQueue(SqsDestination $dest): void + { + $result = $this->client->createQueue([ + '@region' => $dest->getRegion(), 'Attributes' => $dest->getAttributes(), 'QueueName' => $dest->getQueueName(), ]); @@ -173,35 +201,12 @@ public function declareQueue(SqsDestination $dest) $this->queueUrls[$dest->getQueueName()] = $result->get('QueueUrl'); } - /** - * @param SqsDestination $dest - */ - public function deleteQueue(SqsDestination $dest) + public function deleteQueue(SqsDestination $dest): void { - $this->getClient()->deleteQueue([ + $this->client->deleteQueue([ 'QueueUrl' => $this->getQueueUrl($dest), ]); unset($this->queueUrls[$dest->getQueueName()]); } - - /** - * @deprecated since 0.8 will be removed 0.9 use self::purgeQueue() - * - * @param SqsDestination $dest - */ - public function purge(SqsDestination $dest) - { - $this->purgeQueue($dest); - } - - /** - * @param SqsDestination $destination - */ - public function purgeQueue(SqsDestination $destination) - { - $this->getClient()->purgeQueue([ - 'QueueUrl' => $this->getQueueUrl($destination), - ]); - } } diff --git a/pkg/sqs/SqsDestination.php b/pkg/sqs/SqsDestination.php index e1e4bed5e..d77966f15 100644 --- a/pkg/sqs/SqsDestination.php +++ b/pkg/sqs/SqsDestination.php @@ -1,22 +1,34 @@ name = $name; $this->attributes = []; } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { return $this->name; } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getTopicName(): string { return $this->name; } - /** - * @return array - */ - public function getAttributes() + public function getAttributes(): array { return $this->attributes; } @@ -60,58 +61,68 @@ public function getAttributes() /** * The number of seconds for which the delivery of all messages in the queue is delayed. * Valid values: An integer from 0 to 900 seconds (15 minutes). The default is 0 (zero). - * - * @param int $seconds */ - public function setDelaySeconds($seconds) + public function setDelaySeconds(?int $seconds = null): void { - $this->attributes['DelaySeconds'] = (int) $seconds; + if (null == $seconds) { + unset($this->attributes['DelaySeconds']); + } else { + $this->attributes['DelaySeconds'] = $seconds; + } } /** * The limit of how many bytes a message can contain before Amazon SQS rejects it. * Valid values: An integer from 1,024 bytes (1 KiB) to 262,144 bytes (256 KiB). * The default is 262,144 (256 KiB). - * - * @param int $bytes */ - public function setMaximumMessageSize($bytes) + public function setMaximumMessageSize(?int $bytes = null): void { - $this->attributes['MaximumMessageSize'] = (int) $bytes; + if (null == $bytes) { + unset($this->attributes['MaximumMessageSize']); + } else { + $this->attributes['MaximumMessageSize'] = $bytes; + } } /** * The number of seconds for which Amazon SQS retains a message. * Valid values: An integer from 60 seconds (1 minute) to 1,209,600 seconds (14 days). * The default is 345,600 (4 days). - * - * @param int $seconds */ - public function setMessageRetentionPeriod($seconds) + public function setMessageRetentionPeriod(?int $seconds = null): void { - $this->attributes['MessageRetentionPeriod'] = (int) $seconds; + if (null == $seconds) { + unset($this->attributes['MessageRetentionPeriod']); + } else { + $this->attributes['MessageRetentionPeriod'] = $seconds; + } } /** * The queue's policy. A valid AWS policy. For more information about policy structure, * see http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html. - * - * @param string $policy */ - public function setPolicy($policy) + public function setPolicy(?string $policy = null): void { - $this->attributes['Policy'] = $policy; + if (null == $policy) { + unset($this->attributes['Policy']); + } else { + $this->attributes['Policy'] = $policy; + } } /** * The number of seconds for which a ReceiveMessage action waits for a message to arrive. * Valid values: An integer from 0 to 20 (seconds). The default is 0 (zero). - * - * @param int $seconds */ - public function setReceiveMessageWaitTimeSeconds($seconds) + public function setReceiveMessageWaitTimeSeconds(?int $seconds = null): void { - $this->attributes['ReceiveMessageWaitTimeSeconds'] = (int) $seconds; + if (null == $seconds) { + unset($this->attributes['ReceiveMessageWaitTimeSeconds']); + } else { + $this->attributes['ReceiveMessageWaitTimeSeconds'] = $seconds; + } } /** @@ -120,11 +131,8 @@ public function setReceiveMessageWaitTimeSeconds($seconds) * see http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html. * The dead letter queue of a FIFO queue must also be a FIFO queue. * Similarly, the dead letter queue of a standard queue must also be a standard queue. - * - * @param int $maxReceiveCount - * @param string $deadLetterTargetArn */ - public function setRedrivePolicy($maxReceiveCount, $deadLetterTargetArn) + public function setRedrivePolicy(int $maxReceiveCount, string $deadLetterTargetArn): void { $this->attributes['RedrivePolicy'] = json_encode([ 'maxReceiveCount' => (string) $maxReceiveCount, @@ -136,12 +144,14 @@ public function setRedrivePolicy($maxReceiveCount, $deadLetterTargetArn) * The visibility timeout for the queue. Valid values: An integer from 0 to 43,200 (12 hours). * The default is 30. For more information about the visibility timeout, * see http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html. - * - * @param int $seconds */ - public function setVisibilityTimeout($seconds) + public function setVisibilityTimeout(?int $seconds = null): void { - $this->attributes['VisibilityTimeout'] = (int) $seconds; + if (null == $seconds) { + unset($this->attributes['VisibilityTimeout']); + } else { + $this->attributes['VisibilityTimeout'] = $seconds; + } } /** @@ -150,10 +160,8 @@ public function setVisibilityTimeout($seconds) * Designates a queue as FIFO. You can provide this attribute only during queue creation. * You can't change it for an existing queue. When you set this attribute, you must provide a MessageGroupId explicitly. * For more information, see http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html#FIFO-queues-understanding-logic. - * - * @param bool $enable */ - public function setFifoQueue($enable) + public function setFifoQueue(bool $enable): void { if ($enable) { $this->attributes['FifoQueue'] = 'true'; @@ -180,10 +188,8 @@ public function setFifoQueue($enable) * * If you send one message with ContentBasedDeduplication enabled and then another message with a MessageDeduplicationId * that is the same as the one generated for the first MessageDeduplicationId, the two messages are treated as * duplicates and only one copy of the message is delivered. - * - * @param bool $enable */ - public function setContentBasedDeduplication($enable) + public function setContentBasedDeduplication(bool $enable): void { if ($enable) { $this->attributes['ContentBasedDeduplication'] = 'true'; @@ -191,4 +197,24 @@ public function setContentBasedDeduplication($enable) unset($this->attributes['ContentBasedDeduplication']); } } + + public function getQueueOwnerAWSAccountId(): ?string + { + return $this->queueOwnerAWSAccountId; + } + + public function setQueueOwnerAWSAccountId(?string $queueOwnerAWSAccountId): void + { + $this->queueOwnerAWSAccountId = $queueOwnerAWSAccountId; + } + + public function setRegion(?string $region = null): void + { + $this->region = $region; + } + + public function getRegion(): ?string + { + return $this->region; + } } diff --git a/pkg/sqs/SqsMessage.php b/pkg/sqs/SqsMessage.php index 30095150a..772c3e217 100644 --- a/pkg/sqs/SqsMessage.php +++ b/pkg/sqs/SqsMessage.php @@ -1,10 +1,12 @@ body = $body; $this->properties = $properties; $this->headers = $headers; + $this->attributes = []; $this->redelivered = false; $this->delaySeconds = 0; + $this->requeueVisibilityTimeout = 0; } - /** - * @param string $body - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * {@inheritdoc} - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { $this->properties[$name] = $value; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } - /** - * @param array $headers - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * {@inheritdoc} - */ - public function isRedelivered() + public function setAttributes(array $attributes): void + { + $this->attributes = $attributes; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function getAttribute(string $name, $default = null) + { + return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; + } + + public function isRedelivered(): bool { return $this->redelivered; } - /** - * {@inheritdoc} - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = $redelivered; } - /** - * {@inheritdoc} - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply_to', $replyTo); } - /** - * {@inheritdoc} - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply_to'); } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id'); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id'); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } @@ -229,18 +193,13 @@ public function setTimestamp($timestamp) * When you set FifoQueue, you can't set DelaySeconds per message. You can set this parameter only on a queue level. * * Set delay in seconds - * - * @param int $seconds */ - public function setDelaySeconds($seconds) + public function setDelaySeconds(int $seconds): void { - $this->delaySeconds = (int) $seconds; + $this->delaySeconds = $seconds; } - /** - * @return int - */ - public function getDelaySeconds() + public function getDelaySeconds(): int { return $this->delaySeconds; } @@ -251,18 +210,13 @@ public function getDelaySeconds() * The token used for deduplication of sent messages. If a message with a particular MessageDeduplicationId is sent successfully, * any messages sent with the same MessageDeduplicationId are accepted successfully but aren't delivered during the 5-minute * deduplication interval. For more information, see http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html#FIFO-queues-exactly-once-processing. - * - * @param string|null $id */ - public function setMessageDeduplicationId($id) + public function setMessageDeduplicationId(?string $id = null): void { $this->messageDeduplicationId = $id; } - /** - * @return string|null - */ - public function getMessageDeduplicationId() + public function getMessageDeduplicationId(): ?string { return $this->messageDeduplicationId; } @@ -275,18 +229,13 @@ public function getMessageDeduplicationId() * To interleave multiple ordered streams within a single queue, use MessageGroupId values (for example, session data * for multiple users). In this scenario, multiple readers can process the queue, but the session data * of each user is processed in a FIFO fashion. - * - * @param string|null $id */ - public function setMessageGroupId($id) + public function setMessageGroupId(?string $id = null): void { $this->messageGroupId = $id; } - /** - * @return string|null - */ - public function getMessageGroupId() + public function getMessageGroupId(): ?string { return $this->messageGroupId; } @@ -297,19 +246,29 @@ public function getMessageGroupId() * * If you receive a message more than once, each time you receive it, you get a different receipt handle. * You must provide the most recently received receipt handle when you request to delete the message (otherwise, the message might not be deleted). - * - * @param string $receipt */ - public function setReceiptHandle($receipt) + public function setReceiptHandle(?string $receipt = null): void { $this->receiptHandle = $receipt; } + public function getReceiptHandle(): ?string + { + return $this->receiptHandle; + } + /** - * @return string + * The number of seconds before the message can be visible again when requeuing. Valid values: 0 to 43200. Maximum: 12 hours. + * + * Set requeue visibility timeout */ - public function getReceiptHandle() + public function setRequeueVisibilityTimeout(int $seconds): void { - return $this->receiptHandle; + $this->requeueVisibilityTimeout = $seconds; + } + + public function getRequeueVisibilityTimeout(): int + { + return $this->requeueVisibilityTimeout; } } diff --git a/pkg/sqs/SqsProducer.php b/pkg/sqs/SqsProducer.php index 5be9e48d3..2e43d8370 100644 --- a/pkg/sqs/SqsProducer.php +++ b/pkg/sqs/SqsProducer.php @@ -1,19 +1,21 @@ context = $context; } /** - * {@inheritdoc} - * * @param SqsDestination $destination * @param SqsMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, SqsDestination::class); InvalidMessageException::assertMessageInstanceOf($message, SqsMessage::class); $body = $message->getBody(); - if (is_scalar($body) && strlen($body) > 0) { - $body = (string) $body; - } else { - throw new InvalidMessageException(sprintf( - 'The message body must be a non-empty string. Got: %s', - is_object($body) ? get_class($body) : gettype($body) - )); + if (empty($body)) { + throw new InvalidMessageException('The message body must be a non-empty string.'); } $arguments = [ + '@region' => $destination->getRegion(), 'MessageAttributes' => [ 'Headers' => [ 'DataType' => 'String', @@ -63,7 +56,7 @@ public function send(PsrDestination $destination, PsrMessage $message) ]; if (null !== $this->deliveryDelay) { - $arguments['DelaySeconds'] = (int) $this->deliveryDelay / 1000; + $arguments['DelaySeconds'] = (int) ceil($this->deliveryDelay / 1000); } if ($message->getDelaySeconds()) { @@ -78,7 +71,7 @@ public function send(PsrDestination $destination, PsrMessage $message) $arguments['MessageGroupId'] = $message->getMessageGroupId(); } - $result = $this->context->getClient()->sendMessage($arguments); + $result = $this->context->getSqsClient()->sendMessage($arguments); if (false == $result->hasKey('MessageId')) { throw new \RuntimeException('Message was not sent'); @@ -86,59 +79,50 @@ public function send(PsrDestination $destination, PsrMessage $message) } /** - * {@inheritdoc} + * @return SqsProducer */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { $this->deliveryDelay = $deliveryDelay; return $this; } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return $this->deliveryDelay; } /** - * {@inheritdoc} + * @return SqsProducer */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { if (null === $priority) { - return; + return $this; } throw PriorityNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return null; } /** - * {@inheritdoc} + * @return SqsProducer */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { if (null === $timeToLive) { - return; + return $this; } throw TimeToLiveNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return null; } diff --git a/pkg/sqs/Symfony/SqsTransportFactory.php b/pkg/sqs/Symfony/SqsTransportFactory.php deleted file mode 100644 index 5f68c7684..000000000 --- a/pkg/sqs/Symfony/SqsTransportFactory.php +++ /dev/null @@ -1,111 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->children() - ->scalarNode('client')->defaultNull()->end() - ->scalarNode('key')->defaultNull()->end() - ->scalarNode('secret')->defaultNull()->end() - ->scalarNode('token')->defaultNull()->end() - ->scalarNode('region')->end() - ->integerNode('retries')->defaultValue(3)->end() - ->scalarNode('version')->cannotBeEmpty()->defaultValue('2012-11-05')->end() - ->booleanNode('lazy') - ->defaultTrue() - ->info('the connection will be performed as later as possible, if the option set to true') - ->end() - ->scalarNode('endpoint')->defaultNull()->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $arguments = empty($config['client']) ? $config : new Reference($config['client']); - - $factory = new Definition(SqsConnectionFactory::class); - $factory->setArguments([$arguments]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(SqsContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(SqsDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/sqs/Tests/Client/SqsDriverTest.php b/pkg/sqs/Tests/Client/SqsDriverTest.php deleted file mode 100755 index 36e1e090e..000000000 --- a/pkg/sqs/Tests/Client/SqsDriverTest.php +++ /dev/null @@ -1,418 +0,0 @@ -assertClassImplements(DriverInterface::class, SqsDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new SqsDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createQueueMetaRegistryMock() - ); - } - - public function testShouldReturnConfigObject() - { - $config = Config::create(); - - $driver = new SqsDriver($this->createPsrContextMock(), $config, $this->createQueueMetaRegistryMock()); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new SqsDestination('aQueueName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix_dot_afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new SqsDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - $this->assertSame('aQueueName', $queue->getQueueName()); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new SqsDestination('aQueueName'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new SqsDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new SqsMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content_type', 'ContentType'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new SqsDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createQueueMetaRegistryMock() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $clientMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - $this->assertNull($clientMessage->getDelay()); - - $this->assertNull($clientMessage->getExpire()); - $this->assertSame(MessagePriority::NORMAL, $clientMessage->getPriority()); - - // Test delay - $transportMessage->setDelaySeconds(100); - $clientMessage = $driver->createClientMessage($transportMessage); - $this->assertSame(100, $clientMessage->getDelay()); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setPriority(MessagePriority::VERY_HIGH); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->exactly(2)) - ->method('createMessage') - ->willReturn(new SqsMessage()) - ; - - $driver = new SqsDriver( - $context, - Config::create(), - $this->createQueueMetaRegistryMock() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(SqsMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content_type' => 'ContentType', - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply_to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $transportMessage->getHeaders()); - $this->assertSame([ - 'key' => 'val', - ], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - $this->assertSame(0, $transportMessage->getDelaySeconds()); - - // Test delay - $clientMessage->setDelay(100); - $transportMessage = $driver->createTransportMessage($clientMessage); - $this->assertSame(100, $transportMessage->getDelaySeconds()); - } - - public function testShouldSendMessageToRouterQueue() - { - $topic = new SqsDestination('aDestinationName'); - $transportMessage = new SqsMessage(); - $config = $this->createConfigMock(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('theTransportName') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $meta = $this->createQueueMetaRegistryMock(); - $meta - ->expects($this->once()) - ->method('getQueueMeta') - ->willReturn(new QueueMeta('theClientName', 'theTransportName')) - ; - - $driver = new SqsDriver($context, $config, $meta); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new SqsDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createQueueMetaRegistryMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new SqsDestination('aDestinationName'); - $transportMessage = new SqsMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $meta = $this->createQueueMetaRegistryMock(); - $meta - ->expects($this->once()) - ->method('getQueueMeta') - ->willReturn(new QueueMeta('theClientName', 'theTransportName')) - ; - - $driver = new SqsDriver($context, Config::create(), $meta); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'queue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new SqsDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createQueueMetaRegistryMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new SqsDriver( - $this->createPsrContextMock(), - Config::create(), - $this->createQueueMetaRegistryMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldSetupBroker() - { - $routerQueue = new SqsDestination(''); - $processorQueue = new SqsDestination(''); - - $context = $this->createPsrContextMock(); - // setup router - $context - ->expects($this->at(0)) - ->method('createQueue') - ->willReturn($routerQueue) - ; - $context - ->expects($this->at(1)) - ->method('declareQueue') - ->with($this->identicalTo($routerQueue)) - ; - // setup processor queue - $context - ->expects($this->at(2)) - ->method('createQueue') - ->willReturn($processorQueue) - ; - $context - ->expects($this->at(3)) - ->method('declareQueue') - ->with($this->identicalTo($processorQueue)) - ; - - $metaRegistry = $this->createQueueMetaRegistryMock(); - $metaRegistry - ->expects($this->once()) - ->method('getQueuesMeta') - ->willReturn([new QueueMeta('theClientName', 'theTransportName')]) - ; - $metaRegistry - ->expects($this->exactly(2)) - ->method('getQueueMeta') - ->willReturn(new QueueMeta('theClientName', 'theTransportName')) - ; - - $driver = new SqsDriver($context, $this->createConfigMock(), $metaRegistry); - - $driver->setupBroker(); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|SqsContext - */ - private function createPsrContextMock() - { - return $this->createMock(SqsContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|PsrProducer - */ - private function createPsrProducerMock() - { - return $this->createMock(PsrProducer::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|QueueMetaRegistry - */ - private function createQueueMetaRegistryMock() - { - return $this->createMock(QueueMetaRegistry::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|Config - */ - private function createConfigMock() - { - return $this->createMock(Config::class); - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } -} diff --git a/pkg/sqs/Tests/Functional/SqsCommonUseCasesTest.php b/pkg/sqs/Tests/Functional/SqsCommonUseCasesTest.php index 5c6f11286..7d06df229 100644 --- a/pkg/sqs/Tests/Functional/SqsCommonUseCasesTest.php +++ b/pkg/sqs/Tests/Functional/SqsCommonUseCasesTest.php @@ -27,7 +27,7 @@ class SqsCommonUseCasesTest extends TestCase */ private $queueName; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -39,7 +39,7 @@ protected function setUp() $this->context->declareQueue($this->queue); } - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); @@ -102,7 +102,8 @@ public function testProduceAndReceiveOneMessageSentDirectlyToQueue() $this->assertEquals(__METHOD__, $message->getBody()); $this->assertEquals(['FooProperty' => 'FooVal'], $message->getProperties()); - $this->assertEquals(['BarHeader' => 'BarVal'], $message->getHeaders()); + $this->assertEquals('BarVal', $message->getHeaders()['BarHeader']); + $this->assertNotNull($message->getMessageId()); } public function testProduceAndReceiveOneMessageSentDirectlyToTopic() diff --git a/pkg/sqs/Tests/Functional/SqsConsumptionUseCasesTest.php b/pkg/sqs/Tests/Functional/SqsConsumptionUseCasesTest.php index 490c70ebf..9c57dcbdc 100644 --- a/pkg/sqs/Tests/Functional/SqsConsumptionUseCasesTest.php +++ b/pkg/sqs/Tests/Functional/SqsConsumptionUseCasesTest.php @@ -11,22 +11,22 @@ use Enqueue\Sqs\SqsContext; use Enqueue\Test\RetryTrait; use Enqueue\Test\SqsExtension; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; use PHPUnit\Framework\TestCase; class SqsConsumptionUseCasesTest extends TestCase { - use SqsExtension; use RetryTrait; + use SqsExtension; /** * @var SqsContext */ private $context; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -39,8 +39,8 @@ protected function setUp() $this->context->declareQueue($replyQueue); try { - $this->context->purge($queue); - $this->context->purge($replyQueue); + $this->context->purgeQueue($queue); + $this->context->purgeQueue($replyQueue); } catch (\Exception $e) { } } @@ -65,7 +65,7 @@ public function testConsumeOneMessageAndExit() $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); } @@ -98,22 +98,22 @@ public function testConsumeOneMessageAndSendReplyExit() $queueConsumer->bind($replyQueue, $replyProcessor); $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); - $this->assertInstanceOf(PsrMessage::class, $replyProcessor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $replyProcessor->lastProcessedMessage); $this->assertEquals(__METHOD__.'.reply', $replyProcessor->lastProcessedMessage->getBody()); } } -class StubProcessor implements PsrProcessor +class StubProcessor implements Processor { public $result = self::ACK; - /** @var PsrMessage */ + /** @var Message */ public $lastProcessedMessage; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $this->lastProcessedMessage = $message; diff --git a/pkg/sqs/Tests/Spec/CreateSqsQueueTrait.php b/pkg/sqs/Tests/Spec/CreateSqsQueueTrait.php new file mode 100644 index 000000000..3af2a5129 --- /dev/null +++ b/pkg/sqs/Tests/Spec/CreateSqsQueueTrait.php @@ -0,0 +1,21 @@ +queue = $context->createQueue($queueName); + $context->declareQueue($this->queue); + + return $this->queue; + } +} diff --git a/pkg/sqs/Tests/Spec/SqsMessageTest.php b/pkg/sqs/Tests/Spec/SqsMessageTest.php index c658840e9..994fe5be5 100644 --- a/pkg/sqs/Tests/Spec/SqsMessageTest.php +++ b/pkg/sqs/Tests/Spec/SqsMessageTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Sqs\Tests\Spec; use Enqueue\Sqs\SqsMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class SqsMessageTest extends PsrMessageSpec +class SqsMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new SqsMessage(); diff --git a/pkg/sqs/Tests/Spec/SqsProducerTest.php b/pkg/sqs/Tests/Spec/SqsProducerTest.php index 19e55383b..f0e5c8b06 100644 --- a/pkg/sqs/Tests/Spec/SqsProducerTest.php +++ b/pkg/sqs/Tests/Spec/SqsProducerTest.php @@ -3,18 +3,15 @@ namespace Enqueue\Sqs\Tests\Spec; use Enqueue\Test\SqsExtension; -use Interop\Queue\Spec\PsrProducerSpec; +use Interop\Queue\Spec\ProducerSpec; /** * @group functional */ -class SqsProducerTest extends PsrProducerSpec +class SqsProducerTest extends ProducerSpec { use SqsExtension; - /** - * {@inheritdoc} - */ protected function createProducer() { return $this->buildSqsContext()->createProducer(); diff --git a/pkg/sqs/Tests/Spec/SqsSendAndReceiveDelayedMessageFromQueueTest.php b/pkg/sqs/Tests/Spec/SqsSendAndReceiveDelayedMessageFromQueueTest.php index 2623969f4..40f20d68f 100644 --- a/pkg/sqs/Tests/Spec/SqsSendAndReceiveDelayedMessageFromQueueTest.php +++ b/pkg/sqs/Tests/Spec/SqsSendAndReceiveDelayedMessageFromQueueTest.php @@ -6,15 +6,17 @@ use Enqueue\Sqs\SqsDestination; use Enqueue\Test\RetryTrait; use Enqueue\Test\SqsExtension; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendAndReceiveDelayedMessageFromQueueSpec; /** * @group functional + * * @retry 5 */ class SqsSendAndReceiveDelayedMessageFromQueueTest extends SendAndReceiveDelayedMessageFromQueueSpec { + use CreateSqsQueueTrait; use RetryTrait; use SqsExtension; @@ -23,12 +25,7 @@ class SqsSendAndReceiveDelayedMessageFromQueueTest extends SendAndReceiveDelayed */ private $context; - /** - * @var SqsDestination - */ - private $queue; - - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); @@ -37,26 +34,13 @@ protected function tearDown() } } - /** - * {@inheritdoc} - */ - protected function createContext() + protected function createContext(): SqsContext { return $this->context = $this->buildSqsContext(); } - /** - * {@inheritdoc} - * - * @param SqsContext $context - */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName): SqsDestination { - $queueName = $queueName.time(); - - $this->queue = $context->createQueue($queueName); - $context->declareQueue($this->queue); - - return $this->queue; + return $this->createSqsQueue($context, $queueName); } } diff --git a/pkg/sqs/Tests/Spec/SqsSendToAndReceiveFromQueueTest.php b/pkg/sqs/Tests/Spec/SqsSendToAndReceiveFromQueueTest.php index e694e88aa..db698017d 100644 --- a/pkg/sqs/Tests/Spec/SqsSendToAndReceiveFromQueueTest.php +++ b/pkg/sqs/Tests/Spec/SqsSendToAndReceiveFromQueueTest.php @@ -4,15 +4,20 @@ use Enqueue\Sqs\SqsContext; use Enqueue\Sqs\SqsDestination; +use Enqueue\Test\RetryTrait; use Enqueue\Test\SqsExtension; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromQueueSpec; /** * @group functional + * + * @retry 5 */ class SqsSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec { + use CreateSqsQueueTrait; + use RetryTrait; use SqsExtension; /** @@ -20,12 +25,7 @@ class SqsSendToAndReceiveFromQueueTest extends SendToAndReceiveFromQueueSpec */ private $context; - /** - * @var SqsDestination - */ - private $queue; - - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); @@ -34,26 +34,13 @@ protected function tearDown() } } - /** - * {@inheritdoc} - */ - protected function createContext() + protected function createContext(): SqsContext { return $this->context = $this->buildSqsContext(); } - /** - * {@inheritdoc} - * - * @param SqsContext $context - */ - protected function createQueue(PsrContext $context, $queueName) + protected function createQueue(Context $context, $queueName): SqsDestination { - $queueName = $queueName.time(); - - $this->queue = $context->createQueue($queueName); - $context->declareQueue($this->queue); - - return $this->queue; + return $this->createSqsQueue($context, $queueName); } } diff --git a/pkg/sqs/Tests/Spec/SqsSendToAndReceiveFromTopicTest.php b/pkg/sqs/Tests/Spec/SqsSendToAndReceiveFromTopicTest.php index 56ca7dc08..5cd14468a 100644 --- a/pkg/sqs/Tests/Spec/SqsSendToAndReceiveFromTopicTest.php +++ b/pkg/sqs/Tests/Spec/SqsSendToAndReceiveFromTopicTest.php @@ -6,29 +6,26 @@ use Enqueue\Sqs\SqsDestination; use Enqueue\Test\RetryTrait; use Enqueue\Test\SqsExtension; -use Interop\Queue\PsrContext; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveFromTopicSpec; /** * @group functional + * * @retry 5 */ class SqsSendToAndReceiveFromTopicTest extends SendToAndReceiveFromTopicSpec { - use SqsExtension; + use CreateSqsQueueTrait; use RetryTrait; + use SqsExtension; /** * @var SqsContext */ private $context; - /** - * @var SqsDestination - */ - private $queue; - - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); @@ -37,26 +34,13 @@ protected function tearDown() } } - /** - * {@inheritdoc} - */ - protected function createContext() + protected function createContext(): SqsContext { return $this->context = $this->buildSqsContext(); } - /** - * {@inheritdoc} - * - * @param SqsContext $context - */ - protected function createTopic(PsrContext $context, $topicName) + protected function createTopic(Context $context, $queueName): SqsDestination { - $topicName = $topicName.time(); - - $this->queue = $context->createTopic($topicName); - $context->declareQueue($this->queue); - - return $this->queue; + return $this->createSqsQueue($context, $queueName); } } diff --git a/pkg/sqs/Tests/Spec/SqsSendToAndReceiveNoWaitFromQueueTest.php b/pkg/sqs/Tests/Spec/SqsSendToAndReceiveNoWaitFromQueueTest.php index 9301c647b..7e31a25a4 100644 --- a/pkg/sqs/Tests/Spec/SqsSendToAndReceiveNoWaitFromQueueTest.php +++ b/pkg/sqs/Tests/Spec/SqsSendToAndReceiveNoWaitFromQueueTest.php @@ -2,23 +2,45 @@ namespace Enqueue\Sqs\Tests\Spec; +use Enqueue\Sqs\SqsContext; +use Enqueue\Sqs\SqsDestination; +use Enqueue\Test\RetryTrait; +use Enqueue\Test\SqsExtension; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromQueueSpec; /** * @group functional + * + * @retry 5 */ class SqsSendToAndReceiveNoWaitFromQueueTest extends SendToAndReceiveNoWaitFromQueueSpec { - public function test() - { - $this->markTestSkipped('The test is fragile. This is how SQS.'); - } + use CreateSqsQueueTrait; + use RetryTrait; + use SqsExtension; /** - * {@inheritdoc} + * @var SqsContext */ - protected function createContext() + private $context; + + protected function tearDown(): void + { + parent::tearDown(); + + if ($this->context && $this->queue) { + $this->context->deleteQueue($this->queue); + } + } + + protected function createContext(): SqsContext + { + return $this->context = $this->buildSqsContext(); + } + + protected function createQueue(Context $context, $queueName): SqsDestination { - throw new \LogicException('Should not be ever called'); + return $this->createSqsQueue($context, $queueName); } } diff --git a/pkg/sqs/Tests/Spec/SqsSendToAndReceiveNoWaitFromTopicTest.php b/pkg/sqs/Tests/Spec/SqsSendToAndReceiveNoWaitFromTopicTest.php index 3d9149b05..34f2e75dd 100644 --- a/pkg/sqs/Tests/Spec/SqsSendToAndReceiveNoWaitFromTopicTest.php +++ b/pkg/sqs/Tests/Spec/SqsSendToAndReceiveNoWaitFromTopicTest.php @@ -2,23 +2,45 @@ namespace Enqueue\Sqs\Tests\Spec; +use Enqueue\Sqs\SqsContext; +use Enqueue\Sqs\SqsDestination; +use Enqueue\Test\RetryTrait; +use Enqueue\Test\SqsExtension; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToAndReceiveNoWaitFromTopicSpec; /** * @group functional + * + * @retry 5 */ class SqsSendToAndReceiveNoWaitFromTopicTest extends SendToAndReceiveNoWaitFromTopicSpec { - public function test() - { - $this->markTestSkipped('The test is fragile. This is how SQS.'); - } + use CreateSqsQueueTrait; + use RetryTrait; + use SqsExtension; /** - * {@inheritdoc} + * @var SqsContext */ - protected function createContext() + private $context; + + protected function tearDown(): void + { + parent::tearDown(); + + if ($this->context && $this->queue) { + $this->context->deleteQueue($this->queue); + } + } + + protected function createContext(): SqsContext + { + return $this->context = $this->buildSqsContext(); + } + + protected function createTopic(Context $context, $queueName): SqsDestination { - throw new \LogicException('Should not be ever called'); + return $this->createSqsQueue($context, $queueName); } } diff --git a/pkg/sqs/Tests/Spec/SqsSendToTopicAndReceiveFromQueueTest.php b/pkg/sqs/Tests/Spec/SqsSendToTopicAndReceiveFromQueueTest.php index a9db45362..b8e60aee9 100644 --- a/pkg/sqs/Tests/Spec/SqsSendToTopicAndReceiveFromQueueTest.php +++ b/pkg/sqs/Tests/Spec/SqsSendToTopicAndReceiveFromQueueTest.php @@ -2,23 +2,50 @@ namespace Enqueue\Sqs\Tests\Spec; +use Enqueue\Sqs\SqsContext; +use Enqueue\Sqs\SqsDestination; +use Enqueue\Test\RetryTrait; +use Enqueue\Test\SqsExtension; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveFromQueueSpec; /** * @group functional + * + * @retry 5 */ class SqsSendToTopicAndReceiveFromQueueTest extends SendToTopicAndReceiveFromQueueSpec { - public function test() - { - $this->markTestSkipped('The SQS does not support it'); - } + use CreateSqsQueueTrait; + use RetryTrait; + use SqsExtension; /** - * {@inheritdoc} + * @var SqsContext */ - protected function createContext() + private $context; + + protected function tearDown(): void + { + parent::tearDown(); + + if ($this->context && $this->queue) { + $this->context->deleteQueue($this->queue); + } + } + + protected function createContext(): SqsContext + { + return $this->context = $this->buildSqsContext(); + } + + protected function createTopic(Context $context, $queueName): SqsDestination + { + return $this->createSqsQueue($context, $queueName); + } + + protected function createQueue(Context $context, $queueName): SqsDestination { - throw new \LogicException('Should not be ever called'); + return $this->createSqsQueue($context, $queueName); } } diff --git a/pkg/sqs/Tests/Spec/SqsSendToTopicAndReceiveNoWaitFromQueueTest.php b/pkg/sqs/Tests/Spec/SqsSendToTopicAndReceiveNoWaitFromQueueTest.php index bbb9be63a..e5520e01f 100644 --- a/pkg/sqs/Tests/Spec/SqsSendToTopicAndReceiveNoWaitFromQueueTest.php +++ b/pkg/sqs/Tests/Spec/SqsSendToTopicAndReceiveNoWaitFromQueueTest.php @@ -2,23 +2,50 @@ namespace Enqueue\Sqs\Tests\Spec; +use Enqueue\Sqs\SqsContext; +use Enqueue\Sqs\SqsDestination; +use Enqueue\Test\RetryTrait; +use Enqueue\Test\SqsExtension; +use Interop\Queue\Context; use Interop\Queue\Spec\SendToTopicAndReceiveNoWaitFromQueueSpec; /** * @group functional + * + * @retry 5 */ class SqsSendToTopicAndReceiveNoWaitFromQueueTest extends SendToTopicAndReceiveNoWaitFromQueueSpec { - public function test() - { - $this->markTestSkipped('The SQS does not support it'); - } + use CreateSqsQueueTrait; + use RetryTrait; + use SqsExtension; /** - * {@inheritdoc} + * @var SqsContext */ - protected function createContext() + private $context; + + protected function tearDown(): void + { + parent::tearDown(); + + if ($this->context && $this->queue) { + $this->context->deleteQueue($this->queue); + } + } + + protected function createContext(): SqsContext + { + return $this->context = $this->buildSqsContext(); + } + + protected function createTopic(Context $context, $queueName): SqsDestination + { + return $this->createSqsQueue($context, $queueName); + } + + protected function createQueue(Context $context, $queueName): SqsDestination { - throw new \LogicException('Should not be ever called'); + return $this->createSqsQueue($context, $queueName); } } diff --git a/pkg/sqs/Tests/SqsClientTest.php b/pkg/sqs/Tests/SqsClientTest.php new file mode 100644 index 000000000..ff6a966d4 --- /dev/null +++ b/pkg/sqs/Tests/SqsClientTest.php @@ -0,0 +1,311 @@ + [ + 'key' => '', + 'secret' => '', + 'region' => 'us-west-2', + 'version' => '2012-11-05', + 'endpoint' => 'http://localhost/', + ]]))->createSqs(); + + $client = new SqsClient($awsClient); + + $this->assertSame($awsClient, $client->getAWSClient()); + } + + public function testShouldAllowGetAwsClientIfMultipleClientProvided() + { + $awsClient = (new Sdk(['Sqs' => [ + 'key' => '', + 'secret' => '', + 'region' => 'us-west-2', + 'version' => '2012-11-05', + 'endpoint' => 'http://localhost/', + ]]))->createMultiRegionSqs(); + + $client = new SqsClient($awsClient); + + $this->assertInstanceOf(AwsSqsClient::class, $client->getAWSClient()); + } + + /** + * @dataProvider provideApiCallsSingleClient + * @dataProvider provideApiCallsMultipleClient + */ + public function testApiCall(string $method, array $args, array $result, string $awsClientClass) + { + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->once()) + ->method($method) + ->with($this->identicalTo($args)) + ->willReturn(new Result($result)); + + $client = new SqsClient($awsClient); + + $actualResult = $client->{$method}($args); + + $this->assertInstanceOf(Result::class, $actualResult); + $this->assertSame($result, $actualResult->toArray()); + } + + /** + * @dataProvider provideApiCallsSingleClient + * @dataProvider provideApiCallsMultipleClient + */ + public function testLazyApiCall(string $method, array $args, array $result, string $awsClientClass) + { + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->once()) + ->method($method) + ->with($this->identicalTo($args)) + ->willReturn(new Result($result)); + + $client = new SqsClient(function () use ($awsClient) { + return $awsClient; + }); + + $actualResult = $client->{$method}($args); + + $this->assertInstanceOf(Result::class, $actualResult); + $this->assertSame($result, $actualResult->toArray()); + } + + /** + * @dataProvider provideApiCallsSingleClient + * @dataProvider provideApiCallsMultipleClient + */ + public function testThrowIfInvalidInputClientApiCall(string $method, array $args, array $result, string $awsClientClass) + { + $client = new SqsClient(new \stdClass()); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The input client must be an instance of "Aws\Sqs\SqsClient" or "Aws\MultiRegionClient" or a callable that returns one of those. Got "stdClass"'); + $client->{$method}($args); + } + + /** + * @dataProvider provideApiCallsSingleClient + * @dataProvider provideApiCallsMultipleClient + */ + public function testThrowIfInvalidLazyInputClientApiCall(string $method, array $args, array $result, string $awsClientClass) + { + $client = new SqsClient(function () { return new \stdClass(); }); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The input client must be an instance of "Aws\Sqs\SqsClient" or "Aws\MultiRegionClient" or a callable that returns one of those. Got "stdClass"'); + $client->{$method}($args); + } + + /** + * @dataProvider provideApiCallsMultipleClient + */ + public function testApiCallWithMultiClientAndCustomRegion(string $method, array $args, array $result, string $awsClientClass) + { + $args['@region'] = 'theRegion'; + + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->once()) + ->method($method) + ->with($this->identicalTo($args)) + ->willReturn(new Result($result)); + + $client = new SqsClient($awsClient); + + $actualResult = $client->{$method}($args); + + $this->assertInstanceOf(Result::class, $actualResult); + $this->assertSame($result, $actualResult->toArray()); + } + + /** + * @dataProvider provideApiCallsSingleClient + */ + public function testApiCallWithSingleClientAndCustomRegion(string $method, array $args, array $result, string $awsClientClass) + { + $args['@region'] = 'theRegion'; + + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->never()) + ->method($method) + ; + + $client = new SqsClient($awsClient); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Cannot send message to another region because transport is configured with single aws client'); + $client->{$method}($args); + } + + /** + * @dataProvider provideApiCallsSingleClient + */ + public function testApiCallWithMultiClientAndEmptyCustomRegion(string $method, array $args, array $result, string $awsClientClass) + { + $expectedArgs = $args; + $args['@region'] = ''; + + $awsClient = $this->getMockBuilder($awsClientClass) + ->disableOriginalConstructor() + ->setMethods([$method]) + ->getMock(); + $awsClient + ->expects($this->once()) + ->method($method) + ->with($this->identicalTo($expectedArgs)) + ->willReturn(new Result($result)); + + $client = new SqsClient($awsClient); + + $actualResult = $client->{$method}($args); + + $this->assertInstanceOf(Result::class, $actualResult); + $this->assertSame($result, $actualResult->toArray()); + } + + public function provideApiCallsSingleClient() + { + yield [ + 'deleteMessage', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSqsClient::class, + ]; + + yield [ + 'receiveMessage', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSqsClient::class, + ]; + + yield [ + 'purgeQueue', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSqsClient::class, + ]; + + yield [ + 'getQueueUrl', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSqsClient::class, + ]; + + yield [ + 'getQueueAttributes', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSqsClient::class, + ]; + + yield [ + 'createQueue', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSqsClient::class, + ]; + + yield [ + 'deleteQueue', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSqsClient::class, + ]; + + yield [ + 'sendMessage', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + AwsSqsClient::class, + ]; + } + + public function provideApiCallsMultipleClient() + { + yield [ + 'deleteMessage', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + + yield [ + 'receiveMessage', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + + yield [ + 'purgeQueue', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + + yield [ + 'getQueueUrl', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + + yield [ + 'getQueueAttributes', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + + yield [ + 'createQueue', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + + yield [ + 'deleteQueue', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + + yield [ + 'sendMessage', + ['fooArg' => 'fooArgVal'], + ['bar' => 'barVal'], + MultiRegionClient::class, + ]; + } +} diff --git a/pkg/sqs/Tests/SqsConnectionFactoryConfigTest.php b/pkg/sqs/Tests/SqsConnectionFactoryConfigTest.php index 2bd54c56e..c7a954b0f 100644 --- a/pkg/sqs/Tests/SqsConnectionFactoryConfigTest.php +++ b/pkg/sqs/Tests/SqsConnectionFactoryConfigTest.php @@ -4,6 +4,7 @@ use Enqueue\Sqs\SqsConnectionFactory; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; /** @@ -12,11 +13,12 @@ class SqsConnectionFactoryConfigTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testThrowNeitherArrayStringNorNullGivenAsConfig() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The config must be either an array of options, a DSN string or null'); + $this->expectExceptionMessage('The config must be either an array of options, a DSN string, null or instance of Aws\Sqs\SqsClient'); new SqsConnectionFactory(new \stdClass()); } @@ -24,7 +26,7 @@ public function testThrowNeitherArrayStringNorNullGivenAsConfig() public function testThrowIfSchemeIsNotAmqp() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN "http://example.com/" is not supported. Must start with "sqs:".'); + $this->expectExceptionMessage('The given scheme protocol "http" is not supported. It must be "sqs"'); new SqsConnectionFactory('http://example.com/'); } @@ -32,16 +34,13 @@ public function testThrowIfSchemeIsNotAmqp() public function testThrowIfDsnCouldNotBeParsed() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Failed to parse DSN "sqs://:@/"'); + $this->expectExceptionMessage('The DSN is invalid.'); - new SqsConnectionFactory('sqs://:@/'); + new SqsConnectionFactory('foo'); } /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { @@ -63,6 +62,9 @@ public static function provideConfigs() 'version' => '2012-11-05', 'lazy' => true, 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], ], ]; @@ -77,6 +79,9 @@ public static function provideConfigs() 'version' => '2012-11-05', 'lazy' => true, 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], ], ]; @@ -91,6 +96,9 @@ public static function provideConfigs() 'version' => '2012-11-05', 'lazy' => true, 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], ], ]; @@ -105,6 +113,9 @@ public static function provideConfigs() 'version' => '2012-11-05', 'lazy' => false, 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], ], ]; @@ -119,6 +130,26 @@ public static function provideConfigs() 'version' => '2012-11-05', 'lazy' => false, 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], + ], + ]; + + yield [ + ['dsn' => 'sqs:?profile=staging&lazy=0'], + [ + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'retries' => 3, + 'version' => '2012-11-05', + 'lazy' => false, + 'endpoint' => null, + 'profile' => 'staging', + 'queue_owner_aws_account_id' => null, + 'http' => [], ], ]; @@ -133,6 +164,9 @@ public static function provideConfigs() 'version' => '2012-11-05', 'lazy' => false, 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], ], ]; @@ -153,6 +187,48 @@ public static function provideConfigs() 'version' => '2012-11-05', 'lazy' => false, 'endpoint' => 'http://localstack:1111/', + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], + ], + ]; + + yield [ + [ + 'profile' => 'staging', + ], + [ + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'retries' => 3, + 'version' => '2012-11-05', + 'lazy' => true, + 'endpoint' => null, + 'profile' => 'staging', + 'queue_owner_aws_account_id' => null, + 'http' => [], + ], + ]; + + yield [ + ['dsn' => 'sqs:?http[timeout]=5&http[connect_timeout]=2'], + [ + 'key' => null, + 'secret' => null, + 'token' => null, + 'region' => null, + 'retries' => 3, + 'version' => '2012-11-05', + 'lazy' => true, + 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [ + 'timeout' => '5', + 'connect_timeout' => '2', + ], ], ]; } diff --git a/pkg/sqs/Tests/SqsConnectionFactoryTest.php b/pkg/sqs/Tests/SqsConnectionFactoryTest.php index aa3cf88de..c327522c5 100644 --- a/pkg/sqs/Tests/SqsConnectionFactoryTest.php +++ b/pkg/sqs/Tests/SqsConnectionFactoryTest.php @@ -2,19 +2,23 @@ namespace Enqueue\Sqs\Tests; -use Aws\Sqs\SqsClient; +use Aws\Sqs\SqsClient as AwsSqsClient; +use Enqueue\Sqs\SqsClient; use Enqueue\Sqs\SqsConnectionFactory; use Enqueue\Sqs\SqsContext; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\ConnectionFactory; +use PHPUnit\Framework\TestCase; -class SqsConnectionFactoryTest extends \PHPUnit\Framework\TestCase +class SqsConnectionFactoryTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, SqsConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, SqsConnectionFactory::class); } public function testCouldBeConstructedWithEmptyConfiguration() @@ -30,6 +34,9 @@ public function testCouldBeConstructedWithEmptyConfiguration() 'retries' => 3, 'version' => '2012-11-05', 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], ], 'config', $factory); } @@ -46,19 +53,25 @@ public function testCouldBeConstructedWithCustomConfiguration() 'retries' => 3, 'version' => '2012-11-05', 'endpoint' => null, + 'profile' => null, + 'queue_owner_aws_account_id' => null, + 'http' => [], ], 'config', $factory); } public function testCouldBeConstructedWithClient() { - $client = $this->createMock(SqsClient::class); + $awsClient = $this->createMock(AwsSqsClient::class); - $factory = new SqsConnectionFactory($client); + $factory = new SqsConnectionFactory($awsClient); $context = $factory->createContext(); $this->assertInstanceOf(SqsContext::class, $context); - $this->assertAttributeSame($client, 'client', $context); + + $client = $this->readAttribute($context, 'client'); + $this->assertInstanceOf(SqsClient::class, $client); + $this->assertAttributeSame($awsClient, 'inputClient', $client); } public function testShouldCreateLazyContext() @@ -69,7 +82,8 @@ public function testShouldCreateLazyContext() $this->assertInstanceOf(SqsContext::class, $context); - $this->assertAttributeEquals(null, 'client', $context); - $this->assertInternalType('callable', $this->readAttribute($context, 'clientFactory')); + $client = $this->readAttribute($context, 'client'); + $this->assertInstanceOf(SqsClient::class, $client); + $this->assertAttributeInstanceOf(\Closure::class, 'inputClient', $client); } } diff --git a/pkg/sqs/Tests/SqsConsumerTest.php b/pkg/sqs/Tests/SqsConsumerTest.php index aa4011a82..ef06c6157 100644 --- a/pkg/sqs/Tests/SqsConsumerTest.php +++ b/pkg/sqs/Tests/SqsConsumerTest.php @@ -3,29 +3,25 @@ namespace Enqueue\Sqs\Tests; use Aws\Result; -use Aws\Sqs\SqsClient; +use Enqueue\Sqs\SqsClient; use Enqueue\Sqs\SqsConsumer; use Enqueue\Sqs\SqsContext; use Enqueue\Sqs\SqsDestination; use Enqueue\Sqs\SqsMessage; use Enqueue\Sqs\SqsProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrMessage; +use Interop\Queue\Consumer; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Message; +use PHPUnit\Framework\TestCase; -class SqsConsumerTest extends \PHPUnit_Framework_TestCase +class SqsConsumerTest extends TestCase { use ClassExtensionTrait; public function testShouldImplementConsumerInterface() { - $this->assertClassImplements(PsrConsumer::class, SqsConsumer::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new SqsConsumer($this->createContextMock(), new SqsDestination('queue')); + $this->assertClassImplements(Consumer::class, SqsConsumer::class); } public function testShouldReturnInstanceOfDestination() @@ -40,10 +36,10 @@ public function testShouldReturnInstanceOfDestination() public function testAcknowledgeShouldThrowIfInstanceOfMessageIsInvalid() { $this->expectException(InvalidMessageException::class); - $this->expectExceptionMessage('The message must be an instance of Enqueue\Sqs\SqsMessage but it is Mock_PsrMessage'); + $this->expectExceptionMessage('The message must be an instance of Enqueue\Sqs\SqsMessage but it is Mock_Message'); $consumer = new SqsConsumer($this->createContextMock(), new SqsDestination('queue')); - $consumer->acknowledge($this->createMock(PsrMessage::class)); + $consumer->acknowledge($this->createMock(Message::class)); } public function testCouldAcknowledgeMessage() @@ -52,13 +48,17 @@ public function testCouldAcknowledgeMessage() $client ->expects($this->once()) ->method('deleteMessage') - ->with($this->identicalTo(['QueueUrl' => 'theQueueUrl', 'ReceiptHandle' => 'theReceipt'])) + ->with($this->identicalTo([ + '@region' => null, + 'QueueUrl' => 'theQueueUrl', + 'ReceiptHandle' => 'theReceipt', + ])) ; $context = $this->createContextMock(); $context ->expects($this->once()) - ->method('getClient') + ->method('getSqsClient') ->willReturn($client) ; $context @@ -74,13 +74,48 @@ public function testCouldAcknowledgeMessage() $consumer->acknowledge($message); } + public function testCouldAcknowledgeMessageWithCustomRegion() + { + $client = $this->createSqsClientMock(); + $client + ->expects($this->once()) + ->method('deleteMessage') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueUrl' => 'theQueueUrl', + 'ReceiptHandle' => 'theReceipt', + ])) + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getSqsClient') + ->willReturn($client) + ; + $context + ->expects($this->once()) + ->method('getQueueUrl') + ->willReturn('theQueueUrl') + ; + + $message = new SqsMessage(); + $message->setReceiptHandle('theReceipt'); + + $destination = new SqsDestination('queue'); + $destination->setRegion('theRegion'); + + $consumer = new SqsConsumer($context, $destination); + $consumer->acknowledge($message); + } + public function testRejectShouldThrowIfInstanceOfMessageIsInvalid() { $this->expectException(InvalidMessageException::class); - $this->expectExceptionMessage('The message must be an instance of Enqueue\Sqs\SqsMessage but it is Mock_PsrMessage'); + $this->expectExceptionMessage('The message must be an instance of Enqueue\Sqs\SqsMessage but it is Mock_Message'); $consumer = new SqsConsumer($this->createContextMock(), new SqsDestination('queue')); - $consumer->reject($this->createMock(PsrMessage::class)); + $consumer->reject($this->createMock(Message::class)); } public function testShouldRejectMessage() @@ -89,13 +124,17 @@ public function testShouldRejectMessage() $client ->expects($this->once()) ->method('deleteMessage') - ->with($this->identicalTo(['QueueUrl' => 'theQueueUrl', 'ReceiptHandle' => 'theReceipt'])) + ->with($this->identicalTo([ + '@region' => null, + 'QueueUrl' => 'theQueueUrl', + 'ReceiptHandle' => 'theReceipt', + ])) ; $context = $this->createContextMock(); $context ->expects($this->once()) - ->method('getClient') + ->method('getSqsClient') ->willReturn($client) ; $context @@ -115,31 +154,63 @@ public function testShouldRejectMessage() $consumer->reject($message); } - public function testShouldRejectMessageAndRequeue() + public function testShouldRejectMessageWithCustomRegion() { $client = $this->createSqsClientMock(); $client ->expects($this->once()) ->method('deleteMessage') - ->with($this->identicalTo(['QueueUrl' => 'theQueueUrl', 'ReceiptHandle' => 'theReceipt'])) + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueUrl' => 'theQueueUrl', + 'ReceiptHandle' => 'theReceipt', + ])) + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getSqsClient') + ->willReturn($client) + ; + $context + ->expects($this->once()) + ->method('getQueueUrl') + ->willReturn('theQueueUrl') + ; + $context + ->expects($this->never()) + ->method('createProducer') ; $message = new SqsMessage(); $message->setReceiptHandle('theReceipt'); $destination = new SqsDestination('queue'); + $destination->setRegion('theRegion'); + + $consumer = new SqsConsumer($context, $destination); + $consumer->reject($message); + } - $producer = $this->createProducerMock(); - $producer + public function testShouldRejectMessageAndRequeue() + { + $client = $this->createSqsClientMock(); + $client ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($destination), $this->identicalTo($message)) + ->method('changeMessageVisibility') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueUrl' => 'theQueueUrl', + 'ReceiptHandle' => 'theReceipt', + 'VisibilityTimeout' => 0, + ])) ; $context = $this->createContextMock(); $context ->expects($this->once()) - ->method('getClient') + ->method('getSqsClient') ->willReturn($client) ; $context @@ -147,12 +218,58 @@ public function testShouldRejectMessageAndRequeue() ->method('getQueueUrl') ->willReturn('theQueueUrl') ; + $context + ->expects($this->never()) + ->method('createProducer') + ; + + $message = new SqsMessage(); + $message->setReceiptHandle('theReceipt'); + + $destination = new SqsDestination('queue'); + $destination->setRegion('theRegion'); + + $consumer = new SqsConsumer($context, $destination); + $consumer->reject($message, true); + } + + public function testShouldRejectMessageAndRequeueWithVisibilityTimeout() + { + $client = $this->createSqsClientMock(); + $client + ->expects($this->once()) + ->method('changeMessageVisibility') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueUrl' => 'theQueueUrl', + 'ReceiptHandle' => 'theReceipt', + 'VisibilityTimeout' => 30, + ])) + ; + + $context = $this->createContextMock(); $context ->expects($this->once()) + ->method('getSqsClient') + ->willReturn($client) + ; + $context + ->expects($this->once()) + ->method('getQueueUrl') + ->willReturn('theQueueUrl') + ; + $context + ->expects($this->never()) ->method('createProducer') - ->willReturn($producer) ; + $message = new SqsMessage(); + $message->setReceiptHandle('theReceipt'); + $message->setRequeueVisibilityTimeout(30); + + $destination = new SqsDestination('queue'); + $destination->setRegion('theRegion'); + $consumer = new SqsConsumer($context, $destination); $consumer->reject($message, true); } @@ -160,6 +277,7 @@ public function testShouldRejectMessageAndRequeue() public function testShouldReceiveMessage() { $expectedAttributes = [ + '@region' => null, 'AttributeNames' => ['All'], 'MessageAttributeNames' => ['All'], 'MaxNumberOfMessages' => 1, @@ -170,8 +288,12 @@ public function testShouldReceiveMessage() $expectedSqsMessage = [ 'Body' => 'The Body', 'ReceiptHandle' => 'The Receipt', + 'MessageId' => 'theMessageId', 'Attributes' => [ - 'ApproximateReceiveCount' => 3, + 'SenderId' => 'AROAX5IAWYILCTYIS3OZ5:foo@bar.com', + 'ApproximateFirstReceiveTimestamp' => '1560512269481', + 'ApproximateReceiveCount' => '3', + 'SentTimestamp' => '1560512260079', ], 'MessageAttributes' => [ 'Headers' => [ @@ -192,7 +314,7 @@ public function testShouldReceiveMessage() $context = $this->createContextMock(); $context ->expects($this->once()) - ->method('getClient') + ->method('getSqsClient') ->willReturn($client) ; $context @@ -211,15 +333,81 @@ public function testShouldReceiveMessage() $this->assertInstanceOf(SqsMessage::class, $result); $this->assertEquals('The Body', $result->getBody()); - $this->assertEquals(['hkey' => 'hvalue'], $result->getHeaders()); + $this->assertEquals(['hkey' => 'hvalue', 'message_id' => 'theMessageId'], $result->getHeaders()); $this->assertEquals(['key' => 'value'], $result->getProperties()); + $this->assertEquals([ + 'SenderId' => 'AROAX5IAWYILCTYIS3OZ5:foo@bar.com', + 'ApproximateFirstReceiveTimestamp' => '1560512269481', + 'ApproximateReceiveCount' => '3', + 'SentTimestamp' => '1560512260079', + ], $result->getAttributes()); $this->assertTrue($result->isRedelivered()); $this->assertEquals('The Receipt', $result->getReceiptHandle()); + $this->assertEquals('theMessageId', $result->getMessageId()); + } + + public function testShouldReceiveMessageWithCustomRegion() + { + $expectedAttributes = [ + '@region' => 'theRegion', + 'AttributeNames' => ['All'], + 'MessageAttributeNames' => ['All'], + 'MaxNumberOfMessages' => 1, + 'QueueUrl' => 'theQueueUrl', + 'WaitTimeSeconds' => 0, + ]; + + $client = $this->createSqsClientMock(); + $client + ->expects($this->once()) + ->method('receiveMessage') + ->with($this->identicalTo($expectedAttributes)) + ->willReturn(new Result(['Messages' => [[ + 'Body' => 'The Body', + 'ReceiptHandle' => 'The Receipt', + 'MessageId' => 'theMessageId', + 'Attributes' => [ + 'ApproximateReceiveCount' => 3, + ], + 'MessageAttributes' => [ + 'Headers' => [ + 'StringValue' => json_encode([['hkey' => 'hvalue'], ['key' => 'value']]), + 'DataType' => 'String', + ], + ], + ]]])) + ; + + $context = $this->createContextMock(); + $context + ->expects($this->once()) + ->method('getSqsClient') + ->willReturn($client) + ; + $context + ->expects($this->once()) + ->method('getQueueUrl') + ->willReturn('theQueueUrl') + ; + $context + ->expects($this->once()) + ->method('createMessage') + ->willReturn(new SqsMessage()) + ; + + $destination = new SqsDestination('queue'); + $destination->setRegion('theRegion'); + + $consumer = new SqsConsumer($context, $destination); + $result = $consumer->receiveNoWait(); + + $this->assertInstanceOf(SqsMessage::class, $result); } public function testShouldReturnNullIfThereIsNoNewMessage() { $expectedAttributes = [ + '@region' => null, 'AttributeNames' => ['All'], 'MessageAttributeNames' => ['All'], 'MaxNumberOfMessages' => 1, @@ -238,7 +426,7 @@ public function testShouldReturnNullIfThereIsNoNewMessage() $context = $this->createContextMock(); $context ->expects($this->once()) - ->method('getClient') + ->method('getSqsClient') ->willReturn($client) ; $context @@ -258,29 +446,25 @@ public function testShouldReturnNullIfThereIsNoNewMessage() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SqsProducer + * @return \PHPUnit\Framework\MockObject\MockObject|SqsProducer */ - private function createProducerMock() + private function createProducerMock(): SqsProducer { return $this->createMock(SqsProducer::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SqsClient + * @return \PHPUnit\Framework\MockObject\MockObject|SqsClient */ - private function createSqsClientMock() + private function createSqsClientMock(): SqsClient { - return $this->getMockBuilder(SqsClient::class) - ->disableOriginalConstructor() - ->setMethods(['deleteMessage', 'receiveMessage']) - ->getMock() - ; + return $this->createMock(SqsClient::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SqsContext + * @return \PHPUnit\Framework\MockObject\MockObject|SqsContext */ - private function createContextMock() + private function createContextMock(): SqsContext { return $this->createMock(SqsContext::class); } diff --git a/pkg/sqs/Tests/SqsContextTest.php b/pkg/sqs/Tests/SqsContextTest.php index 2d5b20d30..5081add41 100644 --- a/pkg/sqs/Tests/SqsContextTest.php +++ b/pkg/sqs/Tests/SqsContextTest.php @@ -3,48 +3,31 @@ namespace Enqueue\Sqs\Tests; use Aws\Result; -use Aws\Sqs\SqsClient; +use Enqueue\Sqs\SqsClient; use Enqueue\Sqs\SqsConsumer; use Enqueue\Sqs\SqsContext; use Enqueue\Sqs\SqsDestination; use Enqueue\Sqs\SqsMessage; use Enqueue\Sqs\SqsProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrQueue; +use Interop\Queue\Context; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\TemporaryQueueNotSupportedException; +use Interop\Queue\Queue; +use PHPUnit\Framework\TestCase; -class SqsContextTest extends \PHPUnit\Framework\TestCase +class SqsContextTest extends TestCase { use ClassExtensionTrait; public function testShouldImplementContextInterface() { - $this->assertClassImplements(PsrContext::class, SqsContext::class); - } - - public function testCouldBeConstructedWithSqsClientAsFirstArgument() - { - new SqsContext($this->createSqsClientMock()); - } - - public function testCouldBeConstructedWithSqsClientFactoryAsFirstArgument() - { - new SqsContext(function () { - return $this->createSqsClientMock(); - }); - } - - public function testThrowIfNeitherSqsClientNorFactoryGiven() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The $client argument must be either Aws\Sqs\SqsClient or callable that returns Aws\Sqs\SqsClient once called.'); - new SqsContext(new \stdClass()); + $this->assertClassImplements(Context::class, SqsContext::class); } public function testShouldAllowCreateEmptyMessage() { - $context = new SqsContext($this->createSqsClientMock()); + $context = new SqsContext($this->createSqsClientMock(), []); $message = $context->createMessage(); @@ -57,7 +40,7 @@ public function testShouldAllowCreateEmptyMessage() public function testShouldAllowCreateCustomMessage() { - $context = new SqsContext($this->createSqsClientMock()); + $context = new SqsContext($this->createSqsClientMock(), []); $message = $context->createMessage('theBody', ['aProp' => 'aPropVal'], ['aHeader' => 'aHeaderVal']); @@ -70,7 +53,9 @@ public function testShouldAllowCreateCustomMessage() public function testShouldCreateQueue() { - $context = new SqsContext($this->createSqsClientMock()); + $context = new SqsContext($this->createSqsClientMock(), [ + 'queue_owner_aws_account_id' => null, + ]); $queue = $context->createQueue('aQueue'); @@ -80,7 +65,9 @@ public function testShouldCreateQueue() public function testShouldAllowCreateTopic() { - $context = new SqsContext($this->createSqsClientMock()); + $context = new SqsContext($this->createSqsClientMock(), [ + 'queue_owner_aws_account_id' => null, + ]); $topic = $context->createTopic('aTopic'); @@ -90,16 +77,16 @@ public function testShouldAllowCreateTopic() public function testThrowNotImplementedOnCreateTmpQueueCall() { - $context = new SqsContext($this->createSqsClientMock()); + $context = new SqsContext($this->createSqsClientMock(), []); + + $this->expectException(TemporaryQueueNotSupportedException::class); - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('SQS transport does not support temporary queues'); $context->createTemporaryQueue(); } public function testShouldCreateProducer() { - $context = new SqsContext($this->createSqsClientMock()); + $context = new SqsContext($this->createSqsClientMock(), []); $producer = $context->createProducer(); @@ -108,17 +95,19 @@ public function testShouldCreateProducer() public function testShouldThrowIfNotSqsDestinationGivenOnCreateConsumer() { - $context = new SqsContext($this->createSqsClientMock()); + $context = new SqsContext($this->createSqsClientMock(), []); $this->expectException(InvalidDestinationException::class); - $this->expectExceptionMessage('The destination must be an instance of Enqueue\Sqs\SqsDestination but got Mock_PsrQueue'); + $this->expectExceptionMessage('The destination must be an instance of Enqueue\Sqs\SqsDestination but got Mock_Queue'); - $context->createConsumer($this->createMock(PsrQueue::class)); + $context->createConsumer($this->createMock(Queue::class)); } public function testShouldCreateConsumer() { - $context = new SqsContext($this->createSqsClientMock()); + $context = new SqsContext($this->createSqsClientMock(), [ + 'queue_owner_aws_account_id' => null, + ]); $queue = $context->createQueue('aQueue'); @@ -133,24 +122,85 @@ public function testShouldAllowDeclareQueue() $sqsClient ->expects($this->once()) ->method('createQueue') - ->with($this->identicalTo(['Attributes' => [], 'QueueName' => 'aQueueName'])) + ->with($this->identicalTo([ + '@region' => null, + 'Attributes' => [], + 'QueueName' => 'aQueueName', + ])) ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) ; - $context = new SqsContext($sqsClient); + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); $queue = $context->createQueue('aQueueName'); $context->declareQueue($queue); } + public function testShouldAllowDeclareQueueWithCustomRegion() + { + $sqsClient = $this->createSqsClientMock(); + $sqsClient + ->expects($this->once()) + ->method('createQueue') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'Attributes' => [], + 'QueueName' => 'aQueueName', + ])) + ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) + ; + + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); + + $queue = $context->createQueue('aQueueName'); + $queue->setRegion('theRegion'); + + $context->declareQueue($queue); + } + public function testShouldAllowDeleteQueue() { $sqsClient = $this->createSqsClientMock(); $sqsClient ->expects($this->once()) ->method('getQueueUrl') - ->with($this->identicalTo(['QueueName' => 'aQueueName'])) + ->with($this->identicalTo([ + '@region' => null, + 'QueueName' => 'aQueueName', + ])) + ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) + ; + $sqsClient + ->expects($this->once()) + ->method('deleteQueue') + ->with($this->identicalTo(['QueueUrl' => 'theQueueUrl'])) + ->willReturn(new Result()) + ; + + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); + + $queue = $context->createQueue('aQueueName'); + + $context->deleteQueue($queue); + } + + public function testShouldAllowDeleteQueueWithCustomRegion() + { + $sqsClient = $this->createSqsClientMock(); + $sqsClient + ->expects($this->once()) + ->method('getQueueUrl') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueName' => 'aQueueName', + ])) ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) ; $sqsClient @@ -160,9 +210,12 @@ public function testShouldAllowDeleteQueue() ->willReturn(new Result()) ; - $context = new SqsContext($sqsClient); + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); $queue = $context->createQueue('aQueueName'); + $queue->setRegion('theRegion'); $context->deleteQueue($queue); } @@ -173,21 +226,61 @@ public function testShouldAllowPurgeQueue() $sqsClient ->expects($this->once()) ->method('getQueueUrl') - ->with($this->identicalTo(['QueueName' => 'aQueueName'])) + ->with($this->identicalTo([ + '@region' => null, + 'QueueName' => 'aQueueName', + ])) ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) ; $sqsClient ->expects($this->once()) ->method('purgeQueue') - ->with($this->identicalTo(['QueueUrl' => 'theQueueUrl'])) + ->with($this->identicalTo([ + '@region' => null, + 'QueueUrl' => 'theQueueUrl', + ])) + ->willReturn(new Result()) + ; + + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); + + $queue = $context->createQueue('aQueueName'); + + $context->purgeQueue($queue); + } + + public function testShouldAllowPurgeQueueWithCustomRegion() + { + $sqsClient = $this->createSqsClientMock(); + $sqsClient + ->expects($this->once()) + ->method('getQueueUrl') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueName' => 'aQueueName', + ])) + ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) + ; + $sqsClient + ->expects($this->once()) + ->method('purgeQueue') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueUrl' => 'theQueueUrl', + ])) ->willReturn(new Result()) ; - $context = new SqsContext($sqsClient); + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); $queue = $context->createQueue('aQueueName'); + $queue->setRegion('theRegion'); - $context->purge($queue); + $context->purgeQueue($queue); } public function testShouldAllowGetQueueUrl() @@ -196,26 +289,139 @@ public function testShouldAllowGetQueueUrl() $sqsClient ->expects($this->once()) ->method('getQueueUrl') - ->with($this->identicalTo(['QueueName' => 'aQueueName'])) + ->with($this->identicalTo([ + '@region' => null, + 'QueueName' => 'aQueueName', + ])) + ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) + ; + + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); + + $context->getQueueUrl(new SqsDestination('aQueueName')); + } + + public function testShouldAllowGetQueueArn() + { + $sqsClient = $this->createSqsClientMock(); + $sqsClient + ->expects($this->once()) + ->method('getQueueUrl') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueName' => 'aQueueName', + ])) + ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) + ; + $sqsClient + ->expects($this->once()) + ->method('getQueueAttributes') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueUrl' => 'theQueueUrl', + 'AttributeNames' => ['QueueArn'], + ])) + ->willReturn(new Result([ + 'Attributes' => [ + 'QueueArn' => 'theQueueArn', + ], + ])) + ; + + $context = new SqsContext($sqsClient, []); + + $queue = $context->createQueue('aQueueName'); + $queue->setRegion('theRegion'); + + $this->assertSame('theQueueArn', $context->getQueueArn($queue)); + } + + public function testShouldAllowGetQueueUrlWithCustomRegion() + { + $sqsClient = $this->createSqsClientMock(); + $sqsClient + ->expects($this->once()) + ->method('getQueueUrl') + ->with($this->identicalTo([ + '@region' => 'theRegion', + 'QueueName' => 'aQueueName', + ])) ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) ; - $context = new SqsContext($sqsClient); + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); + + $queue = new SqsDestination('aQueueName'); + $queue->setRegion('theRegion'); + + $context->getQueueUrl($queue); + } + + public function testShouldAllowGetQueueUrlFromAnotherAWSAccountSetGlobally() + { + $sqsClient = $this->createSqsClientMock(); + $sqsClient + ->expects($this->once()) + ->method('getQueueUrl') + ->with($this->identicalTo([ + '@region' => null, + 'QueueName' => 'aQueueName', + 'QueueOwnerAWSAccountId' => 'anotherAWSAccountID', + ])) + ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) + ; + + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => 'anotherAWSAccountID', + ]); $context->getQueueUrl(new SqsDestination('aQueueName')); } + public function testShouldAllowGetQueueUrlFromAnotherAWSAccountSetPerQueue() + { + $sqsClient = $this->createSqsClientMock(); + $sqsClient + ->expects($this->once()) + ->method('getQueueUrl') + ->with($this->identicalTo([ + '@region' => null, + 'QueueName' => 'aQueueName', + 'QueueOwnerAWSAccountId' => 'anotherAWSAccountID', + ])) + ->willReturn(new Result(['QueueUrl' => 'theQueueUrl'])) + ; + + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); + + $queue = new SqsDestination('aQueueName'); + $queue->setQueueOwnerAWSAccountId('anotherAWSAccountID'); + + $context->getQueueUrl($queue); + } + public function testShouldThrowExceptionIfGetQueueUrlResultHasNoQueueUrlProperty() { $sqsClient = $this->createSqsClientMock(); $sqsClient ->expects($this->once()) ->method('getQueueUrl') - ->with($this->identicalTo(['QueueName' => 'aQueueName'])) + ->with($this->identicalTo([ + '@region' => null, + 'QueueName' => 'aQueueName', + ])) ->willReturn(new Result([])) ; - $context = new SqsContext($sqsClient); + $context = new SqsContext($sqsClient, [ + 'queue_owner_aws_account_id' => null, + ]); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('QueueUrl cannot be resolved. queueName: "aQueueName"'); @@ -224,14 +430,10 @@ public function testShouldThrowExceptionIfGetQueueUrlResultHasNoQueueUrlProperty } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SqsClient + * @return \PHPUnit\Framework\MockObject\MockObject|SqsClient */ - private function createSqsClientMock() + private function createSqsClientMock(): SqsClient { - return $this->getMockBuilder(SqsClient::class) - ->disableOriginalConstructor() - ->setMethods(['deleteQueue', 'purgeQueue', 'createQueue', 'getQueueUrl']) - ->getMock() - ; + return $this->createMock(SqsClient::class); } } diff --git a/pkg/sqs/Tests/SqsDestinationTest.php b/pkg/sqs/Tests/SqsDestinationTest.php index 42e14178b..724ce0c42 100644 --- a/pkg/sqs/Tests/SqsDestinationTest.php +++ b/pkg/sqs/Tests/SqsDestinationTest.php @@ -4,17 +4,18 @@ use Enqueue\Sqs\SqsDestination; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrQueue; -use Interop\Queue\PsrTopic; +use Interop\Queue\Queue; +use Interop\Queue\Topic; +use PHPUnit\Framework\TestCase; -class SqsDestinationTest extends \PHPUnit_Framework_TestCase +class SqsDestinationTest extends TestCase { use ClassExtensionTrait; public function testShouldImplementsTopicAndQueueInterfaces() { - $this->assertClassImplements(PsrTopic::class, SqsDestination::class); - $this->assertClassImplements(PsrQueue::class, SqsDestination::class); + $this->assertClassImplements(Topic::class, SqsDestination::class); + $this->assertClassImplements(Queue::class, SqsDestination::class); } public function testShouldReturnNameSetInConstructor() diff --git a/pkg/sqs/Tests/SqsMessageTest.php b/pkg/sqs/Tests/SqsMessageTest.php index a6d4e25fb..5da37b531 100644 --- a/pkg/sqs/Tests/SqsMessageTest.php +++ b/pkg/sqs/Tests/SqsMessageTest.php @@ -16,6 +16,7 @@ public function testCouldBeConstructedWithoutArguments() $this->assertSame('', $message->getBody()); $this->assertSame([], $message->getProperties()); $this->assertSame([], $message->getHeaders()); + $this->assertSame([], $message->getAttributes()); } public function testCouldBeConstructedWithOptionalArguments() @@ -90,4 +91,18 @@ public function testShouldAllowGetReceiptHandle() $this->assertSame('theId', $message->getReceiptHandle()); } + + public function testShouldAllowSettingAndGettingAttributes() + { + $message = new SqsMessage(); + $message->setAttributes($attributes = [ + 'SenderId' => 'AROAX5IAWYILCTYIS3OZ5:foo@bar.com', + 'ApproximateFirstReceiveTimestamp' => '1560512269481', + 'ApproximateReceiveCount' => '2', + 'SentTimestamp' => '1560512260079', + ]); + + $this->assertSame($attributes, $message->getAttributes()); + $this->assertSame($attributes['SenderId'], $message->getAttribute('SenderId')); + } } diff --git a/pkg/sqs/Tests/SqsProducerTest.php b/pkg/sqs/Tests/SqsProducerTest.php index 2e37c31c6..35cb9850b 100644 --- a/pkg/sqs/Tests/SqsProducerTest.php +++ b/pkg/sqs/Tests/SqsProducerTest.php @@ -3,39 +3,35 @@ namespace Enqueue\Sqs\Tests; use Aws\Result; -use Aws\Sqs\SqsClient; +use Enqueue\Sqs\SqsClient; use Enqueue\Sqs\SqsContext; use Enqueue\Sqs\SqsDestination; use Enqueue\Sqs\SqsMessage; use Enqueue\Sqs\SqsProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrDestination; -use Interop\Queue\PsrProducer; +use Interop\Queue\Destination; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Producer; +use PHPUnit\Framework\TestCase; -class SqsProducerTest extends \PHPUnit_Framework_TestCase +class SqsProducerTest extends TestCase { use ClassExtensionTrait; public function testShouldImplementProducerInterface() { - $this->assertClassImplements(PsrProducer::class, SqsProducer::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new SqsProducer($this->createSqsContextMock()); + $this->assertClassImplements(Producer::class, SqsProducer::class); } public function testShouldThrowIfBodyOfInvalidType() { $this->expectException(InvalidMessageException::class); - $this->expectExceptionMessage('The message body must be a non-empty string. Got: stdClass'); + $this->expectExceptionMessage('The message body must be a non-empty string.'); $producer = new SqsProducer($this->createSqsContextMock()); - $message = new SqsMessage(new \stdClass()); + $message = new SqsMessage(''); $producer->send(new SqsDestination(''), $message); } @@ -43,11 +39,11 @@ public function testShouldThrowIfBodyOfInvalidType() public function testShouldThrowIfDestinationOfInvalidType() { $this->expectException(InvalidDestinationException::class); - $this->expectExceptionMessage('The destination must be an instance of Enqueue\Sqs\SqsDestination but got Mock_PsrDestinat'); + $this->expectExceptionMessage('The destination must be an instance of Enqueue\Sqs\SqsDestination but got Mock_Destinat'); $producer = new SqsProducer($this->createSqsContextMock()); - $producer->send($this->createMock(PsrDestination::class), new SqsMessage()); + $producer->send($this->createMock(Destination::class), new SqsMessage()); } public function testShouldThrowIfSendMessageFailed() @@ -67,8 +63,8 @@ public function testShouldThrowIfSendMessageFailed() ; $context ->expects($this->once()) - ->method('getClient') - ->will($this->returnValue($client)) + ->method('getSqsClient') + ->willReturn($client) ; $destination = new SqsDestination('queue-name'); @@ -84,6 +80,7 @@ public function testShouldThrowIfSendMessageFailed() public function testShouldSendMessage() { $expectedArguments = [ + '@region' => null, 'MessageAttributes' => [ 'Headers' => [ 'DataType' => 'String', @@ -102,7 +99,7 @@ public function testShouldSendMessage() ->expects($this->once()) ->method('sendMessage') ->with($this->identicalTo($expectedArguments)) - ->willReturn(new Result()) + ->willReturn(new Result(['MessageId' => 'theMessageId'])) ; $context = $this->createSqsContextMock(); @@ -113,8 +110,8 @@ public function testShouldSendMessage() ; $context ->expects($this->once()) - ->method('getClient') - ->will($this->returnValue($client)) + ->method('getSqsClient') + ->willReturn($client) ; $destination = new SqsDestination('queue-name'); @@ -123,8 +120,48 @@ public function testShouldSendMessage() $message->setMessageDeduplicationId('theDeduplicationId'); $message->setMessageGroupId('groupId'); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Message was not sent'); + $producer = new SqsProducer($context); + $producer->send($destination, $message); + } + + public function testShouldSendMessageWithCustomRegion() + { + $expectedArguments = [ + '@region' => 'theRegion', + 'MessageAttributes' => [ + 'Headers' => [ + 'DataType' => 'String', + 'StringValue' => '[[],[]]', + ], + ], + 'MessageBody' => 'theBody', + 'QueueUrl' => 'theQueueUrl', + ]; + + $client = $this->createSqsClientMock(); + $client + ->expects($this->once()) + ->method('sendMessage') + ->with($this->identicalTo($expectedArguments)) + ->willReturn(new Result(['MessageId' => 'theMessageId'])) + ; + + $context = $this->createSqsContextMock(); + $context + ->expects($this->once()) + ->method('getQueueUrl') + ->willReturn('theQueueUrl') + ; + $context + ->expects($this->once()) + ->method('getSqsClient') + ->willReturn($client) + ; + + $destination = new SqsDestination('queue-name'); + $destination->setRegion('theRegion'); + + $message = new SqsMessage('theBody'); $producer = new SqsProducer($context); $producer->send($destination, $message); @@ -133,6 +170,7 @@ public function testShouldSendMessage() public function testShouldSendDelayedMessage() { $expectedArguments = [ + '@region' => null, 'MessageAttributes' => [ 'Headers' => [ 'DataType' => 'String', @@ -151,7 +189,7 @@ public function testShouldSendDelayedMessage() ->expects($this->once()) ->method('sendMessage') ->with($this->identicalTo($expectedArguments)) - ->willReturn(new Result()) + ->willReturn(new Result(['MessageId' => 'theMessageId'])) ; $context = $this->createSqsContextMock(); @@ -162,8 +200,8 @@ public function testShouldSendDelayedMessage() ; $context ->expects($this->once()) - ->method('getClient') - ->will($this->returnValue($client)) + ->method('getSqsClient') + ->willReturn($client) ; $destination = new SqsDestination('queue-name'); @@ -172,32 +210,24 @@ public function testShouldSendDelayedMessage() $message->setMessageDeduplicationId('theDeduplicationId'); $message->setMessageGroupId('groupId'); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Message was not sent'); - $producer = new SqsProducer($context); $producer->setDeliveryDelay(5000); $producer->send($destination, $message); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SqsContext + * @return \PHPUnit\Framework\MockObject\MockObject|SqsContext */ - private function createSqsContextMock() + private function createSqsContextMock(): SqsContext { return $this->createMock(SqsContext::class); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|SqsClient + * @return \PHPUnit\Framework\MockObject\MockObject|SqsClient */ - private function createSqsClientMock() + private function createSqsClientMock(): SqsClient { - return $this - ->getMockBuilder(SqsClient::class) - ->disableOriginalConstructor() - ->setMethods(['sendMessage']) - ->getMock() - ; + return $this->createMock(SqsClient::class); } } diff --git a/pkg/sqs/Tests/Symfony/SqsTransportFactoryTest.php b/pkg/sqs/Tests/Symfony/SqsTransportFactoryTest.php deleted file mode 100644 index 19bdb3cb8..000000000 --- a/pkg/sqs/Tests/Symfony/SqsTransportFactoryTest.php +++ /dev/null @@ -1,134 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, SqsTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new SqsTransportFactory(); - - $this->assertEquals('sqs', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new SqsTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new SqsTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), [[ - 'key' => 'theKey', - 'secret' => 'theSecret', - 'token' => 'theToken', - 'region' => 'theRegion', - 'retries' => 5, - 'version' => 'theVersion', - 'lazy' => false, - 'endpoint' => 'theEndpoint', - ]]); - - $this->assertEquals([ - 'key' => 'theKey', - 'secret' => 'theSecret', - 'token' => 'theToken', - 'region' => 'theRegion', - 'retries' => 5, - 'version' => 'theVersion', - 'lazy' => false, - 'endpoint' => 'theEndpoint', - 'client' => null, - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new SqsTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'key' => 'theKey', - 'secret' => 'theSecret', - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(SqsConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'key' => 'theKey', - 'secret' => 'theSecret', - ]], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new SqsTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'key' => 'theKey', - 'secret' => 'theSecret', - ]); - - $this->assertEquals('enqueue.transport.sqs.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.sqs.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.sqs.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new SqsTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.sqs.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(SqsDriver::class, $driver->getClass()); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(0)); - $this->assertEquals('enqueue.transport.sqs.context', (string) $driver->getArgument(0)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(1)); - $this->assertEquals('enqueue.client.config', (string) $driver->getArgument(1)); - - $this->assertInstanceOf(Reference::class, $driver->getArgument(2)); - $this->assertEquals('enqueue.client.meta.queue_meta_registry', (string) $driver->getArgument(2)); - } -} diff --git a/pkg/sqs/composer.json b/pkg/sqs/composer.json index c50b3bc7d..2ddc1b267 100644 --- a/pkg/sqs/composer.json +++ b/pkg/sqs/composer.json @@ -6,17 +6,15 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1", - "aws/aws-sdk-php": "~3.26" + "php": "^8.1", + "queue-interop/queue-interop": "^0.8", + "enqueue/dsn": "^0.10", + "aws/aws-sdk-php": "^3.290" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -31,13 +29,10 @@ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/sqs/examples/consume.php b/pkg/sqs/examples/consume.php index a63f41fba..d82274d80 100644 --- a/pkg/sqs/examples/consume.php +++ b/pkg/sqs/examples/consume.php @@ -12,18 +12,12 @@ if ($autoload) { require_once $autoload; } else { - throw new \LogicException('Composer autoload was not found'); + throw new LogicException('Composer autoload was not found'); } use Enqueue\Sqs\SqsConnectionFactory; -$config = [ - 'key' => getenv('AWS_SQS_KEY'), - 'secret' => getenv('AWS_SQS_SECRET'), - 'region' => getenv('AWS_SQS_REGION'), -]; - -$factory = new SqsConnectionFactory($config); +$factory = new SqsConnectionFactory(getenv('SQS_DSN')); $context = $factory->createContext(); $queue = $context->createQueue('enqueue'); @@ -32,7 +26,7 @@ while (true) { if ($m = $consumer->receive(20000)) { $consumer->acknowledge($m); - echo 'Received message: '.$m->getBody().PHP_EOL; + echo 'Received message: '.$m->getBody().\PHP_EOL; } } diff --git a/pkg/sqs/examples/produce.php b/pkg/sqs/examples/produce.php index cb1a4d8e6..a9ba3e3b7 100644 --- a/pkg/sqs/examples/produce.php +++ b/pkg/sqs/examples/produce.php @@ -12,18 +12,12 @@ if ($autoload) { require_once $autoload; } else { - throw new \LogicException('Composer autoload was not found'); + throw new LogicException('Composer autoload was not found'); } use Enqueue\Sqs\SqsConnectionFactory; -$config = [ - 'key' => getenv('AWS_SQS_KEY'), - 'secret' => getenv('AWS_SQS_SECRET'), - 'region' => getenv('AWS_SQS_REGION'), -]; - -$factory = new SqsConnectionFactory($config); +$factory = new SqsConnectionFactory(getenv('SQS_DSN')); $context = $factory->createContext(); $queue = $context->createQueue('enqueue'); @@ -33,7 +27,7 @@ while (true) { $context->createProducer()->send($queue, $message); - echo 'Sent message: '.$message->getBody().PHP_EOL; + echo 'Sent message: '.$message->getBody().\PHP_EOL; sleep(1); } diff --git a/pkg/sqs/phpunit.xml.dist b/pkg/sqs/phpunit.xml.dist index 7c026b4e6..8fbb94ebf 100644 --- a/pkg/sqs/phpunit.xml.dist +++ b/pkg/sqs/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/stomp/.github/workflows/ci.yml b/pkg/stomp/.github/workflows/ci.yml new file mode 100644 index 000000000..0492424e8 --- /dev/null +++ b/pkg/stomp/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/stomp/.travis.yml b/pkg/stomp/.travis.yml deleted file mode 100644 index b9cf57fc9..000000000 --- a/pkg/stomp/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/stomp/BufferedStompClient.php b/pkg/stomp/BufferedStompClient.php index 4ca5a3e99..e2c54d731 100644 --- a/pkg/stomp/BufferedStompClient.php +++ b/pkg/stomp/BufferedStompClient.php @@ -3,6 +3,7 @@ namespace Enqueue\Stomp; use Stomp\Client; +use Stomp\Transport\Frame; class BufferedStompClient extends Client { @@ -47,12 +48,9 @@ public function getBufferSize() } /** - * @param string $subscriptionId - * @param int|float $timeout - * - * @return \Stomp\Transport\Frame + * Timeout is in milliseconds. */ - public function readMessageFrame($subscriptionId, $timeout) + public function readMessageFrame(string $subscriptionId, int $timeout): ?Frame { // pop up frame from the buffer if (isset($this->buffer[$subscriptionId]) && ($frame = array_shift($this->buffer[$subscriptionId]))) { @@ -63,18 +61,18 @@ public function readMessageFrame($subscriptionId, $timeout) // do nothing when buffer is full if ($this->currentBufferSize >= $this->bufferSize) { - return; + return null; } $startTime = microtime(true); - $remainingTimeout = $timeout * 1000000; + $remainingTimeout = $timeout * 1000; while (true) { $this->getConnection()->setReadTimeout(0, $remainingTimeout); // there is nothing to read if (false === $frame = $this->readFrame()) { - return; + return null; } if ('MESSAGE' !== $frame->getCommand()) { @@ -95,7 +93,7 @@ public function readMessageFrame($subscriptionId, $timeout) $remainingTimeout -= (microtime(true) - $startTime) * 1000000; if ($remainingTimeout <= 0) { - return; + return null; } continue; @@ -105,9 +103,6 @@ public function readMessageFrame($subscriptionId, $timeout) } } - /** - * {@inheritdoc} - */ public function disconnect($sync = false) { parent::disconnect($sync); diff --git a/pkg/stomp/Client/ManagementClient.php b/pkg/stomp/Client/ManagementClient.php deleted file mode 100644 index be4e2da12..000000000 --- a/pkg/stomp/Client/ManagementClient.php +++ /dev/null @@ -1,77 +0,0 @@ -client = $client; - $this->vhost = $vhost; - } - - /** - * @param string $vhost - * @param string $host - * @param int $port - * @param string $login - * @param string $password - * - * @return ManagementClient - */ - public static function create($vhost = '/', $host = 'localhost', $port = 15672, $login = 'guest', $password = 'guest') - { - return new static(new Client(null, 'http://'.$host.':'.$port, $login, $password), $vhost); - } - - /** - * @param string $name - * @param array $options - * - * @return array - */ - public function declareQueue($name, $options) - { - return $this->client->queues()->create($this->vhost, $name, $options); - } - - /** - * @param string $name - * @param array $options - * - * @return array - */ - public function declareExchange($name, $options) - { - return $this->client->exchanges()->create($this->vhost, $name, $options); - } - - /** - * @param string $exchange - * @param string $queue - * @param string $routingKey - * @param array $arguments - * - * @return array - */ - public function bind($exchange, $queue, $routingKey, $arguments = null) - { - return $this->client->bindings()->create($this->vhost, $exchange, $queue, $routingKey, $arguments); - } -} diff --git a/pkg/stomp/Client/RabbitMqStompDriver.php b/pkg/stomp/Client/RabbitMqStompDriver.php deleted file mode 100644 index 101c11631..000000000 --- a/pkg/stomp/Client/RabbitMqStompDriver.php +++ /dev/null @@ -1,269 +0,0 @@ -context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - $this->management = $management; - - $this->priorityMap = [ - MessagePriority::VERY_LOW => 0, - MessagePriority::LOW => 1, - MessagePriority::NORMAL => 2, - MessagePriority::HIGH => 3, - MessagePriority::VERY_HIGH => 4, - ]; - } - - /** - * {@inheritdoc} - * - * @return StompMessage - */ - public function createTransportMessage(Message $message) - { - $transportMessage = parent::createTransportMessage($message); - - if ($message->getExpire()) { - $transportMessage->setHeader('expiration', (string) ($message->getExpire() * 1000)); - } - - if ($priority = $message->getPriority()) { - if (false == array_key_exists($priority, $this->priorityMap)) { - throw new \LogicException(sprintf('Cant convert client priority to transport: "%s"', $priority)); - } - - $transportMessage->setHeader('priority', $this->priorityMap[$priority]); - } - - if ($message->getDelay()) { - if (false == $this->config->getTransportOption('delay_plugin_installed', false)) { - throw new \LogicException('The message delaying is not supported. In order to use delay feature install RabbitMQ delay plugin.'); - } - - $transportMessage->setHeader('x-delay', (string) ($message->getDelay() * 1000)); - } - - return $transportMessage; - } - - /** - * @param StompMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = parent::createClientMessage($message); - - $headers = $clientMessage->getHeaders(); - unset( - $headers['x-delay'], - $headers['expiration'], - $headers['priority'] - ); - $clientMessage->setHeaders($headers); - - if ($delay = $message->getHeader('x-delay')) { - if (false == is_numeric($delay)) { - throw new \LogicException(sprintf('x-delay header is not numeric. "%s"', $delay)); - } - - $clientMessage->setDelay((int) ((int) $delay) / 1000); - } - - if ($expiration = $message->getHeader('expiration')) { - if (false == is_numeric($expiration)) { - throw new \LogicException(sprintf('expiration header is not numeric. "%s"', $expiration)); - } - - $clientMessage->setExpire((int) ((int) $expiration) / 1000); - } - - if ($priority = $message->getHeader('priority')) { - if (false === $clientPriority = array_search($priority, $this->priorityMap, true)) { - throw new \LogicException(sprintf('Cant convert transport priority to client: "%s"', $priority)); - } - - $clientMessage->setPriority($clientPriority); - } - - return $clientMessage; - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - if ($message->getDelay()) { - $destination = $this->createDelayedTopic($destination); - } - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function createQueue($queueName) - { - $queue = parent::createQueue($queueName); - $queue->setHeader('x-max-priority', 4); - - return $queue; - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $log = function ($text, ...$args) use ($logger) { - $logger->debug(sprintf('[RabbitMqStompDriver] '.$text, ...$args)); - }; - - if (false == $this->config->getTransportOption('management_plugin_installed', false)) { - $log('Could not setup broker. The option `management_plugin_installed` is not enabled. Please enable that option and install rabbit management plugin'); - - return; - } - - // setup router - $routerExchange = $this->config->createTransportRouterTopicName($this->config->getRouterTopicName()); - $log('Declare router exchange: %s', $routerExchange); - $this->management->declareExchange($routerExchange, [ - 'type' => 'fanout', - 'durable' => true, - 'auto_delete' => false, - ]); - - $routerQueue = $this->config->createTransportQueueName($this->config->getRouterQueueName()); - $log('Declare router queue: %s', $routerQueue); - $this->management->declareQueue($routerQueue, [ - 'auto_delete' => false, - 'durable' => true, - 'arguments' => [ - 'x-max-priority' => 4, - ], - ]); - - $log('Bind router queue to exchange: %s -> %s', $routerQueue, $routerExchange); - $this->management->bind($routerExchange, $routerQueue, $routerQueue); - - // setup queues - foreach ($this->queueMetaRegistry->getQueuesMeta() as $meta) { - $queue = $this->config->createTransportQueueName($meta->getClientName()); - - $log('Declare processor queue: %s', $queue); - $this->management->declareQueue($queue, [ - 'auto_delete' => false, - 'durable' => true, - 'arguments' => [ - 'x-max-priority' => 4, - ], - ]); - } - - // setup delay exchanges - if ($this->config->getTransportOption('delay_plugin_installed', false)) { - foreach ($this->queueMetaRegistry->getQueuesMeta() as $meta) { - $queue = $this->config->createTransportQueueName($meta->getClientName()); - $delayExchange = $queue.'.delayed'; - - $log('Declare delay exchange: %s', $delayExchange); - $this->management->declareExchange($delayExchange, [ - 'type' => 'x-delayed-message', - 'durable' => true, - 'auto_delete' => false, - 'arguments' => [ - 'x-delayed-type' => 'direct', - ], - ]); - - $log('Bind processor queue to delay exchange: %s -> %s', $queue, $delayExchange); - $this->management->bind($delayExchange, $queue, $queue); - } - } else { - $log('Delay exchange and bindings are not setup. if you\'d like to use delays please install delay rabbitmq plugin and set delay_plugin_installed option to true'); - } - } - - /** - * @param StompDestination $queue - * - * @return StompDestination - */ - private function createDelayedTopic(StompDestination $queue) - { - // in order to use delay feature make sure the rabbitmq_delayed_message_exchange plugin is installed. - $destination = $this->context->createTopic($queue->getStompName().'.delayed'); - $destination->setType(StompDestination::TYPE_EXCHANGE); - $destination->setDurable(true); - $destination->setAutoDelete(false); - $destination->setRoutingKey($queue->getStompName()); - - return $destination; - } -} diff --git a/pkg/stomp/Client/StompDriver.php b/pkg/stomp/Client/StompDriver.php deleted file mode 100644 index 421680881..000000000 --- a/pkg/stomp/Client/StompDriver.php +++ /dev/null @@ -1,191 +0,0 @@ -context = $context; - $this->config = $config; - $this->queueMetaRegistry = $queueMetaRegistry; - } - - /** - * {@inheritdoc} - */ - public function sendToRouter(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_TOPIC_NAME)) { - throw new \LogicException('Topic name parameter is required but is not set'); - } - - $topic = $this->createRouterTopic(); - $transportMessage = $this->createTransportMessage($message); - - $this->context->createProducer()->send($topic, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function sendToProcessor(Message $message) - { - if (false == $message->getProperty(Config::PARAMETER_PROCESSOR_NAME)) { - throw new \LogicException('Processor name parameter is required but is not set'); - } - - if (false == $queueName = $message->getProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME)) { - throw new \LogicException('Queue name parameter is required but is not set'); - } - - $transportMessage = $this->createTransportMessage($message); - $destination = $this->createQueue($queueName); - - $this->context->createProducer()->send($destination, $transportMessage); - } - - /** - * {@inheritdoc} - */ - public function setupBroker(LoggerInterface $logger = null) - { - $logger = $logger ?: new NullLogger(); - $logger->debug('[StompDriver] Stomp protocol does not support broker configuration'); - } - - /** - * @return StompMessage - * - * {@inheritdoc} - */ - public function createTransportMessage(Message $message) - { - $headers = $message->getHeaders(); - $headers['content-type'] = $message->getContentType(); - - $transportMessage = $this->context->createMessage(); - $transportMessage->setHeaders($headers); - $transportMessage->setPersistent(true); - $transportMessage->setBody($message->getBody()); - $transportMessage->setProperties($message->getProperties()); - - if ($message->getMessageId()) { - $transportMessage->setMessageId($message->getMessageId()); - } - - if ($message->getTimestamp()) { - $transportMessage->setTimestamp($message->getTimestamp()); - } - - if ($message->getReplyTo()) { - $transportMessage->setReplyTo($message->getReplyTo()); - } - - if ($message->getCorrelationId()) { - $transportMessage->setCorrelationId($message->getCorrelationId()); - } - - return $transportMessage; - } - - /** - * @param StompMessage $message - * - * {@inheritdoc} - */ - public function createClientMessage(PsrMessage $message) - { - $clientMessage = new Message(); - - $headers = $message->getHeaders(); - unset( - $headers['content-type'], - $headers['message_id'], - $headers['timestamp'], - $headers['reply-to'], - $headers['correlation_id'] - ); - - $clientMessage->setHeaders($headers); - $clientMessage->setBody($message->getBody()); - $clientMessage->setProperties($message->getProperties()); - - $clientMessage->setContentType($message->getHeader('content-type')); - - $clientMessage->setMessageId($message->getMessageId()); - $clientMessage->setTimestamp($message->getTimestamp()); - $clientMessage->setReplyTo($message->getReplyTo()); - $clientMessage->setCorrelationId($message->getCorrelationId()); - - return $clientMessage; - } - - /** - * {@inheritdoc} - */ - public function createQueue($queueName) - { - $transportName = $this->queueMetaRegistry->getQueueMeta($queueName)->getTransportName(); - - $queue = $this->context->createQueue($transportName); - $queue->setDurable(true); - $queue->setAutoDelete(false); - $queue->setExclusive(false); - - return $queue; - } - - /** - * {@inheritdoc} - */ - public function getConfig() - { - return $this->config; - } - - /** - * @return StompDestination - */ - private function createRouterTopic() - { - $topic = $this->context->createTopic( - $this->config->createTransportRouterTopicName($this->config->getRouterTopicName()) - ); - $topic->setDurable(true); - $topic->setAutoDelete(false); - - return $topic; - } -} diff --git a/pkg/stomp/ExtensionType.php b/pkg/stomp/ExtensionType.php new file mode 100644 index 000000000..c1c265f68 --- /dev/null +++ b/pkg/stomp/ExtensionType.php @@ -0,0 +1,12 @@ +Supporting Enqueue + +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # STOMP Transport [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/stomp.png?branch=master)](https://travis-ci.org/php-enqueue/stomp) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/stomp/ci.yml?branch=master)](https://github.com/php-enqueue/stomp/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/stomp/d/total.png)](https://packagist.org/packages/enqueue/stomp) [![Latest Stable Version](https://poser.pugx.org/enqueue/stomp/version.png)](https://packagist.org/packages/enqueue/stomp) -This is an implementation of PSR specification. It allows you to send and consume message via STOMP protocol. +This is an implementation of Queue Interop specification. It allows you to send and consume message via STOMP protocol. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/transport/stomp/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/stomp/StompConnectionFactory.php b/pkg/stomp/StompConnectionFactory.php index 275ad14fa..da9c6f9bd 100644 --- a/pkg/stomp/StompConnectionFactory.php +++ b/pkg/stomp/StompConnectionFactory.php @@ -1,12 +1,24 @@ parseDsn($config); } elseif (is_array($config)) { + if (array_key_exists('dsn', $config)) { + $config = array_replace($config, $this->parseDsn($config['dsn'])); + + unset($config['dsn']); + } } else { throw new \LogicException('The config must be either an array of options, a DSN string or null'); } @@ -53,25 +70,21 @@ public function __construct($config = 'stomp:') } /** - * {@inheritdoc} - * * @return StompContext */ - public function createContext() + public function createContext(): Context { - if ($this->config['lazy']) { - return new StompContext(function () { - return $this->establishConnection(); - }); - } + $stomp = $this->config['lazy'] + ? function () { return $this->establishConnection(); } + : $this->establishConnection(); - return new StompContext($this->establishConnection()); + $target = $this->config['target']; + $detectTransientConnections = (bool) $this->config['detect_transient_connections']; + + return new StompContext($stomp, $target, $detectTransientConnections); } - /** - * @return BufferedStompClient - */ - private function establishConnection() + private function establishConnection(): BufferedStompClient { if (false == $this->stomp) { $config = $this->config; @@ -79,11 +92,22 @@ private function establishConnection() $scheme = (true === $config['ssl_on']) ? 'ssl' : 'tcp'; $uri = $scheme.'://'.$config['host'].':'.$config['port']; $connection = new Connection($uri, $config['connection_timeout']); + $connection->setWriteTimeout($config['write_timeout']); + $connection->setReadTimeout($config['read_timeout']); + + if ($config['send_heartbeat']) { + $connection->getObservers()->addObserver(new HeartbeatEmitter($connection)); + } + + if ($config['receive_heartbeat']) { + $connection->getObservers()->addObserver(new ServerAliveObserver()); + } $this->stomp = new BufferedStompClient($connection, $config['buffer_size']); $this->stomp->setLogin($config['login'], $config['password']); $this->stomp->setVhostname($config['vhost']); $this->stomp->setSync($config['sync']); + $this->stomp->setHeartbeat($config['send_heartbeat'], $config['receive_heartbeat']); $this->stomp->connect(); } @@ -91,42 +115,46 @@ private function establishConnection() return $this->stomp; } - /** - * @param string $dsn - * - * @return array - */ - private function parseDsn($dsn) + private function parseDsn(string $dsn): array { - if (false === strpos($dsn, 'stomp:')) { - throw new \LogicException(sprintf('The given DSN "%s" is not supported. Must start with "stomp:".', $dsn)); - } + $dsn = Dsn::parseFirst($dsn); - if (false === $config = parse_url(http://github.com/$dsn)) { - throw new \LogicException(sprintf('Failed to parse DSN "%s"', $dsn)); + if ('stomp' !== $dsn->getSchemeProtocol()) { + throw new \LogicException('The given DSN is not supported. Must start with "stomp:".'); } - if ($query = parse_url(http://github.com/$dsn,%20PHP_URL_QUERY)) { - $queryConfig = []; - parse_str($query, $queryConfig); - - $config = array_replace($queryConfig, $config); + $schemeExtension = current($dsn->getSchemeExtensions()); + if (false === $schemeExtension) { + $schemeExtension = ExtensionType::RABBITMQ; } - unset($config['query'], $config['scheme']); - - $config['sync'] = empty($config['sync']) ? false : true; - $config['lazy'] = empty($config['lazy']) ? false : true; + if (false === in_array($schemeExtension, self::SUPPORTED_SCHEMES, true)) { + throw new \LogicException(sprintf('The given DSN is not supported. The scheme extension "%s" provided is not supported. It must be one of %s.', $schemeExtension, implode(', ', self::SUPPORTED_SCHEMES))); + } - return $config; + return array_filter(array_replace($dsn->getQuery(), [ + 'target' => $schemeExtension, + 'host' => $dsn->getHost(), + 'port' => $dsn->getPort(), + 'login' => $dsn->getUser(), + 'password' => $dsn->getPassword(), + 'vhost' => null !== $dsn->getPath() ? ltrim($dsn->getPath(), '/') : null, + 'buffer_size' => $dsn->getDecimal('buffer_size'), + 'connection_timeout' => $dsn->getDecimal('connection_timeout'), + 'sync' => $dsn->getBool('sync'), + 'lazy' => $dsn->getBool('lazy'), + 'ssl_on' => $dsn->getBool('ssl_on'), + 'write_timeout' => $dsn->getDecimal('write_timeout'), + 'read_timeout' => $dsn->getDecimal('read_timeout'), + 'send_heartbeat' => $dsn->getDecimal('send_heartbeat'), + 'receive_heartbeat' => $dsn->getDecimal('receive_heartbeat'), + ]), function ($value) { return null !== $value; }); } - /** - * @return array - */ - private function defaultConfig() + private function defaultConfig(): array { return [ + 'target' => ExtensionType::RABBITMQ, 'host' => 'localhost', 'port' => 61613, 'login' => 'guest', @@ -137,6 +165,11 @@ private function defaultConfig() 'sync' => false, 'lazy' => true, 'ssl_on' => false, + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, ]; } } diff --git a/pkg/stomp/StompConsumer.php b/pkg/stomp/StompConsumer.php index 34d56359e..5a80be890 100644 --- a/pkg/stomp/StompConsumer.php +++ b/pkg/stomp/StompConsumer.php @@ -1,18 +1,23 @@ stomp = $stomp; @@ -61,10 +62,7 @@ public function __construct(BufferedStompClient $stomp, StompDestination $queue) ; } - /** - * @param string $mode - */ - public function setAckMode($mode) + public function setAckMode(string $mode): void { if (false === in_array($mode, [self::ACK_AUTO, self::ACK_CLIENT, self::ACK_CLIENT_INDIVIDUAL], true)) { throw new \LogicException(sprintf('Ack mode is not valid: "%s"', $mode)); @@ -73,78 +71,67 @@ public function setAckMode($mode) $this->ackMode = $mode; } - /** - * @return string - */ - public function getAckMode() + public function getAckMode(): string { return $this->ackMode; } - /** - * @return int - */ - public function getPrefetchCount() + public function getPrefetchCount(): int { return $this->prefetchCount; } - /** - * @param int $prefetchCount - */ - public function setPrefetchCount($prefetchCount) + public function setPrefetchCount(int $prefetchCount): void { - $this->prefetchCount = (int) $prefetchCount; + $this->prefetchCount = $prefetchCount; } /** - * {@inheritdoc} - * * @return StompDestination */ - public function getQueue() + public function getQueue(): Queue { return $this->queue; } - /** - * {@inheritdoc} - */ - public function receive($timeout = 0) + public function receive(int $timeout = 0): ?Message { $this->subscribe(); - if (0 === $timeout) { - while (true) { - if ($message = $this->stomp->readMessageFrame($this->subscriptionId, 0.1)) { + try { + if (0 === $timeout) { + while (true) { + if ($message = $this->stomp->readMessageFrame($this->subscriptionId, 100)) { + return $this->convertMessage($message); + } + } + } else { + if ($message = $this->stomp->readMessageFrame($this->subscriptionId, $timeout)) { return $this->convertMessage($message); } } - } else { - if ($message = $this->stomp->readMessageFrame($this->subscriptionId, $timeout)) { - return $this->convertMessage($message); - } + } catch (ErrorFrameException $e) { + throw new Exception($e->getMessage()."\n".$e->getFrame()->getBody(), 0, $e); } + + return null; } - /** - * {@inheritdoc} - */ - public function receiveNoWait() + public function receiveNoWait(): ?Message { $this->subscribe(); if ($message = $this->stomp->readMessageFrame($this->subscriptionId, 0)) { return $this->convertMessage($message); } + + return null; } /** - * {@inheritdoc} - * * @param StompMessage $message */ - public function acknowledge(PsrMessage $message) + public function acknowledge(Message $message): void { InvalidMessageException::assertMessageInstanceOf($message, StompMessage::class); @@ -154,25 +141,24 @@ public function acknowledge(PsrMessage $message) } /** - * {@inheritdoc} - * * @param StompMessage $message */ - public function reject(PsrMessage $message, $requeue = false) + public function reject(Message $message, bool $requeue = false): void { InvalidMessageException::assertMessageInstanceOf($message, StompMessage::class); $nackFrame = $this->stomp->getProtocol()->getNackFrame($message->getFrame()); - // rabbitmq STOMP protocol extension - $nackFrame->addHeaders([ - 'requeue' => $requeue ? 'true' : 'false', - ]); + if (ExtensionType::RABBITMQ === $this->queue->getExtensionType()) { + $nackFrame->addHeaders([ + 'requeue' => $requeue ? 'true' : 'false', + ]); + } $this->stomp->sendFrame($nackFrame); } - private function subscribe() + private function subscribe(): void { if (StompDestination::TYPE_TEMP_QUEUE == $this->queue->getType()) { $this->isSubscribed = true; @@ -183,28 +169,41 @@ private function subscribe() if (false == $this->isSubscribed) { $this->isSubscribed = true; - $frame = $this->stomp->getProtocol() - ->getSubscribeFrame($this->queue->getQueueName(), $this->subscriptionId, $this->ackMode); + $frame = $this->stomp->getProtocol()->getSubscribeFrame( + $this->queue->getQueueName(), + $this->subscriptionId, + $this->ackMode + ); - // rabbitmq STOMP protocol extension $headers = $this->queue->getHeaders(); - $headers['prefetch-count'] = $this->prefetchCount; - $headers = StompHeadersEncoder::encode($headers); - foreach ($headers as $key => $value) { - $frame[$key] = $value; + if (ExtensionType::RABBITMQ === $this->queue->getExtensionType()) { + $headers['prefetch-count'] = $this->prefetchCount; + $headers = StompHeadersEncoder::encode($headers); + + foreach ($headers as $key => $value) { + $frame[$key] = $value; + } + } elseif (ExtensionType::ARTEMIS === $this->queue->getExtensionType()) { + $subscriptionName = $this->subscriptionId.'-'.$this->queue->getStompName(); + + $artemisHeaders = []; + + $artemisHeaders['client-id'] = true ? $this->subscriptionId : null; + $artemisHeaders['durable-subscription-name'] = true ? $subscriptionName : null; + + $artemisHeaders = StompHeadersEncoder::encode(array_filter($artemisHeaders)); + + foreach ($artemisHeaders as $key => $value) { + $frame[$key] = $value; + } } $this->stomp->sendFrame($frame); } } - /** - * @param Frame $frame - * - * @return StompMessage - */ - private function convertMessage(Frame $frame) + private function convertMessage(Frame $frame): StompMessage { if ('MESSAGE' !== $frame->getCommand()) { throw new \LogicException(sprintf('Frame is not MESSAGE frame but: "%s"', $frame->getCommand())); @@ -224,7 +223,7 @@ private function convertMessage(Frame $frame) $headers['content-length'] ); - $message = new StompMessage($frame->getBody(), $properties, $headers); + $message = new StompMessage((string) $frame->getBody(), $properties, $headers); $message->setRedelivered($redelivered); $message->setFrame($frame); diff --git a/pkg/stomp/StompContext.php b/pkg/stomp/StompContext.php index c5584f04c..1e77f88ee 100644 --- a/pkg/stomp/StompContext.php +++ b/pkg/stomp/StompContext.php @@ -1,27 +1,52 @@ stomp = $stomp; @@ -30,27 +55,27 @@ public function __construct($stomp) } else { throw new \InvalidArgumentException('The stomp argument must be either BufferedStompClient or callable that return BufferedStompClient.'); } + + $this->extensionType = $extensionType; + $this->useExchangePrefix = ExtensionType::RABBITMQ === $extensionType; + $this->transient = $detectTransientConnections; } /** - * {@inheritdoc} - * * @return StompMessage */ - public function createMessage($body = '', array $properties = [], array $headers = []) + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message { return new StompMessage($body, $properties, $headers); } /** - * {@inheritdoc} - * * @return StompDestination */ - public function createQueue($name) + public function createQueue(string $name): Queue { - if (0 !== strpos($name, '/')) { - $destination = new StompDestination(); + if (!str_starts_with($name, '/')) { + $destination = new StompDestination($this->extensionType); $destination->setType(StompDestination::TYPE_QUEUE); $destination->setStompName($name); @@ -61,11 +86,9 @@ public function createQueue($name) } /** - * {@inheritdoc} - * * @return StompDestination */ - public function createTemporaryQueue() + public function createTemporaryQueue(): Queue { $queue = $this->createQueue(uniqid('', true)); $queue->setType(StompDestination::TYPE_TEMP_QUEUE); @@ -74,15 +97,13 @@ public function createTemporaryQueue() } /** - * {@inheritdoc} - * * @return StompDestination */ - public function createTopic($name) + public function createTopic(string $name): Topic { - if (0 !== strpos($name, '/')) { - $destination = new StompDestination(); - $destination->setType(StompDestination::TYPE_EXCHANGE); + if (!str_starts_with($name, '/')) { + $destination = new StompDestination($this->extensionType); + $destination->setType($this->useExchangePrefix ? StompDestination::TYPE_EXCHANGE : StompDestination::TYPE_TOPIC); $destination->setStompName($name); return $destination; @@ -91,12 +112,7 @@ public function createTopic($name) return $this->createDestination($name); } - /** - * @param string $destination - * - * @return StompDestination - */ - public function createDestination($destination) + public function createDestination(string $destination): StompDestination { $types = [ StompDestination::TYPE_TOPIC, @@ -114,7 +130,7 @@ public function createDestination($destination) foreach ($types as $_type) { $typePrefix = '/'.$_type.'/'; - if (0 === strpos($dest, $typePrefix)) { + if (str_starts_with($dest, $typePrefix)) { $type = $_type; $dest = substr($dest, strlen($typePrefix)); @@ -146,7 +162,7 @@ public function createDestination($destination) $routingKey = $pieces[1]; } - $destination = new StompDestination(); + $destination = new StompDestination($this->extensionType); $destination->setType($type); $destination->setStompName($name); $destination->setRoutingKey($routingKey); @@ -155,54 +171,63 @@ public function createDestination($destination) } /** - * {@inheritdoc} - * * @param StompDestination $destination * * @return StompConsumer */ - public function createConsumer(PsrDestination $destination) + public function createConsumer(Destination $destination): Consumer { InvalidDestinationException::assertDestinationInstanceOf($destination, StompDestination::class); + $this->transient = false; + return new StompConsumer($this->getStomp(), $destination); } /** - * {@inheritdoc} - * * @return StompProducer */ - public function createProducer() + public function createProducer(): Producer { + if ($this->transient && $this->stomp) { + $this->stomp->disconnect(); + } + return new StompProducer($this->getStomp()); } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { $this->getStomp()->disconnect(); } - /** - * @return BufferedStompClient - */ - public function getStomp() + public function createSubscriptionConsumer(): SubscriptionConsumer { - if (false == $this->stomp) { - $stomp = call_user_func($this->stompFactory); - if (false == $stomp instanceof BufferedStompClient) { - throw new \LogicException(sprintf( - 'The factory must return instance of BufferedStompClient. It returns %s', - is_object($stomp) ? get_class($stomp) : gettype($stomp) - )); - } + throw SubscriptionConsumerNotSupportedException::providerDoestNotSupportIt(); + } - $this->stomp = $stomp; + public function purgeQueue(Queue $queue): void + { + throw PurgeQueueNotSupportedException::providerDoestNotSupportIt(); + } + + public function getStomp(): BufferedStompClient + { + if (false == $this->stomp) { + $this->stomp = $this->createStomp(); } return $this->stomp; } + + private function createStomp(): BufferedStompClient + { + $stomp = call_user_func($this->stompFactory); + + if (false == $stomp instanceof BufferedStompClient) { + throw new \LogicException(sprintf('The factory must return instance of BufferedStompClient. It returns %s', is_object($stomp) ? $stomp::class : gettype($stomp))); + } + + return $stomp; + } } diff --git a/pkg/stomp/StompDestination.php b/pkg/stomp/StompDestination.php index 8f92cb423..3364968c4 100644 --- a/pkg/stomp/StompDestination.php +++ b/pkg/stomp/StompDestination.php @@ -1,22 +1,24 @@ headers = [ self::HEADER_DURABLE => false, self::HEADER_AUTO_DELETE => true, self::HEADER_EXCLUSIVE => false, ]; + + $this->extensionType = $extensionType; } - /** - * @return string - */ - public function getStompName() + public function getExtensionType(): string + { + return $this->extensionType; + } + + public function getStompName(): string { return $this->name; } - /** - * @param string $name - */ - public function setStompName($name) + public function setStompName(string $name): void { $this->name = $name; } - /** - * {@inheritdoc} - */ - public function getQueueName() + public function getQueueName(): string { - if (empty($this->getType()) || empty($this->getStompName())) { - throw new \LogicException('Destination type or name is not set'); + if (empty($this->getStompName())) { + throw new \LogicException('Destination name is not set'); + } + + if (ExtensionType::ARTEMIS === $this->extensionType) { + return $this->getStompName(); } $name = '/'.$this->getType().'/'.$this->getStompName(); @@ -81,26 +89,17 @@ public function getQueueName() return $name; } - /** - * {@inheritdoc} - */ - public function getTopicName() + public function getTopicName(): string { return $this->getQueueName(); } - /** - * @return mixed - */ - public function getType() + public function getType(): string { return $this->type; } - /** - * @param mixed $type - */ - public function setType($type) + public function setType(string $type): void { $types = [ self::TYPE_TOPIC, @@ -118,99 +117,62 @@ public function setType($type) $this->type = $type; } - /** - * @return string - */ - public function getRoutingKey() + public function getRoutingKey(): ?string { return $this->routingKey; } - /** - * @param string $routingKey - */ - public function setRoutingKey($routingKey) + public function setRoutingKey(?string $routingKey = null): void { $this->routingKey = $routingKey; } - /** - * @return bool - */ - public function isDurable() + public function isDurable(): bool { return $this->getHeader(self::HEADER_DURABLE, false); } - /** - * @param bool $durable - */ - public function setDurable($durable) + public function setDurable(bool $durable): void { - $this->setHeader(self::HEADER_DURABLE, (bool) $durable); + $this->setHeader(self::HEADER_DURABLE, $durable); } - /** - * @return bool - */ - public function isAutoDelete() + public function isAutoDelete(): bool { return $this->getHeader(self::HEADER_AUTO_DELETE, false); } - /** - * @param bool $autoDelete - */ - public function setAutoDelete($autoDelete) + public function setAutoDelete(bool $autoDelete): void { - $this->setHeader(self::HEADER_AUTO_DELETE, (bool) $autoDelete); + $this->setHeader(self::HEADER_AUTO_DELETE, $autoDelete); } - /** - * @return bool - */ - public function isExclusive() + public function isExclusive(): bool { return $this->getHeader(self::HEADER_EXCLUSIVE, false); } - /** - * @param bool $exclusive - */ - public function setExclusive($exclusive) + public function setExclusive(bool $exclusive): void { - $this->setHeader(self::HEADER_EXCLUSIVE, (bool) $exclusive); + $this->setHeader(self::HEADER_EXCLUSIVE, $exclusive); } - /** - * @param array $headers - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * @param string $name - * @param mixed $value - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { $this->headers[$name] = $value; } diff --git a/pkg/stomp/StompHeadersEncoder.php b/pkg/stomp/StompHeadersEncoder.php index ba982c46f..e3484abf6 100644 --- a/pkg/stomp/StompHeadersEncoder.php +++ b/pkg/stomp/StompHeadersEncoder.php @@ -1,24 +1,20 @@ $value) { - if (0 === strpos($key, self::PROPERTY_PREFIX)) { + if (str_starts_with($key, self::PROPERTY_PREFIX)) { $encodedProperties[substr($key, $prefixLength)] = $value; } else { $encodedHeaders[$key] = $value; @@ -55,12 +49,7 @@ public static function decode(array $headers = []) return [$decodedHeaders, $decodedProperties]; } - /** - * @param array $headers - * - * @return array - */ - private static function doEncode($headers = []) + private static function doEncode(array $headers = []): array { $encoded = []; @@ -99,18 +88,13 @@ private static function doEncode($headers = []) return $encoded; } - /** - * @param array $headers - * - * @return array - */ - private static function doDecode(array $headers = []) + private static function doDecode(array $headers = []): array { $decoded = []; foreach ($headers as $key => $value) { // skip type header - if (0 === strpos($key, self::TYPE_PREFIX)) { + if (str_starts_with($key, self::TYPE_PREFIX)) { continue; } diff --git a/pkg/stomp/StompMessage.php b/pkg/stomp/StompMessage.php index a6b30f92e..7098679b7 100644 --- a/pkg/stomp/StompMessage.php +++ b/pkg/stomp/StompMessage.php @@ -1,11 +1,13 @@ body = $body; $this->properties = $properties; @@ -45,200 +42,132 @@ public function __construct($body = '', array $properties = [], array $headers = $this->redelivered = false; } - /** - * @param string $body - */ - public function setBody($body) + public function setBody(string $body): void { $this->body = $body; } - /** - * {@inheritdoc} - */ - public function getBody() + public function getBody(): string { return $this->body; } - /** - * @param array $properties - */ - public function setProperties(array $properties) + public function setProperties(array $properties): void { $this->properties = $properties; } - /** - * {@inheritdoc} - */ - public function getProperties() + public function getProperties(): array { return $this->properties; } - /** - * {@inheritdoc} - */ - public function setProperty($name, $value) + public function setProperty(string $name, $value): void { - $this->properties[$name] = $value; + if (null === $value) { + unset($this->properties[$name]); + } else { + $this->properties[$name] = $value; + } } - /** - * {@inheritdoc} - */ - public function getProperty($name, $default = null) + public function getProperty(string $name, $default = null) { return array_key_exists($name, $this->properties) ? $this->properties[$name] : $default; } - /** - * @param array $headers - */ - public function setHeaders(array $headers) + public function setHeaders(array $headers): void { $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - /** - * {@inheritdoc} - */ - public function setHeader($name, $value) + public function setHeader(string $name, $value): void { - $this->headers[$name] = $value; + if (null === $value) { + unset($this->headers[$name]); + } else { + $this->headers[$name] = $value; + } } - /** - * {@inheritdoc} - */ - public function getHeader($name, $default = null) + public function getHeader(string $name, $default = null) { return array_key_exists($name, $this->headers) ? $this->headers[$name] : $default; } - /** - * note: rabbitmq STOMP protocol extension. - * - * @return bool - */ - public function isPersistent() + public function isPersistent(): bool { return $this->getHeader('persistent', false); } - /** - * note: rabbitmq STOMP protocol extension. - * - * @param bool $persistent - */ - public function setPersistent($persistent) + public function setPersistent(bool $persistent): void { - $this->setHeader('persistent', (bool) $persistent); + $this->setHeader('persistent', $persistent); } - /** - * @return bool - */ - public function isRedelivered() + public function isRedelivered(): bool { return $this->redelivered; } - /** - * @param bool $redelivered - */ - public function setRedelivered($redelivered) + public function setRedelivered(bool $redelivered): void { $this->redelivered = $redelivered; } - /** - * {@inheritdoc} - */ - public function setCorrelationId($correlationId) + public function setCorrelationId(?string $correlationId = null): void { $this->setHeader('correlation_id', (string) $correlationId); } - /** - * {@inheritdoc} - */ - public function getCorrelationId() + public function getCorrelationId(): ?string { return $this->getHeader('correlation_id'); } - /** - * {@inheritdoc} - */ - public function setMessageId($messageId) + public function setMessageId(?string $messageId = null): void { $this->setHeader('message_id', (string) $messageId); } - /** - * {@inheritdoc} - */ - public function getMessageId() + public function getMessageId(): ?string { return $this->getHeader('message_id'); } - /** - * {@inheritdoc} - */ - public function getTimestamp() + public function getTimestamp(): ?int { $value = $this->getHeader('timestamp'); return null === $value ? null : (int) $value; } - /** - * {@inheritdoc} - */ - public function setTimestamp($timestamp) + public function setTimestamp(?int $timestamp = null): void { $this->setHeader('timestamp', $timestamp); } - /** - * @return Frame - */ - public function getFrame() + public function getFrame(): ?Frame { return $this->frame; } - /** - * @param Frame $frame - */ - public function setFrame(Frame $frame) + public function setFrame(?Frame $frame = null): void { $this->frame = $frame; } - /** - * @param string|null $replyTo - */ - public function setReplyTo($replyTo) + public function setReplyTo(?string $replyTo = null): void { $this->setHeader('reply-to', $replyTo); } - /** - * @return string|null - */ - public function getReplyTo() + public function getReplyTo(): ?string { return $this->getHeader('reply-to'); } diff --git a/pkg/stomp/StompProducer.php b/pkg/stomp/StompProducer.php index 8e386fc3b..909720973 100644 --- a/pkg/stomp/StompProducer.php +++ b/pkg/stomp/StompProducer.php @@ -1,40 +1,37 @@ stomp = $stomp; } /** - * {@inheritdoc} - * * @param StompDestination $destination * @param StompMessage $message */ - public function send(PsrDestination $destination, PsrMessage $message) + public function send(Destination $destination, Message $message): void { InvalidDestinationException::assertDestinationInstanceOf($destination, StompDestination::class); - InvalidMessageException::assertMessageInstanceOf($message, StompMessage::class); $headers = array_merge($message->getHeaders(), $destination->getHeaders()); @@ -46,61 +43,54 @@ public function send(PsrDestination $destination, PsrMessage $message) } /** - * {@inheritdoc} + * @return $this|Producer */ - public function setDeliveryDelay($deliveryDelay) + public function setDeliveryDelay(?int $deliveryDelay = null): Producer { - if (null === $deliveryDelay) { - return; + if (empty($deliveryDelay)) { + return $this; } throw new \LogicException('Not implemented'); } - /** - * {@inheritdoc} - */ - public function getDeliveryDelay() + public function getDeliveryDelay(): ?int { return null; } /** - * {@inheritdoc} + * @throws PriorityNotSupportedException + * + * @return $this|Producer */ - public function setPriority($priority) + public function setPriority(?int $priority = null): Producer { - if (null === $priority) { - return; + if (empty($priority)) { + return $this; } - throw new \LogicException('Not implemented'); + throw PriorityNotSupportedException::providerDoestNotSupportIt(); } - /** - * {@inheritdoc} - */ - public function getPriority() + public function getPriority(): ?int { return null; } /** - * {@inheritdoc} + * @return $this|Producer */ - public function setTimeToLive($timeToLive) + public function setTimeToLive(?int $timeToLive = null): Producer { - if (null === $timeToLive) { - return; + if (empty($timeToLive)) { + return $this; } throw new \LogicException('Not implemented'); } - /** - * {@inheritdoc} - */ - public function getTimeToLive() + public function getTimeToLive(): ?int { return null; } diff --git a/pkg/stomp/Symfony/RabbitMqStompTransportFactory.php b/pkg/stomp/Symfony/RabbitMqStompTransportFactory.php deleted file mode 100644 index 38b43b693..000000000 --- a/pkg/stomp/Symfony/RabbitMqStompTransportFactory.php +++ /dev/null @@ -1,75 +0,0 @@ -children() - ->booleanNode('management_plugin_installed') - ->defaultFalse() - ->info('The option tells whether RabbitMQ broker has management plugin installed or not') - ->end() - ->integerNode('management_plugin_port')->min(1)->defaultValue(15672)->end() - ->booleanNode('delay_plugin_installed') - ->defaultFalse() - ->info('The option tells whether RabbitMQ broker has delay plugin installed or not') - ->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $management = new Definition(ManagementClient::class); - $management->setFactory([ManagementClient::class, 'create']); - $management->setArguments([ - $config['vhost'], - $config['host'], - $config['management_plugin_port'], - $config['login'], - $config['password'], - ]); - - $managementId = sprintf('enqueue.client.%s.management_client', $this->getName()); - $container->setDefinition($managementId, $management); - - $driver = new Definition(RabbitMqStompDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - new Reference($managementId), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } -} diff --git a/pkg/stomp/Symfony/StompTransportFactory.php b/pkg/stomp/Symfony/StompTransportFactory.php deleted file mode 100644 index 4b2587b8a..000000000 --- a/pkg/stomp/Symfony/StompTransportFactory.php +++ /dev/null @@ -1,107 +0,0 @@ -name = $name; - } - - /** - * {@inheritdoc} - */ - public function addConfiguration(ArrayNodeDefinition $builder) - { - $builder - ->children() - ->scalarNode('host')->defaultValue('localhost')->cannotBeEmpty()->end() - ->scalarNode('port')->defaultValue(61613)->end() - ->scalarNode('login')->defaultValue('guest')->cannotBeEmpty()->end() - ->scalarNode('password')->defaultValue('guest')->cannotBeEmpty()->end() - ->scalarNode('vhost')->defaultValue('/')->cannotBeEmpty()->end() - ->booleanNode('sync')->defaultTrue()->end() - ->integerNode('connection_timeout')->min(1)->defaultValue(1)->end() - ->integerNode('buffer_size')->min(1)->defaultValue(1000)->end() - ->booleanNode('lazy')->defaultTrue()->end() - ->booleanNode('ssl_on')->defaultFalse()->end() - ; - } - - /** - * {@inheritdoc} - */ - public function createConnectionFactory(ContainerBuilder $container, array $config) - { - $factory = new Definition(StompConnectionFactory::class); - $factory->setArguments([$config]); - - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - $container->setDefinition($factoryId, $factory); - - return $factoryId; - } - - /** - * {@inheritdoc} - */ - public function createContext(ContainerBuilder $container, array $config) - { - $factoryId = sprintf('enqueue.transport.%s.connection_factory', $this->getName()); - - $context = new Definition(StompContext::class); - $context->setPublic(true); - $context->setFactory([new Reference($factoryId), 'createContext']); - - $contextId = sprintf('enqueue.transport.%s.context', $this->getName()); - $container->setDefinition($contextId, $context); - - return $contextId; - } - - /** - * {@inheritdoc} - */ - public function createDriver(ContainerBuilder $container, array $config) - { - $driver = new Definition(StompDriver::class); - $driver->setPublic(true); - $driver->setArguments([ - new Reference(sprintf('enqueue.transport.%s.context', $this->getName())), - new Reference('enqueue.client.config'), - new Reference('enqueue.client.meta.queue_meta_registry'), - ]); - - $driverId = sprintf('enqueue.client.%s.driver', $this->getName()); - $container->setDefinition($driverId, $driver); - - return $driverId; - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return $this->name; - } -} diff --git a/pkg/stomp/Tests/BufferedStompClientTest.php b/pkg/stomp/Tests/BufferedStompClientTest.php index 9e7bfa8df..e4b6226e1 100644 --- a/pkg/stomp/Tests/BufferedStompClientTest.php +++ b/pkg/stomp/Tests/BufferedStompClientTest.php @@ -4,6 +4,7 @@ use Enqueue\Stomp\BufferedStompClient; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use Enqueue\Test\WriteAttributeTrait; use Stomp\Client; use Stomp\Network\Connection; @@ -12,6 +13,7 @@ class BufferedStompClientTest extends \PHPUnit\Framework\TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; use WriteAttributeTrait; public function testShouldExtendLibStompClient() @@ -19,11 +21,6 @@ public function testShouldExtendLibStompClient() $this->assertClassExtends(Client::class, BufferedStompClient::class); } - public function testCouldBeConstructedWithRequiredArguments() - { - new BufferedStompClient('tcp://localhost:12345'); - } - public function testCouldGetBufferSizeValue() { $client = new BufferedStompClient('tcp://localhost:12345', 12345); @@ -179,7 +176,7 @@ public function testShouldAddFirstFrameToBufferIfSubscriptionNotEqualAndReturnSe } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Connection + * @return \PHPUnit\Framework\MockObject\MockObject|Connection */ private function createStompConnectionMock() { diff --git a/pkg/stomp/Tests/Client/RabbitMqStompDriverTest.php b/pkg/stomp/Tests/Client/RabbitMqStompDriverTest.php deleted file mode 100644 index 6d86e2d99..000000000 --- a/pkg/stomp/Tests/Client/RabbitMqStompDriverTest.php +++ /dev/null @@ -1,678 +0,0 @@ -assertClassImplements(DriverInterface::class, RabbitMqStompDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new RabbitMqStompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - } - - public function testShouldReturnConfigObject() - { - $config = $this->createDummyConfig(); - - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - $config, - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new StompDestination(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new RabbitMqStompDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $queue = $driver->createQueue('aFooQueue'); - - $expectedHeaders = [ - 'durable' => true, - 'auto-delete' => false, - 'exclusive' => false, - 'x-max-priority' => 4, - ]; - - $this->assertSame($expectedQueue, $queue); - $this->assertTrue($queue->isDurable()); - $this->assertFalse($queue->isAutoDelete()); - $this->assertFalse($queue->isExclusive()); - $this->assertSame($expectedHeaders, $queue->getHeaders()); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new StompDestination(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new RabbitMqStompDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new StompMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content-type', 'ContentType'); - $transportMessage->setHeader('expiration', '12345000'); - $transportMessage->setHeader('priority', 3); - $transportMessage->setHeader('x-delay', '5678000'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame(['hkey' => 'hval'], $clientMessage->getHeaders()); - $this->assertSame(['key' => 'val'], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame(12345, $clientMessage->getExpire()); - $this->assertSame(5678, $clientMessage->getDelay()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame(MessagePriority::HIGH, $clientMessage->getPriority()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - } - - public function testShouldThrowExceptionIfXDelayIsNotNumeric() - { - $transportMessage = new StompMessage(); - $transportMessage->setHeader('x-delay', 'is-not-numeric'); - - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('x-delay header is not numeric. "is-not-numeric"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldThrowExceptionIfExpirationIsNotNumeric() - { - $transportMessage = new StompMessage(); - $transportMessage->setHeader('expiration', 'is-not-numeric'); - - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('expiration header is not numeric. "is-not-numeric"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldThrowExceptionIfCantConvertTransportPriorityToClientPriority() - { - $transportMessage = new StompMessage(); - $transportMessage->setHeader('priority', 'unknown'); - - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Cant convert transport priority to client: "unknown"'); - - $driver->createClientMessage($transportMessage); - } - - public function testShouldThrowExceptionIfCantConvertClientPriorityToTransportPriority() - { - $clientMessage = new Message(); - $clientMessage->setPriority('unknown'); - - $transportMessage = new StompMessage(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqStompDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Cant convert client priority to transport: "unknown"'); - - $driver->createTransportMessage($clientMessage); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setExpire(123); - $clientMessage->setPriority(MessagePriority::VERY_HIGH); - $clientMessage->setDelay(432); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new StompMessage()) - ; - - $driver = new RabbitMqStompDriver( - $context, - new Config('', '', '', '', '', '', ['delay_plugin_installed' => true]), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(StompMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content-type' => 'ContentType', - 'persistent' => true, - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply-to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - 'expiration' => '123000', - 'priority' => 4, - 'x-delay' => '432000', - ], $transportMessage->getHeaders()); - $this->assertSame(['key' => 'val'], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testShouldThrowExceptionIfDelayIsSetButDelayPluginInstalledOptionIsFalse() - { - $clientMessage = new Message(); - $clientMessage->setDelay(123); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new StompMessage()) - ; - - $driver = new RabbitMqStompDriver( - $context, - new Config('', '', '', '', '', '', ['delay_plugin_installed' => false]), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The message delaying is not supported. In order to use delay feature install RabbitMQ delay plugin.'); - - $driver->createTransportMessage($clientMessage); - } - - public function testShouldSendMessageToRouter() - { - $topic = new StompDestination(); - $transportMessage = new StompMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqStompDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new StompDestination(); - $transportMessage = new StompMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqStompDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldSendMessageToDelayExchangeIfDelaySet() - { - $queue = new StompDestination(); - $delayTopic = new StompDestination(); - $transportMessage = new StompMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($delayTopic), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($delayTopic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new RabbitMqStompDriver( - $context, - new Config('', '', '', '', '', '', ['delay_plugin_installed' => true]), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - $message->setDelay(10); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testShouldNotSetupBrokerIfManagementPluginInstalledOptionIsNotEnabled() - { - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - new Config('', '', '', '', '', '', ['management_plugin_installed' => false]), - $this->createDummyQueueMetaRegistry(), - $this->createManagementClientMock() - ); - - $logger = $this->createLoggerMock(); - $logger - ->expects($this->once()) - ->method('debug') - ->with('[RabbitMqStompDriver] Could not setup broker. The option `management_plugin_installed` is not enabled. Please enable that option and install rabbit management plugin') - ; - - $driver->setupBroker($logger); - } - - public function testShouldSetupBroker() - { - $metaRegistry = $this->createDummyQueueMetaRegistry(); - - $managementClient = $this->createManagementClientMock(); - $managementClient - ->expects($this->at(0)) - ->method('declareExchange') - ->with('prefix.routertopic', [ - 'type' => 'fanout', - 'durable' => true, - 'auto_delete' => false, - ]) - ; - $managementClient - ->expects($this->at(1)) - ->method('declareQueue') - ->with('prefix.app.routerqueue', [ - 'durable' => true, - 'auto_delete' => false, - 'arguments' => [ - 'x-max-priority' => 4, - ], - ]) - ; - $managementClient - ->expects($this->at(2)) - ->method('bind') - ->with('prefix.routertopic', 'prefix.app.routerqueue', 'prefix.app.routerqueue') - ; - $managementClient - ->expects($this->at(3)) - ->method('declareQueue') - ->with('prefix.app.default', [ - 'durable' => true, - 'auto_delete' => false, - 'arguments' => [ - 'x-max-priority' => 4, - ], - ]) - ; - - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - new Config('prefix', 'app', 'routerTopic', 'routerQueue', 'processorQueue', '', ['management_plugin_installed' => true]), - $metaRegistry, - $managementClient - ); - - $logger = $this->createLoggerMock(); - $logger - ->expects($this->at(0)) - ->method('debug') - ->with('[RabbitMqStompDriver] Declare router exchange: prefix.routertopic') - ; - $logger - ->expects($this->at(1)) - ->method('debug') - ->with('[RabbitMqStompDriver] Declare router queue: prefix.app.routerqueue') - ; - $logger - ->expects($this->at(2)) - ->method('debug') - ->with('[RabbitMqStompDriver] Bind router queue to exchange: prefix.app.routerqueue -> prefix.routertopic') - ; - $logger - ->expects($this->at(3)) - ->method('debug') - ->with('[RabbitMqStompDriver] Declare processor queue: prefix.app.default') - ; - - $driver->setupBroker($logger); - } - - public function testSetupBrokerShouldCreateDelayExchangeIfEnabled() - { - $metaRegistry = $this->createDummyQueueMetaRegistry(); - - $managementClient = $this->createManagementClientMock(); - $managementClient - ->expects($this->at(6)) - ->method('declareExchange') - ->with('prefix.app.default.delayed', [ - 'type' => 'x-delayed-message', - 'durable' => true, - 'auto_delete' => false, - 'arguments' => [ - 'x-delayed-type' => 'direct', - ], - ]) - ; - $managementClient - ->expects($this->at(7)) - ->method('bind') - ->with('prefix.app.default.delayed', 'prefix.app.default', 'prefix.app.default') - ; - - $driver = new RabbitMqStompDriver( - $this->createPsrContextMock(), - new Config('prefix', 'app', 'routerTopic', 'routerQueue', 'processorQueue', '', ['management_plugin_installed' => true, 'delay_plugin_installed' => true]), - $metaRegistry, - $managementClient - ); - - $logger = $this->createLoggerMock(); - $logger - ->expects($this->at(6)) - ->method('debug') - ->with('[RabbitMqStompDriver] Declare delay exchange: prefix.app.default.delayed') - ; - $logger - ->expects($this->at(7)) - ->method('debug') - ->with('[RabbitMqStompDriver] Bind processor queue to delay exchange: prefix.app.default -> prefix.app.default.delayed') - ; - - $driver->setupBroker($logger); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|StompContext - */ - private function createPsrContextMock() - { - return $this->createMock(StompContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|StompProducer - */ - private function createPsrProducerMock() - { - return $this->createMock(StompProducer::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|ManagementClient - */ - private function createManagementClientMock() - { - return $this->createMock(ManagementClient::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface - */ - private function createLoggerMock() - { - return $this->createMock(LoggerInterface::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/stomp/Tests/Client/StompDriverTest.php b/pkg/stomp/Tests/Client/StompDriverTest.php deleted file mode 100644 index 029d275ef..000000000 --- a/pkg/stomp/Tests/Client/StompDriverTest.php +++ /dev/null @@ -1,342 +0,0 @@ -assertClassImplements(DriverInterface::class, StompDriver::class); - } - - public function testCouldBeConstructedWithRequiredArguments() - { - new StompDriver($this->createPsrContextMock(), $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - } - - public function testShouldReturnConfigObject() - { - $config = $this->createDummyConfig(); - - $driver = new StompDriver($this->createPsrContextMock(), $config, $this->createDummyQueueMetaRegistry()); - - $this->assertSame($config, $driver->getConfig()); - } - - public function testShouldCreateAndReturnQueueInstance() - { - $expectedQueue = new StompDestination(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aprefix.afooqueue') - ->willReturn($expectedQueue) - ; - - $driver = new StompDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aFooQueue'); - - $this->assertSame($expectedQueue, $queue); - $this->assertTrue($queue->isDurable()); - $this->assertFalse($queue->isAutoDelete()); - $this->assertFalse($queue->isExclusive()); - $this->assertSame([ - 'durable' => true, - 'auto-delete' => false, - 'exclusive' => false, - ], $queue->getHeaders()); - } - - public function testShouldCreateAndReturnQueueInstanceWithHardcodedTransportName() - { - $expectedQueue = new StompDestination(); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->with('aBarQueue') - ->willReturn($expectedQueue) - ; - - $driver = new StompDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $queue = $driver->createQueue('aBarQueue'); - - $this->assertSame($expectedQueue, $queue); - } - - public function testShouldConvertTransportMessageToClientMessage() - { - $transportMessage = new StompMessage(); - $transportMessage->setBody('body'); - $transportMessage->setHeaders(['hkey' => 'hval']); - $transportMessage->setProperties(['key' => 'val']); - $transportMessage->setHeader('content-type', 'ContentType'); - $transportMessage->setMessageId('MessageId'); - $transportMessage->setTimestamp(1000); - $transportMessage->setReplyTo('theReplyTo'); - $transportMessage->setCorrelationId('theCorrelationId'); - - $driver = new StompDriver($this->createPsrContextMock(), $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $clientMessage = $driver->createClientMessage($transportMessage); - - $this->assertInstanceOf(Message::class, $clientMessage); - $this->assertSame('body', $clientMessage->getBody()); - $this->assertSame(['hkey' => 'hval'], $clientMessage->getHeaders()); - $this->assertSame(['key' => 'val'], $clientMessage->getProperties()); - $this->assertSame('MessageId', $clientMessage->getMessageId()); - $this->assertSame('ContentType', $clientMessage->getContentType()); - $this->assertSame(1000, $clientMessage->getTimestamp()); - $this->assertSame('theReplyTo', $clientMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $clientMessage->getCorrelationId()); - } - - public function testShouldConvertClientMessageToTransportMessage() - { - $clientMessage = new Message(); - $clientMessage->setBody('body'); - $clientMessage->setHeaders(['hkey' => 'hval']); - $clientMessage->setProperties(['key' => 'val']); - $clientMessage->setContentType('ContentType'); - $clientMessage->setMessageId('MessageId'); - $clientMessage->setTimestamp(1000); - $clientMessage->setReplyTo('theReplyTo'); - $clientMessage->setCorrelationId('theCorrelationId'); - - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn(new StompMessage()) - ; - - $driver = new StompDriver($context, $this->createDummyConfig(), $this->createDummyQueueMetaRegistry()); - - $transportMessage = $driver->createTransportMessage($clientMessage); - - $this->assertInstanceOf(StompMessage::class, $transportMessage); - $this->assertSame('body', $transportMessage->getBody()); - $this->assertSame([ - 'hkey' => 'hval', - 'content-type' => 'ContentType', - 'persistent' => true, - 'message_id' => 'MessageId', - 'timestamp' => 1000, - 'reply-to' => 'theReplyTo', - 'correlation_id' => 'theCorrelationId', - ], $transportMessage->getHeaders()); - $this->assertSame(['key' => 'val'], $transportMessage->getProperties()); - $this->assertSame('MessageId', $transportMessage->getMessageId()); - $this->assertSame(1000, $transportMessage->getTimestamp()); - $this->assertSame('theReplyTo', $transportMessage->getReplyTo()); - $this->assertSame('theCorrelationId', $transportMessage->getCorrelationId()); - } - - public function testShouldSendMessageToRouter() - { - $topic = new StompDestination(); - $transportMessage = new StompMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($topic), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createTopic') - ->willReturn($topic) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new StompDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_TOPIC_NAME, 'topic'); - - $driver->sendToRouter($message); - } - - public function testShouldThrowExceptionIfTopicParameterIsNotSet() - { - $driver = new StompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Topic name parameter is required but is not set'); - - $driver->sendToRouter(new Message()); - } - - public function testShouldSendMessageToProcessor() - { - $queue = new StompDestination(); - $transportMessage = new StompMessage(); - - $producer = $this->createPsrProducerMock(); - $producer - ->expects($this->once()) - ->method('send') - ->with($this->identicalTo($queue), $this->identicalTo($transportMessage)) - ; - $context = $this->createPsrContextMock(); - $context - ->expects($this->once()) - ->method('createQueue') - ->willReturn($queue) - ; - $context - ->expects($this->once()) - ->method('createProducer') - ->willReturn($producer) - ; - $context - ->expects($this->once()) - ->method('createMessage') - ->willReturn($transportMessage) - ; - - $driver = new StompDriver( - $context, - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - $message->setProperty(Config::PARAMETER_PROCESSOR_QUEUE_NAME, 'aFooQueue'); - - $driver->sendToProcessor($message); - } - - public function testShouldThrowExceptionIfProcessorNameParameterIsNotSet() - { - $driver = new StompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Processor name parameter is required but is not set'); - - $driver->sendToProcessor(new Message()); - } - - public function testShouldThrowExceptionIfProcessorQueueNameParameterIsNotSet() - { - $driver = new StompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Queue name parameter is required but is not set'); - - $message = new Message(); - $message->setProperty(Config::PARAMETER_PROCESSOR_NAME, 'processor'); - - $driver->sendToProcessor($message); - } - - public function testSetupBrokerShouldOnlyLogMessageThatStompDoesNotSupportBrokerSetup() - { - $driver = new StompDriver( - $this->createPsrContextMock(), - $this->createDummyConfig(), - $this->createDummyQueueMetaRegistry() - ); - - $logger = $this->createLoggerMock(); - $logger - ->expects($this->once()) - ->method('debug') - ->with('[StompDriver] Stomp protocol does not support broker configuration') - ; - - $driver->setupBroker($logger); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|StompContext - */ - private function createPsrContextMock() - { - return $this->createMock(StompContext::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|StompProducer - */ - private function createPsrProducerMock() - { - return $this->createMock(StompProducer::class); - } - - /** - * @return \PHPUnit_Framework_MockObject_MockObject|LoggerInterface - */ - private function createLoggerMock() - { - return $this->createMock(LoggerInterface::class); - } - - /** - * @return QueueMetaRegistry - */ - private function createDummyQueueMetaRegistry() - { - $registry = new QueueMetaRegistry($this->createDummyConfig(), []); - $registry->add('default'); - $registry->add('aFooQueue'); - $registry->add('aBarQueue', 'aBarQueue'); - - return $registry; - } - - /** - * @return Config - */ - private function createDummyConfig() - { - return Config::create('aPrefix'); - } -} diff --git a/pkg/stomp/Tests/Functional/StompCommonUseCasesTest.php b/pkg/stomp/Tests/Functional/StompCommonUseCasesTest.php index 19af898a6..e3f09737a 100644 --- a/pkg/stomp/Tests/Functional/StompCommonUseCasesTest.php +++ b/pkg/stomp/Tests/Functional/StompCommonUseCasesTest.php @@ -12,22 +12,22 @@ */ class StompCommonUseCasesTest extends \PHPUnit\Framework\TestCase { - use RabbitmqStompExtension; use RabbitManagementExtensionTrait; + use RabbitmqStompExtension; /** * @var StompContext */ private $stompContext; - public function setUp() + protected function setUp(): void { $this->stompContext = $this->buildStompContext(); $this->removeQueue('stomp.test'); } - public function tearDown() + protected function tearDown(): void { $this->stompContext->close(); } @@ -41,7 +41,7 @@ public function testWaitsForTwoSecondsAndReturnNullOnReceive() $startAt = microtime(true); $consumer = $this->stompContext->createConsumer($queue); - $message = $consumer->receive(2); + $message = $consumer->receive(2000); $endAt = microtime(true); @@ -85,7 +85,7 @@ public function testProduceAndReceiveOneMessage() $producer->send($queue, $message); $consumer = $this->stompContext->createConsumer($queue); - $message = $consumer->receive(1); + $message = $consumer->receive(1000); $this->assertInstanceOf(StompMessage::class, $message); $consumer->acknowledge($message); diff --git a/pkg/stomp/Tests/Functional/StompConnectionFactoryTest.php b/pkg/stomp/Tests/Functional/StompConnectionFactoryTest.php new file mode 100644 index 000000000..6d4223616 --- /dev/null +++ b/pkg/stomp/Tests/Functional/StompConnectionFactoryTest.php @@ -0,0 +1,51 @@ +getDsn().'?send_heartbeat=2000'; + $factory = new StompConnectionFactory($dsn); + $this->expectException(HeartbeatException::class); + $factory->createContext()->getStomp(); + } + + public function testShouldCreateConnectionWithSendHeartbeat() + { + $dsn = $this->getDsn().'?send_heartbeat=2000&read_timeout=1'; + $factory = new StompConnectionFactory($dsn); + $context = $factory->createContext(); + + $observers = $context->getStomp()->getConnection()->getObservers()->getObservers(); + $this->assertAttributeEquals([2000, 0], 'heartbeat', $context->getStomp()); + $this->assertCount(1, $observers); + $this->assertInstanceOf(HeartbeatEmitter::class, $observers[0]); + } + + public function testShouldCreateConnectionWithReceiveHeartbeat() + { + $dsn = $this->getDsn().'?receive_heartbeat=2000'; + $factory = new StompConnectionFactory($dsn); + $context = $factory->createContext(); + + $observers = $context->getStomp()->getConnection()->getObservers()->getObservers(); + $this->assertAttributeEquals([0, 2000], 'heartbeat', $context->getStomp()); + $this->assertCount(1, $observers); + $this->assertInstanceOf(ServerAliveObserver::class, $observers[0]); + } +} diff --git a/pkg/stomp/Tests/Functional/StompConsumptionUseCasesTest.php b/pkg/stomp/Tests/Functional/StompConsumptionUseCasesTest.php index 0c6de93f5..2025380fd 100644 --- a/pkg/stomp/Tests/Functional/StompConsumptionUseCasesTest.php +++ b/pkg/stomp/Tests/Functional/StompConsumptionUseCasesTest.php @@ -11,31 +11,31 @@ use Enqueue\Stomp\StompContext; use Enqueue\Test\RabbitManagementExtensionTrait; use Enqueue\Test\RabbitmqStompExtension; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; /** * @group functional */ class StompConsumptionUseCasesTest extends \PHPUnit\Framework\TestCase { - use RabbitmqStompExtension; use RabbitManagementExtensionTrait; + use RabbitmqStompExtension; /** * @var StompContext */ private $stompContext; - public function setUp() + protected function setUp(): void { $this->stompContext = $this->buildStompContext(); $this->removeQueue('stomp.test'); } - public function tearDown() + protected function tearDown(): void { $this->stompContext->close(); } @@ -57,7 +57,7 @@ public function testConsumeOneMessageAndExit() $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); } @@ -88,22 +88,22 @@ public function testConsumeOneMessageAndSendReplyExit() $queueConsumer->bind($replyQueue, $replyProcessor); $queueConsumer->consume(); - $this->assertInstanceOf(PsrMessage::class, $processor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $processor->lastProcessedMessage); $this->assertEquals(__METHOD__, $processor->lastProcessedMessage->getBody()); - $this->assertInstanceOf(PsrMessage::class, $replyProcessor->lastProcessedMessage); + $this->assertInstanceOf(Message::class, $replyProcessor->lastProcessedMessage); $this->assertEquals(__METHOD__.'.reply', $replyProcessor->lastProcessedMessage->getBody()); } } -class StubProcessor implements PsrProcessor +class StubProcessor implements Processor { public $result = self::ACK; - /** @var PsrMessage */ + /** @var Message */ public $lastProcessedMessage; - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context) { $this->lastProcessedMessage = $message; diff --git a/pkg/stomp/Tests/Functional/StompRpcUseCasesTest.php b/pkg/stomp/Tests/Functional/StompRpcUseCasesTest.php index 4e4e8385a..4cbb3af47 100644 --- a/pkg/stomp/Tests/Functional/StompRpcUseCasesTest.php +++ b/pkg/stomp/Tests/Functional/StompRpcUseCasesTest.php @@ -14,15 +14,15 @@ */ class StompRpcUseCasesTest extends \PHPUnit\Framework\TestCase { - use RabbitmqStompExtension; use RabbitManagementExtensionTrait; + use RabbitmqStompExtension; /** * @var StompContext */ private $stompContext; - public function setUp() + protected function setUp(): void { $this->stompContext = $this->buildStompContext(); @@ -30,7 +30,7 @@ public function setUp() $this->removeQueue('stomp.rpc.reply_test'); } - public function tearDown() + protected function tearDown(): void { $this->stompContext->close(); } @@ -47,11 +47,11 @@ public function testDoAsyncRpcCallWithCustomReplyQueue() $message = $this->stompContext->createMessage(); $message->setReplyTo($replyQueue->getQueueName()); - $promise = $rpcClient->callAsync($queue, $message, 10); + $promise = $rpcClient->callAsync($queue, $message, 200); $this->assertInstanceOf(Promise::class, $promise); $consumer = $this->stompContext->createConsumer($queue); - $message = $consumer->receive(1); + $message = $consumer->receive(100); $this->assertInstanceOf(StompMessage::class, $message); $this->assertNotNull($message->getReplyTo()); $this->assertNotNull($message->getCorrelationId()); @@ -77,11 +77,11 @@ public function testDoAsyncRecCallWithCastInternallyCreatedTemporaryReplyQueue() $message = $this->stompContext->createMessage(); - $promise = $rpcClient->callAsync($queue, $message, 10); + $promise = $rpcClient->callAsync($queue, $message, 200); $this->assertInstanceOf(Promise::class, $promise); $consumer = $this->stompContext->createConsumer($queue); - $receivedMessage = $consumer->receive(1); + $receivedMessage = $consumer->receive(100); $this->assertInstanceOf(StompMessage::class, $receivedMessage); $this->assertNotNull($receivedMessage->getReplyTo()); diff --git a/pkg/stomp/Tests/Spec/StompMessageTest.php b/pkg/stomp/Tests/Spec/StompMessageTest.php index 39d8bb7d1..8f6748b63 100644 --- a/pkg/stomp/Tests/Spec/StompMessageTest.php +++ b/pkg/stomp/Tests/Spec/StompMessageTest.php @@ -3,13 +3,10 @@ namespace Enqueue\Stomp\Tests\Spec; use Enqueue\Stomp\StompMessage; -use Interop\Queue\Spec\PsrMessageSpec; +use Interop\Queue\Spec\MessageSpec; -class StompMessageTest extends PsrMessageSpec +class StompMessageTest extends MessageSpec { - /** - * {@inheritdoc} - */ protected function createMessage() { return new StompMessage(); diff --git a/pkg/stomp/Tests/StompConnectionFactoryConfigTest.php b/pkg/stomp/Tests/StompConnectionFactoryConfigTest.php index 579479484..d784d4104 100644 --- a/pkg/stomp/Tests/StompConnectionFactoryConfigTest.php +++ b/pkg/stomp/Tests/StompConnectionFactoryConfigTest.php @@ -4,6 +4,7 @@ use Enqueue\Stomp\StompConnectionFactory; use Enqueue\Test\ClassExtensionTrait; +use Enqueue\Test\ReadAttributeTrait; use PHPUnit\Framework\TestCase; /** @@ -12,6 +13,7 @@ class StompConnectionFactoryConfigTest extends TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testThrowNeitherArrayStringNorNullGivenAsConfig() { @@ -21,10 +23,10 @@ public function testThrowNeitherArrayStringNorNullGivenAsConfig() new StompConnectionFactory(new \stdClass()); } - public function testThrowIfSchemeIsNotAmqp() + public function testThrowIfSchemeIsNotStomp() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('The given DSN "http://example.com/" is not supported. Must start with "stomp:".'); + $this->expectExceptionMessage('The given DSN is not supported. Must start with "stomp:".'); new StompConnectionFactory('http://example.com/'); } @@ -32,16 +34,13 @@ public function testThrowIfSchemeIsNotAmqp() public function testThrowIfDsnCouldNotBeParsed() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Failed to parse DSN "stomp://:@/"'); + $this->expectExceptionMessage('The DSN is invalid.'); - new StompConnectionFactory('stomp://:@/'); + new StompConnectionFactory('foo'); } /** * @dataProvider provideConfigs - * - * @param mixed $config - * @param mixed $expectedConfig */ public function testShouldParseConfigurationAsExpected($config, $expectedConfig) { @@ -55,6 +54,7 @@ public static function provideConfigs() yield [ null, [ + 'target' => 'rabbitmq', 'host' => 'localhost', 'port' => 61613, 'login' => 'guest', @@ -65,12 +65,18 @@ public static function provideConfigs() 'sync' => false, 'lazy' => true, 'ssl_on' => false, + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, ], ]; yield [ 'stomp:', [ + 'target' => 'rabbitmq', 'host' => 'localhost', 'port' => 61613, 'login' => 'guest', @@ -81,12 +87,18 @@ public static function provideConfigs() 'sync' => false, 'lazy' => true, 'ssl_on' => false, + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, ], ]; yield [ [], [ + 'target' => 'rabbitmq', 'host' => 'localhost', 'port' => 61613, 'login' => 'guest', @@ -97,12 +109,18 @@ public static function provideConfigs() 'sync' => false, 'lazy' => true, 'ssl_on' => false, + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, ], ]; yield [ 'stomp://localhost:1234?foo=bar&lazy=0&sync=true', [ + 'target' => 'rabbitmq', 'host' => 'localhost', 'port' => 1234, 'login' => 'guest', @@ -114,12 +132,110 @@ public static function provideConfigs() 'lazy' => false, 'foo' => 'bar', 'ssl_on' => false, + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, + ], + ]; + + yield [ + 'stomp+activemq://localhost:1234?foo=bar&lazy=0&sync=true', + [ + 'target' => 'activemq', + 'host' => 'localhost', + 'port' => 1234, + 'login' => 'guest', + 'password' => 'guest', + 'vhost' => '/', + 'buffer_size' => 1000, + 'connection_timeout' => 1, + 'sync' => true, + 'lazy' => false, + 'foo' => 'bar', + 'ssl_on' => false, + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, + ], + ]; + + yield [ + 'stomp+rabbitmq://localhost:1234?foo=bar&lazy=0&sync=true', + [ + 'target' => 'rabbitmq', + 'host' => 'localhost', + 'port' => 1234, + 'login' => 'guest', + 'password' => 'guest', + 'vhost' => '/', + 'buffer_size' => 1000, + 'connection_timeout' => 1, + 'sync' => true, + 'lazy' => false, + 'foo' => 'bar', + 'ssl_on' => false, + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, + ], + ]; + + yield [ + ['dsn' => 'stomp://localhost:1234/theVhost?foo=bar&lazy=0&sync=true', 'baz' => 'bazVal', 'foo' => 'fooVal'], + [ + 'target' => 'rabbitmq', + 'host' => 'localhost', + 'port' => 1234, + 'login' => 'guest', + 'password' => 'guest', + 'vhost' => 'theVhost', + 'buffer_size' => 1000, + 'connection_timeout' => 1, + 'sync' => true, + 'lazy' => false, + 'foo' => 'bar', + 'ssl_on' => false, + 'baz' => 'bazVal', + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, + ], + ]; + + yield [ + ['dsn' => 'stomp:///%2f'], + [ + 'target' => 'rabbitmq', + 'host' => 'localhost', + 'port' => 61613, + 'login' => 'guest', + 'password' => 'guest', + 'vhost' => '/', + 'buffer_size' => 1000, + 'connection_timeout' => 1, + 'sync' => false, + 'lazy' => true, + 'ssl_on' => false, + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, ], ]; yield [ ['host' => 'localhost', 'port' => 1234, 'foo' => 'bar'], [ + 'target' => 'rabbitmq', 'host' => 'localhost', 'port' => 1234, 'login' => 'guest', @@ -131,6 +247,11 @@ public static function provideConfigs() 'lazy' => true, 'foo' => 'bar', 'ssl_on' => false, + 'write_timeout' => 3, + 'read_timeout' => 60, + 'send_heartbeat' => 0, + 'receive_heartbeat' => 0, + 'detect_transient_connections' => false, ], ]; } diff --git a/pkg/stomp/Tests/StompConnectionFactoryTest.php b/pkg/stomp/Tests/StompConnectionFactoryTest.php index 8e57e5f57..1f39e3b20 100644 --- a/pkg/stomp/Tests/StompConnectionFactoryTest.php +++ b/pkg/stomp/Tests/StompConnectionFactoryTest.php @@ -5,15 +5,17 @@ use Enqueue\Stomp\StompConnectionFactory; use Enqueue\Stomp\StompContext; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrConnectionFactory; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\ConnectionFactory; class StompConnectionFactoryTest extends \PHPUnit\Framework\TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementConnectionFactoryInterface() { - $this->assertClassImplements(PsrConnectionFactory::class, StompConnectionFactory::class); + $this->assertClassImplements(ConnectionFactory::class, StompConnectionFactory::class); } public function testShouldCreateLazyContext() @@ -25,6 +27,31 @@ public function testShouldCreateLazyContext() $this->assertInstanceOf(StompContext::class, $context); $this->assertAttributeEquals(null, 'stomp', $context); - $this->assertInternalType('callable', $this->readAttribute($context, 'stompFactory')); + $this->assertAttributeEquals(true, 'useExchangePrefix', $context); + self::assertIsCallable($this->readAttribute($context, 'stompFactory')); + } + + public function testShouldCreateRabbitMQContext() + { + $factory = new StompConnectionFactory('stomp+rabbitmq://'); + + $context = $factory->createContext(); + + $this->assertInstanceOf(StompContext::class, $context); + + $this->assertAttributeEquals(null, 'stomp', $context); + $this->assertAttributeEquals(true, 'useExchangePrefix', $context); + } + + public function testShouldCreateActiveMQContext() + { + $factory = new StompConnectionFactory('stomp+activemq://'); + + $context = $factory->createContext(); + + $this->assertInstanceOf(StompContext::class, $context); + + $this->assertAttributeEquals(null, 'stomp', $context); + $this->assertAttributeEquals(false, 'useExchangePrefix', $context); } } diff --git a/pkg/stomp/Tests/StompConsumerTest.php b/pkg/stomp/Tests/StompConsumerTest.php index d988a24f5..d461284c9 100644 --- a/pkg/stomp/Tests/StompConsumerTest.php +++ b/pkg/stomp/Tests/StompConsumerTest.php @@ -3,47 +3,45 @@ namespace Enqueue\Stomp\Tests; use Enqueue\Stomp\BufferedStompClient; +use Enqueue\Stomp\ExtensionType; use Enqueue\Stomp\StompConsumer; use Enqueue\Stomp\StompDestination; use Enqueue\Stomp\StompMessage; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrConsumer; -use Interop\Queue\PsrMessage; +use Enqueue\Test\ReadAttributeTrait; +use Interop\Queue\Consumer; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Message; use Stomp\Protocol\Protocol; use Stomp\Transport\Frame; class StompConsumerTest extends \PHPUnit\Framework\TestCase { use ClassExtensionTrait; + use ReadAttributeTrait; public function testShouldImplementMessageConsumerInterface() { - $this->assertClassImplements(PsrConsumer::class, StompConsumer::class); - } - - public function testCouldBeConstructedWithRequiredAttributes() - { - new StompConsumer($this->createStompClientMock(), new StompDestination()); + $this->assertClassImplements(Consumer::class, StompConsumer::class); } public function testCouldGetQueue() { - $consumer = new StompConsumer($this->createStompClientMock(), $dest = new StompDestination()); + $consumer = new StompConsumer($this->createStompClientMock(), $dest = $this->createDummyDestination()); $this->assertSame($dest, $consumer->getQueue()); } public function testShouldReturnDefaultAckMode() { - $consumer = new StompConsumer($this->createStompClientMock(), new StompDestination()); + $consumer = new StompConsumer($this->createStompClientMock(), $this->createDummyDestination()); $this->assertSame(StompConsumer::ACK_CLIENT_INDIVIDUAL, $consumer->getAckMode()); } public function testCouldSetGetAckMethod() { - $consumer = new StompConsumer($this->createStompClientMock(), new StompDestination()); + $consumer = new StompConsumer($this->createStompClientMock(), $this->createDummyDestination()); $consumer->setAckMode(StompConsumer::ACK_CLIENT); $this->assertSame(StompConsumer::ACK_CLIENT, $consumer->getAckMode()); @@ -54,20 +52,20 @@ public function testShouldThrowLogicExceptionIfAckModeIsInvalid() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Ack mode is not valid: "invalid-ack-mode"'); - $consumer = new StompConsumer($this->createStompClientMock(), new StompDestination()); + $consumer = new StompConsumer($this->createStompClientMock(), $this->createDummyDestination()); $consumer->setAckMode('invalid-ack-mode'); } public function testShouldReturnDefaultPrefetchCount() { - $consumer = new StompConsumer($this->createStompClientMock(), new StompDestination()); + $consumer = new StompConsumer($this->createStompClientMock(), $this->createDummyDestination()); $this->assertSame(1, $consumer->getPrefetchCount()); } public function testCouldSetGetPrefetchCount() { - $consumer = new StompConsumer($this->createStompClientMock(), new StompDestination()); + $consumer = new StompConsumer($this->createStompClientMock(), $this->createDummyDestination()); $consumer->setPrefetchCount(123); $this->assertSame(123, $consumer->getPrefetchCount()); @@ -78,8 +76,8 @@ public function testAcknowledgeShouldThrowInvalidMessageExceptionIfMessageIsWron $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of'); - $consumer = new StompConsumer($this->createStompClientMock(), new StompDestination()); - $consumer->acknowledge($this->createMock(PsrMessage::class)); + $consumer = new StompConsumer($this->createStompClientMock(), $this->createDummyDestination()); + $consumer->acknowledge($this->createMock(Message::class)); } public function testShouldAcknowledgeMessage() @@ -106,7 +104,7 @@ public function testShouldAcknowledgeMessage() $message = new StompMessage(); $message->setFrame(new Frame()); - $consumer = new StompConsumer($client, new StompDestination()); + $consumer = new StompConsumer($client, $this->createDummyDestination()); $consumer->acknowledge($message); } @@ -115,8 +113,8 @@ public function testRejectShouldThrowInvalidMessageExceptionIfMessageIsWrongType $this->expectException(InvalidMessageException::class); $this->expectExceptionMessage('The message must be an instance of'); - $consumer = new StompConsumer($this->createStompClientMock(), new StompDestination()); - $consumer->reject($this->createMock(PsrMessage::class)); + $consumer = new StompConsumer($this->createStompClientMock(), $this->createDummyDestination()); + $consumer->reject($this->createMock(Message::class)); } public function testShouldRejectMessage() @@ -143,7 +141,7 @@ public function testShouldRejectMessage() $message = new StompMessage(); $message->setFrame(new Frame()); - $consumer = new StompConsumer($client, new StompDestination()); + $consumer = new StompConsumer($client, $this->createDummyDestination()); $consumer->reject($message); $this->assertSame(['requeue' => 'false'], $frame->getHeaders()); @@ -173,7 +171,7 @@ public function testShouldRejectAndRequeueMessage() $message = new StompMessage(); $message->setFrame(new Frame()); - $consumer = new StompConsumer($client, new StompDestination()); + $consumer = new StompConsumer($client, $this->createDummyDestination()); $consumer->reject($message, true); $this->assertSame(['requeue' => 'true'], $frame->getHeaders()); @@ -210,7 +208,7 @@ public function testShouldReceiveMessageNoWait() $message = new StompMessage(); $message->setFrame(new Frame()); - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setType(StompDestination::TYPE_QUEUE); $destination->setStompName('name'); @@ -247,7 +245,7 @@ public function testReceiveMessageNoWaitShouldSubscribeOnlyOnce() $message = new StompMessage(); $message->setFrame(new Frame()); - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setType(StompDestination::TYPE_QUEUE); $destination->setStompName('name'); @@ -280,7 +278,7 @@ public function testShouldAddExtraHeadersOnSubscribe() ->method('readMessageFrame') ; - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setStompName('name'); $destination->setType(StompDestination::TYPE_QUEUE); $destination->setDurable(true); @@ -340,7 +338,7 @@ public function testShouldConvertStompMessageFrameToMessage() ->willReturn($stompMessageFrame) ; - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setStompName('name'); $destination->setType(StompDestination::TYPE_QUEUE); @@ -381,7 +379,7 @@ public function testShouldThrowLogicExceptionIfFrameIsNotMessageFrame() ->willReturn($stompMessageFrame) ; - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setStompName('name'); $destination->setType(StompDestination::TYPE_QUEUE); @@ -418,7 +416,7 @@ public function testShouldReceiveWithUnlimitedTimeout() ->willReturn(new Frame('MESSAGE')) ; - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setStompName('name'); $destination->setType(StompDestination::TYPE_QUEUE); @@ -454,7 +452,7 @@ public function testShouldReceiveWithTimeout() ->willReturn(new Frame('MESSAGE')) ; - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setStompName('name'); $destination->setType(StompDestination::TYPE_QUEUE); @@ -480,7 +478,7 @@ public function testShouldReceiveWithoutSubscribeIfTempQueue() $message = new StompMessage(); $message->setFrame(new Frame()); - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setType(StompDestination::TYPE_TEMP_QUEUE); $destination->setStompName('name'); @@ -503,7 +501,7 @@ public function testShouldReceiveNoWaitWithoutSubscribeIfTempQueue() $message = new StompMessage(); $message->setFrame(new Frame()); - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setType(StompDestination::TYPE_TEMP_QUEUE); $destination->setStompName('name'); @@ -513,15 +511,15 @@ public function testShouldReceiveNoWaitWithoutSubscribeIfTempQueue() public function testShouldGenerateUniqueSubscriptionIdPerConsumer() { - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setType(StompDestination::TYPE_QUEUE); $destination->setStompName('name'); $fooConsumer = new StompConsumer($this->createStompClientMock(), $destination); $barConsumer = new StompConsumer($this->createStompClientMock(), $destination); - $this->assertAttributeNotEmpty('subscriptionId', $fooConsumer); - $this->assertAttributeNotEmpty('subscriptionId', $barConsumer); + $this->assertNotEmpty($this->readAttribute($fooConsumer, 'subscriptionId')); + $this->assertNotEmpty($this->readAttribute($barConsumer, 'subscriptionId')); $fooSubscriptionId = $this->readAttribute($fooConsumer, 'subscriptionId'); $barSubscriptionId = $this->readAttribute($barConsumer, 'subscriptionId'); @@ -530,7 +528,7 @@ public function testShouldGenerateUniqueSubscriptionIdPerConsumer() public function testShouldUseTempQueueNameAsSubscriptionId() { - $destination = new StompDestination(); + $destination = $this->createDummyDestination(); $destination->setType(StompDestination::TYPE_TEMP_QUEUE); $destination->setStompName('foo'); @@ -540,7 +538,7 @@ public function testShouldUseTempQueueNameAsSubscriptionId() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Protocol + * @return \PHPUnit\Framework\MockObject\MockObject|Protocol */ private function createStompProtocolMock() { @@ -548,10 +546,19 @@ private function createStompProtocolMock() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|BufferedStompClient + * @return \PHPUnit\Framework\MockObject\MockObject|BufferedStompClient */ private function createStompClientMock() { return $this->createMock(BufferedStompClient::class); } + + private function createDummyDestination(): StompDestination + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setStompName('aName'); + $destination->setType(StompDestination::TYPE_QUEUE); + + return $destination; + } } diff --git a/pkg/stomp/Tests/StompContextTest.php b/pkg/stomp/Tests/StompContextTest.php index daa6f39e9..cfb9245dc 100644 --- a/pkg/stomp/Tests/StompContextTest.php +++ b/pkg/stomp/Tests/StompContextTest.php @@ -3,15 +3,16 @@ namespace Enqueue\Stomp\Tests; use Enqueue\Stomp\BufferedStompClient; +use Enqueue\Stomp\ExtensionType; use Enqueue\Stomp\StompConsumer; use Enqueue\Stomp\StompContext; use Enqueue\Stomp\StompDestination; use Enqueue\Stomp\StompMessage; use Enqueue\Stomp\StompProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrQueue; +use Interop\Queue\Context; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Queue; class StompContextTest extends \PHPUnit\Framework\TestCase { @@ -19,19 +20,7 @@ class StompContextTest extends \PHPUnit\Framework\TestCase public function testShouldImplementSessionInterface() { - $this->assertClassImplements(PsrContext::class, StompContext::class); - } - - public function testCouldBeCreatedWithRequiredArguments() - { - new StompContext($this->createStompClientMock()); - } - - public function testCouldBeConstructedWithExtChannelCallbackFactoryAsFirstArgument() - { - new StompContext(function () { - return $this->createStompClientMock(); - }); + $this->assertClassImplements(Context::class, StompContext::class); } public function testThrowIfNeitherCallbackNorExtChannelAsFirstArgument() @@ -39,12 +28,12 @@ public function testThrowIfNeitherCallbackNorExtChannelAsFirstArgument() $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The stomp argument must be either BufferedStompClient or callable that return BufferedStompClient.'); - new StompContext(new \stdClass()); + new StompContext(new \stdClass(), ExtensionType::RABBITMQ); } public function testShouldCreateMessageInstance() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $message = $context->createMessage('the body', ['key' => 'value'], ['hkey' => 'hvalue']); @@ -56,7 +45,7 @@ public function testShouldCreateMessageInstance() public function testShouldCreateQueueInstance() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $queue = $context->createQueue('the name'); @@ -68,7 +57,7 @@ public function testShouldCreateQueueInstance() public function testCreateQueueShouldCreateDestinationIfNameIsFullDestinationString() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $destination = $context->createQueue('/amq/queue/name/routing-key'); @@ -79,9 +68,9 @@ public function testCreateQueueShouldCreateDestinationIfNameIsFullDestinationStr $this->assertEquals('/amq/queue/name/routing-key', $destination->getQueueName()); } - public function testShouldCreateTopicInstance() + public function testShouldCreateTopicInstanceWithExchangePrefix() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $topic = $context->createTopic('the name'); @@ -91,9 +80,21 @@ public function testShouldCreateTopicInstance() $this->assertSame(StompDestination::TYPE_EXCHANGE, $topic->getType()); } + public function testShouldCreateTopicInstanceWithTopicPrefix() + { + $context = new StompContext($this->createStompClientMock(), ExtensionType::ACTIVEMQ); + + $topic = $context->createTopic('the name'); + + $this->assertInstanceOf(StompDestination::class, $topic); + $this->assertSame('/topic/the name', $topic->getQueueName()); + $this->assertSame('/topic/the name', $topic->getTopicName()); + $this->assertSame(StompDestination::TYPE_TOPIC, $topic->getType()); + } + public function testCreateTopicShouldCreateDestinationIfNameIsFullDestinationString() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $destination = $context->createTopic('/amq/queue/name/routing-key'); @@ -109,20 +110,20 @@ public function testThrowInvalidDestinationException() $this->expectException(InvalidDestinationException::class); $this->expectExceptionMessage('The destination must be an instance of'); - $session = new StompContext($this->createStompClientMock()); - $session->createConsumer($this->createMock(PsrQueue::class)); + $session = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); + $session->createConsumer($this->createMock(Queue::class)); } public function testShouldCreateMessageConsumerInstance() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); - $this->assertInstanceOf(StompConsumer::class, $context->createConsumer(new StompDestination())); + $this->assertInstanceOf(StompConsumer::class, $context->createConsumer($this->createDummyDestination())); } public function testShouldCreateMessageProducerInstance() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $this->assertInstanceOf(StompProducer::class, $context->createProducer()); } @@ -131,14 +132,14 @@ public function testShouldCloseConnections() { $client = $this->createStompClientMock(); $client - ->expects($this->once()) + ->expects($this->atLeastOnce()) ->method('disconnect') ; - $context = new StompContext($client); + $context = new StompContext($client, ExtensionType::RABBITMQ); $context->createProducer(); - $context->createConsumer(new StompDestination()); + $context->createConsumer($this->createDummyDestination()); $context->close(); } @@ -148,7 +149,7 @@ public function testCreateDestinationShouldThrowLogicExceptionIfTypeIsInvalid() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Destination name is invalid, cant find type: "/invalid-type/name"'); - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $context->createDestination('/invalid-type/name'); } @@ -157,7 +158,7 @@ public function testCreateDestinationShouldThrowLogicExceptionIfExtraSlashFound( $this->expectException(\LogicException::class); $this->expectExceptionMessage('Destination name is invalid, found extra / char: "/queue/name/routing-key/extra'); - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $context->createDestination('/queue/name/routing-key/extra'); } @@ -166,7 +167,7 @@ public function testCreateDestinationShouldThrowLogicExceptionIfNameIsEmpty() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Destination name is invalid, name is empty: "/queue/"'); - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $context->createDestination('/queue/'); } @@ -175,13 +176,13 @@ public function testCreateDestinationShouldThrowLogicExceptionIfRoutingKeyIsEmpt $this->expectException(\LogicException::class); $this->expectExceptionMessage('Destination name is invalid, routing key is empty: "/queue/name/"'); - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $context->createDestination('/queue/name/'); } public function testCreateDestinationShouldParseStringAndCreateDestination() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $destination = $context->createDestination('/amq/queue/name/routing-key'); $this->assertEquals('amq/queue', $destination->getType()); @@ -192,7 +193,7 @@ public function testCreateDestinationShouldParseStringAndCreateDestination() public function testCreateTemporaryQueue() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $tempQueue = $context->createTemporaryQueue(); $this->assertEquals('temp-queue', $tempQueue->getType()); @@ -203,7 +204,7 @@ public function testCreateTemporaryQueue() public function testCreateTemporaryQueuesWithUniqueNames() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $fooTempQueue = $context->createTemporaryQueue(); $barTempQueue = $context->createTemporaryQueue(); @@ -215,16 +216,25 @@ public function testCreateTemporaryQueuesWithUniqueNames() public function testShouldGetBufferedStompClient() { - $context = new StompContext($this->createStompClientMock()); + $context = new StompContext($this->createStompClientMock(), ExtensionType::RABBITMQ); $this->assertInstanceOf(BufferedStompClient::class, $context->getStomp()); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|BufferedStompClient + * @return \PHPUnit\Framework\MockObject\MockObject|BufferedStompClient */ private function createStompClientMock() { return $this->createMock(BufferedStompClient::class); } + + private function createDummyDestination(): StompDestination + { + $destination = new StompDestination(ExtensionType::RABBITMQ); + $destination->setStompName('aName'); + $destination->setType(StompDestination::TYPE_QUEUE); + + return $destination; + } } diff --git a/pkg/stomp/Tests/StompDestinationTest.php b/pkg/stomp/Tests/StompDestinationTest.php index 53cdad3d9..5061655f8 100644 --- a/pkg/stomp/Tests/StompDestinationTest.php +++ b/pkg/stomp/Tests/StompDestinationTest.php @@ -2,10 +2,11 @@ namespace Enqueue\Stomp\Tests; +use Enqueue\Stomp\ExtensionType; use Enqueue\Stomp\StompDestination; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\PsrQueue; -use Interop\Queue\PsrTopic; +use Interop\Queue\Queue; +use Interop\Queue\Topic; class StompDestinationTest extends \PHPUnit\Framework\TestCase { @@ -13,13 +14,13 @@ class StompDestinationTest extends \PHPUnit\Framework\TestCase public function testShouldImplementsTopicAndQueueInterfaces() { - $this->assertClassImplements(PsrTopic::class, StompDestination::class); - $this->assertClassImplements(PsrQueue::class, StompDestination::class); + $this->assertClassImplements(Topic::class, StompDestination::class); + $this->assertClassImplements(Queue::class, StompDestination::class); } public function testShouldReturnDestinationStringWithRoutingKey() { - $destination = new StompDestination(); + $destination = new StompDestination(ExtensionType::RABBITMQ); $destination->setType(StompDestination::TYPE_AMQ_QUEUE); $destination->setStompName('name'); $destination->setRoutingKey('routing-key'); @@ -32,7 +33,7 @@ public function testShouldReturnDestinationStringWithRoutingKey() public function testShouldReturnDestinationStringWithoutRoutingKey() { - $destination = new StompDestination(); + $destination = new StompDestination(ExtensionType::RABBITMQ); $destination->setType(StompDestination::TYPE_TOPIC); $destination->setStompName('name'); @@ -45,21 +46,11 @@ public function testShouldReturnDestinationStringWithoutRoutingKey() public function testShouldThrowLogicExceptionIfNameIsNotSet() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Destination type or name is not set'); + $this->expectExceptionMessage('Destination name is not set'); - $destination = new StompDestination(); + $destination = new StompDestination(ExtensionType::RABBITMQ); $destination->setType(StompDestination::TYPE_QUEUE); - - $destination->getQueueName(); - } - - public function testShouldThrowLogicExceptionIfTypeIsNotSet() - { - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Destination type or name is not set'); - - $destination = new StompDestination(); - $destination->setStompName('name'); + $destination->setStompName(''); $destination->getQueueName(); } @@ -69,7 +60,7 @@ public function testSetTypeShouldThrowLogicExceptionIfTypeIsInvalid() $this->expectException(\LogicException::class); $this->expectExceptionMessage('Invalid destination type: "invalid-type"'); - $destination = new StompDestination(); + $destination = new StompDestination(ExtensionType::RABBITMQ); $destination->setType('invalid-type'); } } diff --git a/pkg/stomp/Tests/StompHeadersEncoderTest.php b/pkg/stomp/Tests/StompHeadersEncoderTest.php index 7997e05a4..cd3d112dd 100644 --- a/pkg/stomp/Tests/StompHeadersEncoderTest.php +++ b/pkg/stomp/Tests/StompHeadersEncoderTest.php @@ -32,9 +32,6 @@ public function propertyValuesDataProvider() /** * @dataProvider headerValuesDataProvider - * - * @param mixed $originalValue - * @param mixed $encodedValue */ public function testShouldEncodeHeaders($originalValue, $encodedValue) { @@ -43,9 +40,6 @@ public function testShouldEncodeHeaders($originalValue, $encodedValue) /** * @dataProvider propertyValuesDataProvider - * - * @param mixed $originalValue - * @param mixed $encodedValue */ public function testShouldEncodeProperties($originalValue, $encodedValue) { @@ -54,9 +48,6 @@ public function testShouldEncodeProperties($originalValue, $encodedValue) /** * @dataProvider headerValuesDataProvider - * - * @param mixed $originalValue - * @param mixed $encodedValue */ public function testShouldDecodeHeaders($originalValue, $encodedValue) { @@ -65,9 +56,6 @@ public function testShouldDecodeHeaders($originalValue, $encodedValue) /** * @dataProvider propertyValuesDataProvider - * - * @param mixed $originalValue - * @param mixed $encodedValue */ public function testShouldDecodeProperties($originalValue, $encodedValue) { diff --git a/pkg/stomp/Tests/StompMessageTest.php b/pkg/stomp/Tests/StompMessageTest.php index 6685433ba..49be1a8c5 100644 --- a/pkg/stomp/Tests/StompMessageTest.php +++ b/pkg/stomp/Tests/StompMessageTest.php @@ -86,4 +86,34 @@ public function testShouldSetReplyToAsHeader() self::assertSame(['reply-to' => 'theQueueName'], $message->getHeaders()); } + + public function testShouldUnsetHeaderIfNullPassed() + { + $message = new StompMessage(); + + $message->setHeader('aHeader', 'aVal'); + + // guard + $this->assertSame('aVal', $message->getHeader('aHeader')); + + $message->setHeader('aHeader', null); + + $this->assertNull($message->getHeader('aHeader')); + $this->assertSame([], $message->getHeaders()); + } + + public function testShouldUnsetPropertyIfNullPassed() + { + $message = new StompMessage(); + + $message->setProperty('aProperty', 'aVal'); + + // guard + $this->assertSame('aVal', $message->getProperty('aProperty')); + + $message->setProperty('aProperty', null); + + $this->assertNull($message->getProperty('aProperty')); + $this->assertSame([], $message->getProperties()); + } } diff --git a/pkg/stomp/Tests/StompProducerTest.php b/pkg/stomp/Tests/StompProducerTest.php index 4db44c5fe..41f35256c 100644 --- a/pkg/stomp/Tests/StompProducerTest.php +++ b/pkg/stomp/Tests/StompProducerTest.php @@ -2,17 +2,18 @@ namespace Enqueue\Stomp\Tests; +use Enqueue\Stomp\ExtensionType; use Enqueue\Stomp\StompDestination; use Enqueue\Stomp\StompMessage; use Enqueue\Stomp\StompProducer; use Enqueue\Test\ClassExtensionTrait; -use Interop\Queue\InvalidDestinationException; -use Interop\Queue\InvalidMessageException; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProducer; -use Interop\Queue\PsrQueue; +use Interop\Queue\Exception\InvalidDestinationException; +use Interop\Queue\Exception\InvalidMessageException; +use Interop\Queue\Message; +use Interop\Queue\Producer; +use Interop\Queue\Queue; use Stomp\Client; -use Stomp\Transport\Message; +use Stomp\Transport\Message as VendorMessage; class StompProducerTest extends \PHPUnit\Framework\TestCase { @@ -20,7 +21,7 @@ class StompProducerTest extends \PHPUnit\Framework\TestCase public function testShouldImplementProducerInterface() { - $this->assertClassImplements(PsrProducer::class, StompProducer::class); + $this->assertClassImplements(Producer::class, StompProducer::class); } public function testShouldThrowInvalidDestinationExceptionWhenDestinationIsWrongType() @@ -30,7 +31,7 @@ public function testShouldThrowInvalidDestinationExceptionWhenDestinationIsWrong $producer = new StompProducer($this->createStompClientMock()); - $producer->send($this->createMock(PsrQueue::class), new StompMessage()); + $producer->send($this->createMock(Queue::class), new StompMessage()); } public function testShouldThrowInvalidMessageExceptionWhenMessageIsWrongType() @@ -40,7 +41,7 @@ public function testShouldThrowInvalidMessageExceptionWhenMessageIsWrongType() $producer = new StompProducer($this->createStompClientMock()); - $producer->send(new StompDestination(), $this->createMock(PsrMessage::class)); + $producer->send(new StompDestination(ExtensionType::RABBITMQ), $this->createMock(Message::class)); } public function testShouldSendMessage() @@ -49,12 +50,12 @@ public function testShouldSendMessage() $client ->expects($this->once()) ->method('send') - ->with('/queue/name', $this->isInstanceOf(Message::class)) + ->with('/queue/name', $this->isInstanceOf(VendorMessage::class)) ; $producer = new StompProducer($client); - $destination = new StompDestination(); + $destination = new StompDestination(ExtensionType::RABBITMQ); $destination->setType(StompDestination::TYPE_QUEUE); $destination->setStompName('name'); @@ -68,7 +69,7 @@ public function testShouldEncodeMessageHeadersAndProperties() $client ->expects($this->once()) ->method('send') - ->willReturnCallback(function ($destination, Message $message) use (&$stompMessage) { + ->willReturnCallback(function ($destination, VendorMessage $message) use (&$stompMessage) { $stompMessage = $message; }) ; @@ -77,7 +78,7 @@ public function testShouldEncodeMessageHeadersAndProperties() $message = new StompMessage('', ['key' => 'value'], ['hkey' => false]); - $destination = new StompDestination(); + $destination = new StompDestination(ExtensionType::RABBITMQ); $destination->setType(StompDestination::TYPE_QUEUE); $destination->setStompName('name'); @@ -100,7 +101,7 @@ public function testShouldEncodeMessageHeadersAndProperties() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Client + * @return \PHPUnit\Framework\MockObject\MockObject|Client */ private function createStompClientMock() { diff --git a/pkg/stomp/Tests/Symfony/RabbitMqStompTransportFactoryTest.php b/pkg/stomp/Tests/Symfony/RabbitMqStompTransportFactoryTest.php deleted file mode 100644 index 9fbdb6bed..000000000 --- a/pkg/stomp/Tests/Symfony/RabbitMqStompTransportFactoryTest.php +++ /dev/null @@ -1,156 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, RabbitMqStompTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new RabbitMqStompTransportFactory(); - - $this->assertEquals('rabbitmq_stomp', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new RabbitMqStompTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new RabbitMqStompTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 61613, - 'login' => 'guest', - 'password' => 'guest', - 'vhost' => '/', - 'sync' => true, - 'connection_timeout' => 1, - 'buffer_size' => 1000, - 'delay_plugin_installed' => false, - 'management_plugin_installed' => false, - 'management_plugin_port' => 15672, - 'lazy' => true, - 'ssl_on' => false, - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqStompTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'uri' => 'tcp://localhost:61613', - 'login' => 'guest', - 'password' => 'guest', - 'vhost' => '/', - 'sync' => true, - 'connection_timeout' => 1, - 'buffer_size' => 1000, - 'delay_plugin_installed' => false, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(StompConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'uri' => 'tcp://localhost:61613', - 'login' => 'guest', - 'password' => 'guest', - 'vhost' => '/', - 'sync' => true, - 'connection_timeout' => 1, - 'buffer_size' => 1000, - 'delay_plugin_installed' => false, - ]], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqStompTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'uri' => 'tcp://localhost:61613', - 'login' => 'guest', - 'password' => 'guest', - 'vhost' => '/', - 'sync' => true, - 'connection_timeout' => 1, - 'buffer_size' => 1000, - 'delay_plugin_installed' => false, - ]); - - $this->assertEquals('enqueue.transport.rabbitmq_stomp.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.rabbitmq_stomp.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.rabbitmq_stomp.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new RabbitMqStompTransportFactory(); - - $serviceId = $transport->createDriver($container, [ - 'vhost' => 'vhost', - 'host' => 'host', - 'management_plugin_port' => 'port', - 'login' => 'login', - 'password' => 'password', - ]); - - $this->assertTrue($container->hasDefinition('enqueue.client.rabbitmq_stomp.management_client')); - $managementClient = $container->getDefinition('enqueue.client.rabbitmq_stomp.management_client'); - $this->assertEquals(ManagementClient::class, $managementClient->getClass()); - $this->assertEquals([ManagementClient::class, 'create'], $managementClient->getFactory()); - $this->assertEquals([ - 'vhost', - 'host', - 'port', - 'login', - 'password', - ], $managementClient->getArguments()); - - $this->assertEquals('enqueue.client.rabbitmq_stomp.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(RabbitMqStompDriver::class, $driver->getClass()); - } -} diff --git a/pkg/stomp/Tests/Symfony/StompTransportFactoryTest.php b/pkg/stomp/Tests/Symfony/StompTransportFactoryTest.php deleted file mode 100644 index 5591c00c0..000000000 --- a/pkg/stomp/Tests/Symfony/StompTransportFactoryTest.php +++ /dev/null @@ -1,131 +0,0 @@ -assertClassImplements(TransportFactoryInterface::class, StompTransportFactory::class); - } - - public function testCouldBeConstructedWithDefaultName() - { - $transport = new StompTransportFactory(); - - $this->assertEquals('stomp', $transport->getName()); - } - - public function testCouldBeConstructedWithCustomName() - { - $transport = new StompTransportFactory('theCustomName'); - - $this->assertEquals('theCustomName', $transport->getName()); - } - - public function testShouldAllowAddConfiguration() - { - $transport = new StompTransportFactory(); - $tb = new TreeBuilder(); - $rootNode = $tb->root('foo'); - - $transport->addConfiguration($rootNode); - $processor = new Processor(); - $config = $processor->process($tb->buildTree(), []); - - $this->assertEquals([ - 'host' => 'localhost', - 'port' => 61613, - 'login' => 'guest', - 'password' => 'guest', - 'vhost' => '/', - 'sync' => true, - 'connection_timeout' => 1, - 'buffer_size' => 1000, - 'lazy' => true, - 'ssl_on' => false, - ], $config); - } - - public function testShouldCreateConnectionFactory() - { - $container = new ContainerBuilder(); - - $transport = new StompTransportFactory(); - - $serviceId = $transport->createConnectionFactory($container, [ - 'uri' => 'tcp://localhost:61613', - 'login' => 'guest', - 'password' => 'guest', - 'vhost' => '/', - 'sync' => true, - 'connection_timeout' => 1, - 'buffer_size' => 1000, - ]); - - $this->assertTrue($container->hasDefinition($serviceId)); - $factory = $container->getDefinition($serviceId); - $this->assertEquals(StompConnectionFactory::class, $factory->getClass()); - $this->assertSame([[ - 'uri' => 'tcp://localhost:61613', - 'login' => 'guest', - 'password' => 'guest', - 'vhost' => '/', - 'sync' => true, - 'connection_timeout' => 1, - 'buffer_size' => 1000, - ]], $factory->getArguments()); - } - - public function testShouldCreateContext() - { - $container = new ContainerBuilder(); - - $transport = new StompTransportFactory(); - - $serviceId = $transport->createContext($container, [ - 'uri' => 'tcp://localhost:61613', - 'login' => 'guest', - 'password' => 'guest', - 'vhost' => '/', - 'sync' => true, - 'connection_timeout' => 1, - 'buffer_size' => 1000, - ]); - - $this->assertEquals('enqueue.transport.stomp.context', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $context = $container->getDefinition('enqueue.transport.stomp.context'); - $this->assertInstanceOf(Reference::class, $context->getFactory()[0]); - $this->assertEquals('enqueue.transport.stomp.connection_factory', (string) $context->getFactory()[0]); - $this->assertEquals('createContext', $context->getFactory()[1]); - } - - public function testShouldCreateDriver() - { - $container = new ContainerBuilder(); - - $transport = new StompTransportFactory(); - - $serviceId = $transport->createDriver($container, []); - - $this->assertEquals('enqueue.client.stomp.driver', $serviceId); - $this->assertTrue($container->hasDefinition($serviceId)); - - $driver = $container->getDefinition($serviceId); - $this->assertSame(StompDriver::class, $driver->getClass()); - } -} diff --git a/pkg/stomp/composer.json b/pkg/stomp/composer.json index 1bd4bf9c5..2cceb9fea 100644 --- a/pkg/stomp/composer.json +++ b/pkg/stomp/composer.json @@ -6,21 +6,21 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "stomp-php/stomp-php": "^4", - "queue-interop/queue-interop": "^0.6@dev|^1.0.0-alpha1", - "php-http/guzzle6-adapter": "^1.1", - "php-http/client-common": "^1.7@dev", - "richardfullmer/rabbitmq-management-api": "^2.0" + "php": "^8.1", + "enqueue/dsn": "^0.10", + "stomp-php/stomp-php": "^4.5|^5.0", + "queue-interop/queue-interop": "^0.8", + "php-http/guzzle7-adapter": "^0.1.1", + "php-http/client-common": "^2.2.1", + "andrewmy/rabbitmq-management-api": "^2.1.2", + "guzzlehttp/guzzle": "^7.0.1", + "php-http/discovery": "^1.13" }, "require-dev": { - "phpunit/phpunit": "~5.4.0", - "enqueue/test": "^0.8@dev", - "enqueue/enqueue": "^0.8@dev", - "enqueue/null": "^0.8@dev", - "queue-interop/queue-spec": "^0.5.3@dev", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4" + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" }, "support": { "email": "opensource@forma-pro.com", @@ -35,13 +35,10 @@ "/Tests/" ] }, - "suggest": { - "enqueue/enqueue": "If you'd like to use advanced features like Client abstract layer or Symfony integration features" - }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/stomp/examples/consume.php b/pkg/stomp/examples/consume.php deleted file mode 100644 index f6cc2fdff..000000000 --- a/pkg/stomp/examples/consume.php +++ /dev/null @@ -1,52 +0,0 @@ - getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_STOMP_PORT'), - 'login' => getenv('RABBITMQ_USER'), - 'password' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - 'sync' => true, -]; - -try { - $factory = new StompConnectionFactory($config); - $context = $factory->createContext(); - - $destination = $context->createQueue('destination'); - $destination->setDurable(true); - $destination->setAutoDelete(false); - - $consumer = $context->createConsumer($destination); - - while (true) { - if ($message = $consumer->receive()) { - $consumer->acknowledge($message); - - var_dump($message->getBody()); - var_dump($message->getProperties()); - var_dump($message->getHeaders()); - echo '-------------------------------------'.PHP_EOL; - } - } -} catch (ErrorFrameException $e) { - var_dump($e->getFrame()); -} diff --git a/pkg/stomp/examples/publish.php b/pkg/stomp/examples/publish.php deleted file mode 100644 index a320a40a6..000000000 --- a/pkg/stomp/examples/publish.php +++ /dev/null @@ -1,48 +0,0 @@ - getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_STOMP_PORT'), - 'login' => getenv('RABBITMQ_USER'), - 'password' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - 'sync' => true, -]; - -try { - $factory = new StompConnectionFactory($config); - $context = $factory->createContext(); - - $destination = $context->createQueue('destination'); - $destination->setDurable(true); - $destination->setAutoDelete(false); - - $producer = $context->createProducer(); - - $i = 1; - while (true) { - $message = $context->createMessage('payload: '.$i++); - $producer->send($destination, $message); - usleep(1000); - } -} catch (ErrorFrameException $e) { - var_dump($e->getFrame()); -} diff --git a/pkg/stomp/phpunit.xml.dist b/pkg/stomp/phpunit.xml.dist index a9291bf93..ae7136aca 100644 --- a/pkg/stomp/phpunit.xml.dist +++ b/pkg/stomp/phpunit.xml.dist @@ -1,16 +1,11 @@ - + diff --git a/pkg/test/ClassExtensionTrait.php b/pkg/test/ClassExtensionTrait.php index b9dea0c8b..75d70ae63 100644 --- a/pkg/test/ClassExtensionTrait.php +++ b/pkg/test/ClassExtensionTrait.php @@ -23,4 +23,24 @@ public function assertClassImplements($expected, $actual) sprintf('Failed assert that class %s implements %s interface.', $actual, $expected) ); } + + public function assertClassFinal($actual) + { + $rc = new \ReflectionClass($actual); + + $this->assertTrue( + $rc->isFinal(), + sprintf('Failed assert that class %s is final.', $actual) + ); + } + + public function assertClassNotFinal($actual) + { + $rc = new \ReflectionClass($actual); + + $this->assertFalse( + $rc->isFinal(), + sprintf('Failed assert that class %s is final.', $actual) + ); + } } diff --git a/pkg/test/GpsExtension.php b/pkg/test/GpsExtension.php new file mode 100644 index 000000000..2b03ef64b --- /dev/null +++ b/pkg/test/GpsExtension.php @@ -0,0 +1,21 @@ +createContext(); + } +} diff --git a/pkg/test/MongodbExtensionTrait.php b/pkg/test/MongodbExtensionTrait.php index 4d94fca40..3ba9e93e0 100644 --- a/pkg/test/MongodbExtensionTrait.php +++ b/pkg/test/MongodbExtensionTrait.php @@ -1,12 +1,15 @@ markTestSkipped('The MONGO_DSN env is not available. Skip tests'); diff --git a/pkg/test/README.md b/pkg/test/README.md index cb8ded237..a2411c1b3 100644 --- a/pkg/test/README.md +++ b/pkg/test/README.md @@ -1,24 +1,33 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Message Queue. Test utils [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) - -Contains stuff needed in tests. Shared among different packages. + +Contains stuff needed in tests. Shared among different packages. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com ## License -It is released under the [MIT License](LICENSE). \ No newline at end of file +It is released under the [MIT License](LICENSE). diff --git a/pkg/test/RabbitManagementExtensionTrait.php b/pkg/test/RabbitManagementExtensionTrait.php index c7665882a..184b1758e 100644 --- a/pkg/test/RabbitManagementExtensionTrait.php +++ b/pkg/test/RabbitManagementExtensionTrait.php @@ -2,6 +2,8 @@ namespace Enqueue\Test; +use Enqueue\Dsn\Dsn; + trait RabbitManagementExtensionTrait { /** @@ -9,30 +11,27 @@ trait RabbitManagementExtensionTrait */ private function removeQueue($queueName) { - $rabbitmqHost = getenv('RABBITMQ_HOST'); - $rabbitmqUser = getenv('RABBITMQ_USER'); - $rabbitmqPassword = getenv('RABBITMQ_PASSWORD'); - $rabbitmqVhost = getenv('RABBITMQ_VHOST'); + $dsn = Dsn::parseFirst(getenv('RABBITMQ_AMQP_DSN')); $url = sprintf( 'http://%s:15672/api/queues/%s/%s', - $rabbitmqHost, - urlencode($rabbitmqVhost), + $dsn->getHost(), + urlencode(ltrim($dsn->getPath(), '/')), $queueName ); $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - curl_setopt($ch, CURLOPT_USERPWD, $rabbitmqUser.':'.$rabbitmqPassword); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ + curl_setopt($ch, \CURLOPT_URL, $url); + curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, \CURLOPT_HTTPAUTH, \CURLAUTH_BASIC); + curl_setopt($ch, \CURLOPT_USERPWD, $dsn->getUser().':'.$dsn->getPassword()); + curl_setopt($ch, \CURLOPT_HTTPHEADER, [ 'Content-Type' => 'application/json', ]); curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $httpCode = curl_getinfo($ch, \CURLINFO_HTTP_CODE); curl_close($ch); @@ -46,30 +45,27 @@ private function removeQueue($queueName) */ private function removeExchange($exchangeName) { - $rabbitmqHost = getenv('RABBITMQ_HOST'); - $rabbitmqUser = getenv('RABBITMQ_USER'); - $rabbitmqPassword = getenv('RABBITMQ_PASSWORD'); - $rabbitmqVhost = getenv('RABBITMQ_VHOST'); + $dsn = Dsn::parseFirst(getenv('RABBITMQ_AMQP_DSN')); $url = sprintf( 'http://%s:15672/api/exchanges/%s/%s', - $rabbitmqHost, - urlencode($rabbitmqVhost), + $dsn->getHost(), + urlencode(ltrim($dsn->getPath(), '/')), $exchangeName ); $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - curl_setopt($ch, CURLOPT_USERPWD, $rabbitmqUser.':'.$rabbitmqPassword); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ + curl_setopt($ch, \CURLOPT_URL, $url); + curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE'); + curl_setopt($ch, \CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, \CURLOPT_HTTPAUTH, \CURLAUTH_BASIC); + curl_setopt($ch, \CURLOPT_USERPWD, $dsn->getUser().':'.$dsn->getPassword()); + curl_setopt($ch, \CURLOPT_HTTPHEADER, [ 'Content-Type' => 'application/json', ]); curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $httpCode = curl_getinfo($ch, \CURLINFO_HTTP_CODE); curl_close($ch); diff --git a/pkg/test/RabbitmqAmqpExtension.php b/pkg/test/RabbitmqAmqpExtension.php index 1a4da0e90..28099f12b 100644 --- a/pkg/test/RabbitmqAmqpExtension.php +++ b/pkg/test/RabbitmqAmqpExtension.php @@ -4,6 +4,7 @@ use Enqueue\AmqpExt\AmqpConnectionFactory; use Enqueue\AmqpExt\AmqpContext; +use PHPUnit\Framework\SkippedTestError; trait RabbitmqAmqpExtension { @@ -11,29 +12,9 @@ trait RabbitmqAmqpExtension * @return AmqpContext */ private function buildAmqpContext() - { - if (false == getenv('RABBITMQ_HOST')) { - throw new \PHPUnit_Framework_SkippedTestError('Functional tests are not allowed in this environment'); - } - - $config = [ - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_AMQP__PORT'), - 'user' => getenv('RABBITMQ_USER'), - 'pass' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - ]; - - return (new AmqpConnectionFactory($config))->createContext(); - } - - /** - * @return AmqpContext - */ - private function buildAmqpContextFromDsn() { if (false == $dsn = getenv('AMQP_DSN')) { - throw new \PHPUnit_Framework_SkippedTestError('Functional tests are not allowed in this environment'); + throw new SkippedTestError('Functional tests are not allowed in this environment'); } return (new AmqpConnectionFactory($dsn))->createContext(); diff --git a/pkg/test/RabbitmqStompExtension.php b/pkg/test/RabbitmqStompExtension.php index ae2bf4bda..240f67edb 100644 --- a/pkg/test/RabbitmqStompExtension.php +++ b/pkg/test/RabbitmqStompExtension.php @@ -4,27 +4,21 @@ use Enqueue\Stomp\StompConnectionFactory; use Enqueue\Stomp\StompContext; +use PHPUnit\Framework\SkippedTestError; trait RabbitmqStompExtension { - /** - * @return StompContext - */ - private function buildStompContext() + private function getDsn() { - if (false == getenv('RABBITMQ_HOST')) { - throw new \PHPUnit_Framework_SkippedTestError('Functional tests are not allowed in this environment'); - } + return getenv('RABITMQ_STOMP_DSN'); + } - $config = [ - 'host' => getenv('RABBITMQ_HOST'), - 'port' => getenv('RABBITMQ_STOMP_PORT'), - 'login' => getenv('RABBITMQ_USER'), - 'password' => getenv('RABBITMQ_PASSWORD'), - 'vhost' => getenv('RABBITMQ_VHOST'), - 'sync' => true, - ]; + private function buildStompContext(): StompContext + { + if (false == $dsn = $this->getDsn()) { + throw new SkippedTestError('Functional tests are not allowed in this environment'); + } - return (new StompConnectionFactory($config))->createContext(); + return (new StompConnectionFactory($dsn))->createContext(); } } diff --git a/pkg/test/ReadAttributeTrait.php b/pkg/test/ReadAttributeTrait.php new file mode 100644 index 000000000..5b9758a64 --- /dev/null +++ b/pkg/test/ReadAttributeTrait.php @@ -0,0 +1,57 @@ +getClassAttribute($object, $attribute); + $refProperty->setAccessible(true); + $value = $refProperty->getValue($object); + $refProperty->setAccessible(false); + + return $value; + } + + private function getClassAttribute( + object $object, + string $attribute, + ?string $class = null, + ): \ReflectionProperty { + if (null === $class) { + $class = $object::class; + } + + try { + return new \ReflectionProperty($class, $attribute); + } catch (\ReflectionException $exception) { + $parentClass = get_parent_class($object); + if (false === $parentClass) { + throw $exception; + } + + return $this->getClassAttribute($object, $attribute, $parentClass); + } + } + + private function assertAttributeSame($expected, string $attribute, object $object): void + { + static::assertSame($expected, $this->readAttribute($object, $attribute)); + } + + private function assertAttributeEquals($expected, string $attribute, object $object): void + { + static::assertEquals($expected, $this->readAttribute($object, $attribute)); + } + + private function assertAttributeInstanceOf(string $expected, string $attribute, object $object): void + { + static::assertInstanceOf($expected, $this->readAttribute($object, $attribute)); + } + + private function assertAttributeCount(int $count, string $attribute, object $object): void + { + static::assertCount($count, $this->readAttribute($object, $attribute)); + } +} diff --git a/pkg/test/RedisExtension.php b/pkg/test/RedisExtension.php index 0740c8f21..3227785c2 100644 --- a/pkg/test/RedisExtension.php +++ b/pkg/test/RedisExtension.php @@ -2,46 +2,43 @@ namespace Enqueue\Test; +use Enqueue\Redis\PhpRedis; +use Enqueue\Redis\PRedis; use Enqueue\Redis\RedisConnectionFactory; use Enqueue\Redis\RedisContext; +use PHPUnit\Framework\SkippedTestError; trait RedisExtension { - /** - * @return RedisContext - */ - private function buildPhpRedisContext() + private function buildPhpRedisContext(): RedisContext { - if (false == getenv('REDIS_HOST')) { - throw new \PHPUnit_Framework_SkippedTestError('Functional tests are not allowed in this environment'); + if (false == getenv('PHPREDIS_DSN')) { + throw new SkippedTestError('Functional tests are not allowed in this environment'); } - $config = [ - 'host' => getenv('REDIS_HOST'), - 'port' => getenv('REDIS_PORT'), - 'vendor' => 'phpredis', - 'lazy' => false, - ]; + $config = getenv('PHPREDIS_DSN'); - return (new RedisConnectionFactory($config))->createContext(); + $context = (new RedisConnectionFactory($config))->createContext(); + + // guard + $this->assertInstanceOf(PhpRedis::class, $context->getRedis()); + + return $context; } - /** - * @return RedisContext - */ - private function buildPRedisContext() + private function buildPRedisContext(): RedisContext { - if (false == getenv('REDIS_HOST')) { - throw new \PHPUnit_Framework_SkippedTestError('Functional tests are not allowed in this environment'); + if (false == getenv('PREDIS_DSN')) { + throw new SkippedTestError('Functional tests are not allowed in this environment'); } - $config = [ - 'host' => getenv('REDIS_HOST'), - 'port' => getenv('REDIS_PORT'), - 'vendor' => 'predis', - 'lazy' => false, - ]; + $config = getenv('PREDIS_DSN'); + + $context = (new RedisConnectionFactory($config))->createContext(); + + // guard + $this->assertInstanceOf(PRedis::class, $context->getRedis()); - return (new RedisConnectionFactory($config))->createContext(); + return $context; } } diff --git a/pkg/test/RetryTrait.php b/pkg/test/RetryTrait.php index de17565bd..1f1042c81 100644 --- a/pkg/test/RetryTrait.php +++ b/pkg/test/RetryTrait.php @@ -2,9 +2,13 @@ namespace Enqueue\Test; +use PHPUnit\Framework\IncompleteTestError; +use PHPUnit\Framework\SkippedTestError; +use PHPUnit\Util\Test; + trait RetryTrait { - public function runBare() + public function runBare(): void { $e = null; @@ -22,9 +26,9 @@ public function runBare() parent::runBare(); return; - } catch (\PHPUnit_Framework_IncompleteTestError $e) { + } catch (IncompleteTestError $e) { throw $e; - } catch (\PHPUnit_Framework_SkippedTestError $e) { + } catch (SkippedTestError $e) { throw $e; } catch (\Throwable $e) { // last one thrown below @@ -43,7 +47,7 @@ public function runBare() */ private function getNumberOfRetries() { - $annotations = $this->getAnnotations(); + $annotations = Test::parseTestMethodAnnotations(static::class, $this->getName(false)); if (isset($annotations['method']['retry'][0])) { return $annotations['method']['retry'][0]; diff --git a/pkg/test/SnsExtension.php b/pkg/test/SnsExtension.php new file mode 100644 index 000000000..050f212dd --- /dev/null +++ b/pkg/test/SnsExtension.php @@ -0,0 +1,19 @@ +createContext(); + } +} diff --git a/pkg/test/SnsQsExtension.php b/pkg/test/SnsQsExtension.php new file mode 100644 index 000000000..6dc7dc9d9 --- /dev/null +++ b/pkg/test/SnsQsExtension.php @@ -0,0 +1,19 @@ +createContext(); + } +} diff --git a/pkg/test/SqsExtension.php b/pkg/test/SqsExtension.php index dacb1836f..c00b42e68 100644 --- a/pkg/test/SqsExtension.php +++ b/pkg/test/SqsExtension.php @@ -4,27 +4,16 @@ use Enqueue\Sqs\SqsConnectionFactory; use Enqueue\Sqs\SqsContext; +use PHPUnit\Framework\SkippedTestError; trait SqsExtension { - /** - * @return SqsContext - */ - private function buildSqsContext() + private function buildSqsContext(): SqsContext { - if (false == getenv('AWS_SQS_ENDPOINT') && false == getenv('AWS_SQS_KEY')) { - throw new \PHPUnit_Framework_SkippedTestError('Functional tests are not allowed in this environment'); + if (false == $dsn = getenv('SQS_DSN')) { + throw new SkippedTestError('Functional tests are not allowed in this environment'); } - $config = [ - 'key' => getenv('AWS_SQS_KEY'), - 'secret' => getenv('AWS_SQS_SECRET'), - 'region' => getenv('AWS_SQS_REGION'), - 'version' => getenv('AWS_SQS_VERSION'), - 'endpoint' => getenv('AWS_SQS_ENDPOINT'), - 'lazy' => false, - ]; - - return (new SqsConnectionFactory($config))->createContext(); + return (new SqsConnectionFactory($dsn))->createContext(); } } diff --git a/pkg/test/TestLogger.php b/pkg/test/TestLogger.php new file mode 100644 index 000000000..9db2c2a5e --- /dev/null +++ b/pkg/test/TestLogger.php @@ -0,0 +1,144 @@ + 0) { + $genericMethod = $matches[1].('Records' !== $matches[3] ? 'Record' : '').$matches[3]; + $level = strtolower($matches[2]); + if (method_exists($this, $genericMethod)) { + $args[] = $level; + + return call_user_func_array([$this, $genericMethod], $args); + } + } + throw new \BadMethodCallException('Call to undefined method TestLogger::'.$method.'()'); + } + + public function log($level, $message, array $context = []): void + { + $record = [ + 'level' => $level, + 'message' => $message, + 'context' => $context, + ]; + + $this->recordsByLevel[$record['level']][] = $record; + $this->records[] = $record; + } + + public function hasRecords($level) + { + return isset($this->recordsByLevel[$level]); + } + + public function hasRecord($record, $level) + { + if (is_string($record)) { + $record = ['message' => $record]; + } + + return $this->hasRecordThatPasses(function ($rec) use ($record) { + if ($rec['message'] !== $record['message']) { + return false; + } + if (isset($record['context']) && $rec['context'] !== $record['context']) { + return false; + } + + return true; + }, $level); + } + + public function hasRecordThatContains($message, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($message) { + return str_contains($rec['message'], $message); + }, $level); + } + + public function hasRecordThatMatches($regex, $level) + { + return $this->hasRecordThatPasses(function ($rec) use ($regex) { + return preg_match($regex, $rec['message']) > 0; + }, $level); + } + + public function hasRecordThatPasses(callable $predicate, $level) + { + if (!isset($this->recordsByLevel[$level])) { + return false; + } + foreach ($this->recordsByLevel[$level] as $i => $rec) { + if (call_user_func($predicate, $rec, $i)) { + return true; + } + } + + return false; + } + + public function reset() + { + $this->records = []; + $this->recordsByLevel = []; + } +} diff --git a/pkg/test/WampExtension.php b/pkg/test/WampExtension.php new file mode 100644 index 000000000..5b17fe7cf --- /dev/null +++ b/pkg/test/WampExtension.php @@ -0,0 +1,19 @@ +createContext(); + } +} diff --git a/pkg/test/WriteAttributeTrait.php b/pkg/test/WriteAttributeTrait.php index e2e84bd2a..6f8c1aab5 100644 --- a/pkg/test/WriteAttributeTrait.php +++ b/pkg/test/WriteAttributeTrait.php @@ -7,11 +7,10 @@ trait WriteAttributeTrait /** * @param object $object * @param string $attribute - * @param mixed $value */ public function writeAttribute($object, $attribute, $value) { - $refProperty = new \ReflectionProperty(get_class($object), $attribute); + $refProperty = new \ReflectionProperty($object::class, $attribute); $refProperty->setAccessible(true); $refProperty->setValue($object, $value); $refProperty->setAccessible(false); diff --git a/pkg/test/composer.json b/pkg/test/composer.json index 3ce4390db..ad9234cda 100644 --- a/pkg/test/composer.json +++ b/pkg/test/composer.json @@ -9,13 +9,16 @@ "source": "https://github.com/php-enqueue/enqueue-dev", "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" }, + "require": { + "enqueue/dsn": "^0.10" + }, "autoload": { "psr-4": { "Enqueue\\Test\\": "" } }, "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } } diff --git a/pkg/wamp/.gitattributes b/pkg/wamp/.gitattributes new file mode 100644 index 000000000..bdf2dcb14 --- /dev/null +++ b/pkg/wamp/.gitattributes @@ -0,0 +1,5 @@ +/Tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +phpunit.xml.dist export-ignore diff --git a/pkg/wamp/.github/workflows/ci.yml b/pkg/wamp/.github/workflows/ci.yml new file mode 100644 index 000000000..5448d7b1a --- /dev/null +++ b/pkg/wamp/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/pkg/wamp/.gitignore b/pkg/wamp/.gitignore new file mode 100644 index 000000000..a770439e5 --- /dev/null +++ b/pkg/wamp/.gitignore @@ -0,0 +1,6 @@ +*~ +/composer.lock +/composer.phar +/phpunit.xml +/vendor/ +/.idea/ diff --git a/pkg/wamp/JsonSerializer.php b/pkg/wamp/JsonSerializer.php new file mode 100644 index 000000000..9a224fbb8 --- /dev/null +++ b/pkg/wamp/JsonSerializer.php @@ -0,0 +1,33 @@ + $message->getBody(), + 'properties' => $message->getProperties(), + 'headers' => $message->getHeaders(), + ]); + + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); + } + + return $json; + } + + public function toMessage(string $string): WampMessage + { + $data = json_decode($string, true); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); + } + + return new WampMessage($data['body'], $data['properties'], $data['headers']); + } +} diff --git a/pkg/wamp/LICENSE b/pkg/wamp/LICENSE new file mode 100644 index 000000000..7afbaa1ff --- /dev/null +++ b/pkg/wamp/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright (c) 2018 Forma-Pro + +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/pkg/wamp/README.md b/pkg/wamp/README.md new file mode 100644 index 000000000..ee0bcaa17 --- /dev/null +++ b/pkg/wamp/README.md @@ -0,0 +1,36 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + +# Web Application Messaging Protocol (WAMP) Transport + +[![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/wamp/ci.yml?branch=master)](https://github.com/php-enqueue/wamp/actions?query=workflow%3ACI) +[![Total Downloads](https://poser.pugx.org/enqueue/wamp/d/total.png)](https://packagist.org/packages/enqueue/wamp) +[![Latest Stable Version](https://poser.pugx.org/enqueue/wamp/version.png)](https://packagist.org/packages/enqueue/wamp) + +This is an implementation of [queue interop](https://github.com/queue-interop/queue-interop). It uses [Thruway](https://github.com/thruway/client) internally. + +## Resources + +* [Site](https://enqueue.forma-pro.com/) +* [Documentation](https://php-enqueue.github.io/transport/wamp/) +* [Questions](https://gitter.im/php-enqueue/Lobby) +* [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) + +## Developed by Forma-Pro + +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. + +If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com + +## License + +It is released under the [MIT License](LICENSE). diff --git a/pkg/wamp/Serializer.php b/pkg/wamp/Serializer.php new file mode 100644 index 000000000..414fcf414 --- /dev/null +++ b/pkg/wamp/Serializer.php @@ -0,0 +1,12 @@ +serializer = $serializer; + } + + /** + * @return Serializer + */ + public function getSerializer() + { + return $this->serializer; + } +} diff --git a/pkg/wamp/Tests/Functional/WampConsumerTest.php b/pkg/wamp/Tests/Functional/WampConsumerTest.php new file mode 100644 index 000000000..bb2dd89a4 --- /dev/null +++ b/pkg/wamp/Tests/Functional/WampConsumerTest.php @@ -0,0 +1,69 @@ +buildWampContext(); + $topic = $context->createTopic('topic'); + $consumer = $context->createConsumer($topic); + $producer = $context->createProducer(); + $message = $context->createMessage('the body'); + + // init client + $consumer->receive(1); + + $consumer->getClient()->getLoop()->futureTick(function () use ($producer, $topic, $message) { + $producer->send($topic, $message); + }); + + $receivedMessage = $consumer->receive(100); + + $this->assertInstanceOf(WampMessage::class, $receivedMessage); + $this->assertSame('the body', $receivedMessage->getBody()); + } + + public function testShouldSendAndReceiveNoWaitMessage() + { + $context = $this->buildWampContext(); + $topic = $context->createTopic('topic'); + $consumer = $context->createConsumer($topic); + $producer = $context->createProducer(); + $message = $context->createMessage('the body'); + + // init client + $consumer->receive(1); + + $consumer->getClient()->getLoop()->futureTick(function () use ($producer, $topic, $message) { + $producer->send($topic, $message); + }); + + $receivedMessage = $consumer->receiveNoWait(); + + $this->assertInstanceOf(WampMessage::class, $receivedMessage); + $this->assertSame('the body', $receivedMessage->getBody()); + } +} diff --git a/pkg/wamp/Tests/Functional/WampSubscriptionConsumerTest.php b/pkg/wamp/Tests/Functional/WampSubscriptionConsumerTest.php new file mode 100644 index 000000000..e272f42ed --- /dev/null +++ b/pkg/wamp/Tests/Functional/WampSubscriptionConsumerTest.php @@ -0,0 +1,51 @@ +buildWampContext(); + $topic = $context->createTopic('topic'); + $consumer = $context->createSubscriptionConsumer(); + $producer = $context->createProducer(); + $message = $context->createMessage('the body'); + + $receivedMessage = null; + $consumer->subscribe($context->createConsumer($topic), function ($message) use (&$receivedMessage) { + $receivedMessage = $message; + + return false; + }); + + // init client + $consumer->consume(1); + + $consumer->getClient()->getLoop()->futureTick(function () use ($producer, $topic, $message) { + $producer->send($topic, $message); + }); + + $consumer->consume(100); + + $this->assertInstanceOf(WampMessage::class, $receivedMessage); + $this->assertSame('the body', $receivedMessage->getBody()); + } +} diff --git a/pkg/wamp/Tests/Spec/JsonSerializerTest.php b/pkg/wamp/Tests/Spec/JsonSerializerTest.php new file mode 100644 index 000000000..f062a7058 --- /dev/null +++ b/pkg/wamp/Tests/Spec/JsonSerializerTest.php @@ -0,0 +1,71 @@ +assertClassImplements(Serializer::class, JsonSerializer::class); + } + + public function testShouldConvertMessageToJsonString() + { + $serializer = new JsonSerializer(); + + $message = new WampMessage('theBody', ['aProp' => 'aPropVal'], ['aHeader' => 'aHeaderVal']); + + $json = $serializer->toString($message); + + $this->assertSame('{"body":"theBody","properties":{"aProp":"aPropVal"},"headers":{"aHeader":"aHeaderVal"}}', $json); + } + + public function testThrowIfFailedToEncodeMessageToJson() + { + $serializer = new JsonSerializer(); + + $resource = fopen(__FILE__, 'r'); + + // guard + $this->assertIsResource($resource); + + $message = new WampMessage('theBody', ['aProp' => $resource]); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The malformed json given.'); + $serializer->toString($message); + } + + public function testShouldConvertJsonStringToMessage() + { + $serializer = new JsonSerializer(); + + $message = $serializer->toMessage('{"body":"theBody","properties":{"aProp":"aPropVal"},"headers":{"aHeader":"aHeaderVal"}}'); + + $this->assertInstanceOf(WampMessage::class, $message); + + $this->assertSame('theBody', $message->getBody()); + $this->assertSame(['aProp' => 'aPropVal'], $message->getProperties()); + $this->assertSame(['aHeader' => 'aHeaderVal'], $message->getHeaders()); + } + + public function testThrowIfFailedToDecodeJsonToMessage() + { + $serializer = new JsonSerializer(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The malformed json given.'); + $serializer->toMessage('{]'); + } +} diff --git a/pkg/wamp/Tests/Spec/WampConnectionFactoryTest.php b/pkg/wamp/Tests/Spec/WampConnectionFactoryTest.php new file mode 100644 index 000000000..5b6e418c9 --- /dev/null +++ b/pkg/wamp/Tests/Spec/WampConnectionFactoryTest.php @@ -0,0 +1,17 @@ +buildWampContext(); + } +} diff --git a/pkg/wamp/Tests/Spec/WampMessageTest.php b/pkg/wamp/Tests/Spec/WampMessageTest.php new file mode 100644 index 000000000..3e030d8c1 --- /dev/null +++ b/pkg/wamp/Tests/Spec/WampMessageTest.php @@ -0,0 +1,17 @@ +buildWampContext()->createProducer(); + } +} diff --git a/pkg/wamp/Tests/Spec/WampQueueTest.php b/pkg/wamp/Tests/Spec/WampQueueTest.php new file mode 100644 index 000000000..015536fd1 --- /dev/null +++ b/pkg/wamp/Tests/Spec/WampQueueTest.php @@ -0,0 +1,17 @@ + 'wamp://127.0.0.1:9090', + * 'host' => '127.0.0.1', + * 'port' => '9090', + * 'max_retries' => 15, + * 'initial_retry_delay' => 1.5, + * 'max_retry_delay' => 300, + * 'retry_delay_growth' => 1.5, + * ] + * + * or + * + * wamp://127.0.0.1:9090?max_retries=10 + * + * @param array|string|null $config + */ + public function __construct($config = 'wamp:') + { + if (empty($config)) { + $config = $this->parseDsn('wamp:'); + } elseif (is_string($config)) { + $config = $this->parseDsn($config); + } elseif (is_array($config)) { + $config = empty($config['dsn']) ? $config : $this->parseDsn($config['dsn']); + } else { + throw new \LogicException('The config must be either an array of options, a DSN string or null'); + } + + $config = array_replace([ + 'host' => '127.0.0.1', + 'port' => '9090', + 'max_retries' => 15, + 'initial_retry_delay' => 1.5, + 'max_retry_delay' => 300, + 'retry_delay_growth' => 1.5, + ], $config); + + $this->config = $config; + } + + public function createContext(): Context + { + return new WampContext(function () { + return $this->establishConnection(); + }); + } + + private function establishConnection(): Client + { + $uri = sprintf('ws://%s:%s', $this->config['host'], $this->config['port']); + + $client = new Client('realm1'); + $client->addTransportProvider(new PawlTransportProvider($uri)); + $client->setReconnectOptions([ + 'max_retries' => $this->config['max_retries'], + 'initial_retry_delay' => $this->config['initial_retry_delay'], + 'max_retry_delay' => $this->config['max_retry_delay'], + 'retry_delay_growth' => $this->config['retry_delay_growth'], + ]); + + return $client; + } + + private function parseDsn(string $dsn): array + { + $dsn = Dsn::parseFirst($dsn); + + if (false === in_array($dsn->getSchemeProtocol(), ['wamp', 'ws'], true)) { + throw new \LogicException(sprintf('The given scheme protocol "%s" is not supported. It must be "wamp"', $dsn->getSchemeProtocol())); + } + + return array_filter(array_replace($dsn->getQuery(), [ + 'host' => $dsn->getHost(), + 'port' => $dsn->getPort(), + 'max_retries' => $dsn->getDecimal('max_retries'), + 'initial_retry_delay' => $dsn->getFloat('initial_retry_delay'), + 'max_retry_delay' => $dsn->getDecimal('max_retry_delay'), + 'retry_delay_growth' => $dsn->getFloat('retry_delay_growth'), + ]), function ($value) { return null !== $value; }); + } +} diff --git a/pkg/wamp/WampConsumer.php b/pkg/wamp/WampConsumer.php new file mode 100644 index 000000000..8a6733e36 --- /dev/null +++ b/pkg/wamp/WampConsumer.php @@ -0,0 +1,135 @@ +context = $context; + $this->queue = $destination; + } + + public function getQueue(): Queue + { + return $this->queue; + } + + public function getClient(): ?Client + { + return $this->client; + } + + public function receive(int $timeout = 0): ?Message + { + $init = false; + $this->timer = null; + $this->message = null; + + if (null === $this->client) { + $init = true; + + $this->client = $this->context->getNewClient(); + $this->client->setAttemptRetry(true); + $this->client->on('open', function (ClientSession $session) { + $session->subscribe($this->queue->getQueueName(), function ($args) { + $this->message = $this->context->getSerializer()->toMessage($args[0]); + + $this->client->emit('do-stop'); + }); + }); + + $this->client->on('do-stop', function () { + if ($this->timer) { + $this->client->getLoop()->cancelTimer($this->timer); + } + + $this->client->getLoop()->stop(); + }); + } + + if ($timeout > 0) { + $timeout /= 1000; + $timeout = $timeout >= 0.1 ? $timeout : 0.1; + + $this->timer = $this->client->getLoop()->addTimer($timeout, function () { + $this->client->emit('do-stop'); + }); + } + + if ($init) { + $this->client->start(false); + } + + $this->client->getLoop()->run(); + + $message = $this->message; + + $this->timer = null; + $this->message = null; + + return $message; + } + + public function receiveNoWait(): ?Message + { + return $this->receive(100); + } + + /** + * @param WampMessage $message + */ + public function acknowledge(Message $message): void + { + // do nothing. wamp transport always works in auto ack mode + } + + /** + * @param WampMessage $message + */ + public function reject(Message $message, bool $requeue = false): void + { + InvalidMessageException::assertMessageInstanceOf($message, WampMessage::class); + + // do nothing on reject. wamp transport always works in auto ack mode + + if ($requeue) { + $this->context->createProducer()->send($this->queue, $message); + } + } +} diff --git a/pkg/wamp/WampContext.php b/pkg/wamp/WampContext.php new file mode 100644 index 000000000..623aa33f9 --- /dev/null +++ b/pkg/wamp/WampContext.php @@ -0,0 +1,107 @@ +clientFactory = $clientFactory; + + $this->setSerializer(new JsonSerializer()); + } + + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message + { + return new WampMessage($body, $properties, $headers); + } + + public function createTopic(string $topicName): Topic + { + return new WampDestination($topicName); + } + + public function createQueue(string $queueName): Queue + { + return new WampDestination($queueName); + } + + public function createTemporaryQueue(): Queue + { + throw TemporaryQueueNotSupportedException::providerDoestNotSupportIt(); + } + + public function createProducer(): Producer + { + return new WampProducer($this); + } + + public function createConsumer(Destination $destination): Consumer + { + InvalidDestinationException::assertDestinationInstanceOf($destination, WampDestination::class); + + return new WampConsumer($this, $destination); + } + + public function createSubscriptionConsumer(): SubscriptionConsumer + { + return new WampSubscriptionConsumer($this); + } + + public function purgeQueue(Queue $queue): void + { + throw PurgeQueueNotSupportedException::providerDoestNotSupportIt(); + } + + public function close(): void + { + foreach ($this->clients as $client) { + if (null === $client->getSession()) { + return; + } + + $client->setAttemptRetry(false); + $client->getSession()->close(); + } + } + + public function getNewClient(): Client + { + $client = call_user_func($this->clientFactory); + + if (false == $client instanceof Client) { + throw new \LogicException(sprintf('The factory must return instance of "%s". But it returns %s', Client::class, is_object($client) ? $client::class : gettype($client))); + } + + $this->clients[] = $client; + + return $client; + } +} diff --git a/pkg/wamp/WampDestination.php b/pkg/wamp/WampDestination.php new file mode 100644 index 000000000..a99bc1fa0 --- /dev/null +++ b/pkg/wamp/WampDestination.php @@ -0,0 +1,31 @@ +name = $name; + } + + public function getQueueName(): string + { + return $this->name; + } + + public function getTopicName(): string + { + return $this->name; + } +} diff --git a/pkg/wamp/WampMessage.php b/pkg/wamp/WampMessage.php new file mode 100644 index 000000000..9ad41f5a4 --- /dev/null +++ b/pkg/wamp/WampMessage.php @@ -0,0 +1,21 @@ +body = $body; + $this->properties = $properties; + $this->headers = $headers; + $this->redelivered = false; + } +} diff --git a/pkg/wamp/WampProducer.php b/pkg/wamp/WampProducer.php new file mode 100644 index 000000000..71ea625ae --- /dev/null +++ b/pkg/wamp/WampProducer.php @@ -0,0 +1,182 @@ +context = $context; + } + + /** + * @param WampDestination $destination + * @param WampMessage $message + */ + public function send(Destination $destination, Message $message): void + { + InvalidDestinationException::assertDestinationInstanceOf($destination, WampDestination::class); + InvalidMessageException::assertMessageInstanceOf($message, WampMessage::class); + + $init = false; + $this->message = $message; + $this->destination = $destination; + + if (null === $this->client) { + $init = true; + + $this->client = $this->context->getNewClient(); + $this->client->setAttemptRetry(true); + $this->client->on('open', function (ClientSession $session) { + $this->session = $session; + + $this->doSendMessageIfPossible(); + }); + + $this->client->on('close', function () { + if ($this->session === $this->client->getSession()) { + $this->session = null; + } + }); + + $this->client->on('error', function () { + if ($this->session === $this->client->getSession()) { + $this->session = null; + } + }); + + $this->client->on('do-send', function (WampDestination $destination, WampMessage $message) { + $onFinish = function () { + $this->client->emit('do-stop'); + }; + + $payload = $this->context->getSerializer()->toString($message); + + $this->session->publish($destination->getTopicName(), [$payload], [], ['acknowledge' => true]) + ->then($onFinish, $onFinish); + }); + + $this->client->on('do-stop', function () { + $this->client->getLoop()->stop(); + }); + } + + $this->client->getLoop()->futureTick(function () { + $this->doSendMessageIfPossible(); + }); + + if ($init) { + $this->client->start(false); + } + + $this->client->getLoop()->run(); + } + + /** + * @return WampProducer + */ + public function setDeliveryDelay(?int $deliveryDelay = null): Producer + { + if (null === $deliveryDelay) { + return $this; + } + + throw DeliveryDelayNotSupportedException::providerDoestNotSupportIt(); + } + + public function getDeliveryDelay(): ?int + { + return null; + } + + /** + * @return WampProducer + */ + public function setPriority(?int $priority = null): Producer + { + if (null === $priority) { + return $this; + } + + throw PriorityNotSupportedException::providerDoestNotSupportIt(); + } + + public function getPriority(): ?int + { + return null; + } + + /** + * @return WampProducer + */ + public function setTimeToLive(?int $timeToLive = null): Producer + { + if (null === $timeToLive) { + return $this; + } + + throw TimeToLiveNotSupportedException::providerDoestNotSupportIt(); + } + + public function getTimeToLive(): ?int + { + return null; + } + + private function doSendMessageIfPossible() + { + if (null === $this->session) { + return; + } + + if (null === $this->message) { + return; + } + + $message = $this->message; + $destination = $this->destination; + + $this->message = null; + $this->destination = null; + + $this->client->emit('do-send', [$destination, $message]); + } +} diff --git a/pkg/wamp/WampSubscriptionConsumer.php b/pkg/wamp/WampSubscriptionConsumer.php new file mode 100644 index 000000000..2d25a673b --- /dev/null +++ b/pkg/wamp/WampSubscriptionConsumer.php @@ -0,0 +1,161 @@ +context = $context; + $this->subscribers = []; + } + + public function getClient(): ?Client + { + return $this->client; + } + + public function consume(int $timeout = 0): void + { + if (empty($this->subscribers)) { + throw new \LogicException('There is no subscribers. Consider calling basicConsumeSubscribe before consuming'); + } + + $init = false; + + if (null === $this->client) { + $init = true; + + $this->client = $this->context->getNewClient(); + $this->client->setAttemptRetry(true); + $this->client->on('open', function (ClientSession $session) { + foreach ($this->subscribers as $queue => $subscriber) { + $session->subscribe($queue, function ($args) use ($subscriber) { + $message = $this->context->getSerializer()->toMessage($args[0]); + + /** @var WampConsumer $consumer */ + /** @var callable $callback */ + list($consumer, $callback) = $subscriber; + + if (false === call_user_func($callback, $message, $consumer)) { + $this->client->emit('do-stop'); + } + }); + } + }); + + $this->client->on('do-stop', function () { + if ($this->timer) { + $this->client->getLoop()->cancelTimer($this->timer); + } + + $this->client->getLoop()->stop(); + }); + } + + if ($timeout > 0) { + $timeout /= 1000; + $timeout = $timeout >= 0.1 ? $timeout : 0.1; + + $this->timer = $this->client->getLoop()->addTimer($timeout, function () { + $this->client->emit('do-stop'); + }); + } + + if ($init) { + $this->client->start(false); + } + + $this->client->getLoop()->run(); + } + + /** + * @param WampConsumer $consumer + */ + public function subscribe(Consumer $consumer, callable $callback): void + { + if (false == $consumer instanceof WampConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', WampConsumer::class, $consumer::class)); + } + + if ($this->client) { + throw new \LogicException('Could not subscribe after consume was called'); + } + + $queueName = $consumer->getQueue()->getQueueName(); + if (array_key_exists($queueName, $this->subscribers)) { + if ($this->subscribers[$queueName][0] === $consumer && $this->subscribers[$queueName][1] === $callback) { + return; + } + + throw new \InvalidArgumentException(sprintf('There is a consumer subscribed to queue: "%s"', $queueName)); + } + + $this->subscribers[$queueName] = [$consumer, $callback]; + } + + /** + * @param WampConsumer $consumer + */ + public function unsubscribe(Consumer $consumer): void + { + if (false == $consumer instanceof WampConsumer) { + throw new \InvalidArgumentException(sprintf('The consumer must be instance of "%s" got "%s"', WampConsumer::class, $consumer::class)); + } + + if ($this->client) { + throw new \LogicException('Could not unsubscribe after consume was called'); + } + + $queueName = $consumer->getQueue()->getQueueName(); + + if (false == array_key_exists($queueName, $this->subscribers)) { + return; + } + + if ($this->subscribers[$queueName][0] !== $consumer) { + return; + } + + unset($this->subscribers[$queueName]); + } + + public function unsubscribeAll(): void + { + if ($this->client) { + throw new \LogicException('Could not unsubscribe after consume was called'); + } + + $this->subscribers = []; + } +} diff --git a/pkg/wamp/composer.json b/pkg/wamp/composer.json new file mode 100644 index 000000000..b510627bd --- /dev/null +++ b/pkg/wamp/composer.json @@ -0,0 +1,47 @@ +{ + "name": "enqueue/wamp", + "type": "library", + "description": "The Web Application Messaging Protocol Transport", + "keywords": ["messaging", "queue", "wamp", "thruway"], + "homepage": "https://enqueue.forma-pro.com/", + "license": "MIT", + "require": { + "php": "^8.1", + "queue-interop/queue-interop": "^0.8.1", + "enqueue/dsn": "^0.10.8", + "thruway/client": "^0.5.5", + "thruway/pawl-transport": "^0.5.1", + "voryx/thruway-common": "^1.0.1", + "react/dns": "^1.4", + "react/event-loop": "^1.2", + "react/promise": "^2.8" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "enqueue/test": "0.10.x-dev", + "enqueue/null": "0.10.x-dev", + "queue-interop/queue-spec": "^0.6.2" + }, + "support": { + "email": "opensource@forma-pro.com", + "issues": "https://github.com/php-enqueue/enqueue-dev/issues", + "forum": "https://gitter.im/php-enqueue/Lobby", + "source": "https://github.com/php-enqueue/enqueue-dev", + "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" + }, + "autoload": { + "psr-4": { "Enqueue\\Wamp\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "beta", + "extra": { + "branch-alias": { + "dev-master": "0.10.x-dev" + } + }, + "config": { + "prefer-stable": true + } +} diff --git a/pkg/wamp/phpunit.xml.dist b/pkg/wamp/phpunit.xml.dist new file mode 100644 index 000000000..9e8558ce8 --- /dev/null +++ b/pkg/wamp/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + + ./Tests + + + + + + . + + ./vendor + ./Tests + + + +