A Context is created by your application to provide the right dependencies ('context') for the statemachine to work with. It seperates the concerns for the statemachine of how you are reading/writing state data and of how you access your domain models. Important are: - the entity id, which references an application domain specific object like 'Order' or 'Customer' that goes through some finite states in it's lifecycle. - the machine name, which is the type identifier for the machine and related to the entity (eg: 'order-machine') - persistence adapter, which reads/writes to/from a storage facility - entity_builder, which constructs the stateful entity. The entity is the object that will be acted upon by the Statemachine. This stateful object will be uniquely identified by it's id, which will mostly be some sort of primary key for that object that is defined by the application specific implementation. A reference to the stateful object can be obtained via the factory method getEntity(). This class delegates reading and writing states to specific implementations of the Adapter classes. this is useful for testing and creating specific behaviour for statemachines that need extra functionality to get and set the correct states.
Author: Rolf Vreijdenberger
Example #1
0
 /**
  * returns the associated Command for the entry/exit/transition action on a
  * State or a Transition.
  * the Command will be configured with the 'reference' of the stateful
  * object
  *
  * @param string $command_name
  *            entry~,exit~ or transition command name.
  *            multiple commands can be split by a ',' in which case a
  *            composite command will be returned.
  * @param Context $context
  *            to be able to get the entity
  * @return ICommand
  * @throws Exception
  */
 public static function getCommand($command_name, Context $context)
 {
     // it's oke to have no command, as there might be 'marker' states, where
     // we just need to transition something to a next state (according to a
     // rule)
     // where useful work can be done (eg: from the 'initial' type state to
     // a 'shortcut' state for special cases.
     if ($command_name === '' || $command_name === null) {
         // return a command without side effects
         return new Null();
     }
     $output = new Composite();
     // a command string can be made up of multiple commands seperated by a
     // comma
     $all_commands = explode(',', $command_name);
     // get the correct object to inject in the command(s)
     $entity = $context->getEntity();
     foreach ($all_commands as $single_command) {
         if (!class_exists($single_command)) {
             $e = new Exception(sprintf("failed command creation, class does not exist: (%s) for Context (%s)", $single_command, $context->toString()), Exception::COMMAND_CREATION_FAILURE);
             throw $e;
         }
         try {
             $command = new $single_command($entity);
             $output->add($command);
         } catch (\Exception $e) {
             $e = new Exception(sprintf("command (%s) objects to construction for Context (%s). message: '%s'", $single_command, $context->toString(), $e->getMessage()), Exception::COMMAND_CREATION_FAILURE);
             throw $e;
         }
     }
     return $output;
 }
 /**
  * @test
  */
 public function shouldLoadAndWriteViaDelegator()
 {
     $loader = XML::createFromFile(__DIR__ . '/../loader/fixture-example.xml');
     $writer = new Memory();
     $identifier = new Identifier('readerwriter-test', 'test-machine');
     $delegator = new ReaderWriterDelegator($loader, $writer);
     $context = new Context($identifier, null, $delegator);
     $machine = new StateMachine($context);
     $this->assertCount(0, $machine->getTransitions());
     $count = $delegator->load($machine);
     //add to the backend
     $this->assertTrue($context->add('a'));
     $this->assertCount(4, $machine->getTransitions(), 'there is a regex transition that adds 2 transitions (a-c and b-c)');
     $this->assertEquals(4, $count);
     $this->assertTrue($machine->ab());
     //get the data from the memory storage facility
     $data = $writer->getStorageFromRegistry($machine->getContext()->getIdentifier());
     $this->assertEquals('b', $data->state);
     $this->assertEquals('b', $machine->getCurrentState()->getName());
     $this->assertTrue($machine->bdone());
     $data = $writer->getStorageFromRegistry($machine->getContext()->getIdentifier());
     $this->assertEquals('done', $data->state);
 }
