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.

  1. getPlugin() returns the plugin object which was wired up by the service provider.
  2. getInterpreter() returns the interpreter object which was wired up by the service provider.
  3. getRegistry() returns the registry object which was wired up by the service provider.
  4. attachPlugin($execution) calls $execution->addPlugin() with our plugin as wired up by the service provider.
  5. 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’.
  6. 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:

  1. Adding a plugin class the first time returns true.
  2. 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