Completed
Branch master (c08b38)
by Oliver
03:58
created

Statemachine::isAutoreleaseLock()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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
     * @var bool
80
     */
81
    private $autoreleaseLock = true;
82
83
    /**
84
     * @param object                      $subject
85
     * @param ProcessInterface            $process
86
     * @param string                      $stateName
87
     * @param TransitionSelectorInterface $transitonSelector
88
     * @param MutexInterface              $mutex
89
     */
90
    public function __construct(
91
        $subject,
92
        ProcessInterface $process,
93
        $stateName = null,
94
        TransitionSelectorInterface $transitonSelector = null,
95
        MutexInterface $mutex = null
96
    ) {
97
        parent::__construct();
98
        $this->subject = $subject;
99
        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...
100
            $this->currentState = $process->getState($stateName);
101
        } else {
102
            $this->currentState = $process->getInitialState();
103
        }
104
        if ($transitonSelector) {
105
            $this->transitonSelector = $transitonSelector;
106
        } else {
107
            $this->transitonSelector = new OneOrNoneActiveTransition();
108
        }
109
        $this->process = $process;
110
        if ($mutex) {
111
            $this->mutex = $mutex;
112
        } else {
113
            $this->mutex = new NullMutex();
114
        }
115
    }
116
117
    /**
118
     * @return ProcessInterface
119
     */
120
    public function getProcess()
121
    {
122
        return $this->process;
123
    }
124
125
    /**
126
     * @see MetaborStd\Statemachine.StatemachineInterface::getCurrentState()
127
     */
128
    public function getCurrentState()
129
    {
130
        return $this->currentState;
131
    }
132
133
    /**
134
     * @return StateInterface
135
     */
136
    public function getLastState()
137
    {
138
        return $this->lastState;
139
    }
140
141
    /**
142
     * @param \ArrayAccess   $context
143
     * @param EventInterface $event
144
     */
145
    protected function doCheckTransitions(\ArrayAccess $context, EventInterface $event = null)
146
    {
147
        try {
148
            $transitions = $this->currentState->getTransitions();
149
            $activeTransitions = new ActiveTransitionFilter($transitions, $this->getSubject(), $context, $event);
150
            $this->selectedTransition = $this->transitonSelector->selectTransition($activeTransitions);
151
            if ($this->selectedTransition) {
152
                $targetState = $this->selectedTransition->getTargetState();
153
                if ($this->currentState != $targetState) {
154
                    $this->lastState = $this->currentState;
155
                    $this->currentState = $targetState;
156
                    $this->currentContext = $context;
157
                    $this->currentEvent = $event;
158
                    $this->notify();
159
                    $this->currentContext = null;
160
                    $this->currentEvent = null;
161
                    $this->selectedTransition = null;
162
                    $this->lastState = null;
163
                }
164
                $this->doCheckTransitions($context);
165
            }
166
        } catch (\Exception $exception) {
167
            $message = 'Exception was thrown when doing a transition from current state "' . $this->currentState->getName() . '"';
168
            if ($this->currentEvent instanceof NamedInterface) {
169
                $message .= ' with event "' . $this->currentEvent->getName() . '"';
170
            }
171
            if ($this->subject instanceof NamedInterface) {
172
                $message .= ' for "' . $this->subject->getName() . '"';
173
            }
174
            throw new \RuntimeException($message, 0, $exception);
175
        }
176
    }
177
178
    /**
179
     * @return \MetaborStd\Statemachine\TransitionInterface
180
     */
181
    public function getSelectedTransition()
182
    {
183
        return $this->selectedTransition;
184
    }
185
186
    /**
187
     * is called after dispatcher was executed.
188
     */
189
    public function onDispatcherReady()
190
    {
191
        if ($this->dispatcher && $this->dispatcher->isReady()) {
192
            $context = $this->currentContext;
193
            $event = $this->currentEvent;
194
            $this->dispatcher = null;
195
            $this->currentContext = null;
196
            $this->currentEvent = null;
197
            $this->doCheckTransitions($context, $event);
198
            if ($this->autoreleaseLock) {
199
                $this->mutex->releaseLock();
200
            }
201
        }
202
    }
203
204
    /**
205
     * @param DispatcherInterface $dispatcher
206
     * @param string              $name
207
     * @param \ArrayAccess        $context
208
     *
209
     * @throws \RuntimeException
210
     */
211
    public function dispatchEvent(DispatcherInterface $dispatcher, $name, \ArrayAccess $context = null)
212
    {
213
        if ($this->dispatcher) {
214
            throw new \RuntimeException('Event dispatching is still running!');
215
        } else {
216
            if ($this->currentState->hasEvent($name)) {
217
                $this->acquireLockOrThrowException();
218
                $this->dispatcher = $dispatcher;
219
220
                if ($context) {
221
                    $this->currentContext = $context;
222
                } else {
223
                    $this->currentContext = new \ArrayIterator(array());
224
                }
225
                $this->currentEvent = $this->currentState->getEvent($name);
226
227
                $dispatcher->dispatch($this->currentEvent, array($this->subject, $this->currentContext), new Callback(array($this, 'onDispatcherReady')));
228
            } else {
229
                throw new WrongEventForStateException($this->currentState->getName(), $name);
230
            }
231
        }
232
    }
233
234
    /**
235
     * @param string $name
236
     * @param \ArrayAccess|null $context
237
     */
238
    public function triggerEvent($name, \ArrayAccess $context = null)
239
    {
240
        $dispatcher = new Dispatcher();
241
        $this->dispatchEvent($dispatcher, $name, $context);
242
        $dispatcher();
243
    }
244
245
    /**
246
     * @param \ArrayAccess|null $context
247
     */
248
    public function checkTransitions(\ArrayAccess $context = null)
249
    {
250
        $this->acquireLockOrThrowException();
251
        if (!$context) {
252
            $context = new \ArrayIterator(array());
253
        }
254
        $this->doCheckTransitions($context);
255
        if ($this->autoreleaseLock) {
256
            $this->mutex->releaseLock();
257
        }
258
    }
259
260
    /**
261
     * @see \MetaborStd\Statemachine\StatemachineInterface::getSubject()
262
     */
263
    public function getSubject()
264
    {
265
        return $this->subject;
266
    }
267
268
    /**
269
     * @return \ArrayAccess
270
     */
271
    public function getCurrentContext()
272
    {
273
        return $this->currentContext;
274
    }
275
276
    /**
277
     * @throws LockCanNotBeAcquiredException
278
     */
279
    protected function acquireLockOrThrowException()
280
    {
281
        if (!$this->acquireLock()) {
282
            throw new LockCanNotBeAcquiredException('Lock can not be acquired!');
283
        }
284
    }
285
286
    /**
287
     * @return bool
288
     */
289
    public function isAutoreleaseLock()
290
    {
291
        return $this->autoreleaseLock;
292
    }
293
294
    /**
295
     * @param bool $autoreleaseLock
296
     */
297
    public function setAutoreleaseLock($autoreleaseLock)
298
    {
299
        $this->autoreleaseLock = $autoreleaseLock;
300
    }
301
302
    /**
303
     * Use this function if you want to aquire lock before calling triggerEvent or checkTransitions.
304
     * Lock is aquired automatically when calling dispatchEvent or checkTransitions.
305
     *
306
     * @return bool
307
     */
308
    public function acquireLock()
309
    {
310
        if ($this->mutex->isAcquired()) {
311
            return true;
312
        } else {
313
            return $this->mutex->acquireLock();
314
        }
315
    }
316
}
317