1
|
|
|
<?php |
2
|
|
|
namespace izzum\statemachine; |
3
|
|
|
use izzum\statemachine\Context; |
4
|
|
|
use izzum\statemachine\State; |
5
|
|
|
use izzum\statemachine\Transition; |
6
|
|
|
use izzum\statemachine\Exception; |
7
|
|
|
use izzum\statemachine\utils\Utils; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* A statemachine is used to execute transitions from certain states to other |
11
|
|
|
* states, for an entity that represents a domain object, by applying guard logic |
12
|
|
|
* to see if a transition is allowed and transition logic to process the transition. |
13
|
|
|
* |
14
|
|
|
* |
15
|
|
|
* PACKAGE PHILOSOPHY |
16
|
|
|
* This whole package strives to follow the open/closed principle, making it |
17
|
|
|
* open for extension (adding your own logic through subclassing) but closed |
18
|
|
|
* for modification. For that purpose, we provide Loader interfaces, Builders |
19
|
|
|
* and Persistence Adapters for the backend implementation. States and |
20
|
|
|
* Transitions can also be subclassed to store data and provide functionality. |
21
|
|
|
* |
22
|
|
|
* Following the same philosophy: if you want more functionality in the |
23
|
|
|
* statemachine, you can use the provided methods and override them in your |
24
|
|
|
* subclass. multiple hooks are provided. Most functionality should be provided by |
25
|
|
|
* using the diverse ways of interacting with the statemachine: callables can be injected, |
26
|
|
|
* commands can be injected, event handlers can be defined and hooks can be overriden. |
27
|
|
|
* |
28
|
|
|
* We have provided multiple persistance backends to function as a data store to store all |
29
|
|
|
* relevant information for a statemachine including configuration, transition history and current states. |
30
|
|
|
* - relational databases: postgresql, mysql, sqlite |
31
|
|
|
* - nosql key/value: redis |
32
|
|
|
* - nosql document based: mongodb |
33
|
|
|
* Memory and session backend adapters can be used to temporarily store the state information. |
34
|
|
|
* yaml, json and xml loaders can be used to load configuration data from files containing those |
35
|
|
|
* data formats. |
36
|
|
|
* |
37
|
|
|
* Examples are provided in the 'examples' folder and serve to highlight some of the |
38
|
|
|
* features and the way to work with the package. The unittests can serve as examples too, |
39
|
|
|
* including interaction with databases. |
40
|
|
|
* |
41
|
|
|
* |
42
|
|
|
* ENVIRONMENTS OF USAGE: |
43
|
|
|
* - 1: a one time process: |
44
|
|
|
* -- on webpages where there are page refreshes in between (use session/pdo adapter) |
45
|
|
|
* see 'examples/session' |
46
|
|
|
* -- an api where succesive calls are made (use pdo adapter) |
47
|
|
|
* -- cron jobs (pdo adapter) |
48
|
|
|
* - 2: a longer running process: |
49
|
|
|
* -- a php daemon that runs as a background process (use memory adapter) |
50
|
|
|
* -- an interactive shell environment (use memory adapter) |
51
|
|
|
* see 'examples/interactive' |
52
|
|
|
* |
53
|
|
|
* |
54
|
|
|
* DESCRIPTION OF THE 4 MAIN MODELS OF USAGE: |
55
|
|
|
* - 1: DELEGATION: Use an existing domain model. You can use a subclass of the |
56
|
|
|
* AbstractFactory to get a StateMachine, since that will put the creation of |
57
|
|
|
* all the relevant Contextual classes and Transitions in a reusable model. |
58
|
|
|
* usage: This is the most formal, least invasive and powerful model of usage, but the most complex. |
59
|
|
|
* Use Rules and Commands to interact with your domain model without altering a domain model |
60
|
|
|
* to work with the statemachine. |
61
|
|
|
* see 'examples/trafficlight' |
62
|
|
|
* - 2: INHERITANCE: Subclass a statemachine. Build the full Context and all transitions in |
63
|
|
|
* the constructor of your subclass (which could be a domain model itself) and |
64
|
|
|
* call the parent constructor. use the hooks to provide functionality (optionally a ModelBuilder and callbacks). |
65
|
|
|
* usage: This is the most flexible model of usage, but mixes your domain logic with the statemachine. |
66
|
|
|
* see 'examples/inheritance' |
67
|
|
|
* - 3: COMPOSITION: Use object composition. Instantiate and build a statemachine in a domain |
68
|
|
|
* model and build the full Context, Transitions and statemachine there. Use a ModelBuilder and callbacks |
69
|
|
|
* to drive functionality. |
70
|
|
|
* usage: This is a good mixture between encapsulating statemachine logic and flexibility/formal usage. |
71
|
|
|
* see 'examples/composition' |
72
|
|
|
* - 4: STANDALONE: Use the statemachine as is, without any domain models. Your application will use it and inspect the |
73
|
|
|
* statemachine to act on it. Use callables to provide functionality |
74
|
|
|
* usage: This is the easiest model of usage, but the least powerful. |
75
|
|
|
* see 'examples/interactive' |
76
|
|
|
* |
77
|
|
|
* MECHANISMS FOR GUARD AND TRANSITION LOGIC |
78
|
|
|
* 1. rules and commands: they are fully qualified class names of Rule and Command (sub)classes that can be injected |
79
|
|
|
* in Transition and State instances. they accept the domain model provided via an EntityBuilder |
80
|
|
|
* in their constructor. |
81
|
|
|
* 2. hooks: the methods in this class that start with an underscore and can be overriden in a subclass. |
82
|
|
|
* these can be used to provide a subclass of this machine specifically tailored to your application domain. |
83
|
|
|
* @link http://c2.com/cgi/wiki?HookMethod |
84
|
|
|
* 3. eventHandlers: methods that can be defined on the domain model. They will only be called when they |
85
|
|
|
* are defined on the domain model and all start with 'on' prefix eg: onExitState, onTransition |
86
|
|
|
* @link: https://en.wikipedia.org/wiki/Event_(computing)#Event_handler |
87
|
|
|
* 4. callables: callables can be injected in Transition and State instances. callables are |
88
|
|
|
* user defined methods in various flavours (closure, instance methods, static methods etc.) that |
89
|
|
|
* will be called if they are valid. This is the easiest way to start using the statemachine. |
90
|
|
|
* @link https://php.net/manual/en/language.types.callable.php |
91
|
|
|
* @see TransitionTest::shouldAcceptMultipleCallableTypes for all implementations |
92
|
|
|
* |
93
|
|
|
* DESCRIPTION OF FULL TRANSITION ALGORITHM (and the usage modes where they apply) |
94
|
|
|
* 1. guard: _onCheckCanTransition($transition) //hook: inheritance. |
95
|
|
|
* 2. guard: $entity->onCheckCanTransition($identifier, $transition) // event handler: delegation, composition, inheritance |
96
|
|
|
* 3. guard: $transition->can($context) // check Rule: delegation, composition, inheritance, standalone |
97
|
|
|
* 4. guard: $transition->can($context) // callable: standalone, delegation, composition, inheritance |
98
|
|
|
* !!! If all (existing) guards return true, then the transition is allowed. return true/false to allow/disallow transition. |
99
|
|
|
* 5. logic: _onExitState($transition) //hook: inheritance |
100
|
|
|
* 6. logic: $entity->onExitState($identifier, $transition) //event handler: delegation, composition, inheritance |
101
|
|
|
* 7. logic: $state_from->exitAction($context) //execute Command: delegation, composition, inheritance, standalone |
102
|
|
|
* 8. logic: state exit: $callable($entity) // callable: standalone, delegation, composition, inheritance |
103
|
|
|
* 9. logic: _onTransition($transition) //hook: inheritance |
104
|
|
|
* 10. logic: $entity->onTransition($identifier, $transition) //event handler: delegation, composition, inheritance |
105
|
|
|
* 11. logic: $transition->process($context) //execute Command: delegation, composition, inheritance, standalone |
106
|
|
|
* 12. logic: $transition->process($context) // callable: standalone, delegation, composition, inheritance |
107
|
|
|
* 13. logic: $entity->onEnterState($identifier, $transition) //event handler: delegation, composition, inheritance |
108
|
|
|
* 14. logic: $state_to->entryAction($context) //execute Command: delegation, composition, inheritance, standalone |
109
|
|
|
* 15. logic: state entry: $callable($entity) // callable: standalone, delegation, composition, inheritance |
110
|
|
|
* 16. logic: _onEnterState($transition) //hook: inheritance |
111
|
|
|
* |
112
|
|
|
* each hook can be overriden and implemented in a subclass, providing |
113
|
|
|
* functionality that is specific to your application. This allows you to use |
114
|
|
|
* the core mechanisms of the izzum package and extend it to your needs. |
115
|
|
|
* |
116
|
|
|
* |
117
|
|
|
* each event handler might be implemented on the entity/domain object and will be called, only if |
118
|
|
|
* available, with the $transition and $event arguments. |
119
|
|
|
* |
120
|
|
|
* each callable might be set on a transition or state and comes in different forms: |
121
|
|
|
* closure, anonymouse function, instance method, static methods etc. (see php manual) |
122
|
|
|
* |
123
|
|
|
* Each transition will take place only if the transition logic (Rule, hooks, event handlers & |
124
|
|
|
* callables) allows it. Each exit state, transition and entry state will then execute specific logic |
125
|
|
|
* (Command, hooks, event handlers & callables). |
126
|
|
|
* |
127
|
|
|
* for simple applications, the use of callables and event handlers is advised since it |
128
|
|
|
* is easier to setup and use with existing code. |
129
|
|
|
* |
130
|
|
|
* for more formal applications, the use of rules and commands is advised since |
131
|
|
|
* it allows for better encapsulated (and tested) business logic and more flexibility in |
132
|
|
|
* configuration via a database or config file. |
133
|
|
|
* |
134
|
|
|
* The statemachine should be loaded with States and Transitions, which define |
135
|
|
|
* from what state to what other state transitions are allowed. The transitions |
136
|
|
|
* are checked against guard clauses (in the form of a business Rule instance |
137
|
|
|
* and hooks, event handlers and callables) and transition logic (in the form of a Command |
138
|
|
|
* instance, hooks, event handlers and callables). |
139
|
|
|
* |
140
|
|
|
* Transitions can also trigger an exit action for the current state and an |
141
|
|
|
* entry action for the new state (also via Command instances, event handlers,hooks and |
142
|
|
|
* callables). This allows you to implement logic that is dependent on the |
143
|
|
|
* State and independent of the Transition. |
144
|
|
|
* |
145
|
|
|
* RULE/COMMAND logic |
146
|
|
|
* The Rule checks if the domain model (or it's derived data) applies and |
147
|
|
|
* therefore allows the transition, after which the Command is executed that can |
148
|
|
|
* actually alter data in the underlying domain models, call services etc. |
149
|
|
|
* Rules should have NO side effects, since their only function is to check if a |
150
|
|
|
* Transition is allowed. Commands do the heavy lifting for executing logic that |
151
|
|
|
* relates to a transition. |
152
|
|
|
* |
153
|
|
|
* EXCEPTIONS |
154
|
|
|
* All high level interactions that a client conducts with a statemachine |
155
|
|
|
* should expect exceptions since runtime exceptions could bubble up from the |
156
|
|
|
* application specific classes from your domain (eg: calling services, database interactions etc). |
157
|
|
|
* Exceptions that bubble up from this statemachine are always izzum\statemachine\Exception types. |
158
|
|
|
* |
159
|
|
|
* NAMING CONVENTIONS |
160
|
|
|
* - A good naming convention for states is to use lowercase-hypen-seperated names: |
161
|
|
|
* new, waiting-for-input, starting-order-process, enter-cancel-flow, done |
162
|
|
|
* - A good naming convention for transitions is to bind the input and exit state |
163
|
|
|
* with the string '_to_', which is done automatically by this package eg: |
164
|
|
|
* new_to_waiting-for-input, new_to_done so you're able to call $statemachine->transition('new_to_done'); |
165
|
|
|
* - A good naming convention for events (transition trigger names) is to use |
166
|
|
|
* lowercase-underscore-seperated names (or singe words) so your able to call |
167
|
|
|
* $machine->start() or $machine->event_name() or $machine->handle('event_name') |
168
|
|
|
* |
169
|
|
|
* |
170
|
|
|
* MISC |
171
|
|
|
* The implementation details of this machine make it that it can act both as |
172
|
|
|
* a mealy machine and as a moore machine. the concepts can be mixed and |
173
|
|
|
* matched. |
174
|
|
|
* |
175
|
|
|
* |
176
|
|
|
* @author Rolf Vreijdenberger |
177
|
|
|
* @link https://en.wikipedia.org/wiki/Finite-state_machine |
178
|
|
|
* @link https://en.wikipedia.org/wiki/UML_state_machine |
179
|
|
|
*/ |
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) |
228
|
|
|
{ |
229
|
|
|
// sets up bidirectional association |
230
|
58 |
|
$this->setContext($context); |
231
|
58 |
|
} |
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) |
249
|
|
|
{ |
250
|
4 |
|
$transition = $this->getTransitionWithNullCheck($transition_name); |
251
|
4 |
|
return $this->performTransition($transition, $message); |
252
|
|
|
} |
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) |
279
|
|
|
{ |
280
|
10 |
|
$transitioned = false; |
281
|
10 |
|
$transitions = $this->getCurrentState()->getTransitionsTriggeredByEvent($event); |
282
|
10 |
|
foreach ($transitions as $transition) { |
283
|
10 |
|
$transitioned = $this->performTransition($transition, $message); |
284
|
|
|
if ($transitioned) |
285
|
9 |
|
break; |
286
|
9 |
|
} |
287
|
9 |
|
return $transitioned; |
288
|
|
|
} |
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) |
317
|
|
|
{ |
318
|
|
|
try { |
319
|
8 |
|
$transitions = $this->getCurrentState()->getTransitions(); |
320
|
8 |
|
foreach ($transitions as $transition) { |
321
|
8 |
|
$transitioned = $this->performTransition($transition, $message); |
322
|
7 |
|
if ($transitioned) { |
323
|
7 |
|
return true; |
324
|
|
|
} |
325
|
4 |
|
} |
326
|
5 |
|
} catch(Exception $e) { |
327
|
|
|
// will be rethrown |
328
|
1 |
|
$this->handlePossibleNonStatemachineException($e, Exception::SM_RUN_FAILED); |
329
|
|
|
} |
330
|
|
|
// no transition done |
331
|
4 |
|
return false; |
332
|
|
|
} |
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) |
346
|
|
|
{ |
347
|
4 |
|
$transitions = 0; |
348
|
|
|
try { |
349
|
4 |
|
$run = true; |
350
|
4 |
|
while ( $run ) { |
351
|
|
|
// run the first transition possible |
352
|
4 |
|
$run = $this->run($message); |
353
|
3 |
|
if ($run) { |
354
|
3 |
|
$transitions++; |
355
|
3 |
|
} |
356
|
3 |
|
} |
357
|
4 |
|
} catch(Exception $e) { |
358
|
1 |
|
$this->handlePossibleNonStatemachineException($e, $e->getCode()); |
359
|
|
|
} |
360
|
3 |
|
return $transitions; |
361
|
|
|
} |
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) |
372
|
|
|
{ |
373
|
4 |
|
$transition = $this->getTransition($transition_name); |
374
|
4 |
|
return $transition === null ? false : $this->doCheckCanTransition($transition); |
375
|
|
|
} |
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) |
385
|
|
|
{ |
386
|
3 |
|
$transitions = $this->getCurrentState()->getTransitionsTriggeredByEvent($event); |
387
|
3 |
|
foreach ($transitions as $transition) { |
388
|
3 |
|
if ($this->doCheckCanTransition($transition)) { |
389
|
2 |
|
return true; |
390
|
|
|
} |
391
|
3 |
|
} |
392
|
2 |
|
return false; |
393
|
|
|
} |
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) |
403
|
|
|
{ |
404
|
3 |
|
$transitions = $this->getCurrentState()->getTransitionsTriggeredByEvent($event); |
405
|
3 |
|
if (count($transitions) > 0) { |
406
|
3 |
|
return true; |
407
|
|
|
} |
408
|
1 |
|
return false; |
409
|
|
|
} |
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) |
426
|
|
|
{ |
427
|
|
|
// every method in this core routine has hook methods, event handlers and |
428
|
|
|
// callbacks it can call during the execution phase of the |
429
|
|
|
// transition steps if they are available on the domain model. |
430
|
|
|
try { |
431
|
|
|
|
432
|
17 |
|
if (!$this->doCheckCanTransition($transition)) { |
433
|
|
|
// one of the guards returned false or transition not found on current state. |
434
|
4 |
|
return false; |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
// state exit action: performed when exiting the state |
438
|
16 |
|
$this->doExitState($transition); |
439
|
|
|
// the transition is performed, with the associated logic |
440
|
16 |
|
$this->doTransition($transition, $message); |
441
|
|
|
// state entry action: performed when entering the state |
442
|
16 |
|
$this->doEnterState($transition); |
443
|
17 |
|
} catch(Exception $e) { |
444
|
2 |
|
$this->handlePossibleNonStatemachineException($e, Exception::SM_TRANSITION_FAILED, $transition); |
445
|
|
|
} |
446
|
16 |
|
return true; |
447
|
|
|
} |
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) |
459
|
|
|
{ |
460
|
|
|
try { |
461
|
|
|
// check if we have this transition on the current state. |
462
|
17 |
|
if (!$this->getCurrentState()->hasTransition($transition->getName())) { |
463
|
3 |
|
return false; |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
// possible hook so your application can place an extra guard on the |
467
|
|
|
// transition. possible entry~ or exit state type of checks can also |
468
|
|
|
// take place in this hook. |
469
|
17 |
|
if (!$this->_onCheckCanTransition($transition)) { |
470
|
1 |
|
return false; |
471
|
|
|
} |
472
|
|
|
// an event handler that is possibly defined on the domain model: onCheckCanTransition |
473
|
17 |
|
if (!$this->callEventHandler($this->getContext()->getEntity(), 'onCheckCanTransition', $this->getContext()->getIdentifier(), $transition)) { |
474
|
1 |
|
return false; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
// this will check the guards defined for the transition: Rule and callable |
478
|
17 |
|
if (!$transition->can($this->getContext())) { |
479
|
4 |
|
return false; |
480
|
|
|
} |
481
|
17 |
|
} catch(Exception $e) { |
482
|
2 |
|
$this->handlePossibleNonStatemachineException($e, Exception::SM_CAN_FAILED); |
483
|
|
|
} |
484
|
|
|
// if we come here, this acts as a green light to start the transition. |
485
|
16 |
|
return true; |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
/** |
489
|
|
|
* the exit state action method |
490
|
|
|
* |
491
|
|
|
* @param Transition $transition |
492
|
|
|
*/ |
493
|
16 |
|
private function doExitState(Transition $transition) |
494
|
|
|
{ |
495
|
|
|
// hook for subclasses to implement |
496
|
16 |
|
$this->_onExitState($transition); |
497
|
|
|
// an event handler that is possibly defined on the domain model: onExitState |
498
|
16 |
|
$this->callEventHandler($this->getContext()->getEntity(), 'onExitState', $this->getContext()->getIdentifier(), $transition); |
499
|
|
|
// executes the command and callable associated with the state object |
500
|
16 |
|
$transition->getStateFrom()->exitAction($this->getContext()); |
501
|
16 |
|
} |
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) |
511
|
|
|
{ |
512
|
|
|
// hook for subclasses to implement |
513
|
16 |
|
$this->_onTransition($transition); |
514
|
16 |
|
$entity = $this->getContext()->getEntity(); |
515
|
16 |
|
$identifier = $this->getContext()->getIdentifier(); |
516
|
|
|
// an event handler that is possibly defined on the domain model: onTransition |
517
|
16 |
|
$this->callEventHandler($entity, 'onTransition', $identifier, $transition); |
518
|
|
|
// executes the command and callable associated with the transition object |
519
|
16 |
|
$transition->process($this->getContext()); |
520
|
|
|
|
521
|
|
|
// this actually sets the state so we are now in the new state |
522
|
16 |
|
$this->setState($transition->getStateTo(), $message); |
523
|
16 |
|
} |
524
|
|
|
|
525
|
|
|
/** |
526
|
|
|
* the enter state action method |
527
|
|
|
* |
528
|
|
|
* @param Transition $transition |
529
|
|
|
*/ |
530
|
16 |
|
private function doEnterState(Transition $transition) |
531
|
|
|
{ |
532
|
|
|
// an event handler that is possibly defined on the domain model: onEnterState |
533
|
16 |
|
$this->callEventHandler($this->getContext()->getEntity(), 'onEnterState', $this->getContext()->getIdentifier(), $transition); |
534
|
|
|
|
535
|
|
|
// executes the command and callable associated with the state object |
536
|
16 |
|
$transition->getStateTo()->entryAction($this->getContext()); |
537
|
|
|
|
538
|
|
|
// hook for subclasses to implement |
539
|
16 |
|
$this->_onEnterState($transition); |
540
|
16 |
|
} |
541
|
|
|
|
542
|
|
|
// ######################## SUPPORTING METHODS ############################# |
543
|
|
|
/** |
544
|
|
|
* All known/loaded states for this statemachine |
545
|
|
|
* |
546
|
|
|
* @return State[] |
547
|
|
|
*/ |
548
|
45 |
|
public function getStates() |
549
|
|
|
{ |
550
|
45 |
|
return $this->states; |
551
|
|
|
} |
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) |
560
|
|
|
{ |
561
|
44 |
|
return isset($this->states [$name]) ? $this->states [$name] : null; |
562
|
|
|
} |
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) |
581
|
|
|
{ |
582
|
|
|
//no regex states |
583
|
44 |
|
if ($state->isRegex()) { |
584
|
2 |
|
return false; |
585
|
|
|
} |
586
|
|
|
//check for duplicates |
587
|
44 |
|
if (isset($this->states [$state->getName()])) { |
588
|
41 |
|
return false; |
589
|
|
|
} |
590
|
44 |
|
$this->states [$state->getName()] = $state; |
591
|
44 |
|
return true; |
592
|
|
|
} |
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) |
611
|
|
|
{ |
612
|
|
|
|
613
|
16 |
|
if($this->getState($state->getName()) === null) { |
614
|
1 |
|
throw new Exception(sprintf("%s state '%s' not known to this machine", $this->toString(), $state->getName()), Exception::SM_UNKNOWN_STATE); |
615
|
|
|
} |
616
|
|
|
//get the state known to this machine so we are sure we have the correct reference |
617
|
|
|
//even if the client provides another instance of State with the same name |
618
|
16 |
|
$state = $this->getState($state->getName()); |
619
|
16 |
|
$this->getContext()->setState($state->getName(), $message); |
620
|
16 |
|
$this->state = $state; |
621
|
16 |
|
} |
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) |
642
|
|
|
{ |
643
|
5 |
|
return $this->getContext()->add($this->getInitialState()->getName(), $message); |
644
|
|
|
} |
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() |
659
|
|
|
{ |
660
|
|
|
// do we have a current state? |
661
|
18 |
|
if ($this->state) { |
662
|
17 |
|
return $this->state; |
663
|
|
|
} |
664
|
|
|
// retrieve state from the context if we do not find any state set. |
665
|
18 |
|
$state = $this->getState($this->getContext()->getState()); |
666
|
18 |
|
if (!$state || $state == State::STATE_UNKNOWN) { |
667
|
|
|
// possible wrong configuration |
668
|
2 |
|
throw new Exception(sprintf("%s current state not found for state with name '%s'. %s", $this->toString(), $this->getContext()->getState(), 'are the transitions/states loaded and configured correctly?'), Exception::SM_NO_CURRENT_STATE_FOUND); |
669
|
|
|
} |
670
|
|
|
// state is found, set it and return |
671
|
17 |
|
$this->state = $state; |
672
|
17 |
|
return $this->state; |
673
|
|
|
} |
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) |
688
|
|
|
{ |
689
|
22 |
|
$states = $this->getStates(); |
690
|
22 |
|
foreach ($states as $state) { |
691
|
21 |
|
if ($state->isInitial()) { |
692
|
21 |
|
return $state; |
693
|
|
|
} |
694
|
4 |
|
} |
695
|
3 |
|
if ($allow_null) { |
696
|
3 |
|
return null; |
697
|
|
|
} |
698
|
1 |
|
throw new Exception(sprintf("%s no initial state found, bad configuration. %s", $this->toString(), 'are the transitions/states loaded and configured correctly?'), Exception::SM_NO_INITIAL_STATE_FOUND); |
699
|
|
|
} |
700
|
|
|
|
701
|
|
|
/** |
702
|
|
|
* All known/loaded transitions for this statemachine |
703
|
|
|
* |
704
|
|
|
* @return Transition[] |
705
|
|
|
*/ |
706
|
22 |
|
public function getTransitions() |
707
|
|
|
{ |
708
|
22 |
|
return $this->transitions; |
709
|
|
|
} |
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) |
719
|
|
|
{ |
720
|
43 |
|
return isset($this->transitions [$name]) ? $this->transitions [$name] : null; |
721
|
|
|
} |
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) |
756
|
|
|
{ |
757
|
43 |
|
$from = $transition->getStateFrom(); |
758
|
43 |
|
$to = $transition->getStateTo(); |
759
|
43 |
|
$all_states = $this->getStates(); |
760
|
|
|
// check if we have regex states in the transition and get either the |
761
|
|
|
// original state back if it's not a regex, or all currently known |
762
|
|
|
// states that match the regex. |
763
|
43 |
|
$all_from = Utils::getAllRegexMatchingStates($from, $all_states); |
764
|
43 |
|
$all_to = Utils::getAllRegexMatchingStates($to, $all_states); |
765
|
43 |
|
$contains_regex = $from->isRegex() || $to->isRegex(); |
766
|
43 |
|
$non_regex_transition = $transition; |
767
|
43 |
|
$count = 0; |
768
|
|
|
// loop trought all possible 'from' states |
769
|
43 |
|
foreach ($all_from as $from) { |
770
|
|
|
// loop through all possible 'to' states |
771
|
43 |
|
foreach ($all_to as $to) { |
772
|
43 |
|
if ($contains_regex && $from->getName() === $to->getName() && !$allow_self_transition_by_regex) { |
773
|
|
|
// disallow self transition for regexes and from final states unless it is explicitely allowed. |
774
|
6 |
|
continue; |
775
|
|
|
} |
776
|
43 |
|
if ($contains_regex) { |
777
|
|
|
/* |
778
|
|
|
* get a copy of the current transition that has a regex (so |
779
|
|
|
* we have a non-regex state) and delegate to $transition to |
780
|
|
|
* get that copy in case it's a subclass of transition and |
781
|
|
|
* we need to copy specific fields |
782
|
|
|
*/ |
783
|
18 |
|
$non_regex_transition = $transition->getCopy($from, $to); |
784
|
18 |
|
} |
785
|
43 |
|
if ($this->addTransitionWithoutRegex($non_regex_transition)) { |
786
|
43 |
|
$count++; |
787
|
43 |
|
} |
788
|
43 |
|
} |
789
|
43 |
|
} |
790
|
43 |
|
return $count; |
791
|
|
|
} |
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) |
801
|
|
|
{ |
802
|
|
|
// don't allow transitions from a final state |
803
|
43 |
|
if ($transition->getStateFrom()->isFinal()) { |
804
|
13 |
|
return false; |
805
|
|
|
} |
806
|
|
|
|
807
|
|
|
// add the transition only if it already exists (no overwrites) |
808
|
43 |
|
if ($this->getTransition($transition->getName()) !== null) { |
809
|
5 |
|
return false; |
810
|
|
|
} |
811
|
43 |
|
$this->transitions [$transition->getName()] = $transition; |
812
|
|
|
|
813
|
43 |
|
$from = $transition->getStateFrom(); |
814
|
|
|
// adds state only if it does not already exist (no overwrites) |
815
|
43 |
|
$this->addState($from); |
816
|
|
|
/* |
817
|
|
|
* transitions create bidirectional references to the States |
818
|
|
|
* when they are made, but here the States that are set on the machine |
819
|
|
|
* can actually be different instances from different transitions (eg: |
820
|
|
|
* a->b and a->c => we now have two State instances of a) |
821
|
|
|
* we therefore need to merge the transitions on the existing states. |
822
|
|
|
* The LoaderArray class does this for us by default, but we do it here |
823
|
|
|
* too, just in case a client decides to call the 'addTransition' method |
824
|
|
|
* directly without a loader. |
825
|
|
|
*/ |
826
|
|
|
//get the state known to the machine, to prevent possible bug where client |
827
|
|
|
//creates 2 different state instances with different configs. we won't know |
828
|
|
|
//how to resolve this anyway, so just pick the existing state (first in wins) |
829
|
43 |
|
$state = $this->getState($from->getName()); |
830
|
|
|
// adds transition only if it does not already exist (no overwrites) |
831
|
43 |
|
$state->addTransition($transition); |
832
|
|
|
|
833
|
43 |
|
$to = $transition->getStateTo(); |
834
|
|
|
// adds state only if it dooes not already exist (no overwrites) |
835
|
43 |
|
$this->addState($to); |
836
|
43 |
|
return true; |
837
|
|
|
} |
838
|
|
|
|
839
|
|
|
/** |
840
|
|
|
* Get the current context |
841
|
|
|
* |
842
|
|
|
* @return Context |
843
|
|
|
*/ |
844
|
58 |
|
public function getContext() |
845
|
|
|
{ |
846
|
58 |
|
return $this->context; |
847
|
|
|
} |
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) |
871
|
|
|
{ |
872
|
58 |
|
if ($this->getContext()) { |
873
|
|
|
// context already exists. |
874
|
2 |
|
if ($this->getContext()->getMachine() !== $context->getMachine()) { |
875
|
1 |
|
throw new Exception(sprintf("Trying to set context for a different machine. currently '%s' and new '%s'", $this->getContext()->getMachine(), $context->getMachine()), Exception::SM_CONTEXT_DIFFERENT_MACHINE); |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
// reset state TODO: move outside if statement |
879
|
2 |
|
$this->state = null; |
880
|
2 |
|
} |
881
|
58 |
|
$context->setStateMachine($this); |
882
|
58 |
|
$this->context = $context; |
883
|
58 |
|
} |
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) |
896
|
|
|
{ |
897
|
|
|
// override if necessary to log exceptions or to add some extra info |
898
|
|
|
// to the underlying storage facility (for example, an exception will |
899
|
|
|
// not lead to a transition, so this can be used to indicate a failed |
900
|
|
|
// transition in some sort of history structure) |
901
|
2 |
|
$this->getContext()->setFailedTransition($transition, $e); |
902
|
2 |
|
} |
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) |
918
|
|
|
{ |
919
|
2 |
|
$e = Utils::wrapToStateMachineException($e, $code); |
920
|
2 |
|
if ($transition !== null) { |
921
|
2 |
|
$this->handleTransitionException($transition, $e); |
922
|
2 |
|
} |
923
|
2 |
|
throw $e; |
924
|
|
|
} |
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) |
959
|
|
|
{ |
960
|
|
|
//prepend the $name (event/trigger) to other arguments and call the 'handle' method |
961
|
8 |
|
array_unshift($arguments, $name); |
962
|
8 |
|
return call_user_func_array(array($this, 'handle'), $arguments); |
963
|
|
|
} |
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) |
978
|
|
|
{ |
979
|
|
|
// return true by default (because of transition guard callbacks that |
980
|
|
|
// might not exist) |
981
|
17 |
|
$output = true; |
982
|
|
|
// check if method exists. |
983
|
17 |
|
if (method_exists($object, $method)) { // && $object !== $this) { //prevent recursion |
|
|
|
|
984
|
1 |
|
$args = func_get_args(); |
985
|
|
|
// remove $object and $method from $args so we only have the other arguments |
986
|
1 |
|
array_shift($args); |
987
|
1 |
|
array_shift($args); |
988
|
|
|
// have the methods be able to return what they like |
989
|
|
|
// but make sure the 'onCheckCanTransition method returns a boolean |
990
|
1 |
|
$output = call_user_func_array(array($object,$method), $args); |
991
|
1 |
|
} |
992
|
17 |
|
return $output; |
993
|
|
|
} |
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) |
1005
|
|
|
{ |
1006
|
4 |
|
$transition = $this->getTransition($name); |
1007
|
4 |
|
if ($transition === null) { |
1008
|
1 |
|
throw new Exception(sprintf("transition not found for '%s'", $name), Exception::SM_NO_TRANSITION_FOUND); |
1009
|
|
|
} |
1010
|
4 |
|
return $transition; |
1011
|
|
|
} |
1012
|
|
|
|
1013
|
5 |
|
public function toString($elaborate = false) |
1014
|
|
|
{ |
1015
|
5 |
|
$output = get_class($this) . ": [" . $this->getContext()->getId(true) . "]"; |
1016
|
5 |
|
if (!$elaborate) { |
1017
|
5 |
|
return $output; |
1018
|
|
|
} else { |
1019
|
1 |
|
$output . ' transitions: ' . count($this->getTransitions()) . ', states: ' . count($this->getStates()) . '.'; |
1020
|
1 |
|
$output .= '[transitions]: ' . implode(",", $this->getTransitions()); |
1021
|
1 |
|
$output .= '. [states]: ' . implode(",", $this->getStates()) . '.'; |
1022
|
1 |
|
return $output; |
1023
|
|
|
} |
1024
|
|
|
} |
1025
|
|
|
|
1026
|
1 |
|
public function __toString() |
1027
|
|
|
{ |
1028
|
1 |
|
return $this->toString(true); |
1029
|
|
|
} |
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) |
|
|
|
|
1047
|
|
|
{ |
1048
|
|
|
// eg: dispatch an event and see if it is rejected by a listener |
1049
|
16 |
|
return true; |
1050
|
|
|
} |
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) |
|
|
|
|
1065
|
15 |
|
{} |
1066
|
|
|
|
1067
|
|
|
/** |
1068
|
|
|
* hook method. |
1069
|
|
|
* override in subclass if necessary. |
1070
|
|
|
* |
1071
|
|
|
* @param Transition $transition |
1072
|
|
|
*/ |
1073
|
15 |
|
protected function _onTransition(Transition $transition) |
|
|
|
|
1074
|
15 |
|
{} |
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) |
|
|
|
|
1089
|
|
|
{} |
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.