From 5b02124f7f3ced008074e32acd2b161a4bd84033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juli=C3=A1n=20Guti=C3=A9rrez?= Date: Tue, 27 Nov 2018 23:23:47 +0100 Subject: [PATCH] event bus dispatcher --- .coveralls.yml | 1 + .editorconfig | 27 ++++ .gitattributes | 14 ++ .gitignore | 7 + .php_cs | 141 ++++++++++++++++++ .scrutinizer.yml | 84 +++++++++++ .styleci.yml | 90 +++++++++++ .travis.yml | 46 ++++++ CONTRIBUTING.md | 22 +++ LICENSE | 21 +++ README.md | 61 ++++++++ composer.json | 108 ++++++++++++++ infection.json.dist | 11 ++ phpstan.neon | 11 ++ phpunit.xml.dist | 30 ++++ src/ContainerAwareEventDispatcher.php | 136 +++++++++++++++++ src/EventBus.php | 45 ++++++ src/EventDispatcher.php | 117 +++++++++++++++ src/EventEnvelope.php | 45 ++++++ .../ContainerAwareEventDispatcherTest.php | 127 ++++++++++++++++ tests/Symfony/EventBusTest.php | 37 +++++ tests/Symfony/EventDispatcherTest.php | 84 +++++++++++ tests/Symfony/EventEnvelopeTest.php | 33 ++++ tests/Symfony/Stub/EventStub.php | 32 ++++ .../Stub/EventSubscriberInterfaceStub.php | 45 ++++++ tests/bootstrap.php | 14 ++ 26 files changed, 1389 insertions(+) create mode 100644 .coveralls.yml create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .scrutinizer.yml create mode 100644 .styleci.yml create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 infection.json.dist create mode 100644 phpstan.neon create mode 100644 phpunit.xml.dist create mode 100644 src/ContainerAwareEventDispatcher.php create mode 100644 src/EventBus.php create mode 100644 src/EventDispatcher.php create mode 100644 src/EventEnvelope.php create mode 100644 tests/Symfony/ContainerAwareEventDispatcherTest.php create mode 100644 tests/Symfony/EventBusTest.php create mode 100644 tests/Symfony/EventDispatcherTest.php create mode 100644 tests/Symfony/EventEnvelopeTest.php create mode 100644 tests/Symfony/Stub/EventStub.php create mode 100644 tests/Symfony/Stub/EventSubscriberInterfaceStub.php create mode 100644 tests/bootstrap.php diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..9160059 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +service_name: travis-ci diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c21d79c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.js] +indent_size = 2 + +[*.html] +indent_size = 2 + +[*.json] +indent_size = 2 + +[*.neon] +indent_size = 2 + +[*.yml] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..256c226 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +* text=auto + +.gitattributes export-ignore +.gitignore export-ignore +.editorconfig export-ignore +.php_cs export-ignore +.travis.yml export-ignore +.coveralls.yml export-ignore +.scrutinizer.yml export-ignore +.styleci.yml export-ignore +infection.json.dist export-ignore +README.md export-ignore +CONTRIBUTING.md export-ignore +phpunit.xml.dist export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3cef788 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +!.gitignore +composer.lock +cghooks.lock +vendor/ +*.cache +infection-log.* +build/ diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..32986e1 --- /dev/null +++ b/.php_cs @@ -0,0 +1,141 @@ + + */ + +use PhpCsFixer\Config; +use PhpCsFixer\Finder; + +$header = <<<'HEADER' +event-symfony-event-dispatcher (https://github.com/phpgears/event-symfony-event-dispatcher). +Event bus with Symfony Event Dispatcher. + +@license MIT +@link https://github.com/phpgears/event-symfony-event-dispatcher +@author Julián Gutiérrez +HEADER; + +$finder = Finder::create() + ->exclude(['vendor', 'build']) + ->in(__DIR__); + +return Config::create() + ->setUsingCache(true) + ->setRiskyAllowed(true) + ->setRules([ + '@PSR2' => true, + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'binary_operator_spaces' => true, + 'blank_line_after_opening_tag' => true, + 'cast_spaces' => true, + 'class_attributes_separation' => true, + 'combine_consecutive_unsets' => true, + 'compact_nullable_typehint' => true, + 'concat_space' => [ + 'spacing' => 'one' + ], + 'declare_equal_normalize' => true, + 'declare_strict_types' => true, + 'dir_constant' => true, + 'function_typehint_space' => true, + 'hash_to_slash_comment' => true, + 'header_comment' => [ + 'header' => $header, + 'location' => 'after_open', + ], + 'heredoc_to_nowdoc' => true, + 'include' => true, + 'linebreak_after_opening_tag' => true, + 'list_syntax' => [ + 'syntax' => 'short', + ], + 'lowercase_cast' => true, + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'modernize_types_casting' => true, + 'native_constant_invocation' => true, + 'native_function_casing' => true, + 'native_function_invocation' => true, + 'new_with_braces' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_multiline_whitespace_before_semicolons' => true, + 'no_php4_constructor' => true, + 'no_short_bool_cast' => true, + 'no_short_echo_tag' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_around_offset' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unreachable_default_argument_value' => true, + 'no_unneeded_final_method' => true, + 'no_unset_on_property' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'normalize_index_brace' => true, + 'ordered_imports' => true, + 'php_unit_construct' => true, + 'php_unit_dedicate_assert' => true, + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_align' => true, + 'phpdoc_annotation_without_dot' => true, + 'phpdoc_indent' => true, + 'phpdoc_inline_tag' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_order' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'pow_to_exponentiation' => true, + 'random_api_migration' => true, + 'return_type_declaration' => [ + 'space_before' => 'none', + ], + 'self_accessor' => true, + 'set_type_to_cast' => true, + 'short_scalar_cast' => true, + 'single_blank_line_before_namespace' => true, + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_increment' => true, + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline_array' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'void_return' => true, + 'whitespace_after_comma_in_array' => true, + 'yoda_style' => false, + ]) + ->setFinder($finder); diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..9a9bb96 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,84 @@ +# language: php + +filter: + paths: [src/*] + excluded_paths: [tests/*, vendor/*] + +before_commands: + - 'composer self-update' + - 'composer update --prefer-stable --prefer-source --no-interaction --no-scripts --no-progress --no-suggest' + +coding_style: + php: + upper_lower_casing: + keywords: + general: lower + constants: + true_false_null: lower + spaces: + around_operators: + concatenation: true + negation: false + other: + after_type_cast: true + +tools: + php_code_coverage: false + php_code_sniffer: + enabled: true + config: + standard: 'PSR2' + filter: + paths: [src/*, tests/*] + php_mess_detector: + enabled: true + config: + ruleset: 'unusedcode,naming,design,controversial,codesize' + + php_cpd: true + php_loc: true + php_pdepend: true + php_analyzer: true + sensiolabs_security_checker: true + +checks: + php: + code_rating: true + duplication: true + uppercase_constants: true + properties_in_camelcaps: true + prefer_while_loop_over_for_loop: true + parameters_in_camelcaps: true + optional_parameters_at_the_end: true + no_short_variable_names: + minimum: '3' + no_short_method_names: + minimum: '3' + no_goto: true + newline_at_end_of_file: true + more_specific_types_in_doc_comments: true + line_length: + max_length: '120' + function_in_camel_caps: true + encourage_single_quotes: true + encourage_postdec_operator: true + classes_in_camel_caps: true + avoid_perl_style_comments: true + avoid_multiple_statements_on_same_line: true + parameter_doc_comments: true + use_self_instead_of_fqcn: true + simplify_boolean_return: true + avoid_fixme_comments: true + return_doc_comments: true + remove_extra_empty_lines: true + remove_php_closing_tag: true + remove_trailing_whitespace: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: true + order_alphabetically: true + fix_php_opening_tag: true + fix_linefeed: true + fix_line_ending: true + fix_identation_4spaces: true diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..e643fbb --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,90 @@ +preset: psr2 + +finder: + exclude: + - vendor + - build + +enabled: + - alpha_ordered_imports + - binary_operator_spaces + - blank_line_after_opening_tag + - cast_spaces + - combine_consecutive_unsets + - compact_nullable_typehint + - concat_with_spaces + - declare_equal_normalize + - declare_strict_types + - function_typehint_space + - hash_to_slash_comment + - heredoc_to_nowdoc + - include + - linebreak_after_opening_tag + - lowercase_cast + - method_separation + - modernize_types_casting + - native_function_casing + - native_function_invocation + - new_with_braces + - no_alias_functions + - no_blank_lines_after_class_opening + - no_blank_lines_after_phpdoc + - no_empty_comment + - no_empty_phpdoc + - no_empty_statement + - no_leading_import_slash + - no_leading_namespace_whitespace + - no_multiline_whitespace_around_double_arrow + - no_multiline_whitespace_before_semicolons + - no_php4_constructor + - no_short_bool_cast + - no_short_echo_tag + - no_singleline_whitespace_before_semicolons + - no_spaces_inside_offset + - no_spaces_outside_offset + - no_trailing_comma_in_list_call + - no_trailing_comma_in_singleline_array + - no_unneeded_control_parentheses + - no_unreachable_default_argument_value + - no_unused_imports + - no_useless_else + - no_useless_return + - no_whitespace_before_comma_in_array + - no_whitespace_in_blank_line + - normalize_index_brace + - php_unit_construct + - php_unit_dedicate_assert + - phpdoc_add_missing_param_annotation + - phpdoc_align + - phpdoc_annotation_without_dot + - phpdoc_indent + - phpdoc_inline_tag + - phpdoc_no_access + - phpdoc_no_empty_return + - phpdoc_no_package + - phpdoc_no_useless_inheritdoc + - phpdoc_order + - phpdoc_scalar + - phpdoc_separation + - phpdoc_single_line_var_spacing + - phpdoc_summary + - phpdoc_to_comment + - phpdoc_trim + - phpdoc_types + - phpdoc_var_without_name + - pow_to_exponentiation + - random_api_migration + - return_type_declaration + - self_accessor + - short_array_syntax + - short_scalar_cast + - single_blank_line_before_namespace + - single_quote + - space_after_semicolon + - standardize_not_equals + - ternary_operator_spaces + - ternary_to_null_coalescing + - trailing_comma_in_multiline_array + - trim_array_spaces + - unary_operator_spaces + - whitespace_after_comma_in_array diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..10268ce --- /dev/null +++ b/.travis.yml @@ -0,0 +1,46 @@ +language: php + +sudo: false + +git: + depth: 3 + +cache: + directories: + - $HOME/.composer/cache/files + +env: + - COMPOSER_FLAGS="--prefer-stable --prefer-dist" + +php: + - 7.2 + - nightly + +matrix: + fast_finish: true + include: + - php: 7.1 + env: + - COMPOSER_FLAGS="--prefer-lowest --prefer-stable --prefer-dist" + - php: 7.1 + env: + - TEST_VERSION=true + - COMPOSER_FLAGS="--prefer-stable --prefer-dist" + allow_failures: + - php: nightly + +before_install: + - if [[ -z $TEST_VERSION && -f "/home/travis/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini" ]]; then phpenv config-rm xdebug.ini; fi + - composer global require hirak/prestissimo + - composer self-update --stable --no-progress + +install: + - travis_retry composer update $COMPOSER_FLAGS --no-interaction --no-scripts --no-progress + - if [[ $TEST_VERSION ]]; then travis_retry composer require php-coveralls/php-coveralls $COMPOSER_FLAGS --no-interaction --no-scripts --no-progress ; fi + +script: + - if [[ $TEST_VERSION ]]; then composer qa && composer report-phpunit-clover ; fi + - if [[ -z $TEST_VERSION ]]; then composer test-phpunit ; fi + +after_script: + - if [[ $TEST_VERSION ]]; then travis_retry php vendor/bin/php-coveralls --verbose ; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..145c542 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing + +First of all **thank you** for contributing! + +Make your contributions through Pull Requests + +Find here a few rules to follow in order to keep the code clean and easy to review and merge: + +- Follow **[PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** coding standard +- **Unit test everything** and run the test suite +- Try not to bring **code coverage** down +- Keep documentation **updated** +- Just **one pull request per feature** at a time +- Check that **[Travis CI](https://travis-ci.org/phpgears/event-symfony-event-dispatcher)** build passed + +Composer scripts are provided to help you keep code quality and run the test suite: + +- `composer lint` will run PHP linting and [PHP Code Sniffer](https://github.com/squizlabs/PHP_CodeSniffer) and [PHP-CS-Fixer](https://github.com/FriendsOfPhp/PHP-CS-Fixer) for coding style guidelines check +- `composer fix` will run [PHP-CS-Fixer](https://github.com/FriendsOfPhp/PHP-CS-Fixer) trying to fix coding styles +- `composer qa` will run [PHPCPD](https://github.com/sebastianbergmann/phpcpd) for copy/paste detection, [PHPMD](https://github.com/phpmd/phpmd) and [PHPStan](https://github.com/phpstan/phpstan) for static analysis +- `composer security` will run [Composer](https://getcomposer.org) (>=1.1.0) for outdated dependencies +- `composer test` will run [PHPUnit](https://github.com/sebastianbergmann/phpunit) for unit tests and [Infection](https://github.com/infection/infection) for mutation tests diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e0a3dff --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018, Julián Gutiérrez (juliangut@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c177a2 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +[![PHP version](https://img.shields.io/badge/PHP-%3E%3D7.1-8892BF.svg?style=flat-square)](http://php.net) +[![Latest Version](https://img.shields.io/packagist/v/phpgears/event-symfony-event-dispatcher.svg?style=flat-square)](https://packagist.org/packages/phpgears/event-symfony-event-dispatcher) +[![License](https://img.shields.io/github/license/phpgears/event-symfony-event-dispatcher.svg?style=flat-square)](https://github.com/phpgears/event-symfony-event-dispatcher/blob/master/LICENSE) + +[![Build Status](https://img.shields.io/travis/phpgears/event-symfony-event-dispatcher.svg?style=flat-square)](https://travis-ci.org/phpgears/event-symfony-event-dispatcher) +[![Style Check](https://styleci.io/repos/158948865/shield)](https://styleci.io/repos/158948865) +[![Code Quality](https://img.shields.io/scrutinizer/g/phpgears/event-symfony-event-dispatcher.svg?style=flat-square)](https://scrutinizer-ci.com/g/phpgears/event-symfony-event-dispatcher) +[![Code Coverage](https://img.shields.io/coveralls/phpgears/event-symfony-event-dispatcher.svg?style=flat-square)](https://coveralls.io/github/phpgears/event-symfony-event-dispatcher) + +[![Total Downloads](https://img.shields.io/packagist/dt/phpgears/event-symfony-event-dispatcher.svg?style=flat-square)](https://packagist.org/packages/phpgears/event-symfony-event-dispatcher/stats) +[![Monthly Downloads](https://img.shields.io/packagist/dm/phpgears/event-symfony-event-dispatcher.svg?style=flat-square)](https://packagist.org/packages/phpgears/event-symfony-event-dispatcher/stats) + +# Event bus Event Dispatcher + +Event bus implementation with Symfony's Event Dispatcher + +## Installation + +### Composer + +``` +composer require phpgears/event-symfony-event-dispatcher +``` + +## Usage + +Require composer autoload file + +```php +require './vendor/autoload.php'; +``` + +### Events Bus + +```php +use Gears\Event\Symfony\ContainerAwareEventDispatcher; +use Gears\Event\Symfony\EventBus; +use Gears\Event\Symfony\EventDispatcher; + +$eventToHandlerMap = []; + +$symfonyDispatcher = new EventDispatcher($eventToHandlerMap); +// OR +/** @var \Psr\Container\ContainerInterface $container */ +$symfonyDispatcher = new ContainerAwareEventDispatcher($container, $eventToHandlerMap); + +$eventBus = new EventBus($symfonyDispatcher); + +/** @var \Gears\Event\Event $event */ +$eventBus->dispatch($event); +``` + +## Contributing + +Found a bug or have a feature request? [Please open a new issue](https://github.com/phpgears/event-symfony-event-dispatcher/issues). Have a look at existing issues before. + +See file [CONTRIBUTING.md](https://github.com/phpgears/event-symfony-event-dispatcher/blob/master/CONTRIBUTING.md) + +## License + +See file [LICENSE](https://github.com/phpgears/event-symfony-event-dispatcher/blob/master/LICENSE) included with the source code for a copy of the license terms. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..62a67f9 --- /dev/null +++ b/composer.json @@ -0,0 +1,108 @@ +{ + "name": "phpgears/event-symfony-event-dispatcher", + "description": "Event bus implementation with Symfony's Event Dispatcher", + "keywords": [ + "Event", + "bus", + "symfony" + ], + "homepage": "https://github.com/phpgears/event-symfony-event-dispatcher", + "license": "MIT", + "authors": [ + { + "name": "Julián Gutiérrez", + "email": "juliangut@gmail.com", + "homepage": "http://juliangut.com", + "role": "Developer" + } + ], + "support": { + "source": "https://github.com/phpgears/event-symfony-event-dispatcher", + "issues": "https://github.com/phpgears/event-symfony-event-dispatcher/issues" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "php": "^7.1", + "phpgears/event": "~0.1", + "symfony/event-dispatcher": "^4.1" + }, + "require-dev": { + "brainmaestro/composer-git-hooks": "^2.1", + "friendsofphp/php-cs-fixer": "^2.0", + "infection/infection": "^0.9", + "phpmd/phpmd": "^2.0", + "phpstan/phpstan": "^0.10", + "phpstan/phpstan-deprecation-rules": "^0.10", + "phpstan/phpstan-strict-rules": "^0.10", + "phpunit/phpunit": "^6.0|^7.0", + "povils/phpmnd": "^2.0", + "roave/security-advisories": "dev-master", + "sebastian/phpcpd": "^3.0|^4.0", + "squizlabs/php_codesniffer": "^2.0", + "thecodingmachine/phpstan-strict-rules": "^0.10.1" + }, + "suggest": { + }, + "autoload": { + "psr-4": { + "Gears\\Event\\Symfony\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Gears\\Event\\Symfony\\Tests\\": "tests/Symfony/" + } + }, + "bin": [ + ], + "config": { + "preferred-install": "dist", + "sort-packages": true + }, + "scripts": { + "cghooks": "cghooks", + "post-install-cmd": "cghooks add --ignore-lock", + "post-update-cmd": "cghooks update", + "lint-php": "php -l src && php -l tests", + "lint-phpcs": "phpcs --standard=PSR2 src tests", + "lint-phpcs-fixer": "php-cs-fixer fix --config=.php_cs --dry-run --verbose", + "fix-phpcs": "php-cs-fixer fix --config=.php_cs --verbose", + "qa-phpcpd": "phpcpd src", + "qa-phpmd": "phpmd src text unusedcode,naming,design,controversial,codesize", + "qa-phpmnd": "phpmnd ./ --exclude=tests", + "qa-phpstan": "phpstan analyse --configuration=phpstan.neon --memory-limit=2G --no-progress", + "test-phpunit": "phpunit", + "test-infection": "infection", + "report-phpunit-coverage": "phpunit --coverage-html build/coverage", + "report-phpunit-clover": "phpunit --coverage-clover build/logs/clover.xml", + "lint": [ + "@lint-php", + "@lint-phpcs", + "@lint-phpcs-fixer" + ], + "fix": [ + "@fix-phpcs" + ], + "qa": [ + "@qa-phpcpd", + "@qa-phpmd", + "@qa-phpmnd", + "@qa-phpstan" + ], + "security": "composer outdated", + "test": [ + "@test-phpunit", + "@test-infection" + ], + "report": [ + "@report-phpunit-coverage", + "@report-phpunit-clover" + ] + }, + "extra": { + "hooks": { + "pre-commit": "composer lint && composer qa && composer test-phpunit" + } + } +} diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 0000000..1ad8ead --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,11 @@ +{ + "source": { + "directories": [ + "src" + ] + }, + "timeout": 10, + "logs": { + "text": "infection-log.txt" + } +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..245a4df --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,11 @@ +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon + - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon + +parameters: + level: max + paths: + - src + ignoreErrors: + - '/^Parameter #2 \$listener of method .+\\EventDispatcher::addListener\(\) expects callable, .+ given.$/' diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..9e1b362 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + tests/Symfony/ + + + + + + src/ + + + + + + + diff --git a/src/ContainerAwareEventDispatcher.php b/src/ContainerAwareEventDispatcher.php new file mode 100644 index 0000000..8161085 --- /dev/null +++ b/src/ContainerAwareEventDispatcher.php @@ -0,0 +1,136 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony; + +use Gears\Event\EventHandler; +use Psr\Container\ContainerInterface; +use Symfony\Component\EventDispatcher\Event as SymfonyEvent; +use Symfony\Component\EventDispatcher\EventDispatcher as SymfonyEventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class ContainerAwareEventDispatcher extends SymfonyEventDispatcher +{ + /** + * @var ContainerInterface + */ + private $container; + + /** + * ContainerAwareEventDispatcher constructor. + * + * @param ContainerInterface $container + * @param array $listenersMap + */ + public function __construct(ContainerInterface $container, array $listenersMap = []) + { + $this->container = $container; + + foreach ($listenersMap as $eventName => $listeners) { + if (!\is_array($listeners)) { + $listeners = [$listeners]; + } + + foreach ($listeners as $listener) { + $this->addListener($eventName, $listener); + } + } + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber): void + { + foreach ($subscriber::getSubscribedEvents() as $eventName => $params) { + if (!\is_array($params)) { + $params = [$params]; + } + + foreach ($params as $listener) { + if (\is_string($listener)) { + $this->addListener($eventName, $listener); + } else { + $this->addListener($eventName, $listener[0], $listener[1] ?? 0); + } + } + } + } + + /** + * Adds an event listener that listens on the specified events. + * + * @param string $eventName + * @param mixed $listener + * @param mixed $priority + */ + public function addListener($eventName, $listener, $priority = 0): void + { + if (!\is_string($listener)) { + throw new \InvalidArgumentException(\sprintf( + 'Event handler must be a container entry, %s given', + \is_object($listener) ? \get_class($listener) : \gettype($listener) + )); + } + + parent::addListener($eventName, $listener, (int) $priority); + } + + /** + * {@inheritdoc} + */ + public function dispatch($eventName, SymfonyEvent $event = null): SymfonyEvent + { + if ($event === null) { + throw new \InvalidArgumentException('Dispatched event cannot be empty'); + } + + if (!$event instanceof EventEnvelope) { + throw new \InvalidArgumentException(\sprintf( + 'Dispatched event must implement %s, %s given', + EventEnvelope::class, + \get_class($event) + )); + } + + $this->dispatchEvent($this->getListeners($eventName), $event); + + return $event; + } + + /** + * Dispatch event to registered listeners. + * + * @param string[] $listeners + * @param EventEnvelope $event + */ + private function dispatchEvent(array $listeners, EventEnvelope $event): void + { + $dispatchEvent = $event->getWrappedEvent(); + + foreach ($listeners as $listener) { + /* @var EventHandler $handler */ + $handler = $this->container->get($listener); + + if (!$handler instanceof EventHandler) { + throw new \RuntimeException(\sprintf( + 'Event handler should implement %s, %s given', + EventHandler::class, + \is_object($handler) ? \get_class($handler) : \gettype($handler) + )); + } + + $handler->handle($dispatchEvent); + } + } +} diff --git a/src/EventBus.php b/src/EventBus.php new file mode 100644 index 0000000..712ebce --- /dev/null +++ b/src/EventBus.php @@ -0,0 +1,45 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony; + +use Gears\Event\Event; +use Gears\Event\EventBus as EventBusInterface; + +final class EventBus implements EventBusInterface +{ + /** + * Wrapped event dispatcher. + * + * @var ContainerAwareEventDispatcher + */ + private $wrappedDispatcher; + + /** + * EventBus constructor. + * + * @param ContainerAwareEventDispatcher $wrappedDispatcher + */ + public function __construct(ContainerAwareEventDispatcher $wrappedDispatcher) + { + $this->wrappedDispatcher = $wrappedDispatcher; + } + + /** + * {@inheritdoc} + */ + public function dispatch(Event $event): void + { + $this->wrappedDispatcher->dispatch(\get_class($event), new EventEnvelope($event)); + } +} diff --git a/src/EventDispatcher.php b/src/EventDispatcher.php new file mode 100644 index 0000000..a0957e4 --- /dev/null +++ b/src/EventDispatcher.php @@ -0,0 +1,117 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony; + +use Gears\Event\EventHandler; +use Symfony\Component\EventDispatcher\Event as SymfonyEvent; +use Symfony\Component\EventDispatcher\EventDispatcher as SymfonyEventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class EventDispatcher extends SymfonyEventDispatcher +{ + /** + * ContainerAwareEventDispatcher constructor. + * + * @param array $listenersMap + */ + public function __construct(array $listenersMap = []) + { + foreach ($listenersMap as $eventName => $listeners) { + if (!\is_array($listeners)) { + $listeners = [$listeners]; + } + + foreach ($listeners as $listener) { + $this->addListener($eventName, $listener); + } + } + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber): void + { + foreach ($subscriber::getSubscribedEvents() as $eventName => $params) { + if (!\is_array($params)) { + $params = [$params]; + } + + foreach ($params as $listener) { + if (!\is_array($listener)) { + $this->addListener($eventName, $listener); + } else { + $this->addListener($eventName, $listener[0], $listener[1] ?? 0); + } + } + } + } + + /** + * Adds an event listener that listens on the specified events. + * + * @param string $eventName + * @param mixed $listener + * @param mixed $priority + */ + public function addListener($eventName, $listener, $priority = 0): void + { + if (!$listener instanceof EventHandler) { + throw new \InvalidArgumentException(\sprintf( + 'Event handler must be an instance of %s, %s given', + EventHandler::class, + \is_object($listener) ? \get_class($listener) : \gettype($listener) + )); + } + + parent::addListener($eventName, $listener, (int) $priority); + } + + /** + * {@inheritdoc} + */ + public function dispatch($eventName, SymfonyEvent $event = null): SymfonyEvent + { + if ($event === null) { + throw new \InvalidArgumentException('Dispatched event cannot be empty'); + } + + if (!$event instanceof EventEnvelope) { + throw new \InvalidArgumentException(\sprintf( + 'Dispatched event must implement %s, %s given', + EventEnvelope::class, + \get_class($event) + )); + } + + $this->dispatchEvent($this->getListeners($eventName), $event); + + return $event; + } + + /** + * Dispatch event to registered listeners. + * + * @param EventHandler[] $listeners + * @param EventEnvelope $event + */ + private function dispatchEvent(array $listeners, EventEnvelope $event): void + { + $dispatchEvent = $event->getWrappedEvent(); + + foreach ($listeners as $handler) { + $handler->handle($dispatchEvent); + } + } +} diff --git a/src/EventEnvelope.php b/src/EventEnvelope.php new file mode 100644 index 0000000..6704dd6 --- /dev/null +++ b/src/EventEnvelope.php @@ -0,0 +1,45 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony; + +use Gears\Event\Event; +use Symfony\Component\EventDispatcher\Event as SymfonyEvent; + +final class EventEnvelope extends SymfonyEvent +{ + /** + * @var Event + */ + private $wrappedEvent; + + /** + * EventWrapper constructor. + * + * @param Event $event + */ + public function __construct(Event $event) + { + $this->wrappedEvent = $event; + } + + /** + * Get wrapped event. + * + * @return Event + */ + public function getWrappedEvent(): Event + { + return $this->wrappedEvent; + } +} diff --git a/tests/Symfony/ContainerAwareEventDispatcherTest.php b/tests/Symfony/ContainerAwareEventDispatcherTest.php new file mode 100644 index 0000000..db448ab --- /dev/null +++ b/tests/Symfony/ContainerAwareEventDispatcherTest.php @@ -0,0 +1,127 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony\Tests; + +use Gears\Event\EventHandler; +use Gears\Event\Symfony\ContainerAwareEventDispatcher; +use Gears\Event\Symfony\EventEnvelope; +use Gears\Event\Symfony\Tests\Stub\EventStub; +use Gears\Event\Symfony\Tests\Stub\EventSubscriberInterfaceStub; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\EventDispatcher\Event; + +/** + * Symfony event dispatcher wrapper test. + */ +class ContainerAwareEventDispatcherTest extends TestCase +{ + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Event handler must be a container entry, stdClass given + */ + public function testInvalidListener(): void + { + /** @var ContainerInterface $containerMock */ + $containerMock = $this->getMockBuilder(ContainerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + new ContainerAwareEventDispatcher($containerMock, ['eventName' => new \stdClass()]); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Dispatched event cannot be empty + */ + public function testEmptyEvent(): void + { + /** @var ContainerInterface $containerMock */ + $containerMock = $this->getMockBuilder(ContainerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $eventDispatcher = new ContainerAwareEventDispatcher($containerMock); + + $eventDispatcher->dispatch('eventName'); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /^Dispatched event must implement .+\\EventEnvelope, .+ given$/ + */ + public function testInvalidEvent(): void + { + /** @var ContainerInterface $containerMock */ + $containerMock = $this->getMockBuilder(ContainerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $eventDispatcher = new ContainerAwareEventDispatcher($containerMock); + + $eventDispatcher->dispatch('eventName', new Event()); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Event handler should implement Gears\Event\EventHandler, string given + */ + public function testInvalidHandler(): void + { + $containerMock = $this->getMockBuilder(ContainerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $containerMock->expects($this->once()) + ->method('get') + ->with('eventHandler') + ->will($this->returnValue('thisIsNoHandler')); + /** @var ContainerInterface $containerMock */ + $eventDispatcher = new ContainerAwareEventDispatcher($containerMock, ['eventName' => 'eventHandler']); + + $eventDispatcher->dispatch('eventName', new EventEnvelope(EventStub::instance())); + } + + public function testEventDispatch(): void + { + $event = EventStub::instance(); + + $eventHandler = $this->getMockBuilder(EventHandler::class) + ->disableOriginalConstructor() + ->getMock(); + $eventHandler->expects($this->once()) + ->method('handle') + ->with($event); + + $containerMock = $this->getMockBuilder(ContainerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $containerMock->expects($this->once()) + ->method('get') + ->with('eventHandler') + ->will($this->returnValue($eventHandler)); + /** @var ContainerInterface $containerMock */ + $subscriber = new EventSubscriberInterfaceStub([ + 'eventName' => 'eventHandler', + 'otherEvent' => ['eventHandler'], + 'anotherEvent' => [ + ['eventHandler'], + ], + ]); + + $eventDispatcher = new ContainerAwareEventDispatcher($containerMock); + $eventDispatcher->addSubscriber($subscriber); + + $eventDispatcher->dispatch('eventName', new EventEnvelope($event)); + } +} diff --git a/tests/Symfony/EventBusTest.php b/tests/Symfony/EventBusTest.php new file mode 100644 index 0000000..2366c6a --- /dev/null +++ b/tests/Symfony/EventBusTest.php @@ -0,0 +1,37 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony\Tests; + +use Gears\Event\Symfony\ContainerAwareEventDispatcher; +use Gears\Event\Symfony\EventBus; +use Gears\Event\Symfony\Tests\Stub\EventStub; +use PHPUnit\Framework\TestCase; + +/** + * Symfony event bus test. + */ +class EventBusTest extends TestCase +{ + public function testHandling(): void + { + $eventDispatcherMock = $this->getMockBuilder(ContainerAwareEventDispatcher::class) + ->disableOriginalConstructor() + ->getMock(); + $eventDispatcherMock->expects($this->once()) + ->method('dispatch'); + /* @var ContainerAwareEventDispatcher $eventDispatcherMock */ + + (new EventBus($eventDispatcherMock))->dispatch(EventStub::instance()); + } +} diff --git a/tests/Symfony/EventDispatcherTest.php b/tests/Symfony/EventDispatcherTest.php new file mode 100644 index 0000000..e8ba2c4 --- /dev/null +++ b/tests/Symfony/EventDispatcherTest.php @@ -0,0 +1,84 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony\Tests; + +use Gears\Event\EventHandler; +use Gears\Event\Symfony\EventDispatcher; +use Gears\Event\Symfony\EventEnvelope; +use Gears\Event\Symfony\Tests\Stub\EventStub; +use Gears\Event\Symfony\Tests\Stub\EventSubscriberInterfaceStub; +use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\Event; + +/** + * Symfony event dispatcher wrapper test. + */ +class EventDispatcherTest extends TestCase +{ + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /^Event handler must be an instance of .+\\EventHandler, stdClass given$/ + */ + public function testInvalidListener(): void + { + new EventDispatcher(['eventName' => new \stdClass()]); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Dispatched event cannot be empty + */ + public function testEmptyEvent(): void + { + $eventDispatcher = new EventDispatcher(); + + $eventDispatcher->dispatch('eventName'); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /^Dispatched event must implement .+\\EventEnvelope, .+ given$/ + */ + public function testInvalidEvent(): void + { + $eventDispatcher = new EventDispatcher(); + + $eventDispatcher->dispatch('eventName', new Event()); + } + + public function testEventDispatch(): void + { + $event = EventStub::instance(); + + $eventHandler = $this->getMockBuilder(EventHandler::class) + ->disableOriginalConstructor() + ->getMock(); + $eventHandler->expects($this->once()) + ->method('handle') + ->with($event); + + $subscriber = new EventSubscriberInterfaceStub([ + 'eventName' => [ + [$eventHandler], + ], + 'anotherEvent' => $eventHandler, + 'otherEvent' => [$eventHandler], + ]); + + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addSubscriber($subscriber); + + $eventDispatcher->dispatch('eventName', new EventEnvelope($event)); + } +} diff --git a/tests/Symfony/EventEnvelopeTest.php b/tests/Symfony/EventEnvelopeTest.php new file mode 100644 index 0000000..51530a3 --- /dev/null +++ b/tests/Symfony/EventEnvelopeTest.php @@ -0,0 +1,33 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony\Tests; + +use Gears\Event\Symfony\EventEnvelope; +use Gears\Event\Symfony\Tests\Stub\EventStub; +use PHPUnit\Framework\TestCase; + +/** + * Symfony event envelope test. + */ +class EventEnvelopeTest extends TestCase +{ + public function testEnvelope(): void + { + $event = EventStub::instance(); + + $eventEnvelope = new EventEnvelope($event); + + $this->assertSame($event, $eventEnvelope->getWrappedEvent()); + } +} diff --git a/tests/Symfony/Stub/EventStub.php b/tests/Symfony/Stub/EventStub.php new file mode 100644 index 0000000..39232e8 --- /dev/null +++ b/tests/Symfony/Stub/EventStub.php @@ -0,0 +1,32 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony\Tests\Stub; + +use Gears\Event\AbstractEmptyEvent; + +/** + * Event stub class. + */ +class EventStub extends AbstractEmptyEvent +{ + /** + * Instantiate event. + * + * @return self + */ + public static function instance(): self + { + return self::occurred(); + } +} diff --git a/tests/Symfony/Stub/EventSubscriberInterfaceStub.php b/tests/Symfony/Stub/EventSubscriberInterfaceStub.php new file mode 100644 index 0000000..4572897 --- /dev/null +++ b/tests/Symfony/Stub/EventSubscriberInterfaceStub.php @@ -0,0 +1,45 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Symfony\Tests\Stub; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Symfony event subscriber stub class. + */ +class EventSubscriberInterfaceStub implements EventSubscriberInterface +{ + /** + * @var array + */ + protected static $listeners; + + /** + * EventSubscriberInterfaceStub constructor. + * + * @param array $listeners + */ + public function __construct(array $listeners) + { + self::$listeners = $listeners; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return self::$listeners; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..846bd00 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,14 @@ + + */ + +declare(strict_types=1); + +require __DIR__ . '/../vendor/autoload.php';