Completed
Push — symfony-5-php-8 ( f707a4...affbd0 )
by Yohan
03:10
created

StateMachine::findStateWithProperty()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 18
cts 18
cp 1
rs 9.536
c 0
b 0
f 0
cc 4
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
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 185
    public function __construct(
74
        $object = null,
75
        EventDispatcherInterface $dispatcher = null,
76
        StateAccessorInterface $stateAccessor = null
77
    ) {
78 185
        $this->object = $object;
79 185
        $this->dispatcher = $dispatcher ?: new EventDispatcher();
80 185
        $this->stateAccessor = $stateAccessor ?: new PropertyPathStateAccessor();
81 185
    }
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(sprintf(
96
               'StateMachine can\'t be initialized because the defined property_path of object "%s" does not exist.',
97
                $this->getObject() ? get_class($this->getObject()) : null
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 8
        }
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
                $this->getObject() ? get_class($this->getObject()) : null,
128 5
                $this->getGraph()
129 2
            ));
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 185
    public function addState($state)
168
    {
169 185
        if (!$state instanceof StateInterface) {
170 175
            $state = new State($state);
171 70
        }
172
173 185
        $this->states[$state->getName()] = $state;
174 185
    }
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 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 62
        }
195
196 180
        $this->transitions[$transition->getName()] = $transition;
197
198
        // We add missings states to the State Machine
199
        try {
200 180
            $this->getState($transition->getState());
201 126
        } catch (Exception\StateException $e) {
202 90
            $this->addState($transition->getState());
203
        }
204 180
        foreach ($transition->getInitialStates() as $state) {
205
            try {
206 180
                $this->getState($state);
207 93
            } catch (Exception\StateException $e) {
208 35
                $this->addState($state);
209
            }
210 180
            $state = $this->getState($state);
211 180
            if ($state instanceof State) {
212 180
                $state->addTransition($transition);
213 72
            }
214 72
        }
215 180
    }
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
                $this->getObject() ? get_class($this->getObject()) : null,
227 155
                $this->getGraph()
228 62
            ));
229
        }
230
231 55
        return $this->transitions[$name];
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 185
    public function getState($name)
238
    {
239 185
        $name = (string) $name;
240
241 185
        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 95
                $name,
245 95
                $this->getObject() ? get_class($this->getObject()) : null,
246 95
                $this->getGraph()
247 38
            ));
248
        }
249
250 185
        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
            $this->getObject() ? get_class($this->getObject()) : null,
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 175
    public function getGraph()
359
    {
360 175
        return $this->graph;
361
    }
362
363
    /**
364
     * {@inheritDoc}
365
     */
366 10
    public function findStateWithProperty($property, $value = null)
367
    {
368 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...
369 10
            array_map(
370 2
                function (State $state) {
371 10
                    return $state->getName();
372 10
                },
373 10
                array_filter(
374 10
                    $this->states,
375 10
                    function (State $state) use ($property, $value) {
376 10
                        if (!$state->has($property)) {
377 10
                            return false;
378
                        }
379
380 10
                        if (null !== $value && $state->get($property) !== $value) {
381 5
                            return false;
382
                        }
383
384 10
                        return true;
385 6
                    }
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 50
    private function dispatchTransitionEvent(TransitionInterface $transition, TransitionEvent $event, $transitionState)
399
    {
400 50
        $this->dispatcher->dispatch($transitionState, $event);
401 50
        $this->dispatcher->dispatch($transitionState.'.'.$transition->getName(), $event);
402 50
        if (null !== $this->getGraph()) {
403 10
            $this->dispatcher->dispatch($transitionState.'.'.$this->getGraph().'.'.$transition->getName(), $event);
404 4
        }
405 50
    }
406
}
407