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(); ``` diff --git a/src/Engine/Engine.php b/src/Engine/Engine.php new file mode 100644 index 0000000..980d68d --- /dev/null +++ b/src/Engine/Engine.php @@ -0,0 +1,138 @@ +logger = new NullLogger(); + $this->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 Workflow $workflow + * @param $input + * @return WorkflowInstance + */ + public function createInstance($workflow, $input): WorkflowInstance + { + $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 + */ + public function getProcessorRepository(): Repository + { + return $this->processorRepository; + } +} diff --git a/src/Engine/WorkflowInstance.php b/src/Engine/WorkflowInstance.php index e034aca..6c10357 100644 --- a/src/Engine/WorkflowInstance.php +++ b/src/Engine/WorkflowInstance.php @@ -2,22 +2,11 @@ namespace Phlow\Engine; -use Phlow\Node\Callback; +use Phlow\Connection\Connection; 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 +49,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()); @@ -123,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); } @@ -151,29 +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 (array_key_exists($nodeClass, $this->processors)) { - $processorClass = $this->processors[$nodeClass]; - - /** @var Processor $processor */ - $processor = new $processorClass; - - $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 @@ -192,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; } @@ -223,6 +179,7 @@ private function initNodes(): void } $this->currentNode = $startEvents[0]; + $this->executionPath->add($this->currentNode); $this->nextNode = null; } @@ -308,4 +265,32 @@ function ($workflowObject) use ($executionPath) { ); return (string) $viewer->render($itr); } + + /** + * @return null|Engine + */ + 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/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..29d64f4 --- /dev/null +++ b/tests/Engine/EngineTest.php @@ -0,0 +1,39 @@ +add($workflow); + + $instance = $engine->createInstance($workflow, []); + $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..e6e7941 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; @@ -13,9 +13,27 @@ 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() { $workflow = $this->getPipeline(); @@ -40,7 +58,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 +66,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 +77,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 +110,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 +125,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 +147,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 +162,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()); @@ -152,9 +170,7 @@ public function testExecution() public function testLogger() { - $logger = new TestLogger(); $instance = $this->getPipeline(); - $instance->setLogger($logger); $instance->execute(); // 1. Workflow execution initiated @@ -162,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() @@ -171,7 +187,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 +203,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 +237,6 @@ private function getPipeline() ->end(); $in = ['a' => null, 'b' => null, 'c' => null]; - return new WorkflowInstance($builder->getWorkflow(), $in); + return $this->engine->createInstance($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, [