Completed
Push — master ( 128e93...5474ab )
by Yohan
11:26
created

StateMachine   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 13

Test Coverage

Coverage 89.17%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 50
cbo 13
dl 0
loc 384
c 6
b 0
f 0
ccs 140
cts 157
cp 0.8917
rs 8.6206

22 Methods

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

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
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...
Complexity introduced by
This class has a complexity of 50 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...
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 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 131
    public function initialize()
87
    {
88 131
        if (null === $this->object) {
89
            throw new Exception\ObjectException('No object bound to the State Machine');
90
        }
91
92
        try {
93 131
            $initialState = $this->stateAccessor->getState($this->object);
94 64
        } 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 131
        if (null === $initialState) {
102 18
            $initialState = $this->findInitialState();
103 18
            $this->stateAccessor->setState($this->object, $initialState);
104
105 18
            $this->dispatcher->dispatch(FiniteEvents::SET_INITIAL_STATE, new StateMachineEvent($this));
106 8
        }
107
108 131
        $this->currentState = $this->getState($initialState);
109
110 131
        $this->dispatcher->dispatch(FiniteEvents::INITIALIZE, new StateMachineEvent($this));
111 131
    }
112
113
    /**
114
     * {@inheritdoc}
115
     *
116
     * @throws Exception\StateException
117
     */
118 17
    public function apply($transitionName, array $parameters = array())
