GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

StateMachine::transition()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 1
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
c 2
b 0
f 1
cc 1
eloc 3
nc 1
nop 2
crap 1
1
<?php
2
namespace izzum\statemachine;
3
use izzum\statemachine\Context;
4
use izzum\statemachine\State;
5
use izzum\statemachine\Transition;
6
use izzum\statemachine\Exception;
7
use izzum\statemachine\utils\Utils;
8
9
/**
10
 * A statemachine is used to execute transitions from certain states to other
11
 * states, for an entity that represents a domain object, by applying guard logic 
12
 * to see if a transition is allowed and transition logic to process the transition.
13
 *
14
 *
15
 * PACKAGE PHILOSOPHY
16
 * This whole package strives to follow the open/closed principle, making it
17
 * open for extension (adding your own logic through subclassing) but closed
18
 * for modification. For that purpose, we provide Loader interfaces, Builders
19
 * and Persistence Adapters for the backend implementation. States and
20
 * Transitions can also be subclassed to store data and provide functionality.
21
 *
22
 * Following the same philosophy: if you want more functionality in the
23
 * statemachine, you can use the provided methods and override them in your
24
 * subclass. multiple hooks are provided. Most functionality should be provided by
25
 * using the diverse ways of interacting with the statemachine: callables can be injected,
26
 * commands can be injected, event handlers can be defined and hooks can be overriden.
27
 *
28
 * We have provided multiple persistance backends to function as a data store to store all
29
 * relevant information for a statemachine including configuration, transition history and current states.
30
 * - relational databases: postgresql, mysql, sqlite
31
 * - nosql key/value: redis
32
 * - nosql document based: mongodb 
33
 * Memory and session backend adapters can be used to temporarily store the state information.
34
 * yaml, json and xml loaders can be used to load configuration data from files containing those
35
 * data formats.
36
 * 
37
 * Examples are provided in the 'examples' folder and serve to highlight some of the
38
 * features and the way to work with the package. The unittests can serve as examples too, 
39
 * including interaction with databases.
40
 *
41
 *
42
 * ENVIRONMENTS OF USAGE:
43
 * - 1: a one time process:
44
 * -- on webpages where there are page refreshes in between (use session/pdo adapter)
45
 * see 'examples/session'
46
 * -- an api where succesive calls are made (use pdo adapter)
47
 * -- cron jobs (pdo adapter)
48
 * - 2: a longer running process:
49
 * -- a php daemon that runs as a background process (use memory adapter)
50
 * -- an interactive shell environment (use memory adapter)
51
 * see 'examples/interactive'
52
 * 
53
 * 
54
 * DESCRIPTION OF THE 4 MAIN MODELS OF USAGE:
55
 * - 1: DELEGATION: Use an existing domain model. You can use a subclass of the
56
 * AbstractFactory to get a StateMachine, since that will put the creation of
57
 * all the relevant Contextual classes and Transitions in a reusable model.
58
 * usage: This is the most formal, least invasive and powerful model of usage, but the most complex.
59
 * Use Rules and Commands to interact with your domain model without altering a domain model
60
 * to work with the statemachine.
61
 * see 'examples/trafficlight'
62
 * - 2: INHERITANCE: Subclass a statemachine. Build the full Context and all transitions in
63
 * the constructor of your subclass (which could be a domain model itself) and
64
 * call the parent constructor. use the hooks to provide functionality (optionally a ModelBuilder and callbacks).
65
 * usage: This is the most flexible model of usage, but mixes your domain logic with the statemachine.
66
 * see 'examples/inheritance'
67
 * - 3: COMPOSITION: Use object composition. Instantiate and build a statemachine in a domain
68
 * model and build the full Context, Transitions and statemachine there. Use a ModelBuilder and callbacks
69
 * to drive functionality.
70
 * usage: This is a good mixture between encapsulating statemachine logic and flexibility/formal usage. 
71
 * see 'examples/composition'
72
 * - 4: STANDALONE: Use the statemachine as is, without any domain models. Your application will use it and inspect the
73
 * statemachine to act on it. Use callables to provide functionality
74
 * usage: This is the easiest model of usage, but the least powerful.
75
 * see 'examples/interactive'
76
 *
77
 * MECHANISMS FOR GUARD AND TRANSITION LOGIC
78
 * 1. rules and commands: they are fully qualified class names of Rule and Command (sub)classes that can be injected
79
 *      in Transition and State instances. they accept the domain model provided via an EntityBuilder 
80
 *      in their constructor.
81
 * 2. hooks: the methods in this class that start with an underscore and can be overriden in a subclass.
82
 *      these can be used to provide a subclass of this machine specifically tailored to your application domain.
83
 *      @link http://c2.com/cgi/wiki?HookMethod
84
 * 3. eventHandlers: methods that can be defined on the domain model. They will only be called when they
85
 *      are defined on the domain model and all start with 'on' prefix eg: onExitState, onTransition
86
 *      @link: https://en.wikipedia.org/wiki/Event_(computing)#Event_handler
87
 * 4. callables: callables can be injected in Transition and State instances. callables are 
88
 *      user defined methods in various flavours (closure, instance methods, static methods etc.) that
89
 *      will be called if they are valid. This is the easiest way to start using the statemachine.
90
 *      @link https://php.net/manual/en/language.types.callable.php
91
 *      @see TransitionTest::shouldAcceptMultipleCallableTypes for all implementations
92
 *
93
 * DESCRIPTION OF FULL TRANSITION ALGORITHM (and the usage modes where they apply)
94
 * 1. guard: _onCheckCanTransition($transition) //hook: inheritance.
95
 * 2. guard: $entity->onCheckCanTransition($identifier, $transition) // event handler: delegation, composition, inheritance
96
 * 3. guard: $transition->can($context) // check Rule: delegation, composition, inheritance, standalone
97
 * 4. guard: $transition->can($context) // callable: standalone, delegation, composition, inheritance
98
 * !!!  If all (existing) guards return true, then the transition is allowed. return true/false to allow/disallow transition.
99
 * 5. logic: _onExitState($transition) //hook: inheritance
100
 * 6. logic: $entity->onExitState($identifier, $transition) //event handler: delegation, composition, inheritance
101
 * 7. logic: $state_from->exitAction($context) //execute Command: delegation, composition, inheritance, standalone
102
 * 8. logic: state exit: $callable($entity) // callable: standalone, delegation, composition, inheritance
103
 * 9. logic: _onTransition($transition) //hook: inheritance
104
 * 10. logic: $entity->onTransition($identifier, $transition) //event handler: delegation, composition, inheritance
105
 * 11. logic: $transition->process($context) //execute Command: delegation, composition, inheritance, standalone
106
 * 12. logic: $transition->process($context) // callable: standalone, delegation, composition, inheritance
107
 * 13. logic: $entity->onEnterState($identifier, $transition) //event handler: delegation, composition, inheritance
108
 * 14. logic: $state_to->entryAction($context) //execute Command: delegation, composition, inheritance, standalone
109
 * 15. logic: state entry: $callable($entity) // callable: standalone, delegation, composition, inheritance
110
 * 16. logic: _onEnterState($transition) //hook: inheritance
111
 *
112
 * each hook can be overriden and implemented in a subclass, providing
113
 * functionality that is specific to your application. This allows you to use
114
 * the core mechanisms of the izzum package and extend it to your needs.
115
 *
116
 *
117
 * each event handler might be implemented on the entity/domain object and will be called, only if
118
 * available, with the $transition and $event arguments.
119
 * 
120
 * each callable might be set on a transition or state and comes in different forms:
121
 * closure, anonymouse function, instance method, static methods etc. (see php manual)
122
 * 
123
 *  Each transition will take place only if the transition logic (Rule, hooks, event handlers &
124
 *  callables) allows it. Each exit state, transition and entry state will then execute specific logic
125
 * (Command, hooks, event handlers & callables).
126
 *
127
 * for simple applications, the use of callables and event handlers is advised since it
128
 * is easier to setup and use with existing code.
129
 *
130
 * for more formal applications, the use of rules and commands is advised since
131
 * it allows for better encapsulated (and tested) business logic and more flexibility in
132
 * configuration via a database or config file.
133
 * 
134
 * The statemachine should be loaded with States and Transitions, which define
135
 * from what state to what other state transitions are allowed. The transitions
136
 * are checked against guard clauses (in the form of a business Rule instance
137
 * and hooks, event handlers and callables) and transition logic (in the form of a Command
138
 * instance, hooks, event handlers and callables).
139
 *
140
 * Transitions can also trigger an exit action for the current state and an
141
 * entry action for the new state (also via Command instances, event handlers,hooks and
142
 * callables). This allows you to implement logic that is dependent on the
143
 * State and independent of the Transition.
144
 *
145
 * RULE/COMMAND logic
146
 * The Rule checks if the domain model (or it's derived data) applies and
147
 * therefore allows the transition, after which the Command is executed that can
148
 * actually alter data in the underlying domain models, call services etc.
149
 * Rules should have NO side effects, since their only function is to check if a 
150
 * Transition is allowed. Commands do the heavy lifting for executing logic that
151
 * relates to a transition.
152
 * 
153
 * EXCEPTIONS
154
 * All high level interactions that a client conducts with a statemachine
155
 * should expect exceptions since runtime exceptions could bubble up from the
156
 * application specific classes from your domain (eg: calling services, database interactions etc). 
157
 * Exceptions that bubble up from this statemachine are always izzum\statemachine\Exception types.
158
 *
159
 * NAMING CONVENTIONS
160
 * - A good naming convention for states is to use lowercase-hypen-seperated names:
161
 * new, waiting-for-input, starting-order-process, enter-cancel-flow, done
162
 * - A good naming convention for transitions is to bind the input and exit state
163
 * with the string '_to_', which is done automatically by this package eg: 
164
 * new_to_waiting-for-input, new_to_done so you're able to call $statemachine->transition('new_to_done');
165
 * - A good naming convention for events (transition trigger names) is to use
166
 * lowercase-underscore-seperated names (or singe words) so your able to call
167
 * $machine->start() or $machine->event_name() or $machine->handle('event_name') 
168
 * 
169
 * 
170
 * MISC
171
 * The implementation details of this machine make it that it can act both as
172
 * a mealy machine and as a moore machine. the concepts can be mixed and
173
 * matched.
174
 * 
175
 *
176
 * @author Rolf Vreijdenberger
177
 * @link https://en.wikipedia.org/wiki/Finite-state_machine
178
 * @link https://en.wikipedia.org/wiki/UML_state_machine
179
 */
