Completed
Push — master ( fb3d14...dcd42f )
by Yohan
07:25 queued 01:07
created

StateMachine::can()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 2 Features 1
Metric Value
c 5
b 2
f 1
dl 0
loc 18
rs 8.8571
cc 5
eloc 10
nc 6
nop 1
1
<?php
2
3
namespace Finite\StateMachine;
4
5
use Finite\Event\FiniteEvents;
6
use Finite\Event\StateMachineEvent;
7
use Finite\Event\TransitionEvent;
8
use Finite\Exception;
9
use Finite\State\Accessor\PropertyPathStateAccessor;
10
use Finite\State\Accessor\StateAccessorInterface;
11
use Finite\State\State;
12
use Finite\State\StateInterface;
13
use Finite\Transition\Transition;
14
use Finite\Transition\TransitionInterface;
15
use Symfony\Component\EventDispatcher\EventDispatcher;
16
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
17
18
/**
19
 * The Finite State Machine
20
 *
21
 * @author Yohan Giarelli <[email protected]>
22
 */
23
class StateMachine implements StateMachineInterface
0 ignored issues
show
Complexity introduced by
The class StateMachine has a coupling between objects value of 20. Consider to reduce the number of dependencies under 13.
Loading history...
24
{
25
    /**
26
     * The stateful object
27
     *
28
     * @var object
29
     */
30
    protected $object;
31
32
    /**
33
     * The available states
34
     *
35
     * @var array
36
     */
37
    protected $states = array();
38
39
    /**
40
     * The available transitions
41
     *
42
     * @var array
43
     */
44
    protected $transitions = array();
45
46
    /**
47
     * The current state
48
     *
49
     * @var StateInterface
50
     */
51
    protected $currentState;
52
53
    /**
54
     * @var EventDispatcherInterface
55
     */
56
    protected $dispatcher;
57
58
    /**
59
     * @var StateAccessorInterface
60
     */
61
    protected $stateAccessor;
62
63
    /**
64
     * @var string
65
     */
66
    protected $graph;
67
68
    /**
69
     * @param object                   $object
70
     * @param EventDispatcherInterface $dispatcher
71
     * @param StateAccessorInterface   $stateAccessor
72
     */
73
    public function __construct(
74
        $object = null,
75
        EventDispatcherInterface $dispatcher = null,
76
        StateAccessorInterface $stateAccessor = null
77
    )
78
    {
79
        $this->object        = $object;
80
        $this->dispatcher    = $dispatcher ?: new EventDispatcher;
81
        $this->stateAccessor = $stateAccessor ?: new PropertyPathStateAccessor;
82
    }
83
84
    /**
85
     * @{inheritDoc}
86
     */
87
    public function initialize()
88
    {
89
        if (null === $this->object) {
90
            throw new Exception\ObjectException('No object bound to the State Machine');
91
        }
92
93
        try {
94
            $initialState = $this->stateAccessor->getState($this->object);
95
        } catch (Exception\NoSuchPropertyException $e) {
96
            throw new Exception\ObjectException(sprintf(
97
               'StateMachine can\'t be initialized because the defined property_path of object "%s" does not exist.',
98
                get_class($this->object)
99
            ), $e->getCode(), $e);
100
        }
101
102
        if (null === $initialState) {
103
            $initialState = $this->findInitialState();
104
            $this->stateAccessor->setState($this->object, $initialState);
105
106
            $this->dispatcher->dispatch(FiniteEvents::SET_INITIAL_STATE, new StateMachineEvent($this));
107
        }
108
109
        $this->currentState = $this->getState($initialState);
110
111
        $this->dispatcher->dispatch(FiniteEvents::INITIALIZE, new StateMachineEvent($this));
112
    }
113
114
    /**
115
     * @{inheritDoc}
116
     *
117
     * @throws Exception\StateException
118
     */
119
    public function apply($transitionName)
120
    {
121
        $transition = $this->getTransition($transitionName);
122
        $event      = new TransitionEvent($this->getCurrentState(), $transition, $this);
123
        if (!$this->can($transition)) {
124
            throw new Exception\StateException(sprintf(
125
                'The "%s" transition can not be applied to the "%s" state of object "%s" with graph "%s".',
126
                $transition->getName(),
127
                $this->currentState->getName(),
128
                get_class($this->getObject()),
129
                $this->getGraph()
130
            ));
131
        }
132
133
        $this->dispatcher->dispatch(FiniteEvents::PRE_TRANSITION, $event);
134
        $this->dispatcher->dispatch(FiniteEvents::PRE_TRANSITION . '.' . $transitionName, $event);
135
136
        $returnValue = $transition->process($this);
137
        $this->stateAccessor->setState($this->object, $transition->getState());
138
        $this->currentState = $this->getState($transition->getState());
139
140
        $this->dispatcher->dispatch(FiniteEvents::POST_TRANSITION, $event);
141
        $this->dispatcher->dispatch(FiniteEvents::POST_TRANSITION . '.' . $transitionName, $event);
142
143
        return $returnValue;
144
    }
145
146
    /**
147
     * @{inheritDoc}
148
     */
149
    public function can($transition)
150
    {
151
        $transition = $transition instanceof TransitionInterface ? $transition : $this->getTransition($transition);
152
153
        if (null !== $transition->getGuard() && !call_user_func($transition->getGuard(), $this)) {
154
            return false;
155
        }
156
157
        if (!in_array($transition->getName(), $this->getCurrentState()->getTransitions())) {
158
            return false;
159
        }
160
161
        $event = new TransitionEvent($this->getCurrentState(), $transition, $this);
162
        $this->dispatcher->dispatch(FiniteEvents::TEST_TRANSITION, $event);
163
        $this->dispatcher->dispatch(FiniteEvents::TEST_TRANSITION . '.' . $transition->getName(), $event);
164
165
        return !$event->isRejected();
166
    }
167
168
    /**
169
     * @{inheritDoc}
170
     */
171
    public function addState($state)
172
    {
173
        if (!$state instanceof StateInterface) {
174
            $state = new State($state);
175
        }
176
177
        $this->states[$state->getName()] = $state;
178
    }
179
180
    /**
181
     * @{inheritDoc}
182
     */
183
    public function addTransition($transition, $initialState = null, $finalState = null)
184
    {
185
        if ((null === $initialState || null === $finalState) && !$transition instanceof TransitionInterface) {
186
            throw new \InvalidArgumentException(
187
                'You must provide a TransitionInterface instance or the $transition, ' .
188
                '$initialState and $finalState parameters'
189
            );
190
        }
191
        // If transition isn't a TransitionInterface instance, we create one from the states date
192
        if (!$transition instanceof TransitionInterface) {
193
            try {
194
                $transition = $this->getTransition($transition);
195
            } catch (Exception\TransitionException $e) {
196
                $transition = new Transition($transition, $initialState, $finalState);
0 ignored issues
show
Documentation introduced by
$transition is of type object<Finite\Transition\TransitionInterface>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
197
            }
198
        }
199
200
        $this->transitions[$transition->getName()] = $transition;
201
202
        // We add missings states to the State Machine
203
        try {
204
            $this->getState($transition->getState());
205
        } catch (Exception\StateException $e) {
206
            $this->addState($transition->getState());
207
        }
208
        foreach ($transition->getInitialStates() as $state) {
209
            try {
210
                $this->getState($state);
211
            } catch (Exception\StateException $e) {
212
                $this->addState($state);
213
            }
214
            $state = $this->getState($state);
215
            if ($state instanceof State) {
216
                $state->addTransition($transition);
217
            }
218
        }
219
    }
220
221
    /**
222
     * @{inheritDoc}
223
     */
224
    public function getTransition($name)
225
    {
226
        if (!isset($this->transitions[$name])) {
227
            throw new Exception\TransitionException(sprintf(
228
                'Unable to find a transition called "%s" on object "%s" with graph "%s".',
229
                $name,
230
                get_class($this->getObject()),
231
                $this->getGraph()
232
            ));
233
        }
234
235
        return $this->transitions[$name];
236
    }
237
238
    /**
239
     * @{inheritDoc}
240
     */
241
    public function getState($name)
242
    {
243
        $name = (string) $name;
244
245
        if (!isset($this->states[$name])) {
246
            throw new Exception\StateException(sprintf(
247
                'Unable to find a state called "%s" on object "%s" with graph "%s".',
248
                $name,
249
                get_class($this->getObject()),
250
                $this->getGraph()
251
            ));
252
        }
253
254
        return $this->states[$name];
255
    }
256
257
    /**
258
     * @{inheritDoc}
259
     */
260
    public function getTransitions()
261
    {
262
        return array_keys($this->transitions);
263
    }
264
265
    /**
266
     * @{inheritDoc}
267
     */
268
    public function getStates()
269
    {
270
        return array_keys($this->states);
271
    }
272
273
    /**
274
     * {@inheritDoc}
275
     */
276
    public function setObject($object)
277
    {
278
        $this->object = $object;
279
    }
280
281
    /**
282
     * @{inheritDoc}
283
     */
284
    public function getObject()
285
    {
286
        return $this->object;
287
    }
288
289
    /**
290
     * @{inheritDoc}
291
     */
292
    public function getCurrentState()
293
    {
294
        return $this->currentState;
295
    }
296
297
    /**
298
     * Find and return the Initial state if exists
299
     *
300
     * @return string
301
     *
302
     * @throws Exception\StateException
303
     */
304
    protected function findInitialState()
305
    {
306
        foreach ($this->states as $state) {
307
            if (State::TYPE_INITIAL === $state->getType()) {
308
                return $state->getName();
309
            }
310
        }
311
312
        throw new Exception\StateException(sprintf(
313
            'No initial state found on object "%s" with graph "%s".',
314
            get_class($this->getObject()),
315
            $this->getGraph()
316
        ));
317
    }
318
319
    /**
320
     * @param EventDispatcherInterface $dispatcher
321
     */
322
    public function setDispatcher(EventDispatcherInterface $dispatcher)
323
    {
324
        $this->dispatcher = $dispatcher;
325
    }
326
327
    /**
328
     * @return EventDispatcherInterface
329
     */
330
    public function getDispatcher()
331
    {
332
        return $this->dispatcher;
333
    }
334
335
    /**
336
     * @param StateAccessorInterface $stateAccessor
337
     */
338
    public function setStateAccessor(StateAccessorInterface $stateAccessor)
339
    {
340
        $this->stateAccessor = $stateAccessor;
341
    }
342
343
    /**
344
     * @{inheritDoc}
345
     */
346
    public function setGraph($graph)
347
    {
348
        $this->graph = $graph;
349
    }
350
351
    /**
352
     * @{inheritDoc}
353
     */
354
    public function getGraph()
355
    {
356
        return $this->graph;
357
    }
358
}
359