StateMachine::can()   A
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 9.3888
c 0
b 0
f 0
cc 5
nc 6
nop 2
crap 5
1
<?php
2
3
namespace Finite\StateMachine;
4
5
use Finite\Event\FiniteEvents;
6
use Finite\Event\StateMachineDispatcher;
7
use Finite\Event\StateMachineEvent;
8
use Finite\Event\TransitionEvent;
9
use Finite\Exception;
10
use Finite\State\Accessor\PropertyPathStateAccessor;
11
use Finite\State\Accessor\StateAccessorInterface;
12
use Finite\State\State;
13
use Finite\State\StateInterface;
14
use Finite\Transition\Transition;
15
use Finite\Transition\TransitionInterface;
16
17
/**
18
 * The Finite State Machine.
19
 *
20
 * @author Yohan Giarelli <[email protected]>
21
 */
22
class StateMachine implements StateMachineInterface
23
{
24
    /**
25
     * The stateful object.
26
     *
27
     * @var object
28
     */
29
    protected $object;
30
31
    /**
32
     * The available states.
33
     *
34
     * @var array
35
     */
36
    protected $states = array();
37
38
    /**
39
     * The available transitions.
40
     *
41
     * @var array
42
     */
43
    protected $transitions = array();
44
45
    /**
46
     * The current state.
47
     *
48
     * @var StateInterface
49
     */
50
    protected $currentState;
51
52
    /**
53
     * @var StateMachineDispatcher
54
     */
55
    protected $dispatcher;
56
57
    /**
58
     * @var StateAccessorInterface
59
     */
60
    protected $stateAccessor;
61
62
    /**
63
     * @var string
64
     */
65
    protected $graph;
66
67
    /**
68
     * @param object                   $object
69
     * @param StateMachineDispatcher   $dispatcher
70
     * @param StateAccessorInterface   $stateAccessor
71
     */
72 185
    public function __construct(
73
        $object = null,
74
        StateMachineDispatcher $dispatcher = null,
75
        StateAccessorInterface $stateAccessor = null
76
    ) {
77 185
        $this->object = $object;
78 185
        $this->dispatcher = $dispatcher ?: new StateMachineDispatcher();
79 185
        $this->stateAccessor = $stateAccessor ?: new PropertyPathStateAccessor();
80 185
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85 160
    public function initialize()
86
    {
87 160
        if (null === $this->object) {
88
            throw new Exception\ObjectException('No object bound to the State Machine');
89
        }
90
91
        try {
92 160
            $initialState = $this->stateAccessor->getState($this->object);
93 96
        } catch (Exception\NoSuchPropertyException $e) {
94
            throw new Exception\ObjectException(sprintf(
95
               'StateMachine can\'t be initialized because the defined property_path of object "%s" does not exist.',
96
                $this->getObject() ? get_class($this->getObject()) : null
97
            ), $e->getCode(), $e);
98
        }
99
100 160
        if (null === $initialState) {
101 20
            $initialState = $this->findInitialState();
102 20
            $this->stateAccessor->setState($this->object, $initialState);
103
104 20
            $this->dispatcher->dispatch(FiniteEvents::SET_INITIAL_STATE, new StateMachineEvent($this));
105 12
        }
106
107 160
        $this->currentState = $this->getState($initialState);
108
109 160
        $this->dispatcher->dispatch(FiniteEvents::INITIALIZE, new StateMachineEvent($this));
110 160
    }
111
112
    /**
113
     * {@inheritdoc}
114
     *
115
     * @throws Exception\StateException
116
     */
117 20
    public function apply($transitionName, array $parameters = array())
118
    {
119 20
        $transition = $this->getTransition($transitionName);
120 20
        $event = new TransitionEvent($this->getCurrentState(), $transition, $this, $parameters);
121 20
        if (!$this->can($transition, $parameters)) {
122 5
            throw new Exception\StateException(sprintf(
123 5
                'The "%s" transition can not be applied to the "%s" state of object "%s" with graph "%s".',
124 5
                $transition->getName(),
125 5
                $this->currentState->getName(),
126 5
                $this->getObject() ? get_class($this->getObject()) : null,
127 5
                $this->getGraph()
128 3
            ));
129
        }
130
131 20
        $this->dispatchTransitionEvent($transition, $event, FiniteEvents::PRE_TRANSITION);
132
133 20
        $returnValue = $transition->process($this);
134 20
        $this->stateAccessor->setState($this->object, $transition->getState());
135 20
        $this->currentState = $this->getState($transition->getState());
136
137 20
        $this->dispatchTransitionEvent($transition, $event, FiniteEvents::POST_TRANSITION);
138
139 20
        return $returnValue;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 55
    public function can($transition, array $parameters = array())
146
    {
147 55
        $transition = $transition instanceof TransitionInterface ? $transition : $this->getTransition($transition);
148
149 55
        if (null !== $transition->getGuard() && !call_user_func($transition->getGuard(), $this)) {
150 5
            return false;
151
        }
152
153 50
        if (!in_array($transition->getName(), $this->getCurrentState()->getTransitions())) {
154 30
            return false;
155
        }
156
157 50
        $event = new TransitionEvent($this->getCurrentState(), $transition, $this, $parameters);
158 50
        $this->dispatchTransitionEvent($transition, $event, FiniteEvents::TEST_TRANSITION);
159
160 50
        return !$event->isRejected();
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166 185
    public function addState($state)
167
    {
168 185
        if (!$state instanceof StateInterface) {
169 175
            $state = new State($state);
170 105
        }
171
172 185
        $this->states[$state->getName()] = $state;
173 185
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 180
    public function addTransition($transition, $initialState = null, $finalState = null)
179
    {
180 180
        if ((null === $initialState || null === $finalState) && !$transition instanceof TransitionInterface) {
181
            throw new \InvalidArgumentException(
182
                'You must provide a TransitionInterface instance or the $transition, '.
183
                '$initialState and $finalState parameters'
184
            );
185
        }
186
        // If transition isn't a TransitionInterface instance, we create one from the states date
187 180
        if (!$transition instanceof TransitionInterface) {
188
            try {
189 155
                $transition = $this->getTransition($transition);
190 155
            } catch (Exception\TransitionException $e) {
191 155
                $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...
192
            }
193 93
        }
194
195 180
        $this->transitions[$transition->getName()] = $transition;
196
197
        // We add missings states to the State Machine
198
        try {
199 180
            $this->getState($transition->getState());
200 144
        } catch (Exception\StateException $e) {
201 90
            $this->addState($transition->getState());
202
        }
203 180
        foreach ($transition->getInitialStates() as $state) {
204
            try {
205 180
                $this->getState($state);
206 122
            } catch (Exception\StateException $e) {
207 35
                $this->addState($state);
208
            }
209 180
            $state = $this->getState($state);
210 180
            if ($state instanceof State) {
211 180
                $state->addTransition($transition);
212 108
            }
213 108
        }
214 180
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219 160
    public function getTransition($name)
220
    {
221 160
        if (!isset($this->transitions[$name])) {
222 155
            throw new Exception\TransitionException(sprintf(
223 155
                'Unable to find a transition called "%s" on object "%s" with graph "%s".',
224 155
                $name,
225 155
                $this->getObject() ? get_class($this->getObject()) : null,
226 155
                $this->getGraph()
227 93
            ));
228
        }
229
230 55
        return $this->transitions[$name];
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     */
236 185
    public function getState($name)
237
    {
238 185
        $name = (string) $name;
239
240 185
        if (!isset($this->states[$name])) {
241 95
            throw new Exception\StateException(sprintf(
242 95
                'Unable to find a state called "%s" on object "%s" with graph "%s".',
243 95
                $name,
244 95
                $this->getObject() ? get_class($this->getObject()) : null,
245 95
                $this->getGraph()
246 57
            ));
247
        }
248
249 185
        return $this->states[$name];
250
    }
251
252
    /**
253
     * {@inheritdoc}
254
     */
255 5
    public function getTransitions()
256
    {
257 5
        return array_keys($this->transitions);
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263 5
    public function getStates()
264
    {
265 5
        return array_keys($this->states);
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271 150
    public function setObject($object)
272
    {
273 150
        $this->object = $object;
274 150
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279 175
    public function getObject()
280
    {
281 175
        return $this->object;
282
    }
283
284
    /**
285
     * {@inheritdoc}
286
     */
287 115
    public function getCurrentState()
288
    {
289 115
        return $this->currentState;
290
    }
291
292
    /**
293
     * Find and return the Initial state if exists.
294
     *
295
     * @return string
296
     *
297
     * @throws Exception\StateException
298
     */
299 20
    protected function findInitialState()
300
    {
301 20
        foreach ($this->states as $state) {
302 20
            if (State::TYPE_INITIAL === $state->getType()) {
303 20
                return $state->getName();
304
            }
305
        }
306
307
        throw new Exception\StateException(sprintf(
308
            'No initial state found on object "%s" with graph "%s".',
309
            $this->getObject() ? get_class($this->getObject()) : null,
310
            $this->getGraph()
311
        ));
312
    }
313
314
    /**
315
     * @param StateMachineDispatcher $dispatcher
316
     */
317
    public function setDispatcher(StateMachineDispatcher $dispatcher)
318
    {
319
        $this->dispatcher = $dispatcher;
320
    }
321
322
    /**
323
     * @return StateMachineDispatcher
324
     */
325 5
    public function getDispatcher()
326
    {
327 5
        return $this->dispatcher;
328
    }
329
330
    /**
331
     * @param StateAccessorInterface $stateAccessor
332
     */
333 5
    public function setStateAccessor(StateAccessorInterface $stateAccessor)
334
    {
335 5
        $this->stateAccessor = $stateAccessor;
336 5
    }
337
338
    /**
339
     * {@inheritdoc}
340
     */
341 15
    public function hasStateAccessor()
342
    {
343 15
        return null !== $this->stateAccessor;
344
    }
345
346
    /**
347
     * {@inheritdoc}
348
     */
349 20
    public function setGraph($graph)
350
    {
351 20
        $this->graph = $graph;
352 20
    }
353
354
    /**
355
     * {@inheritdoc}
356
     */
357 175
    public function getGraph()
358
    {
359 175
        return $this->graph;
360
    }
361
362
    /**
363
     * {@inheritDoc}
364
     */
365 10
    public function findStateWithProperty($property, $value = null)
366
    {
367 10
        return array_keys(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array_keys(array_... return true; }))); (array<integer|string>) is incompatible with the return type declared by the interface Finite\StateMachine\Stat...::findStateWithProperty of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
368 10
            array_map(
369 2
                function (State $state) {
370 10
                    return $state->getName();
371 10
                },
372 10
                array_filter(
373 10
                    $this->states,
374 10
                    function (State $state) use ($property, $value) {
375 10
                        if (!$state->has($property)) {
376 10
                            return false;
377
                        }
378
379 10
                        if (null !== $value && $state->get($property) !== $value) {
380 5
                            return false;
381
                        }
382
383 10
                        return true;
384 4
                    }
385 6
                )
386 6
            )
387 6
        );
388
    }
389
390
    /**
391
     * Dispatches event for the transition
392
     *
393
     * @param TransitionInterface $transition
394
     * @param TransitionEvent $event
395
     * @param type $transitionState
396
     */
397 50
    private function dispatchTransitionEvent(TransitionInterface $transition, TransitionEvent $event, $transitionState)
398
    {
399 50
        $this->dispatcher->dispatch($transitionState, $event);
400 50
        $this->dispatcher->dispatch($transitionState.'.'.$transition->getName(), $event);
401 50
        if (null !== $this->getGraph()) {
402 10
            $this->dispatcher->dispatch($transitionState.'.'.$this->getGraph().'.'.$transition->getName(), $event);
403 6
        }
404 50
    }
405
}
406