Completed
Push — master ( 90aaa2...59aa59 )
by Yohan
03:13
created

StateMachine::findStateWithProperty()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

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