Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
60.00% |
27 / 45 |
CRAP | |
86.82% |
303 / 349 |
SWActiveRecordBehavior | |
0.00% |
0 / 1 |
|
60.00% |
27 / 45 |
173.50 | |
86.82% |
303 / 349 |
swGetWorkflowSource() | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
canFireEvent($owner,$className) | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
swGetStatus() | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
swIsEventEnabled() | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
swIsStatus($status) | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 1 |
|||
swStatusEquals($status=null) | |
0.00% |
0 / 1 |
6.56 | |
75.00% |
3 / 4 |
|||
swHasStatus() | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
_lock() | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
_unlock() | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
_updateStatus($SWNode) | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
_updateOwnerStatus($status) | |
0.00% |
0 / 1 |
4.12 | |
50.00% |
3 / 6 |
|||
swGetWorkflowId() | |
100.00% |
1 / 1 |
2 | |
100.00% |
1 / 1 |
|||
attach($owner) | |
100.00% |
1 / 1 |
8 | |
100.00% |
18 / 18 |
|||
swGetDefaultWorkflowId() | |
0.00% |
0 / 1 |
7.44 | |
79.17% |
19 / 24 |
|||
swInsertToWorkflow($workflowId=null) | |
0.00% |
0 / 1 |
4.01 | |
92.86% |
13 / 14 |
|||
swRemoveFromWorkflow() | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 10 |
|||
swGetNextStatus() | |
0.00% |
0 / 1 |
5.01 | |
92.86% |
13 / 14 |
|||
swGetAllStatus() | |
100.00% |
1 / 1 |
3 | |
100.00% |
3 / 3 |
|||
swIsNextStatus($nextStatus) | |
0.00% |
0 / 1 |
6.01 | |
93.33% |
14 / 15 |
|||
swCreateNode($str) | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
_evaluateConstraint($constraint) | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
_runTransition($sourceSt,$destSt,$params=null) | |
0.00% |
0 / 1 |
6 | |
95.00% |
19 / 20 |
|||
swIsFinalStatus($status=null) | |
0.00% |
0 / 1 |
5.01 | |
91.67% |
11 / 12 |
|||
swIsInitialStatus($status=null) | |
100.00% |
1 / 1 |
4 | |
100.00% |
14 / 14 |
|||
swValidate($attribute, $value) | |
0.00% |
0 / 1 |
5.64 | |
70.59% |
12 / 17 |
|||
swNextStatus($nextStatus,$params=null) | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
swSetStatus($nextStatus,$params=null) | |
0.00% |
0 / 1 |
14.13 | |
91.38% |
53 / 58 |
|||
events() | |
0.00% |
0 / 1 |
3 | |
95.83% |
23 / 24 |
|||
beforeSave($event) | |
0.00% |
0 / 1 |
4.18 | |
77.78% |
7 / 9 |
|||
afterSave($event) | |
0.00% |
0 / 1 |
3.43 | |
63.64% |
7 / 11 |
|||
afterFind($event) | |
0.00% |
0 / 1 |
4.01 | |
90.91% |
10 / 11 |
|||
_logEventFire($ev,$source,$dest) | |
100.00% |
1 / 1 |
2 | |
100.00% |
8 / 8 |
|||
_raiseEvent($evName,$event) | |
100.00% |
1 / 1 |
2 | |
100.00% |
5 / 5 |
|||
enterWorkflow($event) | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
onEnterWorkflow($event) | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
leaveWorkflow($event) | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
onLeaveWorkflow($event) | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
beforeTransition($event) | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
onBeforeTransition($event) | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
processTransition($event) | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
onProcessTransition($event) | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
afterTransition($event) | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
onAfterTransition($event) | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
|||
finalStatus($event) | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
onFinalStatus($event) | |
100.00% |
1 / 1 |
3 | |
100.00% |
5 / 5 |
<?php | |
/** | |
* This class implements all the logic for the simpleWorkflow extension. | |
* Following attributes can be initialized when this behavior is attached to the owner component : | |
* <ul> | |
* <li><b>statusAttribute</b> (string) : This is the column name where status is stored<br/> | |
* If this attribute doesn't exist for a model, the Workflow behavior is automatically disabled and a warning is | |
* logged.<br/> | |
* In the database, this attribute must be defined as a VARCHAR() whose length should be large enough to | |
* contains a complete status name with format <b>workflowId/nodeId</b>.<br/> | |
* example : | |
* <pre> | |
* task/pending | |
* postWorkflow/to_review | |
* </pre> | |
* Default : 'status' | |
* </li> | |
* <li><b>defaultWorkflow</b> (string) : workflow name that should be used by default for the owner model <br/> | |
* If this parameter is not set, then it is automatically created based on the name of the owner model, prefixed | |
* with 'workflowNamePrefix' defined by the workflow source component. By default this value is set to 'sw' and so, | |
* for example 'Model1' is associated by default with workflow 'swModel1'.<br/> | |
* Default : SWWorkflowSource->workflowNamePrefix . ModelName | |
* </li> | |
* <li><b>autoInsert</b> (boolean) : <br/> | |
* If TRUE, the model is automatically inserted in the workflow (if not already done) when it is saved. | |
* If FALSE, it is developer responsability to insert the model in the workflow.<br/> | |
* Default : true | |
* </li> | |
* <li><b>workflowSourceComponent</b> (string) : <br/> | |
* Name of the workflow source component to use with this behavior.<br/> | |
* By ddefault this parameter is set to <em>swSource</em> (see {@link SWPhpWorkflowSource}) | |
* </li> | |
* <li><b>enableEvent</b> (boolean) : <br/> | |
* If TRUE, this behavior will fire SWEvents. Note that even if it | |
* is true, this doesn't garantee that SW events will be fired as another condition is that the owner | |
* component provides SWEvent handlers.<br/> | |
* Default : true | |
* </li> | |
* <li><b>transitionBeforeSave</b> (boolean) : <br/> | |
* If TRUE, SWEvents are fired and possible transitions tasks are executed <b>before</b> the owner model is | |
* actually saved. If FALSE, events and task transitions are processed after save.<br/> | |
* It has no effect if the transition is done programatically by a call to swNextStatus(), but only if it is done when the | |
* owner model is saved.<br/> | |
* Default : true | |
* </li> | |
* </ul> | |
*/ | |
class SWActiveRecordBehavior extends CBehavior | |
{ | |
/** | |
* @var string This is the column name where status is stored. | |
*/ | |
public $statusAttribute = 'status'; | |
/** | |
* @var string workflow name that should be used by default for the owner model. | |
*/ | |
public $defaultWorkflow=null; | |
/** | |
* @var boolean | |
*/ | |
public $autoInsert=true; | |
/** | |
* @var string name of the workflow source component | |
*/ | |
public $workflowSourceComponent='swSource'; | |
/** | |
* @var boolean | |
*/ | |
public $enableEvent=true; | |
/** | |
* @var boolean | |
*/ | |
public $transitionBeforeSave=true; | |
/////////////////////////////////////////////////////////////////////////////////////////// | |
// private members | |
private $_delayedTransition=null; // delayed transition (only when change status occures during save) | |
private $_delayedEvent=array(); // delayed event stack (only when change status occures during save) | |
private $_beforeSaveInProgress=false; // prevent delayed event fire when status is changed by a call to swNextStatus | |
private $_status=null; // internal status for the owner model | |
private $_wfs; // workflow source component reference | |
private $_locked=false; // prevent reentrance | |
private $_final=null; | |
// | |
/////////////////////////////////////////////////////////////////////////////////////////// | |
/** | |
* @var string name of the class the owner should inherit from in order for SW events | |
* to be enabled. | |
*/ | |
protected $eventClassName='SWActiveRecord'; | |
const SW_LOG_CATEGORY='application.simpleWorkflow'; | |
const SW_I8N_CATEGORY='simpleworkflow'; | |
/** | |
* @return reference to the workflow source used by this behavior | |
*/ | |
public function swGetWorkflowSource() | |
{ | |
return $this->_wfs; | |
} | |
/** | |
* Checks that the owner component is able to handle workflow events that could be fired | |
* by this behavior | |
* | |
* @param CComponent $owner the owner component attaching this behavior | |
* @param string $className | |
* @return bool TRUE if workflow events are fired, FALSE if not. | |
*/ | |
protected function canFireEvent($owner,$className) | |
{ | |
return $owner instanceof $className; | |
} | |
/** | |
* If the owner component is inserted into a workflow, this method returns the SWNode object | |
* that represent this status, otherwise NULL is returned. | |
* | |
* @return SWNode the current status or NULL if no status is set | |
*/ | |
public function swGetStatus() | |
{ | |
return $this->_status; | |
} | |
/** | |
* Event may be enabled by configuration (when the behavior is attached to the owner component) but it | |
* can be automatically disabled if the owner component does not define handlers for all SWEvents (i.e events | |
* fired when the owner component evolves in the workflow). | |
* {@link SWActiveRecordBehavior::attach} | |
* | |
* @return bool TRUE if workflow events are fire by this behavior, FALSE if not. | |
*/ | |
public function swIsEventEnabled() | |
{ | |
return $this->enableEvent; | |
} | |
/** | |
* Test if the owner component is currently in the status passed as argument. | |
* | |
* @param mixed $status name or SWNode instance of the status to test | |
* @returns boolean TRUE if the owner component is in the status passed as argument, FALSE otherwise | |
*/ | |
public function swIsStatus($status) | |
{ | |
return $this->swHasStatus() && $this->swGetStatus()->equals($status); | |
} | |
/** | |
* Test if the current status is the same as the one passed as argument. | |
* A call to swStatusEquals(<em>null</em>) returns TRUE only if the owner component is not in a workflow. | |
* | |
* @param mixed $status string or SWNode instance. | |
* @return boolean | |
*/ | |
public function swStatusEquals($status=null) | |
{ | |
if( ($status == null && $this->swHasStatus() == false) || | |
($status != null && $this->swHasStatus() && $this->swGetStatus()->equals($status)) ) | |
return true; | |
else | |
return false; | |
} | |
/** | |
* Test if the owner component is currently inserted in a workflow. | |
* This method is equivalent to swGetStatus()!=null. | |
* | |
* @return boolean true if the owner model is in a workflow, FALSE otherwise | |
* @see swGetStatus | |
*/ | |
public function swHasStatus() | |
{ | |
return ! $this->_status == null; | |
} | |
/** | |
* acquire the lock in order to avoid reentrance | |
* | |
* @throws SWException | |
*/ | |
private function _lock() | |
{ | |
if($this->_locked==true){ | |
throw new SWException('re-entrant exception on set status',SWException::SW_ERR_REETRANCE); | |
} | |
$this->_locked=true; | |
} | |
/** | |
* Release the lock | |
*/ | |
private function _unlock() | |
{ | |
$this->_locked=false; | |
} | |
/** | |
* Update the owner model attribute configured to store the current status and the internal | |
* value too. | |
* | |
* @param SWnode $SWNode internal status is set to this node | |
*/ | |
private function _updateStatus($SWNode) | |
{ | |
if(! $SWNode instanceof SWNode) | |
throw new SWException('SWNode object expected',SWException::SW_ERR_WRONG_TYPE); | |
$this->_status=$SWNode; | |
$this->_final = null; | |
} | |
/** | |
* Updates the owner component status attribute with the value passed as argument. | |
* | |
* @param mixed $status the new owner status value provided as a SWNode object or string | |
*/ | |
private function _updateOwnerStatus($status) | |
{ | |
if($status instanceof SWNode) | |
$this->getOwner()->{$this->statusAttribute} = $status->toString(); | |
elseif( is_string($status)) | |
$this->getOwner()->{$this->statusAttribute} = $status; | |
else | |
throw new SWException('SWNode or string expected',SWException::SW_ERR_WRONG_TYPE); | |
} | |
/** | |
* Returns the current workflow Id the owner component is inserted in, or NULL if the owner | |
* component is not inserted into a workflow. | |
* | |
* @param string current workflow Id or NULL | |
*/ | |
public function swGetWorkflowId() | |
{ | |
return ($this->swHasStatus()?$this->_status->getWorkflowId():null); | |
} | |
/** | |
* Overloads parent attach method so at the time the behavior is about to be | |
* attached to the owner component, the behavior is initialized.<br/> | |
* During the initialisation, following actions are performed:<br/> | |
* <ul> | |
* <li>The status attribute exists</li> | |
* <li>Check whether or not, workflow events should be enabled, by testing if the owner component | |
* class inherits from the 'SWComponent' or 'SWActiveRecord' class. </li> | |
* </ul> | |
* | |
* @see base/CBehavior::attach() | |
*/ | |
public function attach($owner) | |
{ | |
if( ! $this->canFireEvent($owner, $this->eventClassName)){ | |
if( $this->swIsEventEnabled()){ | |
// workflow events are enabled by configuration but the owner component is not | |
// able to handle workflow event : warning | |
Yii::log('events disabled : owner component doesn\'t inherit from '. $this->eventClassName, | |
CLogger::LEVEL_WARNING,self::SW_LOG_CATEGORY); | |
} | |
$this->enableEvent=false; // force | |
} | |
parent::attach($owner); | |
if( $this->getOwner() instanceof CActiveRecord ){ | |
$statusAttributeCol = $this->getOwner()->getTableSchema()->getColumn($this->statusAttribute); | |
if(!isset($statusAttributeCol) || $statusAttributeCol->type != 'string' ){ | |
throw new SWException('attribute '.$this->statusAttribute.' not found',SWException::SW_ERR_ATTR_NOT_FOUND); | |
} | |
} | |
// preload the workflow source component | |
$this->_wfs= Yii::app()->{$this->workflowSourceComponent}; | |
// load the default workflow id now because the owner model maybe able to provide it | |
// together with the whole workflow definition. In this case, this definition must be pushed | |
// to the SWWorkflowSource component (done by swGetDefaultWorkflowId). | |
$defWid = $this->swGetDefaultWorkflowId(); | |
// autoInsert now ! | |
if($this->autoInsert == true && $this->getOwner()->{$this->statusAttribute} == null){ | |
$this->swInsertToWorkflow($defWid); | |
} | |
} | |
/** | |
* Finds out what should be the default workflow to use with the owner model. | |
* To find out what is the default workflow, this method perform following tests : | |
* <ul> | |
* <li>behavior initialization parameter <i>defaultWorkflow</i></li> | |
* <li>owner component method <i>workflow</i> : if the owner component is able to provide the | |
* complete workflow, this method will invoke SWWorkflowSource.addWorkflow</li> | |
* <li>created based on the configured prefix followed by the model class name. The default workflow prefix is 'sw' so | |
* if the owner model is MyModel, the default workflow id will be swMyModel (case sensitive) </li> | |
* </ul> | |
* @return string workflow id to use with the owner component or NULL if no workflow was found | |
*/ | |
public function swGetDefaultWorkflowId() | |
{ | |
if( $this->defaultWorkflow == null) | |
{ | |
$workflowName=null; | |
if( $this->defaultWorkflow != null) | |
{ | |
// the behavior has been initialized with the default workflow name | |
$workflowName=$this->defaultWorkflow; | |
} | |
elseif(method_exists($this->getOwner(),'workflow')) | |
{ | |
$wf=$this->getOwner()->workflow(); | |
if( is_array($wf)){ | |
// Cool ! the owner is able to provide its own private workflow definition ...and optionally | |
// a workflow name too. If no workflow name is provided, the model name is used to | |
// identity the workflow | |
$workflowName=(isset($wf['name']) | |
? $wf['name'] | |
: $this->swGetWorkflowSource()->workflowNamePrefix.get_class($this->getOwner()) | |
); | |
$this->swGetWorkflowSource()->addWorkflow($wf,$workflowName); | |
}elseif(is_string($wf)) { | |
// the owner returned a string considered as its default workflow Id | |
$workflowName=$wf; | |
}else { | |
throw new SWException('incorrect type returned by owner method : string or array expected',SWException::SW_ERR_WRONG_TYPE); | |
} | |
}else { | |
// ok then, let's use the owner model name as the workflow name and hope that | |
// its definition is available in the workflow basePath. | |
$workflowName=$this->swGetWorkflowSource()->workflowNamePrefix.get_class($this->getOwner()); | |
} | |
$this->defaultWorkflow=$workflowName; | |
} | |
return $this->defaultWorkflow; | |
} | |
/** | |
* Insert the owner component into the workflow whose id is passed as argument. | |
* If NULL is passed as argument, the default workflow is used. If no error occurs, when this method ends, the owner | |
* component's status is the initial node of the selected workflow. | |
* | |
* @param string $workflowId workflow Id or NULL. If NULL the default workflow Id is used | |
* @throws SWException the owner model is already in a workflow | |
* @return boolean TRUE | |
*/ | |
public function swInsertToWorkflow($workflowId=null) | |
{ | |
if($this->swHasStatus()){ | |
throw new SWException('object already in a workflow : '.$this->swGetStatus(),SWException::SW_ERR_IN_WORKFLOW); | |
} | |
$wfName=( $workflowId == null | |
? $this->swGetDefaultWorkflowId() | |
: $workflowId | |
); | |
if( $wfName == null ){ | |
throw new SWException('failed to get the workflow name',SWException::SW_ERR_IN_WORKFLOW); | |
} | |
$initialNode=$this->swGetWorkflowSource()->getInitialNode($wfName); | |
$this->onEnterWorkflow( | |
new SWEvent($this->getOwner(),null,$initialNode) | |
); | |
$this->_updateStatus($initialNode); | |
$this->_updateOwnerStatus($initialNode); | |
return true; | |
} | |
/** | |
* Removes the owner component from its current workflow. | |
* An exception is thrown if the owner model is not in a final status (i.e a status | |
* with no outgoing transition). | |
* | |
* see {@link SWActiveRecordBehavior::swIsFinalStatus()} | |
* @throws SWException | |
*/ | |
public function swRemoveFromWorkflow() | |
{ | |
if( $this->swIsFinalStatus() == false) | |
throw new SWException('current status is not final : '.$this->swGetStatus()->toString(), | |
SWException::SW_ERR_STATUS_UNREACHABLE); | |
$this->onLeaveWorkflow( | |
new SWEvent($this->getOwner(),$this->_status,null) | |
); | |
$this->_status = null; | |
$this->_final = null; | |
$this->_updateOwnerStatus(''); | |
} | |
/** | |
* This method returns a list of nodes that can be actually reached at the time the method is called. To be reachable, | |
* a transition must exist between the current status and the next status, AND if a constraint is defined, it must be | |
* evaluated to true. | |
* | |
* @return array SWNode object array for all nodes thats can be reached from the current node. | |
*/ | |
public function swGetNextStatus() | |
{ | |
$n=array(); | |
if($this->swHasStatus()){ | |
$allNxtSt=$this->swGetWorkflowSource()->getNextNodes($this->_status); | |
if( $allNxtSt != null) | |
{ | |
foreach ( $allNxtSt as $aStatus ) { | |
if($this->swIsNextStatus($aStatus) == true){ | |
$n[]=$aStatus; | |
} | |
} | |
} | |
}else{ | |
$n[]=$this->swGetWorkflowSource()->getInitialNode($this->swGetDefaultWorkflowId()); | |
} | |
return $n; | |
} | |
/** | |
* Returns all statuses belonging to the workflow the owner component is inserted in or is related to. If the | |
* owner component is not inserted in a workflow or related to no workflow, an empty array is returned. | |
* | |
* @return array list of SWNode objects. | |
*/ | |
public function swGetAllStatus() | |
{ | |
if(!$this->swHasStatus() || $this->swGetWorkflowId() == null) | |
return array(); | |
else | |
return $this->swGetWorkflowSource()->getAllNodes($this->swGetWorkflowId()); | |
} | |
/** | |
* Checks if the status passed as argument can be reached from the current status. This occurs when | |
* <br/> | |
* <ul> | |
* <li>a transition has been defined in the workflow between those 2 status</li> | |
* <li>the destination status has a constraint that is evaluated to true in the context of the | |
* owner model</li> | |
* </ul> | |
* Note that if the owner component is not in a workflow, this method returns true if argument | |
* $nextStatus is the initial status for the workflow associated with the owner model. In other words | |
* the initial status for a given workflow is considered as the 'next' status, for all component associated | |
* to this workflow but not inserted in it. Of course, if a constraint is associated with the initial | |
* status, it must be evaluated to true. | |
* | |
* @param mixed nextStatus String or SWNode object for the next status | |
* @return boolean TRUE if the status passed as argument can be reached from the current status, FALSE | |
* otherwise. | |
*/ | |
public function swIsNextStatus($nextStatus) | |
{ | |
$bIsNextStatus=false; | |
// get (create) a SWNode object | |
$nxtNode=$this->swGetWorkflowSource()->createSWNode( | |
$nextStatus, | |
$this->swGetDefaultWorkflowId() | |
); | |
if( (! $this->swHasStatus() and $this->swIsInitialStatus($nextStatus)) or | |
( $this->swHasStatus() and $this->swGetWorkflowSource()->isNextNode($this->_status,$nxtNode)) ){ | |
// Note : the transition NULL -> S is valid only if S is an initial status | |
// there is a transition between current and next status, | |
// now let's see if constraints to actually enter in the next status | |
// are evaluated to true. | |
$swNodeNext=$this->swGetWorkflowSource()->getNodeDefinition($nxtNode); | |
if($this->_evaluateConstraint($swNodeNext->getConstraint()) == true) | |
{ | |
$bIsNextStatus=true; | |
} | |
else | |
{ | |
$bIsNextStatus=false; | |
} | |
} | |
return $bIsNextStatus; | |
} | |
/** | |
* Creates a new node from the string passed as argument. If $str doesn't contain | |
* a workflow Id, this method uses the workflowId associated with the owner | |
* model. The node created here doesn't have to exist within a workflow. | |
* This method is mainly used by the SWValidator | |
* | |
* @param string $str string status name | |
* @return SWNode the node | |
*/ | |
public function swCreateNode($str) | |
{ | |
return $this->swGetWorkflowSource()->createSWNode( | |
$str, | |
$this->swGetDefaultWorkflowId() | |
); | |
} | |
/** | |
* Evaluate the expression passed as argument in the context of the owner | |
* model and returns the result of evaluation as a boolean value. | |
*/ | |
private function _evaluateConstraint($constraint) | |
{ | |
return ( $constraint == null or | |
$this->getOwner()->evaluateExpression($constraint) ==true?true:false); | |
} | |
/** | |
* If a expression is attached to the transition, then it is evaluated in the context | |
* of the owner model, otherwise, the processTransition event is raised. Note that the value | |
* returned by the expression evaluation is ignored. | |
*/ | |
private function _runTransition($sourceSt,$destSt,$params=null) | |
{ | |
if($sourceSt != null && $sourceSt instanceof SWNode ){ | |
$tr=$sourceSt->getTransitionTask($destSt); | |
if( $tr != null) | |
{ | |
if( $this->transitionBeforeSave){ | |
if( is_string($tr)) | |
{ | |
$this->getOwner()->evaluateExpression($tr,array( | |
'owner' => $this->getOwner(), | |
'sourceStatus' => $sourceSt->toString(), | |
'targetStatus' => $destSt->toString(), | |
'params' => $params) | |
); | |
} | |
else | |
{ | |
$this->getOwner()->evaluateExpression($tr,array($this->getOwner(),$sourceSt->toString(), $destSt->toString(), $params)); | |
} | |
}else { | |
$this->_delayedTransition = $tr; | |
} | |
} | |
} | |
} | |
/** | |
* Checks if the status passed as argument, or the current status (if NULL is passed) is a final status | |
* of the corresponding workflow. | |
* By definition a final status as no outgoing transition to other status. | |
* | |
* @param status status to test, or null (will test current status) | |
* @return boolean TRUE when the owner component is in a final status, FALSE otherwise | |
*/ | |
public function swIsFinalStatus($status=null) | |
{ | |
if($this->_final == null) | |
{ | |
$workflowId=($this->swHasStatus()?$this->swGetWorkflowId():$this->swGetDefaultWorkflowId()); | |
if( $status != null){ | |
$swNode=$this->swGetWorkflowSource()->createSWNode($status,$workflowId); | |
}elseif($this->swHasStatus() == true) { | |
$swNode=$this->_status; | |
}else { | |
return false; | |
} | |
$this->_final = (count($this->swGetWorkflowSource()->getNextNodes($swNode,$workflowId))===0); | |
} | |
return $this->_final; | |
} | |
/** | |
* Checks if the status passed as argument, or the current status (if NULL is passed) is the initial status | |
* of the corresponding workflow. An exception is raised if the owner model is not in a workflow | |
* and if $status is null. | |
* | |
* @param mixed $status string or SWNode instance | |
* @return boolean TRUE if the owner component is in an initial status or if $status is an initial | |
* status. | |
* @throws SWException | |
*/ | |
public function swIsInitialStatus($status=null) | |
{ | |
if( $status != null) | |
{ | |
// create the node to compare with initial node | |
$workflowId=( $this->swHasStatus() | |
? $this->swGetWorkflowId() | |
: $this->swGetDefaultWorkflowId() | |
); | |
$swNode=$this->swGetWorkflowSource()->createSWNode($status,$workflowId); | |
} | |
elseif($this->swHasStatus() == true) | |
{ | |
// $status is null : the current status will be compared with initial node | |
$swNode=$this->_status; | |
} | |
else { | |
throw new SWException('no status passed and no current status available',SWException::SW_ERR_CREATE_FAILS); | |
} | |
$swInit=$this->swGetWorkflowSource()->getInitialNode($swNode->getWorkflowId()); | |
return $swInit->equals($swNode); | |
} | |
/** | |
* Validates the status attribute stored in the owner model. This attribute is valid if : <br/> | |
* <ul> | |
* <li>it is not empty</li> | |
* <li>it contains a valid status name</li> | |
* <li>this status can be reached from the current status</li> | |
* <li>or it is equal to the current status (no status change)</li> | |
* </ul> | |
* @param string $attribute status attribute name (by default 'status') | |
* @param mixed $value current value of the status attribute provided as a string or a SWNode object | |
* @return boolean TRUE if the status attribute contains a valid value, FALSE otherwise | |
*/ | |
public function swValidate($attribute, $value) | |
{ | |
$bResult=false; | |
try{ | |
if($value instanceof SWNode){ | |
$swNode=$value; | |
}else { | |
$swNode = $this->swGetWorkflowSource()->createSWNode( | |
$value, | |
$this->swGetDefaultWorkflowId() | |
); | |
} | |
if($this->swIsNextStatus($value)==false and $swNode->equals($this->swGetStatus()) == false){ | |
$this->getOwner()->addError($attribute,Yii::t(self::SW_I8N_CATEGORY,'not a valid next status')); | |
}else { | |
$bResult=true; | |
} | |
}catch(SWException $e){ | |
$this->getOwner()->addError($attribute,Yii::t(self::SW_I8N_CATEGORY,'value {node} is not a valid status',array( | |
'{node}'=>$value) | |
)); | |
} | |
return $bResult; | |
} | |
/** | |
* This is an alias for methode {@link SWActiveRecordBehavior::swSetStatus()} and should not be used anymore | |
* @deprecated | |
*/ | |
public function swNextStatus($nextStatus,$params=null) | |
{ | |
return $this->swSetStatus($nextStatus,$params); | |
} | |
/** | |
* Set the owner component into the status passed as argument. | |
* If a transition could be performed, the owner status attribute is updated with the new status value in the form <em>workflowId/nodeId</em>. | |
* This method is responsible for firing {@link SWEvents} and executing workflow tasks if defined for the given transition. | |
* | |
* @param mixed $nextStatus string or array. If array, it must contains a key equals to the name of the status | |
* attribute, and its value is the one of the destination node (e.g. $arr['status']). This is mainly useful when | |
* processing _POST array. If a string is provided, it must contain the fullname of the target node (e.g. <em>workfowId/nodeId</em>) | |
* @return boolean True if the transition could be performed, FALSE otherwise | |
*/ | |
public function swSetStatus($nextStatus,$params=null) | |
{ | |
if( $nextStatus == null ) | |
throw new SWException('argument "nextStatus" is missing'); | |
$bResult = false; | |
$nextNode = null; | |
if(is_array($nextStatus) && isset($nextStatus[$this->statusAttribute])) | |
{ | |
// $nextStatus may be provided as an array with a 'statusAttribute' key | |
// example : $array['status'] | |
$nextStatus=$nextStatus[$this->statusAttribute]; | |
} | |
elseif( $nextStatus instanceof SWNode) | |
{ | |
$nextStatus = $nextStatus->toString(); | |
} | |
try{ | |
$this->_lock(); | |
if( $this->swHasStatus() == false && $nextStatus != null) | |
{ | |
// insertion into workflow ////////////////////////////////////////////////////////////// | |
// $c->swNextStatus($status) was called. $c is not currently in a workflow and $status is | |
// assumed to be an initial node | |
$nextNode=$this->swGetWorkflowSource()->getNodeDefinition( | |
$nextStatus, | |
$this->swGetDefaultWorkflowId() | |
); | |
if( $this->swIsInitialStatus($nextNode) == false) | |
throw new SWException('status is not initial : '.$nextNode->toString(), | |
SWException::SW_ERR_STATUS_UNREACHABLE); | |
$this->onEnterWorkflow( | |
new SWEvent($this->getOwner(),null,$nextNode) | |
); | |
$this->_updateStatus($nextNode); | |
$this->_updateOwnerStatus($nextNode); | |
$bResult = true; | |
} | |
elseif( $this->swHasStatus() == true && $nextStatus != null) | |
{ | |
// perform transition ////////////////////////////////////////////////////////////// | |
$nextNode=$this->swGetWorkflowSource()->getNodeDefinition( | |
$nextStatus, | |
$this->swGetWorkflowId() | |
); | |
if( $this->swIsNextStatus($nextNode) ) | |
{ | |
$event=new SWEvent($this->getOwner(),$this->_status,$nextNode); | |
$this->onBeforeTransition($event); | |
$this->onProcessTransition($event); | |
$this->_runTransition($this->_status,$nextNode,$params); | |
$this->_updateStatus($nextNode); | |
$this->_updateOwnerStatus($nextNode); | |
$this->onAfterTransition($event); | |
if($this->swIsFinalStatus()){ | |
$this->onFinalStatus($event); | |
} | |
$bResult = true; | |
} | |
elseif( $nextNode->equals($this->swGetStatus()) == false) | |
{ | |
throw new SWException('no transition between current and next status : ' | |
.$this->swGetStatus()->toString().' -> '. $nextNode->toString(), | |
SWException::SW_ERR_STATUS_UNREACHABLE); | |
} | |
// else | |
// there is not transition between both status but as they are identical, no operation | |
// should be performed. | |
} | |
} catch (CException $e) { | |
$this->_unlock(); | |
Yii::log('set status failed : '.$e->getMessage(),CLogger::LEVEL_ERROR,self::SW_LOG_CATEGORY); | |
throw $e; | |
} | |
$this->_unlock(); | |
return $bResult; | |
} | |
/////////////////////////////////////////////////////////////////////////////////////// | |
// Events | |
// | |
/** | |
* Attach event handlers. | |
* The behavior registers its own mandatory event handlers in case the owner model is a CActiveRecord instance. | |
* <ul> | |
* <li>onBeforeSave : perform status validation and update if needed. If configured, a task is also executed</li> | |
* <li>onAfterSave : if configured a task is executed</li> | |
* <li>onAfterFind : initialize internal status value</li> | |
* </ul> | |
* Additionnally, the behavior will fire custom events on various steps of the owner model life-cycle within its workflow : | |
* <ul> | |
<li>onEnterWorkflow : the owner model is inserted in a workflow. Its status is now the initial status of the workflow</li> | |
<li>onFinalStatus : the owner model is in a status with no out going edge.</li> | |
<li>onLeaveWorkflow : the owner model status is set to NULL. This is possible only if the model is in a final status</li> | |
<li>onBeforeTransition : the owner model is about to change status</li> | |
<li>onProcessTransition : the owner model is changing status</li> | |
<li>onAfterTransition : the owner model has changed status</li> | |
</ul> | |
* @see base/CBehavior::events() | |
*/ | |
public function events() | |
{ | |
// this behavior could be attached to a CComponent based class other | |
// than CActiveRecord. | |
if($this->getOwner() instanceof CActiveRecord){ | |
$ev=array( | |
'onBeforeSave'=> 'beforeSave', | |
'onAfterSave' => 'afterSave', | |
'onAfterFind' => 'afterFind' | |
); | |
} else { | |
$ev=array(); | |
} | |
if($this->swIsEventEnabled()) | |
{ | |
$this->getOwner()->attachEventHandler('onEnterWorkflow',array($this->getOwner(),'enterWorkflow')); | |
$this->getOwner()->attachEventHandler('onBeforeTransition',array($this->getOwner(),'beforeTransition')); | |
$this->getOwner()->attachEventHandler('onAfterTransition',array($this->getOwner(),'afterTransition')); | |
$this->getOwner()->attachEventHandler('onProcessTransition',array($this->getOwner(),'processTransition')); | |
$this->getOwner()->attachEventHandler('onFinalStatus',array($this->getOwner(),'finalStatus')); | |
$this->getOwner()->attachEventHandler('onLeaveWorkflow',array($this->getOwner(),'leaveWorkflow')); | |
$ev=array_merge($ev, array( | |
// Custom events | |
'onEnterWorkflow' => 'enterWorkflow', | |
'onBeforeTransition' => 'beforeTransition', | |
'onProcessTransition'=> 'processTransition', | |
'onAfterTransition' => 'afterTransition', | |
'onFinalStatus' => 'finalStatus', | |
'onLeaveWorkflow' => 'leaveWorkflow', | |
)); | |
} | |
return $ev; | |
} | |
/** | |
* Depending on the value of the owner status attribute, and the current status, this method performs an | |
* actual transition. | |
* | |
* @param Event $event | |
* @return boolean | |
*/ | |
public function beforeSave($event) | |
{ | |
$this->_beforeSaveInProgress = true; | |
$ownerStatus = $this->getOwner()->{$this->statusAttribute}; | |
if( $ownerStatus == null && $this->swHasStatus() == false ) | |
{ | |
if($this->autoInsert == true) | |
$this->swNextStatus(); // insert into workflow | |
} | |
else | |
{ | |
$this->swNextStatus($ownerStatus); | |
} | |
$this->_beforeSaveInProgress = false; | |
return true; | |
} | |
/** | |
* When option transitionBeforeSave is false, if a task is associated with | |
* the transition that was performed, it is executed now, that it after the activeRecord | |
* owner component has been saved. The onAfterTransition is also raised. | |
* | |
* @param SWEvent $event | |
*/ | |
public function afterSave($event) | |
{ | |
if( $this->_delayedTransition != null ) | |
{ | |
$tr=$this->_delayedTransition; | |
$this->_delayedTransition=null; | |
$this->getOwner()->evaluateExpression($tr); | |
} | |
foreach ($this->_delayedEvent as $delayedEvent) { | |
$this->_raiseEvent($delayedEvent['name'],$delayedEvent['objEvent']); | |
} | |
$this->_delayedEvent=array(); | |
} | |
/** | |
* Responds to {@link CActiveRecord::onAfterFind} event. | |
* This method is called when a CActiveRecord instance is created from DB access (model | |
* read from DB). At this time, the worklow behavior must be initialized. | |
* | |
* @param CEvent event parameter | |
*/ | |
public function afterFind($event) | |
{ | |
if( !$this->getEnabled()) | |
return; | |
try{ | |
// call _init here because 'afterConstruct' is not called when an AR is created | |
// as the result of a query, and we need to initialize the behavior. | |
$status=$this->getOwner()->{$this->statusAttribute}; | |
if( $status != null ) | |
{ | |
// the owner model already has a status value (it has been read from db) | |
// and so, set the underlying status value without performing any transition | |
$st=$this->swGetWorkflowSource()->getNodeDefinition($status,$this->swGetWorkflowId()); | |
$this->_updateStatus($st); | |
} | |
}catch(SWException $e){ | |
Yii::log('failed to set status : '.$status. 'message : '.$e->getMessage(), CLogger::LEVEL_ERROR, self::SW_LOG_CATEGORY); | |
} | |
} | |
/** | |
* Log event fired | |
* | |
* @param string $ev event name | |
* @param SWNode $source | |
* @param SWNode $dest | |
*/ | |
private function _logEventFire($ev,$source,$dest) | |
{ | |
Yii::log(Yii::t('simpleWorkflow','event fired : \'{event}\' status [{source}] -> [{destination}]', | |
array( | |
'{event}' => $ev, | |
'{source}' => ( $source == null ?'null':$source), | |
'{destination}' => $dest, | |
)), | |
CLogger::LEVEL_INFO, | |
self::SW_LOG_CATEGORY | |
); | |
} | |
private function _raiseEvent($evName,$event) | |
{ | |
if( $this->swIsEventEnabled() ){ | |
$this->_logEventFire($evName, $event->source, $event->destination); | |
$this->getOwner()->raiseEvent($evName, $event); | |
} | |
} | |
/** | |
* Default implementation for the onEnterWorkflow event.<br/> | |
* This method is dedicated to be overloaded by custom event handler. | |
* @param SWEvent the event parameter | |
*/ | |
public function enterWorkflow($event) | |
{ | |
} | |
/** | |
* This event is raised after the record instance is inserted into a workflow. This may occur | |
* at construction time (new) if the behavior is initialized with autoInsert set to TRUE and in this | |
* case, the 'onEnterWorkflow' event is always fired. Consequently, when a model instance is created | |
* from database (find), the onEnterWorkflow is fired even if the record has already be inserted | |
* in a workflow (e.g contains a valid status). | |
* | |
* @param SWEvent the event parameter | |
*/ | |
public function onEnterWorkflow($event) | |
{ | |
$this->_raiseEvent('onEnterWorkflow',$event); | |
} | |
/** | |
* Default implementation for the onEnterWorkflow event.<br/> | |
* This method is dedicated to be overloaded by custom event handler. | |
* @param SWEvent the event parameter | |
*/ | |
public function leaveWorkflow($event) | |
{ | |
} | |
/** | |
* This event is raised after the record instance is removed from a workflow. | |
* This occures when the owner status attribut is set to NULL, for instance by calling | |
* $c->swNextStatus() | |
* | |
* @param SWEvent the event parameter | |
*/ | |
public function onLeaveWorkflow($event) | |
{ | |
$this->_raiseEvent('onLeaveWorkflow',$event); | |
} | |
/** | |
* Default implementation for the onBeforeTransition event.<br/> | |
* This method is dedicated to be overloaded by custom event handler. | |
* @param SWEvent the event parameter | |
*/ | |
public function beforeTransition($event) | |
{ | |
} | |
/** | |
* This event is raised before a workflow transition is applied to the owner instance. | |
* | |
* @param SWEvent the event parameter | |
*/ | |
public function onBeforeTransition($event) | |
{ | |
$this->_raiseEvent('onBeforeTransition',$event); | |
} | |
/** | |
* Default implementation for the onProcessTransition event.<br/> | |
* This method is dedicated to be overloaded by custom event handler. | |
* @param SWEvent the event parameter | |
*/ | |
public function processTransition($event) | |
{ | |
} | |
/** | |
* This event is raised when a workflow transition is in progress. In such case, the user may | |
* define a handler for this event in order to run specific process.<br/> | |
* Depending on the <b>'transitionBeforeSave'</b> initialization parameters, this event could be | |
* fired before or after the owner model is actually saved to the database. Of course this only | |
* applies when status change is initiated when saving the record. A call to swNextStatus() | |
* is not affected by the 'transitionBeforeSave' option. | |
* | |
* @param SWEvent the event parameter | |
*/ | |
public function onProcessTransition($event) | |
{ | |
if( $this->transitionBeforeSave || $this->_beforeSaveInProgress == false){ | |
$this->_raiseEvent('onProcessTransition',$event); | |
}else { | |
$this->_delayedEvent[]=array('name'=> 'onProcessTransition','objEvent'=>$event); | |
} | |
} | |
/** | |
* Default implementation for the onAfterTransition event.<br/> | |
* This method is dedicated to be overloaded by custom event handler. | |
* | |
* @param SWEvent the event parameter | |
*/ | |
public function afterTransition($event) | |
{ | |
} | |
/** | |
* This event is raised after the onProcessTransition is fired. It is the last event fired | |
* during a non-final transition.<br/> | |
* Again, in the case of an AR being saved, this event may be fired before or after the record | |
* is actually save, depending on the <b>'transitionBeforeSave'</b> initialization parameters. | |
* | |
* @param SWEvent the event parameter | |
*/ | |
public function onAfterTransition($event) | |
{ | |
if( $this->transitionBeforeSave || $this->_beforeSaveInProgress == false){ | |
$this->_raiseEvent('onAfterTransition',$event); | |
}else { | |
$this->_delayedEvent[]=array('name'=> 'onAfterTransition','objEvent'=>$event); | |
} | |
} | |
/** | |
* Default implementation for the onFinalStatus event.<br/> | |
* This method is dedicated to be overloaded by custom event handler. | |
* @param SWEvent the event parameter | |
*/ | |
public function finalStatus($event) | |
{ | |
} | |
/** | |
* This event is raised at the end of a transition, when the destination status is a | |
* final status (i.e the owner model has reached a status from where it will not be able | |
* to move). | |
* | |
* @param SWEvent the event parameter | |
*/ | |
public function onFinalStatus($event) | |
{ | |
if( $this->transitionBeforeSave || $this->_beforeSaveInProgress == false){ | |
$this->_raiseEvent('onFinalStatus',$event); | |
}else { | |
$this->_delayedEvent[]=array('name'=> 'onFinalStatus','objEvent'=>$event); | |
} | |
} | |
} |