180
class StateMachine {
181
    
182
    /**
183
     * The context instance that provides the context for the statemachine to
184
     * operate in.
185
     *
186
     * @var Context
187
     */
188
    private $context;
189
    
190
    /**
191
     * The available states.
192
     * these should be loaded via transitions
193
     * A hashmap where the key is the state name and the value is
194
     * the State.
195
     *
196
     * @var State[]
197
     */
198
    private $states = array();
199
    
200
    /**
201
     * The available transitions.
202
     * these should be loaded via the 'addTransition' method.
203
     * A hashmap where the key is the transition name and the value is
204
     * the Transition.
205
     *
206
     * @var Transition[]
207
     */
208
    private $transitions = array();
209
    
210
    /**
211
     * the current state
212
     *
213
     * @var State
214
     */
215
    private $state;
216
    
217
    // ######################## TRANSITION METHODS #############################
218
    
219
220
    /**
221
     * Constructor
222
     *
223
     * @param Context $context
224
     *            a fully configured context providing all the relevant
225
     *            parameters/dependencies to be able to run this statemachine for an entity.
226
     */
227 58
    public function __construct(Context $context)
228
    {
229
        // sets up bidirectional association
230 58
        $this->setContext($context);
231 58
    }
232
233
    /**
234
     * Apply a transition by name.
235
     *
236
     * this type of handling is found in moore machines.
237
     *
238
     * @param string $transition_name
239
     *            convention: <state-from>_to_<state-to>
240
     * @param string $message optional message. this can be used by the persistence adapter
241
     *          to be part of the transition history to provide extra information about the transition. 
242
     * @return boolean true if the transition was made
243
     * @throws Exception in case something went disastrously wrong or if the
244
     *         transition does not exist. An exception will lead to a (partially
245
     *         or fully) failed transition.
246
     * @link https://en.wikipedia.org/wiki/Moore_machine
247
     */
248 4
    public function transition($transition_name, $message = null)
249
    {
250 4
        $transition = $this->getTransitionWithNullCheck($transition_name);
251 4
        return $this->performTransition($transition, $message);
252
    }
253
254
    /**
255
     * Try to apply a transition from the current state by handling an event
256
     * string as a trigger.
257
     * If the event is applicable for a transition then that transition from the
258
     * current state will be applied.
259
     * If there are multiple transitions possible for the event, the transitions
260
     * will be tried until one of them is possible.
261
     *
262
     * This type of (event/trigger) handling is found in mealy machines.
263
     *
264
     * @param string $event
265
     *            in case the transition will be triggered by an event code
266
     *            (mealy machine)
267
     *            this will also match on the transition name
268
     *            (<state_to>_to_<state_from>)
269
     * @param string $message optional message. this can be used by the persistence adapter
270
     *          to be part of the transition history to provide extra information about the transition.  
271
     * @return bool true in case a transition was triggered by the event, false
272
     *         otherwise
273
     * @throws Exception in case something went horribly wrong
274
     * @link https://en.wikipedia.org/wiki/Mealy_machine
275
     * @link http://martinfowler.com/books/dsl.html for event handling
276
     *       statemachines
277
     */
278 10
    public function handle($event, $message = null)
279
    {
280 10
        $transitioned = false;
281 10
        $transitions = $this->getCurrentState()->getTransitionsTriggeredByEvent($event);
282 10
        foreach ($transitions as $transition) {
283 10
            $transitioned = $this->performTransition($transition, $message);
284
            if ($transitioned)
285 9
                break;
286 9
        }
287 9
        return $transitioned;
288
    }
289
290
    /**
291
     * Have the statemachine do the first possible transition.
292
     * The first possible transition is based on the configuration of
293
     * the guard logic and the current state of the statemachine.
294
     *
295
     * This method is the main way to have the statemachine do useful work.
296
     * 
297
     * TRICKY: Be careful when using this function, since all guard logic must
298
     * be mutually exclusive! If not, you might end up performing the state
299
     * transition with priority n when you really want to perform transition
300
     * n+1.
301
     *
302
     * An alternative is to use the 'transition' method to target 1 transition
303
     * specifically:
304
     * $statemachine->transition('a_to_b');
305
     * So you are always sure that you are actually doing the intented
306
     * transition instead of relying on the configuration and guard logic (which
307
     * *might* not be correctly implemented, leading to transitions that would
308
     * normally not be executed).
309
     * 
310
     * @param string $message optional message. this can be used by the persistence adapter
311
     *          to be part of the transition history to provide extra information about the transition. 
312
     * @return boolean true if a transition was applied.
313
     * @throws Exception in case something went awfully wrong.
314
     *        
315
     */
316 8
    public function run($message = null)
317
    {
318
        try {
319 8
            $transitions = $this->getCurrentState()->getTransitions();
320 8
            foreach ($transitions as $transition) {
321 8
                $transitioned = $this->performTransition($transition, $message);
322 7
                if ($transitioned) {
323 7
                    return true;
324
                }
325 4
            }
326 5
        } catch(Exception $e) {
327
            // will be rethrown
328 1
            $this->handlePossibleNonStatemachineException($e, Exception::SM_RUN_FAILED);
329
        }
330
        // no transition done
331 4
        return false;
332
    }
333
334
    /**
335
     * run a statemachine until it cannot run any transition in the current
336
     * state or until it is in a final state.
337
     *
338
     * when using cyclic graphs, you could get into an infinite loop between
339
     * states. design your machine correctly.
340
     * @param string $message optional message. this can be used by the persistence adapter
341
     *          to be part of the transition history to provide extra information about the transition. 
342
     * @return int the number of sucessful transitions made.
343
     * @throws Exception in case something went badly wrong.
344
     */
345 4
    public function runToCompletion($message = null)
346
    {
347 4
        $transitions = 0;
348
        try {
349 4
            $run = true;
350 4
            while ( $run ) {
351
                // run the first transition possible
352 4
                $run = $this->run($message);
353 3
                if ($run) {
354 3
                    $transitions++;
355 3
                }
356 3
            }
357 4
        } catch(Exception $e) {
358 1
            $this->handlePossibleNonStatemachineException($e, $e->getCode());
359
        }
360 3
        return $transitions;
361
    }
362
363
    /**
364
     * Check if an existing transition on the curent state is allowed by the
365
     * guard logic.
366
     *
367
     * @param string $transition_name
368
     *            convention: <state-from>_to_<state-to>
369
     * @return boolean
370
     */
371 4
    public function canTransition($transition_name)
372
    {
373 4
        $transition = $this->getTransition($transition_name);
374 4
        return $transition === null ? false : $this->doCheckCanTransition($transition);
375
    }
376
377
    /**
378
     * checks if one or more transitions are possible and/or allowed for the
379
     * current state when triggered by an event
380
     *
381
     * @param string $event            
382
     * @return boolean false if no transitions possible or existing
383
     */
384 3
    public function canHandle($event)
385
    {
386 3
        $transitions = $this->getCurrentState()->getTransitionsTriggeredByEvent($event);
387 3
        foreach ($transitions as $transition) {
388 3
            if ($this->doCheckCanTransition($transition)) {
389 2
                return true;
390
            }
391 3
        }
392 2
        return false;
393
    }
394
395
    /**
396
     * check if the current state has one or more transitions that can be
397
     * triggered by an event
398
     *
399
     * @param string $event            
400
     * @return boolean
401
     */
402 3
    public function hasEvent($event)
403
    {
404 3
        $transitions = $this->getCurrentState()->getTransitionsTriggeredByEvent($event);
405 3
        if (count($transitions) > 0) {
406 3
            return true;
407
        }
408 1
        return false;
409
    }
410
    
411
    // ######################## CORE TRANSITION & TEMPLATE METHODS #############################
412
    
413
414
    /**
415
     * Perform a transition by specifiying the transitions' name from a state
416
     * that the transition is allowed to run.
417
     *
418
     * @param Transition $transition            
419
     * @param string $message optional message. this can be used by the persistence adapter
420
     *          to be part of the transition history to provide extra information about the transition.  
421
     * @return boolean true if the transition was succesful
422
     * @throws Exception in case something went horribly wrong
423
     * @link https://en.wikipedia.org/wiki/Template_method_pattern
424
     */
425 17
    private function performTransition(Transition $transition, $message = null)
426
    {
427
        // every method in this core routine has hook methods, event handlers and
428
        // callbacks it can call during the execution phase of the
429
        // transition steps if they are available on the domain model.
430
        try {
431
            
432 17
            if (!$this->doCheckCanTransition($transition)) {
433
                // one of the guards returned false or transition not found on current state.
434 4
                return false;
435
            }
436
            
437
            // state exit action: performed when exiting the state
438 16
            $this->doExitState($transition);
439
            // the transition is performed, with the associated logic
440 16
            $this->doTransition($transition, $message);
441
            // state entry action: performed when entering the state
442 16
            $this->doEnterState($transition);
443 17
        } catch(Exception $e) {
444 2
            $this->handlePossibleNonStatemachineException($e, Exception::SM_TRANSITION_FAILED, $transition);
445
        }
446 16
        return true;
447
    }
448
449
    /**
450
     * Template method to call a hook and to call a possible method
451
     * defined on the domain object/contextual entity
452
     *
453
     * @param Transition $transition            
454
     * @return boolean if false, the transition and its' associated logic will
455
     *         not take place
456
     * @throws Exception in case something went horribly wrong
457
     */
458 17
    private function doCheckCanTransition(Transition $transition)
459
    {
460
        try {
461
            // check if we have this transition on the current state.
462 17
            if (!$this->getCurrentState()->hasTransition($transition->getName())) {
463 3
                return false;
464
            }
465
            
466
            // possible hook so your application can place an extra guard on the
467
            // transition. possible entry~ or exit state type of checks can also
468
            // take place in this hook.
469 17
            if (!$this->_onCheckCanTransition($transition)) {
470 1
                return false;
471
            }
472
            // an event handler that is possibly defined on the domain model: onCheckCanTransition
473 17
            if (!$this->callEventHandler($this->getContext()->getEntity(), 'onCheckCanTransition', $this->getContext()->getIdentifier(), $transition)) {
474 1
                return false;
475
            }
476
            
477
            // this will check the guards defined for the transition: Rule and callable
478 17
            if (!$transition->can($this->getContext())) {
479 4
                return false;
480
            }
481 17
        } catch(Exception $e) {
482 2
            $this->handlePossibleNonStatemachineException($e, Exception::SM_CAN_FAILED);
483
        }
484
        // if we come here, this acts as a green light to start the transition.
485 16
        return true;
486
    }
487
488
    /**
489
     * the exit state action method
490
     *
491
     * @param Transition $transition            
492
     */
493 16
    private function doExitState(Transition $transition)
494
    {
495
        // hook for subclasses to implement
496 16
        $this->_onExitState($transition);
497
        // an event handler that is possibly defined on the domain model: onExitState
498 16
        $this->callEventHandler($this->getContext()->getEntity(), 'onExitState', $this->getContext()->getIdentifier(), $transition);
499
        // executes the command and callable associated with the state object
500 16
        $transition->getStateFrom()->exitAction($this->getContext());
501 16
    }
502
503
    /**
504
     * the transition action method
505
     *
506
     * @param Transition $transition            
507
     * @param string $message optional message. this can be used by the persistence adapter
508
     *          to be part of the transition history to provide extra information about the transition.          
509
     */
510 16
    private function doTransition(Transition $transition, $message = null)
511
    {
512
        // hook for subclasses to implement
513 16
        $this->_onTransition($transition);
514 16
        $entity = $this->getContext()->getEntity();
515 16
        $identifier = $this->getContext()->getIdentifier();
516
        // an event handler that is possibly defined on the domain model: onTransition
517 16
        $this->callEventHandler($entity, 'onTransition', $identifier, $transition);
518
        // executes the command and callable associated with the transition object
519 16
        $transition->process($this->getContext());
520
        
521
        // this actually sets the state so we are now in the new state
522 16
        $this->setState($transition->getStateTo(), $message);
523 16
    }
524
525
    /**
526
     * the enter state action method
527
     *
528
     * @param Transition $transition            
529
     */
530 16
    private function doEnterState(Transition $transition)
531
    {
532
        // an event handler that is possibly defined on the domain model: onEnterState
533 16
        $this->callEventHandler($this->getContext()->getEntity(), 'onEnterState', $this->getContext()->getIdentifier(), $transition);
534
        
535
        // executes the command and callable associated with the state object
536 16
        $transition->getStateTo()->entryAction($this->getContext());
537
        
538
        // hook for subclasses to implement
539 16
        $this->_onEnterState($transition);
540 16
    }
541
    
542
    // ######################## SUPPORTING METHODS #############################
543
    /**
544
     * All known/loaded states for this statemachine
545
     *
546
     * @return State[]
547
     */
548 45
    public function getStates()
549
    {
550 45
        return $this->states;
551
    }
552
553
    /**
554
     * get a state by name.
555
     *
556
     * @param string $name            
557
     * @return State or null if not found
558
     */
559 44
    public function getState($name)
560
    {
561 44
        return isset($this->states [$name]) ? $this->states [$name] : null;
562
    }
563
564
    /**
565
     * Add a state. without a transition.
566
     * Normally, you would not use this method directly but instead use
567
     * addTransition to add the transitions with the states in one go.
568
     * 
569
     * This method makes sense if you would want to load the statemachine with states
570
     * and not transitions, and then add the transitions with regex states.
571
     * This saves you the hassle of adding transitions before you add the
572
     * regex transitions (just so the states are already known on the machine).
573
     * 
574
     * in case a State is not used in a Transition, it will be orphaned and not 
575
     * reachable via other states.
576
     *
577
     * @param State $state 
578
     * @return boolean true if the state was not know to the machine or wasn't added, false otherwise.           
579
     */
580 44
    public function addState(State $state)
581
    {
582
        //no regex states
583 44
        if ($state->isRegex()) {
584 2
            return false;
585
        }
586
        //check for duplicates
587 44
        if (isset($this->states [$state->getName()])) {
588 41
            return false;
589
        }
590 44
        $this->states [$state->getName()] = $state;
591 44
        return true;
592
    }
593
594
    /**
595
     * sets the state as the current state and on the backend.
596
     * This should only be done:
597
     * - initially, right after a machine has been created, to set it in a
598
     * certain state if the state has not been persisted before.
599
     * - when changing context (since this resets the current state) via
600
     * $machine->setState($machine->getCurrentState())
601
     *
602
     * This method allows you to bypass the transition guards and the transition
603
     * logic. no exit/entry/transition logic will be performed
604
     *
605
     * @param State $state 
606
     * @param string $message optional message. this can be used by the persistence adapter
607
     *          to be part of the transition history to provide extra information about the transition.  
608
     * @throws Exception in case the state is not valid/known for this machine          
609
     */
610 16
    public function setState(State $state, $message = null)
611
    {
612
        
613 16
        if($this->getState($state->getName()) === null) {
614 1
            throw new Exception(sprintf("%s state '%s' not known to this machine", $this->toString(), $state->getName()), Exception::SM_UNKNOWN_STATE);
615
        }
616
        //get the state known to this machine so we are sure we have the correct reference
617
        //even if the client provides another instance of State with the same name
618 16
        $state = $this->getState($state->getName());
619 16
        $this->getContext()->setState($state->getName(), $message);
620 16
        $this->state = $state;
621 16
    }
622
623
    /**
624
     * add state information to the persistence layer if it is not there.
625
     * 
626
     * Used to mark the initial construction of a statemachine at a certain
627
     * point in time. This method only makes sense the first time a statemachine
628
     * is initialized and used since it will do nothing once a transition has been made.
629
     * 
630
     * It will set the initial state on the backend which sets
631
     * the construction time.
632
     * 
633
     * Make sure that the transitions and states are loaded before you call this method.
634
     * in other words: the machine should be ready to go.
635
     * 
636
     * @param string $message optional message. this can be used by the persistence adapter
637
     *          to provide extra information in the history of the machine transitions,
638
     *          in this case, about the first adding of this machine to the persistence layer. 
639
     * @return boolean true if not persisted before, false otherwise
640
     */
641 5
    public function add($message = null)
642
    {
643 5
        return $this->getContext()->add($this->getInitialState()->getName(), $message);
644
    }
645
646
    /**
647
     * gets the current state (or retrieve it from the backend if not set).
648
     *
649
     * the state will be:
650
     * - the state that was explicitely set via setState
651
     * - the state we have moved to after the last transition
652
     * - the initial state. if we haven't had a transition yet and no current
653
     * state has been set, the initial state will be retrieved (the state with State::TYPE_INITIAL)
654
     *
655
     * @return State
656
     * @throws Exception in case there is no valid current state found
657
     */
658 18
    public function getCurrentState()
659
    {
660
        // do we have a current state?
661 18
        if ($this->state) {
662 17
            return $this->state;
663
        }
664
        // retrieve state from the context if we do not find any state set.
665 18
        $state = $this->getState($this->getContext()->getState());
666 18
        if (!$state || $state == State::STATE_UNKNOWN) {
667
            // possible wrong configuration
668 2
            throw new Exception(sprintf("%s current state not found for state with name '%s'. %s", $this->toString(), $this->getContext()->getState(), 'are the transitions/states loaded and configured correctly?'), Exception::SM_NO_CURRENT_STATE_FOUND);
669
        }
670
        // state is found, set it and return
671 17
        $this->state = $state;
672 17
        return $this->state;
673
    }
674
675
    /**
676
     * Get the initial state, the only state with type State::TYPE_INITIAL
677
     *
678
     * This method can be used to 'add' the state information to the backend via
679
     * the context/persistence adapter when a machine has been initialized for
680
     * the first time.
681
     *
682
     * @param boolean $allow_null optional
683
     * @return State (or null or Exception, only when statemachine is improperly
684
     *         loaded)
685
     * @throws Exception if $allow_null is false an no inital state was found
686
     */
687 22
    public function getInitialState($allow_null = false)
688
    {
689 22
        $states = $this->getStates();
690 22
        foreach ($states as $state) {
691 21
            if ($state->isInitial()) {
692 21
                return $state;
693
            }
694 4
        }
695 3
        if ($allow_null) {
696 3
            return null;
697
        }
698 1
        throw new Exception(sprintf("%s no initial state found, bad configuration. %s", $this->toString(), 'are the transitions/states loaded and configured correctly?'), Exception::SM_NO_INITIAL_STATE_FOUND);
699
    }
700
701
    /**
702
     * All known/loaded transitions for this statemachine
703
     *
704
     * @return Transition[]
705
     */
706 22
    public function getTransitions()
707
    {
708 22
        return $this->transitions;
709
    }
710
711
    /**
712
     * get a transition by name.
713
     *
714
     * @param string $name
715
     *            convention: <state_from>_to_<state_to>
716
     * @return Transition or null if not found
717
     */
718 43
    public function getTransition($name)
719
    {
720 43
        return isset($this->transitions [$name]) ? $this->transitions [$name] : null;
721
    }
722
723
    /**
724
     * Add a fully configured transition to the machine.
725
     *
726
     * It is possible to add transition that have 'regex' states: states that
727
     * have a name in the format of 'regex:/<regex-here>/' or 'not-regex:/<regex-here>/'. 
728
     * When adding a transition with a regex state, it will be matched against all currently
729
     * known states if there is a match. If you just want to use regex transitions, 
730
     * it might be preferable to store some states via 'addState' first, so the 
731
     * 
732
     * Self transitions for regex states are disallowed by default
733
     * since you would probably only want to do that explicitly. Regex states
734
     * can be both the 'to' and the 'from' state of a transition.
735
     *
736
     * Transitions from a 'final' type of state are not allowed.
737
     *
738
     * the order in which transitions are added matters insofar that when a
739
     * StateMachine::run() is called, the first Transition for the current State
740
     * will be tried first.
741
     *
742
     * Since a transition has complete knowledge about it's states,
743
     * the addition of a transition will also trigger the adding of the
744
     * to and from state on this class.
745
     *
746
     * this method can also be used to add a Transition directly (instead of via
747
     * a loader). Make sure that transitions that share a common State use the same
748
     * instance of that State object and vice versa.
749
     *
750
     * @param Transition $transition  
751
     * @param boolean $allow_self_transition_by_regex optional: to allow regexes to set a self transition.
752
     * @return int a count of how many transitions were added. In case of a regex transition this might be
753
     *              multiple and in case a transition already exists it might be 0.
754
     */
755 43
    public function addTransition(Transition $transition, $allow_self_transition_by_regex = false)
756
    {
757 43
        $from = $transition->getStateFrom();
758 43
        $to = $transition->getStateTo();
759 43
        $all_states = $this->getStates();
760
        // check if we have regex states in the transition and get either the
761
        // original state back if it's not a regex, or all currently known
762
        // states that match the regex.
763 43
        $all_from = Utils::getAllRegexMatchingStates($from, $all_states);
764 43
        $all_to = Utils::getAllRegexMatchingStates($to, $all_states);
765 43
        $contains_regex = $from->isRegex() || $to->isRegex();
766 43
        $non_regex_transition = $transition;
767 43
        $count = 0;
768
        // loop trought all possible 'from' states
769 43
        foreach ($all_from as $from) {
770
            // loop through all possible 'to' states
771 43
            foreach ($all_to as $to) {
772 43
                if ($contains_regex && $from->getName() === $to->getName() && !$allow_self_transition_by_regex) {
773
                    // disallow self transition for regexes and from final states unless it is explicitely allowed.
774 6
                    continue;
775
                }
776 43
                if ($contains_regex) {
777
                    /*
778
                     * get a copy of the current transition that has a regex (so
779
                     * we have a non-regex state) and delegate to $transition to
780
                     * get that copy in case it's a subclass of transition and
781
                     * we need to copy specific fields
782
                     */
783 18
                    $non_regex_transition = $transition->getCopy($from, $to);
784 18
                }
785 43
                if ($this->addTransitionWithoutRegex($non_regex_transition)) {
786 43
                    $count++;
787 43
                }
788 43
            }
789 43
        }
790 43
        return $count;
791
    }
792
793
    /**
794
     * Add the transition, after it has previously been checked that is did not
795
     * contain states with a regex.
796
     *
797
     * @param Transition $transition   
798
     * @return boolean true in case it was added. false otherwise         
799
     */
800 43
    protected function addTransitionWithoutRegex(Transition $transition)
801
    {
802
        // don't allow transitions from a final state
803 43
        if ($transition->getStateFrom()->isFinal()) {
804 13
            return false;
805
        }
806
        
807
        // add the transition only if it already exists (no overwrites)
808 43
        if ($this->getTransition($transition->getName()) !== null) {
809 5
            return false;
810
        }
811 43
        $this->transitions [$transition->getName()] = $transition;
812
        
813 43
        $from = $transition->getStateFrom();
814
        // adds state only if it does not already exist (no overwrites)
815 43
        $this->addState($from);
816
        /* 
817
         * transitions create bidirectional references to the States
818
         * when they are made, but here the States that are set on the machine
819
         * can actually be different instances from different transitions (eg:
820
         * a->b and a->c => we now have two State instances of a)
821
         * we therefore need to merge the transitions on the existing states.
822
         * The LoaderArray class does this for us by default, but we do it here
823
         * too, just in case a client decides to call the 'addTransition' method
824
         * directly without a loader.
825
         */
826
        //get the state known to the machine, to prevent possible bug where client 
827
        //creates 2 different state instances with different configs. we won't know
828
        //how to resolve this anyway, so just pick the existing state (first in wins)
829 43
        $state = $this->getState($from->getName());
830
        // adds transition only if it does not already exist (no overwrites)
831 43
        $state->addTransition($transition);
832
        
833 43
        $to = $transition->getStateTo();
834
        // adds state only if it dooes not already exist (no overwrites)
835 43
        $this->addState($to);
836 43
        return true;
837
    }
838
839
    /**
840
     * Get the current context
841
     *
842
     * @return Context
843
     */
844 58
    public function getContext()
845
    {
846 58
        return $this->context;
847
    }
848
849
    /**
850
     * set the context on the statemachine and provide bidirectional
851
     * association.
852
     *
853
     * change the context for a statemachine that already has a context.
854
     * When the context is changed, but it is for the same statemachine (with
855
     * the same transitions), the statemachine can be used directly with the
856
     * new context.
857
     *
858
     * The current state is reset to whatever state the machine should be in
859
     * (either the initial state or the stored state) whenever a context change
860
     * is made.
861
     *
862
     * we can change context to:
863
     * - switch builders/persistence adapters at runtime
864
     * - reuse the statemachine for a different entity so we do not
865
     * have to load the statemachine with the same transition definitions
866
     *
867
     * @param Context $context            
868
     * @throws Exception
869
     */
870 58
    public function setContext(Context $context)
871
    {
872 58
        if ($this->getContext()) {
873
            // context already exists.
874 2
            if ($this->getContext()->getMachine() !== $context->getMachine()) {
875 1
                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);
876
            }
877
            
878
            // reset state TODO: move outside if statement
879 2
            $this->state = null;
880 2
        }
881 58
        $context->setStateMachine($this);
882 58
        $this->context = $context;
883 58
    }
