From 45abab57ced7a9e405e4d845d0ce0dc2cfd4be05 Mon Sep 17 00:00:00 2001 From: Iacovos Constantinou Date: Wed, 25 Jul 2018 15:33:51 +0300 Subject: [PATCH 1/3] Add Engine / Repository class --- src/Engine/Engine.php | 119 +++++++++++++++++++++++ src/Engine/WorkflowInstance.php | 46 +++------ src/Processor/Repository.php | 32 ++++++ tests/Engine/EngineTest.php | 39 ++++++++ tests/Engine/WorkflowInstanceTest.php | 29 +++--- tests/Renderer/PlainTextRendererTest.php | 5 +- 6 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 src/Engine/Engine.php create mode 100644 src/Processor/Repository.php create mode 100644 tests/Engine/EngineTest.php diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php new file mode 100644 index 0000000..b7b16e9 --- /dev/null +++ b/src/Engine/Engine.php @@ -0,0 +1,119 @@ +processorRepository = new Repository(); + + // $this->processorsRepository->register(Node::class, , NextConnectionProcessor::class); + // $this->processorsRepository->register(Event::class, NextConnectionProcessor::class); + $this->processorRepository->register(Start::class, NextConnectionProcessor::class); + $this->processorRepository->register(Error::class, NextConnectionProcessor::class); + +// $this->processorsRepository->register(Conditional::class, ChildConnectionProcessor::class); + $this->processorRepository->register(Choice::class, ChildConnectionProcessor::class); + +// $this->processorsRepository->register(Executable::class, CallbackProcessor::class); + $this->processorRepository->register(Callback::class, CallbackProcessor::class); + $this->processorRepository->register(Filter::class, CallbackProcessor::class); + $this->processorRepository->register(First::class, CallbackProcessor::class); + $this->processorRepository->register(Find::class, CallbackProcessor::class); + $this->processorRepository->register(Last::class, CallbackProcessor::class); + $this->processorRepository->register(Sort::class, CallbackProcessor::class); + $this->processorRepository->register(Map::class, CallbackProcessor::class); + } + + /** + * Registers the provided Workflow in the engine + * @param Workflow $workflow + */ + public function add(Workflow $workflow): void + { + $id = $workflow->getId(); + if (empty($id)) { + throw new \InvalidArgumentException(); + } + + if (array_key_exists($id, $this->workflows)) { + throw new \InvalidArgumentException(); + } + + $this->workflows[$id] = $workflow; + } + + /** + * Returns the workflow identified by the provided $id + * @param string $id + * @return Workflow + */ + public function get(string $id): Workflow + { + if (!array_key_exists($id, $this->workflows)) { + throw new \OutOfBoundsException(); + } + + return $this->workflows[$id]; + } + + /** + * Creates and returns a new Instance for the given Workflow + * @param string $id + * @param $input + * @return WorkflowInstance + */ + public function createInstance(string $id, $input): WorkflowInstance + { + $instance = new WorkflowInstance($this, $this->get($id), $input); + if ($this->logger !== null) { + $instance->setLogger($this->logger); + } + + return $instance; + } + + /** + * @return Repository + */ + public function getProcessorRepository(): Repository + { + return $this->processorRepository; + } +} diff --git a/src/Engine/WorkflowInstance.php b/src/Engine/WorkflowInstance.php index e034aca..b08f607 100644 --- a/src/Engine/WorkflowInstance.php +++ b/src/Engine/WorkflowInstance.php @@ -2,22 +2,10 @@ namespace Phlow\Engine; -use Phlow\Node\Callback; use Phlow\Node\Error; -use Phlow\Node\Find; -use Phlow\Node\First; -use Phlow\Node\Last; -use Phlow\Node\Map; use Phlow\Node\RecursiveIterator; -use Phlow\Node\Sort; -use Phlow\Processor\ChildConnectionProcessor; -use Phlow\Processor\Processor; -use Phlow\Processor\NextConnectionProcessor; -use Phlow\Processor\CallbackProcessor; use Phlow\Node\End; use Phlow\Node\Start; -use Phlow\Node\Choice; -use Phlow\Node\Filter; use Phlow\Model\Workflow; use Phlow\Node\Node; use Phlow\Renderer\Renderer; @@ -60,28 +48,18 @@ class WorkflowInstance implements LoggerAwareInterface private $executionPath; /** - * @var array Mapping between Workflow Nodes and Processors + * @var Engine|null The Engine created this instance */ - private $processors = [ - Start::class => NextConnectionProcessor::class, - Error::class => NextConnectionProcessor::class, - Callback::class => CallbackProcessor::class, - Choice::class => ChildConnectionProcessor::class, - Filter::class => CallbackProcessor::class, - First::class => CallbackProcessor::class, - Find::class => CallbackProcessor::class, - Last::class => CallbackProcessor::class, - Sort::class => CallbackProcessor::class, - Map::class => CallbackProcessor::class, - ]; + private $engine; /** * WorkflowInstance constructor. * @param Workflow $workflow * @param $inbound */ - public function __construct(Workflow $workflow, $inbound) + public function __construct(Engine $engine, Workflow $workflow, $inbound) { + $this->engine = $engine; $this->workflow = $workflow; $this->exchange = new Exchange($inbound); $this->setLogger(new NullLogger()); @@ -160,12 +138,8 @@ private function handleCurrentNode(): void $nodeClass = get_class($this->current()); $this->logger->info(sprintf('Workflow execution reached %s', $nodeClass)); - if (array_key_exists($nodeClass, $this->processors)) { - $processorClass = $this->processors[$nodeClass]; - - /** @var Processor $processor */ - $processor = new $processorClass; - + if ($this->engine->getProcessorRepository()->has($nodeClass)) { + $processor = $this->engine->getProcessorRepository()->getInstance($nodeClass); $connection = $processor->process($this->current(), $this->exchange); $this->executionPath->add($connection); $this->nextNode = $connection->getTarget(); @@ -308,4 +282,12 @@ function ($workflowObject) use ($executionPath) { ); return (string) $viewer->render($itr); } + + /** + * @return null|Engine + */ + public function getEngine(): Engine + { + return $this->engine; + } } diff --git a/src/Processor/Repository.php b/src/Processor/Repository.php new file mode 100644 index 0000000..a7509a9 --- /dev/null +++ b/src/Processor/Repository.php @@ -0,0 +1,32 @@ +processors[$nodeClass] = $processorClass; + } + + public function has(string $nodeClass): string + { + return array_key_exists($nodeClass, $this->processors); + } + + public function get(string $nodeClass): string + { + return $this->processors[$nodeClass]; + } + + public function getInstance(string $nodeClass): Processor + { + $processor = $this->get($nodeClass); + return new $processor(); + } +} diff --git a/tests/Engine/EngineTest.php b/tests/Engine/EngineTest.php new file mode 100644 index 0000000..e900430 --- /dev/null +++ b/tests/Engine/EngineTest.php @@ -0,0 +1,39 @@ +add($workflow); + + $instance = $engine->createInstance('test', []); + $this->assertEquals($workflow, $instance->getWorkflow()); + } + + public function testAdd() + { + $workflow = new Workflow('test'); + $engine = new Engine(); + $engine->add($workflow); + $this->assertEquals($workflow, $engine->get('test')); + + $this->expectException(\InvalidArgumentException::class); + $engine->add($workflow); + } + + public function testGet() + { + $engine = new Engine(); + $this->expectException(\OutOfBoundsException::class); + $engine->get('-'); + } +} diff --git a/tests/Engine/WorkflowInstanceTest.php b/tests/Engine/WorkflowInstanceTest.php index 4dde842..2fe3b9e 100644 --- a/tests/Engine/WorkflowInstanceTest.php +++ b/tests/Engine/WorkflowInstanceTest.php @@ -2,8 +2,8 @@ namespace Phlow\Tests\Model; +use Phlow\Engine\Engine; use Phlow\Node\Callback; -use Phlow\Engine\UndefinedProcessorException; use Phlow\Node\End; use Phlow\Node\Start; use Phlow\Model\Workflow; @@ -16,6 +16,13 @@ class WorkflowInstanceTest extends TestCase { + private $engine; + + public function setUp() + { + $this->engine = new Engine(); + } + public function testAdvance() { $workflow = $this->getPipeline(); @@ -40,7 +47,7 @@ public function testExchangeInOut() return $in; }) ->end(); - $instance = new WorkflowInstance($builder->getWorkflow(), (object) ['num' => 0]); + $instance = new WorkflowInstance($this->engine, $builder->getWorkflow(), (object) ['num' => 0]); $d = $instance->advance(2); $this->assertEquals(10, $d->num); } @@ -48,7 +55,7 @@ public function testExchangeInOut() public function testNoStartEvent() { $flow = new Workflow(); - $instance = new WorkflowInstance($flow, []); + $instance = new WorkflowInstance($this->engine, $flow, []); $this->expectException(InvalidStateException::class); $instance->advance(); } @@ -59,7 +66,7 @@ public function testAlreadyCompleted() $builder ->start() ->end(); - $instance = new WorkflowInstance($builder->getWorkflow(), []); + $instance = new WorkflowInstance($this->engine, $builder->getWorkflow(), []); $this->expectException(InvalidStateException::class); $instance->advance(3); @@ -92,7 +99,7 @@ public function testErrorHandling() throw new \RuntimeException(); }) ->end(); - $instance = new WorkflowInstance($builder->getWorkflow(), ['num' => 10]); + $instance = new WorkflowInstance($this->engine, $builder->getWorkflow(), ['num' => 10]); $instance->advance(2); $this->assertNotInstanceOf(End::class, $instance->current()); @@ -107,7 +114,7 @@ public function testMissingErrorHandling() throw new \BadFunctionCallException(); }) ->end(); - $instance = new WorkflowInstance($builder->getWorkflow(), ['num' => 10]); + $instance = new WorkflowInstance($this->engine, $builder->getWorkflow(), ['num' => 10]); $this->expectException(\BadFunctionCallException::class); $instance->advance(2); @@ -129,7 +136,7 @@ public function testUndefinedErrorHandling() throw new \BadFunctionCallException(); }) ->end(); - $instance = new WorkflowInstance($builder->getWorkflow(), ['num' => 10]); + $instance = new WorkflowInstance($this->engine, $builder->getWorkflow(), ['num' => 10]); $this->expectException(\BadFunctionCallException::class); $instance->advance(2); @@ -144,7 +151,7 @@ public function testExecution() return $d; }) ->end(); - $instance = new WorkflowInstance($builder->getWorkflow(), ['num' => 10]); + $instance = new WorkflowInstance($this->engine, $builder->getWorkflow(), ['num' => 10]); $instance->execute(); $this->assertTrue($instance->isCompleted()); @@ -171,7 +178,7 @@ public function testExecutionPath() $builder ->start() ->end(); - $instance = new WorkflowInstance($builder->getWorkflow(), []); + $instance = new WorkflowInstance($this->engine, $builder->getWorkflow(), []); $instance->execute(); $this->assertEquals(3, count($instance->getExecutionPath())); @@ -187,7 +194,7 @@ public function testGetWorkflow() $builder ->start() ->end(); - $instance = new WorkflowInstance($builder->getWorkflow(), []); + $instance = new WorkflowInstance($this->engine, $builder->getWorkflow(), []); $this->assertEquals($builder->getWorkflow(), $instance->getWorkflow()); } @@ -221,6 +228,6 @@ private function getPipeline() ->end(); $in = ['a' => null, 'b' => null, 'c' => null]; - return new WorkflowInstance($builder->getWorkflow(), $in); + return new WorkflowInstance($this->engine, $builder->getWorkflow(), $in); } } diff --git a/tests/Renderer/PlainTextRendererTest.php b/tests/Renderer/PlainTextRendererTest.php index b07aaf1..f52f612 100644 --- a/tests/Renderer/PlainTextRendererTest.php +++ b/tests/Renderer/PlainTextRendererTest.php @@ -2,6 +2,7 @@ namespace Phlow\Tests\Renderer; +use Phlow\Engine\Engine; use Phlow\Engine\WorkflowInstance; use Phlow\Renderer\PlainTextRenderer; use PHPUnit\Framework\TestCase; @@ -41,7 +42,7 @@ public function testRenderSequentialExecution() ->end() ->getWorkflow(); - $instance = new WorkflowInstance($workflow, []); + $instance = new WorkflowInstance(new Engine(), $workflow, []); $instance->execute(); $expectedOutput = implode(PHP_EOL, [ @@ -92,7 +93,7 @@ public function testRenderConditionalExecution() ->endAll() ->getWorkflow(); - $instance = new WorkflowInstance($workflow, []); + $instance = new WorkflowInstance(new Engine(), $workflow, []); $instance->execute(); $expectedOutput = implode(PHP_EOL, [ From 7d72d278e3ca7ba58ad835517bf7e05e6f942bb8 Mon Sep 17 00:00:00 2001 From: Iacovos Constantinou Date: Wed, 25 Jul 2018 15:39:21 +0300 Subject: [PATCH 2/3] Update documentation to use Engine class --- README.md | 4 ++-- docs/README.md | 3 ++- docs/conditional-flow.md | 5 +++-- docs/sequence-flow.md | 5 +++-- docs/workflow-engine.md | 6 ++++-- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index fda9fa7..9c01410 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ $builder Once the model bas been built, it can be executed by creating a new instance. At this point it is possible to pass some data that could be made available throughout the process. The data can be any object which could be also updated as part of the process. ``` php -$workflow = $builder->getWorkflow(); -$instance = new WorkflowInstance($workflow, $data); +$engine = new Engine(); +$instance = $engine->createInstance($builder->getWorkflow(), $input); $instance->execute(); ``` diff --git a/docs/README.md b/docs/README.md index 655bdbb..fd4c4d4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,7 +39,8 @@ During the execution, information is exchanged between each Workflow Node. In pa Here is a short example to get you started: ``` php -$instance = new WorkflowInstance($workflow, $input); +$engine = new Engine(); +$instance = $engine->createInstance($workflow, $input); $output = $instance->execute(); ``` diff --git a/docs/conditional-flow.md b/docs/conditional-flow.md index dbee400..ac2d0c2 100644 --- a/docs/conditional-flow.md +++ b/docs/conditional-flow.md @@ -4,7 +4,7 @@ The following example demonstrates how to apply conditions and branching in your ``` php require __DIR__.'/../vendor/autoload.php'; -$flow = (new \Phlow\Model\WorkflowBuilder()) +$workflow = (new \Phlow\Model\WorkflowBuilder()) ->start() ->choice() ->when('number < 100') @@ -18,7 +18,8 @@ $flow = (new \Phlow\Model\WorkflowBuilder()) ->endAll() ->getWorkflow(); -$instance = new \Phlow\Engine\WorkflowInstance($flow, ['number' => 99]); +$engine = new \Phlow\Engine\Engine(); +$instance = $engine->createInstance($workflow, ['number' => 99]); $instance->execute(); print $flow->render(new \Phlow\Renderer\PlainTextRenderer()); diff --git a/docs/sequence-flow.md b/docs/sequence-flow.md index 93d70b5..3e4b0a0 100644 --- a/docs/sequence-flow.md +++ b/docs/sequence-flow.md @@ -4,7 +4,7 @@ The following example demonstrates how to execute steps in sequence. As part of ``` php require __DIR__.'/../vendor/autoload.php'; -$flow = (new \Phlow\Model\WorkflowBuilder()) +$workflow = (new \Phlow\Model\WorkflowBuilder()) ->start() ->callback(function ($data) { $data['a'] = rand(1, 100); @@ -22,7 +22,8 @@ $flow = (new \Phlow\Model\WorkflowBuilder()) ->end() ->getWorkflow(); -$instance = new \Phlow\Engine\WorkflowInstance($flow, []); +$engine = new \Phlow\Engine\Engine(); +$instance = $engine->createInstance($workflow, []); $instance->execute(); print $flow->render(new \Phlow\Renderer\PlainTextRenderer()); diff --git a/docs/workflow-engine.md b/docs/workflow-engine.md index 36f0a2d..673a2be 100644 --- a/docs/workflow-engine.md +++ b/docs/workflow-engine.md @@ -7,14 +7,16 @@ During the execution, information is exchanged between each Workflow Node. In pa Here is a short example to get you started: ``` php -$instance = new WorkflowInstance($workflow, $input); +$engine = new Engine(); +$instance = $engine->createInstance($workflow, $input); $output = $instance->execute(); ``` It is also possible to advance the workflow for only one node. In this case, the execution will proceed to the next node and return the generated outbound message. ``` php -$instance = new WorkflowInstance($workflow, $input); +$engine = new Engine(); +$instance = $engine->createInstance($workflow, $input); $output = $instance->advance(); ``` From 66ba082baefd96b9398efda99abd1a869169ec9f Mon Sep 17 00:00:00 2001 From: Iacovos Constantinou Date: Wed, 25 Jul 2018 16:22:52 +0300 Subject: [PATCH 3/3] Move Node processing to Engine --- src/Engine/Engine.php | 31 ++++++++++++++---- src/Engine/WorkflowInstance.php | 45 ++++++++++++++------------- tests/Engine/EngineTest.php | 4 +-- tests/Engine/WorkflowInstanceTest.php | 17 +++++++--- 4 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php index b7b16e9..980d68d 100644 --- a/src/Engine/Engine.php +++ b/src/Engine/Engine.php @@ -21,6 +21,7 @@ use Phlow\Node\First; use Phlow\Node\Last; use Phlow\Node\Map; +use Psr\Log\NullLogger; class Engine implements LoggerAwareInterface { @@ -41,6 +42,7 @@ class Engine implements LoggerAwareInterface */ public function __construct() { + $this->logger = new NullLogger(); $this->processorRepository = new Repository(); // $this->processorsRepository->register(Node::class, , NextConnectionProcessor::class); @@ -95,20 +97,37 @@ public function get(string $id): Workflow /** * Creates and returns a new Instance for the given Workflow - * @param string $id + * @param Workflow $workflow * @param $input * @return WorkflowInstance */ - public function createInstance(string $id, $input): WorkflowInstance + public function createInstance($workflow, $input): WorkflowInstance { - $instance = new WorkflowInstance($this, $this->get($id), $input); - if ($this->logger !== null) { - $instance->setLogger($this->logger); - } + $instance = new WorkflowInstance($this, $workflow, $input); + $instance->setLogger($this->logger); return $instance; } + /** + * Executes the current node and moves the node pointer to the next node + * @param WorkflowInstance $instance + */ + public function processInstance(WorkflowInstance $instance): void + { + $node = $instance->current(); + $nodeClass = get_class($node); + $this->logger->info(sprintf('Workflow execution reached %s', $node)); + if ($this->getProcessorRepository()->has($nodeClass)) { + $processor = $this->getProcessorRepository()->getInstance($nodeClass); + $connection = $processor->process($node, $instance->getExchange()); + $instance->followConnection($connection); + $this->logger->info(sprintf('Workflow execution completed for %s', $node)); +// } else { +// throw new \Exception('Processor not found'); + } + } + /** * @return Repository */ diff --git a/src/Engine/WorkflowInstance.php b/src/Engine/WorkflowInstance.php index b08f607..6c10357 100644 --- a/src/Engine/WorkflowInstance.php +++ b/src/Engine/WorkflowInstance.php @@ -2,6 +2,7 @@ namespace Phlow\Engine; +use Phlow\Connection\Connection; use Phlow\Node\Error; use Phlow\Node\RecursiveIterator; use Phlow\Node\End; @@ -101,7 +102,7 @@ public function advance($howMany = 1) // Retrieve and execute the next node $this->initNodes(); try { - $this->handleCurrentNode(); + $this->engine->processInstance($this); } catch (\Exception $e) { $this->handleException($e); } @@ -129,25 +130,6 @@ private function prepareExchange() } } - /** - * Executes the current node and moves the node pointer to the next node - */ - private function handleCurrentNode(): void - { - $this->executionPath->add($this->current()); - - $nodeClass = get_class($this->current()); - $this->logger->info(sprintf('Workflow execution reached %s', $nodeClass)); - if ($this->engine->getProcessorRepository()->has($nodeClass)) { - $processor = $this->engine->getProcessorRepository()->getInstance($nodeClass); - $connection = $processor->process($this->current(), $this->exchange); - $this->executionPath->add($connection); - $this->nextNode = $connection->getTarget(); - - $this->logger->info(sprintf('Workflow execution completed for %s', $nodeClass)); - } - } - /** * Handles a raised exception by moving the flow to an error event * If no error handling was configured, another Exception will be thrown halting the execution @@ -166,7 +148,7 @@ private function handleException(\Exception $exception): void while (!empty($exceptionClass)) { if (array_key_exists($exceptionClass, $errorEvents)) { $this->currentNode = $errorEvents[$exceptionClass]; - $this->handleCurrentNode(); + $this->engine->processInstance($this); return; } @@ -197,6 +179,7 @@ private function initNodes(): void } $this->currentNode = $startEvents[0]; + $this->executionPath->add($this->currentNode); $this->nextNode = null; } @@ -290,4 +273,24 @@ public function getEngine(): Engine { return $this->engine; } + + public function followConnection(Connection $connection) + { + $this->executionPath->add($connection); + $this->moveTo($connection->getTarget()); + } + + public function moveTo(Node $node) + { + $this->executionPath->add($node); + $this->nextNode = $node; + } + + /** + * @return Exchange + */ + public function getExchange(): Exchange + { + return $this->exchange; + } } diff --git a/tests/Engine/EngineTest.php b/tests/Engine/EngineTest.php index e900430..29d64f4 100644 --- a/tests/Engine/EngineTest.php +++ b/tests/Engine/EngineTest.php @@ -11,11 +11,11 @@ class EngineTest extends TestCase public function testCreateInstance() { - $workflow = new Workflow('test'); + $workflow = new Workflow('id'); $engine = new Engine(); $engine->add($workflow); - $instance = $engine->createInstance('test', []); + $instance = $engine->createInstance($workflow, []); $this->assertEquals($workflow, $instance->getWorkflow()); } diff --git a/tests/Engine/WorkflowInstanceTest.php b/tests/Engine/WorkflowInstanceTest.php index 2fe3b9e..e6e7941 100644 --- a/tests/Engine/WorkflowInstanceTest.php +++ b/tests/Engine/WorkflowInstanceTest.php @@ -13,14 +13,25 @@ use Phlow\Connection\Connection; use Phlow\Tests\Engine\TestLogger; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; class WorkflowInstanceTest extends TestCase { + /** + * @var Engine + */ private $engine; + /** + * @var LoggerInterface + */ + private $logger; + public function setUp() { $this->engine = new Engine(); + $this->logger = new TestLogger(); + $this->engine->setLogger($this->logger); } public function testAdvance() @@ -159,9 +170,7 @@ public function testExecution() public function testLogger() { - $logger = new TestLogger(); $instance = $this->getPipeline(); - $instance->setLogger($logger); $instance->execute(); // 1. Workflow execution initiated @@ -169,7 +178,7 @@ public function testLogger() // 3/5/7. Start/Callback/Callback executed // 8. Workflow execution reached Phlow\Node\End // 9. Workflow execution completed - $this->assertEquals(9, count($logger->getAllRecords())); + $this->assertEquals(9, count($this->logger->getAllRecords())); } public function testExecutionPath() @@ -228,6 +237,6 @@ private function getPipeline() ->end(); $in = ['a' => null, 'b' => null, 'c' => null]; - return new WorkflowInstance($this->engine, $builder->getWorkflow(), $in); + return $this->engine->createInstance($builder->getWorkflow(), $in); } }