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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
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 |
||
180 | class StateMachine { |
||
181 | |||
182 | /** |
||
183 | * The context instance that provides the context for the statemachine to |
||
184 | * operate in. |
||
185 | * |
||
186 | * @var Context |
||
187 | */ |
||
188 | private $context; |
||
189 | |||
190 | /** |
||
191 | * The available states. |
||
192 | * these should be loaded via transitions |
||
193 | * A hashmap where the key is the state name and the value is |
||
194 | * the State. |
||
195 | * |
||
196 | * @var State[] |
||
197 | */ |
||
198 | private $states = array(); |
||
199 | |||
200 | /** |
||
201 | * The available transitions. |
||
202 | * these should be loaded via the 'addTransition' method. |
||
203 | * A hashmap where the key is the transition name and the value is |
||
204 | * the Transition. |
||
205 | * |
||
206 | * @var Transition[] |
||
207 | */ |
||
208 | private $transitions = array(); |
||
209 | |||
210 | /** |
||
211 | * the current state |
||
212 | * |
||
213 | * @var State |
||
214 | */ |
||
215 | private $state; |
||
216 | |||
217 | // ######################## TRANSITION METHODS ############################# |
||
218 | |||
219 | |||
220 | /** |
||
221 | * Constructor |
||
222 | * |
||
223 | * @param Context $context |
||
224 | * a fully configured context providing all the relevant |
||
225 | * parameters/dependencies to be able to run this statemachine for an entity. |
||
226 | */ |
||
227 | 58 | public function __construct(Context $context) |
|
232 | |||
233 | /** |
||
234 | * Apply a transition by name. |
||
235 | * |
||
236 | * this type of handling is found in moore machines. |
||
237 | * |
||
238 | * @param string $transition_name |
||
239 | * convention: <state-from>_to_<state-to> |
||
240 | * @param string $message optional message. this can be used by the persistence adapter |
||
241 | * to be part of the transition history to provide extra information about the transition. |
||
242 | * @return boolean true if the transition was made |
||
243 | * @throws Exception in case something went disastrously wrong or if the |
||
244 | * transition does not exist. An exception will lead to a (partially |
||
245 | * or fully) failed transition. |
||
246 | * @link https://en.wikipedia.org/wiki/Moore_machine |
||
247 | */ |
||
248 | 4 | public function transition($transition_name, $message = null) |
|
253 | |||
254 | /** |
||
255 | * Try to apply a transition from the current state by handling an event |
||
256 | * string as a trigger. |
||
257 | * If the event is applicable for a transition then that transition from the |
||
258 | * current state will be applied. |
||
259 | * If there are multiple transitions possible for the event, the transitions |
||
260 | * will be tried until one of them is possible. |
||
261 | * |
||
262 | * This type of (event/trigger) handling is found in mealy machines. |
||
263 | * |
||
264 | * @param string $event |
||
265 | * in case the transition will be triggered by an event code |
||
266 | * (mealy machine) |
||
267 | * this will also match on the transition name |
||
268 | * (<state_to>_to_<state_from>) |
||
269 | * @param string $message optional message. this can be used by the persistence adapter |
||
270 | * to be part of the transition history to provide extra information about the transition. |
||
271 | * @return bool true in case a transition was triggered by the event, false |
||
272 | * otherwise |
||
273 | * @throws Exception in case something went horribly wrong |
||
274 | * @link https://en.wikipedia.org/wiki/Mealy_machine |
||
275 | * @link http://martinfowler.com/books/dsl.html for event handling |
||
276 | * statemachines |
||
277 | */ |
||
278 | 10 | public function handle($event, $message = null) |
|
289 | |||
290 | /** |
||
291 | * Have the statemachine do the first possible transition. |
||
292 | * The first possible transition is based on the configuration of |
||
293 | * the guard logic and the current state of the statemachine. |
||
294 | * |
||
295 | * This method is the main way to have the statemachine do useful work. |
||
296 | * |
||
297 | * TRICKY: Be careful when using this function, since all guard logic must |
||
298 | * be mutually exclusive! If not, you might end up performing the state |
||
299 | * transition with priority n when you really want to perform transition |
||
300 | * n+1. |
||
301 | * |
||
302 | * An alternative is to use the 'transition' method to target 1 transition |
||
303 | * specifically: |
||
304 | * $statemachine->transition('a_to_b'); |
||
305 | * So you are always sure that you are actually doing the intented |
||
306 | * transition instead of relying on the configuration and guard logic (which |
||
307 | * *might* not be correctly implemented, leading to transitions that would |
||
308 | * normally not be executed). |
||
309 | * |
||
310 | * @param string $message optional message. this can be used by the persistence adapter |
||
311 | * to be part of the transition history to provide extra information about the transition. |
||
312 | * @return boolean true if a transition was applied. |
||
313 | * @throws Exception in case something went awfully wrong. |
||
314 | * |
||
315 | */ |
||
316 | 8 | public function run($message = null) |
|
333 | |||
334 | /** |
||
335 | * run a statemachine until it cannot run any transition in the current |
||
336 | * state or until it is in a final state. |
||
337 | * |
||
338 | * when using cyclic graphs, you could get into an infinite loop between |
||
339 | * states. design your machine correctly. |
||
340 | * @param string $message optional message. this can be used by the persistence adapter |
||
341 | * to be part of the transition history to provide extra information about the transition. |
||
342 | * @return int the number of sucessful transitions made. |
||
343 | * @throws Exception in case something went badly wrong. |
||
344 | */ |
||
345 | 4 | public function runToCompletion($message = null) |
|
362 | |||
363 | /** |
||
364 | * Check if an existing transition on the curent state is allowed by the |
||
365 | * guard logic. |
||
366 | * |
||
367 | * @param string $transition_name |
||
368 | * convention: <state-from>_to_<state-to> |
||
369 | * @return boolean |
||
370 | */ |
||
371 | 4 | public function canTransition($transition_name) |
|
376 | |||
377 | /** |
||
378 | * checks if one or more transitions are possible and/or allowed for the |
||
379 | * current state when triggered by an event |
||
380 | * |
||
381 | * @param string $event |
||
382 | * @return boolean false if no transitions possible or existing |
||
383 | */ |
||
384 | 3 | public function canHandle($event) |
|
394 | |||
395 | /** |
||
396 | * check if the current state has one or more transitions that can be |
||
397 | * triggered by an event |
||
398 | * |
||
399 | * @param string $event |
||
400 | * @return boolean |
||
401 | */ |
||
402 | 3 | public function hasEvent($event) |
|
410 | |||
411 | // ######################## CORE TRANSITION & TEMPLATE METHODS ############################# |
||
412 | |||
413 | |||
414 | /** |
||
415 | * Perform a transition by specifiying the transitions' name from a state |
||
416 | * that the transition is allowed to run. |
||
417 | * |
||
418 | * @param Transition $transition |
||
419 | * @param string $message optional message. this can be used by the persistence adapter |
||
420 | * to be part of the transition history to provide extra information about the transition. |
||
421 | * @return boolean true if the transition was succesful |
||
422 | * @throws Exception in case something went horribly wrong |
||
423 | * @link https://en.wikipedia.org/wiki/Template_method_pattern |
||
424 | */ |
||
425 | 17 | private function performTransition(Transition $transition, $message = null) |
|
448 | |||
449 | /** |
||
450 | * Template method to call a hook and to call a possible method |
||
451 | * defined on the domain object/contextual entity |
||
452 | * |
||
453 | * @param Transition $transition |
||
454 | * @return boolean if false, the transition and its' associated logic will |
||
455 | * not take place |
||
456 | * @throws Exception in case something went horribly wrong |
||
457 | */ |
||
458 | 17 | private function doCheckCanTransition(Transition $transition) |
|
487 | |||
488 | /** |
||
489 | * the exit state action method |
||
490 | * |
||
491 | * @param Transition $transition |
||
492 | */ |
||
493 | 16 | private function doExitState(Transition $transition) |
|
502 | |||
503 | /** |
||
504 | * the transition action method |
||
505 | * |
||
506 | * @param Transition $transition |
||
507 | * @param string $message optional message. this can be used by the persistence adapter |
||
508 | * to be part of the transition history to provide extra information about the transition. |
||
509 | */ |
||
510 | 16 | private function doTransition(Transition $transition, $message = null) |
|
524 | |||
525 | /** |
||
526 | * the enter state action method |
||
527 | * |
||
528 | * @param Transition $transition |
||
529 | */ |
||
530 | 16 | private function doEnterState(Transition $transition) |
|
541 | |||
542 | // ######################## SUPPORTING METHODS ############################# |
||
543 | /** |
||
544 | * All known/loaded states for this statemachine |
||
545 | * |
||
546 | * @return State[] |
||
547 | */ |
||
548 | 45 | public function getStates() |
|
552 | |||
553 | /** |
||
554 | * get a state by name. |
||
555 | * |
||
556 | * @param string $name |
||
557 | * @return State or null if not found |
||
558 | */ |
||
559 | 44 | public function getState($name) |
|
563 | |||
564 | /** |
||
565 | * Add a state. without a transition. |
||
566 | * Normally, you would not use this method directly but instead use |
||
567 | * addTransition to add the transitions with the states in one go. |
||
568 | * |
||
569 | * This method makes sense if you would want to load the statemachine with states |
||
570 | * and not transitions, and then add the transitions with regex states. |
||
571 | * This saves you the hassle of adding transitions before you add the |
||
572 | * regex transitions (just so the states are already known on the machine). |
||
573 | * |
||
574 | * in case a State is not used in a Transition, it will be orphaned and not |
||
575 | * reachable via other states. |
||
576 | * |
||
577 | * @param State $state |
||
578 | * @return boolean true if the state was not know to the machine or wasn't added, false otherwise. |
||
579 | */ |
||
580 | 44 | public function addState(State $state) |
|
593 | |||
594 | /** |
||
595 | * sets the state as the current state and on the backend. |
||
596 | * This should only be done: |
||
597 | * - initially, right after a machine has been created, to set it in a |
||
598 | * certain state if the state has not been persisted before. |
||
599 | * - when changing context (since this resets the current state) via |
||
600 | * $machine->setState($machine->getCurrentState()) |
||
601 | * |
||
602 | * This method allows you to bypass the transition guards and the transition |
||
603 | * logic. no exit/entry/transition logic will be performed |
||
604 | * |
||
605 | * @param State $state |
||
606 | * @param string $message optional message. this can be used by the persistence adapter |
||
607 | * to be part of the transition history to provide extra information about the transition. |
||
608 | * @throws Exception in case the state is not valid/known for this machine |
||
609 | */ |
||
610 | 16 | public function setState(State $state, $message = null) |
|
622 | |||
623 | /** |
||
624 | * add state information to the persistence layer if it is not there. |
||
625 | * |
||
626 | * Used to mark the initial construction of a statemachine at a certain |
||
627 | * point in time. This method only makes sense the first time a statemachine |
||
628 | * is initialized and used since it will do nothing once a transition has been made. |
||
629 | * |
||
630 | * It will set the initial state on the backend which sets |
||
631 | * the construction time. |
||
632 | * |
||
633 | * Make sure that the transitions and states are loaded before you call this method. |
||
634 | * in other words: the machine should be ready to go. |
||
635 | * |
||
636 | * @param string $message optional message. this can be used by the persistence adapter |
||
637 | * to provide extra information in the history of the machine transitions, |
||
638 | * in this case, about the first adding of this machine to the persistence layer. |
||
639 | * @return boolean true if not persisted before, false otherwise |
||
640 | */ |
||
641 | 5 | public function add($message = null) |
|
645 | |||
646 | /** |
||
647 | * gets the current state (or retrieve it from the backend if not set). |
||
648 | * |
||
649 | * the state will be: |
||
650 | * - the state that was explicitely set via setState |
||
651 | * - the state we have moved to after the last transition |
||
652 | * - the initial state. if we haven't had a transition yet and no current |
||
653 | * state has been set, the initial state will be retrieved (the state with State::TYPE_INITIAL) |
||
654 | * |
||
655 | * @return State |
||
656 | * @throws Exception in case there is no valid current state found |
||
657 | */ |
||
658 | 18 | public function getCurrentState() |
|
674 | |||
675 | /** |
||
676 | * Get the initial state, the only state with type State::TYPE_INITIAL |
||
677 | * |
||
678 | * This method can be used to 'add' the state information to the backend via |
||
679 | * the context/persistence adapter when a machine has been initialized for |
||
680 | * the first time. |
||
681 | * |
||
682 | * @param boolean $allow_null optional |
||
683 | * @return State (or null or Exception, only when statemachine is improperly |
||
684 | * loaded) |
||
685 | * @throws Exception if $allow_null is false an no inital state was found |
||
686 | */ |
||
687 | 22 | public function getInitialState($allow_null = false) |
|
700 | |||
701 | /** |
||
702 | * All known/loaded transitions for this statemachine |
||
703 | * |
||
704 | * @return Transition[] |
||
705 | */ |
||
706 | 22 | public function getTransitions() |
|
710 | |||
711 | /** |
||
712 | * get a transition by name. |
||
713 | * |
||
714 | * @param string $name |
||
715 | * convention: <state_from>_to_<state_to> |
||
716 | * @return Transition or null if not found |
||
717 | */ |
||
718 | 43 | public function getTransition($name) |
|
722 | |||
723 | /** |
||
724 | * Add a fully configured transition to the machine. |
||
725 | * |
||
726 | * It is possible to add transition that have 'regex' states: states that |
||
727 | * have a name in the format of 'regex:/<regex-here>/' or 'not-regex:/<regex-here>/'. |
||
728 | * When adding a transition with a regex state, it will be matched against all currently |
||
729 | * known states if there is a match. If you just want to use regex transitions, |
||
730 | * it might be preferable to store some states via 'addState' first, so the |
||
731 | * |
||
732 | * Self transitions for regex states are disallowed by default |
||
733 | * since you would probably only want to do that explicitly. Regex states |
||
734 | * can be both the 'to' and the 'from' state of a transition. |
||
735 | * |
||
736 | * Transitions from a 'final' type of state are not allowed. |
||
737 | * |
||
738 | * the order in which transitions are added matters insofar that when a |
||
739 | * StateMachine::run() is called, the first Transition for the current State |
||
740 | * will be tried first. |
||
741 | * |
||
742 | * Since a transition has complete knowledge about it's states, |
||
743 | * the addition of a transition will also trigger the adding of the |
||
744 | * to and from state on this class. |
||
745 | * |
||
746 | * this method can also be used to add a Transition directly (instead of via |
||
747 | * a loader). Make sure that transitions that share a common State use the same |
||
748 | * instance of that State object and vice versa. |
||
749 | * |
||
750 | * @param Transition $transition |
||
751 | * @param boolean $allow_self_transition_by_regex optional: to allow regexes to set a self transition. |
||
752 | * @return int a count of how many transitions were added. In case of a regex transition this might be |
||
753 | * multiple and in case a transition already exists it might be 0. |
||
754 | */ |
||
755 | 43 | public function addTransition(Transition $transition, $allow_self_transition_by_regex = false) |
|
792 | |||
793 | /** |
||
794 | * Add the transition, after it has previously been checked that is did not |
||
795 | * contain states with a regex. |
||
796 | * |
||
797 | * @param Transition $transition |
||
798 | * @return boolean true in case it was added. false otherwise |
||
799 | */ |
||
800 | 43 | protected function addTransitionWithoutRegex(Transition $transition) |
|
838 | |||
839 | /** |
||
840 | * Get the current context |
||
841 | * |
||
842 | * @return Context |
||
843 | */ |
||
844 | 58 | public function getContext() |
|
848 | |||
849 | /** |
||
850 | * set the context on the statemachine and provide bidirectional |
||
851 | * association. |
||
852 | * |
||
853 | * change the context for a statemachine that already has a context. |
||
854 | * When the context is changed, but it is for the same statemachine (with |
||
855 | * the same transitions), the statemachine can be used directly with the |
||
856 | * new context. |
||
857 | * |
||
858 | * The current state is reset to whatever state the machine should be in |
||
859 | * (either the initial state or the stored state) whenever a context change |
||
860 | * is made. |
||
861 | * |
||
862 | * we can change context to: |
||
863 | * - switch builders/persistence adapters at runtime |
||
864 | * - reuse the statemachine for a different entity so we do not |
||
865 | * have to load the statemachine with the same transition definitions |
||
866 | * |
||
867 | * @param Context $context |
||
868 | * @throws Exception |
||
869 | */ |
||
870 | 58 | public function setContext(Context $context) |
|
884 | |||
885 | // #################### LOW LEVEL HELPER METHODS ######################### |
||
886 | |||
887 | |||
888 | /** |
||
889 | * called whenever an exception occurs from inside 'performTransition()' |
||
890 | * can be used for logging etc. in some sort of history structure in the persistence layer |
||
891 | * |
||
892 | * @param Transition $transition |
||
893 | * @param Exception $e |
||
894 | */ |
||
895 | 2 | protected function handleTransitionException(Transition $transition, Exception $e) |
|
903 | |||
904 | /** |
||
905 | * Always throws an izzum exception (converts a non-izzum exception to an |
||
906 | * izzum exception) |
||
907 | * |
||
908 | * @param \Exception $e |
||
909 | * @param int $code |
||
910 | * if the exception is not of type Exception, wrap it and use |
||
911 | * this code. |
||
912 | * @param Transition $transition |
||
913 | * optional. if set, we handle it as a transition exception too |
||
914 | * so it can be logged or handled |
||
915 | * @throws Exception an izzum exception |
||
916 | */ |
||
917 | 2 | protected function handlePossibleNonStatemachineException(\Exception $e, $code, $transition = null) |
|
925 | |||
926 | /** |
||
927 | * This method is used to trigger an event on the statemachine and |
||
928 | * delegates the actuall call to the 'handle' method |
||
929 | * |
||
930 | * $statemachine->triggerAnEvent() actually calls |
||
931 | * $this->handle('triggerAnEvent') |
||
932 | * |
||
933 | * This is also very useful if you use object composition to include a |
||
934 | * statemachine in your domain model. The domain model itself can then use it's own |
||
935 | * __call implementation to directly delegate to the statemachines' __call method. |
||
936 | * $model->walk() will actually call $model->statemachine->walk() which will |
||
937 | * then call $model->statemachine->handle('walk'); |
||
938 | * |
||
939 | * since transition event names default to the transition name, it is |
||
940 | * possible to execute this kind of code (if the state names contain allowed |
||
941 | * characters): |
||
942 | * $statemachine-><state_from>_to_<state_to>(); |
||
943 | * $statemachine->eventName(); |
||
944 | * $statemachine->eventName('this is an informational message about why we do this transition'); |
||
945 | * |
||
946 | * |
||
947 | * @param string $name |
||
948 | * the name of the unknown method called |
||
949 | * @param array $arguments |
||
950 | * an array of arguments (if any). |
||
951 | * an argument could be $message (informational message for the transition) |
||
952 | * @return bool true in case a transition was triggered by the event, false |
||
953 | * otherwise |
||
954 | * @throws Exception in case the transition is not possible via the guard |
||
955 | * logic (Rule) |
||
956 | * @link https://en.wikipedia.org/wiki/Object_composition |
||
957 | */ |
||
958 | 8 | public function __call($name, $arguments) |
|
964 | |||
965 | /** |
||
966 | * Helper method to generically call methods on the $object. |
||
967 | * Try to call a method on the contextual entity / domain model ONLY IF the |
||
968 | * method exists.Any arguments passed to this method will be passed on to the method |
||
969 | * called on the entity. |
||
970 | * |
||
971 | * @param mixed $object |
||
972 | * the object on which we want to call the method |
||
973 | * @param string $method |
||
974 | * the method to call on the object |
||
975 | * @return boolean|mixed |
||
976 | */ |
||
977 | 17 | protected function callEventHandler($object, $method) |
|
994 | |||
995 | /** |
||
996 | * Helper method that gets the Transition object from a transition name or |
||
997 | * throws an exception. |
||
998 | * |
||
999 | * @param string $name |
||
1000 | * convention: <state_from>_to_<state_to> |
||
1001 | * @return Transition |
||
1002 | * @throws Exception |
||
1003 | */ |
||
1004 | 4 | private function getTransitionWithNullCheck($name) |
|
1012 | |||
1013 | 5 | public function toString($elaborate = false) |
|
1025 | |||
1026 | 1 | public function __toString() |
|
1030 | |||
1031 | // ###################### HOOK METHODS ####################### |
||
1032 | |||
1033 | |||
1034 | /** |
||
1035 | * hook method. |
||
1036 | * override in subclass if necessary. |
||
1037 | * Before a transition is checked to be possible, you can add domain |
||
1038 | * specific logic here by overriding this method in a subclass. |
||
1039 | * In an overriden implementation of this method you can stop the transition |
||
1040 | * by returning false from this method. |
||
1041 | * |
||
1042 | * @param Transition $transition |
||
1043 | * @return boolean if false, the transition and it's associated logic will |
||
1044 | * not take place |
||
1045 | */ |
||
1046 | 16 | protected function _onCheckCanTransition(Transition $transition) |
|
1051 | |||
1052 | /** |
||
1053 | * hook method. |
||
1054 | * override in subclass if necessary. |
||
1055 | * Called before each transition will run and execute the associated |
||
1056 | * transition logic. |
||
1057 | * A hook to implement in subclasses if necessary, to do stuff such as |
||
1058 | * dispatching events, locking an entity, logging, begin transaction via |
||
1059 | * persistence |
||
1060 | * layer etc. |
||
1061 | * |
||
1062 | * @param Transition $transition |
||
1063 | */ |
||
1064 | 15 | protected function _onExitState(Transition $transition) |
|
1066 | |||
1067 | /** |
||
1068 | * hook method. |
||
1069 | * override in subclass if necessary. |
||
1070 | * |
||
1071 | * @param Transition $transition |
||
1072 | */ |
||
1073 | 15 | protected function _onTransition(Transition $transition) |
|
1075 | |||
1076 | /** |
||
1077 | * hook method. |
||
1078 | * override in subclass if necessary. |
||
1079 | * Called after each transition has run and has executed the associated |
||
1080 | * transition logic. |
||
1081 | * a hook to implement in subclasses if necessary, to do stuff such as |
||
1082 | * dispatching events, unlocking an entity, logging, cleanup, commit |
||
1083 | * transaction via |
||
1084 | * the persistence layer etc. |
||
1085 | * |
||
1086 | * @param Transition $transition |
||
1087 | */ |
||
1088 | 15 | protected function _onEnterState(Transition $transition) |
|
1090 | } |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.