Completed
Pull Request — master (#20)
by Oliver
02:42
created

Statemachine::getLastState()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Metabor\Statemachine;
4
5
use Metabor\Callback\Callback;
6
use Metabor\Event\Dispatcher;
7
use Metabor\Observer\Subject;
8
use Metabor\Semaphore\NullMutex;
9
use Metabor\Statemachine\Exception\LockCanNotBeAcquiredException;
10
use Metabor\Statemachine\Exception\WrongEventForStateException;
11
use Metabor\Statemachine\Factory\TransitionSelector\OneOrNoneActiveTransition;
12
use Metabor\Statemachine\Transition\ActiveTransitionFilter;
13
use MetaborStd\Event\DispatcherInterface;
14
use MetaborStd\Event\EventInterface;
15
use MetaborStd\NamedInterface;
16
use MetaborStd\Semaphore\MutexInterface;
17
use MetaborStd\Statemachine\Factory\TransitionSelectorInterface;
18
use MetaborStd\Statemachine\ProcessInterface;
19
use MetaborStd\Statemachine\StateInterface;
20
use MetaborStd\Statemachine\StatemachineInterface;
21
use MetaborStd\Statemachine\TransitionInterface;
22
23
/**
24
 * @author Oliver Tischlinger
25
 */
26
class Statemachine extends Subject implements StatemachineInterface
27
{
28
    /**
29
     * @var object
30
     */
31
    private $subject;
32
33
    /**
34
     * @var StateInterface
35
     */
36
    private $currentState;
37
38
    /**
39
     * @var StateInterface
40
     */
41
    private $lastState;
42
43
    /**
44
     * @var DispatcherInterface
45
     */
46
    private $dispatcher;
47
48
    /**
49
     * @var EventInterface
50
     */
51
    private $currentEvent;
52
53
    /**
54
     * @var \ArrayAccess
55
     */
56
    private $currentContext;
57
58
    /**
59
     * @var TransitionSelectorInterface
60
     */
61
    private $transitonSelector;
62
63
    /**
64
     * @var TransitionInterface
65
     */
66
    private $selectedTransition;
67
68
    /**
69
     * @var ProcessInterface
70
     */
71
    private $process;
72
73
    /**
74
     * @var MutexInterface
75
     */
76
    private $mutex;
77
78
    /**
79
     * @param object                      $subject
80
     * @param ProcessInterface            $process
81
     * @param string                      $stateName
82
     * @param TransitionSelectorInterface $transitonSelector
83
     * @param MutexInterface              $mutex
84
     */
85 9
    public function __construct(
86
        $subject,
87
        ProcessInterface $process,
88
        $stateName = null,
89
        TransitionSelectorInterface $transitonSelector = null,
90
        MutexInterface $mutex = null
91
    ) {
92 9
        parent::__construct();
93 9
        $this->subject = $subject;
94 9
        if ($stateName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $stateName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
95
            $this->currentState = $process->getState($stateName);
96
        } else {
97 9
            $this->currentState = $process->getInitialState();
98
        }
99 9
        if ($transitonSelector) {
100
            $this->transitonSelector = $transitonSelector;
101
        } else {
102 9
            $this->transitonSelector = new OneOrNoneActiveTransition();
103
        }
104 9
        $this->process = $process;
105 9
        if ($mutex) {
106
            $this->mutex = $mutex;
107
        } else {
108 9
            $this->mutex = new NullMutex();
109
        }
110 9
    }
111
112
    /**
113
     * @return ProcessInterface
114
     */
115
    public function getProcess()
116
    {
117
        return $this->process;
118
    }
119
120
    /**
121
     * @see MetaborStd\Statemachine.StatemachineInterface::getCurrentState()
122
     */
123 6
    public function getCurrentState()
124
    {
125 6
        return $this->currentState;
126
    }
127
128
    /**
129
     * @return StateInterface
130
     */
131 1
    public function getLastState()
132
    {
133 1
        return $this->lastState;
134
    }
135
136
    /**
137
     * @param \ArrayAccess   $context
138
     * @param EventInterface $event
139
     */
140 3
    protected function doCheckTransitions(\ArrayAccess $context, EventInterface $event = null)
141
    {
142
        try {
143 3
            $transitions = $this->currentState->getTransitions();
144 3
            $activeTransitions = new ActiveTransitionFilter($transitions, $this->getSubject(), $context, $event);
145 3
            $this->selectedTransition = $this->transitonSelector->selectTransition($activeTransitions);
146 3
            if ($this->selectedTransition) {
147 3
                $targetState = $this->selectedTransition->getTargetState();
148 3
                if ($this->currentState != $targetState) {
149 3
                    $this->lastState = $this->currentState;
150 3
                    $this->currentState = $targetState;
151 3
                    $this->currentContext = $context;
152 3
                    $this->currentEvent = $event;
153 3
                    $this->notify();
154 3
                    $this->currentContext = null;
155 3
                    $this->currentEvent = null;
156 3
                    $this->selectedTransition = null;
157 3
                    $this->lastState = null;
158 3
                }
159 3
                $this->checkTransitions($context);
160 3
            }
161 3
        } catch (\Exception $exception) {
162
            $message = 'Exception was thrown when doing a transition from current state "' . $this->currentState->getName() . '"';
163
            if ($this->currentEvent instanceof NamedInterface) {
164
                $message .= ' with event "' . $this->currentEvent->getName() . '"';
165
            }
166
            throw new \RuntimeException($message, 0, $exception);
167
        }
168 3
    }
169
170
    /**
171
     * @return \MetaborStd\Statemachine\TransitionInterface
172
     */
173 1
    public function getSelectedTransition()
174
    {
175 1
        return $this->selectedTransition;
176
    }
177
178
    /**
179
     * is called after dispatcher was executed.
180
     */
181 3
    public function onDispatcherReady()
182
    {
183 3
        if ($this->dispatcher && $this->dispatcher->isReady()) {
184 3
            $context = $this->currentContext;
185 3
            $event = $this->currentEvent;
186 3
            $this->dispatcher = null;
187 3
            $this->currentContext = null;
188 3
            $this->currentEvent = null;
189 3
            $this->doCheckTransitions($context, $event);
190 3
            $this->mutex->releaseLock();
191 3
        }
192 3
    }
193
194
    /**
195
     * @param DispatcherInterface $dispatcher
196
     * @param string              $name
197
     * @param \ArrayAccess        $context
198
     *
199
     * @throws \RuntimeException
200
     */
201 4
    public function dispatchEvent(DispatcherInterface $dispatcher, $name, \ArrayAccess $context = null)
202
    {
203 4
        if ($this->dispatcher) {
204
            throw new \RuntimeException('Event dispatching is still running!');
205
        } else {
206 4
            if ($this->currentState->hasEvent($name)) {
207 3
                $this->acquireLockOrThrowException();
208 3
                $this->dispatcher = $dispatcher;
209
210 3
                if ($context) {
211 1
                    $this->currentContext = $context;
212 1
                } else {
213 3
                    $this->currentContext = new \ArrayIterator(array());
214
                }
215 3
                $this->currentEvent = $this->currentState->getEvent($name);
216
217 3
                $dispatcher->dispatch($this->currentEvent, array($this->subject, $this->currentContext), new Callback(array($this, 'onDispatcherReady')));
218 3
            } else {
219 1
                throw new WrongEventForStateException($this->currentState->getName(), $name);
220
            }
221
        }
222 3
    }
223
224
    /**
225
     * @param string $name
226
     * @param \ArrayAccess|null $context
227
     */
228 4
    public function triggerEvent($name, \ArrayAccess $context = null)
229
    {
230 4
        $dispatcher = new Dispatcher();
231 4
        $this->dispatchEvent($dispatcher, $name, $context);
232 3
        $dispatcher();
233 3
    }
234
235
    /**
236
     * @param \ArrayAccess|null $context
237
     */
238 3
    public function checkTransitions(\ArrayAccess $context = null)
239
    {
240 3
        $this->acquireLockOrThrowException();
241 3
        if (!$context) {
242 1
            $context = new \ArrayIterator(array());
243 1
        }
244 3
        $this->doCheckTransitions($context);
245 3
        $this->mutex->releaseLock();
246 3
    }
247
248
    /**
249
     * @see \MetaborStd\Statemachine\StatemachineInterface::getSubject()
250
     */
251 6
    public function getSubject()
252
    {
253 6
        return $this->subject;
254
    }
255
256
    /**
257
     * @return \ArrayAccess
258
     */
259 1
    public function getCurrentContext()
260
    {
261 1
        return $this->currentContext;
262
    }
263
264
    /**
265
     * @throws LockCanNotBeAcquiredException
266
     */
267 3
    protected function acquireLockOrThrowException()
268
    {
269 3
        if (!$this->acquireLock()) {
270
            throw new LockCanNotBeAcquiredException('Lock can not be acquired!');
271
        }
272 3
    }
273
274
    /**
275
     * Use this function if you want to aquire lock before calling triggerEvent or checkTransitions.
276
     * Lock is aquired automatically when calling dispatchEvent or checkTransitions.
277
     *
278
     * @return bool
279
     */
280 3
    public function acquireLock()
281
    {
282 3
        if ($this->mutex->isAcquired()) {
283 3
            return true;
284
        } else {
285 3
            return $this->mutex->acquireLock();
286
        }
287
    }
288
}
289