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 ?>