initial commit
[namibia] / module / Workspace / src / Workspace / Workflow.php
1 <?php
2 namespace Workspace;
3
4 use Zend\ServiceManager\ServiceLocatorAwareInterface;
5 use Zend\ServiceManager\ServiceLocatorInterface;
6
7
8
9 /**
10  * Abstract workflow to provide global functionality to all workspace workflows.
11  * @author andre.fourie
12  */
13 abstract class Workflow implements ServiceLocatorAwareInterface
14 {
15
16         /**
17          * @var string
18          */
19         protected $namespace;
20         /**
21          * @var string
22          */
23         protected $parentFeature;
24         /**
25          * @var string
26          */
27         protected $rootEntity;
28         /**
29          * @var string
30          */
31         protected $rootIdField;
32         /**
33          * @var string
34          */
35         protected $initState;
36         /**
37          * @var string
38          */
39         protected $initTrigger;
40         /**
41          * @var string
42          */
43         protected $primaryEntity;
44         /**
45          * @var object
46          */
47         protected $jobRecord;
48         /**
49          * @var array
50          */
51         protected $entityMap;
52         /**
53          * @var array
54          */
55         protected $linkMap;
56         /**
57          * @var array
58          */
59         protected $listen;
60         /**
61          * @var array
62          */
63         protected $reclaim;
64         /**
65          * @var array
66          */
67         protected $stateLabel = array();
68         /**
69          * @var array
70          */
71         protected $stateMap;
72
73         /**
74          * @var ServiceLocatorInterface
75          */
76         protected $services;
77
78
79         /**
80          * Setup some essentials.
81          */
82         public function __construct()
83         {
84                 if ($this->initTrigger && $this->initState)
85                 {
86                         \Utility\Event::listen($this->initTrigger, $this, 'initEvent');
87                 }
88                 $this->services = \Utility\Registry::getServiceManager();
89         }
90
91         /**
92          * Event fired for initTrigger.
93          * @param string $eventName
94          * @param object $data
95          */
96         public function initEvent($eventName, $data)
97         {
98                 $this->jobRecord = $data;
99                 $this->em->merge($this->jobRecord);
100                 $this->changeState($this->initState);
101         }
102
103         /**
104          * @see \Zend\ServiceManager\ServiceLocatorAwareInterface::setServiceLocator()
105          */
106         public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
107         {
108                 $this->services = $serviceLocator;
109         }
110
111         /**
112          * @see \Zend\ServiceManager\ServiceLocatorAwareInterface::getServiceLocator()
113          */
114         public function getServiceLocator()
115         {
116                 return $this->services;
117         }
118
119         /**
120          * Retrieve Doctrine Entity Manager.
121          * @return \Doctrine\ORM\EntityManager
122          */
123         public function getEntityManager()
124         {
125                 $this->em = $this->services->get('doctrine.entitymanager.orm_default');
126                 return $this->em;
127         }
128
129         /**
130          * List all available workflow states.
131          * @return array
132          */
133         public function listStates()
134         {
135                 return array_keys($this->stateMap);
136         }
137
138         /**
139          * List available global tasks.
140          * @param string $state
141          * @return array
142          */
143         public function listStateTasks($state = 'Global')
144         {
145                 if (!isset($this->stateMap[$state]))
146                 {
147                         return 'No such workflow state.';
148                 }
149                 return $this->filterTasks(
150                                 isset($this->stateMap[$state]['Actions'])
151                                         ? $this->stateMap[$state]['Actions']
152                                         : array(),
153                                 isset($this->stateMap[$state]['Routes'])
154                                         ? $this->stateMap[$state]['Routes']
155                                         : array()
156                 );
157         }
158
159         /**
160          * Filter state specific tasks according to specified restrictions.
161          * @param array $tasks
162          * @param array $routes
163          * @return array
164          */
165         protected function filterTasks(array $tasks, array $routes)
166         {
167                 #-> Shortcut.
168                 if (empty($tasks))
169                 {
170                         return $tasks;
171                 }
172
173                 #-> Retrieve available tasks.
174                 $result   = array();
175                 $filtered = array();
176                 foreach ($tasks as $task => $meta)
177                 {
178                         if (isset($meta['ApplicationRestriction']))
179                         {
180                                 is_array($meta['ApplicationRestriction'])
181                                         || $meta['ApplicationRestriction'] = array($meta['ApplicationRestriction']);
182                                 if (in_array(APPLICATION, $meta['ApplicationRestriction']))
183                                 {
184                                         $filtered[$task] = true;
185                                 }
186                         }
187                         else
188                         {
189                                 $filtered[$task] = true;
190                         }
191                 }
192                 $result['Actions'] = array_keys($filtered);
193
194                 #-> Retrieve available routes.
195                 $filtered = array(
196                                 'Direct'   => array(),
197                                 'Contract' => array()
198
199                 );
200                 foreach ($routes as $task => $meta)
201                 {
202                         isset($meta['ContractRequired'])
203                                 ? $filtered['Contract'][$task] = true
204                                 : $filtered['Direct'][$task] = true;
205                 }
206                 $result['Routes']['Direct']   = array_keys($filtered['Direct']);
207                 $result['Routes']['Contract'] = array_keys($filtered['Contract']);
208
209                 #-> Done.
210                 return $result;
211         }
212
213
214         /**
215          * Validate unique value for a specified field.
216          * @param \Workspace\Utility\ServiceInputParams $contract
217          * @return multitype:string unknown NULL
218          */
219         public function fieldIsUnique(\Workspace\Utility\ServiceInputParams $contract)
220         {
221                 if (!isset($this->entityMap[$contract->Group]))
222                 {
223                         return $contract->error(
224                                         'Invalid `Group` specified.',
225                                         'System Error.'
226                         );
227                 }
228                 /*$entityName = $this->entityMap[$contract->Group];
229                 $entry = $this->getEntityManager()
230                         ->getRepository($entityName)
231                         ->findOneBy(array($contract->Field => $contract->Value));
232                 return is_null($entry)
233                         ? $contract->success()
234                         : $contract->error('Value already used.', 'Value already used.');*/
235                 $entityName = $this->entityMap[$contract->Group];
236                 $where  = array();
237                 $params = array();
238                 $fields = is_array($contract->Field)
239                         ? $contract->Field
240                         : array($contract->Field);
241                 $values = is_array($contract->Value)
242                         ? $contract->Value
243                         : array($contract->Value);
244                 if ($entityName::ARCHIVE)
245                 {
246                         $where[] = 'c.archived = :archived';
247                         $params['archived'] = 0;
248                 }
249                 for ($i = 0; $i < count($fields); $i++)
250                 {
251                         if (false === strpos($fields[$i], ':'))
252                         {
253                                 $where[] = 'LOWER(c.' . $fields[$i] . ') = :' . $fields[$i];
254                                 $params[$fields[$i]] = strtolower($values[$i]);
255                         }
256                         else
257                         {
258                                 list($function, $fieldName) = explode(':', $fields[$i]);
259                                 $where[] = $function . '(c.' . $fieldName . ') = :' . $fieldName;
260                                 $params[$fieldName] = strtolower($values[$i]);
261                         }
262                 }
263                 $query = $this->getEntityManager()
264                                           ->createQuery('SELECT c FROM ' . $entityName . ' c WHERE ' . implode(' AND ', $where))
265                                           ->setParameters($params);
266                 $entry = $query->getOneOrNullResult();
267                 return !is_null($entry) && $contract->Id != $entry->id
268                         ? $contract->error('Value already used.', 'Value already used.')
269                         : $contract->success();
270         }
271
272         /**
273          * Load job item into internal container.
274          * @param integer $jobId
275          * @param integer|null $rootId
276          * @throws \Exception
277          */
278         public function loadJob($jobId, $rootId = null)
279         {
280                 if (!is_null($this->jobRecord) && $jobId == $this->jobRecord->id)
281                 {
282                         return;
283                 }
284                 if (!is_null($jobId))
285                 {
286                         $this->jobRecord = $this->getEntityManager()
287                                 ->getRepository($this->entityMap[$this->primaryEntity])
288                                 ->find($jobId);
289                 }
290                 else
291                 {
292                         $em = $this->getEntityManager();
293                         $this->jobRecord = $em
294                                 ->getRepository($this->entityMap[$this->primaryEntity])
295                                 ->findOneBy(array(
296                                                 $this->rootIdField => $em->getReference(
297                                                                 $this->entityMap[$this->rootEntity],
298                                                                 $rootId
299                                                                 )
300                                 ));
301                 }
302                 if (is_null($this->jobRecord))
303                 {
304                         throw new \Exception('Job Item could not be found.');
305                 }
306         }
307
308         /**
309          * Set job item in local container.
310          * @param object $jobRecord
311          */
312         public function setJob($jobRecord)
313         {
314                 $this->jobRecord = $jobRecord;
315         }
316
317         /**
318          * Job item reclaimed by owning workflow.
319          * @param string $namespace
320          * @param object $rootRecord
321          * @throws \Exception
322          * @return string
323          */
324         public function reclaim($namespace, $rootRecord)
325         {
326                 $this->loadJob(null, $rootRecord->id);
327                 if (!isset($this->reclaim['Destination']))
328                 {
329                         throw new \Exception('No Reclaim state provided.');
330                 }
331                 return $this->changeState(
332                                 $this->reclaim['Destination'],
333                                 $this->jobRecord->jobState
334                                 );
335         }
336
337         /**
338          * Job item handed back from foreign workflow.
339          * @param string $namespace
340          * @param integer $jobId
341          * @throws \Exception
342          * @return string
343          */
344         public function handover($namespace, $jobId, array $data = array())
345         {
346                 $this->loadJob($jobId);
347                 if (!isset($this->stateMap[$this->jobRecord->jobState]['RouteBack']))
348                 {
349                         throw new \Exception('No RouteBack provided for state ' . $this->jobRecord->jobState);
350                 }
351                 if (isset($this->stateMap[$this->jobRecord->jobState]['RouteBack']['State']))
352                 {
353                         $newState = $this->stateMap[$this->jobRecord->jobState]['RouteBack']['State'];
354                 }
355                 elseif ($this->stateMap[$this->jobRecord->jobState]['RouteBack']['Action'])
356                 {
357                         list($handler, $action) = explode('.', $this->stateMap[$this->jobRecord->jobState]['RouteBack']['Action']);
358                         $service = $this->services->get($this->namespace . '.Service.' . $handler);
359                         $service->setWorkflow($this);
360                         $newState = $service->$action($this->jobRecord, $this->jobRecord->jobState, $data);
361                 }
362                 else
363                 {
364                         throw new \Exception('No RouteBack provided for state ' . $this->jobRecord->jobState);
365                 }
366                 return $this->changeState(
367                                 $newState,
368                                 $this->jobRecord->jobState
369                                 );
370         }
371
372         /**
373          * Get the state for a specified item.
374          * @param integer $jobId
375          * @return string
376          * @throws \Exception
377          */
378         protected function getState($jobId)
379         {
380                 $this->loadJob($jobId);
381                 return $this->jobRecord->jobState;
382         }
383
384         /**
385          * Change the state for the current item.
386          * @param string|array $state
387          * @param string|null $previousState
388          */
389         public function changeState($state, $previousState = null)
390         {
391                 #-> Establish next state and routing data if provided.
392                 $routingData = array();
393                 if (is_array($state))
394                 {
395                         isset($state['Data'])
396                                 && $routingData = $state['Data'];
397                         $state = $state['Destination'];
398                 }
399                 if (!strpos($state, '.'))
400                 {
401                         throw new \Exception("New state `$state` should be in format `Workflow.State` ($this->namespace)");
402                 }
403                 list($workflow, $state) = explode('.', $state);
404                 $previousState = is_null($previousState) && !is_null($this->jobRecord)
405                         ? $this->jobRecord->jobState
406                         : null;
407                 $newState = ('This' == $workflow)
408                         ? $state
409                         : $workflow;
410
411                 #-> Direct reroute on initialization of new state.
412                 if (isset($this->stateMap[$newState]['InitRoute']))
413                 {
414                         list($handler, $action) = explode('.', $this->stateMap[$newState]['InitRoute']);
415                         $service = $this->services->get($this->namespace . '.Service.' . $handler);
416                         $service->setWorkflow($this);
417                         $newState = $service->$action($this->jobRecord, $previousState, $routingData);
418                         return $this->changeState($newState, $state);
419                 }
420
421                 #-> Initialization for new state.
422                 if (isset($this->stateMap[$newState]['Init']))
423                 {
424                         list($handler, $action) = explode('.', $this->stateMap[$newState]['Init']);
425                         $service = $this->services->get($this->namespace . '.Service.' . $handler);
426                         $service->setWorkflow($this);
427                         $service->$action($this->jobRecord, $previousState, $routingData);
428                 }
429
430                 #-> Are we passing control over to another workflow?
431                 if ('This' != $workflow)
432                 {
433                         $wf = $this->services->get($workflow);
434                         if ('Handover' == $state)
435                         {
436                                 $rootIdField = $this->rootIdField;
437                                 $wf->handover($this->namespace, $this->jobRecord->$rootIdField->id, $routingData);
438                         }
439                         else
440                         {
441                                 $service = $this->services->get($workflow . '.Service.' . $workflow);
442                                 $service->setWorkflow($wf);
443                                 $action = 'init' . $state;
444                                 $service->$action(
445                                                 $this->jobRecord,
446                                                 $this->namespace . '.' . $previousState,
447                                                 $routingData
448                                                 );
449                         }
450                 }
451                 !is_null($previousState)
452                         && $this->jobRecord->previousState = $previousState;
453                 $this->jobRecord->jobState = isset($this->stateLabel[$newState])
454                         ? $this->stateLabel[$newState]
455                         : $newState;
456                 $this->em->flush();
457                 return $this->jobRecord->jobState;
458         }
459
460         /**
461          * Check if a task is valid for provided context.
462          * @param string $type
463          * @param string $state
464          * @param string $task
465          * @param integer|null $jobId
466          * @return boolean
467          */
468         protected function validTask($type, $state, $task, $jobId = null)
469         {
470                 if (!isset($this->stateMap[$state][$type][$task]))
471                 {
472                         return false;
473                 }
474                 $meta = $this->stateMap[$state][$type][$task];
475                 if (isset($meta['ApplicationRestriction']))
476                 {
477                         is_array($meta['ApplicationRestriction'])
478                                 || $meta['ApplicationRestriction'] = array($meta['ApplicationRestriction']);
479                         if (!in_array(APPLICATION, $meta['ApplicationRestriction']))
480                         {
481                                 return false;
482                         }
483                 }
484                 return true;
485         }
486
487         /**
488          * Retrieve a task contract.
489          * @param string $task
490          * @param integer|null $jobId
491          * @param array $input
492          * @return \Workspace\Contract\AbstractBase
493          * @throws \Exception
494          */
495         public function contractTask($task, $jobId = null, array $input = array())
496         {
497                 !is_null($jobId)
498                         && !isset($input['id'])
499                         && $input['id'] = $jobId;
500                 $state = is_null($jobId)
501                         ? 'Global'
502                         : $this->getState($jobId);
503                 if (!$this->validTask('Actions', $state, $task, $jobId))
504                 {
505                         throw new \Exception('Not a valid task for specified job item. (Id: ' . $jobId . '; State: ' . $state . ')');
506                 }
507                 list($handler, $action) = explode('.', $task);
508                 $service = $this->services->get($this->namespace . '.Service.' . $handler);
509                 $service->setWorkflow($this);
510                 $action = 'contract' . $action;
511                 return $service->$action($this->jobRecord, $input);
512         }
513
514         /**
515          * Execute a contracted task.
516          * @param string $task
517          * @param integer|null $jobId
518          * @param \Workspace\Utility\ServiceInputParams $contract
519          * @return array
520          */
521         public function executeTask($task, $jobId = null, \Workspace\Utility\ServiceInputParams $contract)
522         {
523                 $state = is_null($jobId)
524                         ? 'Global'
525                         : $this->getState($jobId);
526                 list($handler, $action) = explode('.', $task);
527                 $service = $this->services->get($this->namespace . '.Service.' . $handler);
528                 $service->setWorkflow($this);
529                 $action = 'execute' . $action;
530                 return $service->$action($this->jobRecord, $contract);
531         }
532
533         /**
534          * Retrieve a route contract.
535          * @param string $route
536          * @param integer|null $jobId
537          * @param array $input
538          * @return \Workspace\Contract\AbstractBase
539          * @throws \Exception
540          */
541         public function contractRoute($route, $jobId, array $input = array())
542         {
543                 !is_null($jobId)
544                         && $input['id'] = $jobId;
545                 $state = is_null($jobId)
546                         ? 'Global'
547                         : $this->getState($jobId);
548                 //$contract = new \Workspace\Utility\ServiceInputParams();
549                 if (!$this->validTask('Routes', $state, $route, $jobId))
550                 {
551                         throw new \Exception('Not a valid task for specified job item. (Id: ' . $jobId . '; State: ' . $state . ')');
552                 }
553                 list($handler, $action) = explode('.', $route);
554                 $service = $this->services->get($this->namespace . '.Service.' . $handler);
555                 $service->setWorkflow($this);
556                 $action = 'contractRoute' . $action;
557                 return $service->$action($this->jobRecord, $input);
558         }
559
560         /**
561          * Execute a contracted route.
562          * @param string $route
563          * @param integer|null $jobId
564          * @param \Workspace\Utility\ServiceInputParams $contract
565          * @return array
566          */
567         public function executeRoute($route, $jobId, \Workspace\Utility\ServiceInputParams $contract)
568         {
569                 #-> Establish next state for routing.
570                 $state = is_null($jobId)
571                         ? 'Global'
572                         : $this->getState($jobId);
573                 list($handler, $action) = explode('.', $route);
574                 $service = $this->services->get($this->namespace . '.Service.' . $handler);
575                 $service->setWorkflow($this);
576                 $action = 'executeRoute' . $action;
577                 $nextState = $service->$action(
578                                                 $this->jobRecord, $state, $contract
579                                                 );
580
581                 #-> Resolve destination.
582                 (is_null($nextState) || is_bool($nextState)
583                  || (is_array($nextState) && !isset($nextState['Destination'])))
584                         && isset($this->stateMap[$state]['Routes'][$route]['Destination'])
585                         && $nextState = $this->stateMap[$state]['Routes'][$route]['Destination'];
586                 if (is_null($nextState) || is_bool($nextState)
587                         || (is_array($nextState) && !isset($nextState['Destination'])))
588                 {
589                         throw new \Exception('No destination state provided by workflow or contracted routing method.');
590                 }
591
592                 #-> Set new state.
593                 $newState = $this->changeState($nextState);
594                 return $contract->success('State changed.', array('State' => $newState));
595         }
596
597         /**
598          * Execute a direct route.
599          * @param string $route
600          * @param integer|null $jobId
601          * @return array
602          * @throws \Exception
603          */
604         public function directRoute($route, $jobId)
605         {
606                 #-> Check we have a valid request since we don't have a contract.
607                 $state = is_null($jobId)
608                         ? 'Global'
609                         : $this->getState($jobId);
610                 if (!$this->validTask('Routes', $state, $route, $jobId))
611                 {
612                         throw new \Exception('Not a valid task for specified job item. (Id: ' . $jobId . '; State: ' . $state . ')');
613                 }
614                 list($handler, $action) = explode('.', $route);
615                 $contract = new \Workspace\Utility\ServiceInput('Contract');
616                 $contract = $contract->pack();
617
618                 #-> Are we reclaiming an item from another workflow?
619                 /* if (isset($this->stateMap[$state]['Routes'][$route]['ReclaimFrom']))
620                 {
621                         $service = $this->services->get(
622                                         $this->stateMap[$state]['Routes'][$route]['ReclaimFrom']
623                                         );
624                         $service->setServiceLocator($this->services);
625                         $service->reclaim($this->namespace, $this->jobRecord);
626                 } */
627
628                 #-> Shortcut.
629                 if ('Workflow' == $handler)
630                 {
631                         $newState = $this->changeState($this->stateMap[$state]['Routes'][$route]);
632                         return $contract->success('State changed.', array('State' => $newState));
633                 }
634
635                 #-> Check if we have a routing method.
636                 $service = $this->services->get($this->namespace . '.Service.' . $handler);
637                 $service->setWorkflow($this);
638                 $action = 'directRoute' . $action;
639                 $nextState = null;
640                 method_exists($service, $action)
641                         && $nextState = $service->$action($this->jobRecord, $state);
642
643                 #-> Resolve destination.
644                 (is_null($nextState) || is_bool($nextState)
645                  || (is_array($nextState) && !isset($nextState['Destination'])))
646                         && isset($this->stateMap[$state]['Routes'][$route]['Destination'])
647                         && $nextState = $this->stateMap[$state]['Routes'][$route]['Destination'];
648                 if (is_null($nextState) || is_bool($nextState)
649                         || (is_array($nextState) && !isset($nextState['Destination'])))
650                 {
651                         throw new \Exception('No destination state provided by workflow or routing method.');
652                 }
653
654                 #-> Set new state.
655                 $newState = $this->changeState($nextState);
656                 return $contract->success('State changed.', array('State' => $newState));
657         }
658
659
660
661 }