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..e46e3ff --- /dev/null +++ b/.php_cs @@ -0,0 +1,141 @@ + + */ + +use PhpCsFixer\Config; +use PhpCsFixer\Finder; + +$header = <<<'HEADER' +event (https://github.com/phpgears/event). +Event handling. + +@license MIT +@link https://github.com/phpgears/event +@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..dc9af32 --- /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)** 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..88d5f80 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +[![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.svg?style=flat-square)](https://packagist.org/packages/phpgears/event) +[![License](https://img.shields.io/github/license/phpgears/event.svg?style=flat-square)](https://github.com/phpgears/event/blob/master/LICENSE) + +[![Build Status](https://img.shields.io/travis/phpgears/event.svg?style=flat-square)](https://travis-ci.org/phpgears/event) +[![Style Check](https://styleci.io/repos/149037486/shield)](https://styleci.io/repos/149037486) +[![Code Quality](https://img.shields.io/scrutinizer/g/phpgears/event.svg?style=flat-square)](https://scrutinizer-ci.com/g/phpgears/event) +[![Code Coverage](https://img.shields.io/coveralls/phpgears/event.svg?style=flat-square)](https://coveralls.io/github/phpgears/event) + +[![Total Downloads](https://img.shields.io/packagist/dt/phpgears/event.svg?style=flat-square)](https://packagist.org/packages/phpgears/event/stats) +[![Monthly Downloads](https://img.shields.io/packagist/dm/phpgears/event.svg?style=flat-square)](https://packagist.org/packages/phpgears/event/stats) + +# Event + +Event base classes and handling interfaces + +This package only provides the building blocks to Event + +## Installation + +### Composer + +``` +composer require phpgears/event +``` + +## Usage + +Require composer autoload file + +```php +require './vendor/autoload.php'; +``` + +### Events + +Events are DTOs that carry all the information for an action to happen + +You can create your own by implementing `Gears\Event\Event` or extend from `Gears\Event\AbstractEvent` which ensures event immutability and payload is composed only of **scalar values** which is a very interesting capability. AbstractEvent has a private constructor forcing you to create events using the occurred static method + +```php +use Gears\Event\AbstractEvent; + +class CreateUserEvent extends AbstractEvent +{ + public static function fromPersonalData( + string $name, + string lastname, + \DateTimeImmutable $birthDate + ): self { + return new occurred([ + 'name' => $name, + 'lastname' => $lastname, + 'birthDate' => $birthDate->format('U'), + ]); + } +} +``` + +In case of a event without any payload you could extend `Gears\Event\AbstractEmptyEvent` + +```php +use Gears\Event\AbstractEvent; + +class CreateUserEvent extends AbstractEmptyEvent +{ + public static function instance(): self { + return self::occurred(); + } +} +``` + +### Collection + +Events can be grouped into iterables implementing `Gears\Event\EventCollection` objects, `Gears\Event\EventArrayCollection` is provided accepting only instances of `Gears\Event\Event` + +#### Async events + +Having event assuring all of its payload is composed only of scalar values proves handy when you want to delegate event handling to a message queue system such as RabbitMQ, Gearman or Apache Kafka, serializing/deserializing scalar values is trivial in any format and language + +Asynchronous behaviour must be implemented at EventBus level, event bus must be able to identify async events (a map of events, implementing an interface, by a payload parameter, ...) and enqueue them + +If you want to have asynchronous behaviour on your EventBus have a look [phpgears/event-async](https://github.com/phpgears/event-async), there you'll find all the necessary pieces to start your async event bus + +### Handlers + +Events are handed over to implementations of `Gears\Event\EventHandler`, available in this package is `AbstractEventHandler` which verifies the type of the event so you can focus only on implementing the handling logic + +```php +class CreateUserEventHandler extends AbstractEventHandler +{ + protected function getSupportedEventType(): string + { + return CreateUserEvent::class; + } + + protected function handleEvent(Event $event): void + { + /* @var CreateUserEvent $event */ + + $user = new User( + $event->getName(), + $event->getLastname(), + $event->getBirthDate() + ); + + [...] + } +} +``` + +Have a look at [phpgears/dto](https://github.com/phpgears/dto) fo a better understanding of how events are built out of DTOs and how they hold their payload + +### Event Bus + +Only `Gears\Event\EventBus` interface is provided, you can easily use any of the good bus libraries available out there by simply adding an adapter layer + +## Contributing + +Found a bug or have a feature request? [Please open a new issue](https://github.com/phpgears/event/issues). Have a look at existing issues before. + +See file [CONTRIBUTING.md](https://github.com/phpgears/event/blob/master/CONTRIBUTING.md) + +## License + +See file [LICENSE](https://github.com/phpgears/event/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..b34f872 --- /dev/null +++ b/composer.json @@ -0,0 +1,106 @@ +{ + "name": "phpgears/event", + "description": "Event handling", + "keywords": [ + "Event", + "immutable" + ], + "homepage": "https://github.com/phpgears/event", + "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", + "issues": "https://github.com/phpgears/event/issues" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "php": "^7.1", + "phpgears/dto": "~0.2" + }, + "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\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Gears\\Event\\Tests\\": "tests/Event/" + } + }, + "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..02c72a4 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +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 diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..1835d0c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + tests/Event/ + + + + + + src/ + + + + + + + diff --git a/src/AbstractEmptyEvent.php b/src/AbstractEmptyEvent.php new file mode 100644 index 0000000..0dd79ca --- /dev/null +++ b/src/AbstractEmptyEvent.php @@ -0,0 +1,90 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event; + +use Gears\DTO\ScalarPayloadBehaviour; +use Gears\Event\Time\SystemTimeProvider; +use Gears\Event\Time\TimeProvider; +use Gears\Immutability\ImmutabilityBehaviour; + +/** + * Abstract empty immutable event. + */ +abstract class AbstractEmptyEvent implements Event +{ + use ImmutabilityBehaviour, ScalarPayloadBehaviour { + ScalarPayloadBehaviour::__call insteadof ImmutabilityBehaviour; + } + + /** + * @var \DateTimeImmutable + */ + private $createdAt; + + /** + * AbstractEmptyEvent constructor. + * + * @param \DateTimeImmutable $createdAt + */ + private function __construct(\DateTimeImmutable $createdAt) + { + $this->checkImmutability(); + + $this->createdAt = $createdAt->setTimezone(new \DateTimeZone('UTC')); + } + + /** + * Instantiate new event. + * + * @param TimeProvider $timeProvider + * + * @return mixed|self + */ + final protected static function occurred(?TimeProvider $timeProvider = null) + { + $timeProvider = $timeProvider ?? new SystemTimeProvider(); + + return new static($timeProvider->getCurrentTime()); + } + + /** + * {@inheritdoc} + */ + final public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + /** + * {@inheritdoc} + * + * @return mixed|self + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public static function reconstitute(array $payload, array $attributes) + { + return new static($attributes['createdAt']); + } + + /** + * {@inheritdoc} + * + * @return string[] + */ + final protected function getAllowedInterfaces(): array + { + return [Event::class]; + } +} diff --git a/src/AbstractEvent.php b/src/AbstractEvent.php new file mode 100644 index 0000000..b598d27 --- /dev/null +++ b/src/AbstractEvent.php @@ -0,0 +1,92 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event; + +use Gears\DTO\ScalarPayloadBehaviour; +use Gears\Event\Time\SystemTimeProvider; +use Gears\Event\Time\TimeProvider; +use Gears\Immutability\ImmutabilityBehaviour; + +/** + * Abstract immutable event. + */ +abstract class AbstractEvent implements Event +{ + use ImmutabilityBehaviour, ScalarPayloadBehaviour { + ScalarPayloadBehaviour::__call insteadof ImmutabilityBehaviour; + } + + /** + * @var \DateTimeImmutable + */ + private $createdAt; + + /** + * AbstractEvent constructor. + * + * @param array $payload + * @param \DateTimeImmutable $createdAt + */ + private function __construct(array $payload, \DateTimeImmutable $createdAt) + { + $this->checkImmutability(); + + $this->createdAt = $createdAt->setTimezone(new \DateTimeZone('UTC')); + + $this->setPayload($payload); + } + + /** + * Instantiate new event. + * + * @param array $parameters + * @param TimeProvider $timeProvider + * + * @return mixed|self + */ + final protected static function occurred(array $parameters, ?TimeProvider $timeProvider = null) + { + $timeProvider = $timeProvider ?? new SystemTimeProvider(); + + return new static($parameters, $timeProvider->getCurrentTime()); + } + + /** + * {@inheritdoc} + */ + final public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + /** + * {@inheritdoc} + * + * @return mixed|self + */ + public static function reconstitute(array $payload, array $attributes) + { + return new static($payload, $attributes['createdAt']); + } + + /** + * {@inheritdoc} + * + * @return string[] + */ + final protected function getAllowedInterfaces(): array + { + return [Event::class]; + } +} diff --git a/src/AbstractEventHandler.php b/src/AbstractEventHandler.php new file mode 100644 index 0000000..85304c6 --- /dev/null +++ b/src/AbstractEventHandler.php @@ -0,0 +1,52 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event; + +use Gears\Event\Exception\InvalidEventException; + +abstract class AbstractEventHandler implements EventHandler +{ + /** + * {@inheritdoc} + * + * @throws InvalidEventException + */ + final public function handle(Event $event): void + { + $supportedEventType = $this->getSupportedEventType(); + if (!\is_a($event, $supportedEventType)) { + throw new InvalidEventException(\sprintf( + 'Event must implement %s interface, %s given', + $supportedEventType, + \get_class($event) + )); + } + + $this->handleEvent($event); + } + + /** + * Get supported event type. + * + * @return string + */ + abstract protected function getSupportedEventType(): string; + + /** + * Handle event. + * + * @param Event $event + */ + abstract protected function handleEvent(Event $event): void; +} diff --git a/src/Event.php b/src/Event.php new file mode 100644 index 0000000..36c9786 --- /dev/null +++ b/src/Event.php @@ -0,0 +1,62 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event; + +/** + * Event interface. + */ +interface Event +{ + /** + * Check parameter existence. + * + * @param string $parameter + * + * @return bool + */ + public function has(string $parameter): bool; + + /** + * Get parameter. + * + * @param string $parameter + * + * @return mixed + */ + public function get(string $parameter); + + /** + * Export message parameters. + * + * @return array + */ + public function getPayload(): array; + + /** + * Get event creation time. + * + * @return \DateTimeImmutable + */ + public function getCreatedAt(): \DateTimeImmutable; + + /** + * Reconstitute message. + * + * @param array $payload + * @param array $attributes + * + * @return mixed|self + */ + public static function reconstitute(array $payload, array $attributes); +} diff --git a/src/EventArrayCollection.php b/src/EventArrayCollection.php new file mode 100644 index 0000000..6eaf354 --- /dev/null +++ b/src/EventArrayCollection.php @@ -0,0 +1,90 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event; + +use Gears\Event\Exception\InvalidEventException; + +final class EventArrayCollection implements EventCollection +{ + /** + * @var Event[] + */ + private $events = []; + + /** + * EventArrayCollection constructor. + * + * @param (Event|mixed)[] $events + * + * @throws InvalidEventException + */ + public function __construct(array $events) + { + foreach ($events as $event) { + if (!$event instanceof Event) { + throw new InvalidEventException(\sprintf( + 'Event collection only accepts %s, %s given', + Event::class, + \is_object($event) ? \get_class($event) : \gettype($event) + )); + } + + $this->events[] = $event; + } + } + + /** + * {@inheritdoc} + */ + public function rewind(): void + { + \reset($this->events); + } + + /** + * {@inheritdoc} + */ + public function valid(): bool + { + return \key($this->events) !== null; + } + + /** + * {@inheritdoc} + * + * @return Event + */ + public function current(): Event + { + return \current($this->events); + } + + /** + * {@inheritdoc} + * + * @return string|int|null + */ + public function key() + { + return \key($this->events); + } + + /** + * {@inheritdoc} + */ + public function next(): void + { + \next($this->events); + } +} diff --git a/src/EventBus.php b/src/EventBus.php new file mode 100644 index 0000000..e502afb --- /dev/null +++ b/src/EventBus.php @@ -0,0 +1,24 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event; + +interface EventBus +{ + /** + * Dispatch event. + * + * @param Event $event + */ + public function dispatch(Event $event): void; +} diff --git a/src/EventCollection.php b/src/EventCollection.php new file mode 100644 index 0000000..7b3711b --- /dev/null +++ b/src/EventCollection.php @@ -0,0 +1,27 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event; + +/** + * Event collection. + */ +interface EventCollection extends \Iterator +{ + /** + * {@inheritdoc} + * + * @return Event + */ + public function current(): Event; +} diff --git a/src/EventHandler.php b/src/EventHandler.php new file mode 100644 index 0000000..036da8c --- /dev/null +++ b/src/EventHandler.php @@ -0,0 +1,24 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event; + +interface EventHandler +{ + /** + * Handle event. + * + * @param Event $event + */ + public function handle(Event $event): void; +} diff --git a/src/Exception/EventException.php b/src/Exception/EventException.php new file mode 100644 index 0000000..42863cc --- /dev/null +++ b/src/Exception/EventException.php @@ -0,0 +1,18 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Exception; + +class EventException extends \RuntimeException +{ +} diff --git a/src/Exception/InvalidEventException.php b/src/Exception/InvalidEventException.php new file mode 100644 index 0000000..07d5ac0 --- /dev/null +++ b/src/Exception/InvalidEventException.php @@ -0,0 +1,18 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Exception; + +class InvalidEventException extends \RuntimeException +{ +} diff --git a/src/Exception/InvalidEventHandlerException.php b/src/Exception/InvalidEventHandlerException.php new file mode 100644 index 0000000..69ae96d --- /dev/null +++ b/src/Exception/InvalidEventHandlerException.php @@ -0,0 +1,18 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Exception; + +class InvalidEventHandlerException extends \RuntimeException +{ +} diff --git a/src/Time/FixedTimeProvider.php b/src/Time/FixedTimeProvider.php new file mode 100644 index 0000000..15f7921 --- /dev/null +++ b/src/Time/FixedTimeProvider.php @@ -0,0 +1,62 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Time; + +/** + * Fixed date time provider. + */ +final class FixedTimeProvider implements TimeProvider +{ + /** + * Fixed date. + * + * @var \DateTimeImmutable + */ + private $fixedTime; + + /** + * @var \DateTimeZone + */ + private $timeZone; + + /** + * FixedTimeProvider constructor. + * + * @param \DateTimeImmutable $fixedTime + * @param \DateTimeZone|null $timeZone + */ + public function __construct(\DateTimeImmutable $fixedTime, ?\DateTimeZone $timeZone = null) + { + $this->timeZone = $timeZone ?? new \DateTimeZone('UTC'); + $this->fixedTime = $fixedTime->setTimezone($this->timeZone); + } + + /** + * Set fixed date. + * + * @param \DateTimeImmutable $fixedTime + */ + public function setCurrentTime(\DateTimeImmutable $fixedTime): void + { + $this->fixedTime = $fixedTime->setTimezone($this->timeZone); + } + + /** + * {@inheritdoc} + */ + public function getCurrentTime(): \DateTimeImmutable + { + return $this->fixedTime; + } +} diff --git a/src/Time/SystemTimeProvider.php b/src/Time/SystemTimeProvider.php new file mode 100644 index 0000000..a62c180 --- /dev/null +++ b/src/Time/SystemTimeProvider.php @@ -0,0 +1,43 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Time; + +/** + * General system time provider. + */ +final class SystemTimeProvider implements TimeProvider +{ + /** + * @var \DateTimeZone + */ + private $timeZone; + + /** + * SystemTimeProvider constructor. + * + * @param \DateTimeZone|null $timeZone + */ + public function __construct(?\DateTimeZone $timeZone = null) + { + $this->timeZone = $timeZone ?? new \DateTimeZone('UTC'); + } + + /** + * {@inheritdoc} + */ + public function getCurrentTime(): \DateTimeImmutable + { + return new \DateTimeImmutable('now', $this->timeZone); + } +} diff --git a/src/Time/TimeProvider.php b/src/Time/TimeProvider.php new file mode 100644 index 0000000..da06500 --- /dev/null +++ b/src/Time/TimeProvider.php @@ -0,0 +1,27 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Time; + +/** + * Time provider interface. + */ +interface TimeProvider +{ + /** + * Get time from clock. + * + * @return \DateTimeImmutable + */ + public function getCurrentTime(): \DateTimeImmutable; +} diff --git a/tests/Event/AbstractEventHandlerTest.php b/tests/Event/AbstractEventHandlerTest.php new file mode 100644 index 0000000..d321a1d --- /dev/null +++ b/tests/Event/AbstractEventHandlerTest.php @@ -0,0 +1,58 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Tests; + +use Gears\Event\Tests\Stub\AbstractEmptyEventStub; +use Gears\Event\Tests\Stub\AbstractEventHandlerStub; +use Gears\Event\Tests\Stub\AbstractEventStub; +use PHPUnit\Framework\TestCase; + +/** + * Abstract domain event handler test. + */ +class AbstractEventHandlerTest extends TestCase +{ + /** + * @expectedException \Gears\Event\Exception\InvalidEventException + * @expectedExceptionMessageRegExp /Event must implement .+\\AbstractEventStub interface, .+ given/ + */ + public function testInvalidEventType(): void + { + $handler = new AbstractEventHandlerStub(); + $handler->handle(AbstractEmptyEventStub::instance()); + } + + public function testHandling(): void + { + $handler = new AbstractEventHandlerStub(); + $handler->handle(AbstractEventStub::instance()); + + $this->assertTrue(true); + } + + public function testReconstitute(): void + { + $createdAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + $event = AbstractEventStub::reconstitute(['parameter' => 'one'], ['createdAt' => $createdAt]); + + $this->assertTrue($event->has('parameter')); + $this->assertEquals($createdAt, $event->getCreatedAt()); + + $emptyCommand = AbstractEmptyEventStub::reconstitute(['parameter' => 'one'], ['createdAt' => $createdAt]); + + $this->assertFalse($emptyCommand->has('parameter')); + $this->assertEquals($createdAt, $emptyCommand->getCreatedAt()); + } +} diff --git a/tests/Event/EventArrayCollectionTest.php b/tests/Event/EventArrayCollectionTest.php new file mode 100644 index 0000000..4486d06 --- /dev/null +++ b/tests/Event/EventArrayCollectionTest.php @@ -0,0 +1,49 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Tests; + +use Gears\Event\Event; +use Gears\Event\EventArrayCollection; +use Gears\Event\Tests\Stub\AbstractEmptyEventStub; +use PHPUnit\Framework\TestCase; + +/** + * Event array collection test. + */ +class EventArrayCollectionTest extends TestCase +{ + /** + * @expectedException \Gears\Event\Exception\InvalidEventException + * @expectedExceptionMessageRegExp /Event collection only accepts .+, string given/ + */ + public function testInvalidTypeCollection(): void + { + new EventArrayCollection(['event']); + } + + public function testCollection(): void + { + $events = [ + AbstractEmptyEventStub::instance(), + AbstractEmptyEventStub::instance(), + ]; + $collection = new EventArrayCollection($events); + + foreach ($collection as $event) { + $this->assertInstanceOf(Event::class, $event); + } + + $this->assertNull($collection->key()); + } +} diff --git a/tests/Event/Stub/AbstractEmptyEventHandlerStub.php b/tests/Event/Stub/AbstractEmptyEventHandlerStub.php new file mode 100644 index 0000000..cb05ff7 --- /dev/null +++ b/tests/Event/Stub/AbstractEmptyEventHandlerStub.php @@ -0,0 +1,38 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Tests\Stub; + +use Gears\Event\AbstractEventHandler; +use Gears\Event\Event; + +/** + * Abstract empty event handler stub class. + */ +class AbstractEmptyEventHandlerStub extends AbstractEventHandler +{ + /** + * {@inheritdoc} + */ + protected function getSupportedEventType(): string + { + return AbstractEmptyEventStub::class; + } + + /** + * {@inheritdoc} + */ + protected function handleEvent(Event $event): void + { + } +} diff --git a/tests/Event/Stub/AbstractEmptyEventStub.php b/tests/Event/Stub/AbstractEmptyEventStub.php new file mode 100644 index 0000000..f9b07b3 --- /dev/null +++ b/tests/Event/Stub/AbstractEmptyEventStub.php @@ -0,0 +1,32 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Tests\Stub; + +use Gears\Event\AbstractEmptyEvent; + +/** + * Abstract empty event stub class. + */ +class AbstractEmptyEventStub extends AbstractEmptyEvent +{ + /** + * Instantiate event. + * + * @return self + */ + public static function instance(): self + { + return self::occurred(); + } +} diff --git a/tests/Event/Stub/AbstractEventHandlerStub.php b/tests/Event/Stub/AbstractEventHandlerStub.php new file mode 100644 index 0000000..3383da6 --- /dev/null +++ b/tests/Event/Stub/AbstractEventHandlerStub.php @@ -0,0 +1,38 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Tests\Stub; + +use Gears\Event\AbstractEventHandler; +use Gears\Event\Event; + +/** + * Abstract event handler stub class. + */ +class AbstractEventHandlerStub extends AbstractEventHandler +{ + /** + * {@inheritdoc} + */ + protected function getSupportedEventType(): string + { + return AbstractEventStub::class; + } + + /** + * {@inheritdoc} + */ + protected function handleEvent(Event $event): void + { + } +} diff --git a/tests/Event/Stub/AbstractEventStub.php b/tests/Event/Stub/AbstractEventStub.php new file mode 100644 index 0000000..1baf4c7 --- /dev/null +++ b/tests/Event/Stub/AbstractEventStub.php @@ -0,0 +1,32 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Tests\Stub; + +use Gears\Event\AbstractEvent; + +/** + * Abstract event stub class. + */ +class AbstractEventStub extends AbstractEvent +{ + /** + * Instantiate event. + * + * @return self + */ + public static function instance(): self + { + return self::occurred([]); + } +} diff --git a/tests/Event/Time/FixedTimeProviderTest.php b/tests/Event/Time/FixedTimeProviderTest.php new file mode 100644 index 0000000..648a8d1 --- /dev/null +++ b/tests/Event/Time/FixedTimeProviderTest.php @@ -0,0 +1,37 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Tests\Time; + +use Gears\Event\Time\FixedTimeProvider; +use PHPUnit\Framework\TestCase; + +/** + * Fixed time provider test. + */ +class FixedTimeProviderTest extends TestCase +{ + public function testTime(): void + { + $fixedTime = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + $timeProvider = new FixedTimeProvider($fixedTime); + + $this->assertEquals($fixedTime, $timeProvider->getCurrentTime()); + + $newTime = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + $timeProvider->setCurrentTime($newTime); + $this->assertEquals($newTime, $timeProvider->getCurrentTime()); + } +} diff --git a/tests/Event/Time/SystemTimeProviderTest.php b/tests/Event/Time/SystemTimeProviderTest.php new file mode 100644 index 0000000..b68ba7d --- /dev/null +++ b/tests/Event/Time/SystemTimeProviderTest.php @@ -0,0 +1,38 @@ + + */ + +declare(strict_types=1); + +namespace Gears\Event\Tests\Time; + +use Gears\Event\Time\SystemTimeProvider; +use PHPUnit\Framework\TestCase; + +/** + * System's time provider test. + */ +class SystemTimeProviderTest extends TestCase +{ + public function testTime(): void + { + $timeProvider = new SystemTimeProvider(); + + $previousTime = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); + + for ($i = 0; $i < 10; $i++) { + $currentTime = $timeProvider->getCurrentTime(); + + $this->assertTrue($currentTime > $previousTime); + + $previousTime = $currentTime; + } + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..eaf2842 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,14 @@ + + */ + +declare(strict_types=1); + +require __DIR__ . '/../vendor/autoload.php';