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';