Completed
Push — symfony-5-php-8 ( 868ef5...f707a4 )
by Yohan
03:17
created

StateMachine   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 384
Duplicated Lines 0 %

Coupling/Cohesion

Dependencies 13

Test Coverage

Coverage 89.24%

Importance

Changes 0
Metric Value
wmc 50
cbo 13
dl 0
loc 384
ccs 141
cts 158
cp 0.8924
rs 8.4
c 0
b 0
f 0

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 = 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 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);
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected ')'
Loading history...
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);
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(
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