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!
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
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.