1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace OtherCode \ComplexHeart \Domain ;
6+
7+ use OtherCode \ComplexHeart \Domain \Exceptions \StateException ;
8+ use OtherCode \ComplexHeart \Domain \ValueObjects \EnumValue ;
9+
10+ /**
11+ * Class State
12+ *
13+ * @see https://en.wikipedia.org/wiki/State_pattern
14+ * @author Unay Santisteban <[email protected] > 15+ * @package OtherCode\ComplexHeart\Domain
16+ */
17+ abstract class State extends EnumValue
18+ {
19+ private const DEFAULT = 'default ' ;
20+
21+ private string $ defaultState ;
22+
23+ /**
24+ * Mapping of the available transitions with the transition function.
25+ *
26+ * @var array<string, callable|null>
27+ */
28+ private array $ transitions = [];
29+
30+ /**
31+ * State constructor.
32+ *
33+ * @param string $value
34+ */
35+ public function __construct (string $ value = self ::DEFAULT )
36+ {
37+ $ this ->configure ();
38+
39+ parent ::__construct (
40+ $ value === self ::DEFAULT
41+ ? $ this ->defaultState
42+ : $ value
43+ );
44+ }
45+
46+ /**
47+ * Configure the state machine with the specific transitions.
48+ *
49+ * $this->defaultState('SOME_STATE')
50+ * ->allowTransition('SOME_STATE', 'OTHER_STATE')
51+ * ->allowTransition('SOME_STATE', 'ANOTHER_STATE');
52+ */
53+ abstract protected function configure (): void ;
54+
55+ /**
56+ * Set the default value for the state machine.
57+ *
58+ * @param string $state
59+ *
60+ * @return $this
61+ */
62+ protected function defaultState (string $ state ): State
63+ {
64+ $ this ->defaultState = $ state ;
65+ return $ this ;
66+ }
67+
68+ /**
69+ * Define the allowed state transitions.
70+ *
71+ * @param string $from
72+ * @param string $to
73+ * @param callable|null $transition
74+ *
75+ * @return $this
76+ * @throws StateException
77+ */
78+ protected function allowTransition (string $ from , string $ to , ?callable $ transition = null ): State
79+ {
80+ if (!static ::isValid ($ from )) {
81+ throw StateException::stateNotFound ($ from , static ::getValues ());
82+ }
83+
84+ if (!static ::isValid ($ to )) {
85+ throw StateException::stateNotFound ($ to , static ::getValues ());
86+ }
87+
88+ if (is_null ($ transition )) {
89+ $ key = $ this ->getTransitionKey ($ from , $ to );
90+ if ($ this ->canCall ($ method = $ this ->getStringKey ($ key , 'from ' ))) {
91+ // compute method using the exactly transition key: fromOneToAnother
92+ $ transition = [$ this , $ method ];
93+ } elseif ($ this ->canCall ($ method = $ this ->getStringKey ($ to , 'to ' ))) {
94+ // compute the method using only the $to state: toAnother
95+ $ transition = [$ this , $ method ];
96+ }
97+ }
98+
99+ $ this ->transitions [$ this ->getTransitionKey ($ from , $ to )] = $ transition ;
100+
101+ return $ this ;
102+ }
103+
104+ /**
105+ * Set the value and executed the "on{State}" method if it's available.
106+ *
107+ * This method is automatically invoked from the HasAttributes trait
108+ * on set() the value property.
109+ *
110+ * @param string $value
111+ *
112+ * @return string
113+ */
114+ protected function setValueValue (string $ value ): string
115+ {
116+ $ onSetStateMethod = $ this ->getStringKey ($ value , 'on ' );
117+ if ($ this ->canCall ($ onSetStateMethod )) {
118+ call_user_func_array ([$ this , $ onSetStateMethod ], []);
119+ }
120+ return $ value ;
121+ }
122+
123+ /**
124+ * Compute the transition key using the $from and $to strings.
125+ *
126+ * @param string $from
127+ * @param string $to
128+ *
129+ * @return string
130+ */
131+ private function getTransitionKey (string $ from , string $ to ): string
132+ {
133+ return $ this ->getStringKey ("{$ from }_to_ {$ to }" );
134+ }
135+
136+ /**
137+ * Check if the given $from $to transition is allowed.
138+ *
139+ * @param string $from
140+ * @param string $to
141+ *
142+ * @return bool
143+ */
144+ private function isTransitionAllowed (string $ from , string $ to ): bool
145+ {
146+ return array_key_exists ($ this ->getTransitionKey ($ from , $ to ), $ this ->transitions );
147+ }
148+
149+ /**
150+ * Execute the transition $from oneState $to another.
151+ *
152+ * @param string $to
153+ * @param mixed ...$arguments
154+ *
155+ * @return $this
156+ * @throws StateException
157+ */
158+ public function transitionTo (string $ to , ...$ arguments ): State
159+ {
160+ if (!static ::isValid ($ to )) {
161+ throw StateException::stateNotFound ($ to , static ::getValues ());
162+ }
163+
164+ if (!$ this ->isTransitionAllowed ($ this ->value , $ to )) {
165+ throw StateException::transitionNotAllowed ($ this ->value , $ to );
166+ }
167+
168+ if ($ transition = $ this ->transitions [$ this ->getTransitionKey ($ this ->value , $ to )]) {
169+ call_user_func_array ($ transition , $ arguments );
170+ }
171+
172+ $ this ->set ('value ' , $ to );
173+
174+ return $ this ;
175+ }
176+ }
0 commit comments