simpleWorkflow
  • Class
  • Tree

Classes

  • SWActiveRecord
  • SWActiveRecordBehavior
  • SWComponent
  • SWEvent
  • SWException
  • SWHelper
  • SWNode
  • SWPhpWorkflowSource
  • SWValidator
  • SWWorkflowSource
  • SWyEdConverter
  • SWyEdConverterDOM
   1 <?php
   2 /**
   3  * This class implements all the logic for the simpleWorkflow extension.
   4  * Following attributes can be initialized when this behavior is attached to the owner component :
   5  * <ul>
   6  * <li><b>statusAttribute</b> (string) : This is the column name where status is stored<br/>
   7  *      If this attribute doesn't exist for  a model, the Workflow behavior is automatically disabled and a warning is
   8  *      logged.<br/>
   9  *      In the database, this attribute must be defined as a VARCHAR() whose length should be large enough to
  10  *      contains a complete status name with format <b>workflowId/nodeId</b>.<br/>
  11  * example :
  12  * <pre>
  13  * task/pending
  14  * postWorkflow/to_review
  15  * </pre>
  16  * Default : 'status'
  17  * </li>
  18  * <li><b>defaultWorkflow</b> (string) : workflow name that should be used by default for the owner model <br/>
  19  *      If this parameter is not set, then it is automatically created based on the name of the owner model, prefixed
  20  *      with 'workflowNamePrefix' defined by the workflow source component. By default this value is set to 'sw' and so,
  21  *      for example 'Model1' is associated by default with workflow 'swModel1'.<br/>
  22  *      Default : SWWorkflowSource->workflowNamePrefix . ModelName
  23  * </li>
  24  * <li><b>autoInsert</b> (boolean) : <br/>
  25  * If TRUE, the model is automatically inserted in the workflow (if not already done) when it is saved.
  26  * If FALSE, it is developer responsability to insert the model in the workflow.<br/>
  27  * Default : true
  28  * </li>
  29  * <li><b>workflowSourceComponent</b> (string) : <br/>
  30  * Name of the workflow source component to use with this behavior.<br/>
  31  * By ddefault this parameter is set to <em>swSource</em> (see {@link SWPhpWorkflowSource})
  32  * </li>
  33  * <li><b>enableEvent</b> (boolean) : <br/>
  34  * If TRUE, this behavior will fire SWEvents. Note that even if it
  35  * is true, this doesn't garantee that SW events will be fired as another condition is that the owner
  36  * component provides SWEvent handlers.<br/>
  37  * Default : true
  38  * </li>
  39  * <li><b>transitionBeforeSave</b> (boolean) : <br/>
  40  * If TRUE, SWEvents are fired and possible transitions tasks are executed <b>before</b> the owner model is
  41  * actually saved. If FALSE, events and task transitions are processed after save.<br/>
  42  * It has no effect if the transition is done programatically by a call to swNextStatus(), but only if it is done when the
  43  * owner model is saved.<br/>
  44  * Default : true
  45  * </li>
  46  * </ul>
  47  */
  48 class SWActiveRecordBehavior extends CBehavior
  49 {
  50     /**
  51      * @var string  This is the column name where status is stored.
  52      */
  53     public $statusAttribute = 'status';
  54     /**
  55      * @var string workflow name that should be used by default for the owner model.
  56      */
  57     public $defaultWorkflow=null;
  58     /**
  59      * @var boolean
  60      */
  61     public $autoInsert=true;
  62     /**
  63      * @var string name of the workflow source component
  64      */
  65     public $workflowSourceComponent='swSource';
  66     /**
  67      * @var boolean
  68      */
  69     public $enableEvent=true;
  70     /**
  71      * @var boolean
  72      */
  73     public $transitionBeforeSave=true;
  74     
  75     ///////////////////////////////////////////////////////////////////////////////////////////
  76     // private members
  77     
  78     private $_delayedTransition=null;           // delayed transition  (only when change status occures during save)
  79     private $_delayedEvent=array();             // delayed event stack (only when change status occures during save)
  80     private $_beforeSaveInProgress=false;       // prevent delayed event fire when status is changed by a call to swNextStatus
  81     private $_status=null;                      // internal status for the owner model
  82     private $_wfs;                              // workflow source component reference
  83     private $_locked=false;                     // prevent reentrance
  84     private $_final=null;
  85         
  86     //
  87     ///////////////////////////////////////////////////////////////////////////////////////////
  88     
  89     /**
  90      * @var string name of the class the owner should inherit from in order for SW events
  91      * to be enabled.
  92      */
  93     protected $eventClassName='SWActiveRecord';
  94 
  95     const SW_LOG_CATEGORY='application.simpleWorkflow';
  96     const SW_I8N_CATEGORY='simpleworkflow';
  97 
  98 
  99     /**
 100      * @return reference to the workflow source used by this behavior
 101      */
 102     public function swGetWorkflowSource()
 103     {
 104         return $this->_wfs;
 105     }
 106     /**
 107      * Checks that the owner component is able to handle workflow events that could be fired
 108      * by this behavior
 109      *
 110      * @param CComponent $owner the owner component attaching this behavior
 111      * @param string $className
 112      * @return bool TRUE if workflow events are fired, FALSE if not.
 113      */
 114     protected function canFireEvent($owner,$className)
 115     {
 116         return $owner instanceof $className;
 117     }
 118     /**
 119      * If the owner component is inserted into a workflow, this method returns the SWNode object
 120      * that represent this status, otherwise NULL is returned.
 121      *
 122      * @return SWNode the current status or NULL if no status is set
 123      */
 124     public function swGetStatus()
 125     {
 126         return $this->_status;
 127     }
 128     /**
 129      * Event may be enabled by configuration (when the behavior is attached to the owner component) but it
 130      * can be automatically disabled if the owner component does not define handlers for all SWEvents (i.e events
 131      * fired when the owner component evolves in the workflow).
 132      * {@link SWActiveRecordBehavior::attach}
 133      *
 134      * @return bool TRUE if workflow events are fire by this behavior, FALSE if not.
 135      */
 136     public function swIsEventEnabled()
 137     {
 138         return $this->enableEvent;
 139     }
 140     /**
 141      * Test if the owner component is currently in the status passed as argument.
 142      *
 143      * @param mixed $status name or SWNode instance of the status to test
 144      * @returns boolean TRUE if the owner component is in the status passed as argument, FALSE otherwise
 145      */
 146     public function swIsStatus($status)
 147     {
 148         return $this->swHasStatus() && $this->swGetStatus()->equals($status);
 149     }
 150     /**
 151      * Test if the current status is the same as the one passed as argument.
 152      * A call to swStatusEquals(<em>null</em>) returns TRUE only if the owner component is not in a workflow.
 153      *
 154      * @param mixed $status string or SWNode instance.
 155      * @return boolean
 156      */
 157     public function swStatusEquals($status=null)
 158     {
 159         
 160         if( ($status == null && $this->swHasStatus() == false) ||
 161             ($status != null && $this->swHasStatus() &&  $this->swGetStatus()->equals($status)) )
 162             return true;
 163         else
 164             return false;
 165     }
 166     /**
 167      * Test if the owner component is currently inserted in a workflow.
 168      * This method is equivalent to swGetStatus()!=null.
 169      *
 170      * @return boolean true if the owner model is in a workflow, FALSE otherwise
 171      * @see swGetStatus
 172      */
 173     public function swHasStatus()
 174     {
 175         return ! $this->_status == null;
 176     }
 177     /**
 178      * acquire the lock in order to avoid reentrance
 179      *
 180      * @throws SWException
 181      */
 182     private function _lock()
 183     {
 184         if($this->_locked==true){
 185             throw new SWException('re-entrant exception on set status',SWException::SW_ERR_REETRANCE);
 186         }
 187         $this->_locked=true;
 188     }
 189     /**
 190      * Release the lock
 191      */
 192     private function _unlock()
 193     {
 194         $this->_locked=false;
 195     }
 196     /**
 197      * Update the owner model attribute configured to store the current status and the internal
 198      * value too.
 199      *
 200      * @param SWnode $SWNode internal status is set to this node
 201      */
 202     private function _updateStatus($SWNode)
 203     {
 204         if(! $SWNode instanceof SWNode)
 205             throw new SWException('SWNode object expected',SWException::SW_ERR_WRONG_TYPE);
 206         
 207         $this->_status=$SWNode;
 208         $this->_final = null;
 209     }
 210     /**
 211      * Updates the owner component status attribute with the value passed as argument.
 212      *
 213      * @param mixed $status the new owner status value provided as a SWNode object or string
 214      */
 215     private function _updateOwnerStatus($status)
 216     {
 217         
 218         if($status instanceof SWNode)
 219             $this->getOwner()->{$this->statusAttribute} = $status->toString();
 220         elseif( is_string($status))
 221             $this->getOwner()->{$this->statusAttribute} = $status;
 222         else
 223             throw new SWException('SWNode or string expected',SWException::SW_ERR_WRONG_TYPE);
 224     }
 225     /**
 226      * Returns the current workflow Id the owner component is inserted in, or NULL if the owner
 227      * component is not inserted into a workflow.
 228      *
 229      * @param string current workflow Id or NULL
 230      */
 231     public function swGetWorkflowId()
 232     {
 233         return ($this->swHasStatus()?$this->_status->getWorkflowId():null);
 234     }
 235     /**
 236      * Overloads parent attach method so at the time the behavior is about to be
 237      * attached to the owner component, the behavior is initialized.<br/>
 238      * During the initialisation, following actions are performed:<br/>
 239      * <ul>
 240      * <li>The status attribute exists</li>
 241      * <li>Check whether or not, workflow events should be enabled, by testing if the owner component
 242      * class inherits from the 'SWComponent' or 'SWActiveRecord' class. </li>
 243      * </ul>
 244      *
 245      * @see base/CBehavior::attach()
 246      */
 247     public function attach($owner)
 248     {
 249         if( ! $this->canFireEvent($owner, $this->eventClassName)){
 250             if( $this->swIsEventEnabled()){
 251                 
 252                 // workflow events are enabled by configuration but the owner component is not
 253                 // able to handle workflow event : warning
 254                 
 255                 Yii::log('events disabled : owner component doesn\'t inherit from '. $this->eventClassName,
 256                     CLogger::LEVEL_WARNING,self::SW_LOG_CATEGORY);
 257             }
 258             $this->enableEvent=false;   // force
 259         }
 260         
 261         parent::attach($owner);
 262 
 263         if( $this->getOwner() instanceof CActiveRecord ){
 264             $statusAttributeCol = $this->getOwner()->getTableSchema()->getColumn($this->statusAttribute);
 265             if(!isset($statusAttributeCol) || $statusAttributeCol->type != 'string' ){
 266                 throw new SWException('attribute '.$this->statusAttribute.' not found',SWException::SW_ERR_ATTR_NOT_FOUND);
 267             }
 268         }
 269         
 270         // preload the workflow source component
 271         
 272         $this->_wfs= Yii::app()->{$this->workflowSourceComponent};
 273         
 274         // load the default workflow id now because the owner model maybe able to provide it
 275         // together with the whole workflow definition. In this case, this definition must be pushed
 276         // to the SWWorkflowSource component (done by swGetDefaultWorkflowId).
 277     
 278         $defWid = $this->swGetDefaultWorkflowId();
 279         
 280         // autoInsert now !
 281         
 282         if($this->autoInsert == true && $this->getOwner()->{$this->statusAttribute} == null){
 283             $this->swInsertToWorkflow($defWid);
 284         }
 285     }
 286     /**
 287      * Finds out what should be the default workflow to use with the owner model.
 288      * To find out what is the default workflow, this method perform following tests :
 289      * <ul>
 290      *  <li>behavior initialization parameter <i>defaultWorkflow</i></li>
 291      *  <li>owner component method <i>workflow</i> : if the owner component is able to provide the
 292      * complete workflow, this method will invoke SWWorkflowSource.addWorkflow</li>
 293      *  <li>created based on the configured prefix followed by the model class name. The default workflow prefix is 'sw' so
 294      *  if the owner model is MyModel, the default workflow id will be swMyModel (case sensitive) </li>
 295      * </ul>
 296      * @return string workflow id to use with the owner component or NULL if no workflow was found
 297      */
 298     public function swGetDefaultWorkflowId()
 299     {
 300         if( $this->defaultWorkflow == null)
 301         {
 302             $workflowName=null;
 303             if( $this->defaultWorkflow != null)
 304             {
 305                 // the behavior has been initialized with the default workflow name
 306                 
 307                 $workflowName=$this->defaultWorkflow;
 308             }
 309             elseif(method_exists($this->getOwner(),'workflow'))
 310             {
 311                 
 312                 $wf=$this->getOwner()->workflow();
 313                 if( is_array($wf)){
 314                     
 315                     // Cool ! the owner is able to provide its own private workflow definition ...and optionally
 316                     // a workflow name too. If no workflow name is provided, the model name is used to
 317                     // identity the workflow
 318                     
 319                     $workflowName=(isset($wf['name'])
 320                         ? $wf['name']
 321                         : $this->swGetWorkflowSource()->workflowNamePrefix.get_class($this->getOwner())
 322                     );
 323                     
 324                     $this->swGetWorkflowSource()->addWorkflow($wf,$workflowName);
 325                     
 326                 }elseif(is_string($wf)) {
 327                     
 328                     // the owner returned a string considered as its default workflow Id
 329     
 330                     $workflowName=$wf;
 331                 }else {
 332                     throw new SWException('incorrect type returned by owner method : string or array expected',SWException::SW_ERR_WRONG_TYPE);
 333                 }
 334             }else {
 335     
 336                 // ok then, let's use the owner model name as the workflow name and hope that
 337                 // its definition is available in the workflow basePath.
 338                 
 339                 $workflowName=$this->swGetWorkflowSource()->workflowNamePrefix.get_class($this->getOwner());
 340             }
 341             $this->defaultWorkflow=$workflowName;
 342         }
 343         return $this->defaultWorkflow;
 344     }
 345     /**
 346      * Insert the owner component into the workflow whose id is passed as argument.
 347      * If NULL is passed as argument, the default workflow is used. If no error occurs, when this method ends, the owner
 348      * component's status is the initial node of the selected workflow.
 349      *
 350      * @param string $workflowId workflow Id or NULL. If NULL the default workflow Id is used
 351      * @throws SWException the owner model is already in a workflow
 352      * @return boolean TRUE
 353      */
 354     public function swInsertToWorkflow($workflowId=null)
 355     {
 356         if($this->swHasStatus()){
 357             throw new SWException('object already in a workflow : '.$this->swGetStatus(),SWException::SW_ERR_IN_WORKFLOW);
 358         }
 359         
 360         $wfName=( $workflowId == null
 361             ? $this->swGetDefaultWorkflowId()
 362             : $workflowId
 363         );
 364         
 365         if( $wfName == null ){
 366             throw new SWException('failed to get the workflow name',SWException::SW_ERR_IN_WORKFLOW);
 367         }
 368         $initialNode=$this->swGetWorkflowSource()->getInitialNode($wfName);
 369         
 370         $this->onEnterWorkflow(
 371             new SWEvent($this->getOwner(),null,$initialNode)
 372         );
 373         $this->_updateStatus($initialNode);
 374         $this->_updateOwnerStatus($initialNode);
 375         return true;
 376     }
 377     /**
 378      * Removes the owner component from its current workflow.
 379      * An exception is thrown if the owner model is not in a final status (i.e a status
 380      * with no outgoing transition).
 381      *
 382      * see  {@link SWActiveRecordBehavior::swIsFinalStatus()}
 383      * @throws SWException
 384      */
 385     public function swRemoveFromWorkflow()
 386     {
 387         
 388         if( $this->swIsFinalStatus() == false)
 389             throw new SWException('current status is not final : '.$this->swGetStatus()->toString(),
 390                 SWException::SW_ERR_STATUS_UNREACHABLE);
 391 
 392         $this->onLeaveWorkflow(
 393             new SWEvent($this->getOwner(),$this->_status,null)
 394         );
 395         $this->_status = null;
 396         $this->_final  = null;
 397         $this->_updateOwnerStatus('');
 398     }
 399     /**
 400      * This method returns a list of nodes that can be actually reached at the time the method is called. To be reachable,
 401      * a transition must exist between the current status and the next status, AND if a constraint is defined, it must be
 402      * evaluated to true.
 403      *
 404      * @return array SWNode object array for all nodes thats can be reached from the current node.
 405      */
 406     public function swGetNextStatus()
 407     {
 408         $n=array();
 409         if($this->swHasStatus()){
 410             $allNxtSt=$this->swGetWorkflowSource()->getNextNodes($this->_status);
 411             if( $allNxtSt != null)
 412             {
 413                 foreach ( $allNxtSt as $aStatus ) {
 414                     if($this->swIsNextStatus($aStatus) == true){
 415                         $n[]=$aStatus;
 416                     }
 417                 }
 418             }
 419         }else{
 420             $n[]=$this->swGetWorkflowSource()->getInitialNode($this->swGetDefaultWorkflowId());
 421         }
 422         return $n;
 423     }
 424     /**
 425      * Returns all statuses belonging to the workflow the owner component is inserted in or is related to. If the
 426      * owner component is not inserted in a workflow or related to no workflow, an empty array is returned.
 427      *
 428      * @return array list of SWNode objects.
 429      */
 430     public function swGetAllStatus()
 431     {
 432         if(!$this->swHasStatus() || $this->swGetWorkflowId() == null)
 433             return array();
 434         else
 435             return $this->swGetWorkflowSource()->getAllNodes($this->swGetWorkflowId());
 436     }
 437     /**
 438      * Checks if the status passed as argument can be reached from the current status. This occurs when
 439      * <br/>
 440      * <ul>
 441      *  <li>a transition has been defined in the workflow between those 2 status</li>
 442      * <li>the destination status has a constraint that is evaluated to true in the context of the
 443      * owner model</li>
 444      * </ul>
 445      * Note that if the owner component is not in a workflow, this method returns true if argument
 446      * $nextStatus is the initial status for the workflow associated with the owner model. In other words
 447      * the initial status for a given workflow is considered as the 'next' status, for all component associated
 448      * to this workflow but not inserted in it. Of course, if a constraint is associated with the initial
 449      * status, it must be evaluated to true.
 450      *
 451      * @param mixed nextStatus String or SWNode object for the next status
 452      * @return boolean TRUE if the status passed as argument can be reached from the current status, FALSE
 453      * otherwise.
 454      */
 455     public function swIsNextStatus($nextStatus)
 456     {
 457         $bIsNextStatus=false;
 458         
 459         // get (create) a SWNode object
 460         
 461         $nxtNode=$this->swGetWorkflowSource()->createSWNode(
 462                     $nextStatus,
 463                     $this->swGetDefaultWorkflowId()
 464                 );
 465         
 466         if( (! $this->swHasStatus() and $this->swIsInitialStatus($nextStatus)) or
 467             (  $this->swHasStatus() and $this->swGetWorkflowSource()->isNextNode($this->_status,$nxtNode)) ){
 468             
 469             // Note : the transition NULL -> S is valid only if S is an initial status
 470             
 471             // there is a transition between current and next status,
 472             // now let's see if constraints to actually enter in the next status
 473             // are evaluated to true.
 474             
 475             $swNodeNext=$this->swGetWorkflowSource()->getNodeDefinition($nxtNode);
 476             if($this->_evaluateConstraint($swNodeNext->getConstraint()) == true)
 477             {
 478                 $bIsNextStatus=true;
 479             }
 480             else
 481             {
 482                 $bIsNextStatus=false;
 483             }
 484         }
 485         return $bIsNextStatus;
 486     }
 487     /**
 488      * Creates a new node from the string passed as argument. If $str doesn't contain
 489      * a workflow Id, this method uses the workflowId associated with the owner
 490      * model. The node created here doesn't have to exist within a workflow.
 491      * This method is mainly used by the SWValidator
 492      *
 493      * @param string $str string status name
 494      * @return SWNode the node
 495      */
 496     public function swCreateNode($str)
 497     {
 498         return $this->swGetWorkflowSource()->createSWNode(
 499             $str,
 500             $this->swGetDefaultWorkflowId()
 501         );
 502     }
 503     /**
 504      * Evaluate the expression passed as argument in the context of the owner
 505      * model and returns the result of evaluation as a boolean value.
 506      */
 507     private function _evaluateConstraint($constraint)
 508     {
 509         return ( $constraint == null or
 510             $this->getOwner()->evaluateExpression($constraint) ==true?true:false);
 511     }
 512     /**
 513      * If a expression is attached to the transition, then it is evaluated in the context
 514      * of the owner model, otherwise, the processTransition event is raised. Note that the value
 515      * returned by the expression evaluation is ignored.
 516      */
 517     private function _runTransition($sourceSt,$destSt,$params=null)
 518     {
 519         if($sourceSt != null && $sourceSt instanceof SWNode ){
 520             $tr=$sourceSt->getTransitionTask($destSt);
 521 
 522             if( $tr != null)
 523             {
 524                 if( $this->transitionBeforeSave){
 525                     
 526                     if( is_string($tr))
 527                     {
 528                         $this->getOwner()->evaluateExpression($tr,array(
 529                             'owner'         => $this->getOwner(),
 530                             'sourceStatus'  => $sourceSt->toString(),
 531                             'targetStatus'  => $destSt->toString(),
 532                             'params'        => $params)
 533                         );
 534                     }
 535                     else
 536                     {
 537                         $this->getOwner()->evaluateExpression($tr,array($this->getOwner(),$sourceSt->toString(), $destSt->toString(), $params));
 538                     }
 539                     
 540                 }else {
 541                     $this->_delayedTransition = $tr;
 542                 }
 543             }
 544         }
 545     }
 546     /**
 547      * Checks if the status passed as argument, or the current status (if NULL is passed) is a final status
 548      * of the corresponding workflow.
 549      * By definition a final status as no outgoing transition to other status.
 550      *
 551      * @param status status to test, or null (will test current status)
 552      * @return boolean TRUE when the owner component is in a final status, FALSE otherwise
 553      */
 554     public function swIsFinalStatus($status=null)
 555     {
 556         if($this->_final == null)
 557         {
 558             $workflowId=($this->swHasStatus()?$this->swGetWorkflowId():$this->swGetDefaultWorkflowId());
 559             
 560             if( $status != null){
 561                 $swNode=$this->swGetWorkflowSource()->createSWNode($status,$workflowId);
 562             }elseif($this->swHasStatus() == true) {
 563                 $swNode=$this->_status;
 564             }else {
 565                 return false;
 566             }
 567             $this->_final =  (count($this->swGetWorkflowSource()->getNextNodes($swNode,$workflowId))===0);
 568         }
 569         return $this->_final;
 570 
 571     }
 572     /**
 573      * Checks if the status passed as argument, or the current status (if NULL is passed) is the initial status
 574      * of the corresponding workflow. An exception is raised if the owner model is not in a workflow
 575      * and if $status is null.
 576      *
 577      * @param mixed $status string or SWNode instance
 578      * @return boolean TRUE if the owner component is in an initial status or if $status is an initial
 579      * status.
 580      * @throws SWException
 581      */
 582     
 583     public function swIsInitialStatus($status=null)
 584     {
 585         if( $status !=  null)
 586         {
 587             // create the node to compare with initial node
 588             
 589             $workflowId=( $this->swHasStatus()
 590                 ? $this->swGetWorkflowId()
 591                 : $this->swGetDefaultWorkflowId()
 592             );
 593             $swNode=$this->swGetWorkflowSource()->createSWNode($status,$workflowId);
 594         }
 595         elseif($this->swHasStatus() == true)
 596         {
 597             // $status is null : the current status will be compared with initial node
 598             
 599             $swNode=$this->_status;
 600         }
 601         else {
 602             throw new SWException('no status passed and no current status available',SWException::SW_ERR_CREATE_FAILS);
 603         }
 604         
 605         $swInit=$this->swGetWorkflowSource()->getInitialNode($swNode->getWorkflowId());
 606         return $swInit->equals($swNode);
 607     }
 608     /**
 609      * Validates the status attribute stored in the owner model. This attribute is valid if : <br/>
 610      * <ul>
 611      *  <li>it is not empty</li>
 612      *  <li>it contains a valid status name</li>
 613      *  <li>this status can be reached from the current status</li>
 614      *  <li>or it is equal to the current status (no status change)</li>
 615      * </ul>
 616      * @param string $attribute status attribute name (by default 'status')
 617      * @param mixed $value current value of the status attribute provided as a string or a SWNode object
 618      * @return boolean TRUE if the status attribute contains a valid value, FALSE otherwise
 619      */
 620     public function swValidate($attribute, $value)
 621     {
 622         $bResult=false;
 623         try{
 624             if($value instanceof SWNode){
 625                 $swNode=$value;
 626             }else {
 627                 $swNode = $this->swGetWorkflowSource()->createSWNode(
 628                     $value,
 629                     $this->swGetDefaultWorkflowId()
 630                 );
 631             }
 632             if($this->swIsNextStatus($value)==false and $swNode->equals($this->swGetStatus()) == false){
 633                 $this->getOwner()->addError($attribute,Yii::t(self::SW_I8N_CATEGORY,'not a valid next status'));
 634             }else {
 635                 $bResult=true;
 636             }
 637         }catch(SWException $e){
 638             $this->getOwner()->addError($attribute,Yii::t(self::SW_I8N_CATEGORY,'value {node} is not a valid status',array(
 639                 '{node}'=>$value)
 640             ));
 641         }
 642         return $bResult;
 643     }
 644     /**
 645      * This is an alias for methode {@link SWActiveRecordBehavior::swSetStatus()} and should not be used anymore
 646      * @deprecated
 647      */
 648     public function swNextStatus($nextStatus,$params=null)
 649     {
 650         return $this->swSetStatus($nextStatus,$params);
 651     }
 652     /**
 653      * Set the owner component into the status passed as argument.
 654      * If a transition could be performed, the owner status attribute is updated with the new status value in the form <em>workflowId/nodeId</em>.
 655      * This method is responsible for firing {@link SWEvents} and executing workflow tasks if defined for the given transition.
 656      *
 657      * @param mixed $nextStatus string or array. If array, it must contains a key equals to the name of the status
 658      * attribute, and its value is the one of the destination node (e.g. $arr['status']). This is mainly useful when
 659      * processing _POST array. If a string is provided, it must contain the fullname of the target node (e.g. <em>workfowId/nodeId</em>)
 660      * @return boolean True if the transition could be performed, FALSE otherwise
 661      */
 662     public function swSetStatus($nextStatus,$params=null)
 663     {
 664         if( $nextStatus == null )
 665             throw new SWException('argument "nextStatus" is missing');
 666         
 667         $bResult   = false;
 668         $nextNode  = null;
 669         
 670         if(is_array($nextStatus) && isset($nextStatus[$this->statusAttribute]))
 671         {
 672             // $nextStatus may be provided as an array with a 'statusAttribute' key
 673             // example : $array['status']
 674             $nextStatus=$nextStatus[$this->statusAttribute];
 675         }
 676         elseif( $nextStatus instanceof SWNode)
 677         {
 678             $nextStatus = $nextStatus->toString();
 679         }
 680 
 681         try{
 682             $this->_lock();
 683             
 684             if( $this->swHasStatus() == false && $nextStatus != null)
 685             {
 686                 // insertion into workflow //////////////////////////////////////////////////////////////
 687                 //  $c->swNextStatus($status) was called. $c is not currently in a workflow and $status is
 688                 // assumed to be an initial node
 689 
 690                 $nextNode=$this->swGetWorkflowSource()->getNodeDefinition(
 691                     $nextStatus,
 692                     $this->swGetDefaultWorkflowId()
 693                 );
 694                                     
 695                 if( $this->swIsInitialStatus($nextNode) == false)
 696                     throw new SWException('status is not initial : '.$nextNode->toString(),
 697                         SWException::SW_ERR_STATUS_UNREACHABLE);
 698                 
 699                 $this->onEnterWorkflow(
 700                     new SWEvent($this->getOwner(),null,$nextNode)
 701                 );
 702                 $this->_updateStatus($nextNode);
 703                 $this->_updateOwnerStatus($nextNode);
 704                 $bResult = true;
 705             }
 706             elseif( $this->swHasStatus() == true && $nextStatus != null)
 707             {
 708                 // perform transition //////////////////////////////////////////////////////////////
 709                 
 710                 $nextNode=$this->swGetWorkflowSource()->getNodeDefinition(
 711                     $nextStatus,
 712                     $this->swGetWorkflowId()
 713                 );
 714                     
 715                 if( $this->swIsNextStatus($nextNode) )
 716                 {
 717                     $event=new SWEvent($this->getOwner(),$this->_status,$nextNode);
 718                         
 719                     $this->onBeforeTransition($event);
 720                     $this->onProcessTransition($event);
 721                         
 722                     $this->_runTransition($this->_status,$nextNode,$params);
 723                     
 724                     $this->_updateStatus($nextNode);
 725                     $this->_updateOwnerStatus($nextNode);
 726                     
 727                     $this->onAfterTransition($event);
 728                         
 729                     if($this->swIsFinalStatus()){
 730                         $this->onFinalStatus($event);
 731                     }
 732                     $bResult = true;
 733                 }
 734                 elseif( $nextNode->equals($this->swGetStatus()) == false)
 735                 {
 736                     throw new SWException('no transition between current and next status : '
 737                         .$this->swGetStatus()->toString().' -> '. $nextNode->toString(),
 738                         SWException::SW_ERR_STATUS_UNREACHABLE);
 739                 }
 740                 // else
 741                 //      there is not transition between both status but as they are identical, no operation
 742                 //      should be performed.
 743             }
 744         } catch (CException $e) {
 745             $this->_unlock();
 746             Yii::log('set status failed : '.$e->getMessage(),CLogger::LEVEL_ERROR,self::SW_LOG_CATEGORY);
 747             throw $e;
 748         }
 749         $this->_unlock();
 750         return $bResult;
 751     }
 752     
 753     ///////////////////////////////////////////////////////////////////////////////////////
 754     // Events
 755     //
 756     
 757     /**
 758      * Attach event handlers.
 759      * The behavior registers its own mandatory event handlers in case the owner model is a CActiveRecord instance.
 760      * <ul>
 761      *  <li>onBeforeSave : perform status validation and update if needed. If configured, a task is also executed</li>
 762      *  <li>onAfterSave : if configured a task is executed</li>
 763      *  <li>onAfterFind : initialize internal status value</li>
 764      * </ul>
 765      * Additionnally, the behavior will fire custom events on various steps of the owner model life-cycle within its workflow :
 766      * <ul>
 767         <li>onEnterWorkflow : the owner model is inserted in a workflow. Its status is now the initial status of the workflow</li>
 768         <li>onFinalStatus : the owner model is in a status with no out going edge.</li>
 769         <li>onLeaveWorkflow : the owner model status is set to NULL. This is possible only if the model is in a final status</li>
 770         <li>onBeforeTransition : the owner model is about to change status</li>
 771         <li>onProcessTransition : the owner model is changing status</li>
 772         <li>onAfterTransition : the owner model has changed status</li>
 773      </ul>
 774      * @see base/CBehavior::events()
 775      */
 776     public function events()
 777     {
 778         // this behavior could be attached to a CComponent based class other
 779         // than CActiveRecord.
 780         
 781         if($this->getOwner() instanceof CActiveRecord){
 782             $ev=array(
 783                 'onBeforeSave'=> 'beforeSave',
 784                 'onAfterSave' => 'afterSave',
 785                 'onAfterFind' => 'afterFind'
 786             );
 787         } else {
 788             $ev=array();
 789         }
 790         
 791         if($this->swIsEventEnabled())
 792         {
 793             $this->getOwner()->attachEventHandler('onEnterWorkflow',array($this->getOwner(),'enterWorkflow'));
 794             $this->getOwner()->attachEventHandler('onBeforeTransition',array($this->getOwner(),'beforeTransition'));
 795             $this->getOwner()->attachEventHandler('onAfterTransition',array($this->getOwner(),'afterTransition'));
 796             $this->getOwner()->attachEventHandler('onProcessTransition',array($this->getOwner(),'processTransition'));
 797             $this->getOwner()->attachEventHandler('onFinalStatus',array($this->getOwner(),'finalStatus'));
 798             $this->getOwner()->attachEventHandler('onLeaveWorkflow',array($this->getOwner(),'leaveWorkflow'));
 799             $ev=array_merge($ev, array(
 800                 // Custom events
 801                 'onEnterWorkflow'    => 'enterWorkflow',
 802                 'onBeforeTransition' => 'beforeTransition',
 803                 'onProcessTransition'=> 'processTransition',
 804                 'onAfterTransition'  => 'afterTransition',
 805                 'onFinalStatus'      => 'finalStatus',
 806                 'onLeaveWorkflow'    => 'leaveWorkflow',
 807             ));
 808         }
 809         return $ev;
 810     }
 811     /**
 812      * Depending on the value of the owner status attribute, and the current status, this method performs an
 813      * actual transition.
 814      *
 815      * @param Event $event
 816      * @return boolean
 817      */
 818     public function beforeSave($event)
 819     {
 820         $this->_beforeSaveInProgress = true;
 821 
 822         $ownerStatus = $this->getOwner()->{$this->statusAttribute};
 823         if( $ownerStatus == null &&  $this->swHasStatus() == false )
 824         {
 825             if($this->autoInsert == true)
 826                 $this->swNextStatus();  // insert into workflow
 827         }
 828         else
 829         {
 830             $this->swNextStatus($ownerStatus);
 831         }
 832 
 833         $this->_beforeSaveInProgress = false;
 834         return true;
 835     }
 836     /**
 837      * When option transitionBeforeSave is false, if a task is associated with
 838      * the transition that was performed, it is executed now, that it after the activeRecord
 839      * owner component has been saved. The onAfterTransition is also raised.
 840      *
 841      * @param SWEvent $event
 842      */
 843     public function afterSave($event)
 844     {
 845         if( $this->_delayedTransition != null )
 846         {
 847             $tr=$this->_delayedTransition;
 848             $this->_delayedTransition=null;
 849             $this->getOwner()->evaluateExpression($tr);
 850         }
 851         
 852         foreach ($this->_delayedEvent as $delayedEvent) {
 853             $this->_raiseEvent($delayedEvent['name'],$delayedEvent['objEvent']);
 854         }
 855         $this->_delayedEvent=array();
 856     }
 857     /**
 858      * Responds to {@link CActiveRecord::onAfterFind} event.
 859      * This method is called when a CActiveRecord instance is created from DB access (model
 860      * read from DB). At this time, the worklow behavior must be initialized.
 861      *
 862      * @param CEvent event parameter
 863      */
 864     public function afterFind($event)
 865     {
 866         if( !$this->getEnabled())
 867             return;
 868             
 869         try{
 870             // call _init here because 'afterConstruct' is not called when an AR is created
 871             // as the result of a query, and we need to initialize the behavior.
 872         
 873             $status=$this->getOwner()->{$this->statusAttribute};
 874 
 875             if( $status != null )
 876             {
 877                 // the owner model already has a status value (it has been read from db)
 878                 // and so, set the underlying status value without performing any transition
 879                 
 880                 $st=$this->swGetWorkflowSource()->getNodeDefinition($status,$this->swGetWorkflowId());
 881                 $this->_updateStatus($st);
 882             }
 883             
 884         }catch(SWException $e){
 885             Yii::log('failed to set status : '.$status. 'message : '.$e->getMessage(), CLogger::LEVEL_ERROR, self::SW_LOG_CATEGORY);
 886         }
 887     }
 888     /**
 889      * Log event fired
 890      *
 891      * @param string $ev event name
 892      * @param SWNode $source
 893      * @param SWNode $dest
 894      */
 895     private function _logEventFire($ev,$source,$dest)
 896     {
 897         Yii::log(Yii::t('simpleWorkflow','event fired : \'{event}\' status [{source}] -> [{destination}]',
 898             array(
 899                 '{event}'       => $ev,
 900                 '{source}'      => ( $source == null ?'null':$source),
 901                 '{destination}' => $dest,
 902             )),
 903             CLogger::LEVEL_INFO,
 904             self::SW_LOG_CATEGORY
 905         );
 906     }
 907     private function _raiseEvent($evName,$event)
 908     {
 909         if( $this->swIsEventEnabled() ){
 910             $this->_logEventFire($evName, $event->source, $event->destination);
 911             $this->getOwner()->raiseEvent($evName, $event);
 912         }
 913     }
 914     /**
 915      * Default implementation for the onEnterWorkflow event.<br/>
 916      * This method is dedicated to be overloaded by custom event handler.
 917      * @param SWEvent the event parameter
 918      */
 919     public function enterWorkflow($event)
 920     {
 921     }
 922     /**
 923      * This event is raised after the record instance is inserted into a workflow. This may occur
 924      * at construction time (new) if the behavior is initialized with autoInsert set to TRUE and in this
 925      * case, the 'onEnterWorkflow' event is always fired. Consequently, when a model instance is created
 926      * from database (find), the onEnterWorkflow is fired even if the record has already be inserted
 927      * in a workflow (e.g contains a valid status).
 928      *
 929      * @param SWEvent the event parameter
 930      */
 931     public function onEnterWorkflow($event)
 932     {
 933         $this->_raiseEvent('onEnterWorkflow',$event);
 934     }
 935     /**
 936      * Default implementation for the onEnterWorkflow event.<br/>
 937      * This method is dedicated to be overloaded by custom event handler.
 938      * @param SWEvent the event parameter
 939      */
 940     public function leaveWorkflow($event)
 941     {
 942     }
 943     /**
 944      * This event is raised after the record instance is removed from a workflow.
 945      * This occures when the owner status attribut is set to NULL, for instance by calling
 946      * $c->swNextStatus()
 947      *
 948      * @param SWEvent the event parameter
 949      */
 950     public function onLeaveWorkflow($event)
 951     {
 952         $this->_raiseEvent('onLeaveWorkflow',$event);
 953     }
 954     /**
 955      * Default implementation for the onBeforeTransition event.<br/>
 956      * This method is dedicated to be overloaded by custom event handler.
 957      * @param SWEvent the event parameter
 958      */
 959     public function beforeTransition($event)
 960     {
 961     }
 962     /**
 963      * This event is raised before a workflow transition is applied to the owner instance.
 964      *
 965      * @param SWEvent the event parameter
 966      */
 967     public function onBeforeTransition($event)
 968     {
 969         $this->_raiseEvent('onBeforeTransition',$event);
 970     }
 971     /**
 972      * Default implementation for the onProcessTransition event.<br/>
 973      * This method is dedicated to be overloaded by custom event handler.
 974      * @param SWEvent the event parameter
 975      */
 976     public function processTransition($event)
 977     {
 978     }
 979     /**
 980      * This event is raised when a workflow transition is in progress. In such case, the user may
 981      * define a handler for this event in order to run specific process.<br/>
 982      * Depending on the <b>'transitionBeforeSave'</b> initialization parameters, this event could be
 983      * fired before or after the owner model is actually saved to the database. Of course this only
 984      * applies when status change is initiated when saving the record. A call to swNextStatus()
 985      * is not affected by the 'transitionBeforeSave' option.
 986      *
 987      * @param SWEvent the event parameter
 988      */
 989     public function onProcessTransition($event)
 990     {
 991         if( $this->transitionBeforeSave || $this->_beforeSaveInProgress == false){
 992             $this->_raiseEvent('onProcessTransition',$event);
 993         }else {
 994             $this->_delayedEvent[]=array('name'=> 'onProcessTransition','objEvent'=>$event);
 995         }
 996     }
 997     /**
 998      * Default implementation for the onAfterTransition event.<br/>
 999      * This method is dedicated to be overloaded by custom event handler.
1000      *
1001      * @param SWEvent the event parameter
1002      */
1003     public function afterTransition($event)
1004     {
1005     }
1006     /**
1007      * This event is raised after the onProcessTransition is fired. It is the last event fired
1008      * during a non-final transition.<br/>
1009      * Again, in the case of an AR being saved, this event may be fired before or after the record
1010      * is actually save, depending on the <b>'transitionBeforeSave'</b> initialization parameters.
1011      *
1012      * @param SWEvent the event parameter
1013      */
1014     public function onAfterTransition($event)
1015     {
1016         if( $this->transitionBeforeSave || $this->_beforeSaveInProgress == false){
1017             $this->_raiseEvent('onAfterTransition',$event);
1018         }else {
1019             $this->_delayedEvent[]=array('name'=> 'onAfterTransition','objEvent'=>$event);
1020         }
1021     }
1022     /**
1023      * Default implementation for the onFinalStatus event.<br/>
1024      * This method is dedicated to be overloaded by custom event handler.
1025      * @param SWEvent the event parameter
1026      */
1027     public function finalStatus($event)
1028     {
1029     }
1030     /**
1031      * This event is raised at the end of a transition, when the destination status is a
1032      * final status (i.e the owner model has reached a status from where it will not be able
1033      * to move).
1034      *
1035      * @param SWEvent the event parameter
1036      */
1037     public function onFinalStatus($event)
1038     {
1039         if( $this->transitionBeforeSave || $this->_beforeSaveInProgress == false){
1040             $this->_raiseEvent('onFinalStatus',$event);
1041         }else {
1042             $this->_delayedEvent[]=array('name'=> 'onFinalStatus','objEvent'=>$event);
1043         }
1044     }
1045 }
1046 ?>
simpleWorkflow API documentation generated by ApiGen 2.8.0