119
    {
120 17
        $transition = $this->getTransition($transitionName);
121 17
        $event = new TransitionEvent($this->getCurrentState(), $transition, $this, $parameters);
122 17
        if (!$this->can($transition, $parameters)) {
123 4
            throw new Exception\StateException(sprintf(
124 4
                'The "%s" transition can not be applied to the "%s" state of object "%s" with graph "%s".',
125 4
                $transition->getName(),
126 4
                $this->currentState->getName(),
127 4
                get_class($this->getObject()),
128 4
                $this->getGraph()
129 2
            ));
130
        }
131
132 17
        $this->dispatchTransitionEvent($transition, $event, FiniteEvents::PRE_TRANSITION);
133
134 17
        $returnValue = $transition->process($this);
135 17
        $this->stateAccessor->setState($this->object, $transition->getState());
136 17
        $this->currentState = $this->getState($transition->getState());
137
138 17
        $this->dispatchTransitionEvent($transition, $event, FiniteEvents::POST_TRANSITION);
139
140 17
        return $returnValue;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 47
    public function can($transition, array $parameters = array())
147
    {
148 47
        $transition = $transition instanceof TransitionInterface ? $transition : $this->getTransition($transition);
149
150 47
        if (null !== $transition->getGuard() && !call_user_func($transition->getGuard(), $this)) {
151 4
            return false;
152
        }
153
154 43
        if (!in_array($transition->getName(), $this->getCurrentState()->getTransitions())) {
155 25
            return false;
156
        }
157
158 43
        $event = new TransitionEvent($this->getCurrentState(), $transition, $this, $parameters);
159 43
        $this->dispatchTransitionEvent($transition, $event, FiniteEvents::TEST_TRANSITION);
160
161 43
        return !$event->isRejected();
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 180
    public function addState($state)
168
    {
169 180
        if (!$state instanceof StateInterface) {
170 158
            $state = new State($state);
171 70
        }
172
173 180
        $this->states[$state->getName()] = $state;
174 180
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 180
    public function addTransition($transition, $initialState = null, $finalState = null)
180
    {
181 180
        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 180
        if (!$transition instanceof TransitionInterface) {
189
            try {
190 155
                $transition = $this->getTransition($transition);
191 155
            } catch (Exception\TransitionException $e) {
192 125
                $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 62
        }
195
196 150
        $this->transitions[$transition->getName()] = $transition;
197
198
        // We add missings states to the State Machine
199
        try {
200 150
            $this->getState($transition->getState());
201 111
        } catch (Exception\StateException $e) {
202 74
            $this->addState($transition->getState());
203
        }
204 149
        foreach ($transition->getInitialStates() as $state) {
205
            try {
206 149
                $this->getState($state);
207 88
            } catch (Exception\StateException $e) {
208 29
                $this->addState($state);
209
            }
210 148
            $state = $this->getState($state);
211 148
            if ($state instanceof State) {
212 148
                $state->addTransition($transition);
213 72
            }
214 72
        }
215 148
    }
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 155
                $name,
226 155
                get_class($this->getObject()),
227 125
                $this->getGraph()
228 62
            ));
229
        }
230
231 46
        return $this->transitions[$name];
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 155
    public function getState($name)
238
    {
239 155
        $name = (string) $name;
240
241 155
        if (!isset($this->states[$name])) {
242 80
            throw new Exception\StateException(sprintf(
243 80
                'Unable to find a state called "%s" on object "%s" with graph "%s".',
244 80
                $name,
245 80
                get_class($this->getObject()),
246 78
                $this->getGraph()
247 38
            ));
248
        }
249
250 154
        return $this->states[$name];
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256 4
    public function getTransitions()
257
    {
258 4
        return array_keys($this->transitions);
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264 4
    public function getStates()
265
    {
266 4
        return array_keys($this->states);
267
    }
268
269
    /**
270
     * {@inheritdoc}
271
     */
272 121
    public function setObject($object)
273
    {
274 121
        $this->object = $object;
275 121
    }
276
277
    /**
278
     * {@inheritdoc}
279
     */
280 175
    public function getObject()
281
    {
282 175
        return $this->object;
283
    }
284
285
    /**
286
     * {@inheritdoc}
287
     */
288 95
    public function getCurrentState()
289
    {
290 95
        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 18
    protected function findInitialState()
301
    {
302 18
        foreach ($this->states as $state) {
303 18
            if (State::TYPE_INITIAL === $state->getType()) {
304 18
                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 5
    public function setStateAccessor(StateAccessorInterface $stateAccessor)
335
    {
336 5
        $this->stateAccessor = $stateAccessor;
337 5
    }
338
339
    /**
340
     * {@inheritdoc}
341
     */
342 15
    public function hasStateAccessor()
343
    {
344 15
        return null !== $this->stateAccessor;
345
    }
346
347
    /**
348
     * {@inheritdoc}
349
     */
350 20
    public function setGraph($graph)
351
    {
352 20
        $this->graph = $graph;
353 20
    }
354
355
    /**
356
     * {@inheritdoc}
357
     */
358 143
    public function getGraph()
359
    {
360 143
        return $this->graph;
361
    }
362
363
    /**
364
     * {@inheritDoc}
365
     */
366 8
    public function findStateWithProperty($property, $value = null)
367
    {
368 8
        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...
369 8
            array_map(
370
                function (State $state) {
371 8
                    return $state->getName();
372 8
                },
373 8
                array_filter(
374 8
                    $this->states,
375 8
                    function (State $state) use ($property, $value) {
376 8
                        if (!$state->has($property)) {
377 8
                            return false;
378
                        }
379
380 8
                        if (null !== $value && $state->get($property) !== $value) {
381 4
                            return false;
382
                        }
383
384 8
                        return true;
385 4
                    }
386 4
                )
387 4
            )
388 4
        );
389
    }
390
391
    /**
392
     * Dispatches event for the transition
393
     *
394
     * @param TransitionInterface $transition
395
     * @param TransitionEvent $event
396
     * @param type $transitionState
397
     */
398 43
    private function dispatchTransitionEvent(TransitionInterface $transition, TransitionEvent $event, $transitionState)
399
    {
400 43
        $this->dispatcher->dispatch($transitionState, $event);
401 43
        $this->dispatcher->dispatch($transitionState.'.'.$transition->getName(), $event);
402 43
        if (null !== $this->getGraph()) {
403 9
            $this->dispatcher->dispatch($transitionState.'.'.$this->getGraph().'.'.$transition->getName(), $event);
404 4
        }
405 43
    }
406
}
407