Example #3
0
 /**
  * calls a $callable if it exists, with the arguments $context->getEntity()
  * @param $callable $callable
  * @param Context $context
  */
 protected function callCallable($callable, Context $context)
 {
     if ($callable != self::CALLABLE_NULL && is_callable($callable)) {
         call_user_func($callable, $context->getEntity());
     }
 }
 /**
  * set the context on the statemachine and provide bidirectional
  * association.
  *
  * change the context for a statemachine that already has a context.
  * When the context is changed, but it is for the same statemachine (with
  * the same transitions), the statemachine can be used directly with the
  * new context.
  *
  * The current state is reset to whatever state the machine should be in
  * (either the initial state or the stored state) whenever a context change
  * is made.
  *
  * we can change context to:
  * - switch builders/persistence adapters at runtime
  * - reuse the statemachine for a different entity so we do not
  * have to load the statemachine with the same transition definitions
  *
  * @param Context $context            
  * @throws Exception
  */
 public function setContext(Context $context)
 {
     if ($this->getContext()) {
         // context already exists.
         if ($this->getContext()->getMachine() !== $context->getMachine()) {
             throw new Exception(sprintf("Trying to set context for a different machine. currently '%s' and new '%s'", $this->getContext()->getMachine(), $context->getMachine()), Exception::SM_CONTEXT_DIFFERENT_MACHINE);
         }
         // reset state TODO: move outside if statement
         $this->state = null;
     }
     $context->setStateMachine($this);
     $this->context = $context;
 }
 /**
  * @test
  */
 public function shouldEnterWithCallable()
 {
     $state = new State('a');
     $context = new Context(new Identifier('123', 'foo-machine'));
     $event = 'foo';
     $callable = function ($entity) {
         $entity->setEntityId('234');
     };
     $state->setEntryCallable($callable);
     $this->assertEquals('123', $context->getEntityId());
     $state->exitAction($context);
     $this->assertEquals('123', $context->getEntityId());
     $state->entryAction($context);
     $this->assertEquals('234', $context->getEntityId());
 }
 /**
  * returns the associated Rule for this Transition,
  * configured with a 'reference' (stateful) object
  *
  * @param Context $context
  *            the associated Context for a our statemachine
  * @return IRule a Rule or chained AndRule if the rule input was a ','
  *         seperated string of rules.
  * @throws Exception
  */
 public function getRule(Context $context)
 {
     // if no rule is defined, just allow the transition by default
     if ($this->rule === '' || $this->rule === null) {
         return new TrueRule();
     }
     $entity = $context->getEntity();
     // a rule string can be made up of multiple rules seperated by a comma
     $all_rules = explode(',', $this->rule);
     $rule = new TrueRule();
     foreach ($all_rules as $single_rule) {
         // guard clause to check if rule exists
         if (!class_exists($single_rule)) {
             $e = new Exception(sprintf("failed rule creation, class does not exist: (%s) for Context (%s).", $this->rule, $context->toString()), Exception::RULE_CREATION_FAILURE);
             throw $e;
         }
         try {
             $and_rule = new $single_rule($entity);
             // create a chain of rules that need to be true
             $rule = new AndRule($rule, $and_rule);
         } catch (\Exception $e) {
             $e = new Exception(sprintf("failed rule creation, class objects to construction with entity: (%s) for Context (%s). message: %s", $this->rule, $context->toString(), $e->getMessage()), Exception::RULE_CREATION_FAILURE);
             throw $e;
         }
     }
     return $rule;
 }
 /**
  * @test
  */
 public function shouldAcceptMultipleCallableTypes()
 {
     //there are diverse ways to use callables: closures, anonymous function, instance methods
     //static methods.
     //https://php.net/manual/en/functions.anonymous.php
     //https://php.net/manual/en/language.types.callable.php
     $context = new Context(new Identifier('123', 'foo-machine'));
     $event = 'foo';
     $a = new State('a');
     $b = new State('b');
     //scenario 1: Closure without variables from the parent scope
     $transition_callable = function ($entity) {
         $entity->setEntityId('234');
     };
     $t = new Transition($a, $b, $event, null, null, null, $transition_callable);
     $this->assertEquals('123', $context->getEntityId());
     $t->process($context);
     $this->assertEquals('234', $context->getEntityId());
     //scenario 2: Closure with Inheriting variables from the parent scope
     $x = 4;
     $transition_callable = function ($entity) use(&$x) {
         $x += 1;
     };
     $t = new Transition($a, $b, $event, null, null, null, $transition_callable);
     $this->assertEquals(4, $x);
     $t->process($context);
     $this->assertEquals(5, $x);
     //scenario 3: Anonymous function / literal
     $context->getIdentifier()->setEntityId('123');
     $t = new Transition($a, $b, $event, null, null, null, function ($entity) {
         $entity->setEntityId('234');
     });
     $this->assertEquals('123', $context->getEntityId());
     $t->process($context);
     $this->assertEquals('234', $context->getEntityId());
     //scenario 4: instance method invocation (method as string)
     $helper = new CallableHelper();
     $transition_callable = array($helper, 'increaseInstanceId');
     $t = new Transition($a, $b, $event, null, null, null, $transition_callable);
     $this->assertEquals(0, $helper->instance_id);
     $t->process($context);
     $this->assertEquals(1, $helper->instance_id);
     $t->process($context);
     $this->assertEquals(2, $helper->instance_id);
     //scenario 5: static method invocation in array (use fully qualified name)
     $helper = new CallableHelper();
     $transition_callable = array('izzum\\statemachine\\CallableHelper', 'increaseId');
     $t = new Transition($a, $b, $event, null, null, null, $transition_callable);
     $this->assertEquals(0, CallableHelper::$id);
     $t->process($context);
     $this->assertEquals(1, CallableHelper::$id);
     //scenario 6: static method invocation in string (use fully qualified name)
     //THIS IS THE WAY TO be able to specify a callable in a configuration file.
     $helper = new CallableHelper();
     $transition_callable = 'izzum\\statemachine\\CallableHelper::increaseId';
     $t = new Transition($a, $b, $event, null, null, null, $transition_callable);
     $this->assertEquals(1, CallableHelper::$id);
     $t->process($context);
     $this->assertEquals(2, CallableHelper::$id);
     //scenario 7: wrap an existing method in a closure (this is THE way to reuse an existing method)
     function jo($entity)
     {
         $entity->setEntityId($entity->getEntityId() + 1);
     }
     $callable = function ($context) {
         jo($context);
     };
     $context->getIdentifier()->setEntityId('123');
     $t = new Transition($a, $b, $event, null, null, null, $callable);
     $this->assertEquals('123', $context->getEntityId());
     $t->process($context);
     $this->assertEquals('124', $context->getEntityId());
 }
 /**
  * test the factory method with all parameters provided
  * implicitely tests the constructor
  */
 public function testContext()
 {
     $entity_id = "id1";
     $machine = "test machine";
     $identifier = new Identifier($entity_id, $machine);
     $builder = new EntityBuilder();
     $io = new Memory();
     // all parameters
     $o = new Context($identifier, $builder, $io);
     $this->assertEquals($entity_id, $o->getEntityId());
     $this->assertEquals($machine, $o->getMachine());
     $this->assertNull($o->getStateMachine());
     $this->assertTrue(is_a($o->getPersistenceAdapter(), 'izzum\\statemachine\\persistence\\Memory'));
     $this->assertTrue(is_a($o->getBuilder(), 'izzum\\statemachine\\EntityBuilder'));
     $this->assertEquals($identifier, $o->getEntity());
     $this->assertTrue(is_string($o->getEntityId()));
     $this->assertTrue(is_string($o->toString()));
     $this->assertContains($entity_id, $o->toString());
     $this->assertContains($machine, $o->toString());
     $this->assertContains('izzum\\statemachine\\Context', $o->toString());
     // even though we have a valid reader, the state machine does not exist.
     $this->assertEquals(State::STATE_UNKNOWN, $o->getState());
     $this->assertTrue($o->setState(State::STATE_NEW, 'this is an informational message about why we set this state: we set this state to new for a unittest'), 'added');
     $this->assertFalse($o->setState(State::STATE_NEW), 'already there');
     // for coverage.
     $statemachine = new StateMachine($o);
     $this->assertNull($o->setStateMachine($statemachine));
     $this->assertContains('Context', $o . '', '__toString()');
 }