From 7292abd3962ffb740ea4d625469576b48fe68c7c Mon Sep 17 00:00:00 2001 From: codeliner Date: Thu, 22 Mar 2018 15:48:29 +0100 Subject: [PATCH 01/35] v0.2 Refactoring Collect nodes and edges within the message flow --- LICENSE | 4 +- bin/prooph-analyzer | 4 +- src/Cli/AnalyzeProjectCommand.php | 4 +- src/Filter/ExcludeHiddenFileInfo.php | 4 +- src/Filter/ExcludeTestsDir.php | 4 +- src/Filter/ExcludeVendorDir.php | 4 +- src/Filter/FileInfoFilter.php | 4 +- src/Filter/IncludePHPFile.php | 4 +- src/Helper/MessageProducingMethodScanner.php | 23 +- src/Helper/PhpParser/MessageScanner.php | 4 +- .../MessageScanningNodeTraverser.php | 4 +- src/Helper/PhpParser/ScanHelper.php | 102 ++- src/Helper/ProjectTraverserFactory.php | 20 +- src/Helper/Util.php | 8 +- src/MessageFlow.php | 201 +++-- src/MessageFlow/Edge.php | 64 ++ src/MessageFlow/EventRecorder.php | 4 +- src/MessageFlow/EventRecorderInvoker.php | 4 +- src/MessageFlow/Message.php | 129 +-- src/MessageFlow/MessageHandler.php | 4 +- .../MessageHandlingMethodAbstract.php | 4 +- src/MessageFlow/MessageProducer.php | 4 +- src/MessageFlow/Node.php | 738 ++++++++++++++++++ src/MessageFlow/NodeFactory.php | 74 ++ src/Output/Formatter.php | 4 +- src/Output/JsonArangoGraphNodes.php | 30 +- src/Output/JsonCytoscapeElements.php | 27 +- src/Output/JsonPrettyPrint.php | 4 +- src/ProjectTraverser.php | 4 +- src/Visitor/AggregateMethodCollector.php | 76 ++ src/Visitor/ClassVisitor.php | 4 +- src/Visitor/CommandHandlerCollector.php | 63 ++ src/Visitor/EventListenerCollector.php | 67 ++ src/Visitor/EventRecorderCollector.php | 37 - src/Visitor/EventRecorderInvokerCollector.php | 133 ---- src/Visitor/FileInfoVisitor.php | 4 +- src/Visitor/MessageCollector.php | 6 +- src/Visitor/MessageHandlerCollector.php | 65 -- src/Visitor/MessageIOCollector.php | 17 - src/Visitor/MessageProducerCollector.php | 43 +- tests/BaseTestCase.php | 4 +- tests/Filter/ExcludeHiddenFileInfoTest.php | 4 +- tests/Filter/ExcludeTestsDirTest.php | 4 +- tests/Filter/ExcludeVendorDirTest.php | 4 +- tests/Filter/IncludePHPFileTest.php | 4 +- tests/Helper/PhpParser/ScanHelperTest.php | 4 +- tests/MessageFlow/EventRecorderTest.php | 4 +- tests/MessageFlow/MessageTest.php | 7 +- tests/MessageFlowTest.php | 27 +- tests/ProjectTraverserTest.php | 136 ++-- .../Controller/UserController.php | 4 +- .../Infrastucture/CommandBus.php | 4 +- .../ProophIdentityRepository.php | 4 +- .../Infrastucture/ProophUserRepository.php | 4 +- .../Listener/SendConfirmationEmail.php | 4 +- .../Model/EventProducerAbstract.php | 4 +- .../Sample/DefaultProject/Model/Identity.php | 4 +- .../Model/Identity/Command/AddIdentity.php | 4 +- .../Model/Identity/Event/IdentityAdded.php | 4 +- .../Model/Identity/IdentityRepository.php | 4 +- tests/Sample/DefaultProject/Model/User.php | 4 +- .../Model/User/Command/AddUserIdentity.php | 4 +- .../User/Command/AddUserIdentityHandler.php | 4 +- .../Model/User/Command/ChangeUsername.php | 4 +- .../User/Command/ChangeUsernameHandler.php | 4 +- .../Model/User/Command/RegisterUser.php | 4 +- .../User/Command/RegisterUserHandler.php | 4 +- .../Model/User/Event/UserRegistered.php | 4 +- .../Model/User/Event/UsernameChanged.php | 4 +- .../Model/User/UserRepository.php | 4 +- .../ProcessManager/IdentityAdder.php | 4 +- .../DefaultProject/prooph_analyzer.json | 3 +- .../tests/Model/UserTestSimulation.php | 4 +- .../vendor/thirdparty/ThirdPartyQuery.php | 4 +- .../Visitor/AggregateMethodCollectorTest.php | 49 ++ tests/Visitor/EventRecorderCollectorTest.php | 55 -- .../EventRecorderInvokerCollectorTest.php | 109 --- tests/Visitor/MessageCollectorTest.php | 9 +- tests/Visitor/MessageHandlerCollectorTest.php | 41 +- .../Visitor/MessageProducerCollectorTest.php | 28 +- 80 files changed, 1637 insertions(+), 950 deletions(-) create mode 100644 src/MessageFlow/Edge.php create mode 100644 src/MessageFlow/Node.php create mode 100644 src/MessageFlow/NodeFactory.php create mode 100644 src/Visitor/AggregateMethodCollector.php create mode 100644 src/Visitor/CommandHandlerCollector.php create mode 100644 src/Visitor/EventListenerCollector.php delete mode 100644 src/Visitor/EventRecorderCollector.php delete mode 100644 src/Visitor/EventRecorderInvokerCollector.php delete mode 100644 src/Visitor/MessageHandlerCollector.php delete mode 100644 src/Visitor/MessageIOCollector.php create mode 100644 tests/Visitor/AggregateMethodCollectorTest.php delete mode 100644 tests/Visitor/EventRecorderCollectorTest.php delete mode 100644 tests/Visitor/EventRecorderInvokerCollectorTest.php 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/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/src/Cli/AnalyzeProjectCommand.php b/src/Cli/AnalyzeProjectCommand.php index c371ba6..5a4d922 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. 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/MessageProducingMethodScanner.php b/src/Helper/MessageProducingMethodScanner.php index 459f3ee..dc0a881 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,21 @@ trait MessageProducingMethodScanner private $nodeTraverser; private function checkMessageProduction( + MessageFlow $msgFlow, ReflectionClass $reflectionClass, - callable $addMethodToMessageCb, - MessageFlow $msgFlow): MessageFlow - { + callable $onMessageProducingMethodCb, + callable $onNonMessageProducingMethodCb = null + ): 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); + + if (count($messages)) { + foreach ($messages as $message) { + $message = $msgFlow->getMessage($message->name(), $message); + $msgFlow = $onMessageProducingMethodCb($msgFlow, $message, $method); + } + } elseif ($onNonMessageProducingMethodCb) { + $msgFlow = $onNonMessageProducingMethodCb($msgFlow, $method); } } diff --git a/src/Helper/PhpParser/MessageScanner.php b/src/Helper/PhpParser/MessageScanner.php index 14363f4..97e0ba9 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. 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..0c54393 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,7 +15,10 @@ use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; +use Prooph\Common\Messaging\Message as ProophMsg; +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; @@ -185,6 +188,101 @@ public function getEventRecorderVariables(): array return $nodeVisitor->getEventRecorderVariables(); } + 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); + } + private static function isEventRecorderRepositoryParameter(ReflectionParameter $parameter, bool $inspectChildParameters = true): ?ReflectionClass { if (! $parameter->hasType()) { diff --git a/src/Helper/ProjectTraverserFactory.php b/src/Helper/ProjectTraverserFactory.php index 85846c7..1843027 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. @@ -17,14 +17,12 @@ use Prooph\MessageFlowAnalyzer\Filter\ExcludeVendorDir; use Prooph\MessageFlowAnalyzer\Filter\IncludePHPFile; 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; final class ProjectTraverserFactory @@ -38,18 +36,16 @@ 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, ]; public static $fileInfoVisitorAliases = []; public static $outputFormatterAliases = [ 'JsonPrettyPrint' => JsonPrettyPrint::class, - 'JsonArangoGraphNodes' => JsonArangoGraphNodes::class, - 'JsonCytoscapeElements' => JsonCytoscapeElements::class, ]; public static function buildTraverserFromConfig(array $config): ProjectTraverser 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..525f601 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[] - */ - private $messages; - - /** - * @var EventRecorderInvoker[] + * @var Node[] */ - private $eventRecorderInvokers; + private $nodes = []; /** - * @var array + * @var Edge[] */ - private $attributes; + private $edges = []; /** - * Internal cache which is reset when a new command message is set + * Internal command handler cache + * + * Is reset when a new command is added * - * @var string[] + * @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 $this->nodes; + } + + /** + * @return Edge[] indexed by edge id + */ + public function edges(): array { - return array_key_exists($messageName, $this->messages); + return $this->edges; } - public function getMessage(string $name, Message $efault = null): ?Message + public function knowsMessage(Message $message): bool { - if (! array_key_exists($name, $this->messages())) { - return $efault; + 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,71 +133,80 @@ 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 knowsEdge(Edge $edge): bool { - if ($this->knowsMessage($msg->name())) { - throw new \RuntimeException('Message is already known. Got ' . $msg->name()); + return array_key_exists($edge->id(), $this->edges); + } + + public function addEdge(Edge $edge): self + { + if ($this->knowsEdge($edge)) { + throw new \RuntimeException("Edge with id {$edge->id()} is already set. Got " . json_encode($edge->toArray())); } - return $this->setMessage($msg); + return $this->setEdge($edge); } - public function setMessage(Message $msg): self + public function setEdge(Edge $edge): self { $cp = clone $this; - $cp->messages[$msg->name()] = $msg; - - //Reset internal cmd handler cache - if ($msg->type() === ProophMsg::TYPE_COMMAND) { - $cp->commandHandlers = null; - } + $cp->edges[$edge->id()] = $edge; return $cp; } @@ -199,13 +221,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 +236,12 @@ public function toArray(): array return [ 'project' => $this->project, 'rootDir' => $this->rootDir, - 'flow' => $this->flowToArray(), + 'nodes' => array_map(function (Node $node): array { + return $node->toArray(); + }, $this->nodes), + 'edges' => array_map(function (Edge $edge): array { + return $edge->toArray(); + }, $this->edges), ]; } @@ -235,38 +258,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..ad6c158 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. 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/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..89ef9c1 --- /dev/null +++ b/src/MessageFlow/Node.php @@ -0,0 +1,738 @@ + + * (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 $title; + + /** + * 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 connect with another node + * + * Example: + * - Command Handler method handling a command (connected node) + * - Aggregate method called by a command handler method (connected node) + * - Process manager method listening on event (connected 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; + + /** + * 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 string/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('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('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('fa-shield-check')->withColor('#EECA51'); + } + + /** + * 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('fa-industry')->withColor('#EECA51'); + } + + 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('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('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, + $messageProducer->identifier(), + $messageProducer->filename(), + null, + $messageProducer->class(), + $messageProducer->function() + ))->withTag($message->type())->withTag('producer')->withIcon('fa-cogs')->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']['title'] ?? '', + $nodeData['data']['filename'] ?? '', + $nodeData['data']['description'] ?? null, + $nodeData['data']['class'] ?? null, + $nodeData['data']['method'] ?? null, + $nodeData['data']['funcName'] ?? null, + $nodeData['data']['parent'] ?? null, + $tags, + $nodeData['data']['icon'] ?? null, + $nodeData['data']['color'] ?? null, + $nodeData['data']['schema'] ?? null + ); + } + + private function __construct( + string $id, + string $type, + string $title, + string $filename, + string $description = null, + string $class = null, + string $method = null, + string $funcName = null, + string $parent = null, + array $tags = [], + string $icon = null, + string $color = 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 ($title === '') { + throw new \InvalidArgumentException('Node title 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->title = $title; + $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->schema = $schema; + + foreach ($tags as $tag) { + $this->tags[$tag] = null; + } + } + + public function toArray(): array + { + return [ + 'data' => [ + 'id' => $this->id, + 'type' => $this->type, + 'title' => $this->title, + 'filename' => $this->filename, + 'description' => $this->description, + 'class' => $this->class, + 'method' => $this->method, + 'funcName' => $this->funcName, + 'parent' => $this->parent, + 'icon' => $this->icon, + 'color' => $this->color, + '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 title(): string + { + return $this->title; + } + + /** + * @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 string + */ + public function icon(): string + { + return $this->icon; + } + + /** + * @return null|string + */ + public function color(): ?string + { + return $this->color; + } + + /** + * @return array|null + */ + public function schema(): ?array + { + return $this->schema; + } + + public function withTitle(string $title): self + { + $cp = clone $this; + $cp->title = $title; + + 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(string $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 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/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 index c11b055..5dcafc1 100644 --- a/src/Output/JsonArangoGraphNodes.php +++ b/src/Output/JsonArangoGraphNodes.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. @@ -25,7 +25,7 @@ public function messageFlowToString(MessageFlow $messageFlow): string $eventRecorderClasses = []; foreach ($messageFlow->messages() as $message) { - $msgKey = Util::identifierToKey($message->name()); + $msgKey = Util::codeIdentifierToNodeId($message->name()); $messages[$msgKey] = [ '_key' => $msgKey, 'type' => $message->type(), @@ -34,7 +34,7 @@ public function messageFlowToString(MessageFlow $messageFlow): string ]; foreach ($message->handlers() as $handler) { - $handlerKey = Util::identifierToKey($handler->identifier()); + $handlerKey = Util::codeIdentifierToNodeId($handler->identifier()); $handlers[$handlerKey] = [ '_key' => $handlerKey, 'name' => $handler->isClass() ? Util::withoutNamespace($handler->class()) : $handler->function(), @@ -49,7 +49,7 @@ public function messageFlowToString(MessageFlow $messageFlow): string } foreach ($message->producers() as $producer) { - $producerKey = Util::identifierToKey($producer->identifier()); + $producerKey = Util::codeIdentifierToNodeId($producer->identifier()); $handlers[$producerKey] = [ '_key' => $producerKey, 'name' => $producer->isClass() ? Util::withoutNamespace($producer->class()) : $producer->function(), @@ -68,7 +68,7 @@ public function messageFlowToString(MessageFlow $messageFlow): string $eventRecorderClasses[$recorder->class()] = $recorder; } - $recorderKey = Util::identifierToKey($recorder->identifier()); + $recorderKey = Util::codeIdentifierToNodeId($recorder->identifier()); $handlers[$recorderKey] = [ '_key' => $recorderKey, 'name' => $recorder->isClass() ? Util::withoutNamespace($recorder->class()).'::'.$recorder->function() : $recorder->function(), @@ -77,7 +77,7 @@ public function messageFlowToString(MessageFlow $messageFlow): string ]; if ($recorder->isClass()) { - $recorderClassKey = Util::identifierToKey(Util::identifierWithoutMethod($recorder->identifier())); + $recorderClassKey = Util::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($recorder->identifier())); $handlers[$recorderClassKey] = [ '_key' => $recorderClassKey, @@ -104,11 +104,11 @@ public function messageFlowToString(MessageFlow $messageFlow): string } $isEventRecorderClass = function (string $identifier) use ($eventRecorderClasses): bool { - return array_key_exists(Util::identifierWithoutMethod($identifier), $eventRecorderClasses); + return array_key_exists(Util::codeIdentifierWithoutMethod($identifier), $eventRecorderClasses); }; $getEventRecorderFactory = function (string $identifer) use ($eventRecorderClasses): MessageFlow\EventRecorder { - $recorderClass = Util::identifierWithoutMethod($identifer); + $recorderClass = Util::codeIdentifierWithoutMethod($identifer); $factoryMethod = str_replace($recorderClass.MessageFlow\MessageHandlingMethodAbstract::ID_METHOD_DELIMITER, '', $identifer); $orgEventRecorder = $eventRecorderClasses[$recorderClass]->toArray(); @@ -128,8 +128,8 @@ public function messageFlowToString(MessageFlow $messageFlow): string //Add handler for 1.), see above $eventRecorderFactory = $getEventRecorderFactory($eventRecorderInvoker->invokerIdentifier()); - $handlers[Util::identifierToKey($eventRecorderFactory->identifier())] = [ - '_key' => Util::identifierToKey($eventRecorderFactory->identifier()), + $handlers[Util::codeIdentifierToNodeId($eventRecorderFactory->identifier())] = [ + '_key' => Util::codeIdentifierToNodeId($eventRecorderFactory->identifier()), 'name' => Util::withoutNamespace($eventRecorderFactory->class()).'::'.$eventRecorderFactory->function(), 'class' => $eventRecorderFactory->class(), 'function' => $eventRecorderFactory->function(), @@ -137,15 +137,15 @@ public function messageFlowToString(MessageFlow $messageFlow): string //Add 1. edge for factory case (see above) $edges[] = [ - '_from' => 'handlers/'.Util::identifierToKey(Util::identifierWithoutMethod($eventRecorderInvoker->invokerIdentifier())), - '_to' => 'handlers/'.Util::identifierToKey($eventRecorderInvoker->invokerIdentifier()), + '_from' => 'handlers/'.Util::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($eventRecorderInvoker->invokerIdentifier())), + '_to' => 'handlers/'.Util::codeIdentifierToNodeId($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())), + '_from' => 'handlers/'.Util::codeIdentifierToNodeId($eventRecorderInvoker->invokerIdentifier()), + '_to' => 'handlers/'.Util::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($eventRecorderInvoker->eventRecorderIdentifier())), ]; } diff --git a/src/Output/JsonCytoscapeElements.php b/src/Output/JsonCytoscapeElements.php index 05f9ab0..354b3d2 100644 --- a/src/Output/JsonCytoscapeElements.php +++ b/src/Output/JsonCytoscapeElements.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. @@ -24,7 +24,7 @@ public function messageFlowToString(MessageFlow $messageFlow): string $eventRecorderClasses = []; foreach ($messageFlow->messages() as $message) { - $msgKey = Util::identifierToKey($message->name()); + $msgKey = Util::codeIdentifierToNodeId($message->name()); $nodes[] = [ 'data' => [ 'id' => $msgKey, @@ -33,11 +33,10 @@ public function messageFlowToString(MessageFlow $messageFlow): string 'class' => $message->class(), ], 'classes' => 'message '.$message->type(), - ]; foreach ($message->handlers() as $handler) { - $handlerKey = Util::identifierToKey($handler->identifier()); + $handlerKey = Util::codeIdentifierToNodeId($handler->identifier()); $nodes[] = [ 'data' => [ 'id' => $handlerKey, @@ -58,7 +57,7 @@ public function messageFlowToString(MessageFlow $messageFlow): string } foreach ($message->producers() as $producer) { - $producerKey = Util::identifierToKey($producer->identifier()); + $producerKey = Util::codeIdentifierToNodeId($producer->identifier()); $nodes[] = [ 'data' => [ 'id' => $producerKey, @@ -81,7 +80,7 @@ public function messageFlowToString(MessageFlow $messageFlow): string foreach ($message->recorders() as $recorder) { $parent = null; if ($recorder->isClass()) { - $parent = Util::identifierToKey(Util::identifierWithoutMethod($recorder->identifier())); + $parent = Util::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($recorder->identifier())); $nodes[] = [ 'data' => [ 'id' => $parent, @@ -94,7 +93,7 @@ public function messageFlowToString(MessageFlow $messageFlow): string $eventRecorderClasses[$recorder->class()] = $recorder; } - $recorderKey = Util::identifierToKey($recorder->identifier()); + $recorderKey = Util::codeIdentifierToNodeId($recorder->identifier()); $data = [ 'id' => $recorderKey, @@ -123,11 +122,11 @@ public function messageFlowToString(MessageFlow $messageFlow): string } $isEventRecorderClass = function (string $identifier) use ($eventRecorderClasses): bool { - return array_key_exists(Util::identifierWithoutMethod($identifier), $eventRecorderClasses); + return array_key_exists(Util::codeIdentifierWithoutMethod($identifier), $eventRecorderClasses); }; $getEventRecorderFactory = function (string $identifer) use ($eventRecorderClasses): MessageFlow\EventRecorder { - $recorderClass = Util::identifierWithoutMethod($identifer); + $recorderClass = Util::codeIdentifierWithoutMethod($identifer); $factoryMethod = str_replace($recorderClass.MessageFlow\MessageHandlingMethodAbstract::ID_METHOD_DELIMITER, '', $identifer); $orgEventRecorder = $eventRecorderClasses[$recorderClass]->toArray(); @@ -137,8 +136,8 @@ public function messageFlowToString(MessageFlow $messageFlow): string }; foreach ($messageFlow->eventRecorderInvokers() as $eventRecorderInvoker) { - $eventRecorderInvokerKey = Util::identifierToKey($eventRecorderInvoker->invokerIdentifier()); - $eventRecorderKey = Util::identifierToKey($eventRecorderInvoker->eventRecorderIdentifier()); + $eventRecorderInvokerKey = Util::codeIdentifierToNodeId($eventRecorderInvoker->invokerIdentifier()); + $eventRecorderKey = Util::codeIdentifierToNodeId($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: @@ -149,14 +148,14 @@ public function messageFlowToString(MessageFlow $messageFlow): string //Add handler for 1.), see above $eventRecorderFactory = $getEventRecorderFactory($eventRecorderInvoker->invokerIdentifier()); - $eventRecorderFactoryKey = Util::identifierToKey($eventRecorderFactory->identifier()); + $eventRecorderFactoryKey = Util::codeIdentifierToNodeId($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())), + 'parent' => Util::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($eventRecorderFactory->identifier())), ], 'classes' => 'event factory', ]; 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..46339f2 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. diff --git a/src/Visitor/AggregateMethodCollector.php b/src/Visitor/AggregateMethodCollector.php new file mode 100644 index 0000000..a804353 --- /dev/null +++ b/src/Visitor/AggregateMethodCollector.php @@ -0,0 +1,76 @@ + + * (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)); + } + + return $messageFlow->addEdge(new MessageFlow\Edge($eventRecorderNode->id(), $msgNode->id())); + }, + 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->addEdge(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..2086f95 --- /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->addEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($message->name()), $node->id())); + + $eventRecorders = ScanHelper::findInvokedEventRecorders($handler); + + foreach ($eventRecorders as $eventRecorder) { + $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge($node->id(), Util::codeIdentifierToNodeId($eventRecorder->identifier()))); + } + + return $messageFlow; + } +} diff --git a/src/Visitor/EventListenerCollector.php b/src/Visitor/EventListenerCollector.php new file mode 100644 index 0000000..3d67ce9 --- /dev/null +++ b/src/Visitor/EventListenerCollector.php @@ -0,0 +1,67 @@ + + * (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\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 EventListenerCollector implements ClassVisitor +{ + use MessageProducingMethodScanner; + + 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_EVENT) { + return $messageFlow; + } + + $producedMessages = $this->checkMethodProducesMessages($method); + + if ($producedMessages) { + //Looks like a process manager or saga, not a simple event listener + return $messageFlow; + } + + $handler = MessageFlow\MessageHandler::fromReflectionMethod($method); + + $node = MessageFlow\NodeFactory::createEventListenerNode($handler); + + if (! $messageFlow->knowsNode($node)) { + $messageFlow = $messageFlow->addNode($node); + } + + $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($message->name()), $node->id())); + + 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..f33696e 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. @@ -26,7 +26,7 @@ public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow } $msg = MessageFlow\Message::fromReflectionClass($reflectionClass); - if (! $messageFlow->knowsMessage($msg->name())) { + if (! $messageFlow->knowsMessage($msg)) { $messageFlow = $messageFlow->addMessage($msg); } } 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 index 98f06d0..f5f88ec 100644 --- a/src/Visitor/MessageProducerCollector.php +++ b/src/Visitor/MessageProducerCollector.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\Common\Messaging\Message as ProophMsg; 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; @@ -33,11 +35,40 @@ public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow } return $this->checkMessageProduction( + $messageFlow, $reflectionClass, - function (MessageFlow\Message $message, ReflectionMethod $method): MessageFlow\Message { - return $message->addProducer(MessageFlow\MessageProducer::fromReflectionMethod($method)); - }, - $messageFlow + function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMethod $method): MessageFlow { + $msgNode = MessageFlow\NodeFactory::createMessageNode($message); + + if (! $messageFlow->knowsNode($msgNode)) { + $messageFlow = $messageFlow->addMessage($message); + } + + $receivedMsg = ScanHelper::checkIfMethodHandlesMessage($messageFlow, $method); + + $messageProducer = MessageFlow\MessageProducer::fromReflectionMethod($method); + + //process manager or saga that receives event and produces command + if ($receivedMsg) { + //@TODO: Can we identify a Saga here? + $pmNode = MessageFlow\NodeFactory::createProcessManagerNode($messageProducer); + + if (! $messageFlow->knowsNode($pmNode)) { + $messageFlow = $messageFlow->addNode($pmNode); + } + + $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($receivedMsg->name()), $pmNode->id())); + $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge($pmNode->id(), $msgNode->id())); + } else { + $messageProducerNode = MessageFlow\NodeFactory::createMessageProducingServiceNode($messageProducer, $message); + + if (! $messageFlow->knowsNode($messageProducerNode)) { + $messageFlow = $messageFlow->addNode($messageProducerNode); + } + } + + return $messageFlow; + } ); } } diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 1faf589..40fd5bd 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. 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..64f5ab0 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,11 +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\EventListenerCollector; use Prooph\MessageFlowAnalyzer\Visitor\MessageCollector; -use Prooph\MessageFlowAnalyzer\Visitor\MessageHandlerCollector; use Prooph\MessageFlowAnalyzer\Visitor\MessageProducerCollector; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Controller\UserController; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Listener\SendConfirmationEmail; @@ -31,7 +34,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 +54,68 @@ public function it_collects_message_flow() ], [ new MessageCollector(), - new MessageHandlerCollector(), + new CommandHandlerCollector(), new MessageProducerCollector(), - new EventRecorderCollector(), - new EventRecorderInvokerCollector(), + new AggregateMethodCollector(), + new EventListenerCollector(), ] ); - $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(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) => [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'], + ]; + + $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..a080df1 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. 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..085d800 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. 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..c92733e 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. 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/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/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/prooph_analyzer.json b/tests/Sample/DefaultProject/prooph_analyzer.json index c08fd98..80c8112 100644 --- a/tests/Sample/DefaultProject/prooph_analyzer.json +++ b/tests/Sample/DefaultProject/prooph_analyzer.json @@ -10,7 +10,6 @@ "MessageCollector", "MessageHandlerCollector", "MessageProducerCollector", - "EventRecorderCollector", - "EventRecorderInvokerCollector" + "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..eb3c1ea 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,9 +12,9 @@ namespace ProophTest\MessageFlowAnalyzer\Visitor; +use Prooph\MessageFlowAnalyzer\Helper\Util; 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\User\Command\RegisterUser; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUserHandler; @@ -23,13 +23,13 @@ class MessageHandlerCollectorTest extends BaseTestCase { /** - * @var MessageHandlerCollector + * @var CommandHandlerCollector */ private $cut; protected function setUp() { - $this->cut = new MessageHandlerCollector(); + $this->cut = new CommandHandlerCollector(); } /** @@ -45,33 +45,6 @@ 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)); - - $registerUser = $msgFlow->getMessage(RegisterUser::class); - - $handler = $registerUser->handlers()[RegisterUserHandler::class . '::__invoke']; - - $this->assertInstanceOf(MessageHandler::class, $handler); - } - - /** - * @test - */ - public function it_adds_message_if_it_is_not_known_by_message_flow() - { - $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)); - - $registerUser = $updatedMsgFlow->getMessage(RegisterUser::class); - - $handler = $registerUser->handlers()[RegisterUserHandler::class . '::__invoke']; - - $this->assertInstanceOf(MessageHandler::class, $handler); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(RegisterUserHandler::class . '::__invoke'))); } } diff --git a/tests/Visitor/MessageProducerCollectorTest.php b/tests/Visitor/MessageProducerCollectorTest.php index e5009c3..32d0ad6 100644 --- a/tests/Visitor/MessageProducerCollectorTest.php +++ b/tests/Visitor/MessageProducerCollectorTest.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,7 @@ namespace ProophTest\MessageFlowAnalyzer\Visitor; -use Prooph\MessageFlowAnalyzer\MessageFlow\MessageProducer; +use Prooph\MessageFlowAnalyzer\Helper\Util; use Prooph\MessageFlowAnalyzer\Visitor\MessageProducerCollector; use ProophTest\MessageFlowAnalyzer\BaseTestCase; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Controller\UserController; @@ -45,13 +45,8 @@ public function it_adds_producer_if_a_method_creates_a_message_using_new_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); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(AddIdentity::class))); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(IdentityAdder::class.'::onUserRegistered'))); } /** @@ -66,16 +61,11 @@ public function it_adds_producer_if_a_method_creates_a_message_using_named_const $msgFlow = $this->cut->onClassReflection($userController, $msgFlow); //Uses self as return type of named constructor - $this->assertTrue($msgFlow->knowsMessage(RegisterUser::class)); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(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); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(ChangeUsername::class))); - $changeUsername = $msgFlow->getMessage(ChangeUsername::class); - $producer = $changeUsername->producers()[UserController::class.'::patchAction'] ?? null; - $this->assertInstanceOf(MessageProducer::class, $producer); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(UserController::class.'::postAction'))); + $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(UserController::class.'::patchAction'))); } } From a1590d6abb22476d20bde3eb861c4df63dbf2598 Mon Sep 17 00:00:00 2001 From: codeliner Date: Thu, 22 Mar 2018 15:50:43 +0100 Subject: [PATCH 02/35] Rm no longer supported output formatters --- src/Output/JsonArangoGraphNodes.php | 158 ------------------------ src/Output/JsonCytoscapeElements.php | 178 --------------------------- 2 files changed, 336 deletions(-) delete mode 100644 src/Output/JsonArangoGraphNodes.php delete mode 100644 src/Output/JsonCytoscapeElements.php diff --git a/src/Output/JsonArangoGraphNodes.php b/src/Output/JsonArangoGraphNodes.php deleted file mode 100644 index 5dcafc1..0000000 --- a/src/Output/JsonArangoGraphNodes.php +++ /dev/null @@ -1,158 +0,0 @@ - - * (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\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::codeIdentifierToNodeId($message->name()); - $messages[$msgKey] = [ - '_key' => $msgKey, - 'type' => $message->type(), - 'name' => Util::withoutNamespace($message->name()), - 'class' => $message->class(), - ]; - - foreach ($message->handlers() as $handler) { - $handlerKey = Util::codeIdentifierToNodeId($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::codeIdentifierToNodeId($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::codeIdentifierToNodeId($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::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($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::codeIdentifierWithoutMethod($identifier), $eventRecorderClasses); - }; - - $getEventRecorderFactory = function (string $identifer) use ($eventRecorderClasses): MessageFlow\EventRecorder { - $recorderClass = Util::codeIdentifierWithoutMethod($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::codeIdentifierToNodeId($eventRecorderFactory->identifier())] = [ - '_key' => Util::codeIdentifierToNodeId($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::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($eventRecorderInvoker->invokerIdentifier())), - '_to' => 'handlers/'.Util::codeIdentifierToNodeId($eventRecorderInvoker->invokerIdentifier()), - ]; - } - - //Add 2. egde for factory case (see above), or normal message handler invokes event recorder case - $edges[] = [ - '_from' => 'handlers/'.Util::codeIdentifierToNodeId($eventRecorderInvoker->invokerIdentifier()), - '_to' => 'handlers/'.Util::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($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 354b3d2..0000000 --- a/src/Output/JsonCytoscapeElements.php +++ /dev/null @@ -1,178 +0,0 @@ - - * (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\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::codeIdentifierToNodeId($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::codeIdentifierToNodeId($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::codeIdentifierToNodeId($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::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($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::codeIdentifierToNodeId($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::codeIdentifierWithoutMethod($identifier), $eventRecorderClasses); - }; - - $getEventRecorderFactory = function (string $identifer) use ($eventRecorderClasses): MessageFlow\EventRecorder { - $recorderClass = Util::codeIdentifierWithoutMethod($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::codeIdentifierToNodeId($eventRecorderInvoker->invokerIdentifier()); - $eventRecorderKey = Util::codeIdentifierToNodeId($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::codeIdentifierToNodeId($eventRecorderFactory->identifier()); - $nodes[] = [ - 'data' => [ - 'id' => $eventRecorderFactoryKey, - 'name' => Util::withoutNamespace($eventRecorderFactory->class()).'::'.$eventRecorderFactory->function(), - 'class' => $eventRecorderFactory->class(), - 'function' => $eventRecorderFactory->function(), - 'parent' => Util::codeIdentifierToNodeId(Util::codeIdentifierWithoutMethod($eventRecorderFactory->identifier())), - ], - 'classes' => 'event factory', - ]; - } - - $edges[] = [ - 'data' => [ - 'id' => $eventRecorderInvokerKey.'_'.$eventRecorderKey, - 'source' => $eventRecorderInvokerKey, - 'target' => $eventRecorderKey, - ], - ]; - } - - return json_encode([ - 'nodes' => $nodes, - 'edges' => $edges, - ], JSON_PRETTY_PRINT); - } -} From 4e155853d6c46ce125c42bb9f0d5f94cb66714e4 Mon Sep 17 00:00:00 2001 From: codeliner Date: Thu, 22 Mar 2018 16:25:27 +0100 Subject: [PATCH 03/35] Catch IdentifierNotFound errors --- src/Visitor/MessageCollector.php | 19 ++++++++++++------- .../DefaultProject/prooph_analyzer.json | 5 +++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Visitor/MessageCollector.php b/src/Visitor/MessageCollector.php index f33696e..bfad2a1 100644 --- a/src/Visitor/MessageCollector.php +++ b/src/Visitor/MessageCollector.php @@ -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)) { - $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/tests/Sample/DefaultProject/prooph_analyzer.json b/tests/Sample/DefaultProject/prooph_analyzer.json index 80c8112..7c4d3e1 100644 --- a/tests/Sample/DefaultProject/prooph_analyzer.json +++ b/tests/Sample/DefaultProject/prooph_analyzer.json @@ -8,8 +8,9 @@ ], "classVisitors": [ "MessageCollector", - "MessageHandlerCollector", + "CommandHandlerCollector", "MessageProducerCollector", - "AggregateMethodCollector" + "AggregateMethodCollector", + "EventListenerCollector" ] } \ No newline at end of file From fa6b8f860b6a91ba0cee615681ada583476fccae Mon Sep 17 00:00:00 2001 From: codeliner Date: Thu, 22 Mar 2018 16:31:59 +0100 Subject: [PATCH 04/35] Nodes and edges should be an array --- src/MessageFlow.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/MessageFlow.php b/src/MessageFlow.php index 525f601..94cc314 100644 --- a/src/MessageFlow.php +++ b/src/MessageFlow.php @@ -236,12 +236,12 @@ public function toArray(): array return [ 'project' => $this->project, 'rootDir' => $this->rootDir, - 'nodes' => array_map(function (Node $node): array { + 'nodes' => array_values(array_map(function (Node $node): array { return $node->toArray(); - }, $this->nodes), - 'edges' => array_map(function (Edge $edge): array { + }, $this->nodes)), + 'edges' => array_values(array_map(function (Edge $edge): array { return $edge->toArray(); - }, $this->edges), + }, $this->edges)), ]; } From 02495d59266546c16bbe4ee200934fd8011fe6dd Mon Sep 17 00:00:00 2001 From: codeliner Date: Thu, 22 Mar 2018 16:41:22 +0100 Subject: [PATCH 05/35] Use node name instead of title --- src/MessageFlow/Node.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/MessageFlow/Node.php b/src/MessageFlow/Node.php index 89ef9c1..b66a75f 100644 --- a/src/MessageFlow/Node.php +++ b/src/MessageFlow/Node.php @@ -55,7 +55,7 @@ class Node * * @var string */ - private $title; + private $name; /** * File containing the class/function @@ -384,7 +384,7 @@ public static function fromArray(array $nodeData) return new self( $nodeData['data']['id'] ?? '', $nodeData['data']['type'] ?? '', - $nodeData['data']['title'] ?? '', + $nodeData['data']['name'] ?? '', $nodeData['data']['filename'] ?? '', $nodeData['data']['description'] ?? null, $nodeData['data']['class'] ?? null, @@ -401,7 +401,7 @@ public static function fromArray(array $nodeData) private function __construct( string $id, string $type, - string $title, + string $name, string $filename, string $description = null, string $class = null, @@ -421,8 +421,8 @@ private function __construct( throw new \InvalidArgumentException('Node type must not be empty'); } - if ($title === '') { - throw new \InvalidArgumentException('Node title must not be empty'); + if ($name === '') { + throw new \InvalidArgumentException('Node name must not be empty'); } array_walk($tags, function (string $tag) { @@ -433,7 +433,7 @@ private function __construct( $this->id = $id; $this->type = $type; - $this->title = $title; + $this->name = $name; $this->filename = $filename; $this->description = $description; $this->class = $class; @@ -455,7 +455,7 @@ public function toArray(): array 'data' => [ 'id' => $this->id, 'type' => $this->type, - 'title' => $this->title, + 'name' => $this->name, 'filename' => $this->filename, 'description' => $this->description, 'class' => $this->class, @@ -489,9 +489,9 @@ public function type(): string /** * @return string */ - public function title(): string + public function name(): string { - return $this->title; + return $this->name; } /** @@ -574,10 +574,10 @@ public function schema(): ?array return $this->schema; } - public function withTitle(string $title): self + public function withName(string $name): self { $cp = clone $this; - $cp->title = $title; + $cp->name = $name; return $cp; } From 8ee50eb2e1fe380b4cf9fffd81b55767cb547942 Mon Sep 17 00:00:00 2001 From: codeliner Date: Thu, 22 Mar 2018 23:01:48 +0100 Subject: [PATCH 06/35] Replace pro icon with free one --- src/MessageFlow/Node.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MessageFlow/Node.php b/src/MessageFlow/Node.php index b66a75f..5871ec6 100644 --- a/src/MessageFlow/Node.php +++ b/src/MessageFlow/Node.php @@ -256,7 +256,7 @@ public static function asEventRecordingAggregateMethod(EventRecorder $eventRecor $eventRecorder->function(), null, Util::codeIdentifierToNodeId($eventRecorder->class()) - ))->withTag('event')->withTag('recorder')->withIcon('fa-shield-check')->withColor('#EECA51'); + ))->withTag('event')->withTag('recorder')->withIcon('fa-chevron-circle-right')->withColor('#EECA51'); } /** From 4efa79beea40b526bb523cf912c57fb34eedc9dc Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Mar 2018 00:20:55 +0100 Subject: [PATCH 07/35] Introduce parentColor --- src/MessageFlow/Node.php | 46 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/MessageFlow/Node.php b/src/MessageFlow/Node.php index 5871ec6..27507fb 100644 --- a/src/MessageFlow/Node.php +++ b/src/MessageFlow/Node.php @@ -114,6 +114,17 @@ class Node */ 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 * @@ -228,7 +239,7 @@ public static function asAggregate(MessageHandlingMethodAbstract $aggregateMetho $aggregateMethod->filename(), null, $aggregateMethod->class() - ))->withTag('parent')->withColor('#e9f2f7'); + ))->withTag('parent')->withColor('#E9F2F7'); } /** @@ -256,7 +267,9 @@ public static function asEventRecordingAggregateMethod(EventRecorder $eventRecor $eventRecorder->function(), null, Util::codeIdentifierToNodeId($eventRecorder->class()) - ))->withTag('event')->withTag('recorder')->withIcon('fa-chevron-circle-right')->withColor('#EECA51'); + ))->withTag('event')->withTag('recorder') + ->withIcon('fa-chevron-circle-right')->withColor('#EECA51') + ->withParentColor('#E9F2F7'); } /** @@ -284,7 +297,9 @@ public static function asAggregateFactoryMethod(MessageHandlingMethodAbstract $e $eventRecorderInvoker->function(), null, Util::codeIdentifierToNodeId($eventRecorderInvoker->class()) - ))->withTag('event')->withTag('factory')->withIcon('fa-industry')->withColor('#EECA51'); + ))->withTag('event')->withTag('factory') + ->withIcon('fa-industry')->withColor('#EECA51') + ->withParentColor('#E9F2F7'); } public static function asEventListener(MessageHandler $messageHandler): self @@ -394,6 +409,7 @@ public static function fromArray(array $nodeData) $tags, $nodeData['data']['icon'] ?? null, $nodeData['data']['color'] ?? null, + $nodeData['data']['parentColor'] ?? null, $nodeData['data']['schema'] ?? null ); } @@ -411,6 +427,7 @@ private function __construct( array $tags = [], string $icon = null, string $color = null, + string $parentColor = null, array $schema = null ) { if ($id === '') { @@ -442,6 +459,7 @@ private function __construct( $this->parent = $parent; $this->icon = $icon; $this->color = $color; + $this->parentColor = $parentColor; $this->schema = $schema; foreach ($tags as $tag) { @@ -464,6 +482,7 @@ public function toArray(): array 'parent' => $this->parent, 'icon' => $this->icon, 'color' => $this->color, + 'parentColor' => $this->parentColor, 'schema' => $this->schema, ], 'classes' => implode(' ', $this->withTag($this->type)->tags()), @@ -566,6 +585,11 @@ public function color(): ?string return $this->color; } + public function parentColor(): ?string + { + return $this->parentColor; + } + /** * @return array|null */ @@ -720,6 +744,22 @@ public function withoutColor(): self 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; From c85b8bdbcd46d3f0e17f99e298aac2f099aa51bf Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Mar 2018 15:26:59 +0100 Subject: [PATCH 08/35] Use NodeIcon and expand options to also use fa brand/regular, links --- src/MessageFlow/Node.php | 28 ++++++------ src/MessageFlow/NodeIcon.php | 84 ++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 src/MessageFlow/NodeIcon.php diff --git a/src/MessageFlow/Node.php b/src/MessageFlow/Node.php index 27507fb..b3f1f97 100644 --- a/src/MessageFlow/Node.php +++ b/src/MessageFlow/Node.php @@ -137,7 +137,7 @@ class Node * * If not set a circle is used as default shape * - * @var string/null + * @var NodeIcon/null */ private $icon = null; @@ -181,7 +181,7 @@ public static function asMessage(Message $message): self $message->filename(), null, $message->class() - ))->withTag('message')->withIcon('fa-envelope')->withColor($color); + ))->withTag('message')->withIcon(NodeIcon::faSolid('fa-envelope'))->withColor($color); } /** @@ -214,7 +214,7 @@ public static function asCommandHandler(MessageHandler $handler): self null, $handler->class(), $handler->function() - ))->withTag('command')->withIcon('fa-sign-out-alt')->withColor('#1B1C1D'); + ))->withTag('command')->withIcon(NodeIcon::faSolid('fa-sign-out-alt'))->withColor('#1B1C1D'); } /** @@ -268,7 +268,7 @@ public static function asEventRecordingAggregateMethod(EventRecorder $eventRecor null, Util::codeIdentifierToNodeId($eventRecorder->class()) ))->withTag('event')->withTag('recorder') - ->withIcon('fa-chevron-circle-right')->withColor('#EECA51') + ->withIcon(NodeIcon::faSolid('fa-chevron-circle-right'))->withColor('#EECA51') ->withParentColor('#E9F2F7'); } @@ -298,7 +298,7 @@ public static function asAggregateFactoryMethod(MessageHandlingMethodAbstract $e null, Util::codeIdentifierToNodeId($eventRecorderInvoker->class()) ))->withTag('event')->withTag('factory') - ->withIcon('fa-industry')->withColor('#EECA51') + ->withIcon(NodeIcon::faSolid('fa-industry'))->withColor('#EECA51') ->withParentColor('#E9F2F7'); } @@ -324,7 +324,7 @@ public static function asEventListener(MessageHandler $messageHandler): self null, $messageHandler->class(), $messageHandler->function() - ))->withTag('event')->withIcon('fa-bell')->withColor('#6435C9'); + ))->withTag('event')->withIcon(NodeIcon::faSolid('fa-bell'))->withColor('#6435C9'); } /** @@ -357,7 +357,7 @@ public static function asProcessManager(MessageProducer $messageProducer): self null, $messageProducer->class(), $messageProducer->function() - ))->withTag('command')->withTag('producer')->withIcon('fa-cogs')->withColor('#715671'); + ))->withTag('command')->withTag('producer')->withIcon(NodeIcon::faSolid('fa-cogs'))->withColor('#715671'); } /** @@ -383,7 +383,7 @@ public static function asMessageProducingService(MessageProducer $messageProduce null, $messageProducer->class(), $messageProducer->function() - ))->withTag($message->type())->withTag('producer')->withIcon('fa-cogs')->withColor('#1B1C1D'); + ))->withTag($message->type())->withTag('producer')->withIcon(NodeIcon::faSolid('fa-cogs'))->withColor('#1B1C1D'); } public static function fromArray(array $nodeData) @@ -407,7 +407,7 @@ public static function fromArray(array $nodeData) $nodeData['data']['funcName'] ?? null, $nodeData['data']['parent'] ?? null, $tags, - $nodeData['data']['icon'] ?? null, + NodeIcon::fromString($nodeData['data']['icon']) ?? null, $nodeData['data']['color'] ?? null, $nodeData['data']['parentColor'] ?? null, $nodeData['data']['schema'] ?? null @@ -425,7 +425,7 @@ private function __construct( string $funcName = null, string $parent = null, array $tags = [], - string $icon = null, + NodeIcon $icon = null, string $color = null, string $parentColor = null, array $schema = null @@ -480,7 +480,7 @@ public function toArray(): array 'method' => $this->method, 'funcName' => $this->funcName, 'parent' => $this->parent, - 'icon' => $this->icon, + 'icon' => (string) $this->icon, 'color' => $this->color, 'parentColor' => $this->parentColor, 'schema' => $this->schema, @@ -570,9 +570,9 @@ public function tags(): array } /** - * @return string + * @return NodeIcon|null */ - public function icon(): string + public function icon(): ?NodeIcon { return $this->icon; } @@ -712,7 +712,7 @@ public function withoutTags(): self return $cp; } - public function withIcon(string $icon): self + public function withIcon(NodeIcon $icon): self { $cp = clone $this; $cp->icon = $icon; 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; + } +} From 43d88f303e78f01fff346aa3a2f55573e6db46dd Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Mar 2018 20:26:51 +0100 Subject: [PATCH 09/35] Check if event recorder calls others recording methods --- src/Helper/PhpParser/ScanHelper.php | 80 +++++++++++++++++++ src/Visitor/AggregateMethodCollector.php | 13 ++- tests/MessageFlowTest.php | 1 + tests/ProjectTraverserTest.php | 6 ++ tests/Sample/DefaultProject/Model/User.php | 9 +++ .../Model/User/Event/UserActivated.php | 20 +++++ 6 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tests/Sample/DefaultProject/Model/User/Event/UserActivated.php diff --git a/src/Helper/PhpParser/ScanHelper.php b/src/Helper/PhpParser/ScanHelper.php index 0c54393..d2674d8 100644 --- a/src/Helper/PhpParser/ScanHelper.php +++ b/src/Helper/PhpParser/ScanHelper.php @@ -21,6 +21,7 @@ use Prooph\MessageFlowAnalyzer\MessageFlow\Message; use Prooph\MessageFlowAnalyzer\MessageFlow\MessageHandler; use Roave\BetterReflection\Reflection\ReflectionClass; +use Roave\BetterReflection\Reflection\ReflectionFunctionAbstract; use Roave\BetterReflection\Reflection\ReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionParameter; @@ -188,6 +189,85 @@ public function getEventRecorderVariables(): array return $nodeVisitor->getEventRecorderVariables(); } + /** + * @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 && $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(); diff --git a/src/Visitor/AggregateMethodCollector.php b/src/Visitor/AggregateMethodCollector.php index a804353..06e42c4 100644 --- a/src/Visitor/AggregateMethodCollector.php +++ b/src/Visitor/AggregateMethodCollector.php @@ -51,7 +51,18 @@ function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMeth $messageFlow = $messageFlow->addNode(MessageFlow\NodeFactory::createAggregateNode($eventRecorder)); } - return $messageFlow->addEdge(new MessageFlow\Edge($eventRecorderNode->id(), $msgNode->id())); + $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge($eventRecorderNode->id(), $msgNode->id())); + + $invokedEventRecorders = ScanHelper::checkIfEventRecorderMethodCallsOtherEventRecorders($eventRecorder); + + foreach ($invokedEventRecorders as $invokedEventRecorder) { + $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge( + Util::codeIdentifierToNodeId($eventRecorder->identifier()), + Util::codeIdentifierToNodeId($invokedEventRecorder->identifier())) + ); + } + + return $messageFlow; }, function (MessageFlow $messageFlow, ReflectionMethod $method): MessageFlow { $eventRecorder = MessageFlow\EventRecorder::fromReflectionMethod($method); diff --git a/tests/MessageFlowTest.php b/tests/MessageFlowTest.php index 2c36ee5..29630b6 100644 --- a/tests/MessageFlowTest.php +++ b/tests/MessageFlowTest.php @@ -19,6 +19,7 @@ use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ChangeUsernameHandler; 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\UserActivated; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserRegistered; use Roave\BetterReflection\Reflection\ReflectionClass; diff --git a/tests/ProjectTraverserTest.php b/tests/ProjectTraverserTest.php index 64f5ab0..9e5ef4e 100644 --- a/tests/ProjectTraverserTest.php +++ b/tests/ProjectTraverserTest.php @@ -73,11 +73,13 @@ public function it_collects_message_flow() 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'], @@ -110,6 +112,10 @@ public function it_collects_message_flow() 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(); diff --git a/tests/Sample/DefaultProject/Model/User.php b/tests/Sample/DefaultProject/Model/User.php index 085d800..c4be725 100644 --- a/tests/Sample/DefaultProject/Model/User.php +++ b/tests/Sample/DefaultProject/Model/User.php @@ -14,6 +14,7 @@ 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\UsernameChanged; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserRegistered; @@ -26,6 +27,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 +40,11 @@ public function changeUsername(string $username): void $this->recordThat($usernameChanged); } + public function activate(): void + { + $this->recordThat(UserActivated::occur($this->userId, [])); + } + public function addIdentity(string $identityId): Identity { return Identity::add($identityId); 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..0794b3e --- /dev/null +++ b/tests/Sample/DefaultProject/Model/User/Event/UserActivated.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 ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event; + +use Prooph\EventSourcing\AggregateChanged; + +class UserActivated extends AggregateChanged +{ + +} \ No newline at end of file From 37699d45da4acf9a8dcc63e989c3093e13f70f53 Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Mar 2018 20:35:51 +0100 Subject: [PATCH 10/35] Check if invoked recorders are found --- src/Visitor/AggregateMethodCollector.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Visitor/AggregateMethodCollector.php b/src/Visitor/AggregateMethodCollector.php index 06e42c4..057cdad 100644 --- a/src/Visitor/AggregateMethodCollector.php +++ b/src/Visitor/AggregateMethodCollector.php @@ -55,11 +55,13 @@ function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMeth $invokedEventRecorders = ScanHelper::checkIfEventRecorderMethodCallsOtherEventRecorders($eventRecorder); - foreach ($invokedEventRecorders as $invokedEventRecorder) { - $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge( - Util::codeIdentifierToNodeId($eventRecorder->identifier()), - Util::codeIdentifierToNodeId($invokedEventRecorder->identifier())) - ); + if($invokedEventRecorders) { + foreach ($invokedEventRecorders as $invokedEventRecorder) { + $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge( + Util::codeIdentifierToNodeId($eventRecorder->identifier()), + Util::codeIdentifierToNodeId($invokedEventRecorder->identifier())) + ); + } } return $messageFlow; From 208c261ec2d05e51b5754343f8631e903be92ddc Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Mar 2018 20:36:34 +0100 Subject: [PATCH 11/35] CS fixes --- src/Helper/PhpParser/ScanHelper.php | 5 ++--- src/Visitor/AggregateMethodCollector.php | 2 +- tests/MessageFlowTest.php | 1 - .../Sample/DefaultProject/Model/User/Event/UserActivated.php | 3 +-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Helper/PhpParser/ScanHelper.php b/src/Helper/PhpParser/ScanHelper.php index d2674d8..fa9287a 100644 --- a/src/Helper/PhpParser/ScanHelper.php +++ b/src/Helper/PhpParser/ScanHelper.php @@ -21,7 +21,6 @@ use Prooph\MessageFlowAnalyzer\MessageFlow\Message; use Prooph\MessageFlowAnalyzer\MessageFlow\MessageHandler; use Roave\BetterReflection\Reflection\ReflectionClass; -use Roave\BetterReflection\Reflection\ReflectionFunctionAbstract; use Roave\BetterReflection\Reflection\ReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionParameter; @@ -195,7 +194,7 @@ public function getEventRecorderVariables(): array */ public static function checkIfEventRecorderMethodCallsOtherEventRecorders(EventRecorder $eventRecorder): ?array { - if(!$eventRecorder->isClass()) { + if (! $eventRecorder->isClass()) { return []; } @@ -220,7 +219,7 @@ public function leaveNode(Node $node) $producedMsgs = $this->checkMethodProducesMessages($calledMethod); - if(count($producedMsgs)) { + if (count($producedMsgs)) { $this->eventRecorders[] = EventRecorder::fromReflectionMethod($calledMethod); } } diff --git a/src/Visitor/AggregateMethodCollector.php b/src/Visitor/AggregateMethodCollector.php index 057cdad..ecf037a 100644 --- a/src/Visitor/AggregateMethodCollector.php +++ b/src/Visitor/AggregateMethodCollector.php @@ -55,7 +55,7 @@ function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMeth $invokedEventRecorders = ScanHelper::checkIfEventRecorderMethodCallsOtherEventRecorders($eventRecorder); - if($invokedEventRecorders) { + if ($invokedEventRecorders) { foreach ($invokedEventRecorders as $invokedEventRecorder) { $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge( Util::codeIdentifierToNodeId($eventRecorder->identifier()), diff --git a/tests/MessageFlowTest.php b/tests/MessageFlowTest.php index 29630b6..2c36ee5 100644 --- a/tests/MessageFlowTest.php +++ b/tests/MessageFlowTest.php @@ -19,7 +19,6 @@ use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\ChangeUsernameHandler; 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\UserActivated; use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserRegistered; use Roave\BetterReflection\Reflection\ReflectionClass; diff --git a/tests/Sample/DefaultProject/Model/User/Event/UserActivated.php b/tests/Sample/DefaultProject/Model/User/Event/UserActivated.php index 0794b3e..30f5da5 100644 --- a/tests/Sample/DefaultProject/Model/User/Event/UserActivated.php +++ b/tests/Sample/DefaultProject/Model/User/Event/UserActivated.php @@ -16,5 +16,4 @@ class UserActivated extends AggregateChanged { - -} \ No newline at end of file +} From e74c7dd237596cfd2f9c7531f6b366a5d80cfa90 Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Mar 2018 21:22:47 +0100 Subject: [PATCH 12/35] Add finalizer --- src/Cli/AnalyzeProjectCommand.php | 5 +++++ src/Helper/ProjectTraverserFactory.php | 23 +++++++++++++++++++++++ src/MessageFlow/Finalizer.php | 20 ++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 src/MessageFlow/Finalizer.php diff --git a/src/Cli/AnalyzeProjectCommand.php b/src/Cli/AnalyzeProjectCommand.php index 5a4d922..e0d2717 100644 --- a/src/Cli/AnalyzeProjectCommand.php +++ b/src/Cli/AnalyzeProjectCommand.php @@ -83,10 +83,15 @@ public function execute(InputInterface $input, OutputInterface $output) $formatterName = $input->getOption('format'); $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/Helper/ProjectTraverserFactory.php b/src/Helper/ProjectTraverserFactory.php index 1843027..63e115a 100644 --- a/src/Helper/ProjectTraverserFactory.php +++ b/src/Helper/ProjectTraverserFactory.php @@ -16,6 +16,7 @@ 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\JsonPrettyPrint; use Prooph\MessageFlowAnalyzer\ProjectTraverser; @@ -44,6 +45,8 @@ final class ProjectTraverserFactory public static $fileInfoVisitorAliases = []; + public static $finalizerAliases = []; + public static $outputFormatterAliases = [ 'JsonPrettyPrint' => JsonPrettyPrint::class, ]; @@ -74,6 +77,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/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; +} From 2763702a77d9a742c7b556da5146a48b5ea6fee0 Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Mar 2018 21:25:31 +0100 Subject: [PATCH 13/35] Configurable node class --- src/Cli/AnalyzeProjectCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Cli/AnalyzeProjectCommand.php b/src/Cli/AnalyzeProjectCommand.php index e0d2717..d1ef7ae 100644 --- a/src/Cli/AnalyzeProjectCommand.php +++ b/src/Cli/AnalyzeProjectCommand.php @@ -13,6 +13,7 @@ namespace Prooph\MessageFlowAnalyzer\Cli; use Prooph\MessageFlowAnalyzer\Helper\ProjectTraverserFactory; +use Prooph\MessageFlowAnalyzer\MessageFlow\NodeFactory; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -82,6 +83,10 @@ public function execute(InputInterface $input, OutputInterface $output) $targetFile = $input->getOption('output'); $formatterName = $input->getOption('format'); + if (isset($config['nodeClass'])) { + NodeFactory::useNodeClass($config['nodeClass']); + } + $traverser = ProjectTraverserFactory::buildTraverserFromConfig($config); $finalizers = ProjectTraverserFactory::buildFinalizersFromConfig($config); $formatter = ProjectTraverserFactory::buildOutputFormatter($formatterName); From f72a5a8f2b3d99fd14369899314feaacb6a94865 Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Mar 2018 21:27:29 +0100 Subject: [PATCH 14/35] Configure travis.ci --- .travis.yml | 125 +++------------------------------------------------- 1 file changed, 6 insertions(+), 119 deletions(-) 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 From d27b4feded1cf679a108f3e9bca43cdd6311b6f4 Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 23 Mar 2018 21:32:10 +0100 Subject: [PATCH 15/35] Add badges and support hints --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 29d80e5..3c5ca7e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # prooph message flow analyzer +[![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 message flow of a prooph project ## Installation @@ -74,6 +78,19 @@ You can see `prooph/message-flow-analyzer` in action by running it against [proo 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) +## 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. + +## Contribute + +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). From 0e315982accc506081fd4ddd3454dc87885de8f7 Mon Sep 17 00:00:00 2001 From: codeliner Date: Sat, 24 Mar 2018 01:40:58 +0100 Subject: [PATCH 16/35] Add docs --- README.md | 34 ++-- docs/analyzer.md | 25 +++ docs/bookdown.json | 13 ++ docs/configuration.md | 344 +++++++++++++++++++++++++++++++++++++++ docs/filter.md | 2 + docs/img/edge.png | Bin 0 -> 5328 bytes docs/img/node.png | Bin 0 -> 5004 bytes docs/img/nodes_edges.png | Bin 0 -> 50092 bytes docs/img/paper-plane.png | Bin 0 -> 12183 bytes docs/img/parent.png | Bin 0 -> 64506 bytes docs/introduction.md | 36 ++++ docs/message-flow.md | 340 ++++++++++++++++++++++++++++++++++++++ src/MessageFlow/Node.php | 8 +- 13 files changed, 777 insertions(+), 25 deletions(-) create mode 100644 docs/analyzer.md create mode 100644 docs/bookdown.json create mode 100644 docs/configuration.md create mode 100644 docs/filter.md create mode 100644 docs/img/edge.png create mode 100644 docs/img/node.png create mode 100644 docs/img/nodes_edges.png create mode 100644 docs/img/paper-plane.png create mode 100644 docs/img/parent.png create mode 100644 docs/introduction.md create mode 100644 docs/message-flow.md diff --git a/README.md b/README.md index 3c5ca7e..138958f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ [![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 message flow of a prooph project +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 @@ -25,7 +27,7 @@ php vendor/bin/prooph-analyzer project:analyze ## 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: @@ -36,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 @@ -63,20 +57,18 @@ 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). -## Run it against proophessor-do +## Documentation -You can see `prooph/message-flow-analyzer` in action by running it against [proophessor-do](https://github.com/prooph/proophessor-do). +Documentation is [in the doc tree](docs/), and can be compiled using [bookdown](http://bookdown.io). -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` +```console +$ php ./vendor/bin/bookdown docs/bookdown.json +$ php -S 0.0.0.0:8080 -t docs/html/ +``` + +## Run it against proophessor-do -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) +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 diff --git a/docs/analyzer.md b/docs/analyzer.md new file mode 100644 index 0000000..bb203a4 --- /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 for 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..83f8bda --- /dev/null +++ b/docs/bookdown.json @@ -0,0 +1,13 @@ +{ + "title": "Message Flow Analyzer", + "content": [ + {"intro": "introduction.md"}, + {"analyzer": "analyzer.md"}, + {"messageflow": "message-flow.md"}, + {"config": "configuration.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..8f9682e --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,344 @@ +# 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 (namespace is excluded). + +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 message flow. + +`JSON_PRETTY_PRINT` is used by default to get a human readable file. If you want 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')); +``` + + + + + + + + + diff --git a/docs/filter.md b/docs/filter.md new file mode 100644 index 0000000..fb31732 --- /dev/null +++ b/docs/filter.md @@ -0,0 +1,2 @@ +# Filters + diff --git a/docs/img/edge.png b/docs/img/edge.png new file mode 100644 index 0000000000000000000000000000000000000000..553a38838249763976d9ea14f4b22f2cf6b35369 GIT binary patch literal 5328 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 literal 0 HcmV?d00001 diff --git a/docs/img/nodes_edges.png b/docs/img/nodes_edges.png new file mode 100644 index 0000000000000000000000000000000000000000..55a331a076724598f2fb89bb43b3b19f3839c7b4 GIT binary patch literal 50092 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
+9NWHi^CN} zfFV+Z`4cmQUQTXJPVat@+W8bed|Mn3AV7E9 zQD-cO$9`{b7lP?fEhK~vF#r}6dNtnv#byNE^bsQ?Bl7g;r=bH@yFz-t^gy3?$MSVX zA@X66J+kxissg9yAW99YV`XK<@Ay-A*>|rBxRddGt_Ycveqy69)>w_sYYFe!o?Xx5 z?(X9LSu}h`{e2VUO^B2WgcXc(a&(Asac~Hm-aTClMd;7Jn=VV|Gn;T32v z?9sM;bcw@t{fk0536lJ}w@2o{`{p+`dd-yh6AR8Ee+nZU1}zPAzlC5XefkI?HTBXF z|0&2sf|_jX?CiRTS1_@>NGL+>-n{7>jHh-zURPjdVcA%Gm8Sv}S{`~2IER_pSq0$H zR@;4$T6`rK3}caD1i;pSd_~-ApCTqEh96}1l}2^QuCA_08pR;#DlIGPp?kPg1Eh!W z;)dOfzfF`v1fqcPLEz!+>?|V0H4nP;%#jVCGB!2_hD+ha3+nU3l~@oWltfHk{t!VM?e*oEXfm;9o literal 0 HcmV?d00001 diff --git a/docs/img/parent.png b/docs/img/parent.png new file mode 100644 index 0000000000000000000000000000000000000000..71cf39412e356b7702f3df880e4369f9ce9e274f GIT binary patch literal 64506 zcmeFZ^;eW{^e#M#pdg3^NJ~kFgfys>bazRYbT^5AS>afpb`{#$7cS0vI4JTz=Qzusg2NQ^yjjgo_v!juNiHVJ)xvkSK zT8j__@*E;5`bNbqX?xB+Da84@Yai(xJH_3}W{S-b@P;A7Kbl!(w4?oOp&X7(CF@kkSx4yZ(WMVm zB*c9Pd8QL!k&puhb@S6T@Mr7xf3Fu+#9C3)Lj90YzDNJ}3WEEn^uH_o0`Flb0~<16dwZqo`m9g@Ig>ekkaedevcnN=5*V8Z(?j5O@!PkX@O5$m8cpU zXI)%g#{Bq^*G1+j!^_K?-HCqZ&c^YKEi9T|`6(%B@nAB)`uZv`$fUoa^M{z=MXTg; z7rF14GfX#^OweOv&UHc6&aQLaEstd7S8TvUFE+q&J3fyxH56hK9!C&#!Mm zp`jBa+0sX|jzgQdsXil-G}6Xp(c4vfNTj-hL%Gk{CT$2IhxieE{9u&^xJ{D7jm!8c z|C+6HTy?19G-$iy`p{#_Bre~iFIKC;r5Ma>Wqmz2klaV(=g*%B!oJNJ85z`)qJ=y@DXO&E|)BOPl#kica*Rh~Tr#eqP8&jzF1Z1L$M0s>k|qL+qW zs%_@rK9}WvETba0g4Dvo!X_prn_#LcaV)w|iHHh5eY%Y-0aJd*Foo%v_2tW*HdSzg z|Gb=*x1Ehz(sj-`E&zu($6_>xo`T|yYy#(S+MCanZu^GFN!d2n=}HSMY;2|@ z#+NS@BP)%%LX{3i@lng_Sa}aMAg||aYzlI@${igY84M?OA_PTttq zn355<&1~L`ZHsQ=E9-(FXg(q)zb(I`qWlwVp&9-S6j+g1E|gUU-mnE8$!E~pgoF!ykw;PA+c5~|H$!=u`G%NaL7;W40Y4Z zvh)-+JKuNhuC~t7Nhj!0bq$yKY>NX~lv(cOOPlQ(>MrxHSaCS5PkLhZEOJ0T-BpN2 zJLxrz=5jRNC8LsKW`>~S92Z4-3Pij(Q z4iFXW_Yb^vgIw|O7`9>zur|CtfAW}H-m$%mL@q&M$1RIuX zcm1U%p=O_Ia)bFTNb}NUz2wvri6oyT_#-TQpY%_=xu^BmNfiG=4&0c}M3$J*s#8E; zT`6HR+#0r3PV!@GO9}!}EqXs3*UXZxwK8<}8d`+b%z|bHx!Jn3`dTcLxqW@jzgK(C zGh#mny7Xx}9Z| z=mnu#NvvAXX87uEiAj$J-CH#Xk6_({?IUN*@%GhdM&T})>LPZl2oy}w&+`4;Wh|3s zpMcOpB`k*H$|8P3fiSeGJV%J8J$|zTD@>=Kek;Y*MC-FjKvjMMFcyg1lkFQADfkgl zAs)M}5B~HpIHKZb6nk+xP3nwgpXN1Y{KP}s7!8)Kd1SVsO)83rD%8Go!bPgkK6K&4!*TjHU^4_RC>&exoQ(Wu}CtH!n# zTpCb6|7Ucnz2(=8gr;Q@>j?q6p_6xM+Dk5$kEb0t-Ku3y;IiUA<=wC)Ql=Qvzy?&gM$?cL_&2*%IuIaTY$7=ZW*wp~&5m=2m!b=PVIRADP{dL36yH?G|HiWOF&$??YQ?8Gwafo%^*L9&7{jCShEa+xGl&=@az@9B;8THm5m`Ku= zdLBLOL%3#TG{q2qxF1DjBN|hApDjtV+_-=LtPuhsub9Fc@vVJ}@jblgmA?9Nncp#t zZaZDw_zuQt{nMn6|AGlC2k-?tDdeEeAk$Q}&TlHTM+0%nQJ+=7*#>ap8+v=e^; zTTr6Y;gf)V_r7j?U)EjXEa8visaky&;;sJU?)NkW^o76SpFD8s$CW3T`qh8BruN1p zFSWz%a-QGVS7U$o%*NcQ4tlwbubTK9%T>9(um~!jA{kHS-3i4?ZHHA)XF0IHeU}T; zy(HuMp<^MD7WMiy9o(uLrLYxP!Dr)&^U}87vI%LGlP9~~ExJf0<_9|QzKmvnp7fr> zNqJpUH5$m$*#DbTg>lK@SyF%|wbk^G#W>2lt?MQ^3<*_Gy`*(j^ z2fXg?1sxh?;z1IyW-L^~bq=&AyIg(?OD~> z*2E7tf5D4{3x1ug^VnMf6xwZ?Se|<(r&X4$gaPN_j7F*DdhYMg7rf!^x9wk0{oA8% zrKv#0WJo(xESGhWN@ars?Ih(&p0H95&B>$@V};NDf?oVxh}~bdV62$SPPPoJP@$QvA0We-jk{Xt) zN*_B2metl&t9i&DPk*>=KiO7vc=e2at9qDSr;fI|c6sqzGA?AiRp9AkveU9g7z3Zn z%GN5WW1Y@wsPJD6cdKabD>o|+S>^rx{VrsHO2BCPCv^SpIu6x0-T}Qtc-nvK4Z0^b zNW=urBOHX?RXd1%_OGV#O2>+d7qtU==8K%;joee4(m&B~Q0G=m!*BJ9+~n165lj8h z&MF6!86mB~+9meS31p6qo8+_-?F1$>$I;*aT&9$4@z5Piw%cLxk-nvs4^1wBt%THv zArl%Ek!8@HMCa5&MB~9=LHs1A>P45ONfVDK#U)ux)2TmVW}i7?7>iUu{FEcNVDa37 zJvg`^2Sc)4!^Cz%ivo=-?$J{%c(^0-)oSB;Q4+s$fsOYf&p$YtH^x`ratNG3uYH^n z(^oQICSN)p&fybXq)isP)O+wjQ9Z|OowVF{m(0YJBgk`1GtlXB;K*Rz zLLT>jlZL(-)XxVvqwyCwhFt6sRep6;!YRkcNGKQ7yD#@?w~rdF7CSdOJIZ!Ji}`q~ZPkt$ z+fhb-q2(O@!9_BXrYPsrxgdkK7su*=o4To9=z6e55lw^1`2)rVAGrAHm2G2EL$!yH zu!8@8aKY|%r2ZjTr%Z%_04f!^e8*;!^FlzlCLtR0HMWR&d{}_g75yn4VIV@Z_+&X^ znej}6z4?#buT`hsRVO++y6(cIffg9AvPf>!QbMoxvZxQMU)0C=xXdJaqm>$;__r7W z6r}s>b1m&$nnt@3TZ_%hsk4Tv-K12ju)mSM%$5c3dt_F<&)}G9xGK#5kR{RU zajDkI_d024xLOP`qBqKUmo|mf4CnAdM^ZK+weZ)AftBRv2YX%RZzg*fxVO^&a&S0J zXbyPkVnIl1GRB4JICfakk-j?BC%$B*=9neKN}-uwZ?Z4ML5*w0Hm zdBdun>yg5zPCGm7w?AFE&vB|Y8f5Cm)HkVc>H zGcxEksN`gBq#iJ-X>N@vZ!`_{JtoxkkjoW0z2B^G#T`WxXX}xGmhxXD>}hGvN-gvu zZD){}`^hfCID`LOznauYo<}E!ddf2CgaqO@`gWhZFoxIC@9h~dGo|~;E%EnX-<}Dx ztY6JJ*qrKi^lTZ2NQM^=*8%?1T^iTC-0r+*yDCST`9~P0f~0}ae!s~9_9)|gKQVQY zxfo&>CJm#R1n8>N?L%)*jp#*3T*tXi)W8l01;FDzZMBk_G+pK0Zdk}@g>p+(i`YkX zP+IM1Xj*mkmh7zJC2cDW+X^%89$-3LChK{Nq3Qk8A>ASfy<%nB8QPuH25twahW?-c ziA>R~n;G9GFpSxTw>D2_6S`;;J6M_lmUCXu8Jw;S6+6Aa#KY9s}zRm&i0>PDM? zR^NXcBKH!C<{5K(p@+Ce8JW ztNWzg1K;r*FYO`pZs-p`V9*q8{=lynsTdCLAS!Yg-}uvX3W4O(=8A+yFsO?lJsxqrUz{$435igBU8)J^M=Z?g;A{W%eK!&noOu zGFeZMlG)pvY)3J+d%7w?XfqjhTX0cv&_d=~qwe^ZtBe+W>Swl z!2F=$2h__OmxtieLC${zhY>oAH)*rWT?D)|+wlo>`K6YzySY>i@t=`*n`19ZcM#Lt zxpW69shs`IAH2q6yEHVx;N&Uez5R!A;b1JDYNlF|{qGe|Iv3-gpbaKXkwRk|kT@t4 z{v)b**ugqg9x0O~$67JO>%DQ-=XnKFbslctzV;P-_}?!S)A`A~g4OWr0Q2uF$&GEcy)&e$sSS~2M|-@2Wvgv+fCec@Wp4fkQYJFB z#P8wvQ0XS*X3&G?n+-1Jt0OcC=SS<)r7l%T4+7QKxi5`Y@{oB9N8tqy8s+NU)t9H` z6orMatF7gG>E7|~Q!>8Sx>)_{Dat;1i3Z7i(sG|+YT^8;Sx8U}7egn^WD(o$PZ$?U zwwHV9UdKC~l8{k3#@sv51O0o{-%4HA#@`)THyKr7vT{wa6&klvFsh!TuS*E{eNGE& zrg>P6G;;@@ph9_o>LwAy55Df|BY|hDmzfcB-&@h<}tZd+eI?j z**SF9TllN%tExOuwPf@0Fx$L@Dob@u=E2ExlMn8%0}j%<997(F8G7($wV-(^s?84| zt*+P&sUYqZyq5%YD#BnQ8HHFHx1b_nl7c#b9$_31nh zz$#(b_1@G?S}mwJmigPxah7@^<8>a#k7~OfJ#Ew1Xz?FqjWLpGM%6MC;Hbr`xVd-h`riYGDnXQe z)zHy07Pi@QQCiWnG^fL9se4Up)Q$X`jn25kHy2(*iQ*dcIf9Te7Y=Lhpd^y`Kn>|3 zVu@Cm@=AlnU0?#0jXk3`4Ti#51>D6#uKV5+S17AW1s1*XM$mPLpalj%(3 zX?Io>7GT``L7p1#l%^Dar8XBUcTJmpMAcTFkC#XX4Rx>K5)SlhvwV zpyor+WW3P*1EgyW$pV=^A@EWwWhi6}8~_J2O#wOSOKj&&TEIEN(p{!q*GI1Ntyz{$CnX(tYB2o&KIn ztfNB=FEltbL>aZ2->eiu#!-T$>OaIGbL1nL_A#%4f4n?rAR!=mT!1ni^3=Sv@Nmnb zhv33It(*Ds*!b?w6axI4ZsrnW$*pL!FFRVsi~FrG{PL)v42W=$w6mMdNmB|6!UEP< zu6^{r^CK{$0Ey^BwGerg*YDqRsiA5aD#AA*lVp7Ua862kE~B!TUV_xM7X7t~)0|}i zHn?I>@aBqW#qiYJoqT$!nR>CQ_;@gjJi`3ilYoNMaJq(6{eO2$BDo=vi?Q+X`MQj; zR>;9htS*gWwT=#M6|jetnU?BC&fuP{JvW-@B6eJIa`8YSHzO~vCiuY%Ez~S@k+dEE zBC1(D@o&mJ?l)VPl&++t6rGe*WIa=@%F~HhU$?pSP?4kjzYW2;Sr@Z#Ao|nL(pnv? z4fOW)724^jB-aO$X^ktef4{j~EC1EWGA>HqhYx240{P!dH&mTTw_b(#)ra39-1B** zrEL0fpGBCOS->bEs;ppK6=p*d>#4rbDl1qlF%$zGJzp-F&vIeap=xVZ_2|iCO!Zq3 zvr2>ZfF8>#20_85wwK5`*Hr9w1t}>hF0~iV@G;2%1M|6h=T$P{-Npk@u+v49fznDJ z8#buq023;ih)GF-5^-5}vwdf8`SkfSD4FofCccst6BET1_5R zZ`<1n%YZn#_^n~LSsPVb3c_O?%Mc4z>H;;oN{i7Kc=-61pt5HP2AaQIs8$?FjYGnt zAdYv*R;5{C3ZJUTJ$D@CtO5^Y)~bBRJ?9iP$h%I$tkn3JqTS<eVjV>D@bdNZFcb-jt}QOx*cICSPA)oJK2!_+_vf9=kg=(ZAUgclceD zx~d82UR0+vU%1%#TV2(XtN}R(mYWBJ++e{~oXxpRE!g*8>dboiR5iW9?!h*OGy59#baaX&$lOIs8vh10U!7Uaxy)Frs)>k-ev!C^kQcsm z8Y4SuZF$rV5Ge`cczY@{)#prdx8=Io2b96E+Q&gr(|Kz`*8A*m7}TP6$LDp)z1hI> z2{7iiD=z(@-<28pF)Z(HJ)oT2VW^s_eZCoReaMS&*ByA8{43j zprfVj$~?pOxo#2{e5q3sLY|9Ra;0wDD1f@V*5}=N-5u5s*;@^+qt#<7VQWI$bCR}} zI6b~`3 znNiJ^Bb~GFBAqTXlE~J^ncQ)~dB*k{KUhpUmWlqrr}1wuBXDb__wUmpBO?#bwo2ko zrxjIHvbg8nL_kGT$=o~-+JNA*o+=ju&x#(m9FJwzrk1pAxva)+|J~+)e{TH>>>7Ng zrUX>NQMK``Eb0(ael>u8ky&5{I&%%Kanzc{YM>qtKth_qf|$)2u9ECH$cuIFo&p%W zco>=Pnl~S4d_my^sPjsqOiPSFcjZd3x2I>A*tRj9L^f;sRB?3jYi=Cx)pCXysFJf|izL3Xp>y;aAuf@Otaoa7>(Zc$E5z40CI?exR5yn=*fI() z9Hh9sN|Ys+y_V`zD}jyZ!z?xnoVCggt5SSyiqtO4-_G$>kM`mTdkS3lEqBIxT0`sv zUfIqUf#s2KTk{8M+ zQcDtTn>E3NNBs%(HXh6Z1!~+<6?8e&yY50AhXmCD$|)!)gtN3{+&YL4*poJmaB0)+{&V^jqZ7&T~a0X2U1!~1weBf8D8k=IX6nAm%1+NOB>kCH| zl!8u&az0nY_Eq}K(#)E^+Y&meBY?xS|95u;6_@XoSJj6pC2jDBhn`M6A1bG=>dNZV z(q4lH{iX^N$Us%^Im`yfT0f#4O&W!51~EeEoUhPu1=7wD4f4g>H4}%Mqt5#s1ci?4 zgVUu3BJu8FmCjpgRTiV-z<7sI_4NOmEyk4P1+zi)kc&ihY@p0NGAo8r9R>eEU%!U8 zFa73%>13h;1x(j9*>otSI9(*ra(lAOa=akMWp{o@ugj*!>iX&u{yY6MFk>fxdG#e_ z%DHkxd`@|w(q0VOS}gY#e|ns4=8l87sZF2Gr~K zGOFu0{q!(T-8liH;C>`yM|Mnn3-6Ne1WO*^J&!_lha8~CnOYXrur1=dE2O| zspa6a{wN8at{nHbW{suBBftY)C-_@Kvc$mXi+)Ucz-OxxEhoM+sTTIFjzo&xKRUPF zGO3wy-lvI!Z#!z6U)&!m)YI{vb92<^O_pmC+{Xb;JbBlou60AeTqSc< zxL>IyH#jL5=g|SA+d^SyCrqD#mS3VI)K}4rOIRB=r{Iv7JGwa*WFlSVGe-p0f}W30 zE4e`znwK|?I32CF9jy$z$|d$dD*E`O;QI*67Z0|)=AL&b)FVTF%JDuFKH8qk9b-mX z`RHalYE_PPF8IQLPZf%+_sw>(b8uAIUAYE*E6&AVuZjf5FxF$s$jKzrWLqz$JRhI6 zyEdq1uNd*7`7DT%ii+#xW8js01-8gt4tCDX+^MTP_a&OTd-k{`>Iz(s0(zKc5ne4+6Aa@W^#p}C2go`y#3L7)ggUPTbabXej5 zu&g6S`9PBk*fOi%X>ZCkn1G?j03-wrIV(0?cA$v_Jl(SY$IIz*li&m{tDKgrGig-& z%gO%o(c)*^daI4G{3y^hle@jWE!8B9;wykgg8X#78j5Fl!E_5A)H`v+H78huL0z75F zJ0ygddjbV0>nWacsCj^nCQhFVw`A9O_p`lrT)c-5pG)$cZ&x@k`4gzYy~U4iQhOeB z5FH!HHJle)LE-|Ka)JGFr{!#I*=;nm!m_tR+jA8Xg7wZLS52O(4RV!Wt;@^VkC3Hz z4@az?jIFOVM;g$va;qcfc@-9hk~4oyYM|f2U|4Ql==EjNR~EjK*;9gxi(OcHtUH!@=HqWR?^tdV|wa3=;_pQSk9;ZcgOaZVw2vyB4}S?GguArn_(;T&`8j z&^-iOsd>QwXtl?JQ*bPkUY@g+PYl41i`}&X@sNb<+Y@sQF7Q7S>#;>hes77(rf@2@l<4Qi7E6(BlrJ{86WR5JU_HQ81pG=>MJH>H z-xXf8X13KgwLlNL)A#}0^_R3nQ(sow6uV?utFJ?K@zh&B&if)sL)?siP+E;Exw+N; zJvgYeT9Ke`<8TCRo za?VB_^m9sAnbv{C#yBA-M>cWofkt*Dc@h~RR7xorpZO^{c?rsyKn4O>D7Vs%?XWk0gTb=| zZrc=(BX~A&*r^?tWakOZ1a=dt_ycahZvZiF5ayOu2JN4yenQY~ng!el4#--p%HQ^% z{`UzAfIFkoFR3PSbzSEeB2M!(Cv`T(BKi)wxDCA?tunt3e;nD(|xq6@=emh@jN5(W;)tL4tmY5C<#f}>) z>glB*njF*gyby$!`a{>7=*nsfB@ zy{s+I;+{r@eL#b`_MmTw_7BZbUAnCccgN1pB5kP|>zx%N^@z@sP%+bXUkS8b_C!0e zE&D+cF7cfL2bDSAT*|4yIbk}(Vlzt0oSr`o(eDPniwu}^EkVsvS?COi8BS4iHp=4S z0Ul19S+{Ct^YkaIRYDTD0Mnp}IVACR7^{F!W(VKCkZ%6ikmW7WgdC(cF($FQTeL8E zqE%N{hsxyeK%cAxIZ1&ozbnIwS89=!K08F3O!&?Xe|sHDEP5qIIpN=yJg%F1&KxFm z#6Xe(KRMr(o%rs4UxgJ)Eu&lY$Ly#vr{RTsLSAkvvFDhuh(!Hgcp;bjh^TD5fMkiB zmo}g>h5D&_1;9Dt5AYe71av^aByZ%_lo!0pTkB6q*W;egV)WF>TnvABRKLa`csRKX zpDAfEekLV5m61#4tFkj)16#orQnFJh#R?AMT#C7dx3Go|2c^apdUwgiKyh2+mwFjX zR2G`Oxm>n2P`OP3uw@kaeDfKj4y}21RtHE$vcQ5tWM_1~T53>!WizV*KMHE@tg)U^ zLov_4fB&LVupyyy8IVJ%gJ=$tH0zqV9D3y(R1+=8{y|U$xnwiQ7-t=gO-(@nve&Bq z5bv@*NdY*Rjfvff%N0oBWPh-|LCHSAhoQhXoOkDS0dHV&!~v^&=>b}Ii~6^iwX4Uz z6LO4IS*asoRKOvDSOrpxLJ-$X1|{X?QP0P^e}As#PpT)P&ZHA1GDzTPoF8rFg1%o1 zKpqk}Eh6FLg{b^@W8QOD_1(L7{_BAM!9k>s?l4pYvX?X|Xu{as++<*4Vmk6LH#d(j zL7pRbVE`?9n>sZWH2+2tKC|&Nni}j0kl{1e;Qan7K{nEf)3Sxa{Xnt%#uYQpR@Wa9 zdmIHW)^pyw4{_?wLo=Ic-RtVgwXCsl?nhnz=5DFl617@$z5f)*_=AKrM^_+ldJEfz z*6R-Isvp;`rEWp-vh&G(b3(0$U}mXaSq%u%Kh2%}pYJ*!9bK!#6Su2(ScbDOORNwz z6;A7dp8<^L$oXi8Byy^Hy%iA?8=rU*CRD^*VOr~1#c|g4N5vm;BziO)UA`DbUKvM7tGq62YR?A(J-)4TyZ4@U}^#pyYaiagDd{M z!i~Xb&}R)_4imo71Z(Y$F0amaQKoj@a)gQtfWu~i00X}`7=nQ-$12R^k*D|ZnG{O& znw~y>oUv8XqK8r~=4?>B?+6%XT^GO;AiBH*4b&sx7A539CvQe`WJf{gcrhci_AN@z z0^w$pYysTN8z>@(p^EZyr=REq<}j!(XabjvZ)pHc;(!Kk0zE~^9Y~tS6;rd9Yhf4D z&Kj*R5e%g>J7NcoN+=N$HBnSr3sS~Spv6t(DQ5Nx?%H*e;Nz<;?ZR4M)iXPY z+{^IN-NAGWDf@=b^Aq$T3QyMQqZ&Tdku4l z`C_ee{RW7n}p^sWz+Ju=7@Z%g1-8jGBaZAJFAXDi2(` znC2fK5mQw8S=?I@x2z5-hJ==;rz@K#Nug3B(_AeKw6qepKHdWPC@i~k&DgAuQ+6Ox zP#TrqC30Iw4wyOydIVP0FHU-ovh2=_422Qp-ygKdc_R`4|0B(!D)T`Q;IguTJa0ZV z6Emh+F=A#eY=!(`T2%{_;Ye#nnuYlQ2aaw%-D_I0*`B|P5>DZZKe4UPj55zJaR8Sx zT<+B3y!Hg*Xt;x;L*;fCjpbxsgeB~o(ArVV`oeB7MbE;5{^==t7uV2|tsn076ecF- zINqG6Ic$*h%zBOw$U6W4EOt{OlizfCEUIa^)k13v0s#SoomE+>C@4G!V)1H8B&24M zXA8{^{w*{#)*|5yBqUCN6~*zJ0#zFe6BCfj=yR^%jtszP@_^<|B;-{KP}vEjXI9`u zO8rjtjMKdPIz1<+3OYKv$KuDka&(+qBiX@B_`&=ehu~mJs?fEUtiY_Y)78bPl(e+6 zfkAr1buM+&@wAl#U$R!SSDhj5T>SHxS#e+ERu2teh2xbL$~c7VqAYRJLxRX8@lcX{ zfcG2efn*FESEZl=fJy~{s+)pnKDPnFyDOr^3Pb|HH7O`4RuLY8BTLWufzU6LdJ2w2 zWTr62c!ijQ#BjRCwj_c|f`N)k1Sl3GKKt=HlBx@z+!v6+9>1xLv<8AaZDYRJ<3z3T?^v-mtV#Q5p@BjU zU)){$$#nJSWeWbE;(Lz?F(rW75yT z>x+xw-%Syz86m|dQ(V3S%@XZ@ggb;f;RatBa}?S3xo_Bory zQX;NUU6?NNf#P{+^5Nd4$l>AFF2Ioire+}op~ELI+SmpoiAa);4zSn>f>wZ#B`6|( zByA@trFM11?AU4*szpx&!tTG|n`^DD9Fv+_GMhxA>$E|kA33&Nw1!HSBu6gX9aTY^ z;Tb68HU>E{zi6yP<3Cbh49Uu&sZrremP4rps4-#%W34*yfs8RNQ-By64Nm(UHI&7n z=9iK27UUv;V)v>}VSKnw;l>Z!Wd@^Vu!> zT^*!e*XoxjW(MeaAE_pLA5VdkS=RY}M|KSu?QEQOi?2h-h4lcQ+g;a%OPxV|4~GGt z>t&o{e$FrgT*donT)nrq*P$ga&>QG^sK){{o`s!^tLtwjSY6dI(g2GG_$ve_+6##~ zuw4gAUTz&q;6N#|;J{f9keZ`G=?nt{4itlN9PZh)!jfh!Kt6aa+Epb%Zl|ValX6a6 z=Ih9p3?yvD$w%^jaRzz7eqS_mi1}tAe#0D_VWms*8zn2oy0E6iAS^(WOc(Z2iK}QC zTW0mi|MxRun^Nd3-tzO^E zGaM!hqtav8_?i2ZJOc}hi*T!SB%f2WjD^KaOuW0o!a#B{ILV}Y?km`rT*&9my*?A^ z^EnWnD(Dy>bh#li=VT>Zta0ApVzOesm4bF=&{1I2ukr_o0WjQPeDmMlB2^yksZvAz zqZ5L6IgRE&6*OW)Xd^(dwzxRi2k4RtamuJ*2bkm`5RMAOg7L9%aK=%AUC3-W zLyU;mAqymT7N{UTU11hBQKXrPDyx8|MN*pw#IIxkLqL@d58H8tH-MOost($;T)KcW zg5FwXhGN-U+Y2o%K-E|qNX!M#j+uUB^(+8&CaYMtL5)!zIws3Kr(${66d)<8-Z+pV zs11r2XMjIAb%Dz9%(H#~pCC^~DP1URr4R|&h|~8OP7(Ax1XLOh^!D%$LwdYBWCH*q zV}6`mgHyp8Dkj$$d!G2mx@wx}grYj{>?Iy{2#!^bvB!;Y?x?Rt=2&QHHJ#RLTdXx_ zd<9C= z-H3>Yw;}r8M|fFE^n6Yme&AFe1urk5dWl{KU?m|y;Rf;M3B(T|ft!biVNWE@WR(>` zv=j{`B_-R3Nj6Ta37IWtG0I0EQhfm|!(hJAz1I8u)t}{M`=vH?ual)fHZCqaK2K|F z>st`%IF=3|E=I)1e*@)+)~P8%a^H)W4@h|YfcU7>;DQ6xg|;6rjrRIjSA9>lronE$ zH#YtPuxEL1slAOC#5_`L;#b%bp#oRe(x4o>xVsw$BFPd!B+(Bam{5Z2qP1dk&&re*3s1+24W%VW;@eWPp%^LN4b2n(H$He zA2ypr{oYs9`XIxZu`7|T>F8^qtz<|^*4c3z!o;R;mbk_&xeSdmz-Byv*xA{MuK$MqxE%L=`0VNT1G6gEH4QDTpTEYQ z!mjGH5bjT&Jnp@$i_WdPW@O{$?uwCqG}&T?J3A}*yLD80a)GyUYdI#b#`d$o-&bCj z3FbvBMYs_)HcK#lm$sTRaw4u{G>GVhHyLHbJrRk<``oU$OZS63T@eC88}#yB`y6e8VxSYw{PE8Xz}rO+Mav_bedm{?gxvjM<@$dcp z+Xl=*u|!YM2{B7(Sf-IDEFeGvNUEG%RM0VpCm`U?G;g#Z2Dl^*loX}3V4 zj{^cPHkrUP2tyLV52rBYI}cHv2vk*CICVM?>U3C&JhaIi9UZ%k*}*@JIK*C&n0S-$ zg9TQ-FGE7{_Xr3GoOX{2O67gdUwOD-zCF&AcqWMOz0Z&Bdw`RblePHkG?Fz z@b>rb-wiBHG0@P^8aXUEoAt}@Nj$jxmQr&3sMA`b0!!a_pV#Hu{^2e8_=Q77HT(?n z*}$gHXxpS@3vKEe8u!Fjte++RXe~^4_3t7kB&49`d`cTiDt35zZoYWb(z<_Op}c>N z(5-c6#Sj-4zlV+0IT5mDf3kS%`#uoL$1S;UY>ga<#qF=w(h)qKI5|ch_9mmx~YScQJVhyz(CK>#u74C!^o7B7av&bl3#Ig ziA}0GoST}8V*r1JK=$`>U%F0U8}L6G_cknhX4LupC&b*2xZX#{)5=x6aKR;gvr`qWl7(Qtg2Z&NWB7@aAt}B2|_$GS0OyfIi^T};cbM=$T zeoUs`bMiPeH1um&SV`$Tpoy5SyU{*Hpnae{TRM)xOGt`rXuMF}KOtL*9vd6mZhP`& zh+`-T?_*FkenM7}o=$<1VfQN@1$TtMqb7pcxw>`%P6#G-C*mEo%+M$}S)at~C=P@P za$!gkpEDMuSN97i56?4Tr}Ez4Q&LiRe6Iw6!GlE&_abXR8MBS|Rr#Xb2fKGhF%2{b;Nny|K}Rjh+3ilmk0d|NJhML<_F6vNELg z+FJngk?(cL%Ux`dU_Z6e>gqRM7b3Iu_N46+&AG#Pg#-Gza)u{~w(m!BNB1|sYeM{1 zvP7K_zcB{%Ek26S_N6XVRq|e+vWhofzIZ!b^Y(ROH5=DU5PvukbSpouP6^uX*4hYq zq{m+~o;a*IZ7-bj7g@uX&Z-LCJWm>G9rFM1!iAF9Fc$_dA9LI41O^u}iaw-x^@0eQ zi7;4dQGN(%Wn|X+)1M#ZX7yUQo9NVV^KXnsoekAI>E9Rg#4n!~gM)hU@w|`!kpGz$ zR^K1~)w3~{?J%bhATGW!e6CugS$A70|Ec$RT+?NXYpT!>eybOdH1+)7+KHxDSKUKH zU0ViCiyJFn@Um~O^eW}6JmZFwJ|h;^lWfS#d)OoS?Q3v|e@QBNvu^EEX00pRm*MK? z%>iNuSD%pqxME<*fVutJN#x$&$)Gv87^UZNzIYbUa*Cm5JNqeTAhEqOhs;N#{8_Gi zQtOZF^M_zn1ND98LtO)#zvT=hP_c634i6*CHqP;$?XNxmFjq-NxM`tryj)=B!z8M5 z{*F3Qid9^dFS;=3K!{o9$v089moMEBm#gSr)78YlYlul=n*mqqVdPM%Pp$O9!%q}x8J zN=iyNE}!tg^hi^P^F2sm-{QPLmg=si~<(lfiwR zx!L_x-U@$Qal?pba~?+%8}p>X7FhL8JE8inm)`^J?ZE<>kLNJYyB#psdUF1LMvE`~ zkbnRU=w7!3QeM12FYzqhMK0K#t=J3n_P$lf96WK^SGrSanW?A`#+5eQ68t7+eGb|A z{3XRL&?#VN=R{Z0;s`_3l03SVuM$*EdYkK5lKQdJ<^<^x{L-1cBJHZj-!L)loDY6B`b#gt5uxJEA^UR~=xm&vpEQ_0=I1~1nJy)4 z*`CBG6TNL_M!;z?LE?!^+CDT>O)1EX6LOhgxEYkXxVZ7vJ21@PD6DU3aU<)9mzpx8 zfHp>|%zT(qr_t>Z3k%D}WEqtT10MJakzk;2dmT@i;RW=Fj=#x*Zi~u@f*%#=$%$LK zc2&k{DbcGB_sGe~YwPMPNHc8#*xCIxzlTOO?{Z8zW?5yoWxS~^u^Lf>LZq93+^?V2 zg(aC<9z)nYAQPCHdhD0@^=oDDw{LGMOb60b82nE=It*Weo~0n+>kH+f@!`I&q(!J_ z4pFeO%5SL2%F2%O4R&^70C)`VF>c#Is33LgPq^oah3=p&co3%!oGDE<@7Cy(v(67+ zI}55wfZ;X(5uD=13k+C^zJ!MdAN6BQIIIL$xTV~+^x@`hG&SH88DtYFMZbt`o3nTSf{5@n56k`mYG9PiWCCuGI-StGJi20|ET#P4#qbrC zbApB?k|NRZFvrrzj~Dz4wN?NA>Rm;xpy=V> zf8)BdIN#(M-naS{ddQeD(dS*sK&@D!=nN5`=BO_nxR2>~0 zDnlLZ?LW%A_#74%_CHrm`~ADh3B;`x2i`w_{#1s(1c^}&q_nJT*ueSbsVa$VsKI5# zy#>uazl#%qD&(Bh(7?bp0&!<#j8Rs-MF@vz_pHMKdZ8Q`%_g<4u# z@Y1)aQBfA`JVQ-aF*zzc{P~?iPzTrQa(8!s^CIeJd%H;BI2bYbRJYA92HX+;<>JDN zLPb_iZh~)cdRj}P6nxExOduY(0qUY(Hy2q})zYq z;lH7yD6|ByJ!$DtzQMskxqE6M;44BjC{TBL)Wq~}P{of(NdX%T^+X~pNU89H{m~17 zm6iNAzYUZB9#~ma#FVbh#lfMx#UmjhVPK=0AAS7`1fO6{=kS0nqqdP11LYAaMp?PJz@&hUX?5}P^MjX^5%2z+-y@5E%jw~| zK1fRU6zkWoU*@DnhK7_*g%gG9^8YQ>Ba5z^RXC&sZ7v&X1ATp9eggwI?D5$1^YeT~ zq5rnV;bwvngP_}2|FUIovPp=Hlw>AUl4w|28I@8-A$wG|D3wYn zsbsGZWfMw^G7=d{L`KO-;rBi-^?aY_xu1W3*Q@*Wx;wkB^E}SuI6nLR{v29bwxgTi zZ`(7FU?GIe^p^*Ae0e5{cN!RQoB=dhCucxIQ&a5HGvH*X{xy8cKi>kklI=c8-@xFF z+N-7}gJCme6_pPAF!HVR6ynHR%QRzI)4)F*-Q?!x2JarK=1aaBdA{ys)|1C?YvJme zi>(C|mzOnSAHoBs~&aUJ=gGJ}YLc1v?bMn}I<+XkO5CG+mxyMJ%> zhLyZX_e>8TzMpT`2GnU$T-JfAO1A9idWXLwyLW*6y!3Nr-V-_yT0px-`5YY`#Y~Ee zi`)LlkDf9eSwm5h?xm)}R700pHa>d%SV!^_HvYe+;(8P@VJ$libIc(B+VWQi0C}}+ zOiWC~Z`#}2)v_-vujXM#LePG2pc>C}rVVLTERu#s-@pKsv9Ylbp)c_<@h9(mza4PS zF*0oo4tSm7Z?IdOP(?>{o}{8*|N><9b_OOZWA&EER`6M z$~r=gWDcToDSGvJ%5LckKxld;L1{JTIHPgaziuN>zD;o}in4?dP4sByI7)Ct5qk5} zy+7g@Y0E+$w{twi5%P~e2M-;>*gt)e@XRtu{oSic7VbS>{ZjH!UY3N)#A5qQ_ML8D z7akt|XY>4@$`tqWT^?Y{o40Q@O_3p7`HI*IRxtUziRB%+0%Z7CDZF zyLN@GAraNyzr#!4Kz<2jZD8vE>dYHA1OfsAngRFxyGnZR$*c5+6L}ZJ!MGh(A!h*P zU`W8f38tPgdCTfI0<#h3+JWk$2-A=O2^(x?&clS#40k{{Fg9!7pF#`m0x3V#w;pq#miKu-(dXa=S)1 zSy@@l&CO}!i+Jb1+h>odJ+T{2w%)mW7nA0W&P_RvPaFyqX+r)@F7J#~6CyWEL&&8? zz6@D;LL*`;R#$8P>t0^uhyD=!r}l)R{i}ZT)W~y@eo;wDDb%$Mfb*OI=xGJdtbY?s zy~hpY2d+VnDZ(kZc8w3KWN3(K#cTX)!sf|3JzX#(BLjN_f1bX43Gbw>&6M73c~1`y zlAktw0P*|DM}qi+rU{VB8#fqJG9lzFvJ%Uh?0Psw77G^-ujAQ|6vEs*U&=1?w!g0w zrWEoWmEGfVDIp>32GU&()1yycJ@Xy$(bd&u@*S14u&~gWD&FYUN$I$1Rr$9Df!F&o zZ6#K(hRCCYX4$BGq?rU|j_C8RGLLjtinq)$8!d}`JaRak)& z`a5<|kXrL5LU#$M)OK{tCLZeugMzTH$plscn^8^<(pl)_EnHc)sKa>u=sVCxmD_?LC5-+>QDD&Odita zs~Sv~(@wwJMAhV-OSsxL>(&2j2} z?2$M3J_m?#iilv1GfiZ21_+-u(U8{L+xuTDS4F7)Tqod+?n?KaE zr^v@)q>+mN{+=rs&_c3~&NFB%Jw08i3pL|!pRIJ==UaDJ?BD;T{Js=Yu8bKO?6%p!Tu839a|>wDyS#Kras zaZ*fd<4rWV zkw4_5qcxv^Di>^ZVW%p0E_-g` zey&*I)AZW6F9|*LI(lAmJZ`kdf4kN1_Z6!wyE?MgPd&K4Bh})43e93yyNK5SG<0Qx zA+P%ZW5xr^6UkmdPn+HLqN$lwb&B=LRf=6dY?>Ie+?>xi+Kc%Vrj+5{chn?Tuco20 z4W>fSPi$%OXE;%R+NSz>wf5SEU$FvrQc_j5RK2-bJx)&Byjd^M_a*vK&h^@!(!*;n z=<1nn&oOfxZJ*@5#i+b`^>kRAv4Me3$+%{|?^{XKOMZmC`m-4KWQo20ZBD{a&7b>* zTFDkp0a<&0bKdPqU%OV;#ocTSySE{|)mhVQzbX&yUc+y*_3s*(l^+geMHh_OZfM`$ z(^!ztw!pPrPhl<6@*f`0Y6CY=iS~x~?%#Kqi)5ekZ9L`U?>|~5NY~DTs~M@*%!glJ|#mw8S&j_Md<7 z!$#*3p1|I{h8lepoBy@-xbcLS3^6eU(?%QSfBR+{mN;#fQWx}9rnVoS7GsYpO4?HL z^<1oC-Y$=TuKSM~0-aGUj3MyQ-ms$a-Ht_kN`A*QHk1C!83!4kLaBY(#(p2L?v z^7N(W>c>kbI0U#^H$1#yu-byVn}0Jc&9LS{7d!jtyLsVu*Rm;|7`J|JkN3FtZC079 zyDUdd1KDCo0k=IGnRvTTKiXJo$qm{x(=J%u%Y4Wmko}^d%3R8V z@5my*G$TXHv86TAm)ol5cAYqJqWHks$dPaq{&nTeZ|~sT{I7SB=;NaBGr4Ex*j_)% z=l50P;>oEEGs;;`VRaSiVy*QBO?k(pc?O<`+~360{>ZR&zU%BJ&u$MBDd@zO!UqKf z?cOW?s3iV(HI`I@=XPliReYICaU`9&`8Aa0{y{Sy3(jtS#-&HG%JF-iW}I;~OH5GZ zDGH<+6LL|0=)}uy-;>Za_pSW{Z)HOvEMJsS-gdp>ODO34a%@0ta({2?vhrp>YFBNk zD={?a*xbKhuFI}kj(F2Ej z&u&*8#?}_**6e+BKo8`&S9v@1ievO8bVki`6kXn@{Y$%~Ga0e(t_uj(J*`D&v`TEd zM^gw%T_9$inXQ_dn$;!am+I+{xQ~IgDjg}*p zNM~W%Y~|+q%NvS*PAi`3j@h!O!1CSrapyBHZ+1ECU7xJvk>)($EY zX?DQq>9e{g9#_{*ee}Bbnb+i}qo{Z$L*Ddg#Oj?q>#d1o3A}p#7>Z-e32A%#>#F4K zYdfDm-M8R2ZW^T@dNglCL$2#-_jT)Qs@AW#Y%js?=&q|G$U1t?{J1i2*rQvwqJ)1L z+l}5l!j;uFm>d#m%OLLJChjMRFr(7OYOQTKJz|9a+jFvbf$rC{4$Zz3C%T(M~3H%^?OoR~*^Lc%UIgQrc{3m^$&~L^<%XwlF?2|K0&TO%DzCZc(6XPtiRlvFsZUgO)U}nHX z`ie+@WK?$Y?J>FdTL(g-obvkYj*IgYO-EEj#pfH#@YFOmQEHj-9Ey{bFJV7h6eq&nTDSGF}@Lb=j6b;z){xdV4t7d`lVWiB~`!@eh{Hl&!x;H5%! zc~(h*{C6$eV}0jXj7}Y~bERdZTEpQnbwW;#Dve{WyW&O)n)>>A`Kyt`8MHqp&eJe{ z_Sho3tl=}g`10EwcKG_veVNhx?fL5#VTd(3qDZJ8ImjJIM=Ug4dZ&pqrk?&w>k2Sr z?1uihFKq@6fxOy!dJ2gBYmr$RT?83#H2=EOTU80_Eb}}E>}K+(m2qtxyVa$Spskczq_ols2iVntDBAi_sfy&k^8}|G)M6MFgXngD8pt(izGr zW#lZh&%z?|<&6@%sn2o=T6u^jSwys`*YA6qDZP)4iz}2{_R8pL>z7SUXHXq@mA7+w zJYKxy@la7(KGXwTGwVkZG72a?nWhd2Hdk$K2YI99C`D~u-HG95vF@ti0}34kJ^{k@ zI08;t_50z$MC0bhChOkSY%NvyjS|mb4z)8(M#?TLVQ@e}MPWHc11&=kWaw?(T7xVq zk&gWK&EA~IK;omRLimYp&yhM{@_f(%4f#W1b5+B;pHnP^Yls`YFF6(=Qn^&QP3}kn zA|-IrP8Bb2k`_k$YD;Dfxfhdr_9a$6Z;pzO)tI)SWKVbll8gcOR}vZC!f)ddK9kcv{LkB^V&h7DYBL*Z`f?%C7gaq~IytrL|~EH7TX zkWtoJvu5Sc>*S8~RAUQ19tB%#M>X?9N-N9D%kAv!NU3}Ge}1$^T3S0GKqL1+RUC{w zVgd^*tI-Tg@dcDJBOZBa>fcM+5uN^+>+?{G^k&wXnR0=1$fnA(WwbdLV~o#dG<+h# zjtsSAZVi*Nq`sY_tC`K>%B((c-CID3{V330wN1%EweO2ZFWkHt>*V{~t@SA#-ONOU zo_I#<9FXz>_;+R=pcoA*H2Ji9i)7J9a;Q_a=OUg>H-H2IQ&ip&# z-A$Yh$*#X}BasEmv}_JCNmOP+(q>uTbh|dI{>1_)O0oWwNvPcED+bs`dpK?SEQj33 zttRGN+}vWavWzI(W*U_9M%6nYC97q=i61s7UZ2{@X=rGTsRFVrVppHz4`3HrUP)$&s@#O*Oe@e)}ORe=>g~ zJmj>CF>yH&Km5r78CvU|)0a4-Cyt8rN=S5OMI3nB=orri->|Xt z#6aePUqbwa3nZXP-o1Z+rL3y7ln)UYN_Kk4Q+rz8u*}&8>3-s{E-5LgN$Bq0j0?Cg zzFko+vL?X*vc3vfA}OP!5PVTzpB||#Hk<-5<#&sw%rZm#-xhCc<^<&r`;pcRi(+Tf zN57bunGuA?^!3?g?=QS>d6pMxWdc_O-TFGjJ8nu9cKoK}`2FIKM@d145G@806lx<$MiV6?dj#l#d-V;4C6*F%?eg0hg;e*hr zQ>Q>}k@hy!(V+x>jh&4x1dzv(qRRI~l@BnitzewWeszwGj@B_T;VQFSb{b?R_tIvs z2H^25kDMiauz7|_WNhqK_(l)idRdVC&$cSbeV;qg*B8W5>+u`ae52R5903aWMr;Uh+pz8^kdg)wNnVg*5E;@DezHxFKII||c)dtig6hu%X z%gxVHhH8SggEfHs;D3;G=d>LOF_pY@6pd+`L8lE33j-dP(V{9ylV}LwVCCfJQs6Ig+p)Ya=?3So*%gO{|jb8?hj z-ArW?fEv@?vgNcpV_`*wCW@2*nG-qFq|L#?#8e$TD1p6d=jcd90!)Epgo=bB4HnGV z^E3KxAcfD4Y#_>!1o*75aEj4u^h$T;jZ2r#0-3`nAW+xd{;=Bv>3Pxh>p8*40QTVY z&6_v3l^jPU4pT+v+U z#*bjP7?kG_OK!8Z6~<_>07Pc!{@Y>z)nF8X#)v2=ScmlMc^DdvfH^i{VPVCM8@FL) zkPz-49;QEb>{$Q1A3r=1mEQ)@wKV+ZO(FOVkY1u*i*g&_+hR&e9J#r(N>m)xCQ!vemA{OZW6t8g`UHzD}N0 z0tOVQMM$!plM@{>X7+}sPHl8>bnG7=m)Gb;?e9>}zGT#NB#FA{P+L<|=8M^?&*DQ|EPE;lK9SlrQ?udR;b=+4}mHCWZDu-623j32-aG zA9Woayp@%eN4iVbX)Vq<#tvZBTXL*aVov}*SdE42A03TBj}j6I2-X(Brvkvas4W0I z@C>%Pds1GW0!VL)hhOkvb&(&d#{H1fM(d=_efT9P0mB_jpeQBdIjtCvj7DVPSc}vVfl_V>|r8^lyQS7XW1C?yR;= z9-o=;g?KoJXc~YlHLsNME$Ycl`J*_^B(dWyflmXC#o7_aQqVIoJ$ZIXD(o0evCx{G z;uaA(4<3AiZv~iyJUE<#tpQy|98l3b)Bswn09L-XbZY1|F&EHOoFo46v#Ut-tgHYx zB)gAY9Gd<$QPshxa`KZt^dbXL4IYWfbx_J18eG9}e=$)DP^OIOYqZX2#{Vfzj6(fw zd$wU1qJIP`6tLfNJei7)CA6w)Gr|FM8vf$U%*xsbFegzo z{=mB&dYDit+R1I-vxogvk}4I+{lp3Ohpa1=sPPij;3Ho^8WW`$K)4wJI<7@BI`o9B zS#~uLw+_0B>+vK8I6pHM z1D*5rLm4mhVX6PDd_$a24B&$fgG~vw-vAp#Kt^5p^Rs}2goG#*3?|>A=mjN;4&dRHdVaa6*j`;t0;IsAz<~w_@I<5h zZe8k5;p*y2TtNbpf<$Z|I*|Yp%Ei`?Jy*a@r(+d<^5t#Xw<0g`!IP>Gb$eIWxVi|| zRWKtcd4Y-ggeY~)eBDoCQ;Qz>_%YvO$mn=a`APv5*MRY%l+4SX#h@!Nfh^ym7U2#v z4?a}n{OjwTQzP#S4xmDZD>A zZq~y1wxG?2{cj-X&tdo2gM2;*j*lou6!2Jns8rZt-_m%0tg-Nty~CmtFQ1H<$>8lkh?m2=EEZor{CZt)p}B3%D^uQOHLNS3d>p-V!h|k zyD6sTEZxBGpHrYpM6hkZDCxzxTVk4?J|z&Wi|8fBl)5(xs)<1UfpoxKTL~z`M~Kb4 z$6m5JIymm+%9ShEAfkv`3tT^_l`k#+B861=&jKpYf0k8?E$%?R9fi>?jVJ<3Gz~hm zE;K4Is-TIGgcU-GHXf+?aKY6fiFV(&ou3XGXb9Lg=Q}+G|$O#IHD?n~J zzk*Ye{(OINhix5f%QxWKS3%j}v9+TWnD%Sx-ns5KFDe{<@ZiBD+6yFD?fmj;mIIdE zXEdjTje|q{#r|VXP7zQ89}~=NYe2U+4SnV;xE=^{`)qhd1d3nP<1_LMwLLw;jps$S z!;IfIx+znxQgWn)5*=e2l7kob_fHicdd>q!)M2ze=SXWhJEqJ>Yr84v_a=B10RQ5a%qW7>_d7?yvFt194a&+V8cjIh7jU*}S~K>+@UMv155+|@ zpKefbVV6iMDkVzOXii2#gNix`8aoe<-roKioLpE@B1%?K zQL%r43Bu1qz{dFqFbIER*SmMxYk3IFG_l3rSG+*1dFUO2vLBRMD8wk8! zP5WL@07xVR4E%Xg#g?(O%=~>AAFL?>q6N4D^nfa*n_r+9(KDc`xP70}=I_CaQzFs{ z28P!+tN_{?u3XT_#3Ce241pFrzt7V>W!`Rt)*zsG@IOMw&eAexP9`O^K|2!Si#eV{ zt2K6heg;TfXavdq$M7BkA&%K3N_Lwn59R`>oyQ=iM53mlN${_L&#Y83WBvrL1B61Z zu;SD62U5EGVGCksWk~7fdBP?#32aYA)K4*05A;U7G3ZH_J-PVX*Z$R*RInL3Rn!zX zHeR@LMe;?XvxCDK&>}XE*^h7!IWH%P-~8B1myK37Y+9aFMEy4(JScRL^ z1XcHrUXwce@=gD?qsC=})6W(#9XdflT4>`DM?pnf4RALF1%(f2dFxJm)Z@df+^j`j zw)zYWW0R&^KtOpzuaJ2+r$p;K-d7E zy4Bh1QQnx8ZkZGS2#ymy6@*129))n42JB(iy zW{$OwH%SYLqmf8jesM8j&zsRY1wO5~s3-|!^J;|5NLhuz4wI^}OV*(k=E>JTKLy-; z&r{&x>8S_x^yn9CUQ?hoIv407P=kXHf6TO>cL%aRsL)u7D3I(kL!@}e{7>%5`wp-Z zQ7i&K_pO>F03SZTv7t=A*Iz{lM!@?32U(qbb{dU5tI<~}Y%>}xMZkL|Y!=W5u?4G3 zDkUW)b#lzE{%S46nzkLJ&=B0C2u~Dbsy+B5ZvZx zy(2Jq@n>55Je2$06NbJXy4MpEQ{c!FaKpT5II*!b#t5(1p_w6JpJZehpieMawUEMQ zG<<+wW$cjI-WVr8Ss58@oyvx!ocs6hV;wa>a|pYk;^t5rx?;CX!Zk&EO0;}heU3udd+}J78^|^M z9WseBeOu+g@d0hE zFvUtYZ!v^@OdR=woHY@_BY#j+Urz(6Zdcog{X{~FW5KY)XD?(0?6EzEUEgyMbSiih zz%n#dD7S+i0RvhOTeuEg?FiU1tB6)8#!sSWe9ZR63b~7B%y0ly5p!Ow&IK#-B)+dctf^ep`DSjTWy zkvO=@Y}Er+JI|gyi)o|)P3qhZef{QZZ;Le|SVgwM&YggLj~zp)iY-b{hu3t6lN0cG ziVaW&=nq&2{l7u&4SJs{;`v}-l^V{P`r;tRXt+i8bCdmt?sE~57Z}2J&d$;3NkkFS zyha60fMRnGfPq6M;V>0gFeMs>1Exi|s^+=YEt@7_tD8Ynj2((ee+;lZ@ei0_V#91x z`S|JCC3~o094dKK@Dg%V=&0I{UYXRBbJJj9dVubrlVQRzfPTKP3@r?KRgR4;aQz2h z&)FX)RN7QXT$cIi|K*WxSMf2rVoRIP3ut0WN6_20o#Q(V4V0!u2bH}Q&NNJA1p2bX z2I76{)hEiSfNm^bnCT)3SM|U)MPqv%OFAHvpaTU}{P1`u2QpZVFw9MBzpB8~_<;!c z1sAR9*0ntF$aW7bNNZ^cFU?c~1QeswuCv06rX&f*tP`0i1*7 zA~>b5t&LrfY9`l9{Ps@@f0aUh|HVypBGThi^OPpSHo*9has-KLH{%Hw zfeg1d1(X?E>@?(wm}fog3c_~|K<5Ue_+-!kCkkE*;;(*qD|Lts*SYnS8+N}wCmj13?MDoHOn*}8r!v9BP zpa(jm7*9+A!oJYZ(A$U>=RkGt1AP#T%vE4O;xJL;n}ru~7y<}}M7Rje))TxGm~H)t z1G*52A0Grb`406AFd2kTsf(Bp?SCQ>+s1y`h!(1`tXgv<^mE`-aTP>@2j^QxHa9mj zl^>Ok}|YpDtjCSr9c4)}_61lsMb@n&!^ zSkdP$FFL|@;KZMeoB13_g*bxEjOH*6xUce=x9G9ahqWiTpy&w@4p+Pyi9ys4In^vE zh&3iA3)#SRnEm7L@+n{x>fl^aT(;3p4F$guiRn<>!w%Cjhb&Y94s0!Q{lJgG&@(b} zHlR5z42Ke*czSLiqI$#^>}aOQ?%Gox zg`UQJpy>|wY%?(Vp!IuFmq@C^fYn1X5bqNybuOn@Nn7BDtj9x=nq7A@$GK5j0Eu>kh$P&7*eS@MQQqhfdf?A*qA0Gd@AR! zn#_#R8%f(Gy500JoCCgf4%Cb2eZXKYC3WY$8>Ekj@-M)vJUs0yKA#-@*m9?%dDb}G zwCZPf#hA{W1JHEO&4ip6+OCJye*fklC4;3hDZc_&2~TMo!a(KE?li)$3!K3`1!`Cm z=yOw-nlm)?EG*m5khx*zl9X|6XJ=x+Ov}E%j;OOb;CI!CKj3!g;Aq`^wBjXV9CWd+ zL1zVaE-q>S*#ocY{~+2QSepzA(@qY(VEQGKTPhk&q)5g4&P118Ii?~f`#d!}tK_BY z_5HqYtq#ZJ9PV;i=Ti32di~YCZ>=f3X=n^jUGSpiJA5s1cJZ{J5M6?MczplQAg$m1 ziL>PsZhQxQKYRD+nsK0rvUzci3aYgZ>q>+y=Y%!+tCkkk?;9pMfMCR*fQ@R`U3#Jv zXY>+U!LlSSqch73*{FBmsC&(BT(06f(|3zk(}uFa17SlD69o*0ZdThJHH+E}Kx}I!>-z#ygTrj(V{SWtJ|e z|HY3H7zgo_Yw-<~y!SYmXSv{q4>ZXo(KvGLlU>>Gt8+2%LwXO%Wy?Re7LTaOZNGsl zPiM&GU?33*O3kMW@7gRk|Kouyu{Jru%&4e#kxR;-EXcpva|!nlz?FmkGkK#^Mb<6*_NBS*6)NB9oK-Y__yo#0ndvNjG$}_oU|CqW5ryJh4HU0P0 z>1}Mk_V}NJPCvaN{D0l!tW|MVf@6pij&~6y?aNAMa)LB&Yd*JiUa^ z|I|O-%aYQGxDZwWW4`$$SWJR<~8)=!nc?$g!a58lOg`~ zr8ChnF+?X22`Q-qC0T7%fh0;bnwqM>2-@tprv?8mB9YfiAA1)2`S{(@O%nw|Wu=5?C*LV%2C!dK_j50(i&eShQ|$eDR9faW#pAX5rZgct{6BbvzF~BCrs#h| zQCp!N>aReb#=~r~?gRaQtmwfKgKEFj`yFjEqF<(Nr*B%3aYR-k@QUeSbN{ z=E#ONy_WIVyFgaBe>7>N{wV^lcwNUl>)_h6N~62<-yhUA>2#pc?q|C6dN-fd2O52& zjTUEJL;rJcSgG1oT&(jd=5O826F{@|nJei&?f$*#0d~$C`6a8qFDlh%$sE>Y)x(E5=Bk-ueFnb3?bWR#!rFV^ zm+?msL?2h~J33Tj4`UVY-3*sD?(+CRD94C48I0LP?6I+|jzG`lM-OQ)X&JLW=CwngIVtv^plpKPa8eD5hFF8+Y^ zL+-?wjm}x=rH`hO(N@29?K;U+K2F9=U%xyfq@IFKtX|{J#SHc#N>ir#kEf<@)qc`s zYuH#w5hiPAqMIm~rQ;nQy<%$fs#GRXTxks@mn6kFhOxb+j zCpT)DrJ^tVdU~ys=X_V46Xu2)W1UjZ>?Jjo+iT}&FXl#B8yV=eQtP`4 z6Yn9-bs@=S%!Wppg1mVnBad(4yDi-r_f)gm%oOam>#_JC@M1D{!!dD+Sz9w=!`YK`2JOY!Q}oQJn9x1TmEd&2OI4}#D@Y& zAMdB_wf)Vow=@3n%T)KUlm$+<+?%d`HjMWT^tW!_A8acw?(D;`+Isu0h@svk<}kON z{^pK02Hf3}ufm6t4uxDQtjJ1{BvT7PT}jxHFeH{Jlf6DsQ9EIpUt&7tajbc1O|$)q zP!Fro?BK!HK`9FRqWN+?y@Mh~HGzE#Kh$@7F%)>OF%$DuKKuG(vAclgho02koPYH2 z)24gh9ouj2=1XbvrHj6J(@P-lM~K+u)+{$(QPaUKN?cZ6M%aZ4Va6Ulnlt>}+jh}$ z8Ten_=Vm0vJ{k1UOS;R8Qli18HGgiIUVZRT^aJWqV;QF1p97h8aI#jLM2O3~@bYq~4i{cSJe5*EBJURrNWYaG(CN;c*A zhyL$W+o+g=CASQF&B(@GXZo{8%p_K}UXXd~vi^~xPC~`!JQv>P#bmZj9y8f3d&)mJ zdtjadFXsl(rz*TUy~8EQ^o+;cZXX33xv&~XpmHhSl$LNylC+b|fHD@w%;*$IttDU}CN~_b}5K{Sk|L%`o)-B%d z>Y~)4`TegC-q}f3&+P2`(u(=I zs+1pLzw8Y+J5^C;>{uqW>mA%R-yObxJF(oX7bOc^2e`)-J$?9f4uw@rpG#HQOvldt zK}jaKGvs2bca2Fv%h%FQY|I9R4cH{v>1Nw|Ao#GNauz2!ZF`W5zJO zlU29GjnRGZ(6kJ9Id3`jY}LYtpn3}TC_#!|-VR-EyCCK8bwiW}=34X%@x3ocC7rcp z{0Y=Jf{opw1vMS}J_j;Ln<)N#LU|!~vu!`EV?>&0YF>zmZ)MQ2>#fu36BqBCiP?Si zN_VSZY@)3dMa4tPH_Wy&;&(<7w-?MONqVt2RrxUA@AfQ>yH4@!ZP`3U0;hGvE*wKh z-?Z6^#_<(RR$ido#WJbxhF-dk{(WnO-;RAa_dTzNlF5|WjCGI8-!+US=?Tr~6|1ef ztSX`{-7Quw?xtbg&*GT1a(8OS#9o7E=@}iq%1R1J+u!f+X;WPt=1IT;QDjc49c=Pr_M%ct67a7+h^V#UXaxn?wQ;_vyQ9hb5~cSjL-%* zP3F2s0~_;Br^}aaB@r;Xc+*bed?JcNGi4zuaW_*K^5zu|wYD!=L&0zvuBMT;StQAL z2T<|7%zHJNRKaztFgfhgh<)L-O}NpSiI`EjT2tNcd(zD{p1LQvu37GgA^w1W#f7A= z4<79Ht9{phRC~}W?b}89ip7{U)}z7l?Ci??ckVE8_vVDbT)T2Q>9%Aanx-<~PFfYN z)A?{!Q%4nx?)x=aIT@4Aw6p5G)v6lpF#+;k4TTF>9?lIe{gk!HS}&q6aQgJ_>%E+y^ScntFq&4OP6kBV_W`%a?w})t}h2hxVoa zPJ4aT6Z@t(I~mAMmALq^i%9~Ws$!~jeEFrX`em55?q2++WvyiK1NswT~|-JTw!YQ^OlQ!qL4N-I+qetI0~@!_~%+x@(8HBFM=OvLP+lG_a9tGr9w z!)o)mHHI`|^$`)wj1?VK-pqQ8t7t}da zKic)dNb#~x!lHtHgVHEhUu(D86dU`VfkPW~3Qw)Hrj0w>_#^D5UrlsUrQDpYQ+{>OLZ}$ zd*hU>hTZVwhCa&@0f`-t&yBn?b<~`BlI0SZX2vEnE(G6+#AMn$wn)*CZQ|2Cz3K9z z&|rj#(AoZl^KI#Bfqe6vbapRxMLgKQGyRNTjjF>Y308%|485%@i#9LNC6sMc{!u9U z_~_pW-LaQaSd;Y9KFe6{vaKR@C4H+Gt4Scs<@T@EG@=0!RO!EZ{GhPrnAS+81Rd4V zIIIxnafD0M=J6HXLF%-gb?zfV9%GlnLNYdqT%WxKSt_haqBgJ`^lAZwES~1i%;(?cf_SENnZ0{d0eQjBsTW4_f_2E*3 zSpGkbS@w%hJyZs=ucWwK6=^l7_H+mw`J$B4WI^%F?!&@p=uUs_e%ChDsK}?M`IyrU z)XrS2f7`F*Luu{MpwlTyy6_?|fbBZRFw60<@xD`hg|E6YEW}5b6H0mqU5w=HzDI}Z zcnsf9<1i56qq?fwTHs;&#%YVMy{g0|Zgq2ob-t{X0i9An`zq7l1F8u?VRH;z^k{j1`N+YyOBU+DU-+LWC z^4dvsrGcyS%VX<)#^zL><|Wi-;@_9x_pBlZy>Jw_h3+Jsd)d(bDyghb%@G~G&r6vJTli!JkAlqUPCn-`C2 ze>r-0*}WwQ0mNtHLFbLMI&ImNPP&N;8|@x9XywjZr!QJFJWJ2@4phmmE))qpMt9?- zF}~aSjvRL5WmYGsz>6h?9jh?9D)aXCyvI5T-GZMdY}8D6ufRpKyxU~CV(6jb!@yAg zJPvkM{zIIPh8{_5**odP_a_M^E^g)FN_V$+Y+B3a&5(BDj0D$v2@)m4OEKvs>%(;} z6qzd*hCXx)KhV?j+_|@y^?~;aX5EZ4pT0Ow$fU10BJQ@IOndCSno%!-;G3)7YQ(GZ z;2RyR?_~y+>U+f}z04IpQ|oqxvANbfv_atWwRX*MMJC>hV~Duc1X$^`l*d*)zbzTE z+>T0*`N=zHHACBY(!F`(H>Cc|f_fn~+lAw0EN~BFcfR10rh5FojOoT+pP#iJJszkl zjoC2#%f;u9{zUEAMWg5&`tiPw*^PL+M<^-ICO)geX{(B z{>hW!L%K_6GIe?Vuj=fgE&)LdA7}lG91SxuqVhp24((ZZ$e6$4*^2Oo!4Z?u`QEEB z`6LM^^Er(|mhf{PiV`BH*Qs!_N%~*)+9xq_*nOl$Br_!Uf=2n64U4kaA2q;#{a$EB zn_;yoDf+j9XkyctWt{tp(3W79&|^+z?OXKpm<kY;BO;ZE~No_FeVrO?9}mFUwy{;!s^K(H`rQ=pQTD8Nb`7nlGPAa!gg)qkT(e z)3FO)yR@k6owb;2A90uV670G?3x79`$_|=-VM^WRV~AYN+&L(4uZi*D{>DO;b1x;e zOT9}Le-nFh7u&M5-LVR%zBHkDEg)h`rUvWQ8c8B!(!ii?(&lCmCL8*ON;2;Vebosz znFFI&42k{uT z;^xGQDX;lE*8&jlH*b4P$-9yiR>4SKQ(|4kowNT;6@Gx;UYvha3Gq*N{Qh{s!~pb6 zjQ`w?Y^+Ae4S>kM!3=8tc{So!;+ax^mWuqd>E%vTaeF8{-$fS$DUx657VL^g$6l}6H3_)M>gQ394gebk?lD5>>vT^#TiaWoWSUk z(*&nX_gbG@Gtg-F>`r5z>izuNq*W15`o7Ujp~Prqwl^)>DS@+WtS}nKYz0O z)OW;Ca6Kgr4X?oQ4^QeM*jlo1cGN;@O}m8nFGc=X)XujX8ouyqL_2h0)Na#{RIAwH z_(3)Sff!)!+UI=$4*-ht+icZRMSJ5V@b-m;d%Uw6bBJ)mT@W+Mti@l zBj+VLO0Nt0Jx9AgOAG|I6yVi50&FPketsX^=^yb%THXeqirrbt?>d{4)FVkj!ZT~zn679A0lhRnucK+&Xn{U6B%F8}`37B|w z9?M#cEh;h*?5<+1a_Mq@Tb#?)tw}w)IHf8szDnTuE@X4WsL|@@1Pa@uaq1}T;xG0} ziTvqG)#&bp^5fj+8*a`v-l+8xH9bHzZ?_y-D}12vFf+c%E+}|>`aJ-P`2UoaBWyUc zE!cConS%B#1<&m z1gMSY{6ue+2ymBJXwDlC7<{XdlToB5vZjTP|AWYs)9c#AeNu7_rkAe{v|U|_{kAIH z{>g1=jm6oYPxXye1PUFy6!^GWc%SWXnjAA1P~7l%GmD4@iDdC`ZjD|<*vHl=&hK+$ zRwbfpYq>=7tYk%g9hzEV6XJ<3#1C42P1su)N}O7Jyl}E&==H#}u=934h7t|u`NEa+ za?jZ2m87dEsL(NrT#xCeqO+UP^cI`$R!!Pzq~mEJUHNN1Yh=h|Puy_+?ib0L&%bud zE&XEm?p;T7_E&_xwe?!FoZnXkT^mHyBCiEioY?qR3&I*l6|;3_wVOPPQ%@jRJ}No1AmK!vkHdfYnG7|sX5DB!SJRUxB98CA#AnBlWg-Et=uZYP}yF!b}bWurUx~s^BO=xV!%cd zMN_~Yr2stoh*M0Tpw3Mqp-xvxJ6NWnrrGUr_!?>tv&!)Ue1~uS)AI_9M({hABayiai zgG2zWi_U_v=q&ysd2>}en;HFS;CjWOObQAL2-r9yz`6L>-vc5`7a9RJAo|Te@L7ba z(Mi?{L~&Q-#VyUQuO3=JW|84dAwa{uNK)Mw^r6U7&MeP17IlAsnRsz3fC_*fFF$Yg z9v4JqmY@?52m$aWEFS;(-X&{gOk?K#@c3`E7Wa-(Z+r4UF+utC(sp#{U!egyIbL~EWZb)uy{pXx~o zz(moHH3Bew(*dB1P6No6^jv1Sf(mH#8zxc2SaeJs7-r%uLSxlVaAcdzgP zTeI+@RxUI`eAZ|v$@ymQzWn}}*ZCiRI)Na@a<_(Fj9ROLYul|&r?~32$8EFl*|a#- zug1hQ5qs}#+KZ7`0j}8O;CYG`D!Q5~7b`Ui)3kP z8Wu6secrcl{B%|pW5c=RkPEjeChLQ>wp0n*oY0cv&gq(SoPXh;Vj{!dnkqZ~cu32W z=_@r!-$?CjdSyj^|CBL1yDF*f<2KR7<}FMsn_0UaTC?n~PxVp>{H1(FE{Fky4n;nN zrGD29`~`T^JLu}p$FD*ezo%vLSdZZ5#aZ>3j@|U9GdLUiUUrz@bvojvr^ixlm6t(@taQkmUf_JJ`KL*tw29^TZ_k^!VetN`(yzIOAFxP0XQ{>AYuSO z6!GTGUQi8au^-Ac_t?u7ouPy)M10;Lb!f-^XloeLPJ z?bu^rJH+Bhi#{Nq5?-(8V?x8!27n^b(?1#v27)C=FbM#6UfTO~WNeK2_I$%>^Y*sN zK>utYlyH+qK$F=)AOn@9P&+R>n+cq)C*KF3w*Wj%z_JLs6$l^kK%HH@$l-e&yyQ1| zK6M^|VGzn;@7}$N;EgefXc2AW00XNM+(s}kfO#W@&kDCz7^TA@Xogkhp8>@Ub}6D@l?PJ2)V2Q5wZ&pRSliZqW^ASb8|7 z=Xx|uL%wjg+{bOPJe+BUm(DXs3jyosEO$}i;Ek6n*ZV{s>B_zNbj=!GU+0`T&$CEX5L(;+V4Wo2$=1la}Td zqM`~rpO@G9_zUk&y@^VDoxx8MM)z&mh>_3#T3SI8zvrZIV&kgDkHzypJ3df`SS(JM zem|ys@`pwL*ubhcCmCKeRL;!!;~$EAG6L4FmNllPrez>Gd@oY}G~oTgKlw;!X2PZb z@f})MOz*KTx^zXkiiwFA-ZuG^QT*{&a>KYd;!(zFw-P zVx?D5P$=-8u=T|eKN&~bvl9urBswKxIM4_Zq)34BZ?qBgF>q64a5R8;D4PQu=(b*e zUb)QaDbxM?*8p@af7u`v=*;tMI`ns2q=C^~3lSjBnA2!Ks1$_`ctp2s#67D***=1u z#&uC5id)v~=|GWX4j&GyCyC-1p4B4f&z<7}pXYm31har7h^t#MrcQo{Y#9#Rh)&PU zy_%_?Uitlbcn}N2z5Dk+#nG00 zbYfWDMv~Lg@>q46zx_GBcEYTwYGkBvET00A=Ynp{c#!Y)eOGROr?Vple`Y#N1H=X* z*zrsQ{Tu**v64HK>2T^g$^F6AJH#Yl=$dWYO9}O?rqIfu{tY69bEEsIxc8~Mb?)wn zVSia4Cll%97iQSJHboza^y6Cl*IJvUWR2q4%gx>9zt)z8ZUj5RK94W>wfio&wWs3Y zD@?6+gU1-`?%n&yUBlt|E^((P#nHSO#-wmv`}OPKfT>kwFIJ5A^pE+T>t)iQVb^IX zRTj}y^UAhJIY+zvvCsl4zPHytRjO~mWbd6$V=dIwc6Q*I`@$gV=}uaFEg5qq?PurD z_(tCL-fR;~-m2FQX)6{gyu6sZCpb993kLST=#`Y>F3tZhkyF?DdE}z zq5+$qe1akY$4pAQkr(MiNy5|u^}@{7bnxdpq+dZC1DF&Qz-IOpD;;9JMM_a-u}j~1 z{Jq2UUHB$N-i#x&>?ZbJLVf^2;#3jF9rln%GI3xVrM3iPKjvPZ$L{m|`Ez%`&uxkp^1Asm?f_<$u4HY=BwjJmBrF$z$K4=0!vT5f95|B* z9z{}=djXN=q)n4!=G6VNd$9`)41$?~sq+L(ijb6b_>*HB>-ij$@(|MZyN+)jE1H(c z$DeLShHKefNgE!otc5SnWIm zzVdZ7m^t29!F1PF7VlO-emkOhSYAWJxw=Nh2Y5;4POpvb&eQJ^6t_=k6pXZLzxi9B z$z_)CfBz@{kmY(u{kaGWj%($+uXVk*{Jhuis-EI`AlOOACMR?b`-mMCcHHb&Iy$_X+Rf%2)oaD;eq`EE=cyk470bq$DOfUa9^QSO--^cN z)6%=V_xmnu7F;Qy)NA_Zh`?rTi(78hJnGrzkCq37{;_K+^8MN=xDRpU>b7gg&Hor; zWNGDgRy`xj*)DSk_clE3z3Uf9dFZM?qn?3&Fx@zPUD=&5$rNfF#}C}2D9z={%Vw7I z`>yT&=aZeb#pjcLLR(c-comhDj&pNwGFw^20p`OBBzIc?_LyU4}_{b*tAQYCNJ(#+5!!k7%nWk7fhcqPXle*jmXRfqxd z0sF3W1EPJHOYa}&`qa76YKsfMa?I2r0HU637ZVrexD$|LG59EfrThpVK*YnS@-^=< z;Gx+Aa*pNFjFI<}@f^!4suvS~Udn#2NDG^;dP5Fp$}aAQionzWfvEg9YU`tqC!W7Or@OUebxs{taN3>Tb4XN4?(dUa8D22%&@8m&Y0GSeqZw*H zsC9h)kG;p{iWY5Ff)Ll3n4adoq&%H{zi)1zweElVuFL+6*Po{@i;Gn2%E-t} zrBAGFdcQ2Pv44G)fw|uB{aU6hr_spuXNM`D=!z#5vY!to?|m()d1@!S*7@aq8xQk4 zZw_AM;Z?6p%U>)V?j`?_sg993YLn0!xz}wy2fu#2B5sPwpF+paL&wn^sd#p>YPvGr zp+bg8QxaY>O{D2d?WgOc^`_T6)+|R`oVw&!Gc|SJdujL9O?N!Y-#JWE-yJw#n0WFq zsbpd1phi_qMeWI_h64jnnmWy=#8*CUx?VcL#2&hSkaYVz4e5BdLM21wbHLpaA97jSm_Vw5sU(Y zF@Z5bOQ^jd3OCFq6cWDb!jS78@|W$$v2qEE#3<+hr%p<|q$a%h=OB^Krxi`@1YPAZ zI8G2Zzns(|RLm+@J3`Y5}nv+3~KC%7yA3xytA+k!4 zLLt5JASftEAg8G4sK~5zbn2Q0GA1b4eiM!}p8H^X@7%i3*vN=;*?2ivN|&K9KrE?* za2TjmkZAY>z6u+^Z<=ELWMcC$8<~ZTtsHtAe|)=XQq)6ACzYoclSg@Y-`qINV@#=* z6~Cr>>)?Iu)uVxuF`{2nn};Vs2i`>T*nMQ9g#RBu%$-W!#zY4fV<9({(#7o`PB07G zjvPjW^9Y{zl%QIP8ylYmaa>MOQSY9)S!!w^+3(+$jh@O6nID$-ZLest@jArIYbbWS zsLgqc#tXNw@QT$C7pqzZC5F^Rf6K$%fDRo3?xcc(L1wnd+@Q5!H8!=!3<^L6*7n{4-9&#~8ts_m}zWPn{qO;7zE z?W-~OA2LcXbFkzdW_c{~WNvDY4txKAZuxXgq~N(y`1NtpAxI-_>A0NUAYt~dLT+@hMq z_f<+-x*UNxk-rlqBLtXJ^QEAw_kur?u@HI#St}MmCd@w6{m)_9Bxr6>%q|0QI91gE ztPz5ih4Aqm2cJ{7MS>Y(obW4acN=U7%%T3wUj1SMi|TRj>K;!2@4A+ zYZcRoMeWC9BiK%a6E;y60G}^=JI@31dqu=hAS&MFjap(c=I1lOz+?)u|6QO9vWG9U zm<-R;Wcn(-l`*)-oet*GNf3^Siay)~$~A;nXATN2k8l&Lbm*W=Q5{w3+?^~&MB=4~ z*wh|pWN;njyT2LiuKVt*@H+lIRydwomU-Hwr!9}z#`RRfv|wZw#q{GelTJkwTefe@ zM7aUU{amlbCen7DPiw++-7^W~%6(Kjw;)H@N5#DzndnvH?|g>YRD&IH9Kk%(Wu0Ai z>MQa=Z(ck&SmE0`@kaB)SkBYulsk{|zpwe7{%CprI1BgHHM^vylo#P4CQUx49)Fl) z6V|r#iTreBWoGqsfF?N+L7wIO#D`Bz)E~cn+p)uw;!U_^`ASEjiuTz0=$Adz)6~?! zGJHF@PsK(^lMOf7;H5vO?WfV#ocA)yE1tcXzw_y{#IFK!REu(iZl{~d}5CY;l#KfMIhD8yWfJAHZ`^}Kw(n{lAW;QJ_y?CtFC6X+!s`?adp_AO1j#4x+3OjIeLF6A6g1!AfNIZhA7ZPK|8rw)KSR$#z!;|Jk1#QTbFynEY z8PsW_TY=f>29dWpKaUUpYy5M~Hb#zlqE&tTorCu)d%ZE!mHCjg7opZ$-NIzcUOcuJ zSp6KeU^!Yic&L^ky)4n9=r-r!$%TjF6N67i$TcdF4#b9*aB(y#Q{L&+{qJ}rB)6@I)e zZ_RDKwKXVvu!V&ZnlI(s#*4idu5Ocyt&PZv-t1&M`nkjI#+OL(Ltk~vmc>u-4eef= zziM+UQ9fz1H+I~zr)IiYv$ws4QDEUi@vUj{S-t4EC=}O3pFH`MX~OhO?4)CJd;2y2 z?mr`sM@Cqo9)ajTO3IS+w9Fe1ZPzshq2-0ENcP^{i2C@ZK8|xa>`0zVgXJPcr-|y? z$bmuYz}L0b5>^*#0Sx{n{KYOG0K^Bspwj2N-dAv-{+osghH zVc;x!2LvxKSuJ}f!Pml8zB_{d_FJfMoR$gwhoEsmcmS#!V9%C=Oo<3u#d@f23zW0+ zpzlId7r~W2j%ka)7hVh>5g!sI^eFM+xzn0xRKO>Amou7l2#P;Ip`ePMV$U8?1Rg}; zAG`E3Q~+F)W38+~-uqfvN#t&j=%RO-k)Ii9IY@dA;&@c;--Q`60z}4x5M?_m^YBd} zHhA>dsGS^}zEo6fBYI=F_hAnGNB9g)8xyJMq5Ksp;BWvZQ3hEGVs?VltBZO*lEkY} zWFd8<|K-5#Hu{29?GG2n@#K>wc^p#yl9L?b23v_)a; zN@|O5H|iKuYw@5ji$X||$VM}pO;B!_YTDFXk4q=?ltB8lhvHe^fPH_}5rLVXSD_VH z4#N132>xG0hK_EYFDf7r5QieD;1JiSVlQkqJWsd+qhVw`TC^pxP^Zvl*aKwC1V{C- zU$=;|GGt)4_%aCl!n_DpOuEosr0%iB0|s60{-5pE{B-c9m;fR1yUr2{R^%C{y{ZrX zw)qPfG0*k1Sg9S1%^tapNC~j3WgD}}bv$EsyYA0;TDrLG&Pq+`7&VIY)V)+YpDfH( zg!kQRDc8TV@Kv6=7aMhXv#{x_3YUT&+6^Mk)4g}Svro$wUM1~QcU{&Lnpcgv%>>2YnLb@!LE7pk8PpfQ zoBXHX_^0_;T(pKY`6#2uUIrccn%9#DA1fr7PNdCdRUL(_fsxa?9n<~8s3)S>oz@i2 zfFzp|CGNYuAo+_9H@y~7=ZEtzruRsqj}Y^)Z(j%Hh*)lCJ3&7^av znRpLq64l7vNA8^SEG#ViRq9qM@B`&X$n%&cYXpT(Er#m~_?eSer2ezh;c!q^!YP`Jd_ zupi|XLK$=_m?+vJ;Jgx5`?@j1Y2JK;2;#J*0-l$A65Ho?5kMIE?0=bLo0)>t%mo@q zGyHT2la8aqqom|$pXYS!80WqXbel}*v=tTq$&`?g({6oPRndItBgzUbN_L-l4)4Ag z68gA3)bwy-Vt3!Dk9mjF>yDpiIXDhIh3**F*`3wuug81sX0D&OpOLXo&~Tr#OOnM{ zz{|H!qMkk-Y)zppu->y(u2{uz<=vz0yC}qI*K??6(WE-QSY<>>MSaEJotjPk9pY^7 z!m}m~#z%#1DfRVf7M3nt4S(;tV79E>?ly1jyizWPvi=D~wK?kK^n!gMg`~!yRR$bL>?+m6!g&#q3SZO_oTP=UX!!VAIB9lI zOwNincaPl-k9+vg`y!f{OOKy?bNWrKQS?vQHi~2MCMuilhP8%Glyt|(G?`gguC{-b zM?T|$26n>1ihJjTDcDm}w-43sDya@W&KkW7Gvk4sP<*G9EXrDcCUu#a^R`35bFwSwAn|^r_*Km)33A!lS2V-0PhVN;&Wg^Tfj*t8 zg&oZ)Ev?@hPyKvF4RwO&v=pZ!`D=$~34Kp2<};3hDCGYvadEO9ZEYM`*7iYZB~-{o ztz@_W!X^dQLl13(=BiQwL?M*mG@vR6Tn2&|Lb|*)r+7uMBszLCQFa0A9}Tk6u1yeQ zx%d0?4tslhB8sQxAv8>nAKw6I2R4w*_#WUhzJB}mKq288sKoqsW4p0afi92I_ht~4 z#jz9vt8owY5khPBHYcG$0RH|pIl1TUg26#Sp6JjK;b5yLPQwBamwssIKPc#7$Gl7Tod~5RbT#*YU9$odl$UX6Hvk=q}Nc9Bzl??FEe!JnYy8BCf+x;1 zYfi5(+A`*>q&ae2c=_!y0$0|wsU?LJJ$0sfPOeh(qGg8&EB<8NJUz&@Cv#Hj;?&^h+WS9a)T>q2V@dW9af zhL3@1fxM4p_lFITW;mx&yk(LKx;{(7PV;oyqJisfD=t!wCm%fi8O?G+I_6Le%f^S1 z?sq;aH8qxR%?a#E?J8F~B(JzxbpI`pEt^^YVP)Q4clG7ohay*Y?HDTVxKt{9Wz13f zC+8X72hG!y-RXXptj6-gFAd-5FBzyhLW;i<<}fBY+OhhQzGPiXvpy0**Q(82)e*#2 z#!W>v0-9R+#&ub5=oc${m+ZeRlUwqOjURiQJ#$9Bw!&}t$&PioAoG>&+uX^qsQ)-3 zSDJ;E26U$+QQuO4JOZMjR5t)oHYdk$>VK!~ux&FlZccqm!)u-lqd__WdpSI{24q2k zn)H$%C^wKuBugDH_r&Ay*JLTxOkG``W9HzMoh3InM;yPZl;-+K*wufCjCbj;J^3yzjxrjci_Kw;JPjBXZF9}h&T0p z_Af}#SW52~3z8DX_wn}ac8WWuLS?EK`P7CM7sJ?Ns`y146#c%cg|Tn?a7t2Ews~#S zHfZk(!CH7t#|c{Lf|l#6=#v)wsR_Ph5$5dTd>$Zh?wE}t*>}(Ju!-#0ExtGd?3w#c z@egVb=9uEh`6*6W+{at$V*-*m87w+#^GdYFf40`gkQV5m5gokJAL8ZJ8jI%1_ny2g zhGAd;Yg-wbm@i(;QyjG$ZH=j=SVhH2!arKV;zkqG^gsd%bU_-Ih>IpQZn?Qx@%iAh z8$L_N$r-y;PMtZ!E1S8Op?M*tFwo`cgFWtE(Cm#+%``|d$sJS6vwQ)N-o0-hZ8VfWPn-Nw`9clDhL0(SDO z8$P@6<+OdlZc^!Q%ZoDu;_Hz?+H1nluvaDR#1%r$YbYmW^_nE;+X!ps~XkS3fkQ;05)`hKclul!Ku zXE22NU|&;shk3K${4+(^YUIzJA#QzvS@$tIVb>y!<-gbS;U>mx_kBIKiv8M3Z^Ntm zhYq;2Xu(GVU9HM4vwFZy1XpKnl7GE>gnk1{LWch-M#e$kOP)i*>u;il+p?z(+5UcV z0M*G`EFZFY9`>0Pb6prs*>gfrbE9Q><&PgtXpOTMl;BCLWh_|Pj5k&H_87l_;Jpw1 z>hs7bT>(vEPLN3Y_w}RQ1C#X)-xKXJ;ZAPq1_qtMz4^sagx_|cbYij8%hwIu; zmK1$zEk6XZ3JYrw4IMe8={DO~&5|B0^%76{!K1AvNUp=!w~*MmJy`iF^rbtFNy#i$ zIhoyy{uW#!{VI#ipF3A$X)~txQO<29Tgdz9V`EkehTbvc8cAtH9`)!M^8e2IwKBE& zQcmr}={>kC_eugUPk(gv`72h{?l+;G(s`t+>NWA^Ym?Nkrm*OB0EYPVjrX3+ww!^wlH zEm7;o+lK5kFmY75=D)vMaajz&9FeO&_b~gwFE;Acq2MpZ2aZM<G^ z-Y6o-qDUs&*@d{Q`*#m}ta00=%DiQ5(}*9aVG0;p#b6}2rIiZ35tfDQJKIfdl4VT# zpD$l&O0PFn3Etn5RC@QK-kIyKbST&~J;lP3pLTtHRU~;|LysmnR5x}QgE233A7%-| zk1r-GuR4P3GoxA?lY7)UGi4q(QH;fSy0)iE4;)A=57=C>pQ@0ZB`So}w!~K8y zVnkgE4DT!}(n2IlWi|VSKugy>|NOHIyv5kfGO^4d8t||2bmb$KHvyR}*Npi`^w$Y2>f8LQv z0ddc&yxZr7bO2=|clMq}wWJ2Cvq+%sz5egE<~bwh)|tFds-BYBVfi$9NL^oevMAXo zgfS{&L==mT#$}wqt&;kLQx&o%6W@JW-wq1VBw1aPPAN%=^SF@66jJ{C-Oditg~h=X znb8@^W#z=hq($Y-oky3tyU+P7=_JYM>p8r?LQ?SpNTOr?!y|snZg*ltp8a%2>I-{| zuiz<@@5bNWsQ1&eEe_~kx+I;$eN+FNH=8F77RpqC&B=j(&$G*#0H&}UrOyYpwk4X|}Rv)27Nx^OdT5`hB)oE&|2#^Q+UCk{V6@ z3sPPkpZ=xAf|0l`|8jYuiffo_Eq;D4dCaWxXSN}+jltS_O{VOpRNI{KOo5qXRkamc z?ycxHnuc2P1Hs1AtkHGD8S1Pw&HXefv<*_(t~|y{{w2Z(^7-}a)_kEt>*-q`tU0D!HR(_guKPQ}=jRw2 zL!8`Ff7Yx6i*h#0ErNe>I(&U(=s;pJu)u=rE4!8eR{S$MYF5DD%O`SXw0&r3r$C~3 zxVj>DWR7PNr{1@xFL>5o?)qrVXLUdP%f#Bf^W4M6tG+S%hF|Syc~(Dg znCEslsi!z*-c(PaYnOVUMHS?x&X~P>Pt(%JVy`Dj*o(-s)df2#R5o-gjIkA@kx^ZNL**Y=*pG-sTfrO92=D>?jQCzJ;op5#Z0w^ z?M)$X+Y)v2oUXrkp&g;(#viRJ%)l{ipy35^Ru%HVhWId6e|oo-dyNS<<8G{4R&8(% z*E#Iv=cgiK`A>2V%l!G;X4FMa;gch1^6%G#CcVbW;GHN+)NwV8w=o3D%gvGI6;O$ZsS`dZ zG7Gn>hHR-rg7fW}`eWX@dnYOY|4jJmA>G&4J3OR-PjxqbaAsrTYl^+}^&bLhDMR{% z^gqn3~R|ymo@KFTF(sZC3w*|sIG-(;1|tx$TN$0QzyPPDo8Pg_CwftNPJUbAY>D*>TfWnXMb z)qCZA^r}F$ar$5^z2HoL$jiPyrGj0O#AlG~H*PSY%ANUa)Ui^{Pj_9$O1<-RvPaV% zatuX5SA6Wp2j;_LMvj3^d&x1pmvU{?wYtxrH>%O;SnRm_w$e8IA?ZV4f_#|={8q0) z&bSVW*+=sKMjr7~3w>(kNBr+_aoJSZ(azS*vhggD#ZqltZ9~Qk6=ff#B_<&(eOvim zzVheoCyVxnh6k+g=4f^1YlHNbQuKR+q64bPbu0&&$d8V2ThEy`UKDDF@T;S49pfh@ z;@m~DKcqDqpE$*#T_ZkPOXj{8+@6p^URWRhR$`kZTK#ZS;|MR7QQGAh}GxHGPq29T>hW4^F~cEkq>!FWBh){+F5R) z6}ffId<8M85O0Tt4!hwp3WI&b3PU0#^2Bb(<@{)FRzY#T7PDAVBRnuLrh;KT$zxC9 z&DG`Epz!c43Ta9`5P7*#D&fZErS#W4*|DHu00$ct@lsXlysJ%sNd>PVghFyyc zL(qC>)5yDy7Wi44QgkMe&bZ2nh=@1@4BelW<~3T%GRkOM{C9H2uf0A&BC&4*Cgvtk zES1?c&z{{tuJy_!+^)+e2$Ha(d4C67o+%OO$T3*R2FV$v?$|M9y@SMVzm`8hdEmhB zAudV1^jqiu%(cyap?&$9`tP|}s^Tt_Fir$Dufz;dt5#wy%8L;7Hks)=B-J;(Tw#^0 zoMWz8XiRRf+d*WBDB03&Y})w~>tPKH1MBq_y*$&5uwjr^B;hi_nJwN4buKNv8Z-Vuxj`h5SggRWU|SfuxhH?s zc5!(*D8ICh=Az~8^!ZMQ3b|rF2QI6Bp9;JB%I}n82w=u-iZ5>>H`o5EE>!G3|0kel zDC7I*6W_2j-n4(+Iqn)|ytrgvz(_h&8!vxSI$TH^O`j$ECf4e=mt^tfeg3yx&xD1j zRapt&1CXed$u-U^=@nUMpqh0SiQ3N2jg^@g)XV&%f!V= zojK?oecOVgapLY?(TDRNx7Y7xHMx8zT~ zS7Y@;Lb;PvgE78y#}0W!Cr{jz37ZACtt3Sy4;3@jsa^q1u^7&gcS!B9uSnkBOBB;1 zLN5*rpHfYA_gqqS(Q7#Rdx(e(6E}i$CR47O#g4U9MT<9~l&r~j=T;A?k@za@U+r?(IM{VU?j*Vq0Q zD2Olr*T1zzP?I`+G|o6(toSGq|`csdlT!{@#tbzUi=05ImagLB;>L)}o9e>@ z!^6{n6aIcT(+Ehppp~_}yo+eW#=~mh&}yw4)>+gN5_QTvGTK0A`5rEOoVs&IpDKn0w0BK`MFVKt?)3jV19GrrFoj%2vvGRj8>>6 zvK-BVg7WM@4V~HdOND`nUEOE1^KxR2?)Bu)E>2Y(-?e>r!;6>c^!c}m=PVJr`kIo4 zCJ`r)uSM(e4!{YbqZQ0&6$@jr7?7}1Kbbc=_64e7VT~lN7EcObF7siaRHJb}Zo>V$ z!F|IfjY{u_$4M~|4SEMDhR)g6A?vdjE+_-6(F|NzKu}N|bov#4eVqljb+Ad@g1C6x z81K<>SU5hw!kz-e2sRy=)IM~A>33ljYHWTy@MV}oH(b%%L>(h!sE~m-KSg_K@rujE zRl(w!DdEOUJMHHCNsVd=cK5IVC{i(+oo+F`kc~tAngS&Vo*7z%Ee1#FO5||rau?M) z>@tDjlz|gfMvE!&46vT0zpwyZhX_@{E5aHMo~vS<3>^+c1%GyQR9kb*LSTj&0Yiy+ zFdK}!N{ZDP)N@QB&MIxU1CMAh)f@xBOFG$G9G{H|GFeHSfup>2? z$7IcXSt{_=tA5*}Wcer$NT_tU=7kG5DBv@VYa^e4&)n{-w1?(A^V~_{Q>R3+$lruN z5QzOsx#k@T&2U#qLG2O{2`Q`{pc^J?7Ur~_n+9G#;})xNXHh;Znga1AleFSj?>EGs z{@wdKz~4Xl;fgcnEemUMtZ`GKQnIF+apI3~4guNexjE&O2}^`A6qNf3^ur^kZFnW#v~tLWNQuZJVpCtQ?MQWpd+Ft7p7wePI*Gx{{AQVlPs5z!{5Au#mMnSyhWGNlcZM0yoH-}| zbjLuTw%joW3yUX>$uU0&a3k}^HxADXjuem^M<0_-GFHS^Uc zbjl5#u(x0VR@(qz$2%-?Y8alW`WSGvz@z}+8wV4m<`p0Cxa;8!IJmR{Gd6tqDN~pn z2$|rF>1V6;jgO~+17r+1q~!JMau{8VRGk-!?)OPbzu^)E2J%M2uoEm4cpz4S(#qE! zOTaUwFgSh|j+Z0>hf>h!TtR2YjDdoc91IRvZ@v$eF-RApIC?Px!U<;pWEowi>JX|9 z*qo<<4!tCt`xLdb!c1BK3{Gj*a*~G0>se-I%TZm7$vXIaXpX`pFY)O?rn78p($M2c zUF`?1rJ49Z@MjYNSN4y_LTUtjrCR!(CvEXFUA@-)v*2f=Qo87e5r=20aY&KC&39PW zOQ+@#bNecV;2(Z-*hmkSERVg^Ib6P^nQa_MK*CiFO)^_3j~-2~pm(~lzSLY=W%}j7 zvaF<}0!}koIcej@jWehPkm2%Wne7A+Yc`Ow1V)=i&@l^Mwtbct|KK*_{L~?Lao?vK zW;Vp&w#Kf+cXThvRYt$w>%;`N_5l1*(DzE+8URpiSKp1ZaoyL!SE-Gbc&RZuRxr$t zJ#CO-eZdoa)FeDTRDhY!pP#XF+}3U6p)Qn-m&C-#b(2N@s^v5acG$R z-J+yXx6)Oy8sXJw1^<@;;o%&G9a20dO$`A<4Dqt1>KdyZcJptI?aKB5Wza3TBd-`^Vqy~Qntf^b$gX^E zW!SXm0#WwU&s0;o#HHXJlw!Fh%RdWbOc#*kzyJ90s;tMdalOilgIwe1XOrz@FvaM? zIjX529aq_7Nhu_Jj8jfGoIZ`ILNJM^E3HnK!tWJh@p6+M@z14TL29yiwqYRLpi_10q)O)@+j??>-}c5{dRA;Su2P?{5vh)9SGHdL7`G zvmduHFmrJ!U=aI9gISFkG6?fAb{jLm78|_Nr znFoZZ3{#m1Nl>6)mX?;9kbQut90KyFkuA4968JM>MFo?U*gwz1Q@MF`6{`cWK{vpY z48=-We9P~=@Sdo}AP>-l3WOX0Od1acy4L1+`HuQ3cqS?IC?3aF+Xzi18&JN0?KT3} z!MGv*1&F(&j+WT6euD9UG$9s|#~Dml{JI|7gSVg@0my;(2(i&*xh&ff0~=^In)8Ow z33#*%))f8DSU23pzTx2{0$_>)E>XjTYi2z7Rm0vKC6?KlZKIOX(nuqF&%JR6a}oVVx{P*KM&5EB%9*eet?G}wq;VZ*ZXQ@(STLSB>DKghdCZP2 zvu)>ktf@w+OsA00l6i+1-9_pszX7+{XEVDycUP#_lMNF{*s_Z{9?@jTZtIJBWgN;(^uB~yz6&3PHHfNhhiPInM zqggyF;pQ}Fq?&TUWpeZ^-6h&I8};1EyO}1-7QU4sys6D6DW28&(D4s>R?B<&O4api zE;mFO$V#uxCwaAIaH z^X8s1&S%gXXnU^5s#WxBR1iUu*tKhq?~P_R7reqUewadFLL|Ke+stiPZXNN^IgE(I zgHadeVuw^y?Dz*R}{zzFxlkAzhJBhH_#$SEuEOy!>@#UnS zfI{*(5X`iYSl~qiOy{u2B;wB_%1Mnd7)ntutg5;DY=$+VQsBwBz64yPDVLWIOUp=Q`Kg%78E}J` z=isK9Yl|533v^7Lfy+?Fxzq2U|1&t)3`N?=yypD)Tn~(l@7Yy7mWL5SaXLFYKLZ6g zcMcj&T-cc=zq6#}X-RQuxvkpd+9FaDgY|1|q7qAUM(Ii)hX>5R?_saW&B5wKd;|SB zb4p6e!#IWcPA8mP-t>=4J|O;NeRcyL+Jmtds5tx*NZOpqPrEn{6Z_e)FgJJo#MBfQ zJ+SA^8RvAr1lW(f5ppRw{tYfs-a3b$v~}Liq-yhkI*dp4U2euB#22`+h}iC4=K>nl z3?B&5mh$&4rr6gmd6~`QS4|U>ljpFJ5YHLFQCk0J2>dBcb}2GpQRI{fm%Z@2E+eU< zbO(p*c~)4n2^kNFwh1iX={aYG)J04~E$5s??SZm8>4&EevDui|%{1`ouI6Zdc@u4Y zlAW#XF#pKdC*4Zf<@Q#yj^Zxj{J*U9r9G)s$b5EMI^@WM%a^}=v$txS92~lMRiS5c zaDu~pcx|bH+oQvr*Yy{r{^uC|LSI3*_2n)f{jOBY?6&mv_dvHA>@ISN-yihK?Aj=w zA)e@BD`W23kJAQ+X>-=~K5N9!dRwR*@qgx4I{QO(czM9&u+&ugYWfX!g+Z zaG|&Bv#CO+AiK+<`=6fS0Rw@BxO2qiOqRtmU-h4yi1MQvF0|E)uI*0rz>FFzt^p*Q z?Ovf2SAp$#&*b`8)ygGT{-%qT-4Stib|^u}ygu;(@;lLYruXn4-HYXn1^h^GP*pG( zqCd&&`8U9?FLw?biE&-@)5yrkfUvMsh++}J7^-bZ8i)aR3L)M#Wa@0{uT~h_rNA7f5WeEuGstjo>WNi601kjGe-oc&k*?n0-OtoflyBKkgSWFu zm{WZ9kH$sfoD13ThHzjq+xtTTqd^q48T@$}d<+$&Vr?sxrSvvS;+VCwP%$g~b-%C! z5>LXQ<>j$aY~L=28PNW!v4C^ncJ1~iFz1Q=-GjD$V8?C>3Kig>v%re>Zpt)iX5X@H zTV-3DI`-T9>dm<0bo|!WiClcTf4xvC*KWKsW+9f8=ykU`^_9+H$B~|Gv!C+}X1_-H zv2Ze9|9JfP&JxRH?%h`_gIr>M{^VR-SQxAtsyBD}WqQG^qo~$Qwf=53dKBuht>nhE zG=7`md^VHD*c9ZF2q%rQ4ISPR0&&RR614>9ZF}}--NWu#aNJnxea`Y3?Hj- z5>V$fmuyYnxMdmW>q{s+rHD*#F`Ytkhb=$)ljra2>$}dnRs%viYJ-DdfpC^u*uNj6 zBVpnJkBCj*z<`RWTN*M_0&NRl)TI6=o~D%5`D!r4yfgigO8SWZ^XJDBIB#nrg&?fs zT%bSYIPli40YlhktX&o5&w2zJw_=06V-GBjQjA58>!#ld2nb-5;CGr!W~6eO2&}%# za>4CBbak*WOFCQLtere@=~A1SgQ8*p;d*)JEef4=C|k9^f<96d@>md6LaI-fakls8 zuiE&Oru5~ul?*zq&HXw>C3I|Leq$3NnNv(mK?`zaClJk;r@7HeZi>Ga^V+N4i1NoN zi+oOTtNQ-x>^s$dOWnnGvvYqWf@jj68n+hZ^VNe5qG2@T;9)`s73!Mnc3 zQ**Qo3KTaRY zQIeoYn3|ona)KHqV@Ts^1Uk8WI9$*E{rn6JaaGT55HRL$`OV3vVaI6P$> zBEeEmtoBhd_=+O3<=Z^1=J5&)A@O?ptAH|EhFqur9`%aR@nqc_kfDXAw$%|Ni{}@xZwu++`H-u+zt3 zdYb@9rJoqZ912#LTe?ktem*dBD(Dq5ezUo>j)H_&Hj-!?rw(u@k#jaPr)#rd(@|2< zRElkVKt2YUE*PXL!pb@QHaB~!WtNP@$>YUEMX;YdR{e%s9DJHUWJLgd-HKJCxnRmH z(OO_*l5PW37CIb6T}oXVT0!Qc_wV1=h-&r5--#F*8J*H{KEtooz^Zdi!KiqNoP_rR z{PxbgXi}`%ZwNNb2ObUA5rHe#8xuX(7udK)f2H@@bvZTS)Ml19#yf&#a8_c;ame30 zr)9dH{fT&kvWUo}{3zje$vPM&CKUs`PfVqdYam2Z>yXh7qg z+K00JR3>(3K5>Lr(*G*W{Z%j$Y6`So!bhbiZXv7!afo|oOD|NX7Z_#sf3K-g zS$g8>NQI469!09eo4H{dD|NCs(Lq&3&+H*nxHIY>T0~mZHU*B-FtSb}Wsd&zLLIwx z6ZCr9TUAq(k9hPHv}#jcL;#|pK!=4tm{aE=9FEV!oH*A;+jTJp#^z~Qfu;PB+A+Ji zxw*i<-8||9EZ?M=MpwZ`Jd4*@lWgEFS$3{*5vW;?qF@^3;kQv)c-gC8FwJ z2&ssFlXL9PQm^v19e^eJ8EVN+$Ke!Iy_j=KIY2IiyaGWckt(7p&GHf}U2~jK1(^hc zl_{k9?%6@;?z|hZEyVsbdcn&En!~4jmzJ`j*UG}ntAZ%W#^(xqqQwXI4X6rPr%!Bh zAFv*+z1p0Wr)+iCCHj;%bzVy4_wRMnH3A78g|_6T2aohJslsHXoeaDjt@E@7?$i`y zbI>GcpC6d{6<1NIaAn`(bk9|CbCh?<+MOYWCWFJ^?mH;l8(6bTN70--@}{1DK4jM~ zg`Vv-qw?w%YNRh5pt8TLC;8W^O$b( z=4MU1w+JR;ApOXU;xD*k%~)u8AcugquxyR6w*8%75bMA&+H-{K8ZtU}c$7z?_6_*h z^*3+c&_G=;`jdJL!QH`xHw62esH%^_FjS$D3+YpeW}z}}ej}bkW31W;vVHAz8P&fpE7}s7w-t4G2JH6TlIqEb)l!QspvHXK+LT z%w{+xm)z+zu7u6QW^KiR?bdq{gu^sK&Uw+F&Rj$gnq@ttfS@!9aU&T=OE7)|%kc}Q zzak)kqv9tx_nO?CmWE)-r03I25HttUc1GetF1jHo5o#T=p#h4O6z z7>6=9V{g==twSul#6UyFO-Pgxp}%U1M$+cQoo?$SwuRp&9m2oaS8b}oQBqONHtrBZ zbmP@pBr#A^VKVXa(kz+QBA?yRW~^1(qI9yryfh}wytG%Q(Kk5?I)+*HJq83Ltf0#} zlhLSq^lFw~&&Jj1_2OtywGun_@DA!(XHmN>J>B?dV`BkjZgX5*Op;5INyAiuvRYxx z1baAy%4dg~RC!Dq>%P#99qJ&CL{th3W^*okC9A7cmfVVb+m>rorR!!RxRQtwrMBMd zM+5_0nHknIzjn|GSzn*NTccPwZ1GcYUhtSifzuym7lGbQdj_pC#o8}PBH*@Jb1tH; z3Qt1x-r-WiETNFuuTojlrJX6fBnOSyf&>E#8}je9h$e3bolnA9MrDqI^E0>T+Dm2k z&|GL3)4Vj>QnVg#FkXUV7yIw5;r8s}mYg1_svC3?kC)0FAZf9l=)R0atcLA2OuB2Ki9+1U(f0hCNF1z+?QmJcFn5@wWDSPG8ke`Z!+7BJ1W5FRIQQ)hR$>cjQ6DJQ3>w>9_O*iuW`-y__ zxB4x4HB}j`G_bcU%+1^CXZ@w$ENK0GYjzYH|0UmR4&{-90=6e;a?NSy ztA1=_P&>@zx^Rw~!{NM;`QovrRIMZ7V;u)&h1IFF8Ag5D#$O-VzrQ}VgNDI1<5Fj9 z5zmtEyjXQ&V7f)kn0)BRliat`1k?0hi0a7qJRS%>bJHs- zW^_vFt%*w7Gj0P{uiV@sT8`4G)?fTe&{D=K>5a!JCMzqO>^M|DcyUFkp`>M>lkhs1 zd5DnUi4%9L!|3QbDt@O|g#RYUHW~y zA*JiEnbUkl7R!JV$z4D{>fos2M$(6M1(4(09p;PW^;G8CckkYPttIqL`rwOGwRJhe z99@NO1|}MLQ|;rbR{dW-c^+D)JvqhumSKTsYt8C`=>g&A^US0V(%n1%(n{U0Z6wev zB#{#j{-O+z)&8+FM^L|Z52W15=?}L%bpef+NWBe&Es1oU@h=>N#CYIedg_541cjAE zdb;`l`Cu>pMY=q{{(t%NvsLI|lGdEq_wMNv-1w4{f2m^l#f$wFdq~H&5$Dr86TT)i zzW;T!RH^1sCqe5TXZ(P@!YFV5&A3kg`RzFW6F3vjyO5l7@cXa_J_~xpu)lT<`dGc& zNY6LnYTVDMzQWO;X0TeVkVyA$zx;cl|F;YNe>=zjkC*u0KPMen-8*(^zsL8IKlg}1 OA|WbsE%nOnhyMo^R-_34 literal 0 HcmV?d00001 diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..abadba1 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,36 @@ +# Introduction + +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 + +```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 for prooph messages and collects information how these messages flow through your system :) + +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..7906cb7 --- /dev/null +++ b/docs/message-flow.md @@ -0,0 +1,340 @@ +# 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()); + +//Check if edge is already known, otherwise add will throw an exception +if(!$messageFlow->knowsEdge($abEdge)) { + $messageFlow = $messageFlow->addEdge($abEdge); +} + +//Or to override an existing edge for the same nodes: +$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` shipped with the analyzer use a `Prooph\MessageFlowAnalyzer\MessageFlow\NodeFactory` to create +the nodes. The factory is a proxy to the named constructors with an option to call a custom `Node` implementation instead of +the default one. 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 this: + +![Paper Plane Node](img/paper-plane.png) + + diff --git a/src/MessageFlow/Node.php b/src/MessageFlow/Node.php index b3f1f97..33a3fce 100644 --- a/src/MessageFlow/Node.php +++ b/src/MessageFlow/Node.php @@ -81,12 +81,12 @@ class Node private $class = null; /** - * Method of the class connect with another node + * Method of the class that is connected with another node * * Example: - * - Command Handler method handling a command (connected node) - * - Aggregate method called by a command handler method (connected node) - * - Process manager method listening on event (connected node) + * - 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 From 526c0c1d43377800ea7e0d7633e6570e854e11ee Mon Sep 17 00:00:00 2001 From: codeliner Date: Mon, 26 Mar 2018 00:31:19 +0200 Subject: [PATCH 17/35] Add more docs --- README.md | 2 +- docs/analyzer.md | 2 +- docs/bookdown.json | 3 +- docs/configuration.md | 23 ++++---- docs/filter.md | 2 - docs/introduction.md | 43 ++++++++++++++- docs/message-flow.md | 20 +++---- docs/troubleshooting.md | 112 +++++++++++++++++++++++++++++++++++++++ src/MessageFlow/Node.php | 2 +- 9 files changed, 181 insertions(+), 28 deletions(-) delete mode 100644 docs/filter.md create mode 100644 docs/troubleshooting.md diff --git a/README.md b/README.md index 138958f..384ac7a 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ 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? diff --git a/docs/analyzer.md b/docs/analyzer.md index bb203a4..2be3ccf 100644 --- a/docs/analyzer.md +++ b/docs/analyzer.md @@ -15,7 +15,7 @@ php vendor/bin/prooph-analyzer project:analyze --help ```bash php vendor/bin/prooph-analyzer project:analyze -vvv ``` -By default the analyzer uses current working dir as the root for the analysis. +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 diff --git a/docs/bookdown.json b/docs/bookdown.json index 83f8bda..df0fcac 100644 --- a/docs/bookdown.json +++ b/docs/bookdown.json @@ -4,7 +4,8 @@ {"intro": "introduction.md"}, {"analyzer": "analyzer.md"}, {"messageflow": "message-flow.md"}, - {"config": "configuration.md"} + {"config": "configuration.md"}, + {"troubleshooting": "troubleshooting.md"} ], "tocDepth": 1, "numbering": false, diff --git a/docs/configuration.md b/docs/configuration.md index 8f9682e..2fe5e82 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -5,7 +5,7 @@ An example of a default config can be found in the [test example project](https: ## fileInfoFilters -You can add include and exclude filters for files and directories. +You can add `include` and `exclude` filters for files and directories. Configuration should be a List of filter classes implementing `Prooph\MessageFlowAnalyzer\Filter\FileInfoFilter`. @@ -22,7 +22,7 @@ Configuration should be a List of filter classes implementing `Prooph\MessageFlo ``` 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 (namespace is excluded). +with their full qualified class name. Default filters are aliased. The filter interface defines a simple method: @@ -36,6 +36,8 @@ interface FileInfoFilter ``` Return `true` to include current file or directory or `false` otherwise. +*Note: If you want to scan non php files you **should not use the IncludePHPFile** filter because it returns only true for php files.* + ## fileInfoVisitors A file info visitor is called for every included file. It can inspect the file and add information to the @@ -52,7 +54,7 @@ Configuration: } ``` A file info visitor should be constructable without arguments. -Not default visitors are available but you can write your own. +No default visitors are available but you can write your own. The interface is: ```php @@ -113,9 +115,9 @@ interface ClassVisitor ## Finalizers -Finalizers are invoked after project scan is completed. They get the final message flow injected and can add not analyzed -nodes, missing edges or modify the existing ones. This can be very handy if you want to complete the message flow with -information that cannot be analyzed during project scan (for example used databases or queues). +Finalizers are invoked after project scan is completed. They get the final message flow injected and can add missing +nodes and edges or modify the existing ones. This can be very handy if you want to complete the message flow with +information that cannot be analyzed during project scan (for example database or queue nodes). Configuration: @@ -143,7 +145,7 @@ interface Finalizer public function finalize(MessageFlow $messageFlow): MessageFlow; } ``` -Let's take the example from above again. This time we want to change the icon of all message nodes with a finalizer. +Let's take the message icon example again. This time we want to change the icon of all message nodes with a finalizer. So the analyzer still uses the default `MessageCollector` but we change the icon after a scan. ```php @@ -256,9 +258,9 @@ final class JsonPrettyPrint implements Formatter } ``` 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 message flow. +the nodes and edges of the flow. -`JSON_PRETTY_PRINT` is used by default to get a human readable file. If you want send the file over the wire you might use +`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. @@ -331,7 +333,8 @@ You can use all **free** [font awesome icons](https://fontawesome.com/icons?d=ga $node = $node->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')); +$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/filter.md b/docs/filter.md deleted file mode 100644 index fb31732..0000000 --- a/docs/filter.md +++ /dev/null @@ -1,2 +0,0 @@ -# Filters - diff --git a/docs/introduction.md b/docs/introduction.md index abadba1..9273931 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -1,6 +1,6 @@ # Introduction -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). +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) @@ -18,7 +18,46 @@ 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 system :) +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: diff --git a/docs/message-flow.md b/docs/message-flow.md index 7906cb7..54805be 100644 --- a/docs/message-flow.md +++ b/docs/message-flow.md @@ -118,9 +118,9 @@ class Node * 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) + * - Command Handler method (target node) handling a command (source node) + * - Aggregate method (target node) called by a command handler method (source node) + * - Process manager method (target node) listening on event (source node) * - ... * * @var string/null @@ -167,16 +167,16 @@ class Node private $tags = []; /** - * FontAwesome icon name for the node + * FontAwesome or link icon for the node * - * If not set a circle is used as default shape + * If not set a circle icon is used as default * * @var NodeIcon/null */ private $icon = null; /** - * If set it overrides the default color used for the node type + * Specify the color for the node, if not set a default is used depending on node type * * @var string|null */ @@ -288,9 +288,9 @@ Also most of the node attributes can be modified using corresponding `withisDir()) { + 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->addEdge( + new MessageFlow\Edge( + $node->id(), + $esNode->id() + ) + ); + } + } + + return $messageFlow; + } +} +``` diff --git a/src/MessageFlow/Node.php b/src/MessageFlow/Node.php index 33a3fce..9c61916 100644 --- a/src/MessageFlow/Node.php +++ b/src/MessageFlow/Node.php @@ -414,7 +414,7 @@ public static function fromArray(array $nodeData) ); } - private function __construct( + protected function __construct( string $id, string $type, string $name, From 995d29fb0d6fdfe2636f0eb345eb0d7445337810 Mon Sep 17 00:00:00 2001 From: codeliner Date: Mon, 26 Mar 2018 00:58:24 +0200 Subject: [PATCH 18/35] Add link to prooph-do --- docs/troubleshooting.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d4ccb95..94cc708 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -13,12 +13,12 @@ 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. We need to inspect some of the -issues. For example in our demo app [proophessor-do] we need to exclude classes that implement `Psr\Http\Server\MiddlewareInterface` interface. +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 better if you cannot get a result just because of a weird bug. +it would be bad if you cannot get a result just because of a weird bug. -Simply use a blacklist filter to exclude problematic files. Here is an example: +Blacklist filters to the rescue! You can exclude problematic files. Here is an example: ```php Date: Mon, 26 Mar 2018 21:41:51 +0200 Subject: [PATCH 19/35] Always use $messageFlow->setEdge --- docs/message-flow.md | 6 ------ docs/troubleshooting.md | 2 +- src/MessageFlow.php | 14 -------------- src/Visitor/AggregateMethodCollector.php | 6 +++--- src/Visitor/CommandHandlerCollector.php | 4 ++-- src/Visitor/EventListenerCollector.php | 2 +- src/Visitor/MessageProducerCollector.php | 4 ++-- 7 files changed, 9 insertions(+), 29 deletions(-) diff --git a/docs/message-flow.md b/docs/message-flow.md index 54805be..06a06a5 100644 --- a/docs/message-flow.md +++ b/docs/message-flow.md @@ -19,12 +19,6 @@ An edge simply takes a `sourceNodeId` and `targetNodeId` as arguments and is add ```php $abEdge = new Edge($nodeA->id(), $nodeB->id()); -//Check if edge is already known, otherwise add will throw an exception -if(!$messageFlow->knowsEdge($abEdge)) { - $messageFlow = $messageFlow->addEdge($abEdge); -} - -//Or to override an existing edge for the same nodes: $messageFlow = $messageFlow->setEdge($abEdge); ``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 94cc708..af00f65 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -97,7 +97,7 @@ class AddEventStore implements MessageFlow\Finalizer 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->addEdge( + $messageFlow = $messageFlow->setEdge( new MessageFlow\Edge( $node->id(), $esNode->id() diff --git a/src/MessageFlow.php b/src/MessageFlow.php index 94cc314..14e227a 100644 --- a/src/MessageFlow.php +++ b/src/MessageFlow.php @@ -189,20 +189,6 @@ public function setNode(Node $node): self return $cp; } - public function knowsEdge(Edge $edge): bool - { - return array_key_exists($edge->id(), $this->edges); - } - - public function addEdge(Edge $edge): self - { - if ($this->knowsEdge($edge)) { - throw new \RuntimeException("Edge with id {$edge->id()} is already set. Got " . json_encode($edge->toArray())); - } - - return $this->setEdge($edge); - } - public function setEdge(Edge $edge): self { $cp = clone $this; diff --git a/src/Visitor/AggregateMethodCollector.php b/src/Visitor/AggregateMethodCollector.php index ecf037a..c50f0be 100644 --- a/src/Visitor/AggregateMethodCollector.php +++ b/src/Visitor/AggregateMethodCollector.php @@ -51,13 +51,13 @@ function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMeth $messageFlow = $messageFlow->addNode(MessageFlow\NodeFactory::createAggregateNode($eventRecorder)); } - $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge($eventRecorderNode->id(), $msgNode->id())); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($eventRecorderNode->id(), $msgNode->id())); $invokedEventRecorders = ScanHelper::checkIfEventRecorderMethodCallsOtherEventRecorders($eventRecorder); if ($invokedEventRecorders) { foreach ($invokedEventRecorders as $invokedEventRecorder) { - $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge( + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge( Util::codeIdentifierToNodeId($eventRecorder->identifier()), Util::codeIdentifierToNodeId($invokedEventRecorder->identifier())) ); @@ -79,7 +79,7 @@ function (MessageFlow $messageFlow, ReflectionMethod $method): MessageFlow { } $builtEventRecorderNode = MessageFlow\NodeFactory::createEventRecordingAggregateMethodNode($builtEventRecorder); - $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge($aggregateFactoryMethodNode->id(), $builtEventRecorderNode->id())); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($aggregateFactoryMethodNode->id(), $builtEventRecorderNode->id())); } return $messageFlow; diff --git a/src/Visitor/CommandHandlerCollector.php b/src/Visitor/CommandHandlerCollector.php index 2086f95..f6a3505 100644 --- a/src/Visitor/CommandHandlerCollector.php +++ b/src/Visitor/CommandHandlerCollector.php @@ -50,12 +50,12 @@ private function inspectMethod(MessageFlow $messageFlow, ReflectionMethod $metho $messageFlow = $messageFlow->addNode($node); } - $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($message->name()), $node->id())); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($message->name()), $node->id())); $eventRecorders = ScanHelper::findInvokedEventRecorders($handler); foreach ($eventRecorders as $eventRecorder) { - $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge($node->id(), Util::codeIdentifierToNodeId($eventRecorder->identifier()))); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($node->id(), Util::codeIdentifierToNodeId($eventRecorder->identifier()))); } return $messageFlow; diff --git a/src/Visitor/EventListenerCollector.php b/src/Visitor/EventListenerCollector.php index 3d67ce9..4c53099 100644 --- a/src/Visitor/EventListenerCollector.php +++ b/src/Visitor/EventListenerCollector.php @@ -60,7 +60,7 @@ private function inspectMethod(MessageFlow $messageFlow, ReflectionMethod $metho $messageFlow = $messageFlow->addNode($node); } - $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($message->name()), $node->id())); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($message->name()), $node->id())); return $messageFlow; } diff --git a/src/Visitor/MessageProducerCollector.php b/src/Visitor/MessageProducerCollector.php index f5f88ec..5d791bd 100644 --- a/src/Visitor/MessageProducerCollector.php +++ b/src/Visitor/MessageProducerCollector.php @@ -57,8 +57,8 @@ function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMeth $messageFlow = $messageFlow->addNode($pmNode); } - $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($receivedMsg->name()), $pmNode->id())); - $messageFlow = $messageFlow->addEdge(new MessageFlow\Edge($pmNode->id(), $msgNode->id())); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($receivedMsg->name()), $pmNode->id())); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($pmNode->id(), $msgNode->id())); } else { $messageProducerNode = MessageFlow\NodeFactory::createMessageProducingServiceNode($messageProducer, $message); From b20e801e450dd0d4e705330ad80405d4b6a0f785 Mon Sep 17 00:00:00 2001 From: codeliner Date: Mon, 26 Mar 2018 21:49:04 +0200 Subject: [PATCH 20/35] Add file info in case of an exception --- src/ProjectTraverser.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ProjectTraverser.php b/src/ProjectTraverser.php index 46339f2..ae6c386 100644 --- a/src/ProjectTraverser.php +++ b/src/ProjectTraverser.php @@ -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); } } From 80f12ab2da55cc15927572bc1ea3ab0109cf6c60 Mon Sep 17 00:00:00 2001 From: codeliner Date: Thu, 29 Mar 2018 21:11:04 +0200 Subject: [PATCH 21/35] Set edge for message producer services --- src/Helper/PhpParser/ScanHelper.php | 2 +- src/MessageFlow/Node.php | 2 +- src/Visitor/MessageProducerCollector.php | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Helper/PhpParser/ScanHelper.php b/src/Helper/PhpParser/ScanHelper.php index fa9287a..52bfb3c 100644 --- a/src/Helper/PhpParser/ScanHelper.php +++ b/src/Helper/PhpParser/ScanHelper.php @@ -214,7 +214,7 @@ public function __construct(string $recorderClass, ReflectionMethod $method) public function leaveNode(Node $node) { - if ($node instanceof Node\Expr\MethodCall && $this->recorderClass->hasMethod($node->name)) { + 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); diff --git a/src/MessageFlow/Node.php b/src/MessageFlow/Node.php index 9c61916..5c4659c 100644 --- a/src/MessageFlow/Node.php +++ b/src/MessageFlow/Node.php @@ -383,7 +383,7 @@ public static function asMessageProducingService(MessageProducer $messageProduce null, $messageProducer->class(), $messageProducer->function() - ))->withTag($message->type())->withTag('producer')->withIcon(NodeIcon::faSolid('fa-cogs'))->withColor('#1B1C1D'); + ))->withTag($message->type())->withTag('producer')->withIcon(NodeIcon::faSolid('fa-long-arrow-alt-right'))->withColor('#1B1C1D'); } public static function fromArray(array $nodeData) diff --git a/src/Visitor/MessageProducerCollector.php b/src/Visitor/MessageProducerCollector.php index 5d791bd..3e4d1c6 100644 --- a/src/Visitor/MessageProducerCollector.php +++ b/src/Visitor/MessageProducerCollector.php @@ -65,6 +65,8 @@ function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMeth if (! $messageFlow->knowsNode($messageProducerNode)) { $messageFlow = $messageFlow->addNode($messageProducerNode); } + + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($messageProducerNode->id(), Util::codeIdentifierToNodeId($message->name()))); } return $messageFlow; From 842ffadc771dff7ea0c74e3da517b78adeed9176 Mon Sep 17 00:00:00 2001 From: codeliner Date: Thu, 29 Mar 2018 21:13:21 +0200 Subject: [PATCH 22/35] Rm namespace from message producer service name --- src/MessageFlow/Node.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MessageFlow/Node.php b/src/MessageFlow/Node.php index 5c4659c..4411b1e 100644 --- a/src/MessageFlow/Node.php +++ b/src/MessageFlow/Node.php @@ -378,7 +378,7 @@ public static function asMessageProducingService(MessageProducer $messageProduce return (new self( Util::codeIdentifierToNodeId($messageProducer->identifier()), self::TYPE_SERVICE, - $messageProducer->identifier(), + Util::withoutNamespace($messageProducer->identifier()), $messageProducer->filename(), null, $messageProducer->class(), From acfc5d132c7842d5965b2e1222e01c67028bb93a Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 30 Mar 2018 00:29:44 +0200 Subject: [PATCH 23/35] Detect event recorders fetched from inherited methods --- src/Helper/PhpParser/ScanHelper.php | 51 ++++++++++++++----- .../Sample/DefaultProject/Model/Identity.php | 6 +++ .../User/Command/AbstractIdentityHandler.php | 34 +++++++++++++ .../User/Command/AddUserIdentityHandler.php | 9 +--- .../Model/User/Command/DeactivateIdentity.php | 22 ++++++++ .../Command/DeactivateIdentityHandler.php | 25 +++++++++ .../Model/User/Event/IdentityDeactivated.php | 19 +++++++ tests/Visitor/MessageHandlerCollectorTest.php | 42 +++++++++++++++ 8 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 tests/Sample/DefaultProject/Model/User/Command/AbstractIdentityHandler.php create mode 100644 tests/Sample/DefaultProject/Model/User/Command/DeactivateIdentity.php create mode 100644 tests/Sample/DefaultProject/Model/User/Command/DeactivateIdentityHandler.php create mode 100644 tests/Sample/DefaultProject/Model/User/Event/IdentityDeactivated.php diff --git a/src/Helper/PhpParser/ScanHelper.php b/src/Helper/PhpParser/ScanHelper.php index 52bfb3c..1bb8c4b 100644 --- a/src/Helper/PhpParser/ScanHelper.php +++ b/src/Helper/PhpParser/ScanHelper.php @@ -23,6 +23,7 @@ use Roave\BetterReflection\Reflection\ReflectionClass; use Roave\BetterReflection\Reflection\ReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionParameter; +use Roave\BetterReflection\Reflection\ReflectionType; class ScanHelper { @@ -114,7 +115,6 @@ public static function findEventRecorderRepositoryProperties(ReflectionClass $re return []; } - //@TODO Test: parent::__construct but should work, too! $constructor = $reflectionClass->getMethod('__construct'); $properties = []; @@ -141,13 +141,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) @@ -157,20 +159,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; + } } } } @@ -362,6 +370,21 @@ public static function checkIfMethodHandlesMessage(MessageFlow $messageFlow, Ref 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; + } + private static function isEventRecorderRepositoryParameter(ReflectionParameter $parameter, bool $inspectChildParameters = true): ?ReflectionClass { if (! $parameter->hasType()) { diff --git a/tests/Sample/DefaultProject/Model/Identity.php b/tests/Sample/DefaultProject/Model/Identity.php index a080df1..5ec4f9a 100644 --- a/tests/Sample/DefaultProject/Model/Identity.php +++ b/tests/Sample/DefaultProject/Model/Identity.php @@ -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/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/AddUserIdentityHandler.php b/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentityHandler.php index c92733e..3093e8c 100644 --- a/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentityHandler.php +++ b/tests/Sample/DefaultProject/Model/User/Command/AddUserIdentityHandler.php @@ -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/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/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/Visitor/MessageHandlerCollectorTest.php b/tests/Visitor/MessageHandlerCollectorTest.php index eb3c1ea..0ec5b35 100644 --- a/tests/Visitor/MessageHandlerCollectorTest.php +++ b/tests/Visitor/MessageHandlerCollectorTest.php @@ -13,9 +13,13 @@ namespace ProophTest\MessageFlowAnalyzer\Visitor; use Prooph\MessageFlowAnalyzer\Helper\Util; +use Prooph\MessageFlowAnalyzer\MessageFlow\Edge; use Prooph\MessageFlowAnalyzer\MessageFlow\Message; 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; @@ -47,4 +51,42 @@ public function it_adds_handler_to_message_if_message_is_argument_of_a_handler_m $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(RegisterUserHandler::class . '::__invoke'))); } + + /** + * @test + */ + public function it_connects_handler_with_event_recorder_factory() + { + $msgFlow = $this->getDefaultProjectMessageFlow(); + + $handler = ReflectionClass::createFromName(AddUserIdentityHandler::class); + + $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_connects_handler_with_invoked_event_recorder() + { + $msgFlow = $this->getDefaultProjectMessageFlow(); + + $handler = ReflectionClass::createFromName(User\Command\DeactivateIdentityHandler::class); + + $msgFlow = $this->cut->onClassReflection($handler, $msgFlow); + + $edge = new Edge( + Util::codeIdentifierToNodeId(User\Command\DeactivateIdentityHandler::class . '::__invoke'), + Util::codeIdentifierToNodeId(Identity::class . '::deactivate') + ); + + $this->assertArrayHasKey($edge->id(), $msgFlow->edges()); + } } From af2b726e044becc46ee83df3b2ee2c24f5da200f Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 30 Mar 2018 01:03:16 +0200 Subject: [PATCH 24/35] Get and remove node/edge methods --- src/MessageFlow.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/MessageFlow.php b/src/MessageFlow.php index 14e227a..0d53aab 100644 --- a/src/MessageFlow.php +++ b/src/MessageFlow.php @@ -189,6 +189,27 @@ public function setNode(Node $node): self return $cp; } + public function getNode(string $nodeId, Node $default = null): ?Node + { + if (! $this->knowsNodeWithId($nodeId)) { + return $default; + } + + return $this->nodes[$nodeId]; + } + + public function removeNode(string $nodeId): self + { + if (! $this->knowsNodeWithId($nodeId)) { + return $this; + } + + $cp = clone $this; + unset($cp[$nodeId]); + + return $cp; + } + public function setEdge(Edge $edge): self { $cp = clone $this; @@ -197,6 +218,19 @@ public function setEdge(Edge $edge): self return $cp; } + public function removeEdge(Edge $edge): self + { + if (! isset($this->edges[$edge->id()])) { + return $this; + } + + $cp = clone $this; + + unset($cp[$edge->id()]); + + return $cp; + } + /** * Returns a list of class and/or function names of command handlers * From 970df5754040a9f6ec0b88b91b91c33f1a56d8ec Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 30 Mar 2018 01:16:43 +0200 Subject: [PATCH 25/35] Remove linked edges of removed node --- src/MessageFlow.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/MessageFlow.php b/src/MessageFlow.php index 0d53aab..146debf 100644 --- a/src/MessageFlow.php +++ b/src/MessageFlow.php @@ -207,6 +207,12 @@ public function removeNode(string $nodeId): self $cp = clone $this; unset($cp[$nodeId]); + foreach ($this->edges as $edge) { + if($edge->sourceNodeId() === $nodeId || $edge->targetNodeId() === $nodeId) { + $cp = $cp->removeEdge($edge); + } + } + return $cp; } From 94b6e6e501f93bf1ae9426f4ba8517ab189736eb Mon Sep 17 00:00:00 2001 From: codeliner Date: Sat, 31 Mar 2018 01:42:51 +0200 Subject: [PATCH 26/35] Replace listener and producer collectors w/ messaging collector --- src/Helper/ProjectTraverserFactory.php | 2 + src/MessageFlow.php | 2 +- src/Visitor/MessagingCollector.php | 253 ++++++++++++++++++ tests/ProjectTraverserTest.php | 6 +- tests/Sample/DefaultProject/Model/User.php | 6 + .../Model/User/Command/DeactivateUser.php | 22 ++ .../Model/User/Event/UserDeactivated.php | 19 ++ .../ProcessManager/SyncActiveStatus.php | 48 ++++ .../DefaultProject/prooph_analyzer.json | 5 +- tests/Visitor/MessagingCollectorTest.php | 125 +++++++++ 10 files changed, 480 insertions(+), 8 deletions(-) create mode 100644 src/Visitor/MessagingCollector.php create mode 100644 tests/Sample/DefaultProject/Model/User/Command/DeactivateUser.php create mode 100644 tests/Sample/DefaultProject/Model/User/Event/UserDeactivated.php create mode 100644 tests/Sample/DefaultProject/ProcessManager/SyncActiveStatus.php create mode 100644 tests/Visitor/MessagingCollectorTest.php diff --git a/src/Helper/ProjectTraverserFactory.php b/src/Helper/ProjectTraverserFactory.php index 63e115a..0e774ca 100644 --- a/src/Helper/ProjectTraverserFactory.php +++ b/src/Helper/ProjectTraverserFactory.php @@ -25,6 +25,7 @@ use Prooph\MessageFlowAnalyzer\Visitor\EventListenerCollector; use Prooph\MessageFlowAnalyzer\Visitor\MessageCollector; use Prooph\MessageFlowAnalyzer\Visitor\MessageProducerCollector; +use Prooph\MessageFlowAnalyzer\Visitor\MessagingCollector; final class ProjectTraverserFactory { @@ -41,6 +42,7 @@ final class ProjectTraverserFactory 'MessageProducerCollector' => MessageProducerCollector::class, 'AggregateMethodCollector' => AggregateMethodCollector::class, 'EventListenerCollector' => EventListenerCollector::class, + 'MessagingCollector' => MessagingCollector::class, ]; public static $fileInfoVisitorAliases = []; diff --git a/src/MessageFlow.php b/src/MessageFlow.php index 146debf..2ba9db6 100644 --- a/src/MessageFlow.php +++ b/src/MessageFlow.php @@ -208,7 +208,7 @@ public function removeNode(string $nodeId): self unset($cp[$nodeId]); foreach ($this->edges as $edge) { - if($edge->sourceNodeId() === $nodeId || $edge->targetNodeId() === $nodeId) { + if ($edge->sourceNodeId() === $nodeId || $edge->targetNodeId() === $nodeId) { $cp = $cp->removeEdge($edge); } } diff --git a/src/Visitor/MessagingCollector.php b/src/Visitor/MessagingCollector.php new file mode 100644 index 0000000..6fd94b2 --- /dev/null +++ b/src/Visitor/MessagingCollector.php @@ -0,0 +1,253 @@ + + * (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\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; + + public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFlow): MessageFlow + { + if ($reflectionClass->implementsInterface(ProophMsg::class)) { + return $messageFlow; + } + + if (MessageFlow\EventRecorder::isEventRecorder($reflectionClass)) { + return $messageFlow; + } + + $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); + + $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); + $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, $command] = $messageProducers[$node->id()]; + + 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, $command] = $messageProducers[$invokedProducerId]; + + 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, $command] = $messageProducers[$producerNodeId]; + + 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 + { + $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); + $messageProducers[$node->id()] = [$node, $messageProducer, $method, $message]; + + return $messageFlow; + } + ); + + 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; + } +} diff --git a/tests/ProjectTraverserTest.php b/tests/ProjectTraverserTest.php index 9e5ef4e..7f696ce 100644 --- a/tests/ProjectTraverserTest.php +++ b/tests/ProjectTraverserTest.php @@ -22,9 +22,8 @@ use Prooph\MessageFlowAnalyzer\ProjectTraverser; use Prooph\MessageFlowAnalyzer\Visitor\AggregateMethodCollector; use Prooph\MessageFlowAnalyzer\Visitor\CommandHandlerCollector; -use Prooph\MessageFlowAnalyzer\Visitor\EventListenerCollector; use Prooph\MessageFlowAnalyzer\Visitor\MessageCollector; -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; @@ -55,9 +54,8 @@ public function it_collects_message_flow() [ new MessageCollector(), new CommandHandlerCollector(), - new MessageProducerCollector(), new AggregateMethodCollector(), - new EventListenerCollector(), + new MessagingCollector(), ] ); diff --git a/tests/Sample/DefaultProject/Model/User.php b/tests/Sample/DefaultProject/Model/User.php index c4be725..1c8177b 100644 --- a/tests/Sample/DefaultProject/Model/User.php +++ b/tests/Sample/DefaultProject/Model/User.php @@ -15,6 +15,7 @@ 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; @@ -45,6 +46,11 @@ 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/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/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/ProcessManager/SyncActiveStatus.php b/tests/Sample/DefaultProject/ProcessManager/SyncActiveStatus.php new file mode 100644 index 0000000..22b15aa --- /dev/null +++ b/tests/Sample/DefaultProject/ProcessManager/SyncActiveStatus.php @@ -0,0 +1,48 @@ + + * (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 ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Infrastucture\CommandBus; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\DeactivateIdentity; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserDeactivated; + +class SyncActiveStatus +{ + /** + * @var CommandBus + */ + private $commandBus; + + public function __construct(CommandBus $commandBus) + { + $this->commandBus = $commandBus; + } + + 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])); + } + } + + private function loadIdentitesOfUser(string $userId): array + { + return []; + } +} diff --git a/tests/Sample/DefaultProject/prooph_analyzer.json b/tests/Sample/DefaultProject/prooph_analyzer.json index 7c4d3e1..6e4a01c 100644 --- a/tests/Sample/DefaultProject/prooph_analyzer.json +++ b/tests/Sample/DefaultProject/prooph_analyzer.json @@ -9,8 +9,7 @@ "classVisitors": [ "MessageCollector", "CommandHandlerCollector", - "MessageProducerCollector", - "AggregateMethodCollector", - "EventListenerCollector" + "MessagingCollector", + "AggregateMethodCollector" ] } \ No newline at end of file diff --git a/tests/Visitor/MessagingCollectorTest.php b/tests/Visitor/MessagingCollectorTest.php new file mode 100644 index 0000000..c52398c --- /dev/null +++ b/tests/Visitor/MessagingCollectorTest.php @@ -0,0 +1,125 @@ + + * (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\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\ChangeUsername; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\DeactivateIdentity; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Command\RegisterUser; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\Model\User\Event\UserDeactivated; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\ProcessManager\IdentityAdder; +use ProophTest\MessageFlowAnalyzer\Sample\DefaultProject\ProcessManager\SyncActiveStatus; +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() + ); + } +} From 6853a5562c4b59e04c1a273327f8e218f301cd38 Mon Sep 17 00:00:00 2001 From: codeliner Date: Sat, 31 Mar 2018 01:51:29 +0200 Subject: [PATCH 27/35] Configurable EventRecorder check function --- src/Cli/AnalyzeProjectCommand.php | 5 +++++ src/MessageFlow/EventRecorder.php | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Cli/AnalyzeProjectCommand.php b/src/Cli/AnalyzeProjectCommand.php index d1ef7ae..78e16be 100644 --- a/src/Cli/AnalyzeProjectCommand.php +++ b/src/Cli/AnalyzeProjectCommand.php @@ -13,6 +13,7 @@ namespace Prooph\MessageFlowAnalyzer\Cli; use Prooph\MessageFlowAnalyzer\Helper\ProjectTraverserFactory; +use Prooph\MessageFlowAnalyzer\MessageFlow\EventRecorder; use Prooph\MessageFlowAnalyzer\MessageFlow\NodeFactory; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -87,6 +88,10 @@ public function execute(InputInterface $input, OutputInterface $output) NodeFactory::useNodeClass($config['nodeClass']); } + if (isset($config['eventRecorderCheck'])) { + EventRecorder::useEventRecorderCheckFunction($config['eventRecorderCheck']); + } + $traverser = ProjectTraverserFactory::buildTraverserFromConfig($config); $finalizers = ProjectTraverserFactory::buildFinalizersFromConfig($config); $formatter = ProjectTraverserFactory::buildOutputFormatter($formatterName); diff --git a/src/MessageFlow/EventRecorder.php b/src/MessageFlow/EventRecorder.php index ad6c158..1e768a5 100644 --- a/src/MessageFlow/EventRecorder.php +++ b/src/MessageFlow/EventRecorder.php @@ -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; From c17dce9841ef9b6964edbd9a25bcdd0ebce3f0cb Mon Sep 17 00:00:00 2001 From: codeliner Date: Sun, 1 Apr 2018 01:01:55 +0200 Subject: [PATCH 28/35] Fix wrong unset in removeNode --- src/MessageFlow.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MessageFlow.php b/src/MessageFlow.php index 2ba9db6..1ff85f8 100644 --- a/src/MessageFlow.php +++ b/src/MessageFlow.php @@ -205,7 +205,7 @@ public function removeNode(string $nodeId): self } $cp = clone $this; - unset($cp[$nodeId]); + unset($cp->nodes[$nodeId]); foreach ($this->edges as $edge) { if ($edge->sourceNodeId() === $nodeId || $edge->targetNodeId() === $nodeId) { From 5b5b05999ef931703c446a1e5c7fdf1fcb7785dd Mon Sep 17 00:00:00 2001 From: codeliner Date: Sun, 1 Apr 2018 01:11:00 +0200 Subject: [PATCH 29/35] Fix wrong unset in removeEdge --- src/MessageFlow.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MessageFlow.php b/src/MessageFlow.php index 1ff85f8..3da4038 100644 --- a/src/MessageFlow.php +++ b/src/MessageFlow.php @@ -232,7 +232,7 @@ public function removeEdge(Edge $edge): self $cp = clone $this; - unset($cp[$edge->id()]); + unset($cp->edges[$edge->id()]); return $cp; } From 0d62b132489665f5f7b9c33febc57fb45c2d587f Mon Sep 17 00:00:00 2001 From: codeliner Date: Mon, 2 Apr 2018 18:55:51 +0200 Subject: [PATCH 30/35] Detect message production if factory is used --- src/Cli/AnalyzeProjectCommand.php | 14 ++++ src/Helper/MessageClassProvider.php | 18 +++++ src/Helper/MessageNameEqualsClassProvider.php | 21 +++++ src/Helper/MessageProducingMethodScanner.php | 26 ++++--- src/Helper/PhpParser/MessageScanner.php | 57 ++++++++++++++ src/Helper/PhpParser/ScanHelper.php | 52 +++++++++++++ src/Visitor/EventListenerCollector.php | 67 ---------------- src/Visitor/MessageProducerCollector.php | 76 ------------------- src/Visitor/MessagingCollector.php | 34 ++++++++- tests/BaseTestCase.php | 12 +++ .../Model/User/Command/ActivateUser.php | 24 ++++++ .../Model/User/Event/IdentityActivated.php | 19 +++++ .../ProcessManager/SyncActiveStatus.php | 33 +++++++- .../Visitor/MessageProducerCollectorTest.php | 71 ----------------- tests/Visitor/MessagingCollectorTest.php | 69 +++++++++++++++++ 15 files changed, 363 insertions(+), 230 deletions(-) create mode 100644 src/Helper/MessageClassProvider.php create mode 100644 src/Helper/MessageNameEqualsClassProvider.php delete mode 100644 src/Visitor/EventListenerCollector.php delete mode 100644 src/Visitor/MessageProducerCollector.php create mode 100644 tests/Sample/DefaultProject/Model/User/Command/ActivateUser.php create mode 100644 tests/Sample/DefaultProject/Model/User/Event/IdentityActivated.php delete mode 100644 tests/Visitor/MessageProducerCollectorTest.php diff --git a/src/Cli/AnalyzeProjectCommand.php b/src/Cli/AnalyzeProjectCommand.php index 78e16be..2734aad 100644 --- a/src/Cli/AnalyzeProjectCommand.php +++ b/src/Cli/AnalyzeProjectCommand.php @@ -12,9 +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; @@ -92,6 +95,17 @@ public function execute(InputInterface $input, OutputInterface $output) 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); 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 dc0a881..9d33a93 100644 --- a/src/Helper/MessageProducingMethodScanner.php +++ b/src/Helper/MessageProducingMethodScanner.php @@ -31,10 +31,12 @@ private function checkMessageProduction( MessageFlow $msgFlow, ReflectionClass $reflectionClass, callable $onMessageProducingMethodCb, - callable $onNonMessageProducingMethodCb = null + callable $onNonMessageProducingMethodCb = null, + MessageClassProvider $messageClassProvider = null, + array $messageFactoryProperties = [] ): MessageFlow { foreach ($reflectionClass->getMethods() as $method) { - $messages = $this->checkMethodProducesMessages($method); + $messages = $this->checkMethodProducesMessages($method, $messageClassProvider, $messageFactoryProperties); if (count($messages)) { foreach ($messages as $message) { @@ -51,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(); @@ -61,17 +65,15 @@ private function checkMethodProducesMessages(ReflectionMethod $method): array return []; } - $this->getTraverser()->traverse($bodyAst); + $traverser = $this->getTraverser($messageClassProvider, $messageFactoryProperties); - return $this->getTraverser()->messageScanner()->popFoundMessages(); + $traverser->traverse($bodyAst); + + 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 97e0ba9..162684a 100644 --- a/src/Helper/PhpParser/MessageScanner.php +++ b/src/Helper/PhpParser/MessageScanner.php @@ -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 (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/ScanHelper.php b/src/Helper/PhpParser/ScanHelper.php index 1bb8c4b..9479d40 100644 --- a/src/Helper/PhpParser/ScanHelper.php +++ b/src/Helper/PhpParser/ScanHelper.php @@ -16,6 +16,7 @@ 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; @@ -24,6 +25,7 @@ use Roave\BetterReflection\Reflection\ReflectionMethod; use Roave\BetterReflection\Reflection\ReflectionParameter; use Roave\BetterReflection\Reflection\ReflectionType; +use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; class ScanHelper { @@ -103,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 * @@ -415,6 +440,33 @@ private static function isEventRecorderRepositoryParameter(ReflectionParameter $ return null; } + private 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; + } + private static function getPropertyNameForParameter(ReflectionMethod $method, string $parameterName): ?string { $nodeVisitor = new class($parameterName) extends NodeVisitorAbstract { diff --git a/src/Visitor/EventListenerCollector.php b/src/Visitor/EventListenerCollector.php deleted file mode 100644 index 4c53099..0000000 --- a/src/Visitor/EventListenerCollector.php +++ /dev/null @@ -1,67 +0,0 @@ - - * (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\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 EventListenerCollector implements ClassVisitor -{ - use MessageProducingMethodScanner; - - 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_EVENT) { - return $messageFlow; - } - - $producedMessages = $this->checkMethodProducesMessages($method); - - if ($producedMessages) { - //Looks like a process manager or saga, not a simple event listener - return $messageFlow; - } - - $handler = MessageFlow\MessageHandler::fromReflectionMethod($method); - - $node = MessageFlow\NodeFactory::createEventListenerNode($handler); - - if (! $messageFlow->knowsNode($node)) { - $messageFlow = $messageFlow->addNode($node); - } - - $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($message->name()), $node->id())); - - return $messageFlow; - } -} diff --git a/src/Visitor/MessageProducerCollector.php b/src/Visitor/MessageProducerCollector.php deleted file mode 100644 index 3e4d1c6..0000000 --- a/src/Visitor/MessageProducerCollector.php +++ /dev/null @@ -1,76 +0,0 @@ - - * (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\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 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( - $messageFlow, - $reflectionClass, - function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMethod $method): MessageFlow { - $msgNode = MessageFlow\NodeFactory::createMessageNode($message); - - if (! $messageFlow->knowsNode($msgNode)) { - $messageFlow = $messageFlow->addMessage($message); - } - - $receivedMsg = ScanHelper::checkIfMethodHandlesMessage($messageFlow, $method); - - $messageProducer = MessageFlow\MessageProducer::fromReflectionMethod($method); - - //process manager or saga that receives event and produces command - if ($receivedMsg) { - //@TODO: Can we identify a Saga here? - $pmNode = MessageFlow\NodeFactory::createProcessManagerNode($messageProducer); - - if (! $messageFlow->knowsNode($pmNode)) { - $messageFlow = $messageFlow->addNode($pmNode); - } - - $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge(Util::codeIdentifierToNodeId($receivedMsg->name()), $pmNode->id())); - $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($pmNode->id(), $msgNode->id())); - } else { - $messageProducerNode = MessageFlow\NodeFactory::createMessageProducingServiceNode($messageProducer, $message); - - if (! $messageFlow->knowsNode($messageProducerNode)) { - $messageFlow = $messageFlow->addNode($messageProducerNode); - } - - $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($messageProducerNode->id(), Util::codeIdentifierToNodeId($message->name()))); - } - - return $messageFlow; - } - ); - } -} diff --git a/src/Visitor/MessagingCollector.php b/src/Visitor/MessagingCollector.php index 6fd94b2..25bbe48 100644 --- a/src/Visitor/MessagingCollector.php +++ b/src/Visitor/MessagingCollector.php @@ -16,6 +16,8 @@ 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; @@ -27,6 +29,18 @@ 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)) { @@ -37,6 +51,8 @@ public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow return $messageFlow; } + $messageFactoryProperties = ScanHelper::findMessageFactoryProperties($reflectionClass); + $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); $eventListeners = []; @@ -50,7 +66,7 @@ public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow } } - $messageProducers = $this->findMessageProducers($reflectionClass, $messageFlow); + $messageProducers = $this->findMessageProducers($reflectionClass, $messageFlow, $messageFactoryProperties); $handledProducers = []; foreach ($eventListeners as [$node, $eventListener, $method, $event]) { @@ -148,7 +164,7 @@ private function checkIsEventListener(MessageFlow $messageFlow, ReflectionMethod return [$node, $eventListener, $method, $message]; } - private function findMessageProducers(ReflectionClass $reflectionClass, MessageFlow $messageFlow): array + private function findMessageProducers(ReflectionClass $reflectionClass, MessageFlow $messageFlow, array $messageFactoryProperties): array { $messageProducers = []; @@ -162,7 +178,10 @@ function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMeth $messageProducers[$node->id()] = [$node, $messageProducer, $method, $message]; return $messageFlow; - } + }, + null, + $this->getMessageClassProvider(), + $messageFactoryProperties ); return $messageProducers; @@ -250,4 +269,13 @@ private function addEventListener(MessageFlow $messageFlow, MessageFlow\Node $ev 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 40fd5bd..de6c4ec 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -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/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/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/ProcessManager/SyncActiveStatus.php b/tests/Sample/DefaultProject/ProcessManager/SyncActiveStatus.php index 22b15aa..5397f08 100644 --- a/tests/Sample/DefaultProject/ProcessManager/SyncActiveStatus.php +++ b/tests/Sample/DefaultProject/ProcessManager/SyncActiveStatus.php @@ -12,8 +12,13 @@ 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 @@ -23,9 +28,15 @@ class SyncActiveStatus */ private $commandBus; - public function __construct(CommandBus $commandBus) + /** + * @var MessageFactory + */ + private $messageFactory; + + public function __construct(CommandBus $commandBus, MessageFactory $messageFactory) { $this->commandBus = $commandBus; + $this->messageFactory = $messageFactory; } public function onUserDeactivated(UserDeactivated $event): void @@ -41,6 +52,26 @@ public function deactivateIdentities(string $userId): void } } + 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/Visitor/MessageProducerCollectorTest.php b/tests/Visitor/MessageProducerCollectorTest.php deleted file mode 100644 index 32d0ad6..0000000 --- a/tests/Visitor/MessageProducerCollectorTest.php +++ /dev/null @@ -1,71 +0,0 @@ - - * (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\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->knowsNodeWithId(Util::codeIdentifierToNodeId(AddIdentity::class))); - $this->assertTrue($msgFlow->knowsNodeWithId(Util::codeIdentifierToNodeId(IdentityAdder::class.'::onUserRegistered'))); - } - - /** - * @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'))); - } -} diff --git a/tests/Visitor/MessagingCollectorTest.php b/tests/Visitor/MessagingCollectorTest.php index c52398c..6ccb8b5 100644 --- a/tests/Visitor/MessagingCollectorTest.php +++ b/tests/Visitor/MessagingCollectorTest.php @@ -12,6 +12,7 @@ namespace ProophTest\MessageFlowAnalyzer\Visitor; +use Prooph\MessageFlowAnalyzer\Helper\MessageClassProvider; use Prooph\MessageFlowAnalyzer\Helper\Util; use Prooph\MessageFlowAnalyzer\MessageFlow\Edge; use Prooph\MessageFlowAnalyzer\MessageFlow\Node; @@ -19,12 +20,17 @@ 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 @@ -122,4 +128,67 @@ public function it_adds_process_manager_if_event_consuming_method_invokes_comman $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() + ); + } } From 9c986aa35e731deb628bef5bab971611159b4bc1 Mon Sep 17 00:00:00 2001 From: codeliner Date: Mon, 2 Apr 2018 19:08:07 +0200 Subject: [PATCH 31/35] Check if $node->var->name is set --- src/Helper/PhpParser/MessageScanner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Helper/PhpParser/MessageScanner.php b/src/Helper/PhpParser/MessageScanner.php index 162684a..3c43a58 100644 --- a/src/Helper/PhpParser/MessageScanner.php +++ b/src/Helper/PhpParser/MessageScanner.php @@ -88,7 +88,7 @@ public function leaveNode(Node $node) $messageName = null; - if (is_string($node->var->name) && isset($this->messageFactoryProperties[$node->var->name]) + if (isset($node->var->name) && is_string($node->var->name) && isset($this->messageFactoryProperties[$node->var->name]) && isset($node->args[0])) { $messageNameArg = $node->args[0]; From 75c4f17826ed6e213952058ae5214eec9a576a84 Mon Sep 17 00:00:00 2001 From: codeliner Date: Mon, 2 Apr 2018 19:21:10 +0200 Subject: [PATCH 32/35] Remove method filter public for event listener checks A public method meight only be used as proxy but internal methods handle specific events. We're interested in this flow so we should scan all methods --- src/Visitor/MessagingCollector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Visitor/MessagingCollector.php b/src/Visitor/MessagingCollector.php index 25bbe48..b09f131 100644 --- a/src/Visitor/MessagingCollector.php +++ b/src/Visitor/MessagingCollector.php @@ -53,7 +53,7 @@ public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $messageFactoryProperties = ScanHelper::findMessageFactoryProperties($reflectionClass); - $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); + $methods = $reflectionClass->getMethods(); $eventListeners = []; From 42e7ba7d7fb6b715fa816627bfd50812599acb36 Mon Sep 17 00:00:00 2001 From: codeliner Date: Mon, 2 Apr 2018 22:12:40 +0200 Subject: [PATCH 33/35] Message producers can create more than one message --- src/Visitor/MessagingCollector.php | 78 ++++++++++++++++++------------ 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/src/Visitor/MessagingCollector.php b/src/Visitor/MessagingCollector.php index b09f131..c3ff83b 100644 --- a/src/Visitor/MessagingCollector.php +++ b/src/Visitor/MessagingCollector.php @@ -80,18 +80,20 @@ public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow } if (isset($messageProducers[$node->id()])) { - [$producerNode, $producer, $producerMethod, $command] = $messageProducers[$node->id()]; + [$producerNode, $producer, $producerMethod, $commands] = $messageProducers[$node->id()]; - if (! $messageFlow->knowsMessage($command)) { - $messageFlow = $messageFlow->addMessage($command); - } + 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 - ); + $messageFlow = $this->addProcessManager( + $messageFlow, + MessageFlow\NodeFactory::createMessageNode($event), + MessageFlow\NodeFactory::createMessageNode($command), + $producer + ); + } $handledProducers[] = $node->id(); continue; @@ -111,19 +113,22 @@ public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow ); } else { foreach ($invokedProducers as $invokedProducerId) { - [$producerNode, $producer, $producerMethod, $command] = $messageProducers[$invokedProducerId]; - - if (! $messageFlow->knowsMessage($command)) { - $messageFlow = $messageFlow->addMessage($command); + [$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 + ); } - $messageFlow = $this->addProcessManager( - $messageFlow, - MessageFlow\NodeFactory::createMessageNode($event), - MessageFlow\NodeFactory::createMessageNode($command), - $producer, - $method - ); $handledProducers[] = $invokedProducerId; } } @@ -132,19 +137,21 @@ public function onClassReflection(ReflectionClass $reflectionClass, MessageFlow $unhandledProducers = array_diff(array_keys($messageProducers), $handledProducers); foreach ($unhandledProducers as $producerNodeId) { - [$producerNode, $producer, $producerMethod, $command] = $messageProducers[$producerNodeId]; + [$producerNode, $producer, $producerMethod, $commands] = $messageProducers[$producerNodeId]; - if (! $messageFlow->knowsMessage($command)) { - $messageFlow = $messageFlow->addMessage($command); - } + foreach ($commands as $command) { + if (! $messageFlow->knowsMessage($command)) { + $messageFlow = $messageFlow->addMessage($command); + } - $messageProducerNode = MessageFlow\NodeFactory::createMessageProducingServiceNode($producer, $command); + $messageProducerNode = MessageFlow\NodeFactory::createMessageProducingServiceNode($producer, $command); - if (! $messageFlow->knowsNode($messageProducerNode)) { - $messageFlow = $messageFlow->addNode($messageProducerNode); - } + if (! $messageFlow->knowsNode($messageProducerNode)) { + $messageFlow = $messageFlow->addNode($messageProducerNode); + } - $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($messageProducerNode->id(), Util::codeIdentifierToNodeId($command->name()))); + $messageFlow = $messageFlow->setEdge(new MessageFlow\Edge($messageProducerNode->id(), Util::codeIdentifierToNodeId($command->name()))); + } } return $messageFlow; @@ -175,7 +182,16 @@ function (MessageFlow $messageFlow, MessageFlow\Message $message, ReflectionMeth $messageProducer = MessageFlow\MessageProducer::fromReflectionMethod($method); $node = MessageFlow\NodeFactory::createMessageProducingServiceNode($messageProducer, $message); - $messageProducers[$node->id()] = [$node, $messageProducer, $method, $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; }, From 3ff9b908feaa35f503a1fcdd0f54e2f2b3ea61f8 Mon Sep 17 00:00:00 2001 From: codeliner Date: Thu, 5 Apr 2018 22:56:28 +0200 Subject: [PATCH 34/35] Allow symfony/console 3.2 as well --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": { From 91a6cc885aaf390b190a1152baeb814557fa16fb Mon Sep 17 00:00:00 2001 From: codeliner Date: Fri, 4 May 2018 18:57:15 +0200 Subject: [PATCH 35/35] All scan helper funcs should be public --- src/Helper/PhpParser/ScanHelper.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Helper/PhpParser/ScanHelper.php b/src/Helper/PhpParser/ScanHelper.php index 9479d40..4cb6a01 100644 --- a/src/Helper/PhpParser/ScanHelper.php +++ b/src/Helper/PhpParser/ScanHelper.php @@ -410,7 +410,7 @@ public static function isEventRecorderReturnType(ReflectionType $returnType): ?R return null; } - private static function isEventRecorderRepositoryParameter(ReflectionParameter $parameter, bool $inspectChildParameters = true): ?ReflectionClass + public static function isEventRecorderRepositoryParameter(ReflectionParameter $parameter, bool $inspectChildParameters = true): ?ReflectionClass { if (! $parameter->hasType()) { return null; @@ -440,7 +440,7 @@ private static function isEventRecorderRepositoryParameter(ReflectionParameter $ return null; } - private static function isMessageFactoryParameter(ReflectionParameter $parameter): ?ReflectionClass + public static function isMessageFactoryParameter(ReflectionParameter $parameter): ?ReflectionClass { if (! $parameter->hasType()) { return null; @@ -467,7 +467,7 @@ private static function isMessageFactoryParameter(ReflectionParameter $parameter return null; } - private static function getPropertyNameForParameter(ReflectionMethod $method, string $parameterName): ?string + public static function getPropertyNameForParameter(ReflectionMethod $method, string $parameterName): ?string { $nodeVisitor = new class($parameterName) extends NodeVisitorAbstract { private $parameterName;