Completed
Pull Request — master (#124)
by
unknown
04:50 queued 01:06
created

StateMachine   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 405
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 13

Test Coverage

Coverage 89.03%

Importance

Changes 30
Bugs 6 Features 10
Metric Value
wmc 52
c 30
b 6
f 10
cbo 13
dl 0
loc 405
ccs 138
cts 155
cp 0.8903
rs 7.9487

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 3
B initialize() 0 26 4
B apply() 0 24 2
B can() 0 17 5
A addState() 0 8 2
D addTransition() 0 37 10
A getTransition() 0 13 2
A getState() 0 15 2
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 findInitialState() 0 14 3
A setDispatcher() 0 4 1
A getDispatcher() 0 4 1
A setStateAccessor() 0 4 1
A hasStateAccessor() 0 4 1
A setGraph() 0 4 1
A getGraph() 0 4 1
B findStateWithProperty() 0 24 4
A dispatchTransitionEvent() 0 8 2
A getCallbacks() 0 4 1
A setCallbacks() 0 4 1

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\Callback\Callback;
6
use Finite\Event\FiniteEvents;
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
use Symfony\Component\EventDispatcher\EventDispatcher;
17
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
18
19
/**
20
 * The Finite State Machine.
21
 *
22
 * @author Yohan Giarelli <[email protected]>
23
 */
24
class StateMachine implements StateMachineInterface
0 ignored issues
show
Complexity introduced by
This class has a complexity of 52 which exceeds the configured maximum of 50.

The class complexity is the sum of the complexity of all methods. A very high value is usually an indication that your class does not follow the single reponsibility principle and does more than one job.

Some resources for further reading:

You can also find more detailed suggestions for refactoring in the “Code” section of your repository.

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