884
    
885
    // #################### LOW LEVEL HELPER METHODS #########################
886
    
887
888
    /**
889
     * called whenever an exception occurs from inside 'performTransition()'
890
     * can be used for logging etc. in some sort of history structure in the persistence layer
891
     *
892
     * @param Transition $transition            
893
     * @param Exception $e            
894
     */
895 2
    protected function handleTransitionException(Transition $transition, Exception $e)
896
    {
897
        // override if necessary to log exceptions or to add some extra info
898
        // to the underlying storage facility (for example, an exception will
899
        // not lead to a transition, so this can be used to indicate a failed
900
        // transition in some sort of history structure)
901 2
        $this->getContext()->setFailedTransition($transition, $e);
902 2
    }
903
904
    /**
905
     * Always throws an izzum exception (converts a non-izzum exception to an
906
     * izzum exception)
907
     *
908
     * @param \Exception $e            
909
     * @param int $code
910
     *            if the exception is not of type Exception, wrap it and use
911
     *            this code.
912
     * @param Transition $transition
913
     *            optional. if set, we handle it as a transition exception too
914
     *            so it can be logged or handled
915
     * @throws Exception an izzum exception
916
     */
917 2
    protected function handlePossibleNonStatemachineException(\Exception $e, $code, $transition = null)
918
    {
919 2
        $e = Utils::wrapToStateMachineException($e, $code);
920 2
        if ($transition !== null) {
921 2
            $this->handleTransitionException($transition, $e);
922 2
        }
923 2
        throw $e;
924
    }
925
926
    /**
927
     * This method is used to trigger an event on the statemachine and
928
     * delegates the actuall call to the 'handle' method
929
     *
930
     * $statemachine->triggerAnEvent() actually calls
931
     * $this->handle('triggerAnEvent')
932
     *
933
     * This is also very useful if you use object composition to include a
934
     * statemachine in your domain model. The domain model itself can then use it's own
935
     * __call implementation to directly delegate to the statemachines' __call method.
936
     * $model->walk() will actually call $model->statemachine->walk() which will
937
     * then call $model->statemachine->handle('walk');
938
     *
939
     * since transition event names default to the transition name, it is
940
     * possible to execute this kind of code (if the state names contain allowed
941
     * characters):
942
     * $statemachine-><state_from>_to_<state_to>(); 
943
     * $statemachine->eventName();
944
     * $statemachine->eventName('this is an informational message about why we do this transition');
945
     *
946
     *
947
     * @param string $name
948
     *            the name of the unknown method called
949
     * @param array $arguments
950
     *            an array of arguments (if any).
951
     *            an argument could be $message (informational message for the transition)
952
     * @return bool true in case a transition was triggered by the event, false
953
     *         otherwise
954
     * @throws Exception in case the transition is not possible via the guard
955
     *         logic (Rule)
956
     * @link https://en.wikipedia.org/wiki/Object_composition
957
     */
