diff --git a/.travis.yml b/.travis.yml index fd9f3ac..569e983 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,131 +1,23 @@ language: php -sudo: false +dist: trusty matrix: fast_finish: true include: - php: 7.1 - dist: trusty - sudo: true env: - DEPENDENCIES="" - - DRIVER="pdo_mysql" - - DB=mariadb_10.2 - addons: - mariadb: '10.2' - - php: 7.1 - dist: trusty - sudo: true - env: - - DEPENDENCIES="--prefer-lowest --prefer-stable" - - DRIVER="pdo_mysql" - - DB=mariadb_10.2 - addons: - mariadb: '10.2' - - php: 7.1 - dist: trusty - sudo: true - env: - - DEPENDENCIES="" - - DRIVER="pdo_mysql" - - DB=mariadb_10.2 - - DB_ATTR_ERRMODE=2 # \PDO::ERRMODE_EXCEPTION - addons: - mariadb: '10.2' - - php: 7.1 - sudo: true - env: - - DEPENDENCIES="" - - DRIVER="pdo_mysql" - - DB=mysql_5.7 - - php: 7.1 - sudo: true - env: - - DEPENDENCIES="--prefer-lowest --prefer-stable" - - DRIVER="pdo_mysql" - - DB=mysql_5.7 - - php: 7.1 - sudo: true - env: - - DEPENDENCIES="" - - DRIVER="pdo_mysql" - - DB=mysql_5.7 - - DB_ATTR_ERRMODE=2 # \PDO::ERRMODE_EXCEPTION - - php: 7.1 - env: - - DEPENDENCIES="" - - DRIVER="pdo_pgsql" - - DB=postgres_9.4 - addons: - postgresql: '9.4' - - php: 7.1 - env: - - DEPENDENCIES="--prefer-lowest --prefer-stable" - - DRIVER="pdo_pgsql" - - DB=postgres_9.4 - addons: - postgresql: '9.4' - - php: 7.1 - env: - - DEPENDENCIES="" - - DRIVER="pdo_pgsql" - - DB=postgres_9.4 - - DB_ATTR_ERRMODE=2 # \PDO::ERRMODE_EXCEPTION - addons: - postgresql: '9.4' - - php: 7.1 - dist: trusty - env: - - DEPENDENCIES="" - - DRIVER="pdo_pgsql" - - DB=postgres_9.5 - addons: - postgresql: '9.5' + - TEST_COVERAGE=true - php: 7.1 - dist: trusty env: - DEPENDENCIES="--prefer-lowest --prefer-stable" - - DRIVER="pdo_pgsql" - - DB=postgres_9.5 - addons: - postgresql: '9.5' - - php: 7.1 - dist: trusty + - php: 7.2 env: - DEPENDENCIES="" - - DRIVER="pdo_pgsql" - - DB=postgres_9.5 - - DB_ATTR_ERRMODE=2 # \PDO::ERRMODE_EXCEPTION - addons: - postgresql: '9.5' - - php: 7.1 - dist: trusty - env: - - DEPENDENCIES="" - - EXECUTE_CS_CHECK=true - - TEST_COVERAGE=true - - DRIVER="pdo_pgsql" - - DB=postgres_9.6 - addons: - postgresql: '9.6' - - php: 7.1 - dist: trusty + - php: 7.2 env: - DEPENDENCIES="--prefer-lowest --prefer-stable" - - DRIVER="pdo_pgsql" - - DB=postgres_9.6 - addons: - postgresql: '9.6' - - php: 7.1 - dist: trusty - env: - - DEPENDENCIES="" - - DRIVER="pdo_pgsql" - - DB=postgres_9.6 - - DB_ATTR_ERRMODE=2 # \PDO::ERRMODE_EXCEPTION - addons: - postgresql: '9.6' cache: directories: @@ -136,18 +28,13 @@ cache: before_script: - mkdir -p "$HOME/.php-cs-fixer" - phpenv config-rm xdebug.ini - - VENDOR=$(echo $DB | cut -d'_' -f 1) - - if [[ $DB == 'mysql_5.7' ]]; then bash .travis.install-mysql-5.7.sh; fi - - if [[ $DRIVER == 'pdo_mysql' ]]; then mysql -e 'create database event_store_tests;'; fi - - if [[ $DRIVER == 'pdo_pgsql' ]]; then psql -c 'create database event_store_tests;' -U postgres; fi - composer self-update - composer update $DEPENDENCIES script: - - cp phpunit.xml.$VENDOR phpunit.xml - if [[ $TEST_COVERAGE == 'true' ]]; then php -dzend_extension=xdebug.so ./vendor/bin/phpunit --coverage-text --coverage-clover ./build/logs/clover.xml; else ./vendor/bin/phpunit; fi - - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run; fi - - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/docheader check src/ tests/; fi + - ./vendor/bin/php-cs-fixer fix -v --diff --dry-run + - ./vendor/bin/docheader check src/ tests/ after_success: - if [[ $TEST_COVERAGE == 'true' ]]; then php vendor/bin/coveralls -v; fi diff --git a/LICENSE b/LICENSE index 3b375f9..21cd8ca 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ -Copyright (c) 2016-2017, prooph software GmbH -Copyright (c) 2016-2017, Sascha-Oliver Prolic +Copyright (c) 2017-2018, prooph software GmbH +Copyright (c) 2017-2018, Sascha-Oliver Prolic All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 29d80e5..384ac7a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # prooph message flow analyzer -A static code analyzer to extract message flow of a prooph project +[![Build Status](https://travis-ci.org/prooph/message-flow-analyzer.svg?branch=master)](https://travis-ci.org/prooph/message-flow-analyzer) +[![Coverage Status](https://coveralls.io/repos/github/prooph/message-flow-analyzer/badge.svg?branch=master)](https://coveralls.io/github/prooph/message-flow-analyzer?branch=master) +[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/prooph/improoph) + +A static code analyzer to extract a message flow of a prooph project. Results can be visualized in the [prooph Mgmt UI](https://github.com/prooph/event-store-mgmt-ui). + +![Model Exploration](https://github.com/prooph/proophessor/blob/master/assets/prooph_do_exploration.gif) ## Installation @@ -16,12 +22,12 @@ An example of a default config can be found in the [test example project](https: ## Run ```bash -php vendor/bin/prooph-analyzer project:analyze +php vendor/bin/prooph-analyzer project:analyze -vvv ``` ## Why? -The prooph message flow analyzer scans your project for prooph messages and collects information how these messages flow through your project source code :) +The prooph message flow analyzer scans your project for prooph messages and collects information how these messages flow through your system :) The analysis contains information about: @@ -32,19 +38,11 @@ The analysis contains information about: The message flow is written to an output file (`prooph_message_flow.json` by default). -For now that's it. But imagine what you can do with this information! We'll add different output formatters to generate config for d3js or draw.io. -The message flow analyzer will also be part of the upcoming `event-store-mgmt-ui` and will allow you to connect the message flow with your event streams -for debugging and monitoring. - ## How? The package uses the excellent libraries [roave/better-reflection](https://github.com/Roave/BetterReflection) and [nikic/php-parser](https://github.com/nikic/PHP-Parser) (which is used by Roave/BetterReflection internally, too) -## WIP - -`prooph/message-flow-analyzer` and the `event-store-mgmt-ui` are work in progress. There is no roadmap defined yet. If you think your project could benefit -from a stable version and you or your company would like to support development then [get in touch](http://getprooph.org/#get-in-touch). ## Filters @@ -59,21 +57,32 @@ interesting in the class it can add this information to the `MessageFlow`. Again `prooph/message-flow-analyzer` ships with default class visitors (see example config) which can be found in the [Visitor dir](https://github.com/prooph/message-flow-analyzer/tree/master/src/Visitor). +## Documentation + +Documentation is [in the doc tree](docs/), and can be compiled using [bookdown](http://bookdown.io). + +```console +$ php ./vendor/bin/bookdown docs/bookdown.json +$ php -S 0.0.0.0:8080 -t docs/html/ +``` + ## Run it against proophessor-do -You can see `prooph/message-flow-analyzer` in action by running it against [proophessor-do](https://github.com/prooph/proophessor-do). +You can see the `prooph/message-flow-analyzer` in action by running it against [proophessor-do](https://github.com/prooph/proophessor-do) or [proophessor-do-symfony](https://github.com/prooph/proophessor-do-symfony). + +## Support + +- Ask questions on Stack Overflow tagged with [#prooph](https://stackoverflow.com/questions/tagged/prooph). +- File issues at [https://github.com/prooph/message-flow-analyzer/issues](https://github.com/prooph/message-flow-analyzer/issues). +- Say hello in the [prooph gitter](https://gitter.im/prooph/improoph) chat. -1. Clone proophessor-do -2. Add `prooph/message-flow-analyzer: dev-master` to the `require-dev` config of proophessor-do's `composer.json` -3. Run composer install -4. Copy [prooph_analyzer.json](https://github.com/prooph/message-flow-analyzer/blob/master/tests/Sample/DefaultProject/prooph_analyzer.json) into root dir of proophessor-do -5. Copy [ExcludeBlacklistedFiles.php](https://gist.github.com/codeliner/6bae2c3a5de0a9f93e1d2143f7196f75#file-excludeblacklistedfiles-php) into `src/Infrastructure/ProophAnalyzer`. - This is needed because proophessor-do contains a prepared factory for mongodb connection but mongo is not installed by default so the mongo classes cannot be loaded. -6. Add `"Prooph\\ProophessorDo\\Infrastructure\\ProophAnalyzer\\ExcludeBlacklistedFiles"` as last entry in the `prooph_analyzer.json` `fileInfoFilters` array. -7. Run `php vendor/bin/prooph-analyzer project:analyze` and watch the generated output file `prooph_message_flow.json` +## Contribute -If this is too much work right now and you only want to see the result: [prooph_message_flow.json](https://gist.github.com/codeliner/6bae2c3a5de0a9f93e1d2143f7196f75#file-prooph_message_flow-json) +Please feel free to fork and extend existing or add new plugins and send a pull request with your changes! +To establish a consistent code quality, please provide unit tests for all your changes and may adapt the documentation. +## License +Released under the [New BSD License](LICENSE). diff --git a/bin/prooph-analyzer b/bin/prooph-analyzer index 3193532..4ee2d7b 100755 --- a/bin/prooph-analyzer +++ b/bin/prooph-analyzer @@ -2,8 +2,8 @@ - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/composer.json b/composer.json index 149ddfe..aad5449 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "require": { "roave/better-reflection": "^2.0", "nikic/php-parser": "^3.1", - "symfony/console": "^3.3 || ^4.0" + "symfony/console": "^3.2 || ^4.0" }, "autoload": { "psr-4": { diff --git a/docs/analyzer.md b/docs/analyzer.md new file mode 100644 index 0000000..2be3ccf --- /dev/null +++ b/docs/analyzer.md @@ -0,0 +1,25 @@ +# CLI Command + +The prooph message flow analyzer ships with a CLI command to analyze a project or parts of it. + +## Options + +Run the following command to get an overview of your options. + +```bash +php vendor/bin/prooph-analyzer project:analyze --help +``` + +## Run With Defaults + +```bash +php vendor/bin/prooph-analyzer project:analyze -vvv +``` +By default the analyzer uses current working dir as the root of the analysis. +It looks for a config file called `prooph_analyzer.json`. More on this in the configuration section. + +A successful run produces a `prooph_message_flow.json` with the results. This file can be imported into +the [prooph Mgmt UI](https://github.com/prooph/event-store-mgmt-ui) message flow app. + +*Note: It is recommended to always run the command in very verbose mode **-vvv** to get detailed exception traces in case of an error.* + diff --git a/docs/bookdown.json b/docs/bookdown.json new file mode 100644 index 0000000..df0fcac --- /dev/null +++ b/docs/bookdown.json @@ -0,0 +1,14 @@ +{ + "title": "Message Flow Analyzer", + "content": [ + {"intro": "introduction.md"}, + {"analyzer": "analyzer.md"}, + {"messageflow": "message-flow.md"}, + {"config": "configuration.md"}, + {"troubleshooting": "troubleshooting.md"} + ], + "tocDepth": 1, + "numbering": false, + "target": "./html", + "template": "../vendor/prooph/bookdown-template/templates/main.php" +} diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..2fe5e82 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,347 @@ +# Configuration + +The analyzer can be configured using a json file. By default the analyzer uses a `prooph_analyzer.json` located in the current working directory. +An example of a default config can be found in the [test example project](https://github.com/prooph/message-flow-analyzer/blob/master/tests/Sample/DefaultProject/prooph_analyzer.json) + +## fileInfoFilters + +You can add `include` and `exclude` filters for files and directories. + +Configuration should be a List of filter classes implementing `Prooph\MessageFlowAnalyzer\Filter\FileInfoFilter`. + +```json +{ + "fileInfoFilters": [ + "ExcludeVendorDir", + "ExcludeTestsDir", + "ExcludeHiddenFileInfo", + "IncludePHPFile", + "Acme\\Custom\\Filter" + ] +} +``` +A filter should be constructable without arguments. The message flow analyzer ships with a set of default +filters listed above. The last filter in the example is a project specific filter. Such filters needed to be listed +with their full qualified class name. Default filters are aliased. + +The filter interface defines a simple method: + +```php +nodes() as $node) { + if($this->isMessageNode($node)) { + $messageFlow = $messageFlow->setNode( + $node->withIcon(MessageFlow\NodeIcon::faSolid('fa-paper-plane')) + ); + } + } + + return $messageFlow; + } + + private function isMessageNode(MessageFlow\Node $node): bool + { + return array_key_exists($node->type(), MessageFlow\Node::MESSAGE_TYPES); + } +} +``` + +## Custom Node Class + +You can tell the message flow analyzer to use a custom `Node` class instead of the default one. + +Just extend your own node class from `Prooph\MessageFlowAnalyzer\MessageFlow\Node` and point to it in the configuration: + +```json +{ + "nodeClass": "Acme\\Custom\\Node" +} +``` + +Again the message icon example. This time we override the named constructor for messages of the node class with the same result: +All message nodes will use the `fa-paper-plane` icon instead of the default `fa-envelope` set by the default node class. + +```php +withIcon(MessageFlow\NodeIcon::faSolid('fa-paper-plane')); + } +} +``` + +## Output formatter + +An output formatter can be passed as an option to the CLI command. + +```bash +php vendor/bin/prooph-analyzer project:analyze -vvv -f Acme\\Custom\\Formatter +``` +A custom formatter should implement: + +```php +toArray(), JSON_PRETTY_PRINT); + } +} +``` +The resulting json string is read by the [prooph Mgmt UI](https://github.com/prooph/event-store-mgmt-ui) to draw +the nodes and edges of the flow. + +`JSON_PRETTY_PRINT` is used by default to get a human readable file. If you want to send the file over the wire you might use +your own output formatter without the pretty print option or maybe you want to import the nodes and edges in a graph database +and need a different format. + +## Custom Icons + +Checkout the `Prooph\MessageFlowAnalyzer\MessageFlow\NodeIcon` class: + +```php +withIcon(MessageFlow\NodeIcon::faSolid('fa-paper-plane')); +$node = $node->withIcon(MessageFlow\NodeIcon::faBrand('fa-php')); +$node = $node->withIcon(MessageFlow\NodeIcon::faRegular('fa-bell')); +$node = $node->withIcon(MessageFlow\NodeIcon::link('https://static.acme.com/assets/logo.svg')); +//svg: viewPort="0 0 512 512", img: w 75 x h 50 +``` + + + + + + + + + diff --git a/docs/img/edge.png b/docs/img/edge.png new file mode 100644 index 0000000..553a388 Binary files /dev/null and b/docs/img/edge.png differ diff --git a/docs/img/node.png b/docs/img/node.png new file mode 100644 index 0000000..7c72fcd Binary files /dev/null and b/docs/img/node.png differ diff --git a/docs/img/nodes_edges.png b/docs/img/nodes_edges.png new file mode 100644 index 0000000..55a331a Binary files /dev/null and b/docs/img/nodes_edges.png differ diff --git a/docs/img/paper-plane.png b/docs/img/paper-plane.png new file mode 100644 index 0000000..3d64e79 Binary files /dev/null and b/docs/img/paper-plane.png differ diff --git a/docs/img/parent.png b/docs/img/parent.png new file mode 100644 index 0000000..71cf394 Binary files /dev/null and b/docs/img/parent.png differ diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..9273931 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,75 @@ +# Introduction + +A static code analyzer to extract the message flow of your prooph project. The result can be visualized in the [prooph Mgmt UI](https://github.com/prooph/event-store-mgmt-ui). + +![Model Exploration](https://github.com/prooph/proophessor/blob/master/assets/prooph_do_exploration.gif) + +## Installation + +```bash +composer require --dev prooph/message-flow-analyzer +``` + +## Run + +```bash +php vendor/bin/prooph-analyzer project:analyze -vvv +``` + +## Why? + +The prooph message flow analyzer scans your project and collects information about messages and how they are handled or +produced by your system. The result is a message flow that can be used to visualize and highlight the business logic. +All technical parts of the system are hidden and only the core logic is extracted. This gives you a high level overview of +what the system does and the effects of it. + +### Discuss With Domain Experts + +You can discuss implementations with your domain experts because all command, event, aggregate names etc. should reflect the +Ubiquitous Language and that's the only information visible in the message flow. If your domain expert cannot read and verify the +flow you should revisit your implementation. + +### Living Documentation + +The message flow can serve as a living documentation just like you know it from an automatically generated API doc. +The difference here is that no technical information is extracted but instead business knowledge written into code is extracted +and visualized and that's the important information! Only if you get the business logic right your system will have a value for your company. +You can run the analyzer periodically and update the message flow. + +### Debugging +Do you know every part of the system? Do you know every single command and event and what action is connected with them? +The message flow will give you a high level overview so that you can find the right place in your code faster. +Try out the watcher feature of the message flow which is explained in the mgmt UI documentation. You can interact with the application +and the message flow will highlight the parts of the flow that are effected by your current session. This way you can easily see which +processes are involved or triggered. + +### New Developers +The message flow visualized in the [prooph Mgmt UI](https://github.com/prooph/event-store-mgmt-ui) gives new developers a great overview of the system. +In most cases a new developer has to look at the database schema to get an idea of what is going on. But what can a database tell the developer about behaviour? +It's only purpose is to store state. State is not behaviour. Entity relations are not behaviour! This is only structure but you can't understand a system +by looking at the database structure of it. + +With the message flow a new developer gets a better picture and the best thing is: **the picture is NOT static** + +### Inter-Process Communication + +It is possible to combine the message flow results of different services in the mgmt UI. This means that processes can be tracked +across a service mesh and don't stop at the border of a single bounded context! + + +## Collected Information + +The analysis contains information about: + +- commands, events, queries +- message handlers per message (command handler, event listner, process manager, ...) +- message producers per message (controller, cli commands, process manager, ...) +- event recorders per event (classes implementing prooph's AggregateRoot or using the EventProducerTrait) + +The message flow is written to an output file (`prooph_message_flow.json` by default). + +## How? + +The package uses the excellent libraries [roave/better-reflection](https://github.com/Roave/BetterReflection) +and [nikic/php-parser](https://github.com/nikic/PHP-Parser) (which is used by Roave/BetterReflection internally, too) + diff --git a/docs/message-flow.md b/docs/message-flow.md new file mode 100644 index 0000000..06a06a5 --- /dev/null +++ b/docs/message-flow.md @@ -0,0 +1,334 @@ +# Message Flow + +The message flow is an information bag. It is passed to `visitors` and `finalizers` so that they can add +information to it. + +## Nodes and Edges + +![Nodes and Edges](img/nodes_edges.png) + +The `Prooph\MessageFlowAnalyzer\MessageFlow` is organized as a graph of `Prooph\MessageFlowAnalyzer\MessageFlow\Node` objects +and those nodes reference each other through `Prooph\MessageFlowAnalyzer\MessageFlow\Èdge` objects. + +### Edge + +![Edge](img/edge.png) + +An edge simply takes a `sourceNodeId` and `targetNodeId` as arguments and is added to the message flow like this: + +```php +$abEdge = new Edge($nodeA->id(), $nodeB->id()); + +$messageFlow = $messageFlow->setEdge($abEdge); +``` + +*Note: The message flow is implemented as an immutable object. This means that all methods that change state of the flow return +a new version of the object. The old reference is not modified. As the analyzer works a lot with injected logic (custom visitors and finalizers) +an immutable object saves us from weired bugs caused by shared state. Even if it reduces performance of the analyzing process it is the better option. +The message flow analyzer is not meant to be used in production and does not need to run continuously. You want to run it only +after a new feature was added to a project or a refactoring was made.* + +### Node + +![Node](img/node.png) + +A node describes an element of the message flow. You can set various attributes to distinguish between different node types +and visualize them differently. Here is a list of all node attributes with a short description: + +```php +` or `without` methods. +Message flow node objects are immutable, too. Hence, those methods return new node instances instead of modifying the +original one. + +The built-in `class visitors` use a `Prooph\MessageFlowAnalyzer\MessageFlow\NodeFactory` to create nodes. +The factory is a proxy to the named constructors with an option to call a custom `Node` implementation. +This allows you to easily override default attributes. Again see `configuration` page for details. + +Here is an example of a `class visitor` that inspects a reflected php class and if it is a `prooph message` the visitor: + +- creates a `MessageFlow\Message` object +- passes it to the appropriate node factory method which calls the named constructor of the node class +- changes the default icon to another font awesome icon (see configuration page) +- and finally adds the node to the message flow + +```php +implementsInterface(ProophMsg::class)) { + + $msg = MessageFlow\Message::fromReflectionClass($reflectionClass); + + $msgNode = MessageFlow\NodeFactory::createMessageNode($msg); + $msgNode = $msgNode->withIcon(MessageFlow\NodeIcon::faSolid('fa-paper-plane')); + + //Use addNode if you don't want to override the node in case it is already set + //if (! $messageFlow->knowsNode($msgNode)) { + // $messageFlow = $messageFlow->addNode($msgNode); + // } + $messageFlow = $messageFlow->setNode($msgNode); + } + + return $messageFlow; + } +} +``` + +The visualization of that node will look something like that: + +![Paper Plane Node](img/paper-plane.png) + + diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..af00f65 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,112 @@ +# Troubleshooting + +## I don't get an output and no result + +Did you run the analyze command in very verbose mode? + +```php +php vendor/bin/prooph-analyzer project:analyze -vvv +``` + +If you don't do that and the php process has not enough memory available you don't see anything. In verbose mode +you get an appropriate message. + +## Interface or Class not found + +In some cases the underlying parser and reflection libs cannot find a class or interface. +For example in our demo app [proophessor-do](https://github.com/prooph/proophessor-do) we need to exclude classes that implement `Psr\Http\Server\MiddlewareInterface` interface. +For some reason the interface cannot be loaded. We will check the issue but something like that can always happen and +it would be bad if you cannot get a result just because of a weird bug. + +Blacklist filters to the rescue! You can exclude problematic files. Here is an example: + +```php +isDir()) { + return true; + } + foreach ($this->blacklist as $entry) { + if($fileInfo->getPathname() === $rootDir . DIRECTORY_SEPARATOR . $entry) { + return false; + } + } + return true; + } +} + +``` +## Missing nodes or edges + +You're missing a node or edge? Maybe the default visitors are not able to scan your implementation correctly. +You can use the prooph components in many different ways and we cannot prepare the visitors for every situation. +But you can write your own `visitor`. Just look at the existing implementations. It is actually a lot of fun to write +those visitors. + +Another option is to use a finalizer and add missing nodes and/or edges by hand. This is useful in case you want to add +infrastructure as a node and connect it with message flow nodes. + +A simple example. You add the event store as a node and add an edge for every found event: + +```php + Util::codeIdentifierToNodeId('prooph-event-store'), + 'type' => 'event-store', //we can use custom types! + 'name' => 'prooph Event Store', + //Cast icon to string, bc fromArray expects the serialized version of an icon + 'icon' => (string)MessageFlow\NodeIcon::faSolid('fa-database'), + 'color' => '#ED6842' //prooph event store orange ;) + ]); + + $messageFlow = $messageFlow->addNode($esNode); + + foreach ($messageFlow->nodes() as $node) { + if($node->type() === MessageFlow\Node::TYPE_EVENT) { + //Add a new edge with event node id being the source + //and event store being the target + $messageFlow = $messageFlow->setEdge( + new MessageFlow\Edge( + $node->id(), + $esNode->id() + ) + ); + } + } + + return $messageFlow; + } +} +``` diff --git a/src/Cli/AnalyzeProjectCommand.php b/src/Cli/AnalyzeProjectCommand.php index c371ba6..2734aad 100644 --- a/src/Cli/AnalyzeProjectCommand.php +++ b/src/Cli/AnalyzeProjectCommand.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,7 +12,12 @@ namespace Prooph\MessageFlowAnalyzer\Cli; +use Prooph\MessageFlowAnalyzer\Helper\MessageClassProvider; use Prooph\MessageFlowAnalyzer\Helper\ProjectTraverserFactory; +use Prooph\MessageFlowAnalyzer\MessageFlow\EventRecorder; +use Prooph\MessageFlowAnalyzer\MessageFlow\NodeFactory; +use Prooph\MessageFlowAnalyzer\Visitor\MessagingCollector; +use Roave\BetterReflection\Reflection\ReflectionClass; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -82,11 +87,35 @@ public function execute(InputInterface $input, OutputInterface $output) $targetFile = $input->getOption('output'); $formatterName = $input->getOption('format'); + if (isset($config['nodeClass'])) { + NodeFactory::useNodeClass($config['nodeClass']); + } + + if (isset($config['eventRecorderCheck'])) { + EventRecorder::useEventRecorderCheckFunction($config['eventRecorderCheck']); + } + + if (isset($config['messageClassProvider'])) { + $messageClassProvider = ReflectionClass::createFromName($config['messageClassProvider']); + + if (! $messageClassProvider->implementsInterface(MessageClassProvider::class)) { + throw new \RuntimeException("Message factory factory {$messageClassProvider->getName()} does not implement " . MessageClassProvider::class); + } + + $messageClassProvider = $messageClassProvider->getName(); + MessagingCollector::useMessageClassProvider(new $messageClassProvider()); + } + $traverser = ProjectTraverserFactory::buildTraverserFromConfig($config); + $finalizers = ProjectTraverserFactory::buildFinalizersFromConfig($config); $formatter = ProjectTraverserFactory::buildOutputFormatter($formatterName); $msgFlow = $traverser->traverse($rootDir); + foreach ($finalizers as $finalizer) { + $msgFlow = $finalizer->finalize($msgFlow); + } + file_put_contents($targetFile, $formatter->messageFlowToString($msgFlow)); $output->writeln('Analysis written to '.$targetFile.' using format: ' . $formatterName); diff --git a/src/Filter/ExcludeHiddenFileInfo.php b/src/Filter/ExcludeHiddenFileInfo.php index 43732a5..98f1a5f 100644 --- a/src/Filter/ExcludeHiddenFileInfo.php +++ b/src/Filter/ExcludeHiddenFileInfo.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Filter/ExcludeTestsDir.php b/src/Filter/ExcludeTestsDir.php index 29c4c6f..8f84699 100644 --- a/src/Filter/ExcludeTestsDir.php +++ b/src/Filter/ExcludeTestsDir.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Filter/ExcludeVendorDir.php b/src/Filter/ExcludeVendorDir.php index 8b1f35d..a4c0292 100644 --- a/src/Filter/ExcludeVendorDir.php +++ b/src/Filter/ExcludeVendorDir.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Filter/FileInfoFilter.php b/src/Filter/FileInfoFilter.php index 7ae7891..5513763 100644 --- a/src/Filter/FileInfoFilter.php +++ b/src/Filter/FileInfoFilter.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Filter/IncludePHPFile.php b/src/Filter/IncludePHPFile.php index 018200c..dd94416 100644 --- a/src/Filter/IncludePHPFile.php +++ b/src/Filter/IncludePHPFile.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Helper/MessageClassProvider.php b/src/Helper/MessageClassProvider.php new file mode 100644 index 0000000..490cde5 --- /dev/null +++ b/src/Helper/MessageClassProvider.php @@ -0,0 +1,18 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\Helper; + +interface MessageClassProvider +{ + public function provideClass(string $messageName): string; +} diff --git a/src/Helper/MessageNameEqualsClassProvider.php b/src/Helper/MessageNameEqualsClassProvider.php new file mode 100644 index 0000000..f0a4190 --- /dev/null +++ b/src/Helper/MessageNameEqualsClassProvider.php @@ -0,0 +1,21 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\Helper; + +class MessageNameEqualsClassProvider implements MessageClassProvider +{ + public function provideClass(string $messageName): string + { + return $messageName; + } +} diff --git a/src/Helper/MessageProducingMethodScanner.php b/src/Helper/MessageProducingMethodScanner.php index 459f3ee..9d33a93 100644 --- a/src/Helper/MessageProducingMethodScanner.php +++ b/src/Helper/MessageProducingMethodScanner.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -28,16 +28,23 @@ trait MessageProducingMethodScanner private $nodeTraverser; private function checkMessageProduction( + MessageFlow $msgFlow, ReflectionClass $reflectionClass, - callable $addMethodToMessageCb, - MessageFlow $msgFlow): MessageFlow - { + callable $onMessageProducingMethodCb, + callable $onNonMessageProducingMethodCb = null, + MessageClassProvider $messageClassProvider = null, + array $messageFactoryProperties = [] + ): MessageFlow { foreach ($reflectionClass->getMethods() as $method) { - $messages = $this->checkMethodProducesMessages($method); - foreach ($messages as $message) { - $message = $msgFlow->getMessage($message->name(), $message); - $message = $addMethodToMessageCb($message, $method); - $msgFlow = $msgFlow->setMessage($message); + $messages = $this->checkMethodProducesMessages($method, $messageClassProvider, $messageFactoryProperties); + + if (count($messages)) { + foreach ($messages as $message) { + $message = $msgFlow->getMessage($message->name(), $message); + $msgFlow = $onMessageProducingMethodCb($msgFlow, $message, $method); + } + } elseif ($onNonMessageProducingMethodCb) { + $msgFlow = $onNonMessageProducingMethodCb($msgFlow, $method); } } @@ -46,9 +53,11 @@ private function checkMessageProduction( /** * @param ReflectionMethod $method - * @return Message[]|null + * @param MessageClassProvider|null $messageClassProvider + * @param array $messageFactoryProperties + * @return array */ - private function checkMethodProducesMessages(ReflectionMethod $method): array + private function checkMethodProducesMessages(ReflectionMethod $method, MessageClassProvider $messageClassProvider = null, array $messageFactoryProperties = []): array { try { $bodyAst = $method->getBodyAst(); @@ -56,17 +65,15 @@ private function checkMethodProducesMessages(ReflectionMethod $method): array return []; } - $this->getTraverser()->traverse($bodyAst); + $traverser = $this->getTraverser($messageClassProvider, $messageFactoryProperties); + + $traverser->traverse($bodyAst); - return $this->getTraverser()->messageScanner()->popFoundMessages(); + return $traverser->messageScanner()->popFoundMessages(); } - private function getTraverser(): MessageScanningNodeTraverser + private function getTraverser(MessageClassProvider $messageClassProvider = null, array $messageFactoryProperties = []): MessageScanningNodeTraverser { - if (null === $this->nodeTraverser) { - $this->nodeTraverser = new MessageScanningNodeTraverser(new NodeTraverser(), new MessageScanner()); - } - - return $this->nodeTraverser; + return new MessageScanningNodeTraverser(new NodeTraverser(), new MessageScanner($messageClassProvider, $messageFactoryProperties)); } } diff --git a/src/Helper/PhpParser/MessageScanner.php b/src/Helper/PhpParser/MessageScanner.php index 14363f4..3c43a58 100644 --- a/src/Helper/PhpParser/MessageScanner.php +++ b/src/Helper/PhpParser/MessageScanner.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,6 +15,7 @@ use PhpParser\Node; use PhpParser\NodeVisitorAbstract; use Prooph\Common\Messaging\Message as ProophMsg; +use Prooph\MessageFlowAnalyzer\Helper\MessageClassProvider; use Prooph\MessageFlowAnalyzer\MessageFlow\Message; use Roave\BetterReflection\Reflection\ReflectionClass; use Roave\BetterReflection\Reflection\ReflectionMethod; @@ -23,6 +24,27 @@ final class MessageScanner extends NodeVisitorAbstract { private $messages = []; + /** + * @var MessageClassProvider + */ + private $messageClassProvider; + + /** + * @var ReflectionClass[] indexed by property name + */ + private $messageFactoryProperties; + + /** + * MessageScanner constructor. + * @param MessageClassProvider $messageClassProvider + * @param ReflectionClass[] $messageFactoryProperties indexed by property name + */ + public function __construct(MessageClassProvider $messageClassProvider = null, array $messageFactoryProperties = []) + { + $this->messageClassProvider = $messageClassProvider; + $this->messageFactoryProperties = $messageFactoryProperties; + } + public function leaveNode(Node $node) { if ($node instanceof Node\Expr\StaticCall) { @@ -55,6 +77,41 @@ public function leaveNode(Node $node) $this->messages[] = Message::fromReflectionClass($reflectionClass); } } + + if (! $this->messageClassProvider) { + return; + } + + if (! $node instanceof Node\Expr\MethodCall) { + return; + } + + $messageName = null; + + if (isset($node->var->name) && is_string($node->var->name) && isset($this->messageFactoryProperties[$node->var->name]) + && isset($node->args[0])) { + $messageNameArg = $node->args[0]; + + if ($messageNameArg->value instanceof Node\Expr\ClassConstFetch && $messageNameArg->value->class instanceof Node\Name\FullyQualified) { + if (mb_strtolower($messageNameArg->value->name) === 'class') { + $messageName = (string) $messageNameArg->value->class; + } else { + $messageName = ReflectionClass::createFromName((string) $messageNameArg->value->class) + ->getConstant($messageNameArg->value->name); + } + } + } + + if (null === $messageName) { + return; + } + + $reflectionClass = ReflectionClass::createFromName($this->messageClassProvider->provideClass($messageName)); + + if ($reflectionClass->implementsInterface(ProophMsg::class) + && Message::isRealMessage($reflectionClass)) { + $this->messages[] = Message::fromReflectionClass($reflectionClass); + } } /** diff --git a/src/Helper/PhpParser/MessageScanningNodeTraverser.php b/src/Helper/PhpParser/MessageScanningNodeTraverser.php index 0bcca18..8378a35 100644 --- a/src/Helper/PhpParser/MessageScanningNodeTraverser.php +++ b/src/Helper/PhpParser/MessageScanningNodeTraverser.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Helper/PhpParser/ScanHelper.php b/src/Helper/PhpParser/ScanHelper.php index c4b0ea6..4cb6a01 100644 --- a/src/Helper/PhpParser/ScanHelper.php +++ b/src/Helper/PhpParser/ScanHelper.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,11 +15,17 @@ use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; +use Prooph\Common\Messaging\Message as ProophMsg; +use Prooph\Common\Messaging\MessageFactory; +use Prooph\MessageFlowAnalyzer\MessageFlow; use Prooph\MessageFlowAnalyzer\MessageFlow\EventRecorder; +use Prooph\MessageFlowAnalyzer\MessageFlow\Message; use Prooph\MessageFlowAnalyzer\MessageFlow\MessageHandler; use Roave\BetterReflection\Reflection\ReflectionClass; use Roave\BetterReflection\Reflection\ReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionParameter; +use Roave\BetterReflection\Reflection\ReflectionType; +use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; class ScanHelper { @@ -99,6 +105,29 @@ public function getEventRecorders(): array return $nodeVisitor->getEventRecorders(); } + public static function findMessageFactoryProperties(ReflectionClass $reflectionClass): array + { + if (! $reflectionClass->hasMethod('__construct')) { + return []; + } + + $constructor = $reflectionClass->getMethod('__construct'); + + $properties = []; + + foreach ($constructor->getParameters() as $parameter) { + if ($messageFactory = self::isMessageFactoryParameter($parameter)) { + $propertyName = self::getPropertyNameForParameter($constructor, $parameter->getName()); + + if ($propertyName) { + $properties[$propertyName] = $messageFactory; + } + } + } + + return $properties; + } + /** * Returns array of reflected event recorder classes with keys being the associated property names of the recorder repositories * @@ -111,7 +140,6 @@ public static function findEventRecorderRepositoryProperties(ReflectionClass $re return []; } - //@TODO Test: parent::__construct but should work, too! $constructor = $reflectionClass->getMethod('__construct'); $properties = []; @@ -138,13 +166,15 @@ public static function findEventRecorderRepositoryProperties(ReflectionClass $re */ public static function findEventRecorderVariables(ReflectionMethod $method, array $eventRecorderRepositoryProperties): array { - $nodeVisitor = new class($eventRecorderRepositoryProperties) extends NodeVisitorAbstract { + $nodeVisitor = new class($eventRecorderRepositoryProperties, $method) extends NodeVisitorAbstract { private $eventRecorderRepositoryProperties; private $eventRecorderVariables = []; + private $handlerMethod; - public function __construct(array $eventRecorderRepositoryProperties) + public function __construct(array $eventRecorderRepositoryProperties, ReflectionMethod $handlerMethod) { $this->eventRecorderRepositoryProperties = $eventRecorderRepositoryProperties; + $this->handlerMethod = $handlerMethod; } public function leaveNode(Node $node) @@ -154,20 +184,26 @@ public function leaveNode(Node $node) return; } - if (! $node->expr->var instanceof Node\Expr\PropertyFetch) { - return; - } + if ($node->expr->var instanceof Node\Expr\PropertyFetch) { + /** @var Node\Expr\PropertyFetch $propertyFetch */ + $propertyFetch = $node->expr->var; - /** @var Node\Expr\PropertyFetch $propertyFetch */ - $propertyFetch = $node->expr->var; + if (! $propertyFetch->var instanceof Node\Expr\Variable || $propertyFetch->var->name !== 'this') { + return; + } - if (! $propertyFetch->var instanceof Node\Expr\Variable || $propertyFetch->var->name !== 'this') { - return; - } + if (array_key_exists($propertyFetch->name, $this->eventRecorderRepositoryProperties)) { + $eventRecorder = $this->eventRecorderRepositoryProperties[$propertyFetch->name]; + $this->eventRecorderVariables[$node->var->name] = $eventRecorder; + } + } elseif ($node->expr->var instanceof Node\Expr\Variable && $node->expr->var->name === 'this' + && $this->handlerMethod->getImplementingClass()->hasMethod($node->expr->name) + && $this->handlerMethod->getImplementingClass()->getMethod($node->expr->name)->hasReturnType()) { + $returnType = $this->handlerMethod->getImplementingClass()->getMethod($node->expr->name)->getReturnType(); - if (array_key_exists($propertyFetch->name, $this->eventRecorderRepositoryProperties)) { - $eventRecorder = $this->eventRecorderRepositoryProperties[$propertyFetch->name]; - $this->eventRecorderVariables[$node->var->name] = $eventRecorder; + if ($eventRecorder = ScanHelper::isEventRecorderReturnType($returnType)) { + $this->eventRecorderVariables[$node->var->name] = $eventRecorder; + } } } } @@ -185,7 +221,196 @@ public function getEventRecorderVariables(): array return $nodeVisitor->getEventRecorderVariables(); } - private static function isEventRecorderRepositoryParameter(ReflectionParameter $parameter, bool $inspectChildParameters = true): ?ReflectionClass + /** + * @param EventRecorder $eventRecorder + * @return EventRecorder[]|null + */ + public static function checkIfEventRecorderMethodCallsOtherEventRecorders(EventRecorder $eventRecorder): ?array + { + if (! $eventRecorder->isClass()) { + return []; + } + + $method = $eventRecorder->toFunctionLike(); + + $nodeVisitor = new class($eventRecorder->class(), $method) extends NodeVisitorAbstract { + private $recorderClass; + private $method; + private $eventRecorders; + private $nodeTraverser; + + public function __construct(string $recorderClass, ReflectionMethod $method) + { + $this->recorderClass = ReflectionClass::createFromName($recorderClass); + $this->method = $method; + } + + public function leaveNode(Node $node) + { + if ($node instanceof Node\Expr\MethodCall && is_string($node->name) && $this->recorderClass->hasMethod($node->name)) { + $calledMethod = $this->recorderClass->getMethod($node->name); + + $producedMsgs = $this->checkMethodProducesMessages($calledMethod); + + if (count($producedMsgs)) { + $this->eventRecorders[] = EventRecorder::fromReflectionMethod($calledMethod); + } + } + } + + /** + * @return EventRecorder[]|null + */ + public function getEventRecorders(): ?array + { + return $this->eventRecorders; + } + + /** + * @param ReflectionMethod $method + * @return Message[]|null + */ + private function checkMethodProducesMessages(ReflectionMethod $method): array + { + try { + $bodyAst = $method->getBodyAst(); + } catch (\TypeError $error) { + return []; + } + + $this->getTraverser()->traverse($bodyAst); + + return $this->getTraverser()->messageScanner()->popFoundMessages(); + } + + private function getTraverser(): MessageScanningNodeTraverser + { + if (null === $this->nodeTraverser) { + $this->nodeTraverser = new MessageScanningNodeTraverser(new NodeTraverser(), new MessageScanner()); + } + + return $this->nodeTraverser; + } + }; + + $traverser = new NodeTraverser(); + $traverser->addVisitor($nodeVisitor); + $traverser->traverse($method->getBodyAst()); + + return $nodeVisitor->getEventRecorders(); + } + + public static function checkIfEventRecorderMethodIsUsedAsFactory(EventRecorder $eventRecorder): ?EventRecorder + { + $method = $eventRecorder->toFunctionLike(); + + if (! $method->hasReturnType()) { + return null; + } + + $returnType = $method->getReturnType(); + + if ($returnType->isBuiltin()) { + return null; + } + + $reflectedReturnType = ReflectionClass::createFromName((string) $returnType); + + if (! EventRecorder::isEventRecorder($reflectedReturnType)) { + return null; + } + + $nodeVisitor = new class($reflectedReturnType) extends NodeVisitorAbstract { + private $reflectedReturnType; + private $eventRecorder; + + public function __construct(ReflectionClass $reflectedReturnType) + { + $this->reflectedReturnType = $reflectedReturnType; + } + + public function leaveNode(Node $node) + { + if ($node instanceof Node\Expr\StaticCall) { + if ($node->class instanceof Node\Name\FullyQualified + && $this->reflectedReturnType->getName() === $node->class->toString()) { + $reflectionClass = ReflectionClass::createFromName($node->class->toString()); + + if (! EventRecorder::isEventRecorder($reflectionClass)) { + return; + } + + $reflectionMethod = ReflectionMethod::createFromName($node->class->toString(), $node->name); + $this->eventRecorder = EventRecorder::fromReflectionMethod($reflectionMethod); + } + } + } + + public function getEventRecorder(): ?EventRecorder + { + return $this->eventRecorder; + } + }; + + $traverser = new NodeTraverser(); + $traverser->addVisitor($nodeVisitor); + $traverser->traverse($method->getBodyAst()); + + return $nodeVisitor->getEventRecorder(); + } + + public static function checkIfMethodHandlesMessage(MessageFlow $messageFlow, ReflectionMethod $method): ?Message + { + $parameters = $method->getParameters(); + + //command handler, event listener -> func($msg): void {}, query handler/finder -> func($msg, $deferred): void {} + if (count($parameters) === 0 || count($parameters) > 2) { + return null; + } + + $parameter = $method->getParameters()[0]; + + if (! $parameter->hasType()) { + return null; + } + + $parameterType = $parameter->getType(); + + if ($parameterType->isBuiltin()) { + return null; + } + + $reflectionClass = ReflectionClass::createFromName((string) $parameterType); + + if (! $reflectionClass->implementsInterface(ProophMsg::class)) { + return null; + } + + if (! MessageFlow\Message::isRealMessage($reflectionClass)) { + return null; + } + + $message = MessageFlow\Message::fromReflectionClass($reflectionClass); + + return $messageFlow->getMessage($message->name(), $message); + } + + public static function isEventRecorderReturnType(ReflectionType $returnType): ?ReflectionClass + { + if ($returnType->isBuiltin()) { + return null; + } + + $eventRecorder = ReflectionClass::createFromName((string) $returnType); + + if (EventRecorder::isEventRecorder($eventRecorder)) { + return $eventRecorder; + } + + return null; + } + + public static function isEventRecorderRepositoryParameter(ReflectionParameter $parameter, bool $inspectChildParameters = true): ?ReflectionClass { if (! $parameter->hasType()) { return null; @@ -215,7 +440,34 @@ private static function isEventRecorderRepositoryParameter(ReflectionParameter $ return null; } - private static function getPropertyNameForParameter(ReflectionMethod $method, string $parameterName): ?string + public static function isMessageFactoryParameter(ReflectionParameter $parameter): ?ReflectionClass + { + if (! $parameter->hasType()) { + return null; + } + + if ($parameter->getType()->isBuiltin()) { + return null; + } + + try { + $paramReflectionClass = ReflectionClass::createFromName((string) $parameter->getType()); + } catch (IdentifierNotFound $exception) { + return null; + } + + if ($paramReflectionClass->getName() === MessageFactory::class) { + return $paramReflectionClass; + } + + if ($paramReflectionClass->implementsInterface(MessageFactory::class)) { + return $paramReflectionClass; + } + + return null; + } + + public static function getPropertyNameForParameter(ReflectionMethod $method, string $parameterName): ?string { $nodeVisitor = new class($parameterName) extends NodeVisitorAbstract { private $parameterName; diff --git a/src/Helper/ProjectTraverserFactory.php b/src/Helper/ProjectTraverserFactory.php index 85846c7..0e774ca 100644 --- a/src/Helper/ProjectTraverserFactory.php +++ b/src/Helper/ProjectTraverserFactory.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,16 +16,16 @@ use Prooph\MessageFlowAnalyzer\Filter\ExcludeTestsDir; use Prooph\MessageFlowAnalyzer\Filter\ExcludeVendorDir; use Prooph\MessageFlowAnalyzer\Filter\IncludePHPFile; +use Prooph\MessageFlowAnalyzer\MessageFlow\Finalizer; use Prooph\MessageFlowAnalyzer\Output\Formatter; -use Prooph\MessageFlowAnalyzer\Output\JsonArangoGraphNodes; -use Prooph\MessageFlowAnalyzer\Output\JsonCytoscapeElements; use Prooph\MessageFlowAnalyzer\Output\JsonPrettyPrint; use Prooph\MessageFlowAnalyzer\ProjectTraverser; -use Prooph\MessageFlowAnalyzer\Visitor\EventRecorderCollector; -use Prooph\MessageFlowAnalyzer\Visitor\EventRecorderInvokerCollector; +use Prooph\MessageFlowAnalyzer\Visitor\AggregateMethodCollector; +use Prooph\MessageFlowAnalyzer\Visitor\CommandHandlerCollector; +use Prooph\MessageFlowAnalyzer\Visitor\EventListenerCollector; use Prooph\MessageFlowAnalyzer\Visitor\MessageCollector; -use Prooph\MessageFlowAnalyzer\Visitor\MessageHandlerCollector; use Prooph\MessageFlowAnalyzer\Visitor\MessageProducerCollector; +use Prooph\MessageFlowAnalyzer\Visitor\MessagingCollector; final class ProjectTraverserFactory { @@ -38,18 +38,19 @@ final class ProjectTraverserFactory public static $classVisitorAliases = [ 'MessageCollector' => MessageCollector::class, - 'MessageHandlerCollector' => MessageHandlerCollector::class, + 'CommandHandlerCollector' => CommandHandlerCollector::class, 'MessageProducerCollector' => MessageProducerCollector::class, - 'EventRecorderCollector' => EventRecorderCollector::class, - 'EventRecorderInvokerCollector' => EventRecorderInvokerCollector::class, + 'AggregateMethodCollector' => AggregateMethodCollector::class, + 'EventListenerCollector' => EventListenerCollector::class, + 'MessagingCollector' => MessagingCollector::class, ]; public static $fileInfoVisitorAliases = []; + public static $finalizerAliases = []; + public static $outputFormatterAliases = [ 'JsonPrettyPrint' => JsonPrettyPrint::class, - 'JsonArangoGraphNodes' => JsonArangoGraphNodes::class, - 'JsonCytoscapeElements' => JsonCytoscapeElements::class, ]; public static function buildTraverserFromConfig(array $config): ProjectTraverser @@ -78,6 +79,26 @@ public static function buildTraverserFromConfig(array $config): ProjectTraverser return $traverser; } + /** + * @param array $config + * @return Finalizer[] + */ + public static function buildFinalizersFromConfig(array $config): array + { + $finalizers = []; + + foreach ($config['finalizers'] ?? [] as $finalizerClass) { + $finalizerClass = self::$finalizerAliases[$finalizerClass] ?? $finalizerClass; + $f = new $finalizerClass(); + if (! $f instanceof Finalizer) { + throw new \InvalidArgumentException("Invalid finalizer: Finalizer interface not implemented by $finalizerClass"); + } + $finalizers[] = $f; + } + + return $finalizers; + } + public static function buildOutputFormatter(string $nameOrClass): Formatter { $nameOrClass = self::$outputFormatterAliases[$nameOrClass] ?? $nameOrClass; diff --git a/src/Helper/Util.php b/src/Helper/Util.php index 6e01100..89e80b9 100644 --- a/src/Helper/Util.php +++ b/src/Helper/Util.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -39,12 +39,12 @@ public static function withoutNamespace(string $class): string return array_pop($parts); } - public static function identifierToKey(string $identifier): string + public static function codeIdentifierToNodeId(string $identifier): string { return sha1($identifier); } - public static function identifierWithoutMethod(string $identifier): string + public static function codeIdentifierWithoutMethod(string $identifier): string { $parts = explode(MessageHandlingMethodAbstract::ID_METHOD_DELIMITER, $identifier); diff --git a/src/MessageFlow.php b/src/MessageFlow.php index 8c3af5d..3da4038 100644 --- a/src/MessageFlow.php +++ b/src/MessageFlow.php @@ -1,10 +1,11 @@ - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,9 +14,11 @@ namespace Prooph\MessageFlowAnalyzer; use Prooph\Common\Messaging\Message as ProophMsg; -use Prooph\Common\Messaging\MessageDataAssertion; -use Prooph\MessageFlowAnalyzer\MessageFlow\EventRecorderInvoker; +use Prooph\MessageFlowAnalyzer\Helper\Util; +use Prooph\MessageFlowAnalyzer\MessageFlow\Edge; use Prooph\MessageFlowAnalyzer\MessageFlow\Message; +use Prooph\MessageFlowAnalyzer\MessageFlow\Node; +use Prooph\MessageFlowAnalyzer\MessageFlow\NodeFactory; final class MessageFlow { @@ -30,34 +33,27 @@ final class MessageFlow private $rootDir; /** - * @var Message[] + * @var Node[] */ - private $messages; + private $nodes = []; /** - * @var EventRecorderInvoker[] - */ - private $eventRecorderInvokers; - - /** - * @var array + * @var Edge[] */ - private $attributes; + private $edges = []; /** - * Internal cache which is reset when a new command message is set + * Internal command handler cache * - * @var string[] + * Is reset when a new command is added + * + * @var array */ private $commandHandlers; public static function newFlow(string $project, string $rootDir): self { - return new self($project, $rootDir, [ - 'messages' => [], - 'eventRecorderInvokers' => [], - 'attributes' => [], - ]); + return new self($project, $rootDir); } public static function fromArray(array $flowData): self @@ -65,11 +61,12 @@ public static function fromArray(array $flowData): self return new self( $flowData['project'] ?? '', $flowData['rootDir'] ?? '', - self::flowFromArray($flowData['flow'] ?? []) + $flowData['nodes'] ?? [], + $flowData['edges'] ?? [] ); } - private function __construct(string $project, string $rootDir, array $flow) + private function __construct(string $project, string $rootDir, array $nodes = [], array $edges = []) { if (mb_strlen($project) === 0) { throw new \InvalidArgumentException('Project name must not be empty.'); @@ -80,9 +77,8 @@ private function __construct(string $project, string $rootDir, array $flow) } $this->project = $project; $this->rootDir = $rootDir; - $this->messages = $flow['messages'] ?? []; - $this->eventRecorderInvokers = $flow['eventRecorderInvokers'] ?? []; - $this->attributes = $flow['attributes'] ?? []; + $this->nodes = $nodes; + $this->edges = $edges; } /** @@ -101,18 +97,35 @@ public function rootDir(): string return $this->rootDir; } - public function knowsMessage(string $messageName): bool + /** + * @return Node[] indexed by node id + */ + public function nodes(): array { - return array_key_exists($messageName, $this->messages); + return $this->nodes; } - public function getMessage(string $name, Message $efault = null): ?Message + /** + * @return Edge[] indexed by edge id + */ + public function edges(): array { - if (! array_key_exists($name, $this->messages())) { - return $efault; + return $this->edges; + } + + public function knowsMessage(Message $message): bool + { + return array_key_exists(Util::codeIdentifierToNodeId($message->name()), $this->nodes); + } + + public function getMessage(string $name, Message $default = null): ?Message + { + $msgId = Util::codeIdentifierToNodeId($name); + if (! array_key_exists($msgId, $this->nodes)) { + return $default; } - return $this->messages[$name]; + return Message::fromNode($this->nodes[$msgId]); } /** @@ -120,75 +133,110 @@ public function getMessage(string $name, Message $efault = null): ?Message */ public function messages(): array { - return $this->messages; + return array_map(function (Node $node): Message { + return Message::fromNode($node); + }, array_filter($this->nodes, function (Node $node) { + return in_array($node->type(), Node::MESSAGE_TYPES); + })); } - /** - * @return EventRecorderInvoker[] - */ - public function eventRecorderInvokers(): array + public function addMessage(Message $msg): self { - return $this->eventRecorderInvokers; + if ($this->knowsMessage($msg)) { + throw new \RuntimeException('Message is already known. Got ' . $msg->name()); + } + + return $this->setMessage($msg); } - public function setEventRecorderInvoker(EventRecorderInvoker $invoker): self + public function setMessage(Message $msg): self { $cp = clone $this; - $cp->eventRecorderInvokers[$invoker->identifier()] = $invoker; + $node = NodeFactory::createMessageNode($msg); + $cp->nodes[$node->id()] = $node; + + if ($msg->type() === ProophMsg::TYPE_COMMAND) { + $cp->commandHandlers = null; + } return $cp; } - public function attributes(): array + public function knowsNode(Node $node): bool { - return $this->attributes; + return $this->knowsNodeWithId($node->id()); } - public function getAttribute(string $name, $default = null) + public function knowsNodeWithId(string $nodeId): bool { - if (array_key_exists($name, $this->attributes)) { - return $this->attributes[$name]; - } - - return $default; + return array_key_exists($nodeId, $this->nodes); } - public function setAttribute($name, $value): self + public function addNode(Node $node): self { - try { - MessageDataAssertion::assertPayload(['value' => $value]); - } catch (\Throwable $error) { - throw new \InvalidArgumentException('Attribute value should be of type and contain only scalar, NULL or array. Got ' . $error->getMessage()); + if ($this->knowsNode($node)) { + throw new \RuntimeException("Node with id {$node->id()} is already set. Got " . json_encode($node->toArray())); } + return $this->setNode($node); + } + + public function setNode(Node $node): self + { $cp = clone $this; - $cp->attributes[$name] = $value; + $cp->nodes[$node->id()] = $node; return $cp; } - public function addMessage(Message $msg): self + public function getNode(string $nodeId, Node $default = null): ?Node { - if ($this->knowsMessage($msg->name())) { - throw new \RuntimeException('Message is already known. Got ' . $msg->name()); + if (! $this->knowsNodeWithId($nodeId)) { + return $default; } - return $this->setMessage($msg); + return $this->nodes[$nodeId]; } - public function setMessage(Message $msg): self + public function removeNode(string $nodeId): self { + if (! $this->knowsNodeWithId($nodeId)) { + return $this; + } + $cp = clone $this; - $cp->messages[$msg->name()] = $msg; + unset($cp->nodes[$nodeId]); - //Reset internal cmd handler cache - if ($msg->type() === ProophMsg::TYPE_COMMAND) { - $cp->commandHandlers = null; + foreach ($this->edges as $edge) { + if ($edge->sourceNodeId() === $nodeId || $edge->targetNodeId() === $nodeId) { + $cp = $cp->removeEdge($edge); + } } return $cp; } + public function setEdge(Edge $edge): self + { + $cp = clone $this; + $cp->edges[$edge->id()] = $edge; + + return $cp; + } + + public function removeEdge(Edge $edge): self + { + if (! isset($this->edges[$edge->id()])) { + return $this; + } + + $cp = clone $this; + + unset($cp->edges[$edge->id()]); + + return $cp; + } + /** * Returns a list of class and/or function names of command handlers * @@ -199,13 +247,9 @@ public function getKnownCommandHandlers(): array if (null === $this->commandHandlers) { $this->commandHandlers = []; - foreach ($this->messages() as $message) { - if ($message->type() !== ProophMsg::TYPE_COMMAND) { - continue; - } - - foreach ($message->handlers() as $handler) { - $this->commandHandlers[] = $handler->isClass() ? $handler->class() : $handler->function(); + foreach ($this->nodes as $node) { + if ($node->type() === Node::TYPE_HANDLER) { + $this->commandHandlers[] = $node->class() ? $node->class() : $node->funcName(); } } } @@ -218,7 +262,12 @@ public function toArray(): array return [ 'project' => $this->project, 'rootDir' => $this->rootDir, - 'flow' => $this->flowToArray(), + 'nodes' => array_values(array_map(function (Node $node): array { + return $node->toArray(); + }, $this->nodes)), + 'edges' => array_values(array_map(function (Edge $edge): array { + return $edge->toArray(); + }, $this->edges)), ]; } @@ -235,38 +284,4 @@ public function __toString(): string { return json_encode($this->toArray()); } - - private static function flowFromArray(array $flow): array - { - return [ - 'messages' => array_map(function ($msg): Message { - if ($msg instanceof Message) { - return $msg; - } - - return Message::fromArray($msg); - }, $flow['messages'] ?? []), - 'eventRecorderInvokers' => array_map(function ($invoker): EventRecorderInvoker { - if ($invoker instanceof EventRecorderInvoker) { - return $invoker; - } - - return EventRecorderInvoker::fromArray($invoker); - }, $flow['eventRecorderInvoker'] ?? []), - 'attributes' => $flow['attributes'], - ]; - } - - private function flowToArray(): array - { - return [ - 'messages' => array_map(function (Message $msg): array { - return $msg->toArray(); - }, $this->messages), - 'eventRecorderInvokers' => array_map(function (EventRecorderInvoker $invoker): array { - return $invoker->toArray(); - }, $this->eventRecorderInvokers), - 'attributes' => $this->attributes, - ]; - } } diff --git a/src/MessageFlow/Edge.php b/src/MessageFlow/Edge.php new file mode 100644 index 0000000..fb15405 --- /dev/null +++ b/src/MessageFlow/Edge.php @@ -0,0 +1,64 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\MessageFlow; + +class Edge +{ + /** + * @var string + */ + private $sourceNodeId; + + /** + * @var string + */ + private $targetNodeId; + + public function __construct(string $sourceNodeId, string $targetNodeId) + { + $this->sourceNodeId = $sourceNodeId; + $this->targetNodeId = $targetNodeId; + } + + public function id(): string + { + return $this->sourceNodeId . '_' . $this->targetNodeId; + } + + /** + * @return string + */ + public function sourceNodeId(): string + { + return $this->sourceNodeId; + } + + /** + * @return string + */ + public function targetNodeId(): string + { + return $this->targetNodeId; + } + + public function toArray(): array + { + return [ + 'data' => [ + 'id' => $this->id(), + 'source' => $this->sourceNodeId, + 'target' => $this->targetNodeId, + ], + ]; + } +} diff --git a/src/MessageFlow/EventRecorder.php b/src/MessageFlow/EventRecorder.php index a81823d..1e768a5 100644 --- a/src/MessageFlow/EventRecorder.php +++ b/src/MessageFlow/EventRecorder.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -19,7 +19,21 @@ final class EventRecorder extends MessageHandlingMethodAbstract { + private static $isRecorderCb = [EventRecorder::class, 'isProophEventRecorder']; + + public static function useEventRecorderCheckFunction(callable $eventRecorderCheck): void + { + self::$isRecorderCb = $eventRecorderCheck; + } + public static function isEventRecorder(ReflectionClass $reflectionClass): bool + { + $cb = self::$isRecorderCb; + + return $cb($reflectionClass); + } + + public static function isProophEventRecorder(ReflectionClass $reflectionClass): bool { if ($reflectionClass->isSubclassOf(AggregateRoot::class)) { return true; diff --git a/src/MessageFlow/EventRecorderInvoker.php b/src/MessageFlow/EventRecorderInvoker.php index d06da22..c5f56a0 100644 --- a/src/MessageFlow/EventRecorderInvoker.php +++ b/src/MessageFlow/EventRecorderInvoker.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/MessageFlow/Finalizer.php b/src/MessageFlow/Finalizer.php new file mode 100644 index 0000000..fbe027b --- /dev/null +++ b/src/MessageFlow/Finalizer.php @@ -0,0 +1,20 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\MessageFlow; + +use Prooph\MessageFlowAnalyzer\MessageFlow; + +interface Finalizer +{ + public function finalize(MessageFlow $messageFlow): MessageFlow; +} diff --git a/src/MessageFlow/Message.php b/src/MessageFlow/Message.php index a923563..509a0bd 100644 --- a/src/MessageFlow/Message.php +++ b/src/MessageFlow/Message.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -40,21 +40,6 @@ final class Message */ private $filename; - /** - * @var MessageHandler[] - */ - private $handlers; - - /** - * @var MessageProducer[] - */ - private $producers; - - /** - * @var EventRecorder[] - */ - private $recorders; - public static function isRealMessage(ReflectionClass $proophMessage): bool { if ($proophMessage->isAnonymous() || $proophMessage->isAbstract() || $proophMessage->isInterface()) { @@ -93,51 +78,30 @@ public static function fromReflectionClass(ReflectionClass $proophMessage): self $messageName, $messageType, $phpReflectionhMessage->getName(), - $phpReflectionhMessage->getFileName(), - [], - [], - [] + $phpReflectionhMessage->getFileName() ); } - public static function fromArray(array $data): self + public static function fromNode(Node $node): self { - $handlers = array_map(function ($handler): MessageHandler { - if ($handler instanceof MessageHandler) { - return $handler; - } - - return MessageHandler::fromArray($handler); - }, $data['handlers'] ?? []); - - $producers = array_map(function ($producer): MessageProducer { - if ($producer instanceof MessageProducer) { - return $producer; - } - - return MessageProducer::fromArray($producer); - }, $data['producers'] ?? []); - - $recorders = array_map(function ($recorder): EventRecorder { - if ($recorder instanceof EventRecorder) { - return $recorder; - } + if (! in_array($node->type(), Node::MESSAGE_TYPES)) { + throw new \InvalidArgumentException('Not a message node: ' . json_encode($node->toArray())); + } - return EventRecorder::fromArray($recorder); - }, $data['recorders'] ?? []); + return self::fromReflectionClass(ReflectionClass::createFromName($node->class())); + } + public static function fromArray(array $data): self + { return new self( $data['name'] ?? '', $data['type'] ?? '', $data['class'] ?? '', - $data['filename'] ?? '', - $handlers, - $producers, - $recorders + $data['filename'] ?? '' ); } - private function __construct(string $name, string $type, string $class, string $filename, array $handlers, array $producers, array $recorders) + private function __construct(string $name, string $type, string $class, string $filename) { MessageDataAssertion::assertMessageName($name); @@ -153,20 +117,10 @@ private function __construct(string $name, string $type, string $class, string $ throw new \InvalidArgumentException("Message class file not found. Got $filename"); } - array_walk($handlers, function (MessageHandler $handler) { - }); - array_walk($producers, function (MessageProducer $producer) { - }); - array_walk($recorders, function (EventRecorder $recorder) { - }); - $this->name = $name; $this->type = $type; $this->class = $class; $this->filename = $filename; - $this->handlers = $handlers; - $this->producers = $producers; - $this->recorders = $recorders; } /** @@ -201,54 +155,6 @@ public function filename(): string return $this->filename; } - /** - * @return MessageHandler[] - */ - public function handlers(): array - { - return $this->handlers; - } - - /** - * @return MessageProducer[] - */ - public function producers(): array - { - return $this->producers; - } - - /** - * @return EventRecorder[] - */ - public function recorders(): array - { - return $this->recorders; - } - - public function addHandler(MessageHandler $messageHandler): self - { - $cp = clone $this; - $cp->handlers[$messageHandler->identifier()] = $messageHandler; - - return $cp; - } - - public function addProducer(MessageProducer $messageProducer): self - { - $cp = clone $this; - $cp->producers[$messageProducer->identifier()] = $messageProducer; - - return $cp; - } - - public function addRecorder(EventRecorder $eventRecorder): self - { - $cp = clone $this; - $cp->recorders[$eventRecorder->identifier()] = $eventRecorder; - - return $cp; - } - public function toArray(): array { return [ @@ -256,15 +162,6 @@ public function toArray(): array 'type' => $this->type, 'class' => $this->class, 'filename' => $this->filename, - 'handlers' => array_map(function (MessageHandler $handler) { - return $handler->toArray(); - }, $this->handlers), - 'producers' => array_map(function (MessageProducer $producer) { - return $producer->toArray(); - }, $this->producers), - 'recorders' => array_map(function (EventRecorder $recorder) { - return $recorder->toArray(); - }, $this->recorders), ]; } diff --git a/src/MessageFlow/MessageHandler.php b/src/MessageFlow/MessageHandler.php index d158b2f..82349a0 100644 --- a/src/MessageFlow/MessageHandler.php +++ b/src/MessageFlow/MessageHandler.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/MessageFlow/MessageHandlingMethodAbstract.php b/src/MessageFlow/MessageHandlingMethodAbstract.php index c2b62b3..77a659a 100644 --- a/src/MessageFlow/MessageHandlingMethodAbstract.php +++ b/src/MessageFlow/MessageHandlingMethodAbstract.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/MessageFlow/MessageProducer.php b/src/MessageFlow/MessageProducer.php index b8aac61..8289fec 100644 --- a/src/MessageFlow/MessageProducer.php +++ b/src/MessageFlow/MessageProducer.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/MessageFlow/Node.php b/src/MessageFlow/Node.php new file mode 100644 index 0000000..4411b1e --- /dev/null +++ b/src/MessageFlow/Node.php @@ -0,0 +1,778 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\MessageFlow; + +use Prooph\MessageFlowAnalyzer\Helper\Util; + +class Node +{ + const TYPE_COMMAND = 'command'; + const TYPE_EVENT = 'event'; + const TYPE_QUERY = 'query'; + const TYPE_HANDLER = 'handler'; + const TYPE_AGGREGATE = 'aggregate'; + const TYPE_PROCESS_MANAGER = 'pm'; + const TYPE_SAGA = 'saga'; + const TYPE_PROJECTOR = 'projector'; + const TYPE_FINDER = 'finder'; + const TYPE_LISTENER = 'listener'; + const TYPE_QUEUE = 'queue'; + const TYPE_READ_MODEL = 'readmodel'; + const TYPE_SERVICE = 'service'; + + const MESSAGE_TYPES = [ + self::TYPE_COMMAND, + self::TYPE_EVENT, + self::TYPE_QUERY, + ]; + + /** + * Unique identifier of the node + * + * @var string + */ + private $id; + + /** + * Used as class name in the UI + * + * @var string + */ + private $type; + + /** + * Name of the node in the UI + * + * @var string + */ + private $name; + + /** + * File containing the class/function + * + * @var string + */ + private $filename; + + /** + * Description of the node + * + * Becomes a tooltip in the UI + * + * @var string|null + */ + private $description = null; + + /** + * Class referenced by the node + * + * @var string|null + */ + private $class = null; + + /** + * Method of the class that is connected with another node + * + * Example: + * - Command Handler method (source node) handling a command (target node) + * - Aggregate method (source node) called by a command handler method (target node) + * - Process manager method (source node) listening on event (target node) + * - ... + * + * @var string/null + */ + private $method = null; + + /** + * Global function name (incl. namespace) if node references a function instead of a class + * + * @var string/null + */ + private $funcName = null; + + /** + * Optional parent node id + * + * Nodes with the same parent are grouped in the UI + * + * The parent should be a node itself and can be used in an edge + * + * Example: + * Aggregate methods are nodes and their parent is the Aggregate class + * + * @var string|null + */ + private $parent = null; + + /** + * Optional parent color + * + * If this node has a parent with a custom color (default is: #E9F2F7) + * you have to set the parent color for the child node, too. + * Parent color is used as icon background color (default is #FFFFFF). + * + * @var string|null + */ + private $parentColor = null; + + /** + * Additional tags added as class names in the UI + * + * @var string[] + */ + private $tags = []; + + /** + * FontAwesome icon name for the node + * + * If not set a circle is used as default shape + * + * @var NodeIcon/null + */ + private $icon = null; + + /** + * If set it overrides the default color used for the node type + * + * @var string|null + */ + private $color = null; + + /** + * Optional JSON Schema if node references a message or read model. + * + * @var array|null + */ + private $schema = null; + + /** + * Named constructor for message node + * + * @param Message $message + * @return Node + */ + public static function asMessage(Message $message): self + { + switch ($message->type()) { + case \Prooph\Common\Messaging\Message::TYPE_COMMAND: + $color = '#15A2B0'; + break; + case \Prooph\Common\Messaging\Message::TYPE_EVENT: + $color = '#ED6842'; + break; + default: + $color = '#1F8A6D'; + } + + return (new self( + Util::codeIdentifierToNodeId($message->name()), + $message->type(), + Util::withoutNamespace($message->name()), + $message->filename(), + null, + $message->class() + ))->withTag('message')->withIcon(NodeIcon::faSolid('fa-envelope'))->withColor($color); + } + + /** + * Named constructor for command handler nodes + * + * Command handler: Function invoked with command that calls an event recorder (aggregate method in most cases) + * + * @param MessageHandler $handler + * @return Node + */ + public static function asCommandHandler(MessageHandler $handler): self + { + if ($handler->type() !== MessageHandlingMethodAbstract::TYPE_CLASS) { + throw new \InvalidArgumentException("Command handler type {$handler->type()} not supported yet. Found in file {$handler->filename()}"); + } + + $withMethod = true; + + if ($handler->function() === '__invoke' || $handler->function() === 'handle') { + $withMethod = false; + } + + $name = ($withMethod) ? Util::withoutNamespace($handler->identifier()) : Util::withoutNamespace($handler->class()); + + return (new self( + Util::codeIdentifierToNodeId($handler->identifier()), + self::TYPE_HANDLER, + $name, + $handler->filename(), + null, + $handler->class(), + $handler->function() + ))->withTag('command')->withIcon(NodeIcon::faSolid('fa-sign-out-alt'))->withColor('#1B1C1D'); + } + + /** + * Named constructor for aggregate nodes + * + * Aggregate nodes are used as parents. They are identified by FQCN and their childs are event recorders + * aka aggregate methods or functions (in case of prooph/micro) + * + * @param MessageHandlingMethodAbstract $aggregateMethod + * @return Node + */ + public static function asAggregate(MessageHandlingMethodAbstract $aggregateMethod): self + { + if ($aggregateMethod->type() !== MessageHandlingMethodAbstract::TYPE_CLASS) { + throw new \InvalidArgumentException("Aggregate type {$aggregateMethod->type()} not supported yet. Found in file {$aggregateMethod->filename()}"); + } + + return (new self( + Util::codeIdentifierToNodeId($aggregateMethod->class()), + self::TYPE_AGGREGATE, + Util::withoutNamespace($aggregateMethod->class()), + $aggregateMethod->filename(), + null, + $aggregateMethod->class() + ))->withTag('parent')->withColor('#E9F2F7'); + } + + /** + * Named constructor for aggregate method nodes + * + * Aggregate methods are event recorders that link to a parent aggregate node, so they are grouped together + * by aggregate. + * + * @param EventRecorder $eventRecorder + * @return Node + */ + public static function asEventRecordingAggregateMethod(EventRecorder $eventRecorder): self + { + if ($eventRecorder->type() !== MessageHandlingMethodAbstract::TYPE_CLASS) { + throw new \InvalidArgumentException("Event recorder type {$eventRecorder->type()} not supported yet. Found in file {$eventRecorder->filename()}"); + } + + return (new self( + Util::codeIdentifierToNodeId($eventRecorder->identifier()), + self::TYPE_AGGREGATE, + Util::withoutNamespace($eventRecorder->identifier()), + $eventRecorder->filename(), + null, + $eventRecorder->class(), + $eventRecorder->function(), + null, + Util::codeIdentifierToNodeId($eventRecorder->class()) + ))->withTag('event')->withTag('recorder') + ->withIcon(NodeIcon::faSolid('fa-chevron-circle-right'))->withColor('#EECA51') + ->withParentColor('#E9F2F7'); + } + + /** + * Named constructor for aggregate methods that act as a factory for another aggregate but do not record events itself. + * + * Example: + * User::postTodo(): Todo + * + * @param MessageHandlingMethodAbstract $eventRecorderInvoker + * @return Node + */ + public static function asAggregateFactoryMethod(MessageHandlingMethodAbstract $eventRecorderInvoker): self + { + if ($eventRecorderInvoker->type() !== MessageHandlingMethodAbstract::TYPE_CLASS) { + throw new \InvalidArgumentException("Event recorder invoker type {$eventRecorderInvoker->type()} not supported yet. Found in file {$eventRecorderInvoker->filename()}"); + } + + return (new self( + Util::codeIdentifierToNodeId($eventRecorderInvoker->identifier()), + self::TYPE_AGGREGATE, + Util::withoutNamespace($eventRecorderInvoker->identifier()), + $eventRecorderInvoker->filename(), + null, + $eventRecorderInvoker->class(), + $eventRecorderInvoker->function(), + null, + Util::codeIdentifierToNodeId($eventRecorderInvoker->class()) + ))->withTag('event')->withTag('factory') + ->withIcon(NodeIcon::faSolid('fa-industry'))->withColor('#EECA51') + ->withParentColor('#E9F2F7'); + } + + public static function asEventListener(MessageHandler $messageHandler): self + { + if ($messageHandler->type() !== MessageHandlingMethodAbstract::TYPE_CLASS) { + throw new \InvalidArgumentException("Event listener type {$messageHandler->type()} not supported yet. Found in file {$messageHandler->filename()}"); + } + + $withMethod = true; + + if ($messageHandler->function() === '__invoke' || $messageHandler->function() === 'onEvent') { + $withMethod = false; + } + + $name = ($withMethod) ? Util::withoutNamespace($messageHandler->identifier()) : Util::withoutNamespace($messageHandler->class()); + + return (new self( + Util::codeIdentifierToNodeId($messageHandler->identifier()), + self::TYPE_LISTENER, + $name, + $messageHandler->filename(), + null, + $messageHandler->class(), + $messageHandler->function() + ))->withTag('event')->withIcon(NodeIcon::faSolid('fa-bell'))->withColor('#6435C9'); + } + + /** + * Named constructor for process manager nodes + * + * A process manager receives an event and produces a new command + * + * @param MessageProducer $messageProducer + * @return Node + */ + public static function asProcessManager(MessageProducer $messageProducer): self + { + if ($messageProducer->type() !== MessageHandlingMethodAbstract::TYPE_CLASS) { + throw new \InvalidArgumentException("Process manager type {$messageProducer->type()} not supported yet. Found in file {$messageProducer->filename()}"); + } + + $withMethod = true; + + if ($messageProducer->function() === '__invoke' || $messageProducer->function() === 'onEvent') { + $withMethod = false; + } + + $name = ($withMethod) ? Util::withoutNamespace($messageProducer->identifier()) : Util::withoutNamespace($messageProducer->class()); + + return (new self( + Util::codeIdentifierToNodeId($messageProducer->identifier()), + self::TYPE_PROCESS_MANAGER, + $name, + $messageProducer->filename(), + null, + $messageProducer->class(), + $messageProducer->function() + ))->withTag('command')->withTag('producer')->withIcon(NodeIcon::faSolid('fa-cogs'))->withColor('#715671'); + } + + /** + * Named constructor for message producing service nodes + * + * A message producing service can be an MVC controller, a PSR-15 request handler, cli command, application service ... + * + * @param MessageProducer $messageProducer + * @param Message $message + * @return Node + */ + public static function asMessageProducingService(MessageProducer $messageProducer, Message $message): self + { + if ($messageProducer->type() !== MessageHandlingMethodAbstract::TYPE_CLASS) { + throw new \InvalidArgumentException("Message producer type {$messageProducer->type()} not supported yet. Found in file {$messageProducer->filename()}"); + } + + return (new self( + Util::codeIdentifierToNodeId($messageProducer->identifier()), + self::TYPE_SERVICE, + Util::withoutNamespace($messageProducer->identifier()), + $messageProducer->filename(), + null, + $messageProducer->class(), + $messageProducer->function() + ))->withTag($message->type())->withTag('producer')->withIcon(NodeIcon::faSolid('fa-long-arrow-alt-right'))->withColor('#1B1C1D'); + } + + public static function fromArray(array $nodeData) + { + $classes = $nodeData['classes'] ?? null; + + if ($classes) { + $tags = explode(' ', $classes); + } else { + $tags = []; + } + + return new self( + $nodeData['data']['id'] ?? '', + $nodeData['data']['type'] ?? '', + $nodeData['data']['name'] ?? '', + $nodeData['data']['filename'] ?? '', + $nodeData['data']['description'] ?? null, + $nodeData['data']['class'] ?? null, + $nodeData['data']['method'] ?? null, + $nodeData['data']['funcName'] ?? null, + $nodeData['data']['parent'] ?? null, + $tags, + NodeIcon::fromString($nodeData['data']['icon']) ?? null, + $nodeData['data']['color'] ?? null, + $nodeData['data']['parentColor'] ?? null, + $nodeData['data']['schema'] ?? null + ); + } + + protected function __construct( + string $id, + string $type, + string $name, + string $filename, + string $description = null, + string $class = null, + string $method = null, + string $funcName = null, + string $parent = null, + array $tags = [], + NodeIcon $icon = null, + string $color = null, + string $parentColor = null, + array $schema = null + ) { + if ($id === '') { + throw new \InvalidArgumentException('Node id must not be empty'); + } + + if ($type === '') { + throw new \InvalidArgumentException('Node type must not be empty'); + } + + if ($name === '') { + throw new \InvalidArgumentException('Node name must not be empty'); + } + + array_walk($tags, function (string $tag) { + if ($tag === '') { + throw new \InvalidArgumentException('A node tag should be a non empty string'); + } + }); + + $this->id = $id; + $this->type = $type; + $this->name = $name; + $this->filename = $filename; + $this->description = $description; + $this->class = $class; + $this->method = $method; + $this->funcName = $funcName; + $this->parent = $parent; + $this->icon = $icon; + $this->color = $color; + $this->parentColor = $parentColor; + $this->schema = $schema; + + foreach ($tags as $tag) { + $this->tags[$tag] = null; + } + } + + public function toArray(): array + { + return [ + 'data' => [ + 'id' => $this->id, + 'type' => $this->type, + 'name' => $this->name, + 'filename' => $this->filename, + 'description' => $this->description, + 'class' => $this->class, + 'method' => $this->method, + 'funcName' => $this->funcName, + 'parent' => $this->parent, + 'icon' => (string) $this->icon, + 'color' => $this->color, + 'parentColor' => $this->parentColor, + 'schema' => $this->schema, + ], + 'classes' => implode(' ', $this->withTag($this->type)->tags()), + ]; + } + + /** + * @return string + */ + public function id(): string + { + return $this->id; + } + + /** + * @return string + */ + public function type(): string + { + return $this->type; + } + + /** + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * @return string + */ + public function filename(): string + { + return $this->filename; + } + + /** + * @return null|string + */ + public function description() + { + return $this->description; + } + + /** + * @return null|string + */ + public function class() + { + return $this->class; + } + + /** + * @return string + */ + public function method(): string + { + return $this->method; + } + + /** + * @return string + */ + public function funcName(): string + { + return $this->funcName; + } + + /** + * @return null|string + */ + public function parent(): ?string + { + return $this->parent; + } + + /** + * @return string[] + */ + public function tags(): array + { + return array_keys($this->tags); + } + + /** + * @return NodeIcon|null + */ + public function icon(): ?NodeIcon + { + return $this->icon; + } + + /** + * @return null|string + */ + public function color(): ?string + { + return $this->color; + } + + public function parentColor(): ?string + { + return $this->parentColor; + } + + /** + * @return array|null + */ + public function schema(): ?array + { + return $this->schema; + } + + public function withName(string $name): self + { + $cp = clone $this; + $cp->name = $name; + + return $cp; + } + + public function withDescription(string $description): self + { + $cp = clone $this; + $cp->description = $description; + + return $cp; + } + + public function withoutDescription(): self + { + $cp = clone $this; + $cp->description = null; + + return $cp; + } + + public function withClass(string $class): self + { + $cp = clone $this; + $cp->class = $class; + + return $cp; + } + + public function withoutClass(): self + { + $cp = clone $this; + $cp->class = null; + + return $cp; + } + + public function withMethod(string $method): self + { + $cp = clone $this; + $cp->method = $method; + + return $cp; + } + + public function withoutMethod(): self + { + $cp = clone $this; + $cp->method = null; + + return $cp; + } + + public function withFuncName(string $funcName): self + { + $cp = clone $this; + $cp->funcName = $funcName; + + return $cp; + } + + public function withoutFuncName(): self + { + $cp = clone $this; + $cp->funcName = null; + + return $cp; + } + + public function withParent(string $parent): self + { + $cp = clone $this; + $cp->parent = $parent; + + return $cp; + } + + public function withoutParent(): self + { + $cp = clone $this; + $cp->parent = null; + + return $cp; + } + + public function withTag(string $tag): self + { + $cp = clone $this; + $cp->tags[$tag] = null; + + return $cp; + } + + public function withoutTag(string $tag): self + { + $cp = clone $this; + if (array_key_exists($tag, $cp->tags())) { + unset($cp->tags[$tag]); + } + + return $cp; + } + + public function withoutTags(): self + { + $cp = clone $this; + $cp->tags = []; + + return $cp; + } + + public function withIcon(NodeIcon $icon): self + { + $cp = clone $this; + $cp->icon = $icon; + + return $cp; + } + + public function withoutIcon(): self + { + $cp = clone $this; + $cp->icon = null; + + return $cp; + } + + public function withColor(string $color): self + { + $cp = clone $this; + $cp->color = $color; + + return $cp; + } + + public function withoutColor(): self + { + $cp = clone $this; + $cp->color = null; + + return $cp; + } + + public function withParentColor(string $parentColor): self + { + $cp = clone $this; + $cp->parentColor = $parentColor; + + return $cp; + } + + public function withoutParentColor(): self + { + $cp = clone $this; + $cp->parentColor = null; + + return $cp; + } + + public function withSchema(array $schema): self + { + $cp = clone $this; + $cp->schema = $schema; + + return $cp; + } + + public function withoutSchema(): self + { + $cp = clone $this; + $cp->schema = null; + + return $cp; + } +} diff --git a/src/MessageFlow/NodeFactory.php b/src/MessageFlow/NodeFactory.php new file mode 100644 index 0000000..7727bdf --- /dev/null +++ b/src/MessageFlow/NodeFactory.php @@ -0,0 +1,74 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\MessageFlow; + +use Roave\BetterReflection\Reflection\ReflectionClass; + +final class NodeFactory +{ + /** + * @var Node + */ + private static $nodeClass = Node::class; + + public static function useNodeClass(string $nodeClass): void + { + $nodeClassRef = ReflectionClass::createFromName($nodeClass); + + if (! $nodeClassRef->isSubclassOf(Node::class) && $nodeClass !== Node::class) { + throw new \InvalidArgumentException('NodeFactory can only use a sub class of ' . Node::class. ". Got $nodeClass"); + } + + self::$nodeClass = $nodeClass; + } + + public static function createMessageNode(Message $message): Node + { + return self::$nodeClass::asMessage($message); + } + + public static function createCommandHandlerNode(MessageHandler $handler): Node + { + return self::$nodeClass::asCommandHandler($handler); + } + + public static function createAggregateNode(MessageHandlingMethodAbstract $aggregateMethod): Node + { + return self::$nodeClass::asAggregate($aggregateMethod); + } + + public static function createEventRecordingAggregateMethodNode(EventRecorder $eventRecorder): Node + { + return self::$nodeClass::asEventRecordingAggregateMethod($eventRecorder); + } + + public static function createAggregateFactoryMethodNode(MessageHandlingMethodAbstract $eventRecorderInvoker): Node + { + return self::$nodeClass::asAggregateFactoryMethod($eventRecorderInvoker); + } + + public static function createProcessManagerNode(MessageProducer $messageProducer): Node + { + return self::$nodeClass::asProcessManager($messageProducer); + } + + public static function createMessageProducingServiceNode(MessageProducer $messageProducer, Message $message): Node + { + return self::$nodeClass::asMessageProducingService($messageProducer, $message); + } + + public static function createEventListenerNode(MessageHandler $messageHandler): Node + { + return self::$nodeClass::asEventListener($messageHandler); + } +} diff --git a/src/MessageFlow/NodeIcon.php b/src/MessageFlow/NodeIcon.php new file mode 100644 index 0000000..30fc56a --- /dev/null +++ b/src/MessageFlow/NodeIcon.php @@ -0,0 +1,84 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\MessageFlow; + +final class NodeIcon +{ + public const FA_SOLID = 'fas'; + public const FA_REGULAR = 'far'; + public const FA_BRAND = 'fab'; + public const LINK = 'link'; + + public const TYPES = [ + self::FA_SOLID, + self::FA_REGULAR, + self::FA_BRAND, + self::LINK, + ]; + + /** + * @var string + */ + private $type; + + /** + * @var string + */ + private $icon; + + public static function faSolid(string $icon): self + { + return new self(self::FA_SOLID, $icon); + } + + public static function faRegular(string $icon): self + { + return new self(self::FA_REGULAR, $icon); + } + + public static function faBrand(string $icon): self + { + return new self(self::FA_BRAND, $icon); + } + + public static function link(string $link): self + { + return new self(self::LINK, $link); + } + + public static function fromString(string $icon): self + { + [$type, $icon] = explode(' ', $icon); + + return new self($type, $icon); + } + + private function __construct(string $type, string $icon) + { + if (! in_array($type, self::TYPES)) { + throw new \InvalidArgumentException('Invalid icon type given. Should be one of ' . implode(', ', self::TYPES) . ". Got $type"); + } + + if ($icon === '') { + throw new \InvalidArgumentException('Icon should not be an empty string'); + } + + $this->type = $type; + $this->icon = $icon; + } + + public function __toString() + { + return $this->type . ' ' . $this->icon; + } +} diff --git a/src/Output/Formatter.php b/src/Output/Formatter.php index 82b0f86..2ba5540 100644 --- a/src/Output/Formatter.php +++ b/src/Output/Formatter.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Output/JsonArangoGraphNodes.php b/src/Output/JsonArangoGraphNodes.php deleted file mode 100644 index c11b055..0000000 --- a/src/Output/JsonArangoGraphNodes.php +++ /dev/null @@ -1,158 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Prooph\MessageFlowAnalyzer\Output; - -use Prooph\MessageFlowAnalyzer\Helper\Util; -use Prooph\MessageFlowAnalyzer\MessageFlow; - -final class JsonArangoGraphNodes implements Formatter -{ - public function messageFlowToString(MessageFlow $messageFlow): string - { - $messages = []; - $handlers = []; - $edges = []; - $eventRecorderClasses = []; - - foreach ($messageFlow->messages() as $message) { - $msgKey = Util::identifierToKey($message->name()); - $messages[$msgKey] = [ - '_key' => $msgKey, - 'type' => $message->type(), - 'name' => Util::withoutNamespace($message->name()), - 'class' => $message->class(), - ]; - - foreach ($message->handlers() as $handler) { - $handlerKey = Util::identifierToKey($handler->identifier()); - $handlers[$handlerKey] = [ - '_key' => $handlerKey, - 'name' => $handler->isClass() ? Util::withoutNamespace($handler->class()) : $handler->function(), - 'class' => $handler->class(), - 'function' => $handler->function(), - ]; - - $edges[] = [ - '_from' => 'messages/'.$msgKey, - '_to' => 'handlers/'.$handlerKey, - ]; - } - - foreach ($message->producers() as $producer) { - $producerKey = Util::identifierToKey($producer->identifier()); - $handlers[$producerKey] = [ - '_key' => $producerKey, - 'name' => $producer->isClass() ? Util::withoutNamespace($producer->class()) : $producer->function(), - 'class' => $producer->class(), - 'function' => $producer->function(), - ]; - - $edges[] = [ - '_from' => 'handlers/'.$producerKey, - '_to' => 'messages/'.$msgKey, - ]; - } - - foreach ($message->recorders() as $recorder) { - if ($recorder->isClass()) { - $eventRecorderClasses[$recorder->class()] = $recorder; - } - - $recorderKey = Util::identifierToKey($recorder->identifier()); - $handlers[$recorderKey] = [ - '_key' => $recorderKey, - 'name' => $recorder->isClass() ? Util::withoutNamespace($recorder->class()).'::'.$recorder->function() : $recorder->function(), - 'class' => $recorder->class(), - 'function' => $recorder->function(), - ]; - - if ($recorder->isClass()) { - $recorderClassKey = Util::identifierToKey(Util::identifierWithoutMethod($recorder->identifier())); - - $handlers[$recorderClassKey] = [ - '_key' => $recorderClassKey, - 'name' => Util::withoutNamespace($recorder->class()), - 'class' => $recorder->class(), - 'function' => null, - ]; - - $edges[] = [ - '_from' => 'handlers/'.$recorderClassKey, - '_to' => 'handlers/'.$recorderKey, - ]; - $edges[] = [ - '_from' => 'handlers/'.$recorderKey, - '_to' => 'messages/'.$msgKey, - ]; - } else { - $edges[] = [ - '_from' => 'handlers/'.$recorderKey, - '_to' => 'messages/'.$msgKey, - ]; - } - } - } - - $isEventRecorderClass = function (string $identifier) use ($eventRecorderClasses): bool { - return array_key_exists(Util::identifierWithoutMethod($identifier), $eventRecorderClasses); - }; - - $getEventRecorderFactory = function (string $identifer) use ($eventRecorderClasses): MessageFlow\EventRecorder { - $recorderClass = Util::identifierWithoutMethod($identifer); - $factoryMethod = str_replace($recorderClass.MessageFlow\MessageHandlingMethodAbstract::ID_METHOD_DELIMITER, '', $identifer); - - $orgEventRecorder = $eventRecorderClasses[$recorderClass]->toArray(); - $orgEventRecorder['function'] = $factoryMethod; - - return MessageFlow\EventRecorder::fromArray($orgEventRecorder); - }; - - foreach ($messageFlow->eventRecorderInvokers() as $eventRecorderInvoker) { - //Special case: EventRecorder method used as factory for another event recorder - //We want to add following flow to the graph in that case: - // - //1.) EventRecorderFactory -> EventRecorderFactory::method -- EventRecorderFactory::method is not available as handler, we need to add it - //2.) EventRecorderFactory::method -> BuiltEventRecorder -- This edge needs to be added to and is the definition stored in $messageFlow - //3.) BuiltEventRecorder -> BuiltEventRecorder::method -- already added as an edge as event recorder - if ($isEventRecorderClass($eventRecorderInvoker->invokerIdentifier())) { - //Add handler for 1.), see above - $eventRecorderFactory = $getEventRecorderFactory($eventRecorderInvoker->invokerIdentifier()); - - $handlers[Util::identifierToKey($eventRecorderFactory->identifier())] = [ - '_key' => Util::identifierToKey($eventRecorderFactory->identifier()), - 'name' => Util::withoutNamespace($eventRecorderFactory->class()).'::'.$eventRecorderFactory->function(), - 'class' => $eventRecorderFactory->class(), - 'function' => $eventRecorderFactory->function(), - ]; - - //Add 1. edge for factory case (see above) - $edges[] = [ - '_from' => 'handlers/'.Util::identifierToKey(Util::identifierWithoutMethod($eventRecorderInvoker->invokerIdentifier())), - '_to' => 'handlers/'.Util::identifierToKey($eventRecorderInvoker->invokerIdentifier()), - ]; - } - - //Add 2. egde for factory case (see above), or normal message handler invokes event recorder case - $edges[] = [ - '_from' => 'handlers/'.Util::identifierToKey($eventRecorderInvoker->invokerIdentifier()), - '_to' => 'handlers/'.Util::identifierToKey(Util::identifierWithoutMethod($eventRecorderInvoker->eventRecorderIdentifier())), - ]; - } - - return json_encode([ - 'messages' => array_values($messages), - 'handlers' => array_values($handlers), - 'edges' => $edges, - ], JSON_PRETTY_PRINT); - } -} diff --git a/src/Output/JsonCytoscapeElements.php b/src/Output/JsonCytoscapeElements.php deleted file mode 100644 index 05f9ab0..0000000 --- a/src/Output/JsonCytoscapeElements.php +++ /dev/null @@ -1,179 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Prooph\MessageFlowAnalyzer\Output; - -use Prooph\MessageFlowAnalyzer\Helper\Util; -use Prooph\MessageFlowAnalyzer\MessageFlow; - -final class JsonCytoscapeElements implements Formatter -{ - public function messageFlowToString(MessageFlow $messageFlow): string - { - $nodes = []; - $edges = []; - $eventRecorderClasses = []; - - foreach ($messageFlow->messages() as $message) { - $msgKey = Util::identifierToKey($message->name()); - $nodes[] = [ - 'data' => [ - 'id' => $msgKey, - 'type' => $message->type(), - 'name' => Util::withoutNamespace($message->name()), - 'class' => $message->class(), - ], - 'classes' => 'message '.$message->type(), - - ]; - - foreach ($message->handlers() as $handler) { - $handlerKey = Util::identifierToKey($handler->identifier()); - $nodes[] = [ - 'data' => [ - 'id' => $handlerKey, - 'name' => $handler->isClass() ? Util::withoutNamespace($handler->class()) : $handler->function(), - 'class' => $handler->class(), - 'function' => $handler->function(), - ], - 'classes' => $message->type().' handler', - ]; - - $edges[] = [ - 'data' => [ - 'id' => $msgKey.'_'.$handlerKey, - 'source' => $msgKey, - 'target' => $handlerKey, - ], - ]; - } - - foreach ($message->producers() as $producer) { - $producerKey = Util::identifierToKey($producer->identifier()); - $nodes[] = [ - 'data' => [ - 'id' => $producerKey, - 'name' => $producer->isClass() ? Util::withoutNamespace($producer->class()) : $producer->function(), - 'class' => $producer->class(), - 'function' => $producer->function(), - ], - 'classes' => $message->type().' producer', - ]; - - $edges[] = [ - 'data' => [ - 'id' => $producerKey.'_'.$msgKey, - 'source' => $producerKey, - 'target' => $msgKey, - ], - ]; - } - - foreach ($message->recorders() as $recorder) { - $parent = null; - if ($recorder->isClass()) { - $parent = Util::identifierToKey(Util::identifierWithoutMethod($recorder->identifier())); - $nodes[] = [ - 'data' => [ - 'id' => $parent, - 'name' => Util::withoutNamespace($recorder->class()), - 'class' => $recorder->class(), - 'function' => null, - ], - 'classes' => $message->type().' parent', - ]; - $eventRecorderClasses[$recorder->class()] = $recorder; - } - - $recorderKey = Util::identifierToKey($recorder->identifier()); - - $data = [ - 'id' => $recorderKey, - 'name' => $recorder->isClass() ? Util::withoutNamespace($recorder->class()).'::'.$recorder->function() : $recorder->function(), - 'class' => $recorder->class(), - 'function' => $recorder->function(), - ]; - - if ($parent) { - $data['parent'] = $parent; - } - - $nodes[] = [ - 'data' => $data, - 'classes' => $message->type().' recorder', - ]; - - $edges[] = [ - 'data' => [ - 'id' => $recorderKey.'_'.$msgKey, - 'source' => $recorderKey, - 'target' => $msgKey, - ], - ]; - } - } - - $isEventRecorderClass = function (string $identifier) use ($eventRecorderClasses): bool { - return array_key_exists(Util::identifierWithoutMethod($identifier), $eventRecorderClasses); - }; - - $getEventRecorderFactory = function (string $identifer) use ($eventRecorderClasses): MessageFlow\EventRecorder { - $recorderClass = Util::identifierWithoutMethod($identifer); - $factoryMethod = str_replace($recorderClass.MessageFlow\MessageHandlingMethodAbstract::ID_METHOD_DELIMITER, '', $identifer); - - $orgEventRecorder = $eventRecorderClasses[$recorderClass]->toArray(); - $orgEventRecorder['function'] = $factoryMethod; - - return MessageFlow\EventRecorder::fromArray($orgEventRecorder); - }; - - foreach ($messageFlow->eventRecorderInvokers() as $eventRecorderInvoker) { - $eventRecorderInvokerKey = Util::identifierToKey($eventRecorderInvoker->invokerIdentifier()); - $eventRecorderKey = Util::identifierToKey($eventRecorderInvoker->eventRecorderIdentifier()); - - //Special case: EventRecorder method used as factory for another event recorder - //We want to add following flow to the graph in that case: - // - //1.) EventRecorderFactory::method -> BuiltEventRecorder::method -- EventRecorderFactory::method is not available as handler, we need to add it - //2.) - if ($isEventRecorderClass($eventRecorderInvoker->invokerIdentifier())) { - //Add handler for 1.), see above - $eventRecorderFactory = $getEventRecorderFactory($eventRecorderInvoker->invokerIdentifier()); - - $eventRecorderFactoryKey = Util::identifierToKey($eventRecorderFactory->identifier()); - $nodes[] = [ - 'data' => [ - 'id' => $eventRecorderFactoryKey, - 'name' => Util::withoutNamespace($eventRecorderFactory->class()).'::'.$eventRecorderFactory->function(), - 'class' => $eventRecorderFactory->class(), - 'function' => $eventRecorderFactory->function(), - 'parent' => Util::identifierToKey(Util::identifierWithoutMethod($eventRecorderFactory->identifier())), - ], - 'classes' => 'event factory', - ]; - } - - $edges[] = [ - 'data' => [ - 'id' => $eventRecorderInvokerKey.'_'.$eventRecorderKey, - 'source' => $eventRecorderInvokerKey, - 'target' => $eventRecorderKey, - ], - ]; - } - - return json_encode([ - 'nodes' => $nodes, - 'edges' => $edges, - ], JSON_PRETTY_PRINT); - } -} diff --git a/src/Output/JsonPrettyPrint.php b/src/Output/JsonPrettyPrint.php index e17c2bc..45f05c1 100644 --- a/src/Output/JsonPrettyPrint.php +++ b/src/Output/JsonPrettyPrint.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/ProjectTraverser.php b/src/ProjectTraverser.php index fe1451c..ae6c386 100644 --- a/src/ProjectTraverser.php +++ b/src/ProjectTraverser.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -89,9 +89,13 @@ public function traverse(string $dir): MessageFlow $iterator = new \RecursiveIteratorIterator($filter); foreach ($iterator as $file) { - /** @var $file \SplFileInfo */ - if ($file->isFile()) { - $msgFlow = $this->handleFile($file, $msgFlow); + try { + /** @var $file \SplFileInfo */ + if ($file->isFile()) { + $msgFlow = $this->handleFile($file, $msgFlow); + } + } catch (\Throwable $e) { + throw new \RuntimeException("Error while handling file {$file->getPathname()}. Please see previous exception for details.", $e->getCode(), $e); } } diff --git a/src/Visitor/AggregateMethodCollector.php b/src/Visitor/AggregateMethodCollector.php new file mode 100644 index 0000000..c50f0be --- /dev/null +++ b/src/Visitor/AggregateMethodCollector.php @@ -0,0 +1,89 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\Visitor; + +use Prooph\MessageFlowAnalyzer\Helper\MessageProducingMethodScanner; +use Prooph\MessageFlowAnalyzer\Helper\PhpParser\ScanHelper; +use Prooph\MessageFlowAnalyzer\Helper\Util; +use Prooph\MessageFlowAnalyzer\MessageFlow; +use Roave\BetterReflection\Reflection\ReflectionClass; +use Roave\BetterReflection\Reflection\ReflectionMethod; + +final class AggregateMethodCollector implements ClassVisitor +{ + use MessageProducingMethodScanner; + + public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFlow): MessageFlow + { + if (! MessageFlow\EventRecorder::isEventRecorder($reflectionClass)) { + return $messageFlow; + } + + return $this->checkMessageProduction( + $messageFlow, + $reflectionClass, + function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMethod $method): MessageFlow { + $msgNode = MessageFlow\NodeFactory::createMessageNode($message); + + if (! $messageFlow->knowsNode($msgNode)) { + $messageFlow = $messageFlow->addMessage($message); + } + + $eventRecorder = MessageFlow\EventRecorder::fromReflectionMethod($method); + + $eventRecorderNode = MessageFlow\NodeFactory::createEventRecordingAggregateMethodNode($eventRecorder); + + if (! $messageFlow->knowsNode($eventRecorderNode)) { + $messageFlow = $messageFlow->addNode($eventRecorderNode); + } + + if ($eventRecorder->isClass() && ! $messageFlow->knowsNodeWithId(Util::codeIdentifierToNodeId($eventRecorder->class()))) { + $messageFlow = $messageFlow->addNode(MessageFlow\NodeFactory::createAggregateNode($eventRecorder)); + } + + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($eventRecorderNode->id(), $msgNode->id())); + + $invokedEventRecorders = ScanHelper::checkIfEventRecorderMethodCallsOtherEventRecorders($eventRecorder); + + if ($invokedEventRecorders) { + foreach ($invokedEventRecorders as $invokedEventRecorder) { + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge( + Util::codeIdentifierToNodeId($eventRecorder->identifier()), + Util::codeIdentifierToNodeId($invokedEventRecorder->identifier())) + ); + } + } + + return $messageFlow; + }, + function (MessageFlow $messageFlow, ReflectionMethod $method): MessageFlow { + $eventRecorder = MessageFlow\EventRecorder::fromReflectionMethod($method); + + $builtEventRecorder = ScanHelper::checkIfEventRecorderMethodIsUsedAsFactory($eventRecorder); + + if ($builtEventRecorder) { + $aggregateFactoryMethodNode = MessageFlow\NodeFactory::createAggregateFactoryMethodNode($eventRecorder); + + if (! $messageFlow->knowsNode($aggregateFactoryMethodNode)) { + $messageFlow = $messageFlow->addNode($aggregateFactoryMethodNode); + } + + $builtEventRecorderNode = MessageFlow\NodeFactory::createEventRecordingAggregateMethodNode($builtEventRecorder); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($aggregateFactoryMethodNode->id(), $builtEventRecorderNode->id())); + } + + return $messageFlow; + } + ); + } +} diff --git a/src/Visitor/ClassVisitor.php b/src/Visitor/ClassVisitor.php index f157b12..2719a2a 100644 --- a/src/Visitor/ClassVisitor.php +++ b/src/Visitor/ClassVisitor.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Visitor/CommandHandlerCollector.php b/src/Visitor/CommandHandlerCollector.php new file mode 100644 index 0000000..f6a3505 --- /dev/null +++ b/src/Visitor/CommandHandlerCollector.php @@ -0,0 +1,63 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\Visitor; + +use Prooph\Common\Messaging\Message as ProophMsg; +use Prooph\MessageFlowAnalyzer\Helper\PhpParser\ScanHelper; +use Prooph\MessageFlowAnalyzer\Helper\Util; +use Prooph\MessageFlowAnalyzer\MessageFlow; +use Roave\BetterReflection\Reflection\ReflectionClass; +use Roave\BetterReflection\Reflection\ReflectionMethod; + +final class CommandHandlerCollector implements ClassVisitor +{ + public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFlow): MessageFlow + { + $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); + + foreach ($methods as $method) { + if ($method->getNumberOfParameters() === 1) { + $messageFlow = $this->inspectMethod($messageFlow, $method); + } + } + + return $messageFlow; + } + + private function inspectMethod(MessageFlow $messageFlow, ReflectionMethod $method): MessageFlow + { + $message = ScanHelper::checkIfMethodHandlesMessage($messageFlow, $method); + + if (! $message || $message->type() !== ProophMsg::TYPE_COMMAND) { + return $messageFlow; + } + + $handler = MessageFlow\MessageHandler::fromReflectionMethod($method); + + $node = MessageFlow\NodeFactory::createCommandHandlerNode($handler); + + if (! $messageFlow->knowsNode($node)) { + $messageFlow = $messageFlow->addNode($node); + } + + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($message->name()), $node->id())); + + $eventRecorders = ScanHelper::findInvokedEventRecorders($handler); + + foreach ($eventRecorders as $eventRecorder) { + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($node->id(), Util::codeIdentifierToNodeId($eventRecorder->identifier()))); + } + + return $messageFlow; + } +} diff --git a/src/Visitor/EventRecorderCollector.php b/src/Visitor/EventRecorderCollector.php deleted file mode 100644 index a1e8d37..0000000 --- a/src/Visitor/EventRecorderCollector.php +++ /dev/null @@ -1,37 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Prooph\MessageFlowAnalyzer\Visitor; - -use Prooph\MessageFlowAnalyzer\Helper\MessageProducingMethodScanner; -use Prooph\MessageFlowAnalyzer\MessageFlow; -use Roave\BetterReflection\Reflection\ReflectionClass; -use Roave\BetterReflection\Reflection\ReflectionMethod; - -final class EventRecorderCollector implements ClassVisitor -{ - use MessageProducingMethodScanner; - - public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFlow): MessageFlow - { - if (! MessageFlow\EventRecorder::isEventRecorder($reflectionClass)) { - return $messageFlow; - } - - return $this->checkMessageProduction( - $reflectionClass, - function (MessageFlow\Message $message, ReflectionMethod $method): MessageFlow\Message { - return $message->addRecorder(MessageFlow\EventRecorder::fromReflectionMethod($method)); - }, - $messageFlow); - } -} diff --git a/src/Visitor/EventRecorderInvokerCollector.php b/src/Visitor/EventRecorderInvokerCollector.php deleted file mode 100644 index 13be45e..0000000 --- a/src/Visitor/EventRecorderInvokerCollector.php +++ /dev/null @@ -1,133 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Prooph\MessageFlowAnalyzer\Visitor; - -use PhpParser\Node; -use PhpParser\NodeTraverser; -use PhpParser\NodeVisitorAbstract; -use Prooph\MessageFlowAnalyzer\Helper\PhpParser\ScanHelper; -use Prooph\MessageFlowAnalyzer\MessageFlow; -use Roave\BetterReflection\Reflection\ReflectionClass; -use Roave\BetterReflection\Reflection\ReflectionMethod; - -final class EventRecorderInvokerCollector implements ClassVisitor -{ - public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFlow): MessageFlow - { - if (! in_array($reflectionClass->getName(), $messageFlow->getKnownCommandHandlers())) { - return $messageFlow; - } - - $commandHandler = $this->getCommandHandlerFromMessageFlow($reflectionClass->getName(), $messageFlow); - - $eventRecorders = ScanHelper::findInvokedEventRecorders($commandHandler); - - foreach ($eventRecorders as $eventRecorder) { - $messageFlow = $messageFlow->setEventRecorderInvoker( - MessageFlow\EventRecorderInvoker::fromInvokerAndEventRecorder( - $commandHandler, - $eventRecorder - ) - ); - - $builtEventRecorder = $this->checkIfEventRecorderMethodIsUsedAsFactory($eventRecorder); - - if ($builtEventRecorder) { - $messageFlow = $messageFlow->setEventRecorderInvoker( - MessageFlow\EventRecorderInvoker::fromInvokerAndEventRecorder( - $eventRecorder, - $builtEventRecorder - ) - ); - } - } - - return $messageFlow; - } - - private function getCommandHandlerFromMessageFlow(string $handler, MessageFlow $messageFlow): MessageFlow\MessageHandler - { - foreach ($messageFlow->messages() as $message) { - foreach ($message->handlers() as $cmdHandler) { - if ($cmdHandler->isClass() && $cmdHandler->class() === $handler) { - return $cmdHandler; - } - - if (! $cmdHandler->isClass() && $cmdHandler->function() === $handler) { - return $cmdHandler; - } - } - } - - throw new \RuntimeException('No command handler found for handler identifier: ' . $handler); - } - - private function checkIfEventRecorderMethodIsUsedAsFactory(MessageFlow\EventRecorder $eventRecorder): ?MessageFlow\EventRecorder - { - $method = $eventRecorder->toFunctionLike(); - - if (! $method->hasReturnType()) { - return null; - } - - $returnType = $method->getReturnType(); - - if ($returnType->isBuiltin()) { - return null; - } - - $reflectedReturnType = ReflectionClass::createFromName((string) $returnType); - - if (! MessageFlow\EventRecorder::isEventRecorder($reflectedReturnType)) { - return null; - } - - $nodeVisitor = new class($reflectedReturnType) extends NodeVisitorAbstract { - private $reflectedReturnType; - private $eventRecorder; - - public function __construct(ReflectionClass $reflectedReturnType) - { - $this->reflectedReturnType = $reflectedReturnType; - } - - public function leaveNode(Node $node) - { - if ($node instanceof Node\Expr\StaticCall) { - if ($node->class instanceof Node\Name\FullyQualified - && $this->reflectedReturnType->getName() === $node->class->toString()) { - $reflectionClass = ReflectionClass::createFromName($node->class->toString()); - - if (! MessageFlow\EventRecorder::isEventRecorder($reflectionClass)) { - return; - } - - $reflectionMethod = ReflectionMethod::createFromName($node->class->toString(), $node->name); - $this->eventRecorder = MessageFlow\EventRecorder::fromReflectionMethod($reflectionMethod); - } - } - } - - public function getEventRecorder(): ?MessageFlow\EventRecorder - { - return $this->eventRecorder; - } - }; - - $traverser = new NodeTraverser(); - $traverser->addVisitor($nodeVisitor); - $traverser->traverse($method->getBodyAst()); - - return $nodeVisitor->getEventRecorder(); - } -} diff --git a/src/Visitor/FileInfoVisitor.php b/src/Visitor/FileInfoVisitor.php index 520baf2..2105d9b 100644 --- a/src/Visitor/FileInfoVisitor.php +++ b/src/Visitor/FileInfoVisitor.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/src/Visitor/MessageCollector.php b/src/Visitor/MessageCollector.php index 3c8e1e5..bfad2a1 100644 --- a/src/Visitor/MessageCollector.php +++ b/src/Visitor/MessageCollector.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,20 +15,25 @@ use Prooph\Common\Messaging\Message as ProophMsg; use Prooph\MessageFlowAnalyzer\MessageFlow; use Roave\BetterReflection\Reflection\ReflectionClass; +use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; class MessageCollector implements ClassVisitor { public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFlow): MessageFlow { - if ($reflectionClass->implementsInterface(ProophMsg::class)) { - if (! MessageFlow\Message::isRealMessage($reflectionClass)) { - return $messageFlow; - } + try { + if ($reflectionClass->implementsInterface(ProophMsg::class)) { + if (! MessageFlow\Message::isRealMessage($reflectionClass)) { + return $messageFlow; + } - $msg = MessageFlow\Message::fromReflectionClass($reflectionClass); - if (! $messageFlow->knowsMessage($msg->name())) { - $messageFlow = $messageFlow->addMessage($msg); + $msg = MessageFlow\Message::fromReflectionClass($reflectionClass); + if (! $messageFlow->knowsMessage($msg)) { + $messageFlow = $messageFlow->addMessage($msg); + } } + } catch (IdentifierNotFound $exception) { + //An Interface cannot be found, this error can be ignored } return $messageFlow; diff --git a/src/Visitor/MessageHandlerCollector.php b/src/Visitor/MessageHandlerCollector.php deleted file mode 100644 index 8a335ee..0000000 --- a/src/Visitor/MessageHandlerCollector.php +++ /dev/null @@ -1,65 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Prooph\MessageFlowAnalyzer\Visitor; - -use Prooph\Common\Messaging\Message as ProophMsg; -use Prooph\MessageFlowAnalyzer\MessageFlow; -use Roave\BetterReflection\Reflection\ReflectionClass; -use Roave\BetterReflection\Reflection\ReflectionMethod; - -final class MessageHandlerCollector implements ClassVisitor -{ - public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFlow): MessageFlow - { - $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); - - foreach ($methods as $method) { - if ($method->getNumberOfParameters() === 1 || $method->getNumberOfParameters() === 2) { - $messageFlow = $this->inspectMethod($method, $messageFlow); - } - } - - return $messageFlow; - } - - private function inspectMethod(ReflectionMethod $method, MessageFlow $messageFlow): MessageFlow - { - $parameter = $method->getParameters()[0]; - - if (! $parameter->hasType()) { - return $messageFlow; - } - - $parameterType = $parameter->getType(); - - if ($parameterType->isBuiltin()) { - return $messageFlow; - } - - $reflectionClass = ReflectionClass::createFromName((string) $parameterType); - - if (! $reflectionClass->implementsInterface(ProophMsg::class)) { - return $messageFlow; - } - - if (! MessageFlow\Message::isRealMessage($reflectionClass)) { - return $messageFlow; - } - - $message = MessageFlow\Message::fromReflectionClass($reflectionClass); - - $message = $messageFlow->getMessage($message->name(), $message); - - return $messageFlow->setMessage($message->addHandler(MessageFlow\MessageHandler::fromReflectionMethod($method))); - } -} diff --git a/src/Visitor/MessageIOCollector.php b/src/Visitor/MessageIOCollector.php deleted file mode 100644 index 0989fee..0000000 --- a/src/Visitor/MessageIOCollector.php +++ /dev/null @@ -1,17 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Prooph\MessageFlowAnalyzer\Visitor; - -class MessageIOCollector -{ -} diff --git a/src/Visitor/MessageProducerCollector.php b/src/Visitor/MessageProducerCollector.php deleted file mode 100644 index 98f06d0..0000000 --- a/src/Visitor/MessageProducerCollector.php +++ /dev/null @@ -1,43 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Prooph\MessageFlowAnalyzer\Visitor; - -use Prooph\Common\Messaging\Message as ProophMsg; -use Prooph\MessageFlowAnalyzer\Helper\MessageProducingMethodScanner; -use Prooph\MessageFlowAnalyzer\MessageFlow; -use Roave\BetterReflection\Reflection\ReflectionClass; -use Roave\BetterReflection\Reflection\ReflectionMethod; - -class MessageProducerCollector implements ClassVisitor -{ - use MessageProducingMethodScanner; - - public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFlow): MessageFlow - { - if ($reflectionClass->implementsInterface(ProophMsg::class)) { - return $messageFlow; - } - - if (MessageFlow\EventRecorder::isEventRecorder($reflectionClass)) { - return $messageFlow; - } - - return $this->checkMessageProduction( - $reflectionClass, - function (MessageFlow\Message $message, ReflectionMethod $method): MessageFlow\Message { - return $message->addProducer(MessageFlow\MessageProducer::fromReflectionMethod($method)); - }, - $messageFlow - ); - } -} diff --git a/src/Visitor/MessagingCollector.php b/src/Visitor/MessagingCollector.php new file mode 100644 index 0000000..c3ff83b --- /dev/null +++ b/src/Visitor/MessagingCollector.php @@ -0,0 +1,297 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prooph\MessageFlowAnalyzer\Visitor; + +use PhpParser\Node as ParserNode; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitorAbstract; +use Prooph\Common\Messaging\Message as ProophMsg; +use Prooph\MessageFlowAnalyzer\Helper\MessageClassProvider; +use Prooph\MessageFlowAnalyzer\Helper\MessageNameEqualsClassProvider; +use Prooph\MessageFlowAnalyzer\Helper\MessageProducingMethodScanner; +use Prooph\MessageFlowAnalyzer\Helper\PhpParser\ScanHelper; +use Prooph\MessageFlowAnalyzer\Helper\Util; +use Prooph\MessageFlowAnalyzer\MessageFlow; +use Roave\BetterReflection\Reflection\ReflectionClass; +use Roave\BetterReflection\Reflection\ReflectionMethod; + +class MessagingCollector implements ClassVisitor +{ + use MessageProducingMethodScanner; + + private static $messageClassProvider; + + public static function useMessageClassProvider(MessageClassProvider $messageClassProvider): void + { + self::$messageClassProvider = $messageClassProvider; + } + + public static function useDefaultMessageClassProvider(): void + { + self::$messageClassProvider = null; + } + + public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFlow): MessageFlow + { + if ($reflectionClass->implementsInterface(ProophMsg::class)) { + return $messageFlow; + } + + if (MessageFlow\EventRecorder::isEventRecorder($reflectionClass)) { + return $messageFlow; + } + + $messageFactoryProperties = ScanHelper::findMessageFactoryProperties($reflectionClass); + + $methods = $reflectionClass->getMethods(); + + $eventListeners = []; + + foreach ($methods as $method) { + if ($method->getNumberOfParameters() === 1) { + if ($eventListenerScan = $this->checkIsEventListener($messageFlow, $method)) { + [$node] = $eventListenerScan; + $eventListeners[$node->id()] = $eventListenerScan; + } + } + } + + $messageProducers = $this->findMessageProducers($reflectionClass, $messageFlow, $messageFactoryProperties); + $handledProducers = []; + + foreach ($eventListeners as [$node, $eventListener, $method, $event]) { + /* @var $node MessageFlow\Node */ + /* @var $eventListener MessageFlow\MessageHandler */ + /* @var $method ReflectionMethod */ + /* @var $event MessageFlow\Message */ + + if (! $messageFlow->knowsMessage($event)) { + $messageFlow = $messageFlow->addMessage($event); + } + + if (isset($messageProducers[$node->id()])) { + [$producerNode, $producer, $producerMethod, $commands] = $messageProducers[$node->id()]; + + foreach ($commands as $command) { + if (! $messageFlow->knowsMessage($command)) { + $messageFlow = $messageFlow->addMessage($command); + } + + $messageFlow = $this->addProcessManager( + $messageFlow, + MessageFlow\NodeFactory::createMessageNode($event), + MessageFlow\NodeFactory::createMessageNode($command), + $producer + ); + } + + $handledProducers[] = $node->id(); + continue; + } + + $invokedProducers = []; + + if (count($messageProducers) > 0) { + $invokedProducers = $this->checkIfEventListenerInvokesMessageProducer($method, array_keys($messageProducers)); + } + + if (count($invokedProducers) === 0) { + $messageFlow = $this->addEventListener( + $messageFlow, + MessageFlow\NodeFactory::createMessageNode($event), + MessageFlow\NodeFactory::createEventListenerNode($eventListener) + ); + } else { + foreach ($invokedProducers as $invokedProducerId) { + [$producerNode, $producer, $producerMethod, $commands] = $messageProducers[$invokedProducerId]; + + foreach ($commands as $command) { + if (! $messageFlow->knowsMessage($command)) { + $messageFlow = $messageFlow->addMessage($command); + } + + $messageFlow = $this->addProcessManager( + $messageFlow, + MessageFlow\NodeFactory::createMessageNode($event), + MessageFlow\NodeFactory::createMessageNode($command), + $producer, + $method + ); + } + + $handledProducers[] = $invokedProducerId; + } + } + } + + $unhandledProducers = array_diff(array_keys($messageProducers), $handledProducers); + + foreach ($unhandledProducers as $producerNodeId) { + [$producerNode, $producer, $producerMethod, $commands] = $messageProducers[$producerNodeId]; + + foreach ($commands as $command) { + if (! $messageFlow->knowsMessage($command)) { + $messageFlow = $messageFlow->addMessage($command); + } + + $messageProducerNode = MessageFlow\NodeFactory::createMessageProducingServiceNode($producer, $command); + + if (! $messageFlow->knowsNode($messageProducerNode)) { + $messageFlow = $messageFlow->addNode($messageProducerNode); + } + + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($messageProducerNode->id(), Util::codeIdentifierToNodeId($command->name()))); + } + } + + return $messageFlow; + } + + private function checkIsEventListener(MessageFlow $messageFlow, ReflectionMethod $method): ?array + { + $message = ScanHelper::checkIfMethodHandlesMessage($messageFlow, $method); + + if (! $message || $message->type() !== ProophMsg::TYPE_EVENT) { + return null; + } + $eventListener = MessageFlow\MessageHandler::fromReflectionMethod($method); + + $node = MessageFlow\NodeFactory::createEventListenerNode($eventListener); + + return [$node, $eventListener, $method, $message]; + } + + private function findMessageProducers(ReflectionClass $reflectionClass, MessageFlow $messageFlow, array $messageFactoryProperties): array + { + $messageProducers = []; + + $this->checkMessageProduction( + $messageFlow, + $reflectionClass, + function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMethod $method) use (&$messageProducers): MessageFlow { + $messageProducer = MessageFlow\MessageProducer::fromReflectionMethod($method); + + $node = MessageFlow\NodeFactory::createMessageProducingServiceNode($messageProducer, $message); + + if (isset($messageProducers[$node->id()])) { + [$n, $m, $me, $messages] = $messageProducers[$node->id()]; + } else { + $messages = []; + } + + $messages[] = $message; + + $messageProducers[$node->id()] = [$node, $messageProducer, $method, $messages]; + + return $messageFlow; + }, + null, + $this->getMessageClassProvider(), + $messageFactoryProperties + ); + + return $messageProducers; + } + + private function checkIfEventListenerInvokesMessageProducer(ReflectionMethod $eventListener, array $producerNodeIds): array + { + $nodeVisitor = new class($eventListener, $producerNodeIds) extends NodeVisitorAbstract { + private $eventListener; + private $producerNodeIds; + private $invokedProducers = []; + + public function __construct(ReflectionMethod $eventListener, array $producerNodeIds) + { + $this->eventListener = $eventListener; + $this->producerNodeIds = $producerNodeIds; + } + + public function leaveNode(ParserNode $node) + { + if ($node instanceof ParserNode\Expr\MethodCall && $node->var->name === 'this') { + $methodNodeId = Util::codeIdentifierToNodeId( + $this->eventListener->getImplementingClass()->getName() . '::' . $node->name + ); + + if (in_array($methodNodeId, $this->producerNodeIds)) { + $this->invokedProducers[] = $methodNodeId; + } + } + } + + public function getInvokedProducers(): array + { + return $this->invokedProducers; + } + }; + + $traverser = new NodeTraverser(); + $traverser->addVisitor($nodeVisitor); + $traverser->traverse($eventListener->getBodyAst()); + + return $nodeVisitor->getInvokedProducers(); + } + + private function addProcessManager( + MessageFlow $messageFlow, + MessageFlow\Node $event, + MessageFlow\Node $command, + MessageFlow\MessageProducer $processManager, + ReflectionMethod $listenerMethod = null + ): MessageFlow { + $pmNode = MessageFlow\NodeFactory::createProcessManagerNode($processManager); + + if (! $messageFlow->knowsNode($pmNode)) { + $messageFlow = $messageFlow->addNode($pmNode); + } + + if ($listenerMethod) { + $listenerInvokingProducer = MessageFlow\MessageProducer::fromReflectionMethod($listenerMethod); + + $listenerInvokingProducerNode = MessageFlow\NodeFactory::createProcessManagerNode($listenerInvokingProducer); + + if (! $messageFlow->knowsNode($listenerInvokingProducerNode)) { + $messageFlow = $messageFlow->addNode($listenerInvokingProducerNode); + } + + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($event->id(), $listenerInvokingProducerNode->id())); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($listenerInvokingProducerNode->id(), $pmNode->id())); + } else { + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($event->id(), $pmNode->id())); + } + + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($pmNode->id(), $command->id())); + + return $messageFlow; + } + + private function addEventListener(MessageFlow $messageFlow, MessageFlow\Node $event, MessageFlow\Node $eventListener): MessageFlow + { + if (! $messageFlow->knowsNode($eventListener)) { + $messageFlow = $messageFlow->addNode($eventListener); + } + + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($event->id(), $eventListener->id())); + + return $messageFlow; + } + + private function getMessageClassProvider(): MessageClassProvider + { + if (null === self::$messageClassProvider) { + self::$messageClassProvider = new MessageNameEqualsClassProvider(); + } + + return self::$messageClassProvider; + } +} diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 1faf589..de6c4ec 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,12 +13,24 @@ namespace ProophTest\MessageFlowAnalyzer; use PHPUnit\Framework\TestCase; +use Prooph\MessageFlowAnalyzer\Helper\MessageClassProvider; use Prooph\MessageFlowAnalyzer\MessageFlow; +use Prooph\MessageFlowAnalyzer\Visitor\MessagingCollector; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ActivateUser; +use Prophecy\Argument; class BaseTestCase extends TestCase { protected function getDefaultProjectMessageFlow(): MessageFlow { + $messageClassProvider = $this->prophesize(MessageClassProvider::class); + $messageClassProvider->provideClass(Argument::exact('ActivateUser'))->willReturn(ActivateUser::class); + $messageClassProvider->provideClass(Argument::not(Argument::exact('ActivateUser')))->will(function ($args) { + return $args[0]; + }); + + MessagingCollector::useMessageClassProvider($messageClassProvider->reveal()); + return MessageFlow::newFlow('default', __DIR__ . '/Sample/DefaultProject'); } } diff --git a/tests/Filter/ExcludeHiddenFileInfoTest.php b/tests/Filter/ExcludeHiddenFileInfoTest.php index 6d5675a..95ecf26 100644 --- a/tests/Filter/ExcludeHiddenFileInfoTest.php +++ b/tests/Filter/ExcludeHiddenFileInfoTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Filter/ExcludeTestsDirTest.php b/tests/Filter/ExcludeTestsDirTest.php index 713aa18..ef9d3af 100644 --- a/tests/Filter/ExcludeTestsDirTest.php +++ b/tests/Filter/ExcludeTestsDirTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Filter/ExcludeVendorDirTest.php b/tests/Filter/ExcludeVendorDirTest.php index 538ed8b..584f4f1 100644 --- a/tests/Filter/ExcludeVendorDirTest.php +++ b/tests/Filter/ExcludeVendorDirTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Filter/IncludePHPFileTest.php b/tests/Filter/IncludePHPFileTest.php index c7df63f..5534f06 100644 --- a/tests/Filter/IncludePHPFileTest.php +++ b/tests/Filter/IncludePHPFileTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Helper/PhpParser/ScanHelperTest.php b/tests/Helper/PhpParser/ScanHelperTest.php index 776b12a..2e92b20 100644 --- a/tests/Helper/PhpParser/ScanHelperTest.php +++ b/tests/Helper/PhpParser/ScanHelperTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/MessageFlow/EventRecorderTest.php b/tests/MessageFlow/EventRecorderTest.php index 8399b2c..fe8364f 100644 --- a/tests/MessageFlow/EventRecorderTest.php +++ b/tests/MessageFlow/EventRecorderTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/MessageFlow/MessageTest.php b/tests/MessageFlow/MessageTest.php index 57c4ff1..045fabc 100644 --- a/tests/MessageFlow/MessageTest.php +++ b/tests/MessageFlow/MessageTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -35,9 +35,6 @@ public function it_can_be_constructed_from_reflected_message() 'type' => $registerUser->messageType(), 'class' => RegisterUser::class, 'filename' => realpath(__DIR__ . '/../Sample/DefaultProject/Model/User/Command/RegisterUser.php'), - 'handlers' => [], - 'producers' => [], - 'recorders' => [], ], $message->toArray()); } } diff --git a/tests/MessageFlowTest.php b/tests/MessageFlowTest.php index 457def4..2c36ee5 100644 --- a/tests/MessageFlowTest.php +++ b/tests/MessageFlowTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,7 +14,7 @@ use Prooph\MessageFlowAnalyzer\MessageFlow\Message; use Prooph\MessageFlowAnalyzer\MessageFlow\MessageHandler; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User; +use Prooph\MessageFlowAnalyzer\MessageFlow\NodeFactory; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ChangeUsername; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ChangeUsernameHandler; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUser; @@ -35,17 +35,19 @@ public function it_filters_command_handlers() $changeUsername = Message::fromReflectionClass(ReflectionClass::createFromName(ChangeUsername::class)); $userRegistered = Message::fromReflectionClass(ReflectionClass::createFromName(UserRegistered::class)); - $registerUser = $registerUser->addHandler(MessageHandler::fromReflectionMethod( - ReflectionClass::createFromName(RegisterUserHandler::class)->getMethod('__invoke') - )); + $registerUserHandlerNode = NodeFactory::createCommandHandlerNode( + MessageHandler::fromReflectionMethod( + ReflectionClass::createFromName(RegisterUserHandler::class)->getMethod('__invoke') + ) + ); - $changeUsername = $changeUsername->addHandler(MessageHandler::fromReflectionMethod( - ReflectionClass::createFromName(ChangeUsernameHandler::class)->getMethod('handle') - )); + $changeUsernameHandlerNode = NodeFactory::createCommandHandlerNode( + MessageHandler::fromReflectionMethod( + ReflectionClass::createFromName(ChangeUsernameHandler::class)->getMethod('handle') + ) + ); - $userRegistered = $userRegistered->addHandler(MessageHandler::fromReflectionMethod( - ReflectionClass::createFromName(User::class)->getMethod('register') - )); + $msgFlow = $msgFlow->addNode($registerUserHandlerNode); $msgFlow = $msgFlow->setMessage($registerUser); @@ -54,6 +56,7 @@ public function it_filters_command_handlers() ], $msgFlow->getKnownCommandHandlers()); $msgFlow = $msgFlow->setMessage($changeUsername); + $msgFlow = $msgFlow->addNode($changeUsernameHandlerNode); $this->assertEquals([ RegisterUserHandler::class, diff --git a/tests/ProjectTraverserTest.php b/tests/ProjectTraverserTest.php index 12caea2..7f696ce 100644 --- a/tests/ProjectTraverserTest.php +++ b/tests/ProjectTraverserTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -16,12 +16,14 @@ use Prooph\MessageFlowAnalyzer\Filter\ExcludeTestsDir; use Prooph\MessageFlowAnalyzer\Filter\ExcludeVendorDir; use Prooph\MessageFlowAnalyzer\Filter\IncludePHPFile; +use Prooph\MessageFlowAnalyzer\Helper\Util; +use Prooph\MessageFlowAnalyzer\MessageFlow\Edge; +use Prooph\MessageFlowAnalyzer\MessageFlow\Node; use Prooph\MessageFlowAnalyzer\ProjectTraverser; -use Prooph\MessageFlowAnalyzer\Visitor\EventRecorderCollector; -use Prooph\MessageFlowAnalyzer\Visitor\EventRecorderInvokerCollector; +use Prooph\MessageFlowAnalyzer\Visitor\AggregateMethodCollector; +use Prooph\MessageFlowAnalyzer\Visitor\CommandHandlerCollector; use Prooph\MessageFlowAnalyzer\Visitor\MessageCollector; -use Prooph\MessageFlowAnalyzer\Visitor\MessageHandlerCollector; -use Prooph\MessageFlowAnalyzer\Visitor\MessageProducerCollector; +use Prooph\MessageFlowAnalyzer\Visitor\MessagingCollector; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Controller\UserController; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Listener\SendConfirmationEmail; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\Identity; @@ -31,7 +33,6 @@ use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ChangeUsername; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUser; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUserHandler; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UsernameChanged; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserRegistered; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\ProcessManager\IdentityAdder; @@ -52,76 +53,73 @@ public function it_collects_message_flow() ], [ new MessageCollector(), - new MessageHandlerCollector(), - new MessageProducerCollector(), - new EventRecorderCollector(), - new EventRecorderInvokerCollector(), + new CommandHandlerCollector(), + new AggregateMethodCollector(), + new MessagingCollector(), ] ); - $msgFlow = $projectTraverser->traverse(__DIR__.'/Sample/DefaultProject'); + $msgFlow = $projectTraverser->traverse(__DIR__ . '/Sample/DefaultProject'); $this->assertEquals('default', $msgFlow->project()); - $this->assertEquals(__DIR__.DIRECTORY_SEPARATOR.'Sample'.DIRECTORY_SEPARATOR.'DefaultProject', $msgFlow->rootDir()); - - $messageNames = array_keys($msgFlow->messages()); - sort($messageNames); - - $this->assertEquals([ - AddIdentity::class, - IdentityAdded::class, - User\Command\AddUserIdentity::class, - ChangeUsername::class, - RegisterUser::class, - UserRegistered::class, - UsernameChanged::class, - ], $messageNames); - - $registerUser = $msgFlow->getMessage(RegisterUser::class); - - $this->assertEquals([ - RegisterUserHandler::class . '::__invoke', - ], array_keys($registerUser->handlers())); - - $this->assertEquals([ - UserController::class . '::postAction', - ], array_keys($registerUser->producers())); - - $userRegistered = $msgFlow->getMessage(UserRegistered::class); - - $this->assertEquals([ - SendConfirmationEmail::class.'::onUserRegistered', - IdentityAdder::class.'::'.'onUserRegistered', - ], array_keys($userRegistered->handlers())); - - $this->assertEquals([ - User::class.'::register', - ], array_keys($userRegistered->recorders())); - - $changeUsername = $msgFlow->getMessage(ChangeUsername::class); - - $this->assertEquals([ - UserController::class . '::patchAction', - ], array_keys($changeUsername->producers())); - - $addIdentity = $msgFlow->getMessage(AddIdentity::class); - - $this->assertEquals([ - IdentityAdder::class . '::onUserRegistered', - ], array_keys($addIdentity->producers())); - - $identityAdded = $msgFlow->getMessage(IdentityAdded::class); - - $this->assertEquals([ - Identity::class.'::add', - Identity::class.'::addForUser', - ], array_keys($identityAdded->recorders())); - - $this->assertEquals([ - RegisterUserHandler::class.'::__invoke->'.User::class.'::register', - User\Command\ChangeUsernameHandler::class.'::handle->'.User::class.'::changeUsername', - User\Command\AddUserIdentityHandler::class.'::__invoke->'.User::class.'::addIdentity', - User::class.'::addIdentity->'.Identity::class.'::add', - ], array_keys($msgFlow->eventRecorderInvokers())); + $this->assertEquals(__DIR__ . DIRECTORY_SEPARATOR . 'Sample' . DIRECTORY_SEPARATOR . 'DefaultProject', $msgFlow->rootDir()); + + $expectedNodes = [ + Util::codeIdentifierToNodeId(AddIdentity::class) => [Node::TYPE_COMMAND, AddIdentity::class], + Util::codeIdentifierToNodeId(IdentityAdded::class) => [Node::TYPE_EVENT, IdentityAdded::class], + Util::codeIdentifierToNodeId(User\Command\AddUserIdentity::class) => [Node::TYPE_COMMAND, User\Command\AddUserIdentity::class], + Util::codeIdentifierToNodeId(ChangeUsername::class) => [Node::TYPE_COMMAND, ChangeUsername::class], + Util::codeIdentifierToNodeId(RegisterUser::class) => [Node::TYPE_COMMAND, RegisterUser::class], + Util::codeIdentifierToNodeId(UserRegistered::class) => [Node::TYPE_EVENT, UserRegistered::class], + Util::codeIdentifierToNodeId(User\Event\UserActivated::class) => [Node::TYPE_EVENT, UserRegistered::class], + Util::codeIdentifierToNodeId(RegisterUserHandler::class . '::__invoke') => [Node::TYPE_HANDLER, RegisterUserHandler::class . '::__invoke'], + Util::codeIdentifierToNodeId(UserController::class . '::postAction') => [Node::TYPE_SERVICE, UserController::class . '::postAction'], + Util::codeIdentifierToNodeId(UserController::class . '::patchAction') => [Node::TYPE_SERVICE, UserController::class . '::patchAction'], + Util::codeIdentifierToNodeId(SendConfirmationEmail::class.'::onUserRegistered') => [Node::TYPE_LISTENER, SendConfirmationEmail::class.'::onUserRegistered'], + Util::codeIdentifierToNodeId(User::class . '::register') => [Node::TYPE_AGGREGATE, User::class . '::register'], + Util::codeIdentifierToNodeId(User::class . '::activate') => [Node::TYPE_AGGREGATE, User::class . '::activate'], + Util::codeIdentifierToNodeId(User::class) => [Node::TYPE_AGGREGATE, User::class], + Util::codeIdentifierToNodeId(IdentityAdder::class . '::onUserRegistered') => [Node::TYPE_PROCESS_MANAGER, IdentityAdder::class . '::onUserRegistered'], + Util::codeIdentifierToNodeId(Identity::class . '::add') => [Node::TYPE_AGGREGATE, Identity::class . '::add'], + Util::codeIdentifierToNodeId(Identity::class . '::addForUser') => [Node::TYPE_AGGREGATE, Identity::class . '::addForUser'], + Util::codeIdentifierToNodeId(Identity::class) => [Node::TYPE_AGGREGATE, Identity::class], + ]; + + $nodes = $msgFlow->nodes(); + + foreach ($expectedNodes as $nodeId => [$nodeType, $codeIdentifier]) { + $this->assertTrue(array_key_exists($nodeId, $nodes), "Missing node for $codeIdentifier"); + + $this->assertEquals($nodeType, $nodes[$nodeId]->type(), "Wrong node type for $codeIdentifier"); + } + + $expectedEdges = [ + (new Edge( + Util::codeIdentifierToNodeId(RegisterUserHandler::class . '::__invoke'), + Util::codeIdentifierToNodeId(User::class . '::register') + ))->id() => [RegisterUserHandler::class . '::__invoke', User::class . '::register'], + (new Edge( + Util::codeIdentifierToNodeId(User\Command\ChangeUsernameHandler::class . '::handle'), + Util::codeIdentifierToNodeId(User::class . '::changeUsername') + ))->id() => [User\Command\ChangeUsernameHandler::class . '::handle', User::class . '::changeUsername'], + (new Edge( + Util::codeIdentifierToNodeId(User\Command\AddUserIdentityHandler::class . '::__invoke'), + Util::codeIdentifierToNodeId(User::class . '::addIdentity') + ))->id() => [User\Command\AddUserIdentityHandler::class . '::__invoke', User::class . '::addIdentity'], + (new Edge( + Util::codeIdentifierToNodeId(User::class . '::addIdentity'), + Util::codeIdentifierToNodeId(Identity::class . '::add') + ))->id() => [User::class . '::addIdentity', Identity::class . '::add'], + (new Edge( + Util::codeIdentifierToNodeId(User::class . '::register'), + Util::codeIdentifierToNodeId(User::class . '::activate') + ))->id() => [User::class . '::register', User::class . '::activate'], + ]; + + $edges = $msgFlow->edges(); + + foreach ($expectedEdges as $edgeId => [$sourceIdentifier, $targetIdentifier]) { + $this->assertTrue(array_key_exists($edgeId, $edges), "Missing edge $sourceIdentifier -> $targetIdentifier"); + } } } diff --git a/tests/Sample/DefaultProject/Controller/UserController.php b/tests/Sample/DefaultProject/Controller/UserController.php index bfd5738..12288eb 100644 --- a/tests/Sample/DefaultProject/Controller/UserController.php +++ b/tests/Sample/DefaultProject/Controller/UserController.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Infrastucture/CommandBus.php b/tests/Sample/DefaultProject/Infrastucture/CommandBus.php index d7be52d..4ca674f 100644 --- a/tests/Sample/DefaultProject/Infrastucture/CommandBus.php +++ b/tests/Sample/DefaultProject/Infrastucture/CommandBus.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Infrastucture/ProophIdentityRepository.php b/tests/Sample/DefaultProject/Infrastucture/ProophIdentityRepository.php index 5edd51f..31fa574 100644 --- a/tests/Sample/DefaultProject/Infrastucture/ProophIdentityRepository.php +++ b/tests/Sample/DefaultProject/Infrastucture/ProophIdentityRepository.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Infrastucture/ProophUserRepository.php b/tests/Sample/DefaultProject/Infrastucture/ProophUserRepository.php index 270540c..f2cb077 100644 --- a/tests/Sample/DefaultProject/Infrastucture/ProophUserRepository.php +++ b/tests/Sample/DefaultProject/Infrastucture/ProophUserRepository.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Listener/SendConfirmationEmail.php b/tests/Sample/DefaultProject/Listener/SendConfirmationEmail.php index 725794a..c76248d 100644 --- a/tests/Sample/DefaultProject/Listener/SendConfirmationEmail.php +++ b/tests/Sample/DefaultProject/Listener/SendConfirmationEmail.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/EventProducerAbstract.php b/tests/Sample/DefaultProject/Model/EventProducerAbstract.php index d7850ad..9f4875f 100644 --- a/tests/Sample/DefaultProject/Model/EventProducerAbstract.php +++ b/tests/Sample/DefaultProject/Model/EventProducerAbstract.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/Identity.php b/tests/Sample/DefaultProject/Model/Identity.php index 138d799..5ec4f9a 100644 --- a/tests/Sample/DefaultProject/Model/Identity.php +++ b/tests/Sample/DefaultProject/Model/Identity.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,6 +14,7 @@ use Prooph\EventSourcing\AggregateChanged; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\Identity\Event\IdentityAdded; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\IdentityDeactivated; final class Identity extends EventProducerAbstract { @@ -36,6 +37,11 @@ public static function addForUser(string $identityId, string $userId): self return $self; } + public function deactivate(): void + { + $this->recordThat(IdentityDeactivated::occur($this->identityId, [])); + } + protected function aggregateId(): string { return $this->identityId; diff --git a/tests/Sample/DefaultProject/Model/Identity/Command/AddIdentity.php b/tests/Sample/DefaultProject/Model/Identity/Command/AddIdentity.php index bb6c6f8..4a9597f 100644 --- a/tests/Sample/DefaultProject/Model/Identity/Command/AddIdentity.php +++ b/tests/Sample/DefaultProject/Model/Identity/Command/AddIdentity.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/Identity/Event/IdentityAdded.php b/tests/Sample/DefaultProject/Model/Identity/Event/IdentityAdded.php index 380c761..79e9d64 100644 --- a/tests/Sample/DefaultProject/Model/Identity/Event/IdentityAdded.php +++ b/tests/Sample/DefaultProject/Model/Identity/Event/IdentityAdded.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/Identity/IdentityRepository.php b/tests/Sample/DefaultProject/Model/Identity/IdentityRepository.php index a763a0c..fdc7184 100644 --- a/tests/Sample/DefaultProject/Model/Identity/IdentityRepository.php +++ b/tests/Sample/DefaultProject/Model/Identity/IdentityRepository.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/User.php b/tests/Sample/DefaultProject/Model/User.php index eb4bf69..1c8177b 100644 --- a/tests/Sample/DefaultProject/Model/User.php +++ b/tests/Sample/DefaultProject/Model/User.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -14,6 +14,8 @@ use Prooph\EventSourcing\AggregateChanged; use Prooph\EventSourcing\AggregateRoot; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserActivated; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserDeactivated; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UsernameChanged; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserRegistered; @@ -26,6 +28,9 @@ public static function register(string $id): self $self = new self(); $self->recordThat(UserRegistered::occur($id, [])); + //Call another event recording method + $self->activate(); + return $self; } @@ -36,6 +41,16 @@ public function changeUsername(string $username): void $this->recordThat($usernameChanged); } + public function activate(): void + { + $this->recordThat(UserActivated::occur($this->userId, [])); + } + + public function deactivate(): void + { + $this->recordThat(UserDeactivated::occur($this->userId, [])); + } + public function addIdentity(string $identityId): Identity { return Identity::add($identityId); diff --git a/tests/Sample/DefaultProject/Model/User/Command/AbstractIdentityHandler.php b/tests/Sample/DefaultProject/Model/User/Command/AbstractIdentityHandler.php new file mode 100644 index 0000000..46af1a1 --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Command/AbstractIdentityHandler.php @@ -0,0 +1,34 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command; + +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\Identity; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\Identity\IdentityRepository; + +class AbstractIdentityHandler +{ + /** + * @var IdentityRepository + */ + protected $identityRepository; + + public function __construct(IdentityRepository $repository) + { + $this->identityRepository = $repository; + } + + public function getIdentity(string $identityId): Identity + { + return $this->identityRepository->get($identityId); + } +} diff --git a/tests/Sample/DefaultProject/Model/User/Command/ActivateUser.php b/tests/Sample/DefaultProject/Model/User/Command/ActivateUser.php new file mode 100644 index 0000000..b7a2b68 --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Command/ActivateUser.php @@ -0,0 +1,24 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command; + +use Prooph\Common\Messaging\Command; +use Prooph\Common\Messaging\PayloadConstructable; +use Prooph\Common\Messaging\PayloadTrait; + +class ActivateUser extends Command implements PayloadConstructable +{ + const NAME = 'ActivateUser'; + + use PayloadTrait; +} diff --git a/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentity.php b/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentity.php index ae131a7..0f2fa81 100644 --- a/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentity.php +++ b/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentity.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentityHandler.php b/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentityHandler.php index 9d7cf07..3093e8c 100644 --- a/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentityHandler.php +++ b/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentityHandler.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -15,22 +15,17 @@ use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\Identity\IdentityRepository; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\UserRepository; -class AddUserIdentityHandler +class AddUserIdentityHandler extends AbstractIdentityHandler { /** * @var UserRepository */ private $userRepository; - /** - * @var IdentityRepository - */ - private $identityRepository; - public function __construct(UserRepository $userRepository, IdentityRepository $identityRepository) { $this->userRepository = $userRepository; - $this->identityRepository = $identityRepository; + parent::__construct($identityRepository); } public function __invoke(AddUserIdentity $command) diff --git a/tests/Sample/DefaultProject/Model/User/Command/ChangeUsername.php b/tests/Sample/DefaultProject/Model/User/Command/ChangeUsername.php index 813e95c..6da3425 100644 --- a/tests/Sample/DefaultProject/Model/User/Command/ChangeUsername.php +++ b/tests/Sample/DefaultProject/Model/User/Command/ChangeUsername.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/User/Command/ChangeUsernameHandler.php b/tests/Sample/DefaultProject/Model/User/Command/ChangeUsernameHandler.php index f7ea9ad..7cb8007 100644 --- a/tests/Sample/DefaultProject/Model/User/Command/ChangeUsernameHandler.php +++ b/tests/Sample/DefaultProject/Model/User/Command/ChangeUsernameHandler.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/User/Command/DeactivateIdentity.php b/tests/Sample/DefaultProject/Model/User/Command/DeactivateIdentity.php new file mode 100644 index 0000000..34bcc52 --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Command/DeactivateIdentity.php @@ -0,0 +1,22 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command; + +use Prooph\Common\Messaging\Command; +use Prooph\Common\Messaging\PayloadConstructable; +use Prooph\Common\Messaging\PayloadTrait; + +class DeactivateIdentity extends Command implements PayloadConstructable +{ + use PayloadTrait; +} diff --git a/tests/Sample/DefaultProject/Model/User/Command/DeactivateIdentityHandler.php b/tests/Sample/DefaultProject/Model/User/Command/DeactivateIdentityHandler.php new file mode 100644 index 0000000..66f1212 --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Command/DeactivateIdentityHandler.php @@ -0,0 +1,25 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command; + +class DeactivateIdentityHandler extends AbstractIdentityHandler +{ + public function __invoke(DeactivateIdentity $command) + { + $identity = $this->getIdentity($command->payload()['identityId']); + + $identity->deactivate(); + + $this->identityRepository->save($identity); + } +} diff --git a/tests/Sample/DefaultProject/Model/User/Command/DeactivateUser.php b/tests/Sample/DefaultProject/Model/User/Command/DeactivateUser.php new file mode 100644 index 0000000..956400a --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Command/DeactivateUser.php @@ -0,0 +1,22 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command; + +use Prooph\Common\Messaging\Command; +use Prooph\Common\Messaging\PayloadConstructable; +use Prooph\Common\Messaging\PayloadTrait; + +class DeactivateUser extends Command implements PayloadConstructable +{ + use PayloadTrait; +} diff --git a/tests/Sample/DefaultProject/Model/User/Command/RegisterUser.php b/tests/Sample/DefaultProject/Model/User/Command/RegisterUser.php index b4054e4..97dffa4 100644 --- a/tests/Sample/DefaultProject/Model/User/Command/RegisterUser.php +++ b/tests/Sample/DefaultProject/Model/User/Command/RegisterUser.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/User/Command/RegisterUserHandler.php b/tests/Sample/DefaultProject/Model/User/Command/RegisterUserHandler.php index 64bded3..d69a9ef 100644 --- a/tests/Sample/DefaultProject/Model/User/Command/RegisterUserHandler.php +++ b/tests/Sample/DefaultProject/Model/User/Command/RegisterUserHandler.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/User/Event/IdentityActivated.php b/tests/Sample/DefaultProject/Model/User/Event/IdentityActivated.php new file mode 100644 index 0000000..62145cf --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Event/IdentityActivated.php @@ -0,0 +1,19 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event; + +use Prooph\EventSourcing\AggregateChanged; + +class IdentityActivated extends AggregateChanged +{ +} diff --git a/tests/Sample/DefaultProject/Model/User/Event/IdentityDeactivated.php b/tests/Sample/DefaultProject/Model/User/Event/IdentityDeactivated.php new file mode 100644 index 0000000..e2ab6aa --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Event/IdentityDeactivated.php @@ -0,0 +1,19 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event; + +use Prooph\EventSourcing\AggregateChanged; + +class IdentityDeactivated extends AggregateChanged +{ +} diff --git a/tests/Sample/DefaultProject/Model/User/Event/UserActivated.php b/tests/Sample/DefaultProject/Model/User/Event/UserActivated.php new file mode 100644 index 0000000..30f5da5 --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Event/UserActivated.php @@ -0,0 +1,19 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event; + +use Prooph\EventSourcing\AggregateChanged; + +class UserActivated extends AggregateChanged +{ +} diff --git a/tests/Sample/DefaultProject/Model/User/Event/UserDeactivated.php b/tests/Sample/DefaultProject/Model/User/Event/UserDeactivated.php new file mode 100644 index 0000000..823c057 --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Event/UserDeactivated.php @@ -0,0 +1,19 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event; + +use Prooph\EventSourcing\AggregateChanged; + +class UserDeactivated extends AggregateChanged +{ +} diff --git a/tests/Sample/DefaultProject/Model/User/Event/UserRegistered.php b/tests/Sample/DefaultProject/Model/User/Event/UserRegistered.php index e315267..774fa29 100644 --- a/tests/Sample/DefaultProject/Model/User/Event/UserRegistered.php +++ b/tests/Sample/DefaultProject/Model/User/Event/UserRegistered.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/User/Event/UsernameChanged.php b/tests/Sample/DefaultProject/Model/User/Event/UsernameChanged.php index 2bdbf73..46d7779 100644 --- a/tests/Sample/DefaultProject/Model/User/Event/UsernameChanged.php +++ b/tests/Sample/DefaultProject/Model/User/Event/UsernameChanged.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/Model/User/UserRepository.php b/tests/Sample/DefaultProject/Model/User/UserRepository.php index 23f743d..ebd18ad 100644 --- a/tests/Sample/DefaultProject/Model/User/UserRepository.php +++ b/tests/Sample/DefaultProject/Model/User/UserRepository.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/ProcessManager/IdentityAdder.php b/tests/Sample/DefaultProject/ProcessManager/IdentityAdder.php index 459a3be..dbfe798 100644 --- a/tests/Sample/DefaultProject/ProcessManager/IdentityAdder.php +++ b/tests/Sample/DefaultProject/ProcessManager/IdentityAdder.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/ProcessManager/SyncActiveStatus.php b/tests/Sample/DefaultProject/ProcessManager/SyncActiveStatus.php new file mode 100644 index 0000000..5397f08 --- /dev/null +++ b/tests/Sample/DefaultProject/ProcessManager/SyncActiveStatus.php @@ -0,0 +1,79 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\ProcessManager; + +use Prooph\Common\Messaging\MessageFactory; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Infrastucture\CommandBus; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ActivateUser; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\DeactivateIdentity; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\DeactivateUser; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\IdentityActivated; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\IdentityDeactivated; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserDeactivated; + +class SyncActiveStatus +{ + /** + * @var CommandBus + */ + private $commandBus; + + /** + * @var MessageFactory + */ + private $messageFactory; + + public function __construct(CommandBus $commandBus, MessageFactory $messageFactory) + { + $this->commandBus = $commandBus; + $this->messageFactory = $messageFactory; + } + + public function onUserDeactivated(UserDeactivated $event): void + { + $this->deactivateIdentities($event->payload()['userId']); + } + + public function deactivateIdentities(string $userId): void + { + //load identities of user + foreach ($this->loadIdentitesOfUser($userId) as $identityId) { + $this->commandBus->dispatch(new DeactivateIdentity(['identityId' => $identityId])); + } + } + + public function onIdentityDeactivated(IdentityDeactivated $event): void + { + foreach ($this->loadIdentitesOfUser($event->payload()['userId']) as $identityId) { + //... check if all identities are deactivated ... + $deactivateUser = $this->messageFactory->createMessageFromArray(DeactivateUser::class, []); + $this->commandBus->dispatch($deactivateUser); + } + } + + public function onIdentityActivated(IdentityActivated $event): void + { + //... check if user needs to be activated ... + $this->commandBus->dispatch( + $this->messageFactory->createMessageFromArray( + ActivateUser::NAME, + ['userId' => $event->payload()['userId']] + ) + ); + } + + private function loadIdentitesOfUser(string $userId): array + { + return []; + } +} diff --git a/tests/Sample/DefaultProject/prooph_analyzer.json b/tests/Sample/DefaultProject/prooph_analyzer.json index c08fd98..6e4a01c 100644 --- a/tests/Sample/DefaultProject/prooph_analyzer.json +++ b/tests/Sample/DefaultProject/prooph_analyzer.json @@ -8,9 +8,8 @@ ], "classVisitors": [ "MessageCollector", - "MessageHandlerCollector", - "MessageProducerCollector", - "EventRecorderCollector", - "EventRecorderInvokerCollector" + "CommandHandlerCollector", + "MessagingCollector", + "AggregateMethodCollector" ] } \ No newline at end of file diff --git a/tests/Sample/DefaultProject/tests/Model/UserTestSimulation.php b/tests/Sample/DefaultProject/tests/Model/UserTestSimulation.php index 4e1f6ad..2385a4d 100644 --- a/tests/Sample/DefaultProject/tests/Model/UserTestSimulation.php +++ b/tests/Sample/DefaultProject/tests/Model/UserTestSimulation.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Sample/DefaultProject/vendor/thirdparty/ThirdPartyQuery.php b/tests/Sample/DefaultProject/vendor/thirdparty/ThirdPartyQuery.php index 6c30fa0..974b4c2 100644 --- a/tests/Sample/DefaultProject/vendor/thirdparty/ThirdPartyQuery.php +++ b/tests/Sample/DefaultProject/vendor/thirdparty/ThirdPartyQuery.php @@ -1,8 +1,8 @@ - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. diff --git a/tests/Visitor/AggregateMethodCollectorTest.php b/tests/Visitor/AggregateMethodCollectorTest.php new file mode 100644 index 0000000..e078fee --- /dev/null +++ b/tests/Visitor/AggregateMethodCollectorTest.php @@ -0,0 +1,49 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Visitor; + +use Prooph\MessageFlowAnalyzer\Helper\Util; +use Prooph\MessageFlowAnalyzer\Visitor\AggregateMethodCollector; +use ProophTest\MessageFlowAnalyzer\BaseTestCase; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User; +use Roave\BetterReflection\Reflection\ReflectionClass; + +class AggregateMethodCollectorTest extends BaseTestCase +{ + /** + * @var AggregateMethodCollector + */ + private $cut; + + protected function setUp() + { + $this->cut = new AggregateMethodCollector(); + } + + /** + * @test + */ + public function it_detects_recording_of_events() + { + $msgFlow = $this->getDefaultProjectMessageFlow(); + + $user = ReflectionClass::createFromName(User::class); + + $msgFlow = $this->cut->onClassReflection($user, $msgFlow); + + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(User\Event\UserRegistered::class))); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(User\Event\UsernameChanged::class))); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(User::class.'::register'))); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(User::class.'::changeUsername'))); + } +} diff --git a/tests/Visitor/EventRecorderCollectorTest.php b/tests/Visitor/EventRecorderCollectorTest.php deleted file mode 100644 index a696f02..0000000 --- a/tests/Visitor/EventRecorderCollectorTest.php +++ /dev/null @@ -1,55 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ProophTest\MessageFlowAnalyzer\Visitor; - -use Prooph\MessageFlowAnalyzer\MessageFlow\EventRecorder; -use Prooph\MessageFlowAnalyzer\Visitor\EventRecorderCollector; -use ProophTest\MessageFlowAnalyzer\BaseTestCase; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User; -use Roave\BetterReflection\Reflection\ReflectionClass; - -class EventRecorderCollectorTest extends BaseTestCase -{ - /** - * @var EventRecorderCollector - */ - private $cut; - - protected function setUp() - { - $this->cut = new EventRecorderCollector(); - } - - /** - * @test - */ - public function it_detects_recording_of_events() - { - $msgFlow = $this->getDefaultProjectMessageFlow(); - - $user = ReflectionClass::createFromName(User::class); - - $msgFlow = $this->cut->onClassReflection($user, $msgFlow); - - $this->assertTrue($msgFlow->knowsMessage(User\Event\UserRegistered::class)); - $this->assertTrue($msgFlow->knowsMessage(User\Event\UsernameChanged::class)); - - $userRegistered = $msgFlow->getMessage(User\Event\UserRegistered::class); - $recorder = $userRegistered->recorders()[User::class.'::register']; - $this->assertInstanceOf(EventRecorder::class, $recorder); - - $usernameChanged = $msgFlow->getMessage(User\Event\UsernameChanged::class); - $recorder = $usernameChanged->recorders()[User::class.'::changeUsername']; - $this->assertInstanceOf(EventRecorder::class, $recorder); - } -} diff --git a/tests/Visitor/EventRecorderInvokerCollectorTest.php b/tests/Visitor/EventRecorderInvokerCollectorTest.php deleted file mode 100644 index ceb635e..0000000 --- a/tests/Visitor/EventRecorderInvokerCollectorTest.php +++ /dev/null @@ -1,109 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ProophTest\MessageFlowAnalyzer\Visitor; - -use Prooph\MessageFlowAnalyzer\MessageFlow\Message; -use Prooph\MessageFlowAnalyzer\MessageFlow\MessageHandler; -use Prooph\MessageFlowAnalyzer\Visitor\EventRecorderInvokerCollector; -use ProophTest\MessageFlowAnalyzer\BaseTestCase; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\Identity; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUser; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUserHandler; -use Roave\BetterReflection\Reflection\ReflectionClass; - -class EventRecorderInvokerCollectorTest extends BaseTestCase -{ - /** - * @var EventRecorderInvokerCollector - */ - private $cut; - - protected function setUp() - { - $this->cut = new EventRecorderInvokerCollector(); - } - - /** - * @test - */ - public function it_identifies_event_recorder_static_method_invoked_by_command_handler() - { - $msgFlow = $this->getDefaultProjectMessageFlow(); - - $registerUser = Message::fromReflectionClass(ReflectionClass::createFromName(RegisterUser::class)); - - $registerUserHandler = ReflectionClass::createFromName(RegisterUserHandler::class); - - $registerUser = $registerUser->addHandler(MessageHandler::fromReflectionMethod( - $registerUserHandler->getMethod('__invoke') - )); - - $msgFlow = $msgFlow->setMessage($registerUser); - - $msgFlow = $this->cut->onClassReflection($registerUserHandler, $msgFlow); - - $this->assertEquals([ - RegisterUserHandler::class.'::__invoke->'.User::class.'::register', - ], array_keys($msgFlow->eventRecorderInvokers())); - } - - /** - * @test - */ - public function it_identifies_event_recorder_method_invoked_by_command_handler() - { - $msgFlow = $this->getDefaultProjectMessageFlow(); - - $changeUsername = Message::fromReflectionClass(ReflectionClass::createFromName(User\Command\ChangeUsername::class)); - - $changeUsernameHandler = ReflectionClass::createFromName(User\Command\ChangeUsernameHandler::class); - - $changeUsername = $changeUsername->addHandler(MessageHandler::fromReflectionMethod( - $changeUsernameHandler->getMethod('handle') - )); - - $msgFlow = $msgFlow->setMessage($changeUsername); - - $msgFlow = $this->cut->onClassReflection($changeUsernameHandler, $msgFlow); - - $this->assertEquals([ - User\Command\ChangeUsernameHandler::class.'::handle->'.User::class.'::changeUsername', - ], array_keys($msgFlow->eventRecorderInvokers())); - } - - /** - * @test - */ - public function it_identifies_event_recorder_method_used_as_factory_for_another_event_recorder() - { - $msgFlow = $this->getDefaultProjectMessageFlow(); - - $addUserIdentity = Message::fromReflectionClass(ReflectionClass::createFromName(User\Command\AddUserIdentity::class)); - - $addUserIdentityHandler = ReflectionClass::createFromName(User\Command\AddUserIdentityHandler::class); - - $addUserIdentity = $addUserIdentity->addHandler(MessageHandler::fromReflectionMethod( - $addUserIdentityHandler->getMethod('__invoke') - )); - - $msgFlow = $msgFlow->setMessage($addUserIdentity); - - $msgFlow = $this->cut->onClassReflection($addUserIdentityHandler, $msgFlow); - - $this->assertEquals([ - User\Command\AddUserIdentityHandler::class.'::__invoke->'.User::class.'::addIdentity', - User::class.'::addIdentity->'.Identity::class.'::add', - ], array_keys($msgFlow->eventRecorderInvokers())); - } -} diff --git a/tests/Visitor/MessageCollectorTest.php b/tests/Visitor/MessageCollectorTest.php index 0525d3e..3608f78 100644 --- a/tests/Visitor/MessageCollectorTest.php +++ b/tests/Visitor/MessageCollectorTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,6 +12,7 @@ namespace ProophTest\MessageFlowAnalyzer\Visitor; +use Prooph\MessageFlowAnalyzer\MessageFlow\Message; use Prooph\MessageFlowAnalyzer\Visitor\MessageCollector; use ProophTest\MessageFlowAnalyzer\BaseTestCase; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUser; @@ -38,11 +39,11 @@ public function it_adds_message_to_message_flow() $registerUserRef = ReflectionClass::createFromName(RegisterUser::class); - $this->assertFalse($msgFlow->knowsMessage(RegisterUser::class)); + $this->assertFalse($msgFlow->knowsMessage(Message::fromReflectionClass($registerUserRef))); $msgFlow = $this->cut->onClassReflection($registerUserRef, $msgFlow); - $this->assertTrue($msgFlow->knowsMessage(RegisterUser::class)); + $this->assertTrue($msgFlow->knowsMessage(Message::fromReflectionClass($registerUserRef))); } /** diff --git a/tests/Visitor/MessageHandlerCollectorTest.php b/tests/Visitor/MessageHandlerCollectorTest.php index 366f62c..0ec5b35 100644 --- a/tests/Visitor/MessageHandlerCollectorTest.php +++ b/tests/Visitor/MessageHandlerCollectorTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); /** * This file is part of the prooph/message-flow-analyzer. - * (c) 2017-2017 prooph software GmbH - * (c) 2017-2017 Sascha-Oliver Prolic + * (c) 2017-2018 prooph software GmbH + * (c) 2017-2018 Sascha-Oliver Prolic * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -12,10 +12,14 @@ namespace ProophTest\MessageFlowAnalyzer\Visitor; +use Prooph\MessageFlowAnalyzer\Helper\Util; +use Prooph\MessageFlowAnalyzer\MessageFlow\Edge; use Prooph\MessageFlowAnalyzer\MessageFlow\Message; -use Prooph\MessageFlowAnalyzer\MessageFlow\MessageHandler; -use Prooph\MessageFlowAnalyzer\Visitor\MessageHandlerCollector; +use Prooph\MessageFlowAnalyzer\Visitor\CommandHandlerCollector; use ProophTest\MessageFlowAnalyzer\BaseTestCase; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\Identity; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\AddUserIdentityHandler; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUser; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUserHandler; use Roave\BetterReflection\Reflection\ReflectionClass; @@ -23,13 +27,13 @@ class MessageHandlerCollectorTest extends BaseTestCase { /** - * @var MessageHandlerCollector + * @var CommandHandlerCollector */ private $cut; protected function setUp() { - $this->cut = new MessageHandlerCollector(); + $this->cut = new CommandHandlerCollector(); } /** @@ -45,33 +49,44 @@ public function it_adds_handler_to_message_if_message_is_argument_of_a_handler_m $msgFlow = $this->cut->onClassReflection($handler, $msgFlow); - $this->assertTrue($msgFlow->knowsMessage(RegisterUser::class)); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(RegisterUserHandler::class . '::__invoke'))); + } - $registerUser = $msgFlow->getMessage(RegisterUser::class); + /** + * @test + */ + public function it_connects_handler_with_event_recorder_factory() + { + $msgFlow = $this->getDefaultProjectMessageFlow(); - $handler = $registerUser->handlers()[RegisterUserHandler::class . '::__invoke']; + $handler = ReflectionClass::createFromName(AddUserIdentityHandler::class); - $this->assertInstanceOf(MessageHandler::class, $handler); + $msgFlow = $this->cut->onClassReflection($handler, $msgFlow); + + $edge = new Edge( + Util::codeIdentifierToNodeId(AddUserIdentityHandler::class . '::__invoke'), + Util::codeIdentifierToNodeId(User::class . '::addIdentity') + ); + + $this->assertArrayHasKey($edge->id(), $msgFlow->edges()); } /** * @test */ - public function it_adds_message_if_it_is_not_known_by_message_flow() + public function it_connects_handler_with_invoked_event_recorder() { $msgFlow = $this->getDefaultProjectMessageFlow(); - $handler = ReflectionClass::createFromName(RegisterUserHandler::class); - - $updatedMsgFlow = $this->cut->onClassReflection($handler, $msgFlow); - - $this->assertFalse($msgFlow->knowsMessage(RegisterUser::class)); - $this->assertTrue($updatedMsgFlow->knowsMessage(RegisterUser::class)); + $handler = ReflectionClass::createFromName(User\Command\DeactivateIdentityHandler::class); - $registerUser = $updatedMsgFlow->getMessage(RegisterUser::class); + $msgFlow = $this->cut->onClassReflection($handler, $msgFlow); - $handler = $registerUser->handlers()[RegisterUserHandler::class . '::__invoke']; + $edge = new Edge( + Util::codeIdentifierToNodeId(User\Command\DeactivateIdentityHandler::class . '::__invoke'), + Util::codeIdentifierToNodeId(Identity::class . '::deactivate') + ); - $this->assertInstanceOf(MessageHandler::class, $handler); + $this->assertArrayHasKey($edge->id(), $msgFlow->edges()); } } diff --git a/tests/Visitor/MessageProducerCollectorTest.php b/tests/Visitor/MessageProducerCollectorTest.php deleted file mode 100644 index e5009c3..0000000 --- a/tests/Visitor/MessageProducerCollectorTest.php +++ /dev/null @@ -1,81 +0,0 @@ - - * (c) 2017-2017 Sascha-Oliver Prolic - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ProophTest\MessageFlowAnalyzer\Visitor; - -use Prooph\MessageFlowAnalyzer\MessageFlow\MessageProducer; -use Prooph\MessageFlowAnalyzer\Visitor\MessageProducerCollector; -use ProophTest\MessageFlowAnalyzer\BaseTestCase; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Controller\UserController; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\Identity\Command\AddIdentity; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ChangeUsername; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUser; -use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\ProcessManager\IdentityAdder; -use Roave\BetterReflection\Reflection\ReflectionClass; - -class MessageProducerCollectorTest extends BaseTestCase -{ - /** - * @var MessageProducerCollector - */ - private $cut; - - protected function setUp() - { - $this->cut = new MessageProducerCollector(); - } - - /** - * @test - */ - public function it_adds_producer_if_a_method_creates_a_message_using_new_class() - { - $msgFlow = $this->getDefaultProjectMessageFlow(); - - $identityAdder = ReflectionClass::createFromName(IdentityAdder::class); - - $msgFlow = $this->cut->onClassReflection($identityAdder, $msgFlow); - - $this->assertTrue($msgFlow->knowsMessage(AddIdentity::class)); - - $addIdentity = $msgFlow->getMessage(AddIdentity::class); - - $producer = $addIdentity->producers()[IdentityAdder::class.'::onUserRegistered'] ?? null; - - $this->assertInstanceOf(MessageProducer::class, $producer); - } - - /** - * @test - */ - public function it_adds_producer_if_a_method_creates_a_message_using_named_constructor() - { - $msgFlow = $this->getDefaultProjectMessageFlow(); - - $userController = ReflectionClass::createFromName(UserController::class); - - $msgFlow = $this->cut->onClassReflection($userController, $msgFlow); - - //Uses self as return type of named constructor - $this->assertTrue($msgFlow->knowsMessage(RegisterUser::class)); - //Uses message class as return type of named constructor - $this->assertTrue($msgFlow->knowsMessage(ChangeUsername::class)); - - $registerUser = $msgFlow->getMessage(RegisterUser::class); - $producer = $registerUser->producers()[UserController::class.'::postAction'] ?? null; - $this->assertInstanceOf(MessageProducer::class, $producer); - - $changeUsername = $msgFlow->getMessage(ChangeUsername::class); - $producer = $changeUsername->producers()[UserController::class.'::patchAction'] ?? null; - $this->assertInstanceOf(MessageProducer::class, $producer); - } -} diff --git a/tests/Visitor/MessagingCollectorTest.php b/tests/Visitor/MessagingCollectorTest.php new file mode 100644 index 0000000..6ccb8b5 --- /dev/null +++ b/tests/Visitor/MessagingCollectorTest.php @@ -0,0 +1,194 @@ + + * (c) 2017-2018 Sascha-Oliver Prolic + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ProophTest\MessageFlowAnalyzer\Visitor; + +use Prooph\MessageFlowAnalyzer\Helper\MessageClassProvider; +use Prooph\MessageFlowAnalyzer\Helper\Util; +use Prooph\MessageFlowAnalyzer\MessageFlow\Edge; +use Prooph\MessageFlowAnalyzer\MessageFlow\Node; +use Prooph\MessageFlowAnalyzer\Visitor\MessagingCollector; +use ProophTest\MessageFlowAnalyzer\BaseTestCase; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Controller\UserController; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\Identity\Command\AddIdentity; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ActivateUser; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ChangeUsername; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\DeactivateIdentity; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\DeactivateUser; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUser; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\IdentityActivated; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\IdentityDeactivated; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserDeactivated; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\ProcessManager\IdentityAdder; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\ProcessManager\SyncActiveStatus; +use Prophecy\Argument; +use Roave\BetterReflection\Reflection\ReflectionClass; + +class MessagingCollectorTest extends BaseTestCase +{ + /** + * @var MessagingCollector + */ + private $cut; + + protected function setUp() + { + $this->cut = new MessagingCollector(); + } + + /** + * @test + */ + public function it_adds_process_manager_if_a_method_creates_a_message_using_new_class_and_consumes_event() + { + $msgFlow = $this->getDefaultProjectMessageFlow(); + + $identityAdder = ReflectionClass::createFromName(IdentityAdder::class); + + $msgFlow = $this->cut->onClassReflection($identityAdder, $msgFlow); + + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(AddIdentity::class))); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(IdentityAdder::class.'::onUserRegistered'))); + + $node = $msgFlow->getNode(Util::codeIdentifierToNodeId(IdentityAdder::class.'::onUserRegistered')); + + $this->assertEquals(Node::TYPE_PROCESS_MANAGER, $node->type()); + } + + /** + * @test + */ + public function it_adds_producer_if_a_method_creates_a_message_using_named_constructor() + { + $msgFlow = $this->getDefaultProjectMessageFlow(); + + $userController = ReflectionClass::createFromName(UserController::class); + + $msgFlow = $this->cut->onClassReflection($userController, $msgFlow); + + //Uses self as return type of named constructor + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(RegisterUser::class))); + //Uses message class as return type of named constructor + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(ChangeUsername::class))); + + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(UserController::class.'::postAction'))); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(UserController::class.'::patchAction'))); + } + + /** + * @test + */ + public function it_adds_process_manager_if_event_consuming_method_invokes_command_producing_method() + { + $msgFlow = $this->getDefaultProjectMessageFlow(); + + $processManager = ReflectionClass::createFromName(SyncActiveStatus::class); + + $msgFlow = $this->cut->onClassReflection($processManager, $msgFlow); + + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(SyncActiveStatus::class.'::onUserDeactivated'))); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(SyncActiveStatus::class.'::deactivateIdentities'))); + + $listenerNode = $msgFlow->getNode(Util::codeIdentifierToNodeId(SyncActiveStatus::class.'::onUserDeactivated')); + $producerNode = $msgFlow->getNode(Util::codeIdentifierToNodeId(SyncActiveStatus::class.'::deactivateIdentities')); + + $this->assertEquals(Node::TYPE_PROCESS_MANAGER, $listenerNode->type()); + $this->assertEquals(Node::TYPE_PROCESS_MANAGER, $producerNode->type()); + + $this->assertArrayHasKey( + (new Edge( + Util::codeIdentifierToNodeId(UserDeactivated::class), + Util::codeIdentifierToNodeId(SyncActiveStatus::class . '::onUserDeactivated') + ))->id(), + $msgFlow->edges() + ); + + $this->assertArrayHasKey( + (new Edge( + Util::codeIdentifierToNodeId(SyncActiveStatus::class . '::onUserDeactivated'), + Util::codeIdentifierToNodeId(SyncActiveStatus::class . '::deactivateIdentities') + ))->id(), + $msgFlow->edges() + ); + + $this->assertArrayHasKey( + (new Edge( + Util::codeIdentifierToNodeId(SyncActiveStatus::class . '::deactivateIdentities'), + Util::codeIdentifierToNodeId(DeactivateIdentity::class) + ))->id(), + $msgFlow->edges() + ); + } + + /** + * @test + */ + public function it_detects_message_production_if_message_factory_is_used() + { + $msgFlow = $this->getDefaultProjectMessageFlow(); + + $processManager = ReflectionClass::createFromName(SyncActiveStatus::class); + + $msgFlow = $this->cut->onClassReflection($processManager, $msgFlow); + + $this->assertArrayHasKey( + (new Edge( + Util::codeIdentifierToNodeId(IdentityDeactivated::class), + Util::codeIdentifierToNodeId(SyncActiveStatus::class . '::onIdentityDeactivated') + ))->id(), + $msgFlow->edges() + ); + + $this->assertArrayHasKey( + (new Edge( + Util::codeIdentifierToNodeId(SyncActiveStatus::class . '::onIdentityDeactivated'), + Util::codeIdentifierToNodeId(DeactivateUser::class) + ))->id(), + $msgFlow->edges() + ); + } + + /** + * @test + */ + public function it_detects_message_production_if_message_factory_is_used_and_message_name_is_referenced_by_a_constant() + { + $msgFlow = $this->getDefaultProjectMessageFlow(); + + $processManager = ReflectionClass::createFromName(SyncActiveStatus::class); + + $messageClassProvider = $this->prophesize(MessageClassProvider::class); + $messageClassProvider->provideClass(Argument::exact('ActivateUser'))->willReturn(ActivateUser::class)->shouldBeCalled(); + $messageClassProvider->provideClass(Argument::not(Argument::exact('ActivateUser')))->will(function ($args) { + return $args[0]; + }); + MessagingCollector::useMessageClassProvider($messageClassProvider->reveal()); + + $msgFlow = $this->cut->onClassReflection($processManager, $msgFlow); + + $this->assertArrayHasKey( + (new Edge( + Util::codeIdentifierToNodeId(IdentityActivated::class), + Util::codeIdentifierToNodeId(SyncActiveStatus::class . '::onIdentityActivated') + ))->id(), + $msgFlow->edges() + ); + + $this->assertArrayHasKey( + (new Edge( + Util::codeIdentifierToNodeId(SyncActiveStatus::class . '::onIdentityActivated'), + Util::codeIdentifierToNodeId(ActivateUser::class) + ))->id(), + $msgFlow->edges() + ); + } +}