4 use Zend\ServiceManager\ServiceLocatorAwareInterface;
5 use Zend\ServiceManager\ServiceLocatorInterface;
10 * Abstract workflow to provide global functionality to all workspace workflows.
11 * @author andre.fourie
13 abstract class Workflow implements ServiceLocatorAwareInterface
23 protected $parentFeature;
27 protected $rootEntity;
31 protected $rootIdField;
39 protected $initTrigger;
43 protected $primaryEntity;
67 protected $stateLabel = array();
74 * @var ServiceLocatorInterface
80 * Setup some essentials.
82 public function __construct()
84 if ($this->initTrigger && $this->initState)
86 \Utility\Event::listen($this->initTrigger, $this, 'initEvent');
88 $this->services = \Utility\Registry::getServiceManager();
92 * Event fired for initTrigger.
93 * @param string $eventName
96 public function initEvent($eventName, $data)
98 $this->jobRecord = $data;
99 $this->em->merge($this->jobRecord);
100 $this->changeState($this->initState);
104 * @see \Zend\ServiceManager\ServiceLocatorAwareInterface::setServiceLocator()
106 public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
108 $this->services = $serviceLocator;
112 * @see \Zend\ServiceManager\ServiceLocatorAwareInterface::getServiceLocator()
114 public function getServiceLocator()
116 return $this->services;
120 * Retrieve Doctrine Entity Manager.
121 * @return \Doctrine\ORM\EntityManager
123 public function getEntityManager()
125 $this->em = $this->services->get('doctrine.entitymanager.orm_default');
130 * List all available workflow states.
133 public function listStates()
135 return array_keys($this->stateMap);
139 * List available global tasks.
140 * @param string $state
143 public function listStateTasks($state = 'Global')
145 if (!isset($this->stateMap[$state]))
147 return 'No such workflow state.';
149 return $this->filterTasks(
150 isset($this->stateMap[$state]['Actions'])
151 ? $this->stateMap[$state]['Actions']
153 isset($this->stateMap[$state]['Routes'])
154 ? $this->stateMap[$state]['Routes']
160 * Filter state specific tasks according to specified restrictions.
161 * @param array $tasks
162 * @param array $routes
165 protected function filterTasks(array $tasks, array $routes)
173 #-> Retrieve available tasks.
176 foreach ($tasks as $task => $meta)
178 if (isset($meta['ApplicationRestriction']))
180 is_array($meta['ApplicationRestriction'])
181 || $meta['ApplicationRestriction'] = array($meta['ApplicationRestriction']);
182 if (in_array(APPLICATION, $meta['ApplicationRestriction']))
184 $filtered[$task] = true;
189 $filtered[$task] = true;
192 $result['Actions'] = array_keys($filtered);
194 #-> Retrieve available routes.
197 'Contract' => array()
200 foreach ($routes as $task => $meta)
202 isset($meta['ContractRequired'])
203 ? $filtered['Contract'][$task] = true
204 : $filtered['Direct'][$task] = true;
206 $result['Routes']['Direct'] = array_keys($filtered['Direct']);
207 $result['Routes']['Contract'] = array_keys($filtered['Contract']);
215 * Validate unique value for a specified field.
216 * @param \Workspace\Utility\ServiceInputParams $contract
217 * @return multitype:string unknown NULL
219 public function fieldIsUnique(\Workspace\Utility\ServiceInputParams $contract)
221 if (!isset($this->entityMap[$contract->Group]))
223 return $contract->error(
224 'Invalid `Group` specified.',
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];
238 $fields = is_array($contract->Field)
240 : array($contract->Field);
241 $values = is_array($contract->Value)
243 : array($contract->Value);
244 if ($entityName::ARCHIVE)
246 $where[] = 'c.archived = :archived';
247 $params['archived'] = 0;
249 for ($i = 0; $i < count($fields); $i++)
251 if (false === strpos($fields[$i], ':'))
253 $where[] = 'LOWER(c.' . $fields[$i] . ') = :' . $fields[$i];
254 $params[$fields[$i]] = strtolower($values[$i]);
258 list($function, $fieldName) = explode(':', $fields[$i]);
259 $where[] = $function . '(c.' . $fieldName . ') = :' . $fieldName;
260 $params[$fieldName] = strtolower($values[$i]);
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();
273 * Load job item into internal container.
274 * @param integer $jobId
275 * @param integer|null $rootId
278 public function loadJob($jobId, $rootId = null)
280 if (!is_null($this->jobRecord) && $jobId == $this->jobRecord->id)
284 if (!is_null($jobId))
286 $this->jobRecord = $this->getEntityManager()
287 ->getRepository($this->entityMap[$this->primaryEntity])
292 $em = $this->getEntityManager();
293 $this->jobRecord = $em
294 ->getRepository($this->entityMap[$this->primaryEntity])
296 $this->rootIdField => $em->getReference(
297 $this->entityMap[$this->rootEntity],
302 if (is_null($this->jobRecord))
304 throw new \Exception('Job Item could not be found.');
309 * Set job item in local container.
310 * @param object $jobRecord
312 public function setJob($jobRecord)
314 $this->jobRecord = $jobRecord;
318 * Job item reclaimed by owning workflow.
319 * @param string $namespace
320 * @param object $rootRecord
324 public function reclaim($namespace, $rootRecord)
326 $this->loadJob(null, $rootRecord->id);
327 if (!isset($this->reclaim['Destination']))
329 throw new \Exception('No Reclaim state provided.');
331 return $this->changeState(
332 $this->reclaim['Destination'],
333 $this->jobRecord->jobState
338 * Job item handed back from foreign workflow.
339 * @param string $namespace
340 * @param integer $jobId
344 public function handover($namespace, $jobId, array $data = array())
346 $this->loadJob($jobId);
347 if (!isset($this->stateMap[$this->jobRecord->jobState]['RouteBack']))
349 throw new \Exception('No RouteBack provided for state ' . $this->jobRecord->jobState);
351 if (isset($this->stateMap[$this->jobRecord->jobState]['RouteBack']['State']))
353 $newState = $this->stateMap[$this->jobRecord->jobState]['RouteBack']['State'];
355 elseif ($this->stateMap[$this->jobRecord->jobState]['RouteBack']['Action'])
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);
364 throw new \Exception('No RouteBack provided for state ' . $this->jobRecord->jobState);
366 return $this->changeState(
368 $this->jobRecord->jobState
373 * Get the state for a specified item.
374 * @param integer $jobId
378 protected function getState($jobId)
380 $this->loadJob($jobId);
381 return $this->jobRecord->jobState;
385 * Change the state for the current item.
386 * @param string|array $state
387 * @param string|null $previousState
389 public function changeState($state, $previousState = null)
391 #-> Establish next state and routing data if provided.
392 $routingData = array();
393 if (is_array($state))
395 isset($state['Data'])
396 && $routingData = $state['Data'];
397 $state = $state['Destination'];
399 if (!strpos($state, '.'))
401 throw new \Exception("New state `$state` should be in format `Workflow.State` ($this->namespace)");
403 list($workflow, $state) = explode('.', $state);
404 $previousState = is_null($previousState) && !is_null($this->jobRecord)
405 ? $this->jobRecord->jobState
407 $newState = ('This' == $workflow)
411 #-> Direct reroute on initialization of new state.
412 if (isset($this->stateMap[$newState]['InitRoute']))
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);
421 #-> Initialization for new state.
422 if (isset($this->stateMap[$newState]['Init']))
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);
430 #-> Are we passing control over to another workflow?
431 if ('This' != $workflow)
433 $wf = $this->services->get($workflow);
434 if ('Handover' == $state)
436 $rootIdField = $this->rootIdField;
437 $wf->handover($this->namespace, $this->jobRecord->$rootIdField->id, $routingData);
441 $service = $this->services->get($workflow . '.Service.' . $workflow);
442 $service->setWorkflow($wf);
443 $action = 'init' . $state;
446 $this->namespace . '.' . $previousState,
451 !is_null($previousState)
452 && $this->jobRecord->previousState = $previousState;
453 $this->jobRecord->jobState = isset($this->stateLabel[$newState])
454 ? $this->stateLabel[$newState]
457 return $this->jobRecord->jobState;
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
468 protected function validTask($type, $state, $task, $jobId = null)
470 if (!isset($this->stateMap[$state][$type][$task]))
474 $meta = $this->stateMap[$state][$type][$task];
475 if (isset($meta['ApplicationRestriction']))
477 is_array($meta['ApplicationRestriction'])
478 || $meta['ApplicationRestriction'] = array($meta['ApplicationRestriction']);
479 if (!in_array(APPLICATION, $meta['ApplicationRestriction']))
488 * Retrieve a task contract.
489 * @param string $task
490 * @param integer|null $jobId
491 * @param array $input
492 * @return \Workspace\Contract\AbstractBase
495 public function contractTask($task, $jobId = null, array $input = array())
498 && !isset($input['id'])
499 && $input['id'] = $jobId;
500 $state = is_null($jobId)
502 : $this->getState($jobId);
503 if (!$this->validTask('Actions', $state, $task, $jobId))
505 throw new \Exception('Not a valid task for specified job item. (Id: ' . $jobId . '; State: ' . $state . ')');
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);
515 * Execute a contracted task.
516 * @param string $task
517 * @param integer|null $jobId
518 * @param \Workspace\Utility\ServiceInputParams $contract
521 public function executeTask($task, $jobId = null, \Workspace\Utility\ServiceInputParams $contract)
523 $state = is_null($jobId)
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);
534 * Retrieve a route contract.
535 * @param string $route
536 * @param integer|null $jobId
537 * @param array $input
538 * @return \Workspace\Contract\AbstractBase
541 public function contractRoute($route, $jobId, array $input = array())
544 && $input['id'] = $jobId;
545 $state = is_null($jobId)
547 : $this->getState($jobId);
548 //$contract = new \Workspace\Utility\ServiceInputParams();
549 if (!$this->validTask('Routes', $state, $route, $jobId))
551 throw new \Exception('Not a valid task for specified job item. (Id: ' . $jobId . '; State: ' . $state . ')');
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);
561 * Execute a contracted route.
562 * @param string $route
563 * @param integer|null $jobId
564 * @param \Workspace\Utility\ServiceInputParams $contract
567 public function executeRoute($route, $jobId, \Workspace\Utility\ServiceInputParams $contract)
569 #-> Establish next state for routing.
570 $state = is_null($jobId)
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
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'])))
589 throw new \Exception('No destination state provided by workflow or contracted routing method.');
593 $newState = $this->changeState($nextState);
594 return $contract->success('State changed.', array('State' => $newState));
598 * Execute a direct route.
599 * @param string $route
600 * @param integer|null $jobId
604 public function directRoute($route, $jobId)
606 #-> Check we have a valid request since we don't have a contract.
607 $state = is_null($jobId)
609 : $this->getState($jobId);
610 if (!$this->validTask('Routes', $state, $route, $jobId))
612 throw new \Exception('Not a valid task for specified job item. (Id: ' . $jobId . '; State: ' . $state . ')');
614 list($handler, $action) = explode('.', $route);
615 $contract = new \Workspace\Utility\ServiceInput('Contract');
616 $contract = $contract->pack();
618 #-> Are we reclaiming an item from another workflow?
619 /* if (isset($this->stateMap[$state]['Routes'][$route]['ReclaimFrom']))
621 $service = $this->services->get(
622 $this->stateMap[$state]['Routes'][$route]['ReclaimFrom']
624 $service->setServiceLocator($this->services);
625 $service->reclaim($this->namespace, $this->jobRecord);
629 if ('Workflow' == $handler)
631 $newState = $this->changeState($this->stateMap[$state]['Routes'][$route]);
632 return $contract->success('State changed.', array('State' => $newState));
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;
640 method_exists($service, $action)
641 && $nextState = $service->$action($this->jobRecord, $state);
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'])))
651 throw new \Exception('No destination state provided by workflow or routing method.');
655 $newState = $this->changeState($nextState);
656 return $contract->success('State changed.', array('State' => $newState));