1 <?php
2 /**
3 * This class implements a graph node for the simpleWorkflow extension.
4 */
5 class SWNode extends CComponent
6 {
7 /**
8 * @var string workflow identifier
9 */
10 private $_workflowId;
11 /**
12 * @var string node identifier which must be unique within the workflow
13 */
14 private $_id;
15 /**
16 * @var string user friendly node name. If not provided at construction, the string
17 * 'workflowId/nodeId' will be used.
18 */
19 private $_label;
20 /**
21 * @var string expression evaluated in the context of an CActiveRecord object. It must returns
22 * a boolean value that is used to allow access to this node.
23 */
24 private $_constraint = array();
25 /**
26 * @var array
27 */
28 private $_metadata = array();
29 /**
30 * @var array array of transitions that exist between this node and other nodes
31 */
32 private $_tr=array();
33
34 /**
35 * Creates a workflow node instance.
36 * If no workflowId is specified in the nodeId, then the $defaultWorkflowId is used.<br/>
37 * Note that both workflow and node id must begin with a alphabetic character followed by aplha-numeric
38 * characters : all other characters are not accepted and cause an exception to be thrown (see {@link SWNode::parseNodeId()})
39 *
40 * @param mixed $node If a string is passed as argument, it can be both in format workflowId/NodeId
41 * or simply 'nodeId'. In this last case, argument $defaultWorkflowIs must be provided, otherwise it is
42 * ignored. <br/>
43 * The $node argument may also be provided as an associative array, with the following structure :<br/>
44 * <pre>
45 * {
46 * 'id' => string, // mandatory
47 * 'label' => string , // optional
48 * 'constraint' => string, // optional
49 * 'transition' => array, // optional
50 * 'metadata' => array, // optional
51 * }
52 * </pre>
53 * Again, the 'id' value may contain a workflow id (e.g 'workflowId/nodeId') but if it's not the case then
54 * the second argument $defaultWorkflowId must be provided.
55 * @param string defaultWorkflowId workflow Id that is used each time a workflow is needed to complete
56 * a status name.
57 */
58 public function __construct($node, $defaultWorkflowId=null)
59 {
60 if($node==null || empty($node))
61 throw new SWException('illegal argument exception : $node cannot be empty', SWException::SW_ERR_CREATE_NODE);
62
63 $st=array();
64
65 if( $node instanceof SWNode )
66 {
67 // copy constructor : does not copy transitions, constraints and metadata
68
69 $this->_workflowId = $node->getWorkflowId();
70 $this->_id = $node->getId();
71 $this->_label = $node->getLabel();
72 $this->_metadata = $node->getMetadata();
73 }
74 else {
75 if( is_array($node))
76 {
77 if(!isset($node['id']))
78 throw new SWException('missing node id',SWException::SW_ERR_MISSING_NODE_ID);
79
80 // set node id -----------------------
81
82 $st=$this->parseNodeId($node['id'],$defaultWorkflowId);
83
84 if(isset($node['label'])){
85 $this->_label=$node['label'];
86 }
87
88 if(isset($node['constraint'])){
89 $this->_constraint=$node['constraint'];
90 }
91
92 if(isset($node['transition'])){
93 $this->_loadTransition($node['transition'],$st['workflow']);
94 }
95
96 if(isset($node['metadata'])){
97 $this->_metadata = $node['metadata'];
98 }
99 }
100 elseif(is_string($node))
101 {
102 $st=$this->parseNodeId($node,$defaultWorkflowId);
103 }
104
105 $this->_workflowId = $st['workflow'];
106 $this->_id = $st['node'];
107
108 if(!isset($this->_label))
109 $this->_label=$this->_id;
110 }
111 }
112 /**
113 * Parse a status name and return it as an array. The string passed as argument
114 * may be a complete status name (e.g workflowId/nodeId) and if no workflowId is
115 * specified, then an exception is thrown. Both workflow and node ids must match
116 * following pattern:
117 * <pre>
118 * /^[[:alpha:]][[:alnum:]_]*$/
119 * </pre>
120 * For instance :
121 * <ul>
122 * <li>ready : matches</li>
123 * <li>to_process : matches</li>
124 * <li>priority_1 : matches</li>
125 * <li>2_level : does not match</li>
126 * <li>to-production : does not match</li>
127 * <li>enable/disable : does not match</li>
128 *</ul>
129 * @param string status status name (wfId/nodeId or nodeId)
130 * @return array the complete status (e.g array ( [workflow] => 'a' [node] => 'b' ))
131 */
132 public function parseNodeId($status,$workflowId)
133 {
134 $nodeId=$wfId=null;
135
136 if(strstr($status,'/')){
137 if(preg_match('/^([[:alpha:]][[:alnum:]_]*)\/([[:alpha:]][[:alnum:]_]*)$/',$status,$matches) == 1){
138 $wfId = $matches[1];
139 $nodeId = $matches[2];
140 }
141 }
142 else{
143 if(preg_match('/^[[:alpha:]][[:alnum:]_]*$/',$status) == 1){
144 $nodeId = $status;
145 if(preg_match('/^[[:alpha:]][[:alnum:]_]*$/',$workflowId) == 1){
146 $wfId = $workflowId;
147 }
148 }
149 }
150
151 if( $wfId == null || $nodeId == null){
152 throw new SWException('failed to create node from node Id = '.$status.', workflow Id = '.$workflowId, SWException::SW_ERR_CREATE_NODE);
153 }
154 return array('workflow'=>$wfId,'node'=>$nodeId);
155 }
156 /**
157 * Overrides the default magic method defined at the CComponent level in order to
158 * return a metadata value if parent method fails.
159 *
160 * @see CComponent::__get()
161 */
162 public function __get($name)
163 {
164 try{
165 return parent::__get($name);
166 }catch(CException $e){
167
168 if(isset($this->_metadata[$name])){
169 return $this->_metadata[$name];
170 }else{
171 throw new SWException('Property "'.$name.'" is not found.',SWException::SW_ERR_ATTR_NOT_FOUND);
172 }
173 }
174 }
175 /**
176 * Loads the set of transitions passed as argument.
177 *
178 * @param mixed $tr if provided as a string, it is a comma separated list of SWNodes id,
179 * This list can also be provided as an array
180 * @param string $defWfId Default workflow Id if nodes have no workflow id, this value is used
181 * as their workflow id.
182 */
183 private function _loadTransition($tr, $defWfId)
184 {
185 if( is_string($tr))
186 {
187 $trAr=explode(',',$tr);
188 foreach($trAr as $aTr)
189 {
190 $objNode=new SWNode(trim($aTr),$defWfId);
191 $this->_tr[$objNode->toString()]=null;
192 }
193 }
194 elseif( is_array($tr))
195 {
196 foreach($tr as $key => $value){
197 if( is_string($key)){
198 $objNode=new SWNode(trim($key),$defWfId);
199 if($value!=null)
200 $this->_tr[$objNode->toString()]=$value;
201 else
202 $this->_tr[$objNode->toString()]=null;
203 }else {
204 $objNode=new SWNode(trim($value),$defWfId);
205 $this->_tr[$objNode->toString()]=null;
206 }
207 }
208 }else {
209 throw new SWException(__FUNCTION__. 'incorrect arg type : string or array expected');
210 }
211 }
212
213 //////////////////////////////////////////////////////////////////////////////////////////
214 // accessors
215
216 public function getWorkflowId() {return $this->_workflowId;}
217 public function getId() {return $this->_id;}
218 public function getLabel() {return $this->_label;}
219 public function getNext() {return $this->_tr;}
220 public function getConstraint() {return $this->_constraint;}
221 public function getMetadata() {return $this->_metadata;}
222 public function getNextNodeIds() {return array_keys($this->_tr);}
223 /**
224 * @returns String the task for this transition or NULL if no task is defined
225 * @param mixed $endNode SWNode instance or string that will be converted to SWNode instance (e.g 'workflowId/nodeId')
226 * @throws SWException
227 */
228 public function getTransitionTask($endNode){
229
230 if( ! $endNode instanceof SWNode ){
231 $endNode = new SWNode($endNode, $this->getWorkflowId());
232 }
233 $endNodeId = $endNode->toString();
234
235 return ( isset($this->_tr[$endNodeId])
236 ? $this->_tr[$endNodeId]
237 : null
238 );
239 }
240
241 public function __toString(){
242 return $this->getWorkflowId().'/'.$this->getId();
243 }
244 public function toString(){
245 return $this->__toString();
246 }
247 /**
248 * SWnode comparator method. Note that only the node and the workflow id
249 * members are compared.
250 *
251 * @param mixed SWNode object or string. If a string is provided it is used to create
252 * a new SWNode object.
253 */
254 public function equals($status){
255
256 if( $status instanceof SWNode )
257 {
258 return $status->toString() == $this->toString();
259 }
260 else try{
261 $other=new SWNode($status,$this->getWorkflowId());
262 return $other->equals($this);
263 }catch(Exception $e)
264 {
265 throw new SWException('comparaison error - the value passed as argument (value='.$status.') cannot be converted into a SWNode',$e->getCode());
266 }
267 }
268 }
269 ?>