From fb90ab3a1ce9958e2c8f3f51b7cd846482749468 Mon Sep 17 00:00:00 2001 From: andrey Date: Sat, 1 Mar 2014 15:17:06 +0400 Subject: [PATCH 1/2] * mass refactoring for extension * small bug fixing --- SWActiveRecord.php | 70 +- SWActiveRecordBehavior.php | 2119 ++++++++++++++++++------------------ SWComponent.php | 70 +- SWEvent.php | 57 +- SWException.php | 41 +- SWHelper.php | 298 ++--- SWNode.php | 564 +++++----- SWPhpWorkflowSource.php | 531 +++++---- SWValidator.php | 361 +++--- SWWorkflowSource.php | 205 ++-- 10 files changed, 2169 insertions(+), 2147 deletions(-) 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:
    - * - * - * @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 : - * - * @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 - *
    - * - * 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 :
    - * - * @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. - * - * Additionnally, the behavior will fire custom events on various steps of the owner model life-cycle within its workflow : - * - * @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:
    + * + * + * @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 : + * + * + * @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 + *
    + * + * 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 (($constraint === null || $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 :
    + * + * @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. + * + * Additionally, the behavior will fire custom events on various steps of the owner model life-cycle within its workflow : + * + * @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 : - * - * 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 : + * + * 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 : - * - * @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 : + * + * + * @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); - -} - -?> + Date: Sat, 1 Mar 2014 16:29:46 +0400 Subject: [PATCH 2/2] * mass refactoring for extension * small bug fixing --- SWActiveRecordBehavior.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SWActiveRecordBehavior.php b/SWActiveRecordBehavior.php index 0b16893..f387aed 100644 --- a/SWActiveRecordBehavior.php +++ b/SWActiveRecordBehavior.php @@ -541,7 +541,7 @@ public function swCreateNode($str) */ private function _evaluateConstraint($constraint) { - return (($constraint === null || $this->getOwner()->evaluateExpression($constraint) === true) ? true : false); + return (( empty($constraint) || $this->getOwner()->evaluateExpression($constraint) === true) ? true : false); } /**