Completed
Pull Request — master (#152)
by
unknown
10:07
created

StateMachine   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 399
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 13

Test Coverage

Coverage 89.17%

Importance

Changes 0
Metric Value
wmc 55
cbo 13
dl 0
loc 399
ccs 140
cts 157
cp 0.8917
rs 6
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 3
A initialize() 0 28 4
A can() 0 17 5
A addState() 0 8 2
B addTransition() 0 37 10
A getTransition() 0 15 3
A getState() 0 17 3
A getTransitions() 0 4 1
A getStates() 0 4 1
A setObject() 0 4 1
A getObject() 0 4 1
A getCurrentState() 0 4 1
A setDispatcher() 0 4 1
A getDispatcher() 0 4 1
A setStateAccessor() 0 4 1
A apply() 0 26 3
A findInitialState() 0 16 4
A getStateAccessor() 0 4 1
A hasStateAccessor() 0 4 1
A setGraph() 0 4 1
A getGraph() 0 4 1
A findStateWithProperty() 0 24 4
A dispatchTransitionEvent() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like StateMachine often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StateMachine, and based on these observations, apply Extract Interface, too.

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