Zeta Workflow 13: Sniffer Integration
What’s left?
- When constructing the Zeta Components Wrapper, include the Sniffer.
- When the wrapper starts or resumes an execution, attach the Sniffer Plugin.
- Add each of the Sniffer’s current*() methods to the wrapper interface and implementation.
- Copy and modify the sub workflow test. Change the new (copied) test to use currentInterpretation() in the slide verification. This should tell us that everything is integrated and working correctly.
- Add docblock comments everywhere. “make phpdoc” will tell us of anything missing.
- Run all unit tests (make all).
- Add, commit, and push the code.
Having done the above except commit the code, I find that the integration test fails in a big way.
A bit of investigation turns up the problem. We are hitting our own exception for seeing a null execution id in the sub workflow. We’ll add that to the interpreter so it’s clear what happened.
In ExecutionStateRegistry, if the id is null, we now set the id (our internal registry index) to ‘null id’. This will break the unit test which checks for null exception.
The subWorkflowTest distinguishes between the main workflow execution’s slide, and the “current” or “active” sub workflow slide. Since we’re now using the interpreter, we only want the current slide. Adjust the new sniffer sub workflow test accordingly. Now the test passes.
Everything runs cleanly!
Categories: Zeta Components Workflow Tags:
Zeta Workflow 12: The Sniffer Class
Here are the Execution Sniffer features.
- When requested by the wrapper, and given the execution, attach our plugin to the execution. attachPlugin()
- Return the current Execution State Interpreter result. currentInterpretation()
- Return execution state of the current Execution State object. currentIsCancelled(), currentHasEnded(), currentIsSuspended(), currentGetVariables(), currentGetVariable(), currentHasVariable(), currentGetId().
We’ll have the Laravel Service Provider wire everything together.
Test Outcomes
Here is our list of tests.
- getPlugin() returns the plugin object which was wired up by the service provider.
- getInterpreter() returns the interpreter object which was wired up by the service provider.
- getRegistry() returns the registry object which was wired up by the service provider.
- attachPlugin($execution) calls $execution->addPlugin() with our plugin as wired up by the service provider.
- currentInterpretation() calls $registry->getCurrentExecutionState() and returns mock $state, and calls $interpreter->interpretExecutionState() with mock $state and returns ‘whatever’, and the currentInterpretation() call returns the same ‘whatever’.
- currentIsCancelled(), currentHasEnded(), currentIsSuspended(), currentGetVariables(), currentGetVariable(), currentHasVariable(), currentGetId() each call $registry->getCurrentExecutionState which returns mock $state; mock $state gets the relevant method called, and that method’s return value is the return value for the current*() retrieval methods.
Service Provider
The tricky part is getting everything wired up correctly in the Service Provider. Here is the first test of getPlugin(), plus various tests ensuring we have our mocks set up correctly.
namespace StarTribune\Workflow\Tests\Sniffer; use Mockery as m; class snifferTest extends \TestCase { public $execution; public $registry; public $fixture; public $interpreter; public $plugin; public function setUp() { parent::setUp(); $this->plugin = m::mock('\StarTribune\Workflow\Sniffer\Plugin'); $this->interpreter = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter'); $this->registry = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry'); $this->plugin->shouldReceive('getRegistry')->andReturn($this->registry); $this->execution = m::mock('\ezcWorkflowExecution'); \App::instance('workflow.snifferplugin', $this->plugin); \App::instance('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter', $this->interpreter); \App::instance('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry', $this->registry); $this->fixture = \App::make('workflow.sniffer'); } public function testMockClass() { $this->assertTrue($this->plugin instanceof \StarTribune\Workflow\Sniffer\Plugin); $this->assertTrue($this->interpreter instanceof \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter); $this->assertTrue($this->registry instanceof \StarTribune\Workflow\Sniffer\ExecutionStateRegistry); } public function testSamePlugin() { $actual = \App::make('workflow.snifferplugin'); $this->assertSame($this->plugin, $actual); } public function testSameInterpreter() { $actual = \App::make('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter'); $this->assertSame($this->interpreter, $actual); } public function testSameRegistry() { $actual = \App::make('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry'); $this->assertSame($this->registry, $actual); } public function testFixtureCorrectClass() { $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\Sniffer); } public function testgetPluginReturnsPlugin() { $actual = $this->fixture->getPlugin(); $this->assertSame($this->plugin, $actual); } }
Lines 12-16 set up our mock objects. Inside the service provider, I needed to ensure we pass around the same registry object. You’ll see how we do that in a moment. In short, what we do, is create the registry object, pass it in to the plugin object, and then ask the plugin object to give it back to us.
Lines 18-20 register our mocks with the Laravel Inversion of Control (IoC) container.
Line 22 creates the sniffer object as our test fixture. We have to do this last, because the service provider wires together the various objects, and we need to register them on lines 18-20 to ensure that our test fixture incorporates our mock objects.
The first test, lines 25-29, ensures we got our class names correct in the mock objects. This test won’t often fail unless you mess up the initial copy/paste stuff.
The next three tests (testSamePlugin, testSameInterpreter, testSameRegistry) ensure that we get the same mock object every time we ask for the object. I wrote these tests before touching the Laravel Service Provider. That way, as I mess with things in the service provider, I can be sure this aspect did not break.
Line 46 is my usual boilerplate test ensuring the test fixture is what I think it should be.
Finally, lines 50-53 will only pass once we have the Service Provider wiring everything up correctly.
Here is the Sniffer class:
namespace StarTribune\Workflow\Sniffer; class Sniffer { private $_plugin; private $_interpreter; private $_registry; public function __construct(\StarTribune\Workflow\Sniffer\Plugin $plugin, \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter $interpreter, \StarTribune\Workflow\Sniffer\ExecutionStateRegistry $registry) { $this->_plugin = $plugin; $this->_interpreter = $interpreter; $this->_registry = $registry; } public function getPlugin() { return $this->_plugin; } }
Here are the relevant parts of the service provider:
public function register() { $this->registerPurgeExpiredCommand(); $this->commands('workflow.purgeexpired'); $this->registerCleanVersionCommand(); $this->commands('workflow.cleanversion'); $this->registerDeleteByNameCommand(); $this->commands('workflow.deletebyname'); $this->bindZetaComponentsInterface(); $this->bindSnifferPlugin(); $this->bindSniffer(); } public function bindSnifferPlugin() { $this->app['workflow.snifferplugin'] = $this->app->share(function($app){ $registry = $this->app->make('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry'); $plugin = $this->app->make('\StarTribune\Workflow\Sniffer\Plugin', array($registry)); return $plugin; }); } public function bindSniffer() { $this->app['workflow.sniffer'] = $this->app->share(function($app){ $plugin = $this->app->make('workflow.snifferplugin'); $registry = $plugin->getRegistry(); $interpreter = $this->app->make('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter'); $implementation = new \StarTribune\Workflow\Sniffer\Sniffer($plugin, $interpreter, $registry); return $implementation; }); } public function provides() { return array('workflow.purgeexpired', 'workflow.cleanversion', 'workflow.deletebyname', 'workflow.snifferplugin', 'workflow.sniffer'); }
The Service Provider took some figuring out. I’ve not found clear documentation on exactly how to do this.
The Sniffer has three dependencies: plugin, interpreter, registry. However, we also pass registry as a dependency to plugin.
If you’re passing a dependency into the constructor, the way to do that is in the Service Provider.
That means we need to create the plugin in the service provider (it has a dependency to pass in), and we need to create the sniffer in the service provider (it has three dependencies to pass in). That means we have two separate closures, one which creates the plugin and one which creates the sniffer.
The tricky part is that both the plugin and the sniffer need to receive the SAME registry object.
The way I solved the problem is to create the plugin, injecting the registry, and then allow the plugin to disgorge that same registry object.
On lines 14-20 we create the registry and plugin. The plugin now contains a reference to the registry.
On lines 22-30 we create the sniffer.
Line 24 creates the plugin using the previous closure (lines 14-19). Line 15 registered the closure as ‘workflow.snifferplugin’, and now on line 24 we ask for ‘workflow.snifferplugin’.
Next, we take advantage of the fact that the plugin also created the registry. On line 25 we ask for that already-constructed object.
Line 26 instantiates the interpreter object.
Now, on line 27, we create the sniffer. We pass in the plugin, interpreter, and registry objects. We now know that both the plugin and the sniffer reference the SAME registry object, which was the whole point of the exercise.
Meanwhile, we have the already-seen unit tests which validate that everything took place as we just described.
With the tricky part done, the rest of our tests should be quite straightforward. I’ll work back and forth between test case and production code. Having done so, here is the test class followed by the production Sniffer class.
In developing the addPlugin test, I checked the Zeta Workflow source code and discovered something:
/** * Adds a plugin to this execution. * * @param ezcWorkflowExecutionPlugin $plugin * @return bool true when the plugin was added, false otherwise. */ public function addPlugin( ezcWorkflowExecutionPlugin $plugin ) { $pluginClass = get_class( $plugin ); if ( !isset( $this->plugins[$pluginClass] ) ) { $this->plugins[$pluginClass] = $plugin; return true; } else { return false; } }
We can add some test cases:
- Adding a plugin class the first time returns true.
- Adding the same plugin class a second time returns false.
The test class:
class snifferTest extends \TestCase { public $execution; public $registry; public $fixture; public $interpreter; public $plugin; public $state; public function setUp() { parent::setUp(); $this->plugin = m::mock('\StarTribune\Workflow\Sniffer\Plugin'); $this->interpreter = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter'); $this->registry = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry'); $this->plugin->shouldReceive('getRegistry')->andReturn($this->registry); $this->state = m::mock('\StarTribune\Workflow\Sniffer\ExecutionState'); $this->execution = m::mock('\ezcWorkflowExecution'); \App::instance('workflow.snifferplugin', $this->plugin); \App::instance('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter', $this->interpreter); \App::instance('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry', $this->registry); $this->fixture = \App::make('workflow.sniffer'); } public function testMockClass() { $this->assertTrue($this->plugin instanceof \StarTribune\Workflow\Sniffer\Plugin); $this->assertTrue($this->interpreter instanceof \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter); $this->assertTrue($this->registry instanceof \StarTribune\Workflow\Sniffer\ExecutionStateRegistry); } public function testSamePlugin() { $actual = \App::make('workflow.snifferplugin'); $this->assertSame($this->plugin, $actual); } public function testSameInterpreter() { $actual = \App::make('\StarTribune\Workflow\Sniffer\ExecutionStateInterpreter'); $this->assertSame($this->interpreter, $actual); } public function testSameRegistry() { $actual = \App::make('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry'); $this->assertSame($this->registry, $actual); } public function testFixtureCorrectClass() { $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\Sniffer); } public function testgetPluginReturnsPlugin() { $actual = $this->fixture->getPlugin(); $this->assertSame($this->plugin, $actual); } public function testgetInterpreterReturnsInterpreter() { $actual = $this->fixture->getInterpreter(); $this->assertSame($this->interpreter, $actual); } public function testgetRegistryReturnsRegistry() { $actual = $this->fixture->getRegistry(); $this->assertSame($this->registry, $actual); } public function testattachPlugin() { $this->execution->shouldReceive('addPlugin') ->once() ->with($this->plugin) ->andReturn(true); $result = $this->fixture->attachPlugin($this->execution); $this->assertTrue(true === $result); } public function testreattachPlugin() { $this->execution->shouldReceive('addPlugin') ->times(2) ->with($this->plugin) ->andReturn(true, false); $result1 = $this->fixture->attachPlugin($this->execution); $this->assertTrue(true === $result1); $result2 = $this->fixture->attachPlugin($this->execution); $this->assertTrue(false === $result2); } public function testcurrentInterpretation() { $expected = 'whatever'; $this->registry->shouldReceive('getCurrentExecutionState') ->andReturn($this->state); $this->interpreter->shouldReceive('interpretExecutionState') ->with($this->state) ->andReturn($expected); $actual = $this->fixture->currentInterpretation(); $this->assertEquals($expected, $actual); } /** * @dataProvider dataCurrent */ public function testcurrentIsCancelled($function1, $function2) { $expected = 'return_'.$function1; $this->registry->shouldReceive('getCurrentExecutionState') ->andReturn($this->state); $this->state->shouldReceive($function1) ->once() ->andReturn($expected); $actual = $this->fixture->$function2(); $this->assertEquals($expected, $actual); } public function dataCurrent() { $data = array(); $data[] = array('isCancelled', 'currentIsCancelled'); $data[] = array('hasEnded', 'currentHasEnded'); $data[] = array('isSuspended', 'currentIsSuspended'); $data[] = array('getId', 'currentGetId'); $data[] = array('getVariables', 'currentGetVariables'); return $data; } public function testGetVariable() { $expected = 'theValue'; $key = 'theName'; $this->registry->shouldReceive('getCurrentExecutionState') ->andReturn($this->state); $this->state->shouldReceive('getVariable') ->with($key) ->once() ->andReturn($expected); $actual = $this->fixture->currentGetVariable($key); $this->assertEquals($expected, $actual); } public function testHasVariable() { $expected = false; $key = 'theName'; $this->registry->shouldReceive('getCurrentExecutionState') ->andReturn($this->state); $this->state->shouldReceive('hasVariable') ->with($key) ->once() ->andReturn($expected); $actual = $this->fixture->currentHasVariable($key); $this->assertTrue($expected === $actual); } }
The production Sniffer class:
namespace StarTribune\Workflow\Sniffer; class Sniffer { private $_plugin; private $_interpreter; private $_registry; public function __construct(\StarTribune\Workflow\Sniffer\Plugin $plugin, \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter $interpreter, \StarTribune\Workflow\Sniffer\ExecutionStateRegistry $registry) { $this->_plugin = $plugin; $this->_interpreter = $interpreter; $this->_registry = $registry; } public function getPlugin() { return $this->_plugin; } public function getInterpreter() { return $this->_interpreter; } public function getRegistry() { return $this->_registry; } public function attachPlugin(\ezcWorkflowExecution $execution) { return $execution->addPlugin($this->getPlugin()); } public function currentInterpretation() { $state = $this->getRegistry()->getCurrentExecutionState(); return $this->getInterpreter()->interpretExecutionState($state); } public function currentIsCancelled() { return $this->getCurrent('isCancelled'); } public function currentHasEnded() { return $this->getCurrent('hasEnded'); } public function currentIsSuspended() { return $this->getCurrent('isSuspended'); } public function currentGetId() { return $this->getCurrent('getId'); } public function currentGetVariables() { return $this->getCurrent('getVariables'); } public function currentGetVariable($key) { return $this->getCurrentKey('getVariable', $key); } public function currentHasVariable($key) { return $this->getCurrentKey('hasVariable', $key); } protected function getCurrent($function) { $state = $this->getRegistry()->getCurrentExecutionState(); return $state->$function(); } protected function getCurrentKey($function, $key) { $state = $this->getRegistry()->getCurrentExecutionState(); return $state->$function($key); } }
Next up: Sniffer Integration
Categories: Zeta Components Workflow Tags:
Zeta Workflow 11: Sniffer Plugin
The available methods are documented here: http://ezcomponents.org/docs/api/latest/Workflow/ezcWorkflowExecutionPlugin.html. For now let’s keep this simple and only implement afterNodeExecuted().
When called, we want to do the following.
- Notify the Execution State Registry that this execution is, as of this moment, the current execution.
- Obtain the relevant Execution State object from the Execution State Registry.
- Have the relevant Execution State object capture and retain this execution’s current execution state.
Our unit test, therefore, can be frightfully simple. We can pass in mocks of the execution and node, and condition mocks of the Execution State Registry and Execution State object to ensure they are called as intended.
To be sure, we may have some frightfully complex scenarios to explore, such as synchronized parallel executions with paused sub workflows. But we’ll get to those later!
Test Outcomes
Having now built and tested the Execution State Registry, it’s clear that we only need to call the registry’s setCurrentExecution() method. This means we’ll need to pass the registry object in to the plugin’s constructor.
A check of the Zeta Workflow source code http://ezcomponents.org/docs/api/latest/__filesource/fsource_Workflow—Workflow—1.4—src—interfaces—execution_plugin.php.html shows that we’ll be extending the abstract base class, and that it does not define a constructor. So we’re safe defining our constructor to include the registry object.
For our only test, we will condition a mock registry object to require that its setCurrentExecution() method is called with our mock execution object.
Here is the test.
namespace StarTribune\Workflow\Tests\Sniffer; use Mockery as m; class snifferPluginTest extends \TestCase { public $execution; public $registry; public $fixture; public $node; public function setUp() { parent::setUp(); $this->registry = m::mock('\StarTribune\Workflow\Sniffer\ExecutionStateRegistry'); $this->execution = m::mock('\ezcWorkflowExecution'); $this->node = m::mock('\ezcWorkflowNode'); $this->fixture = new \StarTribune\Workflow\Sniffer\Plugin($this->registry); } public function testMockCorrectClass() { $this->assertTrue($this->registry instanceof \StarTribune\Workflow\Sniffer\ExecutionStateRegistry); $this->assertTrue($this->execution instanceof \ezcWorkflowExecution); $this->assertTrue($this->node instanceof \ezcWorkflowNode); } public function testFixtureCorrectClass() { $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\Plugin); } public function testSetCurrentExecution() { $this->registry->shouldReceive('setCurrentExecution') ->once() ->with($this->execution); $this->fixture->afterNodeExecuted($this->execution, $this->node); } }
Here is the production code. As the starting point, I copied the method docblock and signature from the abstract parent class.
namespace StarTribune\Workflow\Sniffer; class Plugin extends \ezcWorkflowExecutionPlugin { protected $registry; public function __construct(\StarTribune\Workflow\Sniffer\ExecutionStateRegistry $registry) { $this->registry = $registry; } /** * Called after a node has been executed. * * @param ezcWorkflowExecution $execution * @param ezcWorkflowNode $node */ public function afterNodeExecuted( \ezcWorkflowExecution $execution, \ezcWorkflowNode $node ) { $ignore = $this->registry->setCurrentExecution($execution); } }
Once again we have a small class with a single responsibility.
Next up: The Sniffer Class.
Categories: Zeta Components Workflow Tags:
Zeta Workflow 10: Execution State Interpreter
The interpreter returns a text string. Here is the logic:
- If the ‘cancel’ variable exists, return “cancel”.
- If the ‘swap’ variable exists, it names the new workflow. Return “swap “ followed by the new workflow name. See the below discussion.
- If the ‘slide’ variable exists, return “slide “ followed by the slide variable’s value.
- Otherwise, return an empty string.
The above steps are in priority order.
The class has only a single method, interpretExecutionState(ExecutionState $state). The object does not need to cache anything; it simply interprets the passed-in execution state object, returning a string as the interpretation.
Given that our unit tests already interpret the slide variable, we can adapt one of those tests to exercise the interpreter. Once interpreter development is complete (with unit tests), make a copy of the subWorkflowTest.php. Change the copy’s verifySlide method to use the interpreter. All sub workflow tests should continue to pass while using the Execution State Interpreter.
Oops, I got ahead of myself. We’ll adapt a copy of the subWorkflowTest once the entire sniffer is complete and is getting attached to the workflow.
Test Outcomes
Given the above simplistic interpretation rules, we have eight possible outcomes depending on whether the cancel, swap, slide variables do or do not exist. We can set up a single test with a data provider driving the eight scenarios.
In the data provider, we’ll use null to indicate the variable does not exist, and its value otherwise. We can condition a mock ExecutionState object.
After the several iterations fleshing out the tests and production code, here is the result.
The test:
namespace StarTribune\Workflow\Tests\Sniffer; use Mockery as m; class executionStateInterpreterTest extends \TestCase { public $state; public $fixture; public function setUp() { parent::setUp(); $this->state = m::mock('\StarTribune\Workflow\Sniffer\ExecutionState'); $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter; } public function conditionState($cancel, $swap, $slide) { $this->conditionStateVariable('cancel', $cancel); $this->conditionStateVariable('swap', $swap); $this->conditionStateVariable('slide', $slide); } public function conditionStateVariable($key, $value) { if(null === $value) { $this->state->shouldReceive('hasVariable') ->with($key) ->andReturn(false); } else { $this->state->shouldReceive('hasVariable') ->with($key) ->andReturn(true); $this->state->shouldReceive('getVariable') ->with($key) ->andReturn($value); } } public function testMockCorrectClass() { $this->assertTrue($this->state instanceof \StarTribune\Workflow\Sniffer\ExecutionState); } public function testFixtureCorrectClass() { $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\ExecutionStateInterpreter); } /** * @dataProvider dataInterpretation */ public function testInterpretation($cancel, $swap, $slide, $result) { $this->conditionState($cancel, $swap, $slide); $actual = $this->fixture->interpretExecutionState($this->state); $this->assertSame($result, $actual); } public function dataInterpretation() { $data = array(); $data[] = array(null, null, null, ''); $data[] = array(1, null, null, 'cancel'); $data[] = array(0, null, null, 'cancel'); $data[] = array('', null, null, 'cancel'); $data[] = array('cancel', null, null, 'cancel'); $data[] = array(1, 1, null, 'cancel'); $data[] = array(1, null, 1, 'cancel'); $data[] = array(1, 1, 1, 'cancel'); $data[] = array(null, 1, null, 'swap 1'); $data[] = array(null, 0, null, 'swap 0'); $data[] = array(null, '', null, 'swap '); $data[] = array(null, 'swap', null, 'swap swap'); $data[] = array(null, 1, 1234, 'swap 1'); $data[] = array(null, 0, 1234, 'swap 0'); $data[] = array(null, '', 1234, 'swap '); $data[] = array(null, 'swap', 1234, 'swap swap'); $data[] = array(null, null, 1, 'slide 1'); $data[] = array(null, null, 0, 'slide 0'); $data[] = array(null, null, '', 'slide '); $data[] = array(null, null, 'slide', 'slide slide'); return $data; } }
The production class:
namespace StarTribune\Workflow\Sniffer; use \StarTribune\Workflow\Sniffer\ExecutionState; class ExecutionStateInterpreter { public function interpretExecutionState(\StarTribune\Workflow\Sniffer\ExecutionState $state) { $result = ''; if($state->hasVariable('cancel')) { $result = 'cancel'; } elseif($state->hasVariable('swap')) { $result = 'swap '.$state->getVariable('swap'); } elseif($state->hasVariable('slide')) { $result = 'slide '.$state->getVariable('slide'); } return $result; } }
Next up: The Sniffer Plugin
Categories: Zeta Components Workflow Tags:
Zeta Workflow 09: Execution State Registry
Since we potentially have multiple executions, we need a registry to manage the multiple Execution State objects. The registry also maintains the notion of which is the “current” Execution State object. The features are:
- Accept notification that “this” is the current execution.
- For a given execution, return the relevant Execution State object. We either fetch it from our cache, or construct it on the spot. I am assuming that, by this point, each execution has an id. If not, initially, throw an exception. Later, we should probably ignore executions with no id, assuming that later in the workflow execution it WILL have an id, and we can snag it at that point.
- Return the “current” Execution State object.
Test Outcomes
For the following tests, $execution is a Zeta Workflow execution object and $executionState is an Execution State object. We consider uniqueness according to $execution->getId() and the corresponding $executionState->getId().
- getExecutionState($execution) returns an initialized Execution State object. The object is initialized via captureExecution().
- A second call to getExecutionState() with the same $execution returns the same initialized Execution State object.
- A second call to getExecutionState() with a different $execution returns a different initialized Execution State object.
- Successive calls to getExecutionState() each call captureExecution on the current execution object.
- If $execution->getId() returns null, we throw an exception.
- setCurrentExecution($execution) marks the current execution as the current one, and calls getExecutionState() to update the current execution state. The method returns the getExecutionState() result. A later call to getCurrentExecutionState() returns the same Execution State object regardless of intervening getExecutionState() calls.
For the test fixture, we’ll use mock execution objects and real Execution State objects. We’ll begin with a brute force setup of each test case, and refactor/simplify as we see the opportunity.
Here comes the first test.
namespace StarTribune\Workflow\Tests\Sniffer; use Mockery as m; class executionStateRegistryTest extends \TestCase { public $fixture; public $execution; public $state; public $expectedVariables = array('one' => 1, 'two' => 2); public $expectedId = 1234; public function setUp() { parent::setUp(); $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionStateRegistry; $this->execution = m::mock('\ezcWorkflowExecution'); } public function conditionExecution() { $this->execution->shouldReceive('isCancelled') ->once() ->andReturn('cancel'); $this->execution->shouldReceive('hasEnded') ->once() ->andReturn('ended'); $this->execution->shouldReceive('isSuspended') ->once() ->andReturn('suspended'); $this->execution->shouldReceive('getVariables') ->once() ->andReturn($this->expectedVariables); $this->execution->shouldReceive('getId') ->once() ->andReturn($this->expectedId); } public function testFixtureCorrectClass() { $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\ExecutionStateRegistry); } public function testMockCorrectClass() { $this->assertTrue($this->execution instanceof \ezcWorkflowExecution); } public function testGetExecutionStateReturnsExecutionState() { $this->conditionExecution(); $actual = $this->fixture->getExecutionState($this->execution); $this->assertTrue($actual instanceof \StarTribune\Workflow\Sniffer\ExecutionState); } }
The production code.
namespace StarTribune\Workflow\Sniffer; use ExecutionState; class ExecutionStateRegistry { public function getExecutionState(\ezcWorkflowExecution $execution) { $state = new \StarTribune\Workflow\Sniffer\ExecutionState; $state->captureExecution($execution); return $state; } }
A second call to getExecutionState() with the same $execution returns the same initialized Execution State object.
public function testSecondGetExecutionStateReturnsSame() { $this->conditionExecution(); $first = $this->fixture->getExecutionState($this->execution); $second = $this->fixture->getExecutionState($this->execution); $this->assertSame($first, $second); $this->assertEquals($first->getId(), $second->getId()); }
The production code.
class ExecutionStateRegistryException extends \Exception { } class ExecutionStateRegistry { private $_registry; public function __construct() { $this->_registry = array(); } public function getExecutionState(\ezcWorkflowExecution $execution) { $state = new \StarTribune\Workflow\Sniffer\ExecutionState; $state->captureExecution($execution); $id = $state->getId(); $object = $this->getRegisteredObject($id); if($object) { $object->captureExecution($execution); return $object; } $this->registerObject($id, $state); return $state; } private function getRegisteredObject($id) { return array_key_exists($id, $this->_registry) ? $this->_registry[$id] : null; } private function registerObject($id, \StarTribune\Workflow\Sniffer\ExecutionState $object) { if(array_key_exists($id, $this->_registry)) { throw new ExecutionStateRegistryException("Duplicate registry $id"); } $this->_registry[$id] = $object; } }
A second call to getExecutionState() with a different $execution returns a different initialized Execution State object. Here is the test; no production changes needed.
public function testDifferentExecutionReturnsDifferent() { $this->conditionExecution(); $execution1 = $this->execution; ++$this->expectedId; $this->execution = m::mock('\ezcWorkflowExecution'); $this->conditionExecution(); $execution2 = $this->execution; $this->assertNotEquals($execution1->getId(), $execution2->getId(), "guard clause"); $first = $this->fixture->getExecutionState($execution1); $second = $this->fixture->getExecutionState($execution2); $this->assertNotSame($first, $second); $this->assertNotEquals($first->getId(), $second->getId()); }
Successive calls to getExecutionState() each call captureExecution on the current execution object: We already have that covered.
If $execution->getId() returns null, we throw an exception. The test:
/** * @expectedException \StarTribune\Workflow\Sniffer\ExecutionStateRegistryException */ public function testNullIdException() { $this->expectedId = null; $this->conditionExecution(); $actual = $this->fixture->getExecutionState($this->execution); $this->fail("expected exception"); }
Production code:
public function getExecutionState(\ezcWorkflowExecution $execution) { $state = new \StarTribune\Workflow\Sniffer\ExecutionState; $state->captureExecution($execution); $id = $state->getId(); if(null === $id) { throw new ExecutionStateRegistryException("Null execution id"); } $object = $this->getRegisteredObject($id); if($object) { $object->captureExecution($execution); return $object; } $this->registerObject($id, $state); return $state; }
setCurrentExecution($execution) marks the current execution as the current one, and calls getExecutionState() to update the current execution state. The method returns the getExecutionState() result. A later call to getCurrentExecutionState() returns the same Execution State object regardless of intervening getExecutionState() calls.
We’ll use the “different execution returns different” set up and run the whole thing as a single scenario.
public function testSetCurrentExecutionScenario() { $this->conditionExecution(); $execution1 = $this->execution; ++$this->expectedId; $this->execution = m::mock('\ezcWorkflowExecution'); $this->conditionExecution(); $execution2 = $this->execution; $this->assertNotEquals($execution1->getId(), $execution2->getId(), "guard clause"); $state1 = $this->fixture->setCurrentExecution($execution1); $state2 = $this->fixture->getExecutionState($execution2); $state3 = $this->fixture->getCurrentExecutionState(); $this->assertSame($state1, $state3); $this->assertNotSame($state1, $state2); }
Here is the production code.
private $_currentId; public function setCurrentExecution(\ezcWorkflowExecution $execution) { $state = $this->getExecutionState($execution); $this->_currentId = $state->getId(); return $state; } public function getCurrentExecutionState() { return $this->getRegisteredObject($this->_currentId); }
Once again, we have a small, tight, single-purpose class.
Next up: The Execution State Interpreter.
Categories: Zeta Components Workflow Tags:
Zeta Workflow 08: Execution State Object
The execution state object retains the execution state as defined by isCancelled(), hasEnded(), and isSuspended(); and the execution variables as obtained via getVariables().
The functionality is:
- Capture execution state, given the execution as passed to the plugin.
- Disgorge the last-captured execution state. Mirror the execution methods isCancelled(), hasEnded(), isSuspended, getVariables(), getVariable(), hasVariable(), getId().
Our unit tests will explore the API of each method, followed by verifying the functionality in each case.
Starting Point
We begin with a list of unit test outcomes. I’m making this list off the top of my head based on the above description. We may add or change tests as we go:
- Can create a mock Zeta Workflow Execution object.
- The Zeta Workflow Execution object is an instance of the correct class, ezcWorkflowExecution.
- Can capture isCancelled().
- Can capture hasEnded().
- Can capture isSuspended().
- Can capture getVariables(). getVariable() and hasVariable are based on getVariables() content, so there is no need for separate capture mechanisms.
- Can capture getId().
- A new capture completely replaces any previous capture.
- captureExecution($execution) captures the above items 3-7.
- If any of the retrieval functions is called before the execution was captured, the retrieval function returns null.
- The retrieval functions return their respective captured isCancelled(), hasEnded(), isSuspended(), getVariables(), getId().
- getVariables(), when there are no variables, returns empty array.
- getVariable(), if the variable does not exist in the current capture, returns null.
- getVariable(), if the variable does exist in the current capture, returns the variable’s value.
- hasVariable() returns true or false for whether or not that variable exists in the capture.
We start coding by:
- Create code directory Sniffer and test directory tests/sniffer.
- Define a new test suite in the phpunit.xml.
- Define the test suite in the Makefile.
- Set up this test suite as the default target in the Makefile.
- Create a new test sniffer/executionStateTest.php which extends TestCase. We do not need to extend the database test case.
- Create a failing test with $this->fail(‘here’). Run the Makefile and observe the test failure.
- Proceed with the first of the above test outcomes.
Here is the initial unit test, set to fail:
<?php /** * Execution State Object * @package StarTribune/Workflow * * @author ewb (3/23/2014) */ namespace StarTribune\Workflow\Tests\Sniffer; use Mockery as m; /** * Execution State Object * @package StarTribune/Workflow */ class executionStateTest extends \TestCase { public function testHere() { $this->fail("here we are"); } }
With Makefile and phpunit.xml correctly set up, we see the failure:
vendor/bin/phpunit --testsuite single PHPUnit 4.0.12 by Sebastian Bergmann. Configuration read from ***/phpunit.xml F Time: 66 ms, Memory: 8.00Mb There was 1 failure: 1) StarTribune\Workflow\Tests\Sniffer\executionStateTest::testHere here we are ***/workbench/star-tribune/workflow/tests/sniffer/executionStateTest.php:17 FAILURES! Tests: 1, Assertions: 0, Failures: 1. make: *** [single] Error 1
Now that we know we are running the tests we think we are, we can proceed.
Mock Object
We start with a couple of tests ensuring we get our mock objects set up correctly.
In each case, we start by observing the test failing, then watch it pass. With the first test, we comment-out the assert to ensure that the expectation fails for not being met. In the second test, we just let it ride; it’s trivial. But it’s important to have the test because it “nails down” the execution class.
public function testMockSetup() { $this->execution->shouldReceive('myMethod') ->once() ->andReturn('foo'); $this->assertEquals('foo', $this->execution->myMethod()); } public function testMockCorrectClass() { $this->assertTrue($this->execution instanceof \ezcWorkflowExecution); }
The Capture Methods
We write the tests first. In this case we only confirm that the (not yet existent) functions are callable. When we get to the retrieval functions, we’ll flesh out the actual capture code.
The first test requires that we create the actual class.
public $execution; public $fixture; public function setUp() { $this->execution = m::mock('\ezcWorkflowExecution'); $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionState; } public function testFixtureCorrectClass() { $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\ExecutionState); }
Once the test passes, we refactor the construction to setUp() as shown above. Here is our class.
<?php /** * Track Zeta Workflow execution state. * @package StarTribune/Workflow */ namespace StarTribune\Workflow\Sniffer; /** * Track Zeta Workflow execution state. * * @package StarTribune/Workflow * * @author ewb (3/29/2014) */ class ExecutionState { }
Now that the class exists, we write just enough code to make our capture tests pass.
The first pair of capture tests looks like this:
public function testCaptureIsCancelledAPI() { $this->execution->shouldReceive('isCancelled')->once(); $return = $this->fixture->captureIsCancelled($this->execution); $this->assertTrue(null === $return, "Method should return null"); } public function testCaptureIsCancelledCalls() { $this->execution->shouldReceive('isCancelled')->once(); $return = $this->fixture->captureIsCancelled($this->execution); }
Now that we’ve written it, we can see the first of the two tests is redundant. Any purpose served by the first test is served by the second.
Here is the class as written to pass the above tests.
class ExecutionState { public $isCancelled; public function captureIsCancelled(\ezcWorkflowExecution $execution) { $this->isCancelled = $execution->isCancelled(); } }
The rest of the captures and production code will be similar. Here are the tests.
public function testCaptureIsCancelledCalls() { $this->execution->shouldReceive('isCancelled')->once(); $return = $this->fixture->captureIsCancelled($this->execution); } public function testCaptureHasEnded() { $this->execution->shouldReceive('hasEnded')->once(); $return = $this->fixture->captureHasEnded($this->execution); } public function testCaptureIsSuspended() { $this->execution->shouldReceive('isSuspended')->once(); $return = $this->fixture->captureIsSuspended($this->execution); } public function testCaptureGetVariables() { $this->execution->shouldReceive('getVariables')->once(); $return = $this->fixture->captureGetVariables($this->execution); } public function testCaptureGetId() { $this->execution->shouldReceive('getId')->once(); $return = $this->fixture->captureGetId($this->execution); } public function testCaptureExecution() { $this->execution->shouldReceive('isCancelled')->once(); $this->execution->shouldReceive('hasEnded')->once(); $this->execution->shouldReceive('isSuspended')->once(); $this->execution->shouldReceive('getVariables')->once(); $this->execution->shouldReceive('getId')->once(); $return = $this->fixture->captureExecution($this->execution); }
Here is the production code.
class ExecutionState { public $isCancelled; public $hasEnded; public $isSuspended; public $variables; public $id; public function captureIsCancelled(\ezcWorkflowExecution $execution) { $this->isCancelled = $execution->isCancelled(); } public function captureHasEnded(\ezcWorkflowExecution $execution) { $this->hasEnded = $execution->hasEnded(); } public function captureIsSuspended(\ezcWorkflowExecution $execution) { $this->isSuspended = $execution->isSuspended(); } public function captureGetVariables(\ezcWorkflowExecution $execution) { $this->variables = $execution->getVariables(); } public function captureGetId(\ezcWorkflowExecution $execution) { $this->id = $execution->getId(); } public function captureExecution(\ezcWorkflowExecution $execution) { $this->captureIsCancelled($execution); $this->captureHasEnded($execution); $this->captureIsSuspended($execution); $this->captureGetVariables($execution); $this->captureGetId($execution); } }
Retrieval Functions
We are skipping the rule “a new capture completely replaces the old capture”. It will be clear from inspecting the production code that this is the case, and I don’t think the test is sufficiently worth the time to write it.
Let’s test that any retrieval prior to the first capture returns null.
public function testIsCancelledNull() { $actual = $this->fixture->isCancelled(); $this->assertTrue(null === $actual); } public function testHasEndedNull() { $actual = $this->fixture->hasEnded(); $this->assertTrue(null === $actual); } public function testIsSuspendedNull() { $actual = $this->fixture->isSuspended(); $this->assertTrue(null === $actual); } public function testGetVariablesNull() { $actual = $this->fixture->getVariables(); $this->assertTrue(null === $actual); } public function testGetIdNull() { $actual = $this->fixture->getId(); $this->assertTrue(null === $actual); }
I changed the captured properties from public to private. Here is the new production code.
class ExecutionState { private $_isCancelled; private $_hasEnded; private $_isSuspended; private $_variables; private $_id; public function captureIsCancelled(\ezcWorkflowExecution $execution) { $this->_isCancelled = $execution->isCancelled(); } public function captureHasEnded(\ezcWorkflowExecution $execution) { $this->_hasEnded = $execution->hasEnded(); } public function captureIsSuspended(\ezcWorkflowExecution $execution) { $this->_isSuspended = $execution->isSuspended(); } public function captureGetVariables(\ezcWorkflowExecution $execution) { $this->_variables = $execution->getVariables(); } public function captureGetId(\ezcWorkflowExecution $execution) { $this->_id = $execution->getId(); } public function captureExecution(\ezcWorkflowExecution $execution) { $this->captureIsCancelled($execution); $this->captureHasEnded($execution); $this->captureIsSuspended($execution); $this->captureGetVariables($execution); $this->captureGetId($execution); } public function isCancelled() { return $this->_isCancelled; } public function hasEnded() { return $this->_hasEnded; } public function isSuspended() { return $this->_isSuspended; } public function getVariables() { return $this->_variables; } public function getId() { return $this->_id; } }
To test that the retrieval really does retrieve the right information, we should go to a different test fixture setup. This means starting a new test class. Here is tests/sniffer/executionStateRetrievalTest.php, with the retrieval functions developed.
class executionStateRetrievalTest extends \TestCase { public $execution; public $fixture; public $expectedVariables = array('one' => 1, 'two' => 2); public $expectedId = 1234; public function setUp() { $this->execution = m::mock('\ezcWorkflowExecution'); $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionState; $this->execution->shouldReceive('isCancelled') ->once() ->andReturn('cancel'); $this->execution->shouldReceive('hasEnded') ->once() ->andReturn('ended'); $this->execution->shouldReceive('isSuspended') ->once() ->andReturn('suspended'); $this->execution->shouldReceive('getVariables') ->once() ->andReturn($this->expectedVariables); $this->execution->shouldReceive('getId') ->once() ->andReturn($this->expectedId); $this->fixture->captureExecution($this->execution); } public function testIsCancelled() { $actual = $this->fixture->isCancelled(); $this->assertEquals('cancel', $actual); } public function testHasEnded() { $actual = $this->fixture->hasEnded(); $this->assertEquals('ended', $actual); } public function testIsSuspended() { $actual = $this->fixture->isSuspended(); $this->assertEquals('suspended', $actual); } public function testGetVariables() { $actual = $this->fixture->getVariables(); $this->assertEquals($this->expectedVariables, $actual); } public function testGetId() { $actual = $this->fixture->getId(); $this->assertEquals($this->expectedId, $actual); } }
In setUp(), we condition the mock object to return the values to be captured. Then we call the production captureExecution().
Then, each of the tests calls the appropriate production retrieval function, and we compare to the expected value. In this case all tests pass without changing any production code. Let’s finish out our test cases, writing production code as needed.
getVariables, when there are no variables, returns an empty array. We’ll skip this test because we know Zeta Workflow returns an array and we pass it through.
getVariable() returns the variable value or null if no such variable.
public function testGetVariableMissingReturnsNull() { $actual = $this->fixture->getVariable('bogus'); $this->assertTrue(null === $actual); } public function testGetVariableFirst() { $actual = $this->fixture->getVariable('one'); $this->assertTrue(1 === $actual); } public function testGetVariableLast() { $actual = $this->fixture->getVariable('two'); $this->assertTrue(2 === $actual); }
The production code:
public function getVariable($key) { return (is_array($this->_variables) && array_key_exists($key, $this->_variables)) ? $this->_variables[$key] : null; }
hasVariable() returns true or false.
public function hasVariable($key) { return (is_array($this->_variables) && array_key_exists($key, $this->_variables)) ? true : false; }
We have covered all our tests cases. We have a nice, tight, Execution State object with a clear single responsibility. Once we add the docblock comments, we’re done.
Next up: Execution State Registry.
Categories: Zeta Components Workflow Tags:
Zeta Workflow 07: Execution Sniffer Plugin
Having now written the narrative documentation, it looks to me like it would be useful to have a plugin which captures the execution state after each node executes. I’m writing this walk through as I design and test this plugin.
Remember: I’m making this up as I go!
Features
I have these preliminary goals:
- Understand the execution state of the current (most recently running) execution
- Provide access to the current execution’s variables
- Provide a list of all executions currently active. This goal is optional, being more an understanding/debugging aid than a production need
I picture the sniffer as being the passive go-between connecting the current Zeta Workflow thread to the Laravel wrapper (the Zeta Components Interface). The wrapper, in theory, only has access to the main thread. We could have any number of subflows, parallel executions, and so on. The wrapper has no way to sort this out short of diving in through the Zeta Workflow internals.
Take, for example, the case where a node decides to switch to another workflow. There is no such mechanism within Zeta Workflow. However, the node could return false, which suspends the workflow, and set an execution variable for the Sniffer to examine.
The Sniffer, in turn, could do something dramatic such as throw an exception. But since the workflow is already suspended, I think the passive approach is sufficient.
In this case the Laravel code which is running the workflow can query the Sniffer, with the Sniffer responding with a message to the effect of “Switch to workflow X.”
I also picture the Sniffer as being hidden entirely within the wrapper. The wrapper (or the Laravel Service Provider) instantiates the Sniffer, attaches it to the workflow execution, and handles any queries of the Sniffer.
We’ll need various tests ensuring that this mechanism actually works, for the main workflow, sub workflows, and parallel executions.
Tracing
As an added bonus, this might be a good mechanism for implementing a workflow event trace. However, an execution visualizer plugin already exists, documented at http://ezcomponents.org/docs/api/latest/Workflow/ezcWorkflowExecutionVisualizerPlugin.html.
That being the case, I’ll set aside the tracing idea for now. If I were to create a tracing mechanism, it looks like creating a separate plugin for that purpose is the way to go. That keeps the plugin in line with the Single Responsiblity Principle, and we only need to attach it to a workflow when tracing is needed.
Single Responsibility
The workflow can run a list of any number of plugins. Each plugin can attach itself to any number of events, or focus on a single specific event. For example, a plugin can be activated when the workflow suspends and when the workflow ends, but ignore all other events.
That being the case, it makes sense, when implementing plugins, to make a number of small plugins with a single clear responsibility or purpose, rather than a larger plugin with multiple responsibilities.
If we come up with a bunch of plugins, we might even want to create a plugin that attaches the plugins!
Sniffer Components
As a first cut, we can separate the Sniffer into the following components. Each component has a single well defined responsibility.
- The plugin itself extends ezcWorkflowExecutionPlugin. The plugin communicates with the execution.
- We maintain execution state with an Execution State object. Given the Zeta Workflow execution, the Execution State object captures and retains that execution’s current execution state. Our Sniffer will have multiple Execution State objects.
- The Execution State Interpreter is closely tied to whatever message a node meant to send. For example, the Execution State Interpreter might pass back the information that we are now to switch to workflow X. This formalizes how we already announce the next slide to display. We check the execution variable ‘slide’.
- The Execution Sniffer communicates with the wrapper. The Execution Sniffer is itself a composition bringing together the above components.
Since we have several components, we’ll create a new Sniffer directory and a corresponding tests/sniffer directory. We’ll define respective file namespaces accordingly.
Feature Details
Let’s lay out what each component should do. We’ll quickly discover that we’ll need another component which keeps track of the various Execution State objects. Call it the Execution State Registry.
The Plugin
The available methods are documented here: http://ezcomponents.org/docs/api/latest/Workflow/ezcWorkflowExecutionPlugin.html. For now let’s keep this simple and only implement afterNodeExecuted().
When called, we want to do the following.
- Notify the Execution State Registry that this execution is, as of this moment, the current execution.
- Obtain the relevant Execution State object from the Execution State Registry.
- Have the relevant Execution State object capture and retain this execution’s current execution state.
Our unit test, therefore, can be frightfully simple. We can pass in mocks of the execution and node, and condition mocks of the Execution State Registry and Execution State object to ensure they are called as intended.
To be sure, we may have some frightfully complex scenarios to explore, such as synchronized parallel executions with paused sub workflows. But we’ll get to those later!
The Execution State Object
The execution state object retains the execution state as defined by isCancelled(), hasEnded(), and isSuspended(); and the execution variables as obtained via getVariables().
The functionality is:
- Capture execution state, given the execution as passed to the plugin.
- Disgorge the last-captured execution state. Mirror the execution methods isCancelled(), hasEnded(), isSuspended, getVariables(), getVariable(), hasVariable(), getId().
Our unit tests will explore the API of each method, followed by verifying the functionality in each case.
The Execution State Interpreter
We’ll keep the interpreter simple. The interpreter returns a text string. Here is the logic:
- If the ‘cancel’ variable exists, return “cancel”.
- If the ‘swap’ variable exists, it names the new workflow. Return “swap “ followed by the new workflow name. See the below discussion.
- If the ‘slide’ variable exists, return “slide “ followed by the slide variable’s value.
- Otherwise, return an empty string.
We could do a series of workflow swaps, for example, if our first workflow determines the customer’s demographics; we swap to a second workflow which determines the appropriate offer group; that offer group is a workflow which presents a specific offer.
Where this gets tricky is with sub workflows and parallel executions. So far as I know, if we are switching to a new workflow, the only thing we can do is cancel the current workflow, and proceed with the new workflow. The cancel() destroys all vestiges of the current workflow, including sub workflows and parallel threads.
Another option might be to leave the current workflow suspended, run the new workflow, and then resume the suspended workflow.
Another option might be that the workflow ends itself, presenting the new workflow name as its final result. We then proceed with the new workflow.
The Execution Sniffer
The Execution Sniffer communicates with the Zeta Components Interface (Wrapper). Either the Wrapper, or the Sniffer, is responsible for ensuring the plugin is attached to all workflows.
Since the Wrapper is responsible for instantiating workflow executions, I’m inclined to make the Wrapper responsible for attaching the plugin when it does so. But, as I think about that, I think we’ll make the Wrapper responsible for telling the Sniffer to attach its plugin.
We need to do a bit of homework. Once a plugin is attached to the main workflow, does it get attached to sub workflows and parallel threads?
Let’s check the sub workflow first. See zetacomponents/workflow/src/nodes/sub_workflow.php.
public function execute( ezcWorkflowExecution $execution ) { if ( $execution->definitionStorage === null ) { throw new ezcWorkflowExecutionException( 'No ezcWorkflowDefinitionStorage implementation available.' ); } $workflow = $execution->definitionStorage->loadByName( $this->configuration['workflow'] ); // Sub Workflow is not interactive. if ( !$workflow->isInteractive() && !$workflow->hasSubWorkflows() ) { $subExecution = $execution->getSubExecution( null, false ); $subExecution->workflow = $workflow; $this->passVariables( $execution, $subExecution, $this->configuration['variables']['in'] ); $subExecution->start(); }
That’s enough code to tell us what we need to know. See line 15. We need to check $execution->getSubExecution( null, false ). On a side note, it looks like we can only use sub workflows if we are using the tied database. definitionStorage is the tied database component, and line three makes it mandatory for the Sub Workflow node.
GetSubExecution() is in zetacomponents/workflow/src/interfaces/execution.php, the same file containing start() and resume() we studied earlier.
/** * Returns a new execution object for a sub workflow. * * If this method is used to resume a subworkflow you must provide * the execution id through $id. * * If $interactive is false an ezcWorkflowExecutionNonInteractive * will be returned. * * This method can be used by nodes implementing sub-workflows * to get a new execution environment for the subworkflow. * * @param int $id * @param bool $interactive * @return ezcWorkflowExecution * @ignore */ public function getSubExecution( $id = null, $interactive = true ) { if ( $interactive ) { $execution = $this->doGetSubExecution( $id ); } else { $execution = new ezcWorkflowExecutionNonInteractive; } foreach ( $this->plugins as $plugin ) { $execution->addPlugin( $plugin ); } return $execution; }
Line 31 is our answer. For each plugin in the parent execution (line 29), we add that plugin to the new sub execution (line 31).
What about parallel executions? I took a moment to chase that down as best I could from the online documentation, and it looks like we activate nodes in the current execution but do NOT create a new execution. So, it appears we have our plugin in place. If we ever do parallel threads, we’ll want to test and validate our assumption.
Homework complete, let’s lay out the Execution Sniffer features.
- When requested by the wrapper, and given the execution, attach our plugin to the execution. attachPlugin()
- Return the current Execution State Interpreter result. currentInterpretation()
- Return execution state of the current Execution State object. currentIsCancelled(), currentHasEnded(), currentIsSuspended(), currentGetVariables(), currentGetVariable(), currentHasVariable(), currentGetId().
We’ll have the Laravel Service Provider wire everything together.
Execution State Registry
Since we potentially have multiple executions, we need a registry to manage the multiple Execution State objects. The registry also maintains the notion of which is the “current” Execution State object. The features are:
- Accept notification that “this” is the current execution.
- For a given execution, return the relevant Execution State object. We either fetch it from our cache, or construct it on the spot. I am assuming that, by this point, each execution has an id. If not, initially, throw an exception. Later, we should probably ignore executions with no id, assuming that later in the workflow execution it WILL have an id, and we can snag it at that point.
- Return the “current” Execution State object.
Development Plan
Our general approach is to begin with the component with the least dependencies. We begin by writing tests exploring its interface (constructor and callable methods). We then drill down, fleshing out and verifying its functionality.
Having fully developed one component, we can accurately mock it while developing later components. Having fully unit tested it, we can be confident that mocking is sufficient.
We’ll develop our components in the following order, based on each component’s dependencies:
- The Execution State Object. Its only dependency is the execution, which we’ll mock.
- The Execution State Registry. Its only dependencies are Execution State Objects.
- The Execution State Interpreter. It acts on an Execution State Object.
- The Plugin. It works with the Execution State Registry and Execution State Objects.
- The Execution Sniffer. It is composed of each of the above objects.
Up next: The Execution State Object.
Categories: Zeta Components Workflow Tags:
Zeta Workflow 06: Zeta Components Wrapper Implementation
This ZetaComponentsWrapper is a Composition bringing together multiple objects needed for using the Zeta Components Workflow:
- $this->db, defined in the parent class. This the Tied Database connection. The Zeta Components Workflow Database Tie-in package makes a separate connection to our Laravel database, using the Zeta Components Database package.
- $this->workflow. This the Zeta Components Workflow object as defined in the Zeta Components Workflow package.
- $this->definition. This is the tied-database version of the Zeta Components Workflow definition. It is used to load and store the workflow object to/from the tied database.
- $this->xmldefinition (not implemented). This is the xml-file version of the Zeta Components Workflow definition. It is used to load and store the workflow object to/from XML files.
- $this->runner. An execution of the current workflow. This is the “interactive” execution, and is associated with the tied database.
- $this->slide. A helper class that maps our slide names to their associated Workflow Action Nodes. This class simplifies the complexity of saying, “At this point in the flow I want to present this slide to the user.”
- $this->node. A helper class that manipulates and builds up Workflow Nodes. This class simplifies various Workflow Node Composition chores.
Categories: Zeta Components Workflow Tags:
Zeta Workflow 05: Zeta Components Interface
When using the Zeta Components package from Laravel, you will be using the Zeta Components Wrapper. In PHP (and Laravel) terms, the Zeta Components Wrapper implements the Zeta Components Interface specification. The rest of this post is a copy of the PHPDocumentor documentation inside the Zeta Components Interface.
- The Zeta Components Interface is file workbench/star-tribune/workflow/src/StarTribune/Workflow/Wrapper/ZetaComponentsInterface.php.
- The PHPDocumentor-generated file is workbench/star-tribune/workflow/phpdoc/classes/StarTribune.Workflow.Wrapper.ZetaComponentsInterface.html.
- The top of the phpdoc tree is workbench/star-tribune/workflow/phpdoc/index.html.
Categories: Zeta Components Workflow Tags:
Zeta Workflow 04: Exception Handling Walkthrough
The released Zeta Workflow code does not correctly handle exceptions. This walkthrough describes the behavior as modified by myself.
The basic issue was that both start() and resume(), when using the Database Tie-In component, issue beginTransaction() to the database engine. The workflows can throw exceptions which remain uncaught. Because the exception is not caught within the workflow code, the transaction is never closed with either rollback or commit. When using an sqlite database, unit tests then fail, complaining of a locked table.
Within start() and resume(), we now catch the exceptions so that we can commit or roll back the transaction before (possibly) re-throwing the exception.
Categories: Zeta Components Workflow Tags: