Passed
Push — master ( b8b5dc...ebd7e1 )
by Javier
03:09
created

StateMachine   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Test Coverage

Coverage 98.94%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 83
dl 0
loc 185
ccs 93
cts 94
cp 0.9894
rs 9.76
c 2
b 0
f 0
wmc 33

17 Methods

Rating   Name   Duplication   Size   Complexity  
A addState() 0 9 1
A addTransition() 0 13 1
A __construct() 0 14 6
A addCommonTransition() 0 11 2
A fireEvent() 0 38 5
A getMachineToArray() 0 3 1
A eventMustExistOrFail() 0 4 2
A argumentIsNotNullOrFail() 0 4 2
A argumentIsValidOrFail() 0 4 1
A setCurrentStateIfThisIsInitialState() 0 4 2
A can() 0 9 2
A getCurrentState() 0 3 1
A stateMustExistOrFail() 0 4 2
A argumentIsNotBlankOrFail() 0 4 2
A getNextState() 0 3 1
A getCurrentEvent() 0 3 1
A cancelTransition() 0 3 1
1
<?php
2
3
namespace jagarsoft\StateMachine;
4
5
use jagarsoft\StateMachine\Stubs\StateMachineBuilder;
6
7
class StateMachine {
8
    private const NEXT_STATE = 0;
9
    private const EXEC_ACTION = 1;
10
    
11
    protected $sm = [];
12
    protected $currentState = null;
13
    protected $currentEvent = null;
14
    protected $nextState = null;
15
16
    private $cancelTransition = false;
17
    private $transitionInProgress = false;
18
    private $eventsQueued = [];
19
20 27
    public function __construct(StateMachineBuilder $smb = null)
21
    {
22 27
        if(  $smb != null )
23 1
            $sm = $smb->__invoke();
24
25 27
        if( ! empty($sm) ){
26
            // StateEnum::CURRENT_STATE => [ EventEnum::ON_EVENT => [ StateEnum::NEXT_STATE_2, ActionClosureOrFunction ]  ],
27 1
            foreach ($sm as $state => $transition){
28 1
                $this->addState($state);
29 1
                foreach ($transition as $onEvent => $nextStateAndAction) {
30 1
                    if( array_key_exists(self::EXEC_ACTION, $nextStateAndAction) ) {
31
                        $this->addTransition($state, $onEvent, $nextStateAndAction[self::NEXT_STATE], $nextStateAndAction[self::EXEC_ACTION]);
32
                    } else {
33 1
                        $this->addTransition($state, $onEvent, $nextStateAndAction[self::NEXT_STATE]);
34
                    }
35
                }
36
            }
37
        }
38 27
    }
39
40 19
    public function addState($state)
41
    {
42 19
        $this->argumentIsValidOrFail($state);
43
44 17
        $this->setCurrentStateIfThisIsInitialState($state);
45
46 17
        $this->sm[$state] = [];
47
48 17
        return $this;
49
    }
50
51 19
    public function addTransition($currentState, $currentEvent, $nextState, \Closure $execAction = null )
52
    {
53 19
        $this->argumentIsValidOrFail($currentState);
54 19
        $this->argumentIsValidOrFail($currentEvent);
55 19
        $this->argumentIsValidOrFail($nextState);
56
57 19
        $this->setCurrentStateIfThisIsInitialState($currentState);
58
59 19
        $this->sm[$currentState][$currentEvent] = [
60 19
                                                self::NEXT_STATE => $nextState,
61 19
                                                self::EXEC_ACTION => $execAction
62
                                            ];
63 19
        return $this;
64
    }
65
66 4
    public function addCommonTransition($currentEvent, $nextState, \Closure $execAction = null )
67
    {
68 4
        $this->argumentIsValidOrFail($currentEvent);
69 4
        $this->argumentIsValidOrFail($nextState);
70
71 4
        $states = array_keys($this->sm);
72 4
        foreach ($states as $state) {
73 4
            $this->addTransition($state, $currentEvent, $nextState, $execAction);
74
        }
75
76 4
        return $this;
77
    }
78
79
    /**
80
     * @param $event
81
     */
82 15
    public function fireEvent($event)
83
    {
84 15
        $this->argumentIsValidOrFail($event);
85
86 15
        if( $this->transitionInProgress ){
87 2
            array_push($this->eventsQueued, $event);
88 2
            return $this;
89
        }
90 15
        $this->transitionInProgress = true;
91
92 15
        $this->eventMustExistOrFail($event);
93
94 14
        $transition = $this->sm[$this->currentState][$event];
95
96 14
        $this->nextState = $transition[self::NEXT_STATE];
97
98 14
        $this->stateMustExistOrFail($this->nextState);
99
100 13
        $this->currentEvent = $event;
101
102 13
        $action = $transition[self::EXEC_ACTION];
103 13
        if( $action ){
104 11
            ($action)($this);
105
        }
106
107 13
        if( $this->cancelTransition ){
108 5
            $this->cancelTransition = false;
109
        } else {
110 9
            $this->currentState = $transition[self::NEXT_STATE];
111
        }
112
113 13
        $this->transitionInProgress = false;
114 13
        $event = array_shift($this->eventsQueued);
115 13
        if(  $event != null ){
116 2
            $this->fireEvent($event);
117
        }
118
119 13
        return $this;
120
    }
121
122 1
    public function can($event)
123
    {
124
        try{
125 1
            $this->eventMustExistOrFail($event);
126 1
            $can = true;
127 1
        } catch(\InvalidArgumentException $e){
128 1
            $can = false;
129
        }
130 1
        return $can;
131
    }
132
133 5
    public function cancelTransition()
134
    {
135 5
        $this->cancelTransition = true;
136 5
    }
137
138 9
    public function getCurrentState()
139
    {
140 9
        return $this->currentState;
141
    }
142
143 4
    public function getCurrentEvent()
144
    {
145 4
        return $this->currentEvent;
146
    }
147
148 1
    public function getNextState()
149
    {
150 1
        return $this->nextState;
151
    }
152
153 1
    public function getMachineToArray()
154
    {
155 1
        return $this->sm;
156
    }
157
158 26
    private function argumentIsValidOrFail($arg): void
159
    {
160 26
        $this->argumentIsNotNullOrFail($arg);
161 25
        $this->argumentIsNotBlankOrFail($arg);
162 24
    }
163
164 26
    private function argumentIsNotNullOrFail($arg): void
165
    {
166 26
        if( $arg === null )
167 4
            throw new \InvalidArgumentException("Null is not an valid argument");
168 25
    }
169
170 25
    private function argumentIsNotBlankOrFail($arg): void
171
    {
172 25
        if( trim($arg) === "" )
173 2
            throw new \InvalidArgumentException("Blank is not an valid argument");
174 24
    }
175
176 16
    private function eventMustExistOrFail($event)
177
    {
178 16
        if( !( isset($this->sm[$this->currentState][$event]) ) )
179 2
            throw new \InvalidArgumentException("Unexpected event {$event} on {$this->currentState} state");
180 15
    }
181
182 14
    private function stateMustExistOrFail($state)
183
    {
184 14
        if( ! isset($this->sm[$state]) )
185 1
            throw new \InvalidArgumentException("Event '{$this->currentEvent}' fired an unexpected '{$state}' state");
186 13
    }
187
188 24
    private function setCurrentStateIfThisIsInitialState($state): void
189
    {
190 24
        if( $this->currentState == null){
191 24
            $this->currentState = $state;
192
        }
193 24
    }
194
}
195