Completed
Push — master ( 101e96...9765fd )
by Yohan
02:38
created

StateMachine::dispatchTransitionEvent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 7
cts 7
cp 1
rs 9.4286
cc 2
eloc 5
nc 2
nop 3
crap 2
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 215
    public function __construct(
74
        $object = null,
75
        EventDispatcherInterface $dispatcher = null,
76
        StateAccessorInterface $stateAccessor = null
77
    ) {
78 215
        $this->object = $object;
79 215
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
80 215
        $this->stateAccessor = $stateAccessor ?: new PropertyPathStateAccessor();
81 215
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86 160
    public function initialize()
87
    {
88 160
        if (null === $this->object) {
89
            throw new Exception\ObjectException('No object bound to the State Machine');
90
        }
91
92
        try {
93 160
            $initialState = $this->stateAccessor->getState($this->object);
94 128
        } catch (Exception\NoSuchPropertyException $e) {
95
            throw new Exception\ObjectException(sprintf(
96
               'StateMachine can\'t be initialized because the defined property_path of object "%s" does not exist.',
97
                get_class($this->object)
98
            ), $e->getCode(), $e);
99
        }
100
101 160
        if (null === $initialState) {
102 20
            $initialState = $this->findInitialState();
103 20
            $this->stateAccessor->setState($this->object, $initialState);
104
105 20
            $this->dispatcher->dispatch(FiniteEvents::SET_INITIAL_STATE, new StateMachineEvent($this));
106 16
        }
107
108 160
        $this->currentState = $this->getState($initialState);
109
110 160
        $this->dispatcher->dispatch(FiniteEvents::INITIALIZE, new StateMachineEvent($this));
111 160
    }
112
113
    /**
114
     * {@inheritdoc}
115
     *
116
     * @throws Exception\StateException
117
     */
118 20
    public function apply($transitionName, array $parameters = array())
119
    {
120 20
        $transition = $this->getTransition($transitionName);
121 20
        $event = new TransitionEvent($this->getCurrentState(), $transition, $this, $parameters);
122 20
        if (!$this->can($transition, $parameters)) {
123 5
            throw new Exception\StateException(sprintf(
124 5
                'The "%s" transition can not be applied to the "%s" state of object "%s" with graph "%s".',
125 5
                $transition->getName(),
126 5
                $this->currentState->getName(),
127 5
                get_class($this->getObject()),
128 5
                $this->getGraph()
129 4
            ));
130
        }
131
132 20
        $this->dispatchTransitionEvent($transition, $event, FiniteEvents::PRE_TRANSITION);
133
134 20
        $returnValue = $transition->process($this);
135 20
        $this->stateAccessor->setState($this->object, $transition->getState());
136 20
        $this->currentState = $this->getState($transition->getState());
137
138 20
        $this->dispatchTransitionEvent($transition, $event, FiniteEvents::POST_TRANSITION);
139
140 20
        return $returnValue;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 55
    public function can($transition, array $parameters = array())
147
    {
148 55
        $transition = $transition instanceof TransitionInterface ? $transition : $this->getTransition($transition);
149
150 55
        if (null !== $transition->getGuard() && !call_user_func($transition->getGuard(), $this)) {
151 5
            return false;
152
        }
153
154 50
        if (!in_array($transition->getName(), $this->getCurrentState()->getTransitions())) {
155 30
            return false;
156
        }
157
158 50
        $event = new TransitionEvent($this->getCurrentState(), $transition, $this, $parameters);
159 50
        $this->dispatchTransitionEvent($transition, $event, FiniteEvents::TEST_TRANSITION);
160
161 50
        return !$event->isRejected();
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 180
    public function addState($state)
168
    {
169 180
        if (!$state instanceof StateInterface) {
170 175
            $state = new State($state);
171 140
        }
172
173 180
        $this->states[$state->getName()] = $state;
174 180
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 175
    public function addTransition($transition, $initialState = null, $finalState = null)
180
    {
181 175
        if ((null === $initialState || null === $finalState) && !$transition instanceof TransitionInterface) {
182
            throw new \InvalidArgumentException(
183
                'You must provide a TransitionInterface instance or the $transition, '.
184
                '$initialState and $finalState parameters'
185
            );
186
        }
187
        // If transition isn't a TransitionInterface instance, we create one from the states date
188 175
        if (!$transition instanceof TransitionInterface) {
189
            try {
190 155
                $transition = $this->getTransition($transition);
191 155
            } catch (Exception\TransitionException $e) {
192 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...
193
            }
194 124
        }
195
196 175
        $this->transitions[$transition->getName()] = $transition;
197
198
        // We add missings states to the State Machine
199
        try {
200 175
            $this->getState($transition->getState());
201 158
        } catch (Exception\StateException $e) {
202 90
            $this->addState($transition->getState());
203
        }
204 175
        foreach ($transition->getInitialStates() as $state) {
205
            try {
206 175
                $this->getState($state);
207 147
            } catch (Exception\StateException $e) {
208 35
                $this->addState($state);
209
            }
210 175
            $state = $this->getState($state);
211 175
            if ($state instanceof State) {
212 175
                $state->addTransition($transition);
213 140
            }
214 140
        }
215 175
    }
216
217
    /**
218
     * {@inheritdoc}
219
     */
220 160
    public function getTransition($name)
221
    {
222 160
        if (!isset($this->transitions[$name])) {
223 155
            throw new Exception\TransitionException(sprintf(
224 155
                'Unable to find a transition called "%s" on object "%s" with graph "%s".',
225 124
                $name,
226 155
                get_class($this->getObject()),
227 155
                $this->getGraph()
228 124
            ));
229
        }
230
231 55
        return $this->transitions[$name];
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 180
    public function getState($name)
238
    {
239 180
        $name = (string) $name;
240
241 180
        if (!isset($this->states[$name])) {
242 95
            throw new Exception\StateException(sprintf(
243 95
                'Unable to find a state called "%s" on object "%s" with graph "%s".',
244 76
                $name,
245 95
                get_class($this->getObject()),
246 95
                $this->getGraph()
247 76
            ));
248
        }
249
250 180
        return $this->states[$name];
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256 5
    public function getTransitions()
257
    {
258 5
        return array_keys($this->transitions);
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264 5
    public function getStates()
265
    {
266 5
        return array_keys($this->states);
267
    }
268
269
    /**
270
     * {@inheritdoc}
271
     */
272 150
    public function setObject($object)
273
    {
274 150
        $this->object = $object;
275 150
    }
276
277
    /**
278
     * {@inheritdoc}
279
     */
280 175
    public function getObject()
281
    {
282 175
        return $this->object;
283
    }
284
285
    /**
286
     * {@inheritdoc}
287
     */
288 115
    public function getCurrentState()
289
    {
290 115
        return $this->currentState;
291
    }
292
293
    /**
294
     * Find and return the Initial state if exists.
295
     *
296
     * @return string
297
     *
298
     * @throws Exception\StateException
299
     */
300 20
    protected function findInitialState()
301
    {
302 20
        foreach ($this->states as $state) {
303 20
            if (State::TYPE_INITIAL === $state->getType()) {
304 20
                return $state->getName();
305
            }
306
        }
307
308
        throw new Exception\StateException(sprintf(
309
            'No initial state found on object "%s" with graph "%s".',
310
            get_class($this->getObject()),
311
            $this->getGraph()
312
        ));
313
    }
314
315
    /**
316
     * @param EventDispatcherInterface $dispatcher
317
     */
318
    public function setDispatcher(EventDispatcherInterface $dispatcher)
319
    {
320
        $this->dispatcher = $dispatcher;
321
    }
322
323
    /**
324
     * @return EventDispatcherInterface
325
     */
326 5
    public function getDispatcher()
327
    {
328 5
        return $this->dispatcher;
329
    }
330
331
    /**
332
     * @param StateAccessorInterface $stateAccessor
333
     */
334 10
    public function setStateAccessor(StateAccessorInterface $stateAccessor)
335
    {
336 10
        $this->stateAccessor = $stateAccessor;
337 10
    }
338
339
    /**
340
     * {@inheritdoc}
341
     */
342 15
    public function setGraph($graph)
343
    {
344 15
        $this->graph = $graph;
345 15
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350 175
    public function getGraph()
351
    {
352 175
        return $this->graph;
353
    }
354
355
    /**
356
     * {@inheritDoc}
357
     */
358 10
    public function findStateWithProperty($property, $value = null)
359
    {
360 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...
361 8
            array_map(
362
                function (State $state) {
363 10
                    return $state->getName();
364 10
                },
365 8
                array_filter(
366 10
                    $this->states,
367 10
                    function (State $state) use ($property, $value) {
368 10
                        if (!$state->has($property)) {
369 10
                            return false;
370
                        }
371
372 10
                        if (null !== $value && $state->get($property) !== $value) {
373 5
                            return false;
374
                        }
375
376 10
                        return true;
377 2
                    }
378 8
                )
379 8
            )
380 8
        );
381
    }
382
383
    /**
384
     * Dispatches event for the transition
385
     *
386
     * @param TransitionInterface $transition
387
     * @param TransitionEvent $event
388
     * @param type $transitionState
389
     */
390 50
    private function dispatchTransitionEvent(TransitionInterface $transition, TransitionEvent $event, $transitionState)
391
    {
392 50
        $this->dispatcher->dispatch($transitionState, $event);
393 50
        $this->dispatcher->dispatch($transitionState.'.'.$transition->getName(), $event);
394 50
        if (null !== $this->getGraph()) {
395 10
            $this->dispatcher->dispatch($transitionState.'.'.$this->getGraph().'.'.$transition->getName(), $event);
396 8
        }
397 50
    }
398
}
399