Completed
Push — master ( 8a8424...13f8fb )
by tsms
28s
created

StateMachineBehavior::createDotFileForRoles()   B

Complexity

Conditions 8
Paths 2

Size

Total Lines 21
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 7.1428
c 0
b 0
f 0
cc 8
eloc 16
nc 2
nop 3
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 14 and the first side effect is on line 11.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/**
3
 * StateMachineBehavior
4
 *
5
 * A finite state machine is a machine that cannot move between states unless
6
 * a specific transition fired. It has a specified amount of legal directions it can
7
 * take from each state. It also supports state listeners and transition listeners.
8
 *
9
 * @author David Steinsland
10
 */
11
App::uses('Model', 'Model');
12
App::uses('Inflector', 'Utility');
13
14
class StateMachineBehavior extends ModelBehavior {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
15
16
/**
17
 * Allows us to support writing both: is('parked') and isParked()
18
 *
19
 * @var array
20
 */
21
	public $mapMethods = array(
22
		'/when([A-Z][a-zA-Z0-9]+)/' => 'when',
23
		'/on([A-Z][a-zA-Z0-9]+)/' => 'on'
24
	);
25
26
	protected $_defaultSettings = array(
27
		'transition_listeners' => array(
28
			'transition' => array(
29
				'before' => array(),
30
				'after' => array()
31
			)
32
		),
33
		'state_listeners' => array(),
34
		'methods' => array()
35
	);
36
37
/**
38
 * Array of all configured states. Initialized by self::setup()
39
 * @var array
40
 */
41
	protected $_availableStates = array();
42
43
/**
44
 * Adds a available state
45
 *
46
 * @param string $state The state to be added.
47
 * @return void
48
 */
49
	protected function _addAvailableState($state) {
50
		if ($state != 'All' && !in_array($state, $this->_availableStates)) {
51
			$this->_availableStates[] = Inflector::camelize($state);
52
		}
53
	}
54
55
/**
56
 * Sets up all the methods that builds up the state machine.
57
 * StateMachine->is<State>            i.e. StateMachine->isParked()
58
 * StateMachine->can<Transition>    i.e. StateMachine->canShiftGear()
59
 * StateMachine-><transition>        i.e. StateMachine->shiftGear();
60
 *
61
 * @param Model $model The model being used
62
 * @param array $config Configuration for the Behavior
63
 * @return void
64
 */
65
	public function setup(Model $model, $config = array()) {
66
		if (!isset($this->settings[$model->alias])) {
67
			$this->settings[$model->alias] = $this->_defaultSettings;
68
		}
69
70
		foreach ($model->transitions as $transition => $states) {
71
			foreach ($states as $stateFrom => $stateTo) {
72
				$this->_addAvailableState(Inflector::camelize($stateFrom));
73
				$this->_addAvailableState(Inflector::camelize($stateTo));
74
				foreach (array(
75
							'is' . Inflector::camelize($stateFrom),
76
							'is' . Inflector::camelize($stateTo)
77
						) as $methodName) {
78
					if (!$this->_hasMethod($model, $methodName)) {
79
						$this->mapMethods['/' . $methodName . '$/'] = 'is';
80
					}
81
				}
82
			}
83
84
			$this->mapMethods['/^can' . Inflector::camelize($transition) . '$/'] = 'can';
85
86
			$transitionFunction = Inflector::variable($transition);
87
			$this->mapMethods['/^' . $transitionFunction . '$/'] = 'transition';
88
		}
89
	}
90
91
/**
92
 * Adds a user defined callback
93
 * {{{
94
 * $this->Vehicle->addMethod('myMethod', function() {});
95
 * $data = $this->Vehicle->myMethod();
96
 * }}}
97
 *
98
 * @param Model $model The model being acted on
99
 * @param string $method The method na,e
100
 * @param string $cb The callback to execute
101
 * @throws InvalidArgumentException If the method already is registered
102
 * @return void
103
 */
104
	public function addMethod(Model $model, $method, $cb) {
105
		if ($this->_hasMethod($model, $method)) {
106
			throw new InvalidArgumentException("A method with the same name is already registered");
107
		}
108
109
		$this->settings[$model->alias]['methods'][$method] = $cb;
110
		$this->mapMethods['/' . $method . '/'] = 'handleMethodCall';
111
112
		// force model to re-load Behavior, so that the mapMethods are working correctly
113
		$model->Behaviors->load('Statemachine.StateMachine');
114
	}
115
116
/**
117
 * Handles user defined method calls, which are implemented using closures.
118
 *
119
 * @param Model $model The model being acted on
120
 * @param string $method The method name
121
 * @return mixed The return value of the callback, or an array if the method doesn't exist
122
 */
123
	public function handleMethodCall(Model $model, $method) {
124
		if (!isset($this->settings[$model->alias]['methods'][$method])) {
125
			return array('unhandled');
126
		}
127
		return call_user_func_array($this->settings[$model->alias]['methods'][$method], func_get_args());
128
	}
129
130
/**
131
 * Updates the model's state when a $model->save() call is performed
132
 *
133
 * @param Model $model The model being acted on
134
 * @param bool $created Whether or not the model was created
135
 * @param array $options Options passed to save
136
 * @return bool
137
 */
138
	public function afterSave(Model $model, $created, $options = array()) {
139
		if ($created) {
140
			$model->saveField('state', $model->initialState);
141
		}
142
143
		return true;
144
	}
145
146
/**
147
 * returns all transitions defined in model
148
 *
149
 * @param Model $model The model being acted on
150
 * @return array array of transitions
151
 * @author Frode Marton Meling
152
 */
153
	public function getAllTransitions($model) {
154
		$transitionArray = array();
155
		foreach ($model->transitions as $transition => $data) {
156
			$transitionArray[] = $transition;
157
		}
158
		return $transitionArray;
159
	}
160
161
/**
162
 * Returns an array of all configured states
163
 *
164
 * @return array
165
 */
166
	public function getAvailableStates() {
167
		return $this->_availableStates;
168
	}
169
170
/**
171
 * checks if $state or Array of states are valid ones
172
 *
173
 * @param string|array $state a string representation of state or a array of states
174
 * @return bool
175
 * @author Frode Marton Meling
176
 */
177
	protected function _validState($state) {
178
		$availableStatesIncludingAll = array_merge(array('All'), $this->_availableStates);
179
		if (!is_array($state)) {
180
			return in_array(Inflector::camelize($state), $availableStatesIncludingAll);
181
		}
182
183
		foreach ($state as $singleState) {
184
			if (!in_array(Inflector::camelize($singleState), $availableStatesIncludingAll)) {
185
				return false;
186
			}
187
		}
188
		return true;
189
	}
190
191
/**
192
 * Finds all records in a specific state. Supports additional conditions, but will overwrite conditions with state
193
 *
194
 * @param Model $model The model being acted on
195
 * @param string $type find type (ref. CakeModel)
196
 * @param array|string $state    The state to find. this will be checked for validity.
197
 * @param array $params Regular $params array for CakeModel->find
198
 * @return array            Returns datarray of $model records or false. Will return false if state is not set, or state is not configured in model
199
 * @author Frode Marton Meling
200
 */
201
	protected function _findByState(Model $model, $type, $state = null, $params = array()) {
202
		if ($state === null || !$this->_validState($state)) {
203
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by StateMachineBehavior::_findByState of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
204
		}
205
206
		if (is_array($state) || Inflector::camelize($state) != 'All') {
207
			$params['conditions']["{$model->alias}.state"] = $state;
208
		}
209
		return $model->find($type, $params);
210
	}
211
212
/**
213
 * This function will add all availble (runnable) transitions on a model and add it to the dataArray given to the function.
214
 *
215
 * @param Model $model The model being acted on
216
 * @param array $modelRows The model dataArray. this is an array of Models returned from a model->find.
217
 * @param string $role if specified, the function will limit the transitions based on a role
218
 * @return array        Returns datarray of $model with the available transitions inserted
219
 * @author Frode Marton Meling
220
 */
221
	protected function _addTransitionsToArray($model, $modelRows, $role) {
222
		if (!isset($modelRows) || $modelRows == false) {
223
			return $modelRows;
224
		}
225
226
		$allTransitions = $this->getAllTransitions($model);
227
		foreach ($modelRows as $key => $modelRow) {
228
			$model->id = $modelRow[$model->alias]['id'];
229
			// Note! We need this empty array if no transitions are availble. then we do not need to test if array exist in views.
230
			$modelRows[$key][$model->alias]['Transitions'] = array();
231
			foreach ($allTransitions as $transition) {
232
				if ($model->can($transition, $model->id, $role)) {
233
					$modelRows[$key][$model->alias]['Transitions'][] = $transition;
234
				}
235
			}
236
		}
237
		return $modelRows;
238
	}
239
240
/**
241
 * Finds all records in a specific state. Supports additional conditions, but will overwrite conditions with state
242
 *
243
 * @param Model $model The model being acted on
244
 * @param array|string $state    The state to find. this will be checked for validity.
245
 * @param array $params Regular $params array for CakeModel->find
246
 * @param bool $withTransitions Whether or not to add available transitions to records, in its current state
247
 * @param string $role The rule executing the transition
248
 * @return array Returns datarray of $model records or false. Will return false if state is not set, or state is not configured in model
249
 * @author Frode Marton Meling
250
 */
251
	public function findAllByState(Model $model, $state = null, $params = array(), $withTransitions = true, $role = null) {
252
		$modelRows = $this->_findByState($model, 'all', $state, $params);
253
		return ($withTransitions) ? $this->_addTransitionsToArray($model, $modelRows, $role) : $modelRows;
254
	}
255
256
/**
257
 * Finds first record in a specific state. Supports additional conditions, but will overwrite conditions with state
258
 *
259
 * @param Model $model The model being acted on
260
 * @param array|string $state    The state to find. this will be checked for validity.
261
 * @param array $params Regular $params array for CakeModel->find
262
 * @param bool $withTransitions Whether or not to add available transitions to records, in its current state
263
 * @param string $role The rule executing the transition
264
 * @return array Returns datarray of $model records or false. Will return false if state is not set, or state is not configured in model
265
 * @author Frode Marton Meling
266
 */
267
	public function findFirstByState(Model $model, $state = null, $params = array(), $withTransitions = true, $role = null) {
268
		$modelRow = $this->_findByState($model, 'first', $state, $params);
269
		return ($withTransitions) ? $this->_addTransitionsToArray($model, ($modelRow) ? array($modelRow) : false, $role) : $modelRow;
270
	}
271
272
/**
273
 * Finds count of records in a specific state. Supports additional conditions, but will overwrite conditions with state
274
 *
275
 * @param Model $model The model being acted on
276
 * @param array|string $state    The state to find. this will be checked for validity.
277
 * @param array $params Regular $params array for CakeModel->find
278
 * @return array            Returns datarray of $model records or false. Will return false if state is not set, or state is not configured in model
279
 * @author Frode Marton Meling
280
 */
281
	public function findCountByState(Model $model, $state = null, $params = array()) {
282
		return $this->_findByState($model, 'count', $state, $params);
283
	}
284
285
/**
286
 * Allows moving from one state to another.
287
 * {{{
288
 * $this->Model->transition('shift_gear');
289
 * // or
290
 * $this->Model->shiftGear();
291
 * }}}
292
 *
293
 * @param Model $model The model being acted on
294
 * @param string $transition The transition being initiated
295
 * @param int $id table id field to find object
296
 * @param string $role The rule executing the transition
297
 * @param bool $validate whether or not validation being checked
298
 * @return bool Returns true if the transition be executed, otherwise false
299
 */
300
	public function transition(Model $model, $transition, $id = null, $role = null, $validate = true) {
301
		if ($id === null) {
302
			$id = $model->getID();
303
		}
304
		if ($id === false) {
305
			return false;
306
		}
307
		$model->id = $id;
308
		$transition = Inflector::underscore($transition);
309
		$state = $this->getStates($model, $transition);
310
		if (!$state || $this->_checkRoleAgainstRule($model, $role, $transition) === false) {
311
			return false;
312
		}
313
314
		$this->_callTransitionListeners($model, $transition, 'before');
315
316
		$model->read(null, $model->id);
317
		$model->set('previous_state', $model->getCurrentState());
318
		$model->set('last_transition', $transition);
319
		$model->set('last_role', $role);
320
		$model->set('state', $state);
321
		$retval = $model->save(null, $validate);
322
323
		if ($retval) {
324
			$this->_callTransitionListeners($model, $transition, 'after');
325
326
			$stateListeners = array();
327
			if (isset($this->settings[$model->alias]['state_listeners'][$state])) {
328
				$stateListeners = $this->settings[$model->alias]['state_listeners'][$state];
329
			}
330
331
			foreach (array(
332
						'onState' . Inflector::camelize($state),
333
						'onStateChange'
334
					) as $method) {
335
				if (method_exists($model, $method)) {
336
					$stateListeners[] = array($model, $method);
337
				}
338
			}
339
340
			foreach ($stateListeners as $cb) {
341
				call_user_func($cb, $state);
342
			}
343
		}
344
345
		return (bool)$retval;
346
	}
347
348
/**
349
 * Checks whether the state machine is in the given state
350
 *
351
 * @param Model $model The model being acted on
352
 * @param string $state The state being checked
353
 * @param int $id The id of the item to check
354
 * @return bool whether or not the state machine is in the given state
355
 * @throws BadMethodCallException when method does not exists
356
 */
357
	public function is(Model $model, $state, $id = null) {
358
		if ($id === null) {
359
			$id = $model->getID();
360
		}
361
		if ($id === false) {
362
			return false;
363
		}
364
		$model->id = $id;
365
		return $this->getCurrentState($model) === $this->_deFormalizeMethodName($state);
366
	}
367
368
/**
369
 * Checks whether or not the machine is able to perform transition, in its current state
370
 *
371
 * @param Model $model The model being acted on
372
 * @param string $transition The transition being checked
373
 * @param int $id The id of the item to check
374
 * @param string $role The role which should execute the transition
375
 * @return bool whether or not the machine can perform the transition
376
 * @throws BadMethodCallException when method does not exists
377
 */
378
	public function can(Model $model, $transition, $id = null, $role = null) {
379
		if ($id === null) {
380
			$id = $model->getID();
381
		}
382
		if ($id === false) {
383
			return false;
384
		}
385
		$model->id = $id;
386
		$transition = $this->_deFormalizeMethodName($transition);
387
		if (!$this->getStates($model, $transition) || $this->_checkRoleAgainstRule($model, $role, $transition) === false) {
388
			return false;
389
		}
390
391
		return true;
392
	}
393
394
/**
395
 * Registers a callback function to be called when the machine leaves one state.
396
 * The callback is fired either before or after the given transition.
397
 *
398
 * @param Model $model The model being acted on
399
 * @param string $transition The transition to listen to
400
 * @param string $triggerType Either before or after
401
 * @param string $cb The callback function that will be called
402
 * @param bool $bubble Whether or not to bubble other listeners
403
 * @return void
404
 */
405
	public function on(Model $model, $transition, $triggerType, $cb, $bubble = true) {
406
		$this->settings[$model->alias]['transition_listeners'][Inflector::underscore($transition)][$triggerType][] = array(
407
			'cb' => $cb,
408
			'bubble' => $bubble
409
		);
410
	}
411
412
/**
413
 * Registers a callback that will be called when the state machine enters the given
414
 * state.
415
 *
416
 * @param Model $model The model being acted on
417
 * @param string $state The state which the machine should enter
418
 * @param string $cb The callback function that will be called
419
 * @return void
420
 */
421
	public function when(Model $model, $state, $cb) {
422
		$this->settings[$model->alias]['state_listeners'][Inflector::underscore($state)][] = $cb;
423
	}
424
425
/**
426
 * Returns the states the machine would be in, after the given transition
427
 *
428
 * @param Model $model The model being acted on
429
 * @param string $transition The transition name
430
 * @return mixed False if the transition doesnt yield any states, or an array of states
431
 */
432
	public function getStates(Model $model, $transition) {
433
		if (!isset($model->transitions[$transition])) {
434
			// transition doesn't exist
435
			return false;
436
		}
437
438
		// get the states the machine can move from and to
439
		$states = $model->transitions[$transition];
440
		$currentState = $model->getCurrentState();
441
442
		if (isset($states[$currentState])) {
443
			return $states[$currentState];
444
		}
445
446
		if (isset($states['all'])) {
447
			return $states['all'];
448
		}
449
450
		return false;
451
	}
452
453
/**
454
 * Returns the current state of the machine
455
 *
456
 * @param Model $model The model being acted on
457
 * @param int $id The id of the item to check
458
 * @return string The current state of the machine
459
 */
460
	public function getCurrentState(Model $model, $id = null) {
461
		if ($id === null) {
462
			$id = $model->getID();
463
		}
464
		if ($id === false) {
465
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by StateMachineBehavior::getCurrentState of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
466
		}
467
		$model->id = $id;
468
		return (($model->field('state') != null)) ? $model->field('state') : $model->initialState;
469
	}
470
471
/**
472
 * Returns the previous state of the machine
473
 *
474
 * @param Model $model The model being acted on
475
 * @param int $id The id of the item to check
476
 * @return string The previous state of the machine
477
 */
478 View Code Duplication
	public function getPreviousState(Model $model, $id = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
479
		if ($id === null) {
480
			$id = $model->getID();
481
		}
482
		if ($id === false) {
483
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by StateMachineBehavior::getPreviousState of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
484
		}
485
		$model->id = $id;
486
		return $model->field('previous_state');
487
	}
488
489
/**
490
 * Returns the last transition ran
491
 *
492
 * @param Model $model The model being acted on
493
 * @param int $id The id of the item to check
494
 * @return string The transition last ran of the machine
495
 */
496 View Code Duplication
	public function getLastTransition(Model $model, $id = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
497
		if ($id === null) {
498
			$id = $model->getID();
499
		}
500
		if ($id === false) {
501
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by StateMachineBehavior::getLastTransition of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
502
		}
503
		$model->id = $id;
504
		return $model->field('last_transition');
505
	}
506
507
/**
508
 * Returns the role that ran the last transition
509
 *
510
 * @param Model $model The model being acted on
511
 * @param int $id The id of the item to check
512
 * @return string The role that last ran a transition of the machine
513
 */
514 View Code Duplication
	public function getLastRole(Model $model, $id = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
515
		if ($id === null) {
516
			$id = $model->getID();
517
		}
518
		if ($id === false) {
519
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by StateMachineBehavior::getLastRole of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
520
		}
521
		$model->id = $id;
522
		return $model->field('last_role');
523
	}
524
525
/**
526
 * Simple method to return contents for a GV file, that
527
 * can be made into graphics by:
528
 * {{{
529
 * dot -Tpng -ofsm.png fsm.gv
530
 * }}}
531
 * Assuming that the contents are written to the file fsm.gv
532
 *
533
 * @param Model $model The model being acted on
534
 * @return string The contents of the graphviz file
535
 */
536
	public function toDot(Model $model) {
537
		$digraph = <<<EOT
538
digraph finite_state_machine {
539
	rankdir=LR
540
	fontsize=12
541
	node [shape = circle];
542
543
EOT;
544
545
		foreach ($model->transitions as $transition => $states) {
546
			foreach ($states as $stateFrom => $stateTo) {
547
				$digraph .= sprintf("\t%s -> %s [ label = \"%s\" ];\n", $stateFrom, $stateTo, $transition);
548
			}
549
		}
550
551
		return $digraph . "}";
552
	}
553
554
/**
555
 * This method prepares an array for each transition in the statemachine making it easier to iterate throug the machine for
556
 * output to various formats
557
 *
558
 * @param Model $model The model being acted on
559
 * @param array $roles The role(s) executing the transition change. with an options array.
560
 *                               'role' => array('color' => color of the arrows)
561
 *                               In the future many more Graphviz options can be added
562
 * @return array      returns an array of all transitions
563
 * @author  Frode Marton Meling <[email protected]>
564
 */
565
	public function prepareForDotWithRoles(Model $model, $roles) {
566
		$preparedForDotArray = array();
567
		foreach ($model->transitions as $transition => $states) {
568
			foreach ($roles as $role => $options) {
569
				foreach ($states as $stateFrom => $stateTo) {
570
					// if roles are not defined in transitionRules we add or if roles are defined, at least one needs to be present
571
					if (!isset($model->transitionRules[$transition]['role']) || (isset($model->transitionRules[$transition]['role']) && $this->_containsAnyRoles($model->transitionRules[$transition]['role'], $roles))) {
572
						$dataToPrepare = array(
573
							'stateFrom' => $stateFrom,
574
							'stateTo' => $stateTo,
575
							'transition' => $transition
576
						);
577
						if (isset($model->transitionRules[$transition]['role'])) {
578
							if (in_array($role, $model->transitionRules[$transition]['role'])) {
579
								$dataToPrepare['roles'] = array($role);
580
							}
581
						}
582
						if (isset($model->transitionRules[$transition]['depends'])) {
583
							$dataToPrepare['depends'] = $model->transitionRules[$transition]['depends'];
584
						}
585
						// we do not add if role is given as transitionRule, but part is not in it.
586
						$preparedForDotArray = $this->addToPrepareArray($model, $dataToPrepare, $preparedForDotArray);
587
					}
588
				}
589
			}
590
		}
591
		return $preparedForDotArray;
592
	}
593
594
/**
595
 * Method to return contents for a GV file based on array of roles. That means you can send
596
 * an array of roles (with options) and this method will calculate the presentation that
597
 * can be made into graphics by:
598
 * {{{
599
 * dot -Tpng -ofsm.png fsm.gv
600
 * }}}
601
 * Assuming that the contents are written to the file fsm.gv
602
 *
603
 * @param Model $model The model being acted on
604
 * @param array $roles The role(s) executing the transition change. with an options array.
605
 *                                 'role' => array('color' => color of the arrows)
606
 *                                 In the future many more Graphviz options can be added
607
 * @param array $dotOptions Options for nodes
608
 *                                 'color' => 'color of all nodes'
609
 *                                 'activeColor' => 'the color you want the active node to have'
610
 * @return string The contents of the graphviz file
611
 * @author Frode Marton Meling <[email protected]>
612
 */
613
	public function createDotFileForRoles(Model $model, $roles, $dotOptions) {
614
		$transitionsArray = $this->prepareForDotWithRoles($model, $roles);
615
		$digraph = "digraph finite_state_machine {\n\tfontsize=12;\n\tnode [shape = oval, style=filled, color = \"%s\"];\n\tstyle=filled;\n\tlabel=\"%s\"\n%s\n%s}\n";
616
		$activeState = "\t" . "\"" . Inflector::humanize($this->getCurrentState($model)) . "\"" . " [ color = " . $dotOptions['activeColor'] . " ];";
617
618
		$node = "\t\"%s\" -> \"%s\" [ style = bold, fontsize = 9, arrowType = normal, label = \"%s %s%s\" %s];\n";
619
		$dotNodes = "";
620
621
		foreach ($transitionsArray as $transition) {
0 ignored issues
show
Bug introduced by
The expression $transitionsArray of type false|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
622
			$dotNodes .= sprintf($node,
623
				Inflector::humanize($transition['stateFrom']),
624
				Inflector::humanize($transition['stateTo']),
625
				Inflector::humanize($transition['transition']),
626
				(isset($transition['roles']) && (!$this->_containsAllRoles($transition['roles'], $roles) || (count($roles) == 1))) ? 'by (' . Inflector::humanize(implode(' or ', $transition['roles'])) . ')' : 'by All',
627
				(isset($transition['depends'])) ? "\nif " . Inflector::humanize($transition['depends']) : '',
628
				(isset($transition['roles']) && count($transition['roles']) == 1) ? "color = \"" . $roles[$transition['roles'][0]]['color'] . "\"" : ''//,
629
			);
630
		}
631
		$graph = sprintf($digraph, $dotOptions['color'], 'Statemachine for ' . Inflector::humanize($model->alias) . ' role(s) : ' . Inflector::humanize(implode(', ', $this->getAllRoles($model, $roles))), $activeState, $dotNodes);
632
		return $graph;
633
	}
634
635
/**
636
 * This helperfunction fetches out all roles from an array of roles with options. Note that this is a ('role' => $options) array
637
 * I did not find a php method for this, so made it myself
638
 *
639
 * @param Model $model The model being acted on
640
 * @param Array $roles This is just an array of roles like array('role1', 'role2'...)
641
 * @return Array Returns an array of roles like array('role1', 'role2'...)
642
 * @author Frode Marton Meling <[email protected]>
643
 * @todo Add separate tests @codingStandardsIgnoreLine
644
 */
645
	public function getAllRoles(Model $model, $roles) {
646
		$arrayToReturn = array();
647
		foreach ($roles as $role => $option) {
648
			$arrayToReturn[] = $role;
649
		}
650
		return $arrayToReturn;
651
	}
652
653
/**
654
 * This function is used to add transitions to Array. This tests for conditions and makes sure duplicates are not added.
655
 *
656
 * @param Model $model The model being acted on
657
 * @param array $data An array of a transition to be added
658
 * @param array $prepareArray The current array to populate
659
 * @return mixed
660
 * @author Frode Marton Meling <[email protected]>
661
 * @todo Move this to protected, Needs a reimplementation of the functiun in test to make it public for testing @codingStandardsIgnoreLine
662
 */
663
	public function addToPrepareArray(Model $model, $data, $prepareArray) {
664
		if (!is_array($data)) {
665
			return false;
666
		}
667
668
		if (!$this->_stateAndTransitionExist($data)) {
669
			return false;
670
		}
671
672
		// Check if we are preparing an object with states, transitions and depends
673
		if ($this->_stateTransitionAndDependsExist($data)) {
674
			$existingDataKey = $this->_stateTransitionAndDependsInArray($data, $prepareArray);
675 View Code Duplication
			if ($existingDataKey === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
676
				$prepareArray[] = $data;
677
			} elseif (isset($data['roles'])) {
678
				$this->_addRoles($data['roles'], $prepareArray[$existingDataKey]);
679
			}
680
			return $prepareArray;
681
		}
682
		$existingDataKey = $this->_stateAndTransitionInArray($data, $prepareArray);
683 View Code Duplication
		if ($existingDataKey !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
684
			if (isset($data['roles'])) {
685
				$this->_addRoles($data['roles'], $prepareArray[$existingDataKey]);
686
			}
687
			return $prepareArray;
688
		}
689
		$prepareArray[] = $data;
690
691
		return $prepareArray;
692
	}
693
694
/**
695
 * This helperfunction checks if all roles in an array (roles) is present in $allArrays. Note that this is a ('role' => $options) array
696
 * I did not find a php method for this, so made it myself
697
 *
698
 * @param Array $roles This is just an array of roles like array('role1', 'role2'...)
699
 * @param Array $allRoles This is the array to test on. This is a multidimentional array like array('role1' => array('of' => 'options'), 'role2' => array('of' => 'options') )
700
 * @return bool Returns true if all roles are present, otherwise false
701
 * @author Frode Marton Meling <[email protected]>
702
 * @todo Add separate tests @codingStandardsIgnoreLine
703
 */
704
	protected function _containsAllRoles($roles, $allRoles) {
705
		foreach ($allRoles as $role => $options) {
706
			if (!in_array($role, $roles)) {
707
				return false;
708
			}
709
		}
710
		return true;
711
	}
712
713
/**
714
 * This helperfunction checks if any of the roles in an array (roles) is present in $allArrays. Note that this is a ('role' => $options) array
715
 * I did not find a php method for this, so made it myself
716
 *
717
 * @param Array $roles This is just an array of roles like array('role1', 'role2'...)
718
 * @param Array $allRoles This is the array to test on. This is a multidimentional array like array('role1' => array('of' => 'options'), 'role2' => array('of' => 'options') )
719
 * @return bool Returns true if just one of the roles are present, otherwise false
720
 * @author Frode Marton Meling <[email protected]>
721
 * @todo Add separate tests @codingStandardsIgnoreLine
722
 */
723
	protected function _containsAnyRoles($roles, $allRoles) {
724
		$atleastOne = false;
725
		foreach ($allRoles as $role => $options) {
726
			if (in_array($role, $roles)) {
727
				$atleastOne = true;
728
			}
729
		}
730
		return $atleastOne;
731
	}
732
733
/**
734
 * This helperfunction adds a role to an array. It checks for duplicates and only adds if it is not already in array
735
 * If also checks that the resultArray is valid and that there are roles there to begin with
736
 *
737
 * @param Array $roles This is just an array of roles like array('role1', 'role2'...)
738
 * @param Array &$resultArray This function writes to this parameter by reference
739
 * @return bool Returns true if added, otherwise false
740
 * @author Frode Marton Meling <[email protected]>
741
 * @todo Add separate tests @codingStandardsIgnoreLine
742
 */
743
	protected function _addRoles($roles, &$resultArray) {
744
		$addedAtleastOne = false;
745
		foreach ($roles as $role) {
746
			if (!isset($resultArray['roles']) || isset($resultArray['roles']) && !in_array($role, $resultArray['roles'])) {
747
				$resultArray['roles'][] = $role;
748
				$addedAtleastOne = true;
749
			}
750
		}
751
		return $addedAtleastOne;
752
	}
753
754
/**
755
 * This helperfunction checks if state and transition is present in the array
756
 *
757
 * @param array $data The array to check
758
 * @return bool true if array is valid, otherwise false
759
 * @author Frode Marton Meling <[email protected]>
760
 * @todo Add separate tests @codingStandardsIgnoreLine
761
 */
762
	protected function _stateAndTransitionExist($data) {
763 View Code Duplication
		if (isset($data['stateFrom']) && isset($data['stateTo']) && isset($data['transition'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
764
			return true;
765
		}
766
		return false;
767
	}
768
769
/**
770
 * This helperfunction checks if state, transition and depends exist in array
771
 *
772
 * @param array $data The array to check
773
 * @return bool True if state, transition and depends exist in array, otherwise false
774
 * @author Frode Marton Meling <[email protected]>
775
 * @todo Add separate tests @codingStandardsIgnoreLine
776
 */
777
	protected function _stateTransitionAndDependsExist($data) {
778 View Code Duplication
		if (isset($data['stateFrom']) && isset($data['stateTo']) && isset($data['transition']) && isset($data['depends'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
779
			return true;
780
		}
781
		return false;
782
	}
783
784
/**
785
 * This helperfunction checks if state and transition is present in prepareArray. this is used to prevent adding duplicates
786
 *
787
 * @param array $data The array for testing
788
 * @param array $prepareArray The array to check against
789
 * @return bool index in array if state and transition is present in prepareArray, otherwise false
790
 * @author Frode Marton Meling <[email protected]>
791
 * @todo Add separate tests @codingStandardsIgnoreLine
792
 */
793
	protected function _stateAndTransitionInArray($data, $prepareArray) {
794
		foreach ($prepareArray as $key => $value) {
795
			if (($value['stateFrom'] == $data['stateFrom']) && ($value['stateTo'] == $data['stateTo']) && ($value['transition'] == $data['transition'])) {
796
				return $key;
797
			}
798
		}
799
		return false;
800
	}
801
802
/**
803
 * This helperfunction checks if state, transition and depends is present in prepareArray. this is used to prevent adding duplicates
804
 *
805
 * @param array $data The array for testing
806
 * @param array $prepareArray The array to check against
807
 * @return bool the index in array if state, transition and depends is present in prepareArray, otherwise false
808
 * @author Frode Marton Meling <[email protected]>
809
 * @todo Add separate tests @codingStandardsIgnoreLine
810
 */
811
	protected function _stateTransitionAndDependsInArray($data, $prepareArray) {
812
		foreach ($prepareArray as $key => $value) {
813
			if (!isset($value['depends'])) {
814
				continue;
815
			}
816
			if (($value['stateFrom'] == $data['stateFrom']) && ($value['stateTo'] == $data['stateTo']) && ($value['transition'] == $data['transition']) && ($value['depends'] == $data['depends'])) {
817
				return $key;
818
			}
819
		}
820
		return false;
821
	}
822
823
/**
824
 * Checks whether or not the given role may perform the transition change.
825
 * The callback in 'depends' must be a valid model method.
826
 *
827
 * @param Model $model The model being acted on
828
 * @param string $role The role executing the transition change
829
 * @param string $transition The transition
830
 * @throws InvalidArgumentException if the transition require it be executed by a rule, and none is given
831
 * @return bool Whether or not the role may perform the action
832
 */
833
	protected function _checkRoleAgainstRule(Model $model, $role, $transition) {
834
		if (!isset($model->transitionRules[$transition])) {
835
			return null;
836
		}
837
838
		if (!$role) {
839
			throw new InvalidArgumentException('The transition ' . $transition . ' requires a role');
840
		}
841
842
		if (!in_array($role, $model->transitionRules[$transition]['role'])) {
843
			return false;
844
		}
845
846
		if (!isset($model->transitionRules[$transition]['depends'])) {
847
			return true;
848
		}
849
850
		$callback = Inflector::variable($model->transitionRules[$transition]['depends']);
851
852
		if ($this->_hasMethod($model, $callback)) {
853
			// Fix: if the method is supplied as an anonymous callback, we cannot call
854
			// it from the model directly
855
			$res = $this->settings[$model->alias]['methods'][$callback]($role);
856
		} else {
857
			$res = call_user_func(array($model, $callback), $role);
858
		}
859
860
		return $res;
861
	}
862
863
/**
864
 * Calls transition listeners before or after a particular transition.
865
 * Special model methods are also called, if they exist:
866
 * - onBeforeTransition
867
 * - onAfterTransition
868
 * - onBefore<Transition>    i.e. onBeforePark()
869
 * - onAfter<Transition>    i.e. onAfterPark()
870
 *
871
 * @param Model $model The model being acted on
872
 * @param string $transition The transition name
873
 * @param string $triggerType Either before or after
874
 * @return void
875
 */
876
	protected function _callTransitionListeners(Model $model, $transition, $triggerType = 'after') {
877
		$transitionListeners = & $this->settings[$model->alias]['transition_listeners'];
878
		$listeners = $transitionListeners['transition'][$triggerType];
879
880
		if (isset($transitionListeners[$transition][$triggerType])) {
881
			$listeners = array_merge($transitionListeners[$transition][$triggerType], $listeners);
882
		}
883
884
		foreach (array(
885
					'on' . Inflector::camelize($triggerType . 'Transition'),
886
					'on' . Inflector::camelize($triggerType . $transition)
887
				) as $method) {
888
			if (method_exists($model, $method)) {
889
				$listeners[] = array(
890
					'cb' => array($model, $method),
891
					'bubble' => true
892
				);
893
			}
894
		}
895
896
		$currentState = $this->getCurrentState($model);
897
		$previousState = $this->getPreviousState($model);
898
899
		foreach ($listeners as $cb) {
900
			call_user_func_array($cb['cb'], array($currentState, $previousState, $transition));
901
902
			if (!$cb['bubble']) {
903
				break;
904
			}
905
		}
906
	}
907
908
/**
909
 * Deformalizes a method name, removing 'can' and 'is' as well as underscoring
910
 * the remaining text.
911
 *
912
 * @param string $name The model name
913
 * @return string The deformalized method name
914
 */
915
	protected function _deFormalizeMethodName($name) {
916
		return Inflector::underscore(preg_replace('#^(can|is)#', '', $name));
917
	}
918
919
/**
920
 * Checks whether or not a user-defined method exists in the Behavior
921
 *
922
 * @param Model $model The model being acted on
923
 * @param string $method The method's name
924
 * @return bool True if the method exists, false otherwise
925
 */
926
	protected function _hasMethod(Model $model, $method) {
927
		return isset($this->settings[$model->alias]['methods'][$method]) || isset($this->mapMethods['/' . $method . '/']);
928
	}
929
}
930