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.
Completed
Push — master ( 50cef7...02ca20 )
by Rolf
03:52
created

StateMachine::addTransition()   D

Complexity

Conditions 9
Paths 14

Size

Total Lines 37
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 9

Importance

Changes 12
Bugs 0 Features 1
Metric Value
c 12
b 0
f 1
dl 0
loc 37
ccs 22
cts 22
cp 1
rs 4.909
cc 9
eloc 18
nc 14
nop 2
crap 9
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 55
    public function __construct(Context $context)
228
    {
229
        // sets up bidirectional association
230 55
        $this->setContext($context);
231 55
    }
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
     * TRICKY: Be careful when using this function, since all guard logic must
296
     * be mutually exclusive! If not, you might end up performing the state
297
     * transition with priority n when you really want to perform transition
298
     * n+1.
299
     *
300
     * An alternative is to use the 'transition' method to target 1 transition
301
     * specifically:
302
     * $statemachine->transition('a_to_b');
303
     * So you are always sure that you are actually doing the intented
304
     * transition instead of relying on the configuration and guard logic (which
305
     * *might* not be correctly implemented, leading to transitions that would
306
     * normally not be executed).
307
     * 
308
     * @param string $message optional message. this can be used by the persistence adapter
309
     *          to be part of the transition history to provide extra information about the transition. 
310
     * @return boolean true if a transition was applied.
311
     * @throws Exception in case something went awfully wrong.
312
     *        
313
     */
314 8
    public function run($message = null)
315
    {
316
        try {
317 8
            $transitions = $this->getCurrentState()->getTransitions();
318 8
            foreach ($transitions as $transition) {
319 8
                $transitioned = $this->performTransition($transition, $message);
320 7
                if ($transitioned) {
321 7
                    return true;
322
                }
323 4
            }
324 5
        } catch(Exception $e) {
325
            // will be rethrown
326 1
            $this->handlePossibleNonStatemachineException($e, Exception::SM_RUN_FAILED);
327
        }
328
        // no transition done
329 4
        return false;
330
    }
331
332
    /**
333
     * run a statemachine until it cannot run any transition in the current
334
     * state or until it is in a final state.
335
     *
336
     * when using cyclic graphs, you could get into an infinite loop between
337
     * states. design your machine correctly.
338
     * @param string $message optional message. this can be used by the persistence adapter
339
     *          to be part of the transition history to provide extra information about the transition. 
340
     * @return int the number of sucessful transitions made.
341
     * @throws Exception in case something went badly wrong.
342
     */
343 4
    public function runToCompletion($message = null)
344
    {
345 4
        $transitions = 0;
346
        try {
347 4
            $run = true;
348 4
            while ( $run ) {
349
                // run the first transition possible
350 4
                $run = $this->run($message);
351 3
                if ($run) {
352 3
                    $transitions++;
353 3
                }
354 3
            }
355 4
        } catch(Exception $e) {
356 1
            $this->handlePossibleNonStatemachineException($e, $e->getCode());
357
        }
358 3
        return $transitions;
359
    }
360
361
    /**
362
     * Check if an existing transition on the curent state is allowed by the
363
     * guard logic.
364
     *
365
     * @param string $transition_name
366
     *            convention: <state-from>_to_<state-to>
367
     * @return boolean
368
     */
369 4
    public function canTransition($transition_name)
370
    {
371 4
        $transition = $this->getTransition($transition_name);
372 4
        return $transition === null ? false : $this->doCheckCanTransition($transition);
373
    }
374
375
    /**
376
     * checks if one or more transitions are possible and/or allowed for the
377
     * current state when triggered by an event
378
     *
379
     * @param string $event            
380
     * @return boolean false if no transitions possible or existing
381
     */
382 3
    public function canHandle($event)
383
    {
384 3
        $transitions = $this->getCurrentState()->getTransitionsTriggeredByEvent($event);
385 3
        foreach ($transitions as $transition) {
386 3
            if ($this->doCheckCanTransition($transition)) {
387 2
                return true;
388
            }
389 3
        }
390 2
        return false;
391
    }
392
393
    /**
394
     * check if the current state has one or more transitions that can be
395
     * triggered by an event
396
     *
397
     * @param string $event            
398
     * @return boolean
399
     */
400 3
    public function hasEvent($event)
401
    {
402 3
        $transitions = $this->getCurrentState()->getTransitionsTriggeredByEvent($event);
403 3
        if (count($transitions) > 0) {
404 3
            return true;
405
        }
406 1
        return false;
407
    }
408
    
409
    // ######################## CORE TRANSITION & TEMPLATE METHODS #############################
410
    
411
412
    /**
413
     * Perform a transition by specifiying the transitions' name from a state
414
     * that the transition is allowed to run.
415
     *
416
     * @param Transition $transition            
417
     * @param string $message optional message. this can be used by the persistence adapter
418
     *          to be part of the transition history to provide extra information about the transition.  
419
     * @return boolean true if the transition was succesful
420
     * @throws Exception in case something went horribly wrong
421
     * @link https://en.wikipedia.org/wiki/Template_method_pattern
422
     */
423 17
    private function performTransition(Transition $transition, $message = null)
424
    {
425
        // every method in this core routine has hook methods, event handlers and
426
        // callbacks it can call during the execution phase of the
427
        // transition steps if they are available on the domain model.
428
        try {
429
            
430 17
            if (!$this->doCheckCanTransition($transition)) {
431
                // one of the guards returned false or transition not found on current state.
432 4
                return false;
433
            }
434
            
435
            // state exit action: performed when exiting the state
436 16
            $this->doExitState($transition);
437
            // the transition is performed, with the associated logic
438 16
            $this->doTransition($transition, $message);
439
            // state entry action: performed when entering the state
440 16
            $this->doEnterState($transition);
441 17
        } catch(Exception $e) {
442 2
            $this->handlePossibleNonStatemachineException($e, Exception::SM_TRANSITION_FAILED, $transition);
443
        }
444 16
        return true;
445
    }
446
447
    /**
448
     * Template method to call a hook and to call a possible method
449
     * defined on the domain object/contextual entity
450
     *
451
     * @param Transition $transition            
452
     * @return boolean if false, the transition and its' associated logic will
453
     *         not take place
454
     * @throws Exception in case something went horribly wrong
455
     */
456 17
    private function doCheckCanTransition(Transition $transition)
457
    {
458
        try {
459
            // check if we have this transition on the current state.
460 17
            if (!$this->getCurrentState()->hasTransition($transition->getName())) {
461 3
                return false;
462
            }
463
            
464
            // possible hook so your application can place an extra guard on the
465
            // transition. possible entry~ or exit state type of checks can also
466
            // take place in this hook.
467 17
            if (!$this->_onCheckCanTransition($transition)) {
468 1
                return false;
469
            }
470
            // an event handler that is possibly defined on the domain model: onCheckCanTransition
471 17
            if (!$this->callEventHandler($this->getContext()->getEntity(), 'onCheckCanTransition', $this->getContext()->getIdentifier(), $transition)) {
472 1
                return false;
473
            }
474
            
475
            // this will check the guards defined for the transition: Rule and callable
476 17
            if (!$transition->can($this->getContext())) {
477 4
                return false;
478
            }
479 17
        } catch(Exception $e) {
480 2
            $this->handlePossibleNonStatemachineException($e, Exception::SM_CAN_FAILED);
481
        }
482
        // if we come here, this acts as a green light to start the transition.
483 16
        return true;
484
    }
485
486
    /**
487
     * the exit state action method
488
     *
489
     * @param Transition $transition            
490
     */
491 16
    private function doExitState(Transition $transition)
492
    {
493
        // hook for subclasses to implement
494 16
        $this->_onExitState($transition);
495
        // an event handler that is possibly defined on the domain model: onExitState
496 16
        $this->callEventHandler($this->getContext()->getEntity(), 'onExitState', $this->getContext()->getIdentifier(), $transition);
497
        // executes the command and callable associated with the state object
498 16
        $transition->getStateFrom()->exitAction($this->getContext());
499 16
    }
500
501
    /**
502
     * the transition action method
503
     *
504
     * @param Transition $transition            
505
     * @param string $message optional message. this can be used by the persistence adapter
506
     *          to be part of the transition history to provide extra information about the transition.          
507
     */
508 16
    private function doTransition(Transition $transition, $message = null)
509
    {
510
        // hook for subclasses to implement
511 16
        $this->_onTransition($transition);
512 16
        $entity = $this->getContext()->getEntity();
513 16
        $identifier = $this->getContext()->getIdentifier();
514
        // an event handler that is possibly defined on the domain model: onTransition
515 16
        $this->callEventHandler($entity, 'onTransition', $identifier, $transition);
516
        // executes the command and callable associated with the transition object
517 16
        $transition->process($this->getContext());
518
        
519
        // this actually sets the state so we are now in the new state
520 16
        $this->setState($transition->getStateTo(), $message);
521 16
    }
522
523
    /**
524
     * the enter state action method
525
     *
526
     * @param Transition $transition            
527
     */
528 16
    private function doEnterState(Transition $transition)
529
    {
530
        // an event handler that is possibly defined on the domain model: onEnterState
531 16
        $this->callEventHandler($this->getContext()->getEntity(), 'onEnterState', $this->getContext()->getIdentifier(), $transition);
532
        
533
        // executes the command and callable associated with the state object
534 16
        $transition->getStateTo()->entryAction($this->getContext());
535
        
536
        // hook for subclasses to implement
537 16
        $this->_onEnterState($transition);
538 16
    }
539
    
540
    // ######################## SUPPORTING METHODS #############################
541
    /**
542
     * All known/loaded states for this statemachine
543
     *
544
     * @return State[]
545
     */
546 42
    public function getStates()
547
    {
548 42
        return $this->states;
549
    }
550
551
    /**
552
     * get a state by name.
553
     *
554
     * @param string $name            
555
     * @return State or null if not found
556
     */
557 41
    public function getState($name)
558
    {
559 41
        return isset($this->states [$name]) ? $this->states [$name] : null;
560
    }
561
562
    /**
563
     * Add a state. without a transition.
564
     * Normally, you would not use this method directly but instead use
565
     * addTransition to add the transitions with the states in one go.
566
     * 
567
     * This method makes sense if you would want to load the statemachine with states
568
     * and not transitions, and then add the transitions with regex states.
569
     * This saves you the hassle of adding transitions before you add the
570
     * regex transitions (just so the states are already known on the machine).
571
     * 
572
     * in case a State is not used in a Transition, it will be orphaned and not 
573
     * reachable via other states.
574
     *
575
     * @param State $state 
576
     * @return boolean true if the state was not know to the machine or wasn't added, false otherwise.           
577
     */
578 41
    public function addState(State $state)
579
    {
580
        //no regex states
581 41
        if ($state->isRegex()) {
582 2
            return false;
583
        }
584
        //check for duplicates
585 41
        if (isset($this->states [$state->getName()])) {
586 38
            return false;
587
        }
588 41
        $this->states [$state->getName()] = $state;
589 41
        return true;
590
    }
591
592
    /**
593
     * sets the state as the current state and on the backend.
594
     * This should only be done:
595
     * - initially, right after a machine has been created, to set it in a
596
     * certain state if the state has not been persisted before.
597
     * - when changing context (since this resets the current state) via
598
     * $machine->setState($machine->getCurrentState())
599
     *
600
     * This method allows you to bypass the transition guards and the transition
601
     * logic. no exit/entry/transition logic will be performed
602
     *
603
     * @param State $state 
604
     * @param string $message optional message. this can be used by the persistence adapter
605
     *          to be part of the transition history to provide extra information about the transition.  
606
     * @throws Exception in case the state is not valid/known for this machine          
607
     */
608 16
    public function setState(State $state, $message = null)
609
    {
610
        
611 16
        if($this->getState($state->getName()) === null) {
612 1
            throw new Exception(sprintf("%s state '%s' not known to this machine", $this->toString(), $state->getName()), Exception::SM_UNKNOWN_STATE);
613
        }
614
        //get the state known to this machine so we are sure we have the correct reference
615
        //even if the client provides another instance of State with the same name
616 16
        $state = $this->getState($state->getName());
617 16
        $this->getContext()->setState($state->getName(), $message);
618 16
        $this->state = $state;
619 16
    }
620
621
    /**
622
     * add state information to the persistence layer if it is not there.
623
     * 
624
     * Used to mark the initial construction of a statemachine at a certain
625
     * point in time. This method only makes sense the first time a statemachine
626
     * is initialized and used since it will do nothing once a transition has been made.
627
     * 
628
     * It will set the initial state on the backend which sets
629
     * the construction time.
630
     * 
631
     * Make sure that the transitions and states are loaded before you call this method.
632
     * in other words: the machine should be ready to go.
633
     * 
634
     * @param string $message optional message. this can be used by the persistence adapter
635
     *          to provide extra information in the history of the machine transitions,
636
     *          in this case, about the first adding of this machine to the persistence layer. 
637
     * @return boolean true if not persisted before, false otherwise
638
     */
639 5
    public function add($message = null)
640
    {
641 5
        return $this->getContext()->add($this->getInitialState()->getName(), $message);
642
    }
643
644
    /**
645
     * gets the current state (or retrieve it from the backend if not set).
646
     *
647
     * the state will be:
648
     * - the state that was explicitely set via setState
649
     * - the state we have moved to after the last transition
650
     * - the initial state. if we haven't had a transition yet and no current
651
     * state has been set, the initial state will be retrieved (the state with State::TYPE_INITIAL)
652
     *
653
     * @return State
654
     * @throws Exception in case there is no valid current state found
655
     */
656 18
    public function getCurrentState()
657
    {
658
        // do we have a current state?
659 18
        if ($this->state) {
660 17
            return $this->state;
661
        }
662
        // retrieve state from the context if we do not find any state set.
663 18
        $state = $this->getState($this->getContext()->getState());
664 18
        if (!$state || $state == State::STATE_UNKNOWN) {
665
            // possible wrong configuration
666 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);
667
        }
668
        // state is found, set it and return
669 17
        $this->state = $state;
670 17
        return $this->state;
671
    }
672
673
    /**
674
     * Get the initial state, the only state with type State::TYPE_INITIAL
675
     *
676
     * This method can be used to 'add' the state information to the backend via
677
     * the context/persistence adapter when a machine has been initialized for
678
     * the first time.
679
     *
680
     * @param boolean $allow_null optional
681
     * @return State (or null or Exception, only when statemachine is improperly
682
     *         loaded)
683
     * @throws Exception if $allow_null is false an no inital state was found
684
     */
685 22
    public function getInitialState($allow_null = false)
686
    {
687 22
        $states = $this->getStates();
688 22
        foreach ($states as $state) {
689 21
            if ($state->isInitial()) {
690 21
                return $state;
691
            }
692 4
        }
693 3
        if ($allow_null) {
694 3
            return null;
695
        }
696 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);
697
    }
698
699
    /**
700
     * All known/loaded transitions for this statemachine
701
     *
702
     * @return Transition[]
703
     */
704 19
    public function getTransitions()
705
    {
706 19
        return $this->transitions;
707
    }
708
709
    /**
710
     * get a transition by name.
711
     *
712
     * @param string $name
713
     *            convention: <state_from>_to_<state_to>
714
     * @return Transition or null if not found
715
     */
716 40
    public function getTransition($name)
717
    {
718 40
        return isset($this->transitions [$name]) ? $this->transitions [$name] : null;
719
    }
720
721
    /**
722
     * Add a fully configured transition to the machine.
723
     *
724
     * It is possible to add transition that have 'regex' states: states that
725
     * have a name in the format of 'regex:/<regex-here>/' or 'not-regex:/<regex-here>/'. 
726
     * When adding a transition with a regex state, it will be matched against all currently
727
     * known states if there is a match. If you just want to use regex transitions, 
728
     * it might be preferable to store some states via 'addState' first, so the 
729
     * 
730
     * Self transitions for regex states are disallowed by default
731
     * since you would probably only want to do that explicitly. Regex states
732
     * can be both the 'to' and the 'from' state of a transition.
733
     *
734
     * Transitions from a 'final' type of state are not allowed.
735
     *
736
     * the order in which transitions are added matters insofar that when a
737
     * StateMachine::run() is called, the first Transition for the current State
738
     * will be tried first.
739
     *
740
     * Since a transition has complete knowledge about it's states,
741
     * the addition of a transition will also trigger the adding of the
742
     * to and from state on this class.
743
     *
744
     * this method can also be used to add a Transition directly (instead of via
745
     * a loader). Make sure that transitions that share a common State use the same
746
     * instance of that State object and vice versa.
747
     *
748
     * @param Transition $transition  
749
     * @param boolean $allow_self_transition_by_regex optional: to allow regexes to set a self transition.
750
     * @return int a count of how many transitions were added. In case of a regex transition this might be
751
     *              multiple and in case a transition already exists it might be 0.
752
     */
753 40
    public function addTransition(Transition $transition, $allow_self_transition_by_regex = false)
754
    {
755 40
        $from = $transition->getStateFrom();
756 40
        $to = $transition->getStateTo();
757 40
        $all_states = $this->getStates();
758
        // check if we have regex states in the transition and get either the
759
        // original state back if it's not a regex, or all currently known
760
        // states that match the regex.
761 40
        $all_from = Utils::getAllRegexMatchingStates($from, $all_states);
762 40
        $all_to = Utils::getAllRegexMatchingStates($to, $all_states);
763 40
        $contains_regex = $from->isRegex() || $to->isRegex();
764 40
        $non_regex_transition = $transition;
765 40
        $count = 0;
766
        // loop trought all possible 'from' states
767 40
        foreach ($all_from as $from) {
768
            // loop through all possible 'to' states
769 40
            foreach ($all_to as $to) {
770 40
                if ($contains_regex && $from->getName() === $to->getName() && !$allow_self_transition_by_regex) {
771
                    // disallow self transition for regexes and from final states unless it is explicitely allowed.
772 6
                    continue;
773
                }
774 40
                if ($contains_regex) {
775
                    /*
776
                     * get a copy of the current transition that has a regex (so
777
                     * we have a non-regex state) and delegate to $transition to
778
                     * get that copy in case it's a subclass of transition and
779
                     * we need to copy specific fields
780
                     */
781 18
                    $non_regex_transition = $transition->getCopy($from, $to);
782 18
                }
783 40
                if ($this->addTransitionWithoutRegex($non_regex_transition)) {
784 40
                    $count++;
785 40
                }
786 40
            }
787 40
        }
788 40
        return $count;
789
    }
790
791
    /**
792
     * Add the transition, after it has previously been checked that is did not
793
     * contain states with a regex.
794
     *
795
     * @param Transition $transition   
796
     * @return boolean true in case it was added. false otherwise         
797
     */
798 40
    protected function addTransitionWithoutRegex(Transition $transition)
799
    {
800
        // don't allow transitions from a final state
801 40
        if ($transition->getStateFrom()->isFinal()) {
802 13
            return false;
803
        }
804
        
805
        // add the transition only if it already exists (no overwrites)
806 40
        if ($this->getTransition($transition->getName()) !== null) {
807 5
            return false;
808
        }
809 40
        $this->transitions [$transition->getName()] = $transition;
810
        
811 40
        $from = $transition->getStateFrom();
812
        // adds state only if it does not already exist (no overwrites)
813 40
        $this->addState($from);
814
        /* 
815
         * transitions create bidirectional references to the States
816
         * when they are made, but here the States that are set on the machine
817
         * can actually be different instances from different transitions (eg:
818
         * a->b and a->c => we now have two State instances of a)
819
         * we therefore need to merge the transitions on the existing states.
820
         * The LoaderArray class does this for us by default, but we do it here
821
         * too, just in case a client decides to call the 'addTransition' method
822
         * directly without a loader.
823
         */
824
        //get the state known to the machine, to prevent possible bug where client 
825
        //creates 2 different state instances with different configs. we won't know
826
        //how to resolve this anyway, so just pick the existing state (first in wins)
827 40
        $state = $this->getState($from->getName());
828
        // adds transition only if it does not already exist (no overwrites)
829 40
        $state->addTransition($transition);
830
        
831 40
        $to = $transition->getStateTo();
832
        // adds state only if it dooes not already exist (no overwrites)
833 40
        $this->addState($to);
834 40
        return true;
835
    }
836
837
    /**
838
     * Get the current context
839
     *
840
     * @return Context
841
     */
842 55
    public function getContext()
843
    {
844 55
        return $this->context;
845
    }
846
847
    /**
848
     * set the context on the statemachine and provide bidirectional
849
     * association.
850
     *
851
     * change the context for a statemachine that already has a context.
852
     * When the context is changed, but it is for the same statemachine (with
853
     * the same transitions), the statemachine can be used directly with the
854
     * new context.
855
     *
856
     * The current state is reset to whatever state the machine should be in
857
     * (either the initial state or the stored state) whenever a context change
858
     * is made.
859
     *
860
     * we can change context to:
861
     * - switch builders/persistence adapters at runtime
862
     * - reuse the statemachine for a different entity so we do not
863
     * have to load the statemachine with the same transition definitions
864
     *
865
     * @param Context $context            
866
     * @throws Exception
867
     */
868 55
    public function setContext(Context $context)
869
    {
870 55
        if ($this->getContext()) {
871
            // context already exists.
872 2
            if ($this->getContext()->getMachine() !== $context->getMachine()) {
873 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);
874
            }
875
            
876
            // reset state TODO: move outside if statement
877 2
            $this->state = null;
878 2
        }
879 55
        $context->setStateMachine($this);
880 55
        $this->context = $context;
881 55
    }
882
    
883
    // #################### LOW LEVEL HELPER METHODS #########################
884
    
885
886
    /**
887
     * called whenever an exception occurs from inside 'performTransition()'
888
     * can be used for logging etc. in some sort of history structure in the persistence layer
889
     *
890
     * @param Transition $transition            
891
     * @param Exception $e            
892
     */
893 2
    protected function handleTransitionException(Transition $transition, Exception $e)
894
    {
895
        // override if necessary to log exceptions or to add some extra info
896
        // to the underlying storage facility (for example, an exception will
897
        // not lead to a transition, so this can be used to indicate a failed
898
        // transition in some sort of history structure)
899 2
        $this->getContext()->setFailedTransition($transition, $e);
900 2
    }
901
902
    /**
903
     * Always throws an izzum exception (converts a non-izzum exception to an
904
     * izzum exception)
905
     *
906
     * @param \Exception $e            
907
     * @param int $code
908
     *            if the exception is not of type Exception, wrap it and use
909
     *            this code.
910
     * @param Transition $transition
911
     *            optional. if set, we handle it as a transition exception too
912
     *            so it can be logged or handled
913
     * @throws Exception an izzum exception
914
     */
915 2
    protected function handlePossibleNonStatemachineException(\Exception $e, $code, $transition = null)
916
    {
917 2
        $e = Utils::wrapToStateMachineException($e, $code);
918 2
        if ($transition !== null) {
919 2
            $this->handleTransitionException($transition, $e);
920 2
        }
921 2
        throw $e;
922
    }
923
924
    /**
925
     * This method is used to trigger an event on the statemachine and
926
     * delegates the actuall call to the 'handle' method
927
     *
928
     * $statemachine->triggerAnEvent() actually calls
929
     * $this->handle('triggerAnEvent')
930
     *
931
     * This is also very useful if you use object composition to include a
932
     * statemachine in your domain model. The domain model itself can then use it's own
933
     * __call implementation to directly delegate to the statemachines' __call method.
934
     * $model->walk() will actually call $model->statemachine->walk() which will
935
     * then call $model->statemachine->handle('walk');
936
     *
937
     * since transition event names default to the transition name, it is
938
     * possible to execute this kind of code (if the state names contain allowed
939
     * characters):
940
     * $statemachine-><state_from>_to_<state_to>(); 
941
     * $statemachine->eventName();
942
     * $statemachine->eventName('this is an informational message about why we do this transition');
943
     *
944
     *
945
     * @param string $name
946
     *            the name of the unknown method called
947
     * @param array $arguments
948
     *            an array of arguments (if any).
949
     *            an argument could be $message (informational message for the transition)
950
     * @return bool true in case a transition was triggered by the event, false
951
     *         otherwise
952
     * @throws Exception in case the transition is not possible via the guard
953
     *         logic (Rule)
954
     * @link https://en.wikipedia.org/wiki/Object_composition
955
     */
956 8
    public function __call($name, $arguments)
957
    {
958
        //prepend the $name (event/trigger) to other arguments and call the 'handle' method
959 8
        array_unshift($arguments, $name);
960 8
        return call_user_func_array(array($this, 'handle'), $arguments);
961
    }
962
963
    /**
964
     * Helper method to generically call methods on the $object.
965
     * Try to call a method on the contextual entity / domain model ONLY IF the
966
     * method exists.Any arguments passed to this method will be passed on to the method
967
     * called on the entity.
968
     *
969
     * @param mixed $object
970
     *            the object on which we want to call the method
971
     * @param string $method
972
     *            the method to call on the object
973
     * @return boolean|mixed
974
     */
975 17
    protected function callEventHandler($object, $method)
976
    {
977
        // return true by default (because of transition guard callbacks that
978
        // might not exist)
979 17
        $output = true;
980
        // check if method exists.
981 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...
982 1
            $args = func_get_args();
983
            // remove $object and $method from $args so we only have the other arguments
984 1
            array_shift($args);
985 1
            array_shift($args);
986
            // have the methods be able to return what they like
987
            // but make sure the 'onCheckCanTransition method returns a boolean
988 1
            $output = call_user_func_array(array($object,$method), $args);
989 1
        }
990 17
        return $output;
991
    }
992
993
    /**
994
     * Helper method that gets the Transition object from a transition name or
995
     * throws an exception.
996
     *
997
     * @param string $name
998
     *            convention: <state_from>_to_<state_to>
999
     * @return Transition
1000
     * @throws Exception
1001
     */
1002 4
    private function getTransitionWithNullCheck($name)
1003
    {
1004 4
        $transition = $this->getTransition($name);
1005 4
        if ($transition === null) {
1006 1
            throw new Exception(sprintf("transition not found for '%s'", $name), Exception::SM_NO_TRANSITION_FOUND);
1007
        }
1008 4
        return $transition;
1009
    }
1010
1011 5
    public function toString($elaborate = false)
1012
    {
1013 5
        $output = get_class($this) . ": [" . $this->getContext()->getId(true) . "]";
1014 5
        if (!$elaborate) {
1015 5
            return $output;
1016
        } else {
1017 1
            $output . ' transitions: ' . count($this->getTransitions()) . ', states: ' . count($this->getStates()) . '.';
1018 1
            $output .= '[transitions]: ' . implode(",", $this->getTransitions());
1019 1
            $output .= '. [states]: ' . implode(",", $this->getStates()) . '.';
1020 1
            return $output;
1021
        }
1022
    }
1023
1024 1
    public function __toString()
1025
    {
1026 1
        return $this->toString(true);
1027
    }
1028
    
1029
    // ###################### HOOK METHODS #######################
1030
    
1031
1032
    /**
1033
     * hook method.
1034
     * override in subclass if necessary.
1035
     * Before a transition is checked to be possible, you can add domain
1036
     * specific logic here by overriding this method in a subclass.
1037
     * In an overriden implementation of this method you can stop the transition
1038
     * by returning false from this method.
1039
     *
1040
     * @param Transition $transition            
1041
     * @return boolean if false, the transition and it's associated logic will
1042
     *         not take place
1043
     */
1044 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...
1045
    {
1046
        // eg: dispatch an event and see if it is rejected by a listener
1047 16
        return true;
1048
    }
1049
1050
    /**
1051
     * hook method.
1052
     * override in subclass if necessary.
1053
     * Called before each transition will run and execute the associated
1054
     * transition logic.
1055
     * A hook to implement in subclasses if necessary, to do stuff such as
1056
     * dispatching events, locking an entity, logging, begin transaction via
1057
     * persistence
1058
     * layer etc.
1059
     *
1060
     * @param Transition $transition            
1061
     */
1062 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...
1063 15
    {}
1064
1065
    /**
1066
     * hook method.
1067
     * override in subclass if necessary.
1068
     *
1069
     * @param Transition $transition            
1070
     */
1071 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...
1072 15
    {}
1073
1074
    /**
1075
     * hook method.
1076
     * override in subclass if necessary.
1077
     * Called after each transition has run and has executed the associated
1078
     * transition logic.
1079
     * a hook to implement in subclasses if necessary, to do stuff such as
1080
     * dispatching events, unlocking an entity, logging, cleanup, commit
1081
     * transaction via
1082
     * the persistence layer etc.
1083
     *
1084
     * @param Transition $transition            
1085
     */
1086 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...
1087
    {}
1088
}