PageRenderTime 62ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/www/protected/extensions/simpleWorkflow/SWActiveRecordBehavior.php

https://bitbucket.org/badenkov/demo
PHP | 882 lines | 443 code | 63 blank | 376 comment | 57 complexity | 436cbdbd419e7b8d52ebd8a3fd24d170 MD5 | raw file
Possible License(s): Apache-2.0, MIT, LGPL-2.1, BSD-2-Clause, CC-BY-SA-3.0, BSD-3-Clause
  1. <?php
  2. /**
  3. * This class implements all the logic for the simpleWorkflow extension. It must be
  4. * attached to an object that inherits from the CActiveRecord class. It can be initialized
  5. * with following parameters :<br/>
  6. * <ul>
  7. * <li><b>statusAttribute</b>: (string) name of the active record attribute that is used to stored the
  8. * value of the current status. In the database, this attribute must be defined as a string. By default
  9. * the name 'status' is used.</li>
  10. * <li><b>defaultWorkflow</b>: (string) id for the workflow the active record will be inserted in. Workflow insertion
  11. * may be automatic (see 'autoInsert') or manual, and in this case, it is possible to sepcify a workflow Id different
  12. * from the default workflow Id defined here. If this parameter is not set, then it is assumed to be
  13. * the name of the model, prefixed with 'workflowNamePrefix' defined by the workflow source component.
  14. * By default this value is set to 'sw' and so, for example Model1 is associated with workflow 'swModel1'.
  15. * </li>
  16. * <li><b>autoInsert</b>: (boolean) when true, the active record object is automatically inserted into
  17. * its default workflow. This occurs at the time this behavior is attached to the active record instance.</li>
  18. * <li><b>workflowSourceComponent</b> : (string) name of the simple workflow source component to use with this
  19. * behavior. By ddefault this parameter is set to 'swSource'.</li>
  20. * <li><b>enableEvent</b> : (boolean) when TRUE, events are fired when the owner model evolves in the workflow. Please
  21. * note that even if events are enabled by configuration they could be automatically disabled by the
  22. * behavior if the owner model doesn't support sW events (i.e if it doesn't inherit from SWActiveRecord). By default
  23. * this parameter is set to true.</li>
  24. * <li><b>transitionBeforeSave</b>: (boolean) if a workflow transition is associated with a task, this parameter
  25. * defines whether the task should be executed before or after the owner model is saved. It has no effect
  26. * if the transition is done programatically by a call to swNextStatus, but only if it is done when the
  27. * owner model is saved.</li>
  28. * </ul>
  29. * @author Raoul
  30. *
  31. */
  32. class SWActiveRecordBehavior extends CBehavior {
  33. /**
  34. * @var string column name where status is stored. If this attribute doesn't exist for
  35. * a model, the Workflow behavior is automatically disabled and a warning is logged.<br/>
  36. * default value : 'status'
  37. */
  38. public $statusAttribute = 'status';
  39. /**
  40. * @var string workflow name that should be used by default for the owner model. If no workflow id
  41. * is configured, it is automatically created based on the owner model name, prefixed with
  42. * the SWWorkflowSource->workflowNamePrefix.
  43. * default value : SWWorkflowSource->workflowNamePrefix . ModelName
  44. */
  45. public $defaultWorkflow=null;
  46. /**
  47. * @var boolean if true, the model is automatically inserted in the workflow just after
  48. * construction. Otherwise, it is developer responsability to insert the model in the workflow.<br/>
  49. * default value : true
  50. */
  51. public $autoInsert=true;
  52. /**
  53. * @var string name of the workflow source component to use with this behavior.<br/>
  54. * default value : swSource
  55. */
  56. public $workflowSourceComponent='swSource';
  57. /**
  58. * @var boolean when TRUE, this behavior will fire SW events. Note that even if
  59. * is true, this doesn't garantee that SW events will be fired as another condition is that the owner
  60. * component provides SWEvent handlers.
  61. * default value : true
  62. */
  63. public $enableEvent=true;
  64. /**
  65. * @var boolean (default TRUE) Tells wether transition process and onAfterTransition event should
  66. * occur before, or after the owner active Record is saved.<br/>
  67. * default value : true
  68. */
  69. public $transitionBeforeSave=true;
  70. ///////////////////////////////////////////////////////////////////////////////////////////
  71. // private members
  72. private $_delayedTransition=null; // delayed transition (only when change status occures during save)
  73. private $_delayedEvent=array(); // delayed event stack (only when change status occures during save)
  74. private $_beforeSaveInProgress=false; // prevent delayed event fire when status is changed by a call to swNextStatus
  75. private $_status=null; // internal status for the owner model
  76. private $_wfs; // workflow source component reference
  77. private $_locked=false; // prevent reentrance
  78. //
  79. ///////////////////////////////////////////////////////////////////////////////////////////
  80. /**
  81. * @var string name of the class the owner should inherit from in order for SW events
  82. * to be enabled.
  83. */
  84. protected $eventClassName='SWActiveRecord';
  85. const SW_LOG_CATEGORY='application.simpleWorkflow';
  86. const SW_I8N_CATEGORY='simpleworkflow';
  87. /**
  88. * @return reference to the workflow source used by this behavior
  89. */
  90. private function getSWSource(){
  91. return $this->_wfs;
  92. }
  93. /**
  94. * Checks that the owner component is able to handle workflow events that could be fired
  95. * by this behavior
  96. *
  97. * @param CComponent $owner the owner component attaching this behavior
  98. * @param string $className
  99. * @return bool TRUE if workflow events are fired, FALSE if not.
  100. */
  101. protected function canFireEvent($owner,$className){
  102. return is_a($owner, $className);
  103. }
  104. /**
  105. * If the owner component is inserted into a workflow, this method returns the SWNode object
  106. * that represent this status, otherwise NULL is returned.
  107. *
  108. * @return SWNode the current status or NULL if no status is set
  109. */
  110. public function swGetStatus(){
  111. return $this->_status;
  112. }
  113. /**
  114. * @return bool TRUE if workflow events are fire by this behavior, FALSE if not.
  115. */
  116. public function swIsEventEnabled(){
  117. return $this->enableEvent;
  118. }
  119. /**
  120. * Use this method to find out if the owner component is currently inserted into a workflow.
  121. * This method is equivalent to swGetStatus()!=null.
  122. *
  123. * @return boolean true if the owner model is in a workflow, FALSE otherwise
  124. */
  125. public function swHasStatus(){
  126. return !is_null($this->_status);
  127. }
  128. /**
  129. * acquire the lock in order to avoid reentrance
  130. *
  131. * @throws SWException
  132. */
  133. private function _lock(){
  134. if($this->_locked==true){
  135. throw new SWException(Yii::t(self::SW_I8N_CATEGORY,'re-entrant exception on set status'),
  136. SWException::SW_ERR_REETRANCE
  137. );
  138. }
  139. $this->_locked=true;
  140. }
  141. /**
  142. * Release the lock
  143. */
  144. private function _unlock(){
  145. $this->_locked=false;
  146. }
  147. /**
  148. * Update the owner model attribute configured to store the current status and the internal
  149. * value too.
  150. *
  151. * @param SWnode $SWNode internal status is set to this node
  152. */
  153. private function _updateStatus($SWNode){
  154. if(!is_a($SWNode,'SWNode'))
  155. throw new SWException(Yii::t(self::SW_I8N_CATEGORY,'SWNode object expected'),SWException::SW_ERR_WRONG_TYPE);
  156. Yii::trace('_updateStatus : '.$SWNode->toString(),self::SW_LOG_CATEGORY);
  157. $this->_status=$SWNode;
  158. }
  159. /**
  160. * Returns the current workflow Id the owner component is inserted in, or NULL if the owner
  161. * component is not inserted into a workflow.
  162. *
  163. * @param string current workflow Id or NULL
  164. */
  165. public function swGetWorkflowId() {
  166. return ($this->swHasStatus()?$this->_status->getWorkflowId():null);
  167. }
  168. /**
  169. * Overloads parent attach method so at the time the behavior is about to be
  170. * attached to the owner component, the behavior is initialized.<br/>
  171. * During the initialisation, following actions are performed:<br/>
  172. * <ul>
  173. * <li>Is there a default workflow associated with the owner component ? : if not, and if the
  174. * behavior is initialized with autoInsert set to TRUE, an exception is thrown as it will not be
  175. * possible to insert the component into a workflow.</li>
  176. * <li>If a default workflow is available for the owner component, and if autoInsert is set to TRUE,
  177. * the component is inserted in the initial status of its default workflow.
  178. * </li>
  179. * <li>Check whether or not, workflow events should be enabled, by testing if the owner component
  180. * class inherits from the 'SWComponent' class. </li>
  181. * </ul>
  182. *
  183. * @see base/CBehavior::attach()
  184. */
  185. public function attach($owner){
  186. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  187. if( ! $this->canFireEvent($owner, $this->eventClassName)){
  188. if( $this->swIsEventEnabled()){
  189. // workflow events are enabled by configuration but the owner component is not
  190. // able to handle workflow event : warning
  191. Yii::log(Yii::t(self::SW_I8N_CATEGORY,'events disabled : owner component doesn\'t inherit from {className}',
  192. array('{className}' => $this->eventClassName)),
  193. CLogger::LEVEL_WARNING,self::SW_LOG_CATEGORY);
  194. }
  195. $this->enableEvent=false; // force
  196. }
  197. parent::attach($owner);
  198. $this->_wfs= Yii::app()->{$this->workflowSourceComponent};
  199. $this->initialize();
  200. }
  201. /**
  202. * This method is called to initialize the current owner status. If a default workflow can
  203. * be found and if 'autoInsert' is set to TRUE, the owner component is inserted in the
  204. * worflow now, by calling swInsertToWorkflow().
  205. *
  206. * @throws SWException
  207. */
  208. protected function initialize(){
  209. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  210. if(is_a($this->getOwner(), 'CActiveRecord')){
  211. $statusAttributeCol = $this->getOwner()->getTableSchema()->getColumn($this->statusAttribute);
  212. if(!isset($statusAttributeCol) || $statusAttributeCol->type != 'string' )
  213. {
  214. throw new SWException(Yii::t(self::SW_I8N_CATEGORY,'attribute {attr} not found',
  215. array('{attr}'=>$this->statusAttribute)),SWException::SW_ERR_ATTR_NOT_FOUND);
  216. }
  217. }
  218. $workflow = $this->swGetDefaultWorkflowId();
  219. if($this->autoInsert){
  220. Yii::trace('owner auto-inserted into workflow ',$workflow,self::SW_LOG_CATEGORY);
  221. $this->swInsertToWorkflow($workflow);
  222. }
  223. }
  224. /**
  225. * Finds out what should be the default workflow to use with the owner model.
  226. * A default workflow id in several ways which are explored by this method, in the following order:
  227. * <ul>
  228. * <li>behavior initialization parameter <i>defaultWorkflow</i></li>
  229. * <li>owner component method <i>workflow</i> : if the owner component is able to provide the
  230. * complete workflow, this method will invoke SWWorkflowSource.addWorkflow</li>
  231. * <li>created based on the configured prefix followed by the model class name </li>
  232. * </ul>
  233. * @return string workflow id to use with the owner component or NULL if now workflow was found
  234. */
  235. public function swGetDefaultWorkflowId(){
  236. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  237. if(is_null($this->defaultWorkflow))
  238. {
  239. $workflowName=null;
  240. if(! is_null($this->defaultWorkflow)) {
  241. // the behavior has been initialized with the default workflow name
  242. // that should be used.
  243. $workflowName=$this->defaultWorkflow;
  244. }
  245. elseif(method_exists($this->getOwner(),'workflow'))
  246. {
  247. $wf=$this->getOwner()->workflow();
  248. if( is_array($wf)){
  249. // Cool ! the owner is able to provide its own private workflow definition ...and optionally
  250. // a workflow name too. If no workflow name is provided, the model name is used to
  251. // identity the workflow
  252. $workflowName=(isset($wf['name'])?$wf['name']:
  253. $this->getSWSource()->workflowNamePrefix.get_class($this->getOwner()));
  254. $this->getSWSource()->addWorkflow($wf,$workflowName);
  255. Yii::trace('workflow provided by owner',self::SW_LOG_CATEGORY);
  256. }elseif(is_string($wf)) {
  257. // the owner returned a string considered as its default workflow Id
  258. $workflowName=$wf;
  259. }else {
  260. throw new SWException(Yii::t(self::SW_I8N_CATEGORY, 'incorrect type returned by owner method : string or array expected'),
  261. SWException::SW_ERR_WRONG_TYPE);
  262. }
  263. }else {
  264. // ok then, let's use the owner model name as the workflow name and hope that
  265. // its definition is available in the workflow basePath.
  266. $workflowName=$this->getSWSource()->workflowNamePrefix.get_class($this->getOwner());
  267. }
  268. $this->defaultWorkflow=$workflowName;
  269. Yii::trace('defaultWorkflow : '.$this->defaultWorkflow,self::SW_LOG_CATEGORY);
  270. }
  271. return $this->defaultWorkflow;
  272. }
  273. /**
  274. * Insert the owner component into the workflow whose id is passed as argument.
  275. * If NULL is passed as argument, the default workflow is used.
  276. *
  277. * @param string workflow Id or NULL.
  278. * @throws SWException the owner model is already in a workflow
  279. */
  280. public function swInsertToWorkflow($workflow=null){
  281. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  282. if($this->swHasStatus()){
  283. throw new SWException(Yii::t(self::SW_I8N_CATEGORY,'object already in a workflow'),
  284. SWException::SW_ERR_IN_WORKFLOW);
  285. }
  286. $this->_lock();
  287. try{
  288. $wfName=(is_null($workflow)==true?$this->swGetDefaultWorkflowId():$workflow);
  289. $initialSt=$this->getSWSource()->getInitialNode($wfName);
  290. $this->onEnterWorkflow(
  291. new SWEvent($this->getOwner(),null,$initialSt)
  292. );
  293. $this->_updateStatus($initialSt);
  294. }catch(SWException $e){
  295. $this->_unlock();
  296. throw $e;
  297. }
  298. $this->_unlock();
  299. }
  300. /**
  301. * This method returns a list of nodes that can be actually reached at the time the method is called. To be reachable,
  302. * a transition must exist between the current status and the next status, AND if a constraint is defined, it must be
  303. * evaluated to true.
  304. *
  305. * @return array SWNode object array for all nodes thats can be reached from the current node.
  306. */
  307. public function swGetNextStatus(){
  308. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  309. $n=array();
  310. if($this->swHasStatus()){
  311. $allNxtSt=$this->getSWSource()->getNextNodes($this->_status);
  312. if(!is_null($allNxtSt)){
  313. foreach ( $allNxtSt as $aStatus ) {
  314. if($this->swIsNextStatus($aStatus) == true){
  315. $n[]=$aStatus;
  316. }
  317. }
  318. }
  319. }else{
  320. $n[]=$this->getSWSource()->getInitialNode($this->swGetDefaultWorkflowId());
  321. }
  322. return $n;
  323. }
  324. /**
  325. * Returns all statuses belonging to the workflow the owner component is inserted in. If the
  326. * owner component is not inserted in a workflow, an empty array is returned.
  327. *
  328. * @return array list of SWNode objects.
  329. */
  330. public function swGetAllStatus(){
  331. if(!$this->swHasStatus() or is_null($this->swGetWorkflowId()))
  332. return array();
  333. else
  334. return $this->getSWSource()->getAllNodes($this->swGetWorkflowId());
  335. }
  336. /**
  337. * Checks if the status passed as argument can be reached from the current status. This occurs when
  338. * <br/>
  339. * <ul>
  340. * <li>a transition has be defined in the workflow between those 2 status</li>
  341. * <li>the destination status has a constraint that is evaluated to true in the context of the
  342. * owner model</li>
  343. * </ul>
  344. * Note that if the owner component is not in a workflow, this method returns true if argument
  345. * $nextStatus is the initial status for the workflow associated with the owner model. In other words
  346. * the initial status for a given workflow is considered as the 'next' status, for all component associated
  347. * to this workflow but not inserted in it. Of course, if a constraint is associated with the initial
  348. * status, it must be evaluated to true.
  349. *
  350. * @param mixed nextStatus String or SWNode object for the next status
  351. * @return boolean TRUE if the status passed as argument can be reached from the current status, FALSE
  352. * otherwise.
  353. */
  354. public function swIsNextStatus($nextStatus){
  355. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  356. $bIsNextStatus=false;
  357. // get (create) a SWNode object
  358. $nxtNode=$this->swCreateNode($nextStatus);
  359. if( (! $this->swHasStatus() and $this->swIsInitialStatus($nextStatus)) or
  360. ( $this->swHasStatus() and $this->getSWSource()->isNextNode($this->_status,$nxtNode)) ){
  361. // a workflow initial status is considered as a valid 'next' status from the NULL
  362. // status.
  363. // there is a transition between current and next status,
  364. // now let's see if constraints to actually enter in the next status
  365. // are evaluated to true.
  366. $swNodeNext=$this->getSWSource()->getNodeDefinition($nxtNode);
  367. if($this->_evaluateConstraint($swNodeNext->getConstraint()) == true)
  368. {
  369. $bIsNextStatus=true;
  370. }
  371. else
  372. {
  373. $bIsNextStatus=false;
  374. Yii::trace('constraint evaluation returned FALSE for : '.$swNodeNext->getConstraint(),
  375. self::SW_LOG_CATEGORY
  376. );
  377. }
  378. }
  379. Yii::trace('SWItemBehavior->swIsNextStatus returns : {result}'.($bIsNextStatus==true?'true':'false'),
  380. self::SW_LOG_CATEGORY
  381. );
  382. return $bIsNextStatus;
  383. }
  384. /**
  385. * Creates a node from the string passed as argument. If $str doesn't contain
  386. * a workflow Id, this method uses the workflowId associated with the owner
  387. * model. The node created here doesn't have to exist within a workflow.
  388. *
  389. * @param string $str string status name
  390. * @return SWNode the node
  391. */
  392. public function swCreateNode($str){
  393. return $this->getSWSource()->createSWNode($str,$this->swGetDefaultWorkflowId());
  394. }
  395. /**
  396. * Evaluate the expression passed as argument in the context of the owner
  397. * model and returns the result of evaluation as a boolean value.
  398. */
  399. private function _evaluateConstraint($constraint){
  400. return (is_null($constraint) or
  401. $this->getOwner()->evaluateExpression($constraint) ==true?true:false);
  402. }
  403. /**
  404. * If a expression is attached to the transition, then it is evaluated in the context
  405. * of the owner model, otherwise, the processTransition event is raised. Note that the value
  406. * returned by the expression evaluation is ignored.
  407. */
  408. private function _runTransition($sourceSt,$destSt,$event=null){
  409. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  410. if(!is_null($sourceSt) and is_a($sourceSt,'SWNode')){
  411. $tr=$sourceSt->getTransition($destSt);
  412. Yii::trace('transition process = '.$tr,self::SW_LOG_CATEGORY);
  413. if(!is_null($tr)){
  414. if( $this->transitionBeforeSave){
  415. $this->getOwner()->evaluateExpression($tr);
  416. }else {
  417. $this->_delayedTransition = $tr;
  418. }
  419. }
  420. }
  421. }
  422. /**
  423. * Test if the status passed as argument a final status. If null is passed as argument
  424. * tests if the current status of the owner component is a final status. By definition a final
  425. * status as no outgoing transition to other status.
  426. *
  427. * @param status status to test, or null (will test current status)
  428. * @return boolean TRUE when the owner component is in a final status, FALSE otherwise
  429. */
  430. public function swIsFinalStatus($status=null){
  431. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  432. $workflowId=($this->swHasStatus()?$this->swGetWorkflowId():$this->swGetDefaultWorkflowId());
  433. if(is_null($status)==false){
  434. $swNode=$this->getSWSource()->createSWNode($status,$workflowId);
  435. }elseif($this->swHasStatus() == true) {
  436. $swNode=$this->_status;
  437. }else {
  438. return false;
  439. }
  440. return count($this->getSWSource()->getNextNodes($swNode,$workflowId))===0;
  441. }
  442. /**
  443. * Checks if the status passed as argument, or the current status (if NULL is passed) is the initial status
  444. * of the corresponding workflow. An exception is raised if the owner model is not in a workflow
  445. * and if $status is null.
  446. *
  447. * @param mixed $status
  448. * @return boolean TRUE if the owner component is in an initial status or if $status is the initial
  449. * status for the owner component default workflow.
  450. * @throws SWException
  451. */
  452. public function swIsInitialStatus($status=null){
  453. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  454. // search for initial status associated with the workflow for the owner component
  455. $workflowId=($this->swHasStatus()?$this->_status->getWorkflowId():$this->swGetDefaultWorkflowId());
  456. $swInit=$this->getSWSource()->getInitialNode($workflowId);
  457. // get the node to compare to the initial node found above
  458. if(is_null($status)==false){
  459. $swNode=$this->getSWSource()->createSWNode($status,$workflowId);
  460. }elseif($this->swHasStatus() == true) {
  461. $swNode=$this->_status;
  462. }else {
  463. throw new SWException(Yii::t(self::SW_I8N_CATEGORY,'could not create node'),
  464. SWException::SW_ERR_CREATE_FAILS);
  465. }
  466. return $swInit->equals($swNode); // compare now
  467. }
  468. /**
  469. * Validate the status attribute stored in the owner model. This attribute is valid if : <br/>
  470. * <ul>
  471. * <li>it is not empty</li>
  472. * <li>it contains a valid status name</li>
  473. * <li>this status can be reached from the current status</li>
  474. * <li>or it is equal to the current status (no status change)</li>
  475. * </ul>
  476. * @param string $attribute status attribute name (by default 'status')
  477. * @param mixed $value current value of the status attribute provided as a string or a SWNode object
  478. * @return boolean TRUE if the status attribute contains a valid value, FALSE otherwise
  479. */
  480. public function swValidate($attribute, $value){
  481. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  482. $bResult=false;
  483. try{
  484. if(is_a($value, 'SWNode')){
  485. $swNode=$value;
  486. }else {
  487. $swNode=$this->swCreateNode($value);
  488. }
  489. if($this->swIsNextStatus($value)==false and $swNode->equals($this->swGetStatus()) == false){
  490. $this->getOwner()->addError($attribute,Yii::t(self::SW_I8N_CATEGORY,'not a valid next status'));
  491. }else {
  492. $bResult=true;
  493. }
  494. }catch(SWException $e){
  495. $this->getOwner()->addError($attribute,Yii::t(self::SW_I8N_CATEGORY,'value {node} is not a valid status',array(
  496. '{node}'=>$value)
  497. ));
  498. }
  499. return $bResult;
  500. }
  501. /**
  502. *
  503. * @param mixed $nextStatus
  504. * @return boolean
  505. */
  506. public function swNextStatus($nextStatus=null){
  507. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  508. $bResult=false;
  509. // if no nextStatus is passed, it is assumed that the owner component
  510. // is not in a workflow and so, try to insert it in its associated default
  511. // workflow.
  512. if( ! $this->swHasStatus()){
  513. $this->swInsertToWorkflow();
  514. $bResult=true;
  515. }else {
  516. // $nextStatus may be provided as an array with a 'statusAttribute' key (POST and
  517. // GET arrays for instance)
  518. if( is_array($nextStatus) and isset($nextStatus[$this->statusAttribute])){
  519. $nextStatus=$nextStatus[$this->statusAttribute];
  520. }
  521. // ok, now nextStatus is known. It is time to validate that it can be reached
  522. // from current status, and if yes, perform the status change
  523. try {
  524. $this->_lock();
  525. $workflowId = $this->swGetWorkflowId();
  526. if( $this->swIsNextStatus($nextStatus,$workflowId))
  527. {
  528. // the $nextStatus can be reached from the current status, it is time
  529. // to run the transition.
  530. $newStObj=$this->getSWSource()->getNodeDefinition($nextStatus,$workflowId);
  531. $event=new SWEvent($this->getOwner(),$this->_status,$newStObj);
  532. if( ! $this->swHasStatus()){
  533. $this->onEnterWorkflow($event);
  534. }else {
  535. $this->onBeforeTransition($event);
  536. }
  537. $this->onProcessTransition($event);
  538. $this->_runTransition($this->_status,$newStObj);
  539. $this->_updateStatus($newStObj);
  540. $this->onAfterTransition($event);
  541. if($this->swIsFinalStatus()){
  542. $this->onFinalStatus($event);
  543. }
  544. $bResult=true;
  545. } else {
  546. throw new SWException('status can\'t be reached',SWException::SW_ERR_STATUS_UNREACHABLE);
  547. }
  548. } catch (CException $e) {
  549. $this->_unlock();
  550. Yii::log($e->getMessage(),CLogger::LEVEL_ERROR,self::SW_LOG_CATEGORY);
  551. throw $e;
  552. }
  553. $this->_unlock();
  554. }
  555. return $bResult;
  556. }
  557. ///////////////////////////////////////////////////////////////////////////////////////
  558. // Events
  559. //
  560. /**
  561. *
  562. * @see base/CBehavior::events()
  563. */
  564. public function events()
  565. {
  566. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  567. // this behavior could be attached to a CComponent based class other
  568. // than CActiveRecord.
  569. if(is_a($this->getOwner(), 'CActiveRecord')){
  570. $ev=array(
  571. 'onBeforeSave'=> 'beforeSave',
  572. 'onAfterSave' => 'afterSave',
  573. 'onAfterFind' => 'afterFind',
  574. );
  575. } else {
  576. $ev=array();
  577. }
  578. if($this->swIsEventEnabled())
  579. {
  580. Yii::trace('workflow event enabled',self::SW_LOG_CATEGORY);
  581. $this->getOwner()->attachEventHandler('onEnterWorkflow',array($this->getOwner(),'enterWorkflow'));
  582. $this->getOwner()->attachEventHandler('onBeforeTransition',array($this->getOwner(),'beforeTransition'));
  583. $this->getOwner()->attachEventHandler('onAfterTransition',array($this->getOwner(),'afterTransition'));
  584. $this->getOwner()->attachEventHandler('onProcessTransition',array($this->getOwner(),'processTransition'));
  585. $this->getOwner()->attachEventHandler('onFinalStatus',array($this->getOwner(),'finalStatus'));
  586. $ev=array_merge($ev, array(
  587. // Custom events
  588. 'onEnterWorkflow' => 'enterWorkflow',
  589. 'onBeforeTransition' => 'beforeTransition',
  590. 'onProcessTransition'=> 'processTransition',
  591. 'onAfterTransition' => 'afterTransition',
  592. 'onFinalStatus' => 'finalStatus',
  593. ));
  594. }
  595. return $ev;
  596. }
  597. /**
  598. * Responds to {@link CActiveRecord::onBeforeSave} event.
  599. *
  600. * Overrides this method if you want to handle the corresponding event of the {@link CBehavior::owner owner}.
  601. * You may set {@link CModelEvent::isValid} to be false to quit the saving process.
  602. * @param CModelEvent event parameter
  603. */
  604. public function beforeSave($event)
  605. {
  606. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  607. $this->_beforeSaveInProgress = true;
  608. if(!$this->getOwner()->hasErrors()){
  609. $ownerStatus = $this->getOwner()->{$this->statusAttribute};
  610. if( $this->swIsNextStatus($ownerStatus))
  611. {
  612. $this->swNextStatus($this->getOwner()->{$this->statusAttribute});
  613. $this->getOwner()->{$this->statusAttribute} = $this->swGetStatus()->toString();
  614. Yii::trace(__CLASS__.'.'.__FUNCTION__.'. New status is now : '.$this->swGetStatus()->toString());
  615. }
  616. elseif( ! $this->swGetStatus()->equals($ownerStatus))
  617. {
  618. throw new SWException(Yii::t(self::SW_I8N_CATEGORY,'incorrect status : {status}',
  619. array('{status}'=>$ownerStatus)),SWException::SW_ERR_WRONG_STATUS);
  620. }
  621. } else {
  622. Yii::trace(__CLASS__.'.'.__FUNCTION__.': hasErros');
  623. }
  624. $this->_beforeSaveInProgress = false;
  625. return true;
  626. }
  627. /**
  628. * When option transitionBeforeSave is false, if a task is associated with
  629. * the transition that was performed, it is executed now, that it after the activeRecord
  630. * owner component has been saved. The onAfterTransition is also raised.
  631. *
  632. * @param SWEvent $event
  633. */
  634. public function afterSave($event){
  635. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  636. if(!is_null($this->_delayedTransition)){
  637. Yii::trace('running delayed transition process');
  638. $tr=$this->_delayedTransition;
  639. $this->_delayedTransition=null;
  640. $this->getOwner()->evaluateExpression($tr);
  641. }
  642. foreach ($this->_delayedEvent as $delayedEvent) {
  643. $this->_raiseEvent($delayedEvent['name'],$delayedEvent['objEvent']);
  644. }
  645. $this->_delayedEvent=array();
  646. }
  647. /**
  648. * Responds to {@link CActiveRecord::onAfterFind} event.
  649. * This method is called when a CActiveRecord instance is created from DB access (model
  650. * read from DB). At this time, the worklow behavior must be initialized.
  651. *
  652. * @param CEvent event parameter
  653. */
  654. public function afterFind($event){
  655. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  656. if( !$this->getEnabled())
  657. return;
  658. try{
  659. // call _init here because 'afterConstruct' is not called when an AR is created
  660. // as the result of a query, and we need to initialize the behavior.
  661. $status=$this->getOwner()->{$this->statusAttribute};
  662. if(!is_null($status)){
  663. // the owner model already has a status value (it has been read from db)
  664. // and so, set the underlying status value without performing any transition
  665. $st=$this->getSWSource()->getNodeDefinition($status,$this->swGetWorkflowId());
  666. $this->_updateStatus($st);
  667. }else {
  668. // the owner doesn't have a status : initialize the behavior. This will
  669. // auto-insert the owner into its default workflow if autoInsert was set to TRUE
  670. $this->initialize();
  671. }
  672. }catch(SWException $e){
  673. Yii::log(Yii::t(self::SW_I8N_CATEGORY,'failed to set status : {status}',
  674. array('{status}'=>$status)),
  675. CLogger::LEVEL_WARNING,
  676. self::SW_LOG_CATEGORY
  677. );
  678. Yii::log($e->getMessage(), CLogger::LEVEL_ERROR);
  679. }
  680. }
  681. /**
  682. * Log event fired
  683. *
  684. * @param string $ev event name
  685. * @param SWNode $source
  686. * @param SWNode $dest
  687. */
  688. private function _logEventFire($ev,$source,$dest){
  689. Yii::log(Yii::t('simpleWorkflow','event fired : \'{event}\' status [{source}] -> [{destination}]',
  690. array(
  691. '{event}' => $ev,
  692. '{source}' => (is_null($source)?'null':$source),
  693. '{destination}' => $dest,
  694. )),
  695. CLogger::LEVEL_INFO,
  696. self::SW_LOG_CATEGORY
  697. );
  698. }
  699. private function _raiseEvent($evName,$event){
  700. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  701. if( $this->swIsEventEnabled() ){
  702. $this->_logEventFire($evName, $event->source, $event->destination);
  703. $this->getOwner()->raiseEvent($evName, $event);
  704. }
  705. }
  706. /**
  707. * Default implementation for the onEnterWorkflow event.<br/>
  708. * This method is dedicated to be overloaded by custom event handler.
  709. * @param SWEvent the event parameter
  710. */
  711. public function enterWorkflow($event){
  712. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  713. }
  714. /**
  715. * This event is raised after the record instance is inserted into a workflow. This may occur
  716. * at construction time (new) if the behavior is initialized with autoInsert set to TRUE and in this
  717. * case, the 'onEnterWorkflow' event is always fired. Consequently, when a model instance is created
  718. * from database (find), the onEnterWorkflow is fired even if the record has already be inserted
  719. * in a workflow (e.g contains a valid status).
  720. *
  721. * @param SWEvent the event parameter
  722. */
  723. public function onEnterWorkflow($event){
  724. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  725. $this->_raiseEvent('onEnterWorkflow',$event);
  726. }
  727. /**
  728. * Default implementation for the onBeforeTransition event.<br/>
  729. * This method is dedicated to be overloaded by custom event handler.
  730. * @param SWEvent the event parameter
  731. */
  732. public function beforeTransition($event){
  733. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  734. }
  735. /**
  736. * This event is raised before a workflow transition is applied to the owner instance.
  737. *
  738. * @param SWEvent the event parameter
  739. */
  740. public function onBeforeTransition($event){
  741. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  742. $this->_raiseEvent('onBeforeTransition',$event);
  743. }
  744. /**
  745. * Default implementation for the onProcessTransition event.<br/>
  746. * This method is dedicated to be overloaded by custom event handler.
  747. * @param SWEvent the event parameter
  748. */
  749. public function processTransition($event){
  750. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  751. }
  752. /**
  753. * This event is raised when a workflow transition is in progress. In such case, the user may
  754. * define a handler for this event in order to run specific process.<br/>
  755. * Depending on the <b>'transitionBeforeSave'</b> initialization parameters, this event could be
  756. * fired before or after the owner model is actually saved to the database. Of course this only
  757. * applies when status change is initiated when saving the record. A call to swNextStatus()
  758. * is not affected by the 'transitionBeforeSave' option.
  759. *
  760. * @param SWEvent the event parameter
  761. */
  762. public function onProcessTransition($event){
  763. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  764. if( $this->transitionBeforeSave || $this->_beforeSaveInProgress == false){
  765. $this->_raiseEvent('onProcessTransition',$event);
  766. }else {
  767. $this->_delayedEvent[]=array('name'=> 'onProcessTransition','objEvent'=>$event);
  768. }
  769. }
  770. /**
  771. * Default implementation for the onAfterTransition event.<br/>
  772. * This method is dedicated to be overloaded by custom event handler.
  773. *
  774. * @param SWEvent the event parameter
  775. */
  776. public function afterTransition($event){
  777. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  778. }
  779. /**
  780. * This event is raised after the onProcessTransition is fired. It is the last event fired
  781. * during a non-final transition.<br/>
  782. * Again, in the case of an AR being saved, this event may be fired before or after the record
  783. * is actually save, depending on the <b>'transitionBeforeSave'</b> initialization parameters.
  784. *
  785. * @param SWEvent the event parameter
  786. */
  787. public function onAfterTransition($event){
  788. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  789. if( $this->transitionBeforeSave || $this->_beforeSaveInProgress == false){
  790. $this->_raiseEvent('onAfterTransition',$event);
  791. }else {
  792. $this->_delayedEvent[]=array('name'=> 'onAfterTransition','objEvent'=>$event);
  793. }
  794. }
  795. /**
  796. * Default implementation for the onEnterWorkflow event.<br/>
  797. * This method is dedicated to be overloaded by custom event handler.
  798. * @param SWEvent the event parameter
  799. */
  800. public function finalStatus($event){
  801. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  802. }
  803. /**
  804. * This event is raised at the end of a transition, when the destination status is a
  805. * final status (i.e the owner model has reached a status from where it will not be able
  806. * to move).
  807. *
  808. * @param SWEvent the event parameter
  809. */
  810. public function onFinalStatus($event){
  811. Yii::trace(__CLASS__.'.'.__FUNCTION__,self::SW_LOG_CATEGORY);
  812. if( $this->transitionBeforeSave || $this->_beforeSaveInProgress == false){
  813. $this->_raiseEvent('onFinalStatus',$event);
  814. }else {
  815. $this->_delayedEvent[]=array('name'=> 'onFinalStatus','objEvent'=>$event);
  816. }
  817. }
  818. }
  819. ?>