diff --git a/SWActiveRecord.php b/SWActiveRecord.php
index bd27553..7a5922a 100644
--- a/SWActiveRecord.php
+++ b/SWActiveRecord.php
@@ -1,47 +1,23 @@
-
\ No newline at end of file
+
- *
statusAttribute (string) : This is the column name where status is stored
- * If this attribute doesn't exist for a model, the Workflow behavior is automatically disabled and a warning is
- * logged.
- * 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 workflowId/nodeId.
- * example :
- *
- * task/pending
- * postWorkflow/to_review
- *
- * Default : 'status'
- *
- * defaultWorkflow (string) : workflow name that should be used by default for the owner model
- * 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'.
- * Default : SWWorkflowSource->workflowNamePrefix . ModelName
- *
- * autoInsert (boolean) :
- * 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.
- * Default : true
- *
- * workflowSourceComponent (string) :
- * Name of the workflow source component to use with this behavior.
- * By ddefault this parameter is set to swSource (see {@link SWPhpWorkflowSource})
- *
- * enableEvent (boolean) :
- * 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.
- * Default : true
- *
- * transitionBeforeSave (boolean) :
- * If TRUE, SWEvents are fired and possible transitions tasks are executed before the owner model is
- * actually saved. If FALSE, events and task transitions are processed after save.
- * 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.
- * Default : true
- *
- *
- */
-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(null) 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.
- * During the initialisation, following actions are performed:
- *
- * - The status attribute exists
- * - Check whether or not, workflow events should be enabled, by testing if the owner component
- * class inherits from the 'SWComponent' or 'SWActiveRecord' class.
- *
- *
- * @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 :
- *
- * - behavior initialization parameter defaultWorkflow
- * - owner component method workflow : if the owner component is able to provide the
- * complete workflow, this method will invoke SWWorkflowSource.addWorkflow
- * - 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)
- *
- * @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
- *
- *
- * - a transition has been defined in the workflow between those 2 status
- * - the destination status has a constraint that is evaluated to true in the context of the
- * owner model
- *
- * 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 :
- *
- * - it is not empty
- * - it contains a valid status name
- * - this status can be reached from the current status
- * - or it is equal to the current status (no status change)
- *
- * @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 workflowId/nodeId.
- * 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. workfowId/nodeId)
- * @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.
- *
- * - onBeforeSave : perform status validation and update if needed. If configured, a task is also executed
- * - onAfterSave : if configured a task is executed
- * - onAfterFind : initialize internal status value
- *
- * Additionnally, the behavior will fire custom events on various steps of the owner model life-cycle within its workflow :
- *
- - onEnterWorkflow : the owner model is inserted in a workflow. Its status is now the initial status of the workflow
- - onFinalStatus : the owner model is in a status with no out going edge.
- - onLeaveWorkflow : the owner model status is set to NULL. This is possible only if the model is in a final status
- - onBeforeTransition : the owner model is about to change status
- - onProcessTransition : the owner model is changing status
- - onAfterTransition : the owner model has changed status
-
- * @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.
- * 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.
- * 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.
- * 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.
- * 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.
- * Depending on the 'transitionBeforeSave' 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.
- * 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.
- * 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 'transitionBeforeSave' 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.
- * 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);
- }
- }
-}
-?>
\ No newline at end of file
+
+ * statusAttribute (string) : This is the column name where status is stored
+ * If this attribute doesn't exist for a model, the Workflow behavior is automatically disabled and a warning is
+ * logged.
+ * 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 workflowId/nodeId.
+ * example :
+ *
+ * task/pending
+ * postWorkflow/to_review
+ *
+ * Default : 'status'
+ *
+ * defaultWorkflow (string) : workflow name that should be used by default for the owner model
+ * 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'.
+ * Default : SWWorkflowSource->workflowNamePrefix . ModelName
+ *
+ * autoInsert (boolean) :
+ * If TRUE, the model is automatically inserted in the workflow (if not already done) when it is saved.
+ * If FALSE, it is developer responsibility to insert the model in the workflow.
+ * Default : true
+ *
+ * workflowSourceComponent (string) :
+ * Name of the workflow source component to use with this behavior.
+ * By default this parameter is set to 'swSource'(see {@link SWPhpWorkflowSource})
+ *
+ * enableEvent (boolean) :
+ * 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.
+ * Default : true
+ *
+ * transitionBeforeSave (boolean) :
+ * If TRUE, SWEvents are fired and possible transitions tasks are executed before the owner model is
+ * actually saved. If FALSE, events and task transitions are processed after save.
+ * 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.
+ * Default : true
+ *
+ *
+ */
+class SWActiveRecordBehavior extends CBehavior
+{
+ const SW_LOG_CATEGORY = 'application.simpleWorkflow';
+ const SW_I8N_CATEGORY = 'simpleworkflow';
+
+ /**
+ * @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;
+
+ /**
+ * @var string name of the class the owner should inherit from in order for SW events
+ * to be enabled.
+ */
+ protected $eventClassName = 'SWActiveRecord';
+
+ /**
+ * delayed transition (only when change status occurs during save)
+ */
+ private $_delayedTransition = null;
+
+ /**
+ * delayed event stack (only when change status occurs during save)
+ */
+ private $_delayedEvent = array();
+
+ /**
+ * prevent delayed event fire when status is changed by a call to swNextStatus
+ */
+ private $_beforeSaveInProgress = false;
+
+ /**
+ * internal status for the owner model
+ */
+ private $_status = null;
+
+ /**
+ * workflow source component reference
+ */
+ private $_wfs;
+
+ /**
+ * prevent re-entrance
+ */
+ private $_locked = false;
+
+ private $_final = null;
+
+ /**
+ * @return SWWorkflowSource 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(null) 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;
+
+ 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 re-entrance
+ *
+ * @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
+ * @throws SWException
+ */
+ 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
+ * @throws SWException
+ */
+ 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.
+ *
+ * @return mixed
+ */
+ 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.
+ * During the initialisation, following actions are performed:
+ *
+ * - The status attribute exists
+ * - Check whether or not, workflow events should be enabled, by testing if the owner component
+ * class inherits from the 'SWComponent' or 'SWActiveRecord' class.
+ *
+ *
+ * @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);
+ }
+
+ /**
+ * force disable event
+ */
+ $this->enableEvent = false;
+ }
+
+ 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);
+ }
+
+ /**
+ * pre-load 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
+ */
+ 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 :
+ *
+ * - behavior initialization parameter defaultWorkflow
+ * - owner component method workflow : if the owner component is able to provide the
+ * complete workflow, this method will invoke SWWorkflowSource.addWorkflow
+ * - 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)
+ *
+ *
+ * @return string workflow id to use with the owner component or NULL if no workflow was found
+ * @throws SWException
+ */
+ 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)) {
+ /**
+ * 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 {
+ /**
+ * 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()->toString(), SWException::SW_ERR_IN_WORKFLOW);
+
+ $wfName = ($workflowId === null
+ ? $this->swGetDefaultWorkflowId()
+ : $workflowId
+ );
+
+ if ($wfName === null)
+ throw new SWException('Failed to get the name of workflow', 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. If the
+ * owner component is not inserted in a workflow, an empty array is returned.
+ *
+ * @return array list of SWNode objects.
+ */
+ public function swGetAllStatus()
+ {
+ if (!$this->swHasStatus() || $this->swGetWorkflowId() === null)
+ return array();
+
+ return $this->swGetWorkflowSource()->getAllNodes($this->swGetWorkflowId());
+ }
+
+ /**
+ * Checks if the status passed as argument can be reached from the current status. This occurs when
+ *
+ *
+ * - a transition has been defined in the workflow between those 2 status
+ * - the destination status has a constraint that is evaluated to true in the context of the
+ * owner model
+ *
+ * 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;
+
+ /**
+ * create SWNode
+ */
+ $nxtNode = $this->swGetWorkflowSource()->createSWNode($nextStatus, $this->swGetDefaultWorkflowId());
+
+ /**
+ * the transition NULL -> $nextStatus is valid only if $nextStatus is an initial status
+ */
+ if ((!$this->swHasStatus() && $this->swIsInitialStatus($nextStatus)) ||
+ ($this->swHasStatus() && $this->swGetWorkflowSource()->isNextNode($this->_status, $nxtNode))
+ ) {
+ /**
+ * 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 (( empty($constraint) || $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 mixed $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)
+ return $this->_final;
+
+ $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) {
+ $workflowId = ($this->swHasStatus()
+ ? $this->swGetWorkflowId()
+ : $this->swGetDefaultWorkflowId()
+ );
+
+ /**
+ * create the node to compare with initial node
+ */
+ $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 :
+ *
+ * - it is not empty
+ * - it contains a valid status name
+ * - this status can be reached from the current status
+ * - or it is equal to the current status (no status change)
+ *
+ * @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 {
+ $swNode = (($value instanceof SWNode) ? $value : $this->swGetWorkflowSource()->createSWNode($value, $this->swGetDefaultWorkflowId()));
+
+ if ($this->swIsNextStatus($value) === false && $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 method {@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 workflowId/nodeId.
+ * 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 full name of the target node (e.g. workfowId/nodeId)
+ *
+ * @param mixed $params
+ * @return bool TRUE if the transition could be performed, FALSE otherwise
+ * @throws CException
+ * @throws Exception
+ * @throws SWException
+ */
+ 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. E.g.: $array['status']
+ */
+ $nextStatus = $nextStatus[$this->statusAttribute];
+ } elseif ($nextStatus instanceof SWNode) {
+ $nextStatus = $nextStatus->toString();
+ }
+
+ try {
+ $this->_lock();
+
+ if ($this->swHasStatus() === false && $nextStatus !== null) {
+ $nextNode = $this->swGetWorkflowSource()->getNodeDefinition($nextStatus, $this->swGetDefaultWorkflowId());
+
+ /**
+ * insertion into workflow
+ */
+ 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) {
+ $nextNode = $this->swGetWorkflowSource()->getNodeDefinition($nextStatus, $this->swGetWorkflowId());
+
+ /**
+ * perform transition
+ */
+ 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 no transition between identical statuses, so no any operation should be performed.
+ */
+ }
+ }
+ } catch (CException $e) {
+ $this->_unlock();
+ Yii::log($e->getMessage(), CLogger::LEVEL_ERROR, self::SW_LOG_CATEGORY);
+ throw $e;
+ }
+
+ $this->_unlock();
+ return $bResult;
+ }
+
+ /**
+ * Attach event handlers.
+ * The behavior registers its own mandatory event handlers in case the owner model is a CActiveRecord instance.
+ *
+ * - onBeforeSave : perform status validation and update if needed. If configured, a task is also executed
+ * - onAfterSave : if configured a task is executed
+ * - onAfterFind : initialize internal status value
+ *
+ * Additionally, the behavior will fire custom events on various steps of the owner model life-cycle within its workflow :
+ *
+ * - onEnterWorkflow : the owner model is inserted in a workflow. Its status is now the initial status of the workflow
+ * - onFinalStatus : the owner model is in a status with no out going edge.
+ * - onLeaveWorkflow : the owner model status is set to NULL. This is possible only if the model is in a final status
+ * - onBeforeTransition : the owner model is about to change status
+ * - onProcessTransition : the owner model is changing status
+ * - onAfterTransition : the owner model has changed status
+ *
+ * @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'));
+
+ /**
+ * custom events
+ */
+ $ev = array_merge($ev, array(
+ '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 CEvent $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) {
+ /**
+ * insert into workflow
+ */
+ $initNode = $this->swGetWorkflowSource()->getInitialNode($this->swGetDefaultWorkflowId());
+ $this->swSetStatus($initNode);
+ }
+ } else {
+ $this->swSetStatus($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;
+
+ $status = null;
+
+ 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),
+ * 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 $src
+ * @param SWNode $dst
+ */
+ private function _logEventFire($ev, $src, $dst)
+ {
+ Yii::log("Event fired : '{$ev}' status [" . ($src == null ? "null" : $src->toString()) . "] -> [" . $dst->toString() . "]'", 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.
+ * This method is dedicated to be overloaded by custom event handler.
+ * @param SWEvent $event 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 $event the event parameter
+ */
+ public function onEnterWorkflow($event)
+ {
+ $this->_raiseEvent('onEnterWorkflow', $event);
+ }
+
+ /**
+ * Default implementation for the onEnterWorkflow event.
+ * This method is dedicated to be overloaded by custom event handler.
+ * @param SWEvent $event 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 $event the event parameter
+ */
+ public function onLeaveWorkflow($event)
+ {
+ $this->_raiseEvent('onLeaveWorkflow', $event);
+ }
+
+ /**
+ * Default implementation for the onBeforeTransition event.
+ * This method is dedicated to be overloaded by custom event handler.
+ * @param SWEvent $event the event parameter
+ */
+ public function beforeTransition($event)
+ {
+ }
+
+ /**
+ * This event is raised before a workflow transition is applied to the owner instance.
+ *
+ * @param SWEvent $event the event parameter
+ */
+ public function onBeforeTransition($event)
+ {
+ $this->_raiseEvent('onBeforeTransition', $event);
+ }
+
+ /**
+ * Default implementation for the onProcessTransition event.
+ * This method is dedicated to be overloaded by custom event handler.
+ * @param SWEvent $event 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.
+ * Depending on the 'transitionBeforeSave' 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 $event 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.
+ * This method is dedicated to be overloaded by custom event handler.
+ *
+ * @param SWEvent $event 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.
+ * 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 'transitionBeforeSave' initialization parameters.
+ *
+ * @param SWEvent $event 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.
+ * This method is dedicated to be overloaded by custom event handler.
+ * @param SWEvent $event 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 $event 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/SWComponent.php b/SWComponent.php
index c878f13..15b1366 100644
--- a/SWComponent.php
+++ b/SWComponent.php
@@ -1,47 +1,23 @@
-
\ No newline at end of file
+source=$source;
- $this->destination=$destination;
- }
-}
-?>
+source = $source;
+ $this->destination = $destination;
+ }
+}
diff --git a/SWException.php b/SWException.php
index 197c40f..cc636fe 100644
--- a/SWException.php
+++ b/SWException.php
@@ -1,21 +1,20 @@
-
\ No newline at end of file
+
- * array(
- * 'statusId' => 'status label',
- * 'status Id2' => 'status label 2',
- * etc ...
- * )
- *
- * Use the $options argument to speficy following options :
- *
- * - prompt : specifies the prompt text shown as the first list option. Its value is empty.
- * Note, the prompt text will NOT be HTML-encoded
- * - includeCurrent : boolean, if TRUE (default) the current model status is included in the list,
- * otherwise current model status is not inserted in the returned array.
- * - exclude : array, list of statuses that should not be inserted in the returned array
- *
- * Note that each status label is html encode by default.
- * @param CModel $model the data model attaching a simpleWorkflow behavior
- * @param array $options additional options
- * @return array the list data that can be used in dropDownList and listBox
- */
- public static function nextStatuslistData($model, $options=array())
- {
- return SWHelper::_createListData($model,$model->swGetNextStatus(),$options);
- }
- /**
- * Returns the list of all statuses belonging to the workflow the model passed as argument
- * is in.
- * see {@link SWHelper::nextStatuslistData} for argument options
- *
- * @param CModel the data model attaching a simpleWorkflow behavior
- * @param array additional options
- * @return array the list data that can be used in dropDownList and listBox
- */
- public static function allStatuslistData($model,$options=array())
- {
- return SWHelper::_createListData($model,$model->swGetAllStatus(),$options);
- }
- /**
- * Create an array containing where keys are statusIds in the form workflowId/statusId
- * and the value is the status label.
- * Note that by default this method never inserts the status of the model passed as argument.
- * see {@link SWHelper::nextStatuslistData} for argument options
- *
- * @param CModel the data model attaching a simpleWorkflow behavior
- * @param array $statusList array of string where each value is the statusId
- * @param array $options the list data that can be used in dropDownList and listBox
- */
- public static function statusListData($model,$statusList,$options=array())
- {
- $nodeList = array();
- $w = $model->swGetWorkflowSource();
- foreach($statusList as $key => $statusId){
- $nodeList[] = $w->getNodeDefinition($statusId);
- }
- $options['includeCurrent'] = (isset($options['includeCurrent'])
- ? $options['includeCurrent']
- : false
- );
- return SWHelper::_createListData($model,$nodeList,$options);
- }
- /**
- * Returns an array where keys are status id and values are status labels.
- *
- * @param array $statusList SWNode list
- * @param array $options (optional)
- * @throws CException
- */
- private static function _createListData($model,$statusList,$options=array())
- {
- $result=array();
- $exclude=null;
- $includeCurrent = true;
-
- $currentStatus = ($model->swHasStatus()
- ? $model->swGetStatus()
- : null
- );
- if($currentStatus != null)
- $result[$currentStatus->toString()]=$currentStatus->getLabel();
-
- $encodeLabel = ( isset($options['encode'])
- ? (bool) $options['encode']
- : true
- );
-
- // process options
-
- if(count($options)!=0){
-
- if(isset($options['prompt'])){
- $result[''] = $options['prompt'];
- }
-
- if(isset($options['exclude']))
- {
- if(is_string($options['exclude']))
- $exclude = array_map('trim',explode(",",$options['exclude']));
- elseif(is_array($options['exclude']))
- $exclude = $options['exclude'];
- else
- throw new CException('incorrect type for option "exclude" : array or string expected');
-
- foreach ($exclude as $key => $value) {
- $node = new SWNode($value, $model->swGetWorkflowId());
- $exclude[$key] = $node->toString();
- }
- }
- if(isset($options['includeCurrent']) )
- $includeCurrent = (bool) $options['includeCurrent'];
-
- if($exclude != null && $currentStatus!= null && in_array($currentStatus->toString(), $exclude))
- $includeCurrent = false;
- }
-
- if(count($statusList)!=0){
- foreach ( $statusList as $nodeObj ) {
-
- if( $exclude == null ||
- ( $exclude != null && !in_array($nodeObj->toString(), $exclude )) )
- {
- $result[$nodeObj->toString()]= ($encodeLabel
- ? CHtml::encode($nodeObj->getLabel())
- : $nodeObj->getLabel()
- );
- }
- }
- }
-
- if($includeCurrent == false && $currentStatus !=null){
- unset($result[$currentStatus->toString()]);
- }
- return $result;
- }
-}
-?>
\ No newline at end of file
+
+ * array(
+ * 'statusId' => 'status label',
+ * 'status Id2' => 'status label 2',
+ * etc ...
+ * )
+ *
+ * Use the $options argument to specify following options :
+ *
+ * - prompt : specifies the prompt text shown as the first list option. Its value is empty.
+ * Note, the prompt text will NOT be HTML-encoded
+ * - includeCurrent : boolean, if TRUE (default) the current model status is included in the list,
+ * otherwise current model status is not inserted in the returned array.
+ * - exclude : array, list of statuses that should not be inserted in the returned array
+ *
+ * Note that each status label is html encode by default.
+ *
+ * @param CModel $model the data model attaching a simpleWorkflow behavior
+ * @param array $options additional options
+ * @return array the list data that can be used in dropDownList and listBox
+ */
+ public static function nextStatuslistData($model, $options = array())
+ {
+ return SWHelper::_createListData($model, $model->swGetNextStatus(), $options);
+ }
+
+ /**
+ * Returns the list of all statuses belonging to the workflow the model passed as argument
+ * is in.
+ * see {@link SWHelper::nextStatuslistData} for argument options
+ *
+ * @param CModel $model the data model attaching a simpleWorkflow behavior
+ * @param array $options additional options
+ * @return array the list data that can be used in dropDownList and listBox
+ */
+ public static function allStatuslistData($model, $options = array())
+ {
+ return SWHelper::_createListData($model, $model->swGetAllStatus(), $options);
+ }
+
+ /**
+ * Create an array containing where keys are statusIds in the form workflowId/statusId
+ * and the value is the status label.
+ * Note that by default this method never inserts the status of the model passed as argument.
+ * see {@link SWHelper::nextStatuslistData} for argument options
+ *
+ * @param CModel $model the data model attaching a simpleWorkflow behavior
+ * @param array $statusList array of string where each value is the statusId
+ * @param array $options the list data that can be used in dropDownList and listBox
+ * @return array
+ */
+ public static function statusListData($model, $statusList, $options = array())
+ {
+ $nodeList = array();
+ $w = $model->swGetWorkflowSource();
+
+ foreach ($statusList as $statusId) {
+ $nodeList[] = $w->getNodeDefinition($statusId);
+ }
+
+ $options['includeCurrent'] = (isset($options['includeCurrent'])
+ ? $options['includeCurrent']
+ : false
+ );
+
+ return SWHelper::_createListData($model, $nodeList, $options);
+ }
+
+ /**
+ * Returns an array where keys are status id and values are status labels.
+ *
+ * @param CModel $model
+ * @param array $statusList SWNode list
+ * @param array $options (optional)
+ * @return array
+ * @throws CException
+ */
+ private static function _createListData($model, $statusList, $options = array())
+ {
+ $result = array();
+ $exclude = null;
+ $includeCurrent = true;
+
+ $currentStatus = ($model->swHasStatus()
+ ? $model->swGetStatus()
+ : null
+ );
+
+ if ($currentStatus !== null)
+ $result[$currentStatus->toString()] = $currentStatus->getLabel();
+
+ $encodeLabel = (isset($options['encode'])
+ ? (bool)$options['encode']
+ : true
+ );
+
+ /**
+ * process options
+ */
+ if (count($options)) {
+ if (isset($options['prompt'])) {
+ $result[''] = $options['prompt'];
+ }
+
+ if (isset($options['exclude'])) {
+ if (is_string($options['exclude']))
+ $exclude = array_map('trim', explode(",", $options['exclude']));
+ elseif (is_array($options['exclude']))
+ $exclude = $options['exclude'];
+ else
+ throw new CException('incorrect type for option "exclude" : array or string expected');
+
+ foreach ($exclude as $key => $value) {
+ $node = new SWNode($value, $model->swGetWorkflowId());
+ $exclude[$key] = $node->toString();
+ }
+ }
+
+ if (isset($options['includeCurrent']))
+ $includeCurrent = (bool)$options['includeCurrent'];
+
+ if ($exclude !== null && $currentStatus !== null && in_array($currentStatus->toString(), $exclude))
+ $includeCurrent = false;
+ }
+
+ if (count($statusList)) {
+ foreach ($statusList as $nodeObj) {
+
+ if ($exclude === null || ($exclude !== null && !in_array($nodeObj->toString(), $exclude))) {
+ $result[$nodeObj->toString()] = ($encodeLabel
+ ? CHtml::encode($nodeObj->getLabel())
+ : $nodeObj->getLabel()
+ );
+ }
+ }
+ }
+
+ if ($includeCurrent === false && $currentStatus !== null) {
+ unset($result[$currentStatus->toString()]);
+ }
+
+ return $result;
+ }
+}
\ No newline at end of file
diff --git a/SWNode.php b/SWNode.php
index 33dec3a..8c56724 100644
--- a/SWNode.php
+++ b/SWNode.php
@@ -1,269 +1,295 @@
-
- * Note that both workflow and node id must begin with a alphabetic character followed by aplha-numeric
- * characters : all other characters are not accepted and cause an exception to be thrown (see {@link SWNode::parseNodeId()})
- *
- * @param mixed $node If a string is passed as argument, it can be both in format workflowId/NodeId
- * or simply 'nodeId'. In this last case, argument $defaultWorkflowIs must be provided, otherwise it is
- * ignored.
- * The $node argument may also be provided as an associative array, with the following structure :
- *
- * {
- * 'id' => string, // mandatory
- * 'label' => string , // optional
- * 'constraint' => string, // optional
- * 'transition' => array, // optional
- * 'metadata' => array, // optional
- * }
- *
- * Again, the 'id' value may contain a workflow id (e.g 'workflowId/nodeId') but if it's not the case then
- * the second argument $defaultWorkflowId must be provided.
- * @param string defaultWorkflowId workflow Id that is used each time a workflow is needed to complete
- * a status name.
- */
- public function __construct($node, $defaultWorkflowId=null)
- {
- if($node==null || empty($node))
- throw new SWException('illegal argument exception : $node cannot be empty', SWException::SW_ERR_CREATE_NODE);
-
- $st=array();
-
- if( $node instanceof SWNode )
- {
- // copy constructor : does not copy transitions, constraints and metadata
-
- $this->_workflowId = $node->getWorkflowId();
- $this->_id = $node->getId();
- $this->_label = $node->getLabel();
- $this->_metadata = $node->getMetadata();
- }
- else {
- if( is_array($node))
- {
- if(!isset($node['id']))
- throw new SWException('missing node id',SWException::SW_ERR_MISSING_NODE_ID);
-
- // set node id -----------------------
-
- $st=$this->parseNodeId($node['id'],$defaultWorkflowId);
-
- if(isset($node['label'])){
- $this->_label=$node['label'];
- }
-
- if(isset($node['constraint'])){
- $this->_constraint=$node['constraint'];
- }
-
- if(isset($node['transition'])){
- $this->_loadTransition($node['transition'],$st['workflow']);
- }
-
- if(isset($node['metadata'])){
- $this->_metadata = $node['metadata'];
- }
- }
- elseif(is_string($node))
- {
- $st=$this->parseNodeId($node,$defaultWorkflowId);
- }
-
- $this->_workflowId = $st['workflow'];
- $this->_id = $st['node'];
-
- if(!isset($this->_label))
- $this->_label=$this->_id;
- }
- }
- /**
- * Parse a status name and return it as an array. The string passed as argument
- * may be a complete status name (e.g workflowId/nodeId) and if no workflowId is
- * specified, then an exception is thrown. Both workflow and node ids must match
- * following pattern:
- *
- * /^[[:alpha:]][[:alnum:]_]*$/
- *
- * For instance :
- *
- * - ready : matches
- * - to_process : matches
- * - priority_1 : matches
- * - 2_level : does not match
- * - to-production : does not match
- * - enable/disable : does not match
- *
- * @param string status status name (wfId/nodeId or nodeId)
- * @return array the complete status (e.g array ( [workflow] => 'a' [node] => 'b' ))
- */
- public function parseNodeId($status,$workflowId)
- {
- $nodeId=$wfId=null;
-
- if(strstr($status,'/')){
- if(preg_match('/^([[:alpha:]][[:alnum:]_]*)\/([[:alpha:]][[:alnum:]_]*)$/',$status,$matches) == 1){
- $wfId = $matches[1];
- $nodeId = $matches[2];
- }
- }
- else{
- if(preg_match('/^[[:alpha:]][[:alnum:]_]*$/',$status) == 1){
- $nodeId = $status;
- if(preg_match('/^[[:alpha:]][[:alnum:]_]*$/',$workflowId) == 1){
- $wfId = $workflowId;
- }
- }
- }
-
- if( $wfId == null || $nodeId == null){
- throw new SWException('failed to create node from node Id = '.$status.', workflow Id = '.$workflowId, SWException::SW_ERR_CREATE_NODE);
- }
- return array('workflow'=>$wfId,'node'=>$nodeId);
- }
- /**
- * Overrides the default magic method defined at the CComponent level in order to
- * return a metadata value if parent method fails.
- *
- * @see CComponent::__get()
- */
- public function __get($name)
- {
- try{
- return parent::__get($name);
- }catch(CException $e){
-
- if(isset($this->_metadata[$name])){
- return $this->_metadata[$name];
- }else{
- throw new SWException('Property "'.$name.'" is not found.',SWException::SW_ERR_ATTR_NOT_FOUND);
- }
- }
- }
- /**
- * Loads the set of transitions passed as argument.
- *
- * @param mixed $tr if provided as a string, it is a comma separated list of SWNodes id,
- * This list can also be provided as an array
- * @param string $defWfId Default workflow Id if nodes have no workflow id, this value is used
- * as their workflow id.
- */
- private function _loadTransition($tr, $defWfId)
- {
- if( is_string($tr))
- {
- $trAr=explode(',',$tr);
- foreach($trAr as $aTr)
- {
- $objNode=new SWNode(trim($aTr),$defWfId);
- $this->_tr[$objNode->toString()]=null;
- }
- }
- elseif( is_array($tr))
- {
- foreach($tr as $key => $value){
- if( is_string($key)){
- $objNode=new SWNode(trim($key),$defWfId);
- if($value!=null)
- $this->_tr[$objNode->toString()]=$value;
- else
- $this->_tr[$objNode->toString()]=null;
- }else {
- $objNode=new SWNode(trim($value),$defWfId);
- $this->_tr[$objNode->toString()]=null;
- }
- }
- }else {
- throw new SWException(__FUNCTION__. 'incorrect arg type : string or array expected');
- }
- }
-
- //////////////////////////////////////////////////////////////////////////////////////////
- // accessors
-
- public function getWorkflowId() {return $this->_workflowId;}
- public function getId() {return $this->_id;}
- public function getLabel() {return $this->_label;}
- public function getNext() {return $this->_tr;}
- public function getConstraint() {return $this->_constraint;}
- public function getMetadata() {return $this->_metadata;}
- public function getNextNodeIds() {return array_keys($this->_tr);}
- /**
- * @returns String the task for this transition or NULL if no task is defined
- * @param mixed $endNode SWNode instance or string that will be converted to SWNode instance (e.g 'workflowId/nodeId')
- * @throws SWException
- */
- public function getTransitionTask($endNode){
-
- if( ! $endNode instanceof SWNode ){
- $endNode = new SWNode($endNode, $this->getWorkflowId());
- }
- $endNodeId = $endNode->toString();
-
- return ( isset($this->_tr[$endNodeId])
- ? $this->_tr[$endNodeId]
- : null
- );
- }
-
- public function __toString(){
- return $this->getWorkflowId().'/'.$this->getId();
- }
- public function toString(){
- return $this->__toString();
- }
- /**
- * SWnode comparator method. Note that only the node and the workflow id
- * members are compared.
- *
- * @param mixed SWNode object or string. If a string is provided it is used to create
- * a new SWNode object.
- */
- public function equals($status){
-
- if( $status instanceof SWNode )
- {
- return $status->toString() == $this->toString();
- }
- else try{
- $other=new SWNode($status,$this->getWorkflowId());
- return $other->equals($this);
- }catch(Exception $e)
- {
- throw new SWException('comparaison error - the value passed as argument (value='.$status.') cannot be converted into a SWNode',$e->getCode());
- }
- }
-}
-?>
\ No newline at end of file
+
+ * Note that both workflow and node id must begin with a alphabetic character followed by aplha-numeric
+ * characters : all other characters are not accepted and cause an exception to be thrown (see {@link SWNode::parseNodeId()})
+ *
+ * @param mixed $node If a string is passed as argument, it can be both in format workflowId/NodeId
+ * or simply 'nodeId'. In this last case, argument $defaultWorkflowIs must be provided, otherwise it is
+ * ignored.
+ * The $node argument may also be provided as an associative array, with the following structure :
+ *
+ * {
+ * 'id' => string, // mandatory
+ * 'label' => string , // optional
+ * 'constraint' => string, // optional
+ * 'transition' => array, // optional
+ * 'metadata' => array, // optional
+ * }
+ *
+ * Again, the 'id' value may contain a workflow id (e.g 'workflowId/nodeId') but if it's not the case then
+ * the second argument $defaultWorkflowId must be provided.
+ *
+ * @param string $defaultWorkflowId workflow Id that is used each time a workflow is needed to complete a status name.
+ * @throws SWException
+ */
+ public function __construct($node, $defaultWorkflowId = null)
+ {
+
+ if ($node === null || empty($node))
+ throw new SWException('Illegal argument exception : $node cannot be empty', SWException::SW_ERR_CREATE_NODE);
+
+ $st = array();
+
+ if ($node instanceof SWNode) {
+ /**
+ * copy constructor: does not copy transitions, constraints and metadata
+ */
+ $this->_workflowId = $node->getWorkflowId();
+ $this->_id = $node->getId();
+ $this->_label = $node->getLabel();
+ $this->_metadata = $node->getMetadata();
+ } else {
+ if (is_array($node)) {
+ if (!isset($node['id']))
+ throw new SWException('Missing node id', SWException::SW_ERR_MISSING_NODE_ID);
+
+ $st = $this->parseNodeId($node['id'], $defaultWorkflowId);
+
+ if (isset($node['label'])) {
+ $this->_label = $node['label'];
+ }
+
+ if (isset($node['constraint'])) {
+ $this->_constraint = $node['constraint'];
+ }
+
+ if (isset($node['transition'])) {
+ $this->_loadTransition($node['transition'], $st['workflow']);
+ }
+
+ if (isset($node['metadata'])) {
+ $this->_metadata = $node['metadata'];
+ }
+ } elseif (is_string($node)) {
+ $st = $this->parseNodeId($node, $defaultWorkflowId);
+ }
+
+ $this->_workflowId = $st['workflow'];
+ $this->_id = $st['node'];
+
+ if (!(isset($this->_label)))
+ $this->_label = $this->_id;
+ }
+ }
+
+ /**
+ * Parse a status name and return it as an array. The string passed as argument
+ * may be a complete status name (e.g workflowId/nodeId) and if no workflowId is
+ * specified, then an exception is thrown. Both workflow and node ids must match
+ * following pattern:
+ *
+ * /^[[:alpha:]][[:alnum:]_]*$/
+ *
+ * For instance :
+ *
+ * - ready : matches
+ * - to_process : matches
+ * - priority_1 : matches
+ * - 2_level : does not match
+ * - to-production : does not match
+ * - enable/disable : does not match
+ *
+ *
+ * @param string $status status name (wfId/nodeId or nodeId)
+ * @param string $workflowId
+ * @return array the complete status (e.g array ( [workflow] => 'a' [node] => 'b' ))
+ * @throws SWException
+ */
+ public function parseNodeId($status, $workflowId)
+ {
+ $nodeId = $wfId = null;
+
+ if (strstr($status, '/')) {
+ if (preg_match('/^([[:alpha:]][[:alnum:]_]*)\/([[:alpha:]][[:alnum:]_]*)$/', $status, $matches) === 1) {
+ $wfId = $matches[1];
+ $nodeId = $matches[2];
+ }
+ } else {
+ if (preg_match('/^[[:alpha:]][[:alnum:]_]*$/', $status) === 1) {
+ $nodeId = $status;
+ if (preg_match('/^[[:alpha:]][[:alnum:]_]*$/', $workflowId) === 1) {
+ $wfId = $workflowId;
+ }
+ }
+ }
+
+ if ($wfId === null || $nodeId === null)
+ throw new SWException("Failed to create node from node Id = {$status}, workflow Id = {$workflowId}", SWException::SW_ERR_CREATE_NODE);
+
+ return array('workflow' => $wfId, 'node' => $nodeId);
+ }
+
+ /**
+ * Overrides the default magic method defined at the CComponent level in order to
+ * return a metadata value if parent method fails.
+ *
+ * @see CComponent::__get()
+ */
+ public function __get($name)
+ {
+ try {
+ return parent::__get($name);
+ } catch (CException $e) {
+
+ if (isset($this->_metadata[$name])) {
+ return $this->_metadata[$name];
+ } else
+ throw new SWException(Yii::t('yii', 'Property "{property}" is not found.', array('{property}' => $name)), SWException::SW_ERR_ATTR_NOT_FOUND);
+ }
+ }
+
+ /**
+ * Loads the set of transitions passed as argument.
+ *
+ * @param mixed $tr if provided as a string, it is a comma separated list of SWNodes id. This list can also be provided as an array.
+ * @param string $defWfId Default workflow Id if nodes have no workflow id, this value is used as their workflow id.
+ * @throws SWException
+ */
+ private function _loadTransition($tr, $defWfId)
+ {
+ if (is_string($tr)) {
+ $trAr = explode(',', $tr);
+ foreach ($trAr as $aTr) {
+ $objNode = new SWNode(trim($aTr), $defWfId);
+ $this->_tr[$objNode->toString()] = null;
+ }
+ } elseif (is_array($tr)) {
+ foreach ($tr as $key => $value) {
+ if (is_string($key)) {
+ $objNode = new SWNode(trim($key), $defWfId);
+ if ($value != null)
+ $this->_tr[$objNode->toString()] = $value;
+ else
+ $this->_tr[$objNode->toString()] = null;
+ } else {
+ $objNode = new SWNode(trim($value), $defWfId);
+ $this->_tr[$objNode->toString()] = null;
+ }
+ }
+ } else {
+ throw new SWException(__FUNCTION__ . ' incorrect arg type : string or array expected');
+ }
+ }
+
+ /**
+ * @returns String the task for this transition or NULL if no task is defined
+ * @param mixed $endNode SWNode instance or string that will be converted to SWNode instance (e.g 'workflowId/nodeId')
+ * @throws SWException
+ */
+ public function getTransitionTask($endNode)
+ {
+
+ if (!($endNode instanceof SWNode))
+ $endNode = new SWNode($endNode, $this->getWorkflowId());
+
+ $endNodeId = $endNode->toString();
+
+ return (isset($this->_tr[$endNodeId])
+ ? $this->_tr[$endNodeId]
+ : null
+ );
+ }
+
+ /**
+ * SWNode comparator method. Note that only the node and the workflow id
+ * members are compared.
+ *
+ * @param mixed $status SWNode object or string. If a string is provided it is used to create a new SWNode object.
+ * @return bool
+ * @throws SWException
+ */
+ public function equals($status)
+ {
+ if ($status instanceof SWNode) {
+ return $status->toString() == $this->toString();
+ } else try {
+ $other = new SWNode($status, $this->getWorkflowId());
+ return $other->equals($this);
+ } catch (Exception $e) {
+ throw new SWException('Comparison error - the value passed as argument (value=' . $status . ') cannot be converted into a SWNode', $e->getCode());
+ }
+ }
+
+ /**
+ * common getters
+ */
+ public function getWorkflowId()
+ {
+ return $this->_workflowId;
+ }
+
+ public function getId()
+ {
+ return $this->_id;
+ }
+
+ public function getLabel()
+ {
+ return $this->_label;
+ }
+
+ public function getNext()
+ {
+ return $this->_tr;
+ }
+
+ public function getConstraint()
+ {
+ return $this->_constraint;
+ }
+
+ public function getMetadata()
+ {
+ return $this->_metadata;
+ }
+
+ public function getNextNodeIds()
+ {
+ return array_keys($this->_tr);
+ }
+
+ public function __toString()
+ {
+ return $this->getWorkflowId() . '/' . $this->getId();
+ }
+
+ public function toString()
+ {
+ return $this->__toString();
+ }
+}
\ No newline at end of file
diff --git a/SWPhpWorkflowSource.php b/SWPhpWorkflowSource.php
index f89cad2..21a4700 100644
--- a/SWPhpWorkflowSource.php
+++ b/SWPhpWorkflowSource.php
@@ -1,268 +1,263 @@
-
- * basePath (string) : the base path alias where all workflow are stored.By default, it is set to
- * application.models.workflows (folder "protected/models/workflows").
- *
- * definitionType (string) : Defines the type of PHP file to load. A Workflow can be defined in
- * a PHP file that contains a simple array definition (definitionType = 'array'), or by a
- * class (definitionType = 'class'). By default this attribute is set to 'array'.
- *
- *
- */
-class SWPhpWorkflowSource extends SWWorkflowSource
-{
- /**
- * @var string the base path alias where all workflow are stored.By default, it is set to
- * application.models.workflows (folder "protected/models/workflows").
- */
- public $basePath = 'application.models.workflows';
- /**
- * @var string Definition type for workflow. Allowed values are : class, array. Default is 'array'
- */
- public $definitionType = 'array';
-
- private $_workflow; // workflow definition collection
- private $_workflowBasePath;
- /**
- * Initialize the component with configured values. To preload workflows, set configuration
- * setting 'preload' to an array containing all workflows to preload. If no preload is set
- * workflows are loaded on demand.
- *
- * @see SWWorkflowSource
- */
- public function init()
- {
- parent::init();
- $this->_workflowBasePath = Yii::getPathOfAlias($this->basePath);
- if( is_array($this->preload) and count($this->preload)!=0){
- foreach ( $this->preload as $wfId ) {
- $this->_load($wfId,true);
- }
- }
- if( $this->definitionType == 'class'){
- Yii::import($this->basePath.'.*');
- }
- }
-
- //
- ///////////////////////////////////////////////////////////////////////////////////
- // private methods
-
- /**
- * Loads a workflow from a php source file into the $this->_workflow
- * associative array. A call to reset() will unload all workflows.
- */
- private function _load($wfId, $forceReload)
- {
- if( !is_string($wfId) or empty($wfId))
- {
- throw new SWException('failed to load workflow - invalid workflow Id : '.$wfId,SWException::SW_ERR_WORKFLOW_ID);
- }
-
- if( !isset($this->_workflow[$wfId]) or $forceReload==true)
- {
-
- if($this->definitionType == 'class')
- {
- $wo = new $wfId;
- $this->_workflow[$wfId] = $this->_createWorkflow($wo->getDefinition(),$wfId);
- }
- elseif( $this->definitionType == 'array')
- {
- $fname=$this->_workflowBasePath.DIRECTORY_SEPARATOR.$wfId.'.php';
- if( file_exists($fname)==false){
- throw new SWException('workflow definition file not found : '.$fname,SWException::SW_ERR_WORKFLOW_NOT_FOUND);
- }
-
- $this->_workflow[$wfId] = $this->_createWorkflow(require($fname),$wfId);
- }
- }
- return $this->_workflow[$wfId];
- }
- /**
- * @param array $wf workflow definition
- * @param string $wfId workflow Id
- */
- private function _createWorkflow($wf,$wfId)
- {
- if(!is_array($wf) || empty($wfId)){
- throw new SWException('invalid argument');
- }
- $wfDefinition=array();
-
- if( !isset($wf['initial'])) {
- throw new SWException('missing initial status for workflow : '.$wfId,SWException::SW_ERR_IN_WORKFLOW);
- }
-
- // load node list
- $nodeIds = array();
- foreach($wf['node'] as $rnode)
- {
- $node=new SWNode($rnode,$wfId);
-
- if(in_array($node->getId(),$nodeIds )){
- throw new SWException('duplicate node id : '.$node->getId(),SWException::SW_ERR_IN_WORKFLOW);
- }else{
- $nodeIds[] = $node->getId();
- }
-
- $wfDefinition[$node->getId()]=$node;
- if( $node->getId()==$wf['initial'] || $node->toString() == $wf['initial']){
- $wfDefinition['swInitialNode']= $node;
- }
- }
- // checks that initialnode is set
-
- if(!isset($wfDefinition['swInitialNode'])){
- throw new SWException('missing initial status for workflow : '.$wfId,SWException::SW_ERR_IN_WORKFLOW);
- }
-
- return $wfDefinition;
- }
- /**
- * Returns the SWNode object from the workflow collection.
- *
- * @param SWnode swNode node to search for in the node list
- * @return SWNode the SWNode object retrieved from the workflow collection, or NULL if this
- * node could not be found in the workflow collection
- */
- private function _getNode($swNode)
- {
- $wfId=$swNode->getWorkflowId();
- if($wfId==null)
- {
- throw new SWException('workflow not found : '.$wfId,SWException::SW_ERR_WORKFLOW_NOT_FOUND);
- }
-
- $this->_load($wfId,false);
- $nodeId=$swNode->getId();
- if(isset($this->_workflow[$wfId][$nodeId])){
- return $this->_workflow[$wfId][$nodeId];
- }else {
- return null;
- }
- }
-
- //
- ///////////////////////////////////////////////////////////////////////////////////
- // public methods
- /**
- * Verify if a workflow has been loaded.
- *
- * @param string $workflowId workflow id
- * @return boolean TRUE if the workflow whose id is $workflowId has already been loaded,
- * FALSE otherwise
- */
- public function isWorkflowLoaded($workflowId)
- {
- return isset($this->_workflow[$workflowId]);
- }
- /**
- * Loads the workflow whose id is passed as argument.
- * By default, if the workflow has already been loaded it is not reloaded unless
- * $forceReload is TRUE
- * @param string $workflowId the workflow id
- * @param boolean $forceReload TRUE to force workflow loading, FALSE otherwise
- */
- public function loadWorkflow($workflowId,$forceReload=false)
- {
- return $this->_load($workflowId,$forceReload) != null;
- }
- /**
- * This method is used to add a new workflow definition to the current workflow collection.
- * @param array $definition the workflow definition in its array form
- * @param string $id the workflow id
- */
- public function addWorkflow($definition, $id)
- {
- if(!is_array($definition))
- throw new SWException('array expected');
-
- if( ! isset($this->_workflow[$id])){
- $this->_workflow[$id] = $this->_createWorkflow($definition,$id);
- }
- }
- /**
- * (non-PHPdoc)
- * @see SWWorkflowSource::getNodeDefinition()
- */
- public function getNodeDefinition($node, $defaultWorkflowId=null)
- {
- return $this->_getNode(
- $this->createSWNode($node,$defaultWorkflowId)
- );
- }
- /**
- * (non-PHPdoc)
- * @see SWWorkflowSource::getNextNodes()
- */
- public function getNextNodes($sourceNode,$workflowId=null)
- {
- $result=array();
-
- // convert startStatus into SWNode
-
- $startNode=$this->getNodeDefinition(
- $this->createSWNode($sourceNode,$workflowId)
- );
-
- if($startNode==null){
- throw new SWException('node could not be found : '.$sourceNode,SWException::SW_ERR_NODE_NOT_FOUND);
- }else {
- foreach($startNode->getNext() as $nxtNodeId => $tr){
- $result[]=$this->_getNode(new SWNode($nxtNodeId,$workflowId));
- }
- }
- return $result;
- }
- /**
- * (non-PHPdoc)
- * @see SWWorkflowSource::isNextNode()
- */
- public function isNextNode($sourceNode,$targetNode,$workflowId=null)
- {
- $startNode=$this->createSWNode($sourceNode,$workflowId);
- $nextNode=$this->createSWNode(
- $targetNode,
- ( $workflowId!=null
- ? $workflowId
- : $startNode->getWorkflowId()
- )
- );
-
- $nxt=$this->getNextNodes($startNode);
- if( $nxt != null){
- return in_array($nextNode->toString(),$nxt);
- }else {
- return false;
- }
- }
- /**
- * (non-PHPdoc)
- * @see SWWorkflowSource::getInitialNode()
- */
- public function getInitialNode($workflowId)
- {
- $this->_load($workflowId,false);
- return $this->_workflow[$workflowId]['swInitialNode'];
- }
- /**
- * (non-PHPdoc)
- * @see SWWorkflowSource::getAllNodes()
- */
- public function getAllNodes($workflowId)
- {
- $result=array();
- $wf=$this->_load($workflowId,false);
- foreach($wf as $key => $value){
- if($key!='swInitialNode'){
- $result[]=$value;
- }
- }
- return $result;
- }
-}
-?>
+
+ * basePath (string) : the base path alias where all workflow are stored.By default, it is set to
+ * application.models.workflows (folder "protected/models/workflows").
+ *
+ * definitionType (string) : Defines the type of PHP file to load. A Workflow can be defined in
+ * a PHP file that contains a simple array definition (definitionType = 'array'), or by a
+ * class (definitionType = 'class'). By default this attribute is set to 'array'.
+ *
+ *
+ */
+class SWPhpWorkflowSource extends SWWorkflowSource
+{
+ /**
+ * @var string the base path alias where all workflow are stored.By default, it is set to
+ * application.models.workflows (folder "protected/models/workflows").
+ */
+ public $basePath = 'application.models.workflows';
+ /**
+ * @var string Definition type for workflow. Allowed values are : class, array. Default is 'array'
+ */
+ public $definitionType = 'array';
+
+ /**
+ * workflow definition collection
+ * @var
+ */
+ private $_workflow;
+ private $_workflowBasePath;
+
+ /**
+ * Initialize the component with configured values. To pre-load workflows, set configuration
+ * setting 'preload' to an array containing all workflows to pre-load. If no pre-load is set
+ * workflows are loaded on demand.
+ *
+ * @see SWWorkflowSource
+ */
+ public function init()
+ {
+ parent::init();
+ $this->_workflowBasePath = Yii::getPathOfAlias($this->basePath);
+
+ if (is_array($this->preload) && count($this->preload)) {
+ foreach ($this->preload as $wfId)
+ $this->_load($wfId, true);
+ }
+
+ if ($this->definitionType == 'class')
+ Yii::import($this->basePath . '.*');
+ }
+
+ /**
+ * Loads a workflow from a php source file into the $this->_workflow associative array. A call to reset() will unload all workflows.
+ */
+ private function _load($wfId, $forceReload)
+ {
+ if (!is_string($wfId) || empty($wfId))
+ throw new SWException('Failed to load workflow - invalid workflow Id: ' . $wfId, SWException::SW_ERR_WORKFLOW_ID);
+
+ if (!isset($this->_workflow[$wfId]) || $forceReload === true) {
+ if ($this->definitionType == 'class') {
+ $wo = new $wfId;
+ $this->_workflow[$wfId] = $this->_createWorkflow($wo->getDefinition(), $wfId);
+ } elseif ($this->definitionType == 'array') {
+ $fname = $this->_workflowBasePath . DIRECTORY_SEPARATOR . $wfId . '.php';
+
+ if (file_exists($fname) === false)
+ throw new SWException("Workflow definition file not found: {$fname}", SWException::SW_ERR_WORKFLOW_NOT_FOUND);
+
+ $this->_workflow[$wfId] = $this->_createWorkflow(require($fname), $wfId);
+ }
+ }
+
+ return $this->_workflow[$wfId];
+ }
+
+ /**
+ * @param array $wf workflow definition
+ * @param string $wfId workflow Id
+ * @return array
+ * @throws SWException
+ */
+ private function _createWorkflow($wf, $wfId)
+ {
+ if (!is_array($wf) || empty($wfId))
+ throw new SWException('Invalid argument');
+
+ $wfDefinition = array();
+
+ if (!isset($wf['initial']))
+ throw new SWException('missing initial status for workflow: ' . $wfId, SWException::SW_ERR_IN_WORKFLOW);
+
+ /**
+ * load node list
+ */
+ $nodeIds = array();
+
+ foreach ($wf['node'] as $rnode) {
+ $node = new SWNode($rnode, $wfId);
+
+ if (in_array($node->getId(), $nodeIds))
+ throw new SWException('Duplicate node id: ' . $node->getId(), SWException::SW_ERR_IN_WORKFLOW);
+
+ $nodeIds[] = $node->getId();
+ $wfDefinition[$node->getId()] = $node;
+
+ if ($node->getId() == $wf['initial'] || $node->toString() == $wf['initial'])
+ $wfDefinition['swInitialNode'] = $node;
+ }
+
+ /**
+ * checks that initial node is set
+ */
+ if (!isset($wfDefinition['swInitialNode']))
+ throw new SWException('Missing initial status for workflow: ' . $wfId, SWException::SW_ERR_IN_WORKFLOW);
+
+ return $wfDefinition;
+ }
+
+ /**
+ * Returns the SWNode object from the workflow collection.
+ *
+ * @param SWNode $swNode node to search for in the node list
+ * @return SWNode the SWNode object retrieved from the workflow collection, or NULL if this node could not be found in the workflow collection
+ * @throws SWException
+ */
+ private function _getNode($swNode)
+ {
+ $wfId = $swNode->getWorkflowId();
+
+ if ($wfId === null)
+ throw new SWException('Workflow not found: ' . $wfId, SWException::SW_ERR_WORKFLOW_NOT_FOUND);
+
+ $this->_load($wfId, false);
+ $nodeId = $swNode->getId();
+
+ if (isset($this->_workflow[$wfId][$nodeId]))
+ return $this->_workflow[$wfId][$nodeId];
+
+ return null;
+ }
+
+ /**
+ * Verify if a workflow has been loaded.
+ *
+ * @param string $workflowId workflow id
+ * @return boolean TRUE if the workflow whose id is $workflowId has already been loaded, FALSE otherwise
+ */
+ public function isWorkflowLoaded($workflowId)
+ {
+ return isset($this->_workflow[$workflowId]);
+ }
+
+ /**
+ * Loads the workflow whose id is passed as argument.
+ * By default, if the workflow has already been loaded it is not reloaded unless
+ * $forceReload is TRUE
+ *
+ * @param string $workflowId the workflow id
+ * @param bool $forceReload TRUE to force workflow loading, FALSE otherwise
+ * @return bool
+ */
+ public function loadWorkflow($workflowId, $forceReload = false)
+ {
+ return ($this->_load($workflowId, $forceReload) !== null);
+ }
+
+ /**
+ * This method is used to add a new workflow definition to the current workflow collection.
+ *
+ * @param array $definition the workflow definition in its array form
+ * @param string $id the workflow id
+ * @throws SWException
+ */
+ public function addWorkflow($definition, $id)
+ {
+ if (!is_array($definition))
+ throw new SWException('Array expected');
+
+ if (!isset($this->_workflow[$id]))
+ $this->_workflow[$id] = $this->_createWorkflow($definition, $id);
+ }
+
+ /**
+ * @see SWWorkflowSource::getNodeDefinition()
+ */
+ public function getNodeDefinition($node, $defaultWorkflowId = null)
+ {
+ return $this->_getNode($this->createSWNode($node, $defaultWorkflowId));
+ }
+
+ /**
+ * @see SWWorkflowSource::getNextNodes()
+ */
+ public function getNextNodes($sourceNode, $workflowId = null)
+ {
+ $result = array();
+
+ /**
+ * convert startStatus into SWNode
+ */
+ $startNode = $this->getNodeDefinition($this->createSWNode($sourceNode, $workflowId));
+
+ if ($startNode === null)
+ throw new SWException('Node could not be found: ' . $sourceNode, SWException::SW_ERR_NODE_NOT_FOUND);
+
+ foreach ($startNode->getNext() as $nxtNodeId => $tr)
+ $result[] = $this->_getNode(new SWNode($nxtNodeId, $workflowId));
+
+ return $result;
+ }
+
+ /**
+ * @see SWWorkflowSource::isNextNode()
+ */
+ public function isNextNode($sourceNode, $targetNode, $workflowId = null)
+ {
+ $startNode = $this->createSWNode($sourceNode, $workflowId);
+
+ $nextNode = $this->createSWNode($targetNode,
+ ($workflowId != null
+ ? $workflowId
+ : $startNode->getWorkflowId()
+ )
+ );
+
+ $nxt = $this->getNextNodes($startNode);
+
+ if ($nxt !== null)
+ return in_array($nextNode->toString(), $nxt);
+
+ return false;
+ }
+
+ /**
+ * @see SWWorkflowSource::getInitialNode()
+ */
+ public function getInitialNode($workflowId)
+ {
+ $this->_load($workflowId, false);
+ return $this->_workflow[$workflowId]['swInitialNode'];
+ }
+
+ /**
+ * @see SWWorkflowSource::getAllNodes()
+ */
+ public function getAllNodes($workflowId)
+ {
+ $result = array();
+ $wf = $this->_load($workflowId, false);
+
+ foreach ($wf as $key => $value) {
+ if ($key !== 'swInitialNode')
+ $result[] = $value;
+ }
+
+ return $result;
+ }
+}
diff --git a/SWValidator.php b/SWValidator.php
index 072fc56..8027c13 100644
--- a/SWValidator.php
+++ b/SWValidator.php
@@ -1,181 +1,180 @@
-
- * This validator should be used to validate the 'status' attribute for an active record
- * object before it is saved. It tests if the transition that is about to occur is valid.
- * Moreover, if $enableSwValidation is set to true, this validator applies all
- * validators that may have been defined by the model for the scenario associated to the transition
- * being done.
- * Scenario names associated with a transition, have the following format :
- *
- * sw:[currentStatus]-[nextStatus]
- *
- * For instance, if the model being validated is currently in status 'A' and it is sent in status 'B', the
- * corresponding scenario name is 'sw:A-B'. Note that if the destination status doesn't belong to the same
- * workflow as the current status, [nextStatus] must be in the form 'workflowId/statusId' (e.g 'sw:A-workflow/B').
- * Eventually, when the model enters in a workflow, the scenario name is '-[nextStatus]' where 'nextStatus'
- * includes the workflow Id (e.g 'sw:-workflowIs/statusId').
- *
- *
- * If this validator is initialized with parameter match set to TRUE, then transitions scenario defined
- * for validators are assumed to be regular expressions. If the current transition matches, then the associated
- * validator is executed.
- * For instance, if validator 'required' for attribute A applies to scenarion 'sw:/S1_.?/' then each time the
- * model leaves status S1, then the required validator will be applied.
- *
- */
-class SWValidator extends CValidator
-{
- /**
- * @var boolean (default FALSE) Enables simpleWorkflow Validation. When TRUE, the SWValidator not only
- * validates status change for the model, but also applies all validators that may have been created and
- * which are associated with the scenario for the transition being done. Such scenario names are based on
- * both the current and the next status name.
- */
- public $enableSwValidation=false;
- /**
- * @var boolean (default FALSE) When true, the scenario name is evaluated as a regular expression that must
- * match the transition name being done.
- */
- public $match=false;
-
- const SW_SCENARIO_STATUS_SEPARATOR='-';
- const SW_SCENARIO_PREFIX='sw:';
- private $_lenPrefix=null;
- /**
- * Validate status change and applies all validators defined by the model for the current transition scenario if
- * enableSwValidation is TRUE. If validator parameter 'match' is true, the transition scenario is matched
- * against validator scenario (which are assumed to be regular expressions).
- *
- * @see validators/CValidator::validateAttribute()
- * @param CModel $model the model to validate
- * @param string $attribute the model attribute to validate
- */
- protected function validateAttribute($model,$attribute)
- {
- $value=$model->$attribute;
-
- if($model->swValidate($attribute,$value)==true and $this->enableSwValidation ===true){
-
- $swScenario=$this->_getSWScenarioName($model, $value);
-
- if(!empty($swScenario))
- {
- if($this->match === true){
-
- // validator scenario are Regular Expression that must match the transition scenarion
- // for the validator to be executed.
-
- $validators=$model->getValidatorList();
- foreach($validators as $validator)
- {
- if($this->_validatorMatches($validator,$swScenario)){
- $validator->validate($model);
- }
- }
- }else {
- $swScenario=SWValidator::SW_SCENARIO_PREFIX.$swScenario;
- // execute only validator defined for the current transition scenario ($swScenario)
-
- // getValidators returns validators with no scenario, and the ones
- // that apply to the current scenario (swScenario).
-
- $saveScenario=$model->getScenario();
- $model->setScenario($swScenario);
-
- $validators=$model->getValidators();
-
- foreach($model->getValidators() as $validator)
- {
- // only run validators that applies to the current (swScenario) scenario
-
- if(isset($validator->on[$swScenario])){
- $validator->validate($model);
- }
- }
- // restore original scenario so validation can continue.
- $model->setScenario($saveScenario);
- }
- }
- }
- }
- /**
- * Create the scenario name for the current transition. Scenario name has following format :
- * [currentStatus]-[nextStatus]
- *
- * @param CModel $model the model being validated
- * @param string $nxtStatus the next status name (destination status for the model)
- * @return string SW scenario name for this transition
- *
- */
- private function _getSWScenarioName($model,$nxtStatus)
- {
- $swScenario=null;
- $nextNode=$model->swCreateNode($nxtStatus);
- $curNode=$model->swGetStatus();
- if( $curNode != null )
- {
- $swScenario=$curNode->getId().SWValidator::SW_SCENARIO_STATUS_SEPARATOR;
- if($curNode->getWorkflowId()!=$nextNode->getWorkflowId()){
- $swScenario.=$nextNode->toString();
- }else {
- $swScenario.=$nextNode->getId();
- }
- }else {
- $swScenario=SWValidator::SW_SCENARIO_STATUS_SEPARATOR.$nextNode->toString();
- }
- return $swScenario;
- }
- /**
- * Check that a CValidator based object is defined for a scenario that matches
- * the simple workflow scenario passed as argument.
- *
- * @param $validator CValidator validator to test
- * @param $swScenario string simple workflow scenario defined as a regular expression
- */
- private function _validatorMatches($validator,$swScenario)
- {
- $bResult=false;
- if(isset($validator->on)){
- $validatorScenarios=(is_array($validator->on)?$validator->on:array($validator->on));
- foreach ($validatorScenarios as $valScenario)
- {
- // SW Scenario validator must begin with a non-empty prefix (default 'sw:')
- // and then define a valide regular expression
-
- $re=$this->_extractSwScenarioPattern($valScenario);
-
- if( $re != null )
- {
- if(preg_match($re, $swScenario)){
- $bResult=true;
- break;
- }
- }
- }
- }
- return $bResult;
- }
- /**
- * Extract a regular expression pattern out of a simepleWorkflow scenario name
- *
- * @param $valScenario String validator scenario name (example : 'sw:/^status1-.*$/')
- * @return String regular expression (example : '/^status1-.*$/')
- */
- private function _extractSwScenarioPattern($valScenario)
- {
- $pattern=null;
-
- if($this->_lenPrefix==null){
- $this->_lenPrefix=strlen(SWValidator::SW_SCENARIO_PREFIX);
- }
-
- if( $this->_lenPrefix != 0 &&
- strpos($valScenario, SWValidator::SW_SCENARIO_PREFIX) === 0)
- {
- $pattern=substr($valScenario, $this->_lenPrefix);
- }
- return $pattern;
- }
-}
-?>
+
+ * This validator should be used to validate the 'status' attribute for an active record
+ * object, before it is saved. It tests if the transition that is about to occur is valid.
+ * Moreover, if $enableSwValidation is set to true, this validator applies all
+ * validators that may have been defined by the model, for the scenario associated to the transition
+ * being done.
+ * Scenario names associated with a transition, have the following format :
+ *
+ * sw:[currentStatus]_[nextStatus]
+ *
+ * For instance, if the model being validated is currently in status 'A' and it is sent in status 'B', the
+ * corresponding scenario name is 'sw:A_B'. Note that if the destination status doesn't belong to the same
+ * workflow as the current status, [nextStatus] must be in the form 'workflowId/statusId' (e.g 'sw:A_workflow/B').
+ * Eventually, when the model enters in a workflow, the scenario name is '_[nextStatus]' where 'nextStatus'
+ * includes the workflow Id (e.g 'sw:_workflowIs/statusId').
+ *
+ *
+ * If this validator is initialized with parameter match set to TRUE, then transitions scenario defined
+ * for validators are assumed to be regular expressions. If the current transition matches, then the associated
+ * validator is executed.
+ * For instance, if validator 'required' for attribute A applies to scenarion 'sw:/S1_.?/' then each time the
+ * model will pass a transition that leaves status S1 then the \'required\' validator will be executed.
+ *
+ */
+class SWValidator extends CValidator
+{
+ /**
+ * @var boolean (default FALSE) Enables simpleWorkflow Validation. When TRUE, the SWValidator not only
+ * validates status change for the model, but also applies all validators that may have been created and
+ * which are associated with the scenario for the transition being done. Such scenario names are based on
+ * both the current and the next status name.
+ */
+ public $enableSwValidation = false;
+
+ /**
+ * @var boolean (default FALSE) When true, the scenario name is evaluated as a regular expression that must
+ * match the transition name being done.
+ */
+ public $match = false;
+
+ const SW_SCENARIO_STATUS_SEPARATOR = '-';
+ const SW_SCENARIO_PREFIX = 'sw:';
+
+ private $_lenPrefix = null;
+
+ /**
+ * Validate status change and applies all validators defined by the model for the current transition scenario if
+ * enableSwValidation is TRUE. If validator parameter 'match' is true, the transition scenario is matched
+ * against validator scenario (which are assumed to be regular expressions).
+ *
+ * @see validators/CValidator::validateAttribute()
+ * @param CModel $model the model to validate
+ * @param string $attribute the model attribute to validate
+ */
+ protected function validateAttribute($model, $attribute)
+ {
+ $value = $model->$attribute;
+
+ if ($model->swValidate($attribute, $value) === true && $this->enableSwValidation === true) {
+ $swScenario = $this->_getSWScenarioName($model, $value);
+
+ if(empty($swScenario))
+ return;
+
+ if ($this->match === true) {
+ /**
+ * validator scenario are Regular Expression that must match the transition scenario for the validator to be executed.
+ */
+ foreach ($model->getValidatorList() as $validator) {
+ if ($this->_validatorMatches($validator, $swScenario))
+ $validator->validate($model);
+ }
+ } else {
+ $swScenario = SWValidator::SW_SCENARIO_PREFIX . $swScenario;
+ $saveScenario = $model->getScenario();
+
+ /**
+ * we must execute validators that defined only for the current transition scenario ($swScenario)
+ */
+ $model->setScenario($swScenario);
+ foreach ($model->getValidators() as $validator) {
+ /**
+ * run only validators that applies to the current (swScenario) scenario
+ */
+ if (isset($validator->on[$swScenario]))
+ $validator->validate($model);
+ }
+
+ /**
+ * restore original scenario, so validation can continue.
+ */
+ $model->setScenario($saveScenario);
+ }
+ }
+ }
+
+ /**
+ * Create the scenario name for the current transition. Scenario name has following format :
+ * [currentStatus]_[nextStatus]
+ *
+ * @param CModel $model the model being validated
+ * @param string $nxtStatus the next status name (destination status for the model)
+ * @return string SW scenario name for this transition
+ *
+ */
+ private function _getSWScenarioName($model, $nxtStatus)
+ {
+ $swScenario = null;
+ $nextNode = $model->swCreateNode($nxtStatus);
+ $curNode = $model->swGetStatus();
+
+ if ($curNode !== null) {
+ $swScenario = $curNode->getId() . SWValidator::SW_SCENARIO_STATUS_SEPARATOR;
+ $swScenario .= (($curNode->getWorkflowId() !== $nextNode->getWorkflowId())
+ ? $nextNode->toString()
+ : $nextNode->getId()
+ );
+
+ } else {
+ $swScenario = SWValidator::SW_SCENARIO_STATUS_SEPARATOR . $nextNode->toString();
+ }
+
+ return $swScenario;
+ }
+
+ /**
+ * Check that a CValidator based object is defined for a scenario that matches
+ * the simple workflow scenario passed as argument.
+ *
+ * @param $validator CValidator validator to test
+ * @param string $swScenario simple workflow scenario defined as a regular expression
+ * @return bool
+ */
+ private function _validatorMatches($validator, $swScenario)
+ {
+ if (!(isset($validator->on)))
+ return false;
+
+ $bResult = false;
+ $validatorScenarios = (is_array($validator->on) ? $validator->on : array($validator->on));
+
+ foreach ($validatorScenarios as $valScenario) {
+ /**
+ * SW Scenario validator must begin with a non-empty prefix (default 'sw:') with a following valid regular expression
+ */
+ $re = $this->_extractSwScenarioPattern($valScenario);
+
+ if ($re !== null) {
+ if (preg_match($re, $swScenario)) {
+ $bResult = true;
+ break;
+ }
+ }
+ }
+
+ return $bResult;
+ }
+
+ /**
+ * Extract a regular expression pattern out of a simepleWorkflow scenario name
+ *
+ * @param $valScenario String validator scenario name (example : 'sw:/^status1_.*$/')
+ * @return String regular expression (example : '/^status1_.*$/')
+ */
+ private function _extractSwScenarioPattern($valScenario)
+ {
+ $pattern = null;
+
+ if ($this->_lenPrefix === null)
+ $this->_lenPrefix = strlen(SWValidator::SW_SCENARIO_PREFIX);
+
+ if ($this->_lenPrefix !== 0 && strpos($valScenario, SWValidator::SW_SCENARIO_PREFIX) === 0)
+ $pattern = substr($valScenario, $this->_lenPrefix);
+
+ return $pattern;
+ }
+}
diff --git a/SWWorkflowSource.php b/SWWorkflowSource.php
index 5d5e00a..43a65e8 100644
--- a/SWWorkflowSource.php
+++ b/SWWorkflowSource.php
@@ -1,97 +1,108 @@
-
- */
-abstract class SWWorkflowSource extends CApplicationComponent
-{
- /**
- * @var array list of workflow names that shoumd ne loaded when the component is initialized
- */
- public $preload=array();
- /**
- * @var string when a workflow name is automatically built from the model name, this prefix is added to the
- * model name so to avoid clashes (e.g. model 'MyModel' is by default inserted into workflow 'swMyModel')
- */
- public $workflowNamePrefix='sw';
- /**
- * Create and returns a SWNode object. The SWNode returned doesn't have to be defined
- * in a workflow currently loaded.
- * If $node is a string, it can be a fully qualified node id (e.g workflowId/NodeId)
- * or only a nodeId, but in this case, argument $workflowId must contain the id of the
- * workflow to use.
- * If $node is a SWNode object, then it is returned with no modification.
- *
- * @return SWNode the node object
- */
- public function createSWNode($node,$workflowId)
- {
- return new SWNode($node,$workflowId);
- }
- /**
- * Add a workflow to the internal workflow collection. The definition
- * of the workflow to add is provided in the $definition argument as an associative array.
- * This method is used for instance when a workflow definition is provided by a
- * model and not by a php file or another source. If a workflow with the same id is already
- * loaded, it is not over written.
- *
- * @param array $definition workflow definition
- * @param string $id unique id for the workflow to add
- */
- abstract public function addWorkflow($definition, $id);
- /**
- * Loads the workflow whose id is passed as argument from the source.
- * If it was already loaded, then it is not reloaded unles $forceReload is set to TRUE.
- * If the workflow could not be found, an exception is thrown.
- *
- * @param string $workflowId the id of the workflow to load
- * @param boolean $forceReload force workflow reload
- */
- abstract public function loadWorkflow($workflowId,$forceReload=false);
- /**
- * Search for the node passed as argument in the workflow definition. Note that if
- * this node is not found among the currently loaded workflows, this method will try
- * to load the workflow it belongs to.
- *
- * @param mixed node String or SWNode object to look for
- * @return SWNode the node as it is defined in a workflow, or NULL if not found
- */
- abstract public function getNodeDefinition($node, $defaultWorkflowId=null);
- /**
- * Returns an array containing all SWNode object for each status that can be reached
- * from $startStatus. It does not evaluate node constraint but only the fact that a transition
- * exist beteween $startStatus and nodes returned. If no nodes are found, an empty array is returned.
- * An exception is thrown if $startStatus is not found among all worklows available.
- *
- *@return array SWNode array
- */
- abstract public function getNextNodes($sourceNode,$workflowId=null);
- /**
- * Checks if there is a transition between the two nodes passed as argument.
- *
- * @param mixed $sourceNode can be provided as a SWNode object, or as a string that
- * can contain a workflowId or not.
- * @param mixed $targetNode target node to test
- * @return boolean true if $nextStatus can be reached from $startStatus
- */
- abstract public function isNextNode($sourceNode,$targetNode,$workflowId=null);
- /**
- * Returns the initial node defined for the workflow whose id is passed as
- * argument. A valid workflow must have one and only one initial status. If it's
- * note the case, workflow can't be loaded.
- *
- * @return SWnode initial node for $workflowId
- */
- abstract public function getInitialNode($workflowId);
- /**
- * Fetch all nodes belonging to the workflow whose Id is passed as argument.
- *
- * @param string $workflowId id of the workflow that owns all nodes returned
- * @return array all nodes belonging to workflow $workflowId
- */
- abstract public function getAllNodes($workflowId);
-
-}
-
-?>
+