958 8
    public function __call($name, $arguments)
959
    {
960
        //prepend the $name (event/trigger) to other arguments and call the 'handle' method
961 8
        array_unshift($arguments, $name);
962 8
        return call_user_func_array(array($this, 'handle'), $arguments);
963
    }
964
965
    /**
966
     * Helper method to generically call methods on the $object.
967
     * Try to call a method on the contextual entity / domain model ONLY IF the
968
     * method exists.Any arguments passed to this method will be passed on to the method
969
     * called on the entity.
970
     *
971
     * @param mixed $object
972
     *            the object on which we want to call the method
973
     * @param string $method
974
     *            the method to call on the object
975
     * @return boolean|mixed
976
     */
977 17
    protected function callEventHandler($object, $method)
978
    {
979
        // return true by default (because of transition guard callbacks that
980
        // might not exist)
981 17
        $output = true;
982
        // check if method exists.
983 17
        if (method_exists($object, $method)) { // && $object !== $this) { //prevent recursion
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
984 1
            $args = func_get_args();
985
            // remove $object and $method from $args so we only have the other arguments
986 1
            array_shift($args);
987 1
            array_shift($args);
988
            // have the methods be able to return what they like
989
            // but make sure the 'onCheckCanTransition method returns a boolean
990 1
            $output = call_user_func_array(array($object,$method), $args);
991 1
        }
992 17
        return $output;
993
    }
994
995
    /**
996
     * Helper method that gets the Transition object from a transition name or
997
     * throws an exception.
998
     *
999
     * @param string $name
1000
     *            convention: <state_from>_to_<state_to>
1001
     * @return Transition
1002
     * @throws Exception
1003
     */
1004 4
    private function getTransitionWithNullCheck($name)
1005
    {
1006 4
        $transition = $this->getTransition($name);
1007 4
        if ($transition === null) {
1008 1
            throw new Exception(sprintf("transition not found for '%s'", $name), Exception::SM_NO_TRANSITION_FOUND);
1009
        }
1010 4
        return $transition;
1011
    }
1012
1013 5
    public function toString($elaborate = false)
1014
    {
1015 5
        $output = get_class($this) . ": [" . $this->getContext()->getId(true) . "]";
1016 5
        if (!$elaborate) {
1017 5
            return $output;
1018
        } else {
1019 1
            $output . ' transitions: ' . count($this->getTransitions()) . ', states: ' . count($this->getStates()) . '.';
1020 1
            $output .= '[transitions]: ' . implode(",", $this->getTransitions());
1021 1
            $output .= '. [states]: ' . implode(",", $this->getStates()) . '.';
1022 1
            return $output;
1023
        }
1024
    }
1025
1026 1
    public function __toString()
1027
    {
1028 1
        return $this->toString(true);
1029
    }
1030
    
1031
    // ###################### HOOK METHODS #######################
1032
    
1033
1034
    /**
1035
     * hook method.
1036
     * override in subclass if necessary.
1037
     * Before a transition is checked to be possible, you can add domain
1038
     * specific logic here by overriding this method in a subclass.
1039
     * In an overriden implementation of this method you can stop the transition
1040
     * by returning false from this method.
1041
     *
1042
     * @param Transition $transition            
1043
     * @return boolean if false, the transition and it's associated logic will
1044
     *         not take place
1045
     */
1046 16
    protected function _onCheckCanTransition(Transition $transition)
0 ignored issues
show
Unused Code introduced by
The parameter $transition is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1047
    {
1048
        // eg: dispatch an event and see if it is rejected by a listener
1049 16
        return true;
1050
    }
1051
1052
    /**
1053
     * hook method.
1054
     * override in subclass if necessary.
1055
     * Called before each transition will run and execute the associated
1056
     * transition logic.
1057
     * A hook to implement in subclasses if necessary, to do stuff such as
1058
     * dispatching events, locking an entity, logging, begin transaction via
1059
     * persistence
1060
     * layer etc.
1061
     *
1062
     * @param Transition $transition            
1063
     */
1064 15
    protected function _onExitState(Transition $transition)
0 ignored issues
show
Unused Code introduced by
The parameter $transition is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1065 15
    {}
1066
1067
    /**
1068
     * hook method.
1069
     * override in subclass if necessary.
1070
     *
1071
     * @param Transition $transition            
1072
     */
1073 15
    protected function _onTransition(Transition $transition)
0 ignored issues
show
Unused Code introduced by
The parameter $transition is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1074 15
    {}
1075
1076
    /**
1077
     * hook method.
1078
     * override in subclass if necessary.
1079
     * Called after each transition has run and has executed the associated
1080
     * transition logic.
1081
     * a hook to implement in subclasses if necessary, to do stuff such as
1082
     * dispatching events, unlocking an entity, logging, cleanup, commit
1083
     * transaction via
1084
     * the persistence layer etc.
1085
     *
1086
     * @param Transition $transition            
1087
     */
1088 15
    protected function _onEnterState(Transition $transition)
0 ignored issues
show
Unused Code introduced by
The parameter $transition is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1089
    {}
1090
}