initTrigger && $this->initState) { \Utility\Event::listen($this->initTrigger, $this, 'initEvent'); } $this->services = \Utility\Registry::getServiceManager(); } /** * Event fired for initTrigger. * @param string $eventName * @param object $data */ public function initEvent($eventName, $data) { $this->jobRecord = $data; $this->em->merge($this->jobRecord); $this->changeState($this->initState); } /** * @see \Zend\ServiceManager\ServiceLocatorAwareInterface::setServiceLocator() */ public function setServiceLocator(ServiceLocatorInterface $serviceLocator) { $this->services = $serviceLocator; } /** * @see \Zend\ServiceManager\ServiceLocatorAwareInterface::getServiceLocator() */ public function getServiceLocator() { return $this->services; } /** * Retrieve Doctrine Entity Manager. * @return \Doctrine\ORM\EntityManager */ public function getEntityManager() { $this->em = $this->services->get('doctrine.entitymanager.orm_default'); return $this->em; } /** * List all available workflow states. * @return array */ public function listStates() { return array_keys($this->stateMap); } /** * List available global tasks. * @param string $state * @return array */ public function listStateTasks($state = 'Global') { if (!isset($this->stateMap[$state])) { return 'No such workflow state.'; } return $this->filterTasks( isset($this->stateMap[$state]['Actions']) ? $this->stateMap[$state]['Actions'] : array(), isset($this->stateMap[$state]['Routes']) ? $this->stateMap[$state]['Routes'] : array() ); } /** * Filter state specific tasks according to specified restrictions. * @param array $tasks * @param array $routes * @return array */ protected function filterTasks(array $tasks, array $routes) { #-> Shortcut. if (empty($tasks)) { return $tasks; } #-> Retrieve available tasks. $result = array(); $filtered = array(); foreach ($tasks as $task => $meta) { if (isset($meta['ApplicationRestriction'])) { is_array($meta['ApplicationRestriction']) || $meta['ApplicationRestriction'] = array($meta['ApplicationRestriction']); if (in_array(APPLICATION, $meta['ApplicationRestriction'])) { $filtered[$task] = true; } } else { $filtered[$task] = true; } } $result['Actions'] = array_keys($filtered); #-> Retrieve available routes. $filtered = array( 'Direct' => array(), 'Contract' => array() ); foreach ($routes as $task => $meta) { isset($meta['ContractRequired']) ? $filtered['Contract'][$task] = true : $filtered['Direct'][$task] = true; } $result['Routes']['Direct'] = array_keys($filtered['Direct']); $result['Routes']['Contract'] = array_keys($filtered['Contract']); #-> Done. return $result; } /** * Validate unique value for a specified field. * @param \Workspace\Utility\ServiceInputParams $contract * @return multitype:string unknown NULL */ public function fieldIsUnique(\Workspace\Utility\ServiceInputParams $contract) { if (!isset($this->entityMap[$contract->Group])) { return $contract->error( 'Invalid `Group` specified.', 'System Error.' ); } /*$entityName = $this->entityMap[$contract->Group]; $entry = $this->getEntityManager() ->getRepository($entityName) ->findOneBy(array($contract->Field => $contract->Value)); return is_null($entry) ? $contract->success() : $contract->error('Value already used.', 'Value already used.');*/ $entityName = $this->entityMap[$contract->Group]; $where = array(); $params = array(); $fields = is_array($contract->Field) ? $contract->Field : array($contract->Field); $values = is_array($contract->Value) ? $contract->Value : array($contract->Value); if ($entityName::ARCHIVE) { $where[] = 'c.archived = :archived'; $params['archived'] = 0; } for ($i = 0; $i < count($fields); $i++) { if (false === strpos($fields[$i], ':')) { $where[] = 'LOWER(c.' . $fields[$i] . ') = :' . $fields[$i]; $params[$fields[$i]] = strtolower($values[$i]); } else { list($function, $fieldName) = explode(':', $fields[$i]); $where[] = $function . '(c.' . $fieldName . ') = :' . $fieldName; $params[$fieldName] = strtolower($values[$i]); } } $query = $this->getEntityManager() ->createQuery('SELECT c FROM ' . $entityName . ' c WHERE ' . implode(' AND ', $where)) ->setParameters($params); $entry = $query->getOneOrNullResult(); return !is_null($entry) && $contract->Id != $entry->id ? $contract->error('Value already used.', 'Value already used.') : $contract->success(); } /** * Load job item into internal container. * @param integer $jobId * @param integer|null $rootId * @throws \Exception */ public function loadJob($jobId, $rootId = null) { if (!is_null($this->jobRecord) && $jobId == $this->jobRecord->id) { return; } if (!is_null($jobId)) { $this->jobRecord = $this->getEntityManager() ->getRepository($this->entityMap[$this->primaryEntity]) ->find($jobId); } else { $em = $this->getEntityManager(); $this->jobRecord = $em ->getRepository($this->entityMap[$this->primaryEntity]) ->findOneBy(array( $this->rootIdField => $em->getReference( $this->entityMap[$this->rootEntity], $rootId ) )); } if (is_null($this->jobRecord)) { throw new \Exception('Job Item could not be found.'); } } /** * Set job item in local container. * @param object $jobRecord */ public function setJob($jobRecord) { $this->jobRecord = $jobRecord; } /** * Job item reclaimed by owning workflow. * @param string $namespace * @param object $rootRecord * @throws \Exception * @return string */ public function reclaim($namespace, $rootRecord) { $this->loadJob(null, $rootRecord->id); if (!isset($this->reclaim['Destination'])) { throw new \Exception('No Reclaim state provided.'); } return $this->changeState( $this->reclaim['Destination'], $this->jobRecord->jobState ); } /** * Job item handed back from foreign workflow. * @param string $namespace * @param integer $jobId * @throws \Exception * @return string */ public function handover($namespace, $jobId, array $data = array()) { $this->loadJob($jobId); if (!isset($this->stateMap[$this->jobRecord->jobState]['RouteBack'])) { throw new \Exception('No RouteBack provided for state ' . $this->jobRecord->jobState); } if (isset($this->stateMap[$this->jobRecord->jobState]['RouteBack']['State'])) { $newState = $this->stateMap[$this->jobRecord->jobState]['RouteBack']['State']; } elseif ($this->stateMap[$this->jobRecord->jobState]['RouteBack']['Action']) { list($handler, $action) = explode('.', $this->stateMap[$this->jobRecord->jobState]['RouteBack']['Action']); $service = $this->services->get($this->namespace . '.Service.' . $handler); $service->setWorkflow($this); $newState = $service->$action($this->jobRecord, $this->jobRecord->jobState, $data); } else { throw new \Exception('No RouteBack provided for state ' . $this->jobRecord->jobState); } return $this->changeState( $newState, $this->jobRecord->jobState ); } /** * Get the state for a specified item. * @param integer $jobId * @return string * @throws \Exception */ protected function getState($jobId) { $this->loadJob($jobId); return $this->jobRecord->jobState; } /** * Change the state for the current item. * @param string|array $state * @param string|null $previousState */ public function changeState($state, $previousState = null) { #-> Establish next state and routing data if provided. $routingData = array(); if (is_array($state)) { isset($state['Data']) && $routingData = $state['Data']; $state = $state['Destination']; } if (!strpos($state, '.')) { throw new \Exception("New state `$state` should be in format `Workflow.State` ($this->namespace)"); } list($workflow, $state) = explode('.', $state); $previousState = is_null($previousState) && !is_null($this->jobRecord) ? $this->jobRecord->jobState : null; $newState = ('This' == $workflow) ? $state : $workflow; #-> Direct reroute on initialization of new state. if (isset($this->stateMap[$newState]['InitRoute'])) { list($handler, $action) = explode('.', $this->stateMap[$newState]['InitRoute']); $service = $this->services->get($this->namespace . '.Service.' . $handler); $service->setWorkflow($this); $newState = $service->$action($this->jobRecord, $previousState, $routingData); return $this->changeState($newState, $state); } #-> Initialization for new state. if (isset($this->stateMap[$newState]['Init'])) { list($handler, $action) = explode('.', $this->stateMap[$newState]['Init']); $service = $this->services->get($this->namespace . '.Service.' . $handler); $service->setWorkflow($this); $service->$action($this->jobRecord, $previousState, $routingData); } #-> Are we passing control over to another workflow? if ('This' != $workflow) { $wf = $this->services->get($workflow); if ('Handover' == $state) { $rootIdField = $this->rootIdField; $wf->handover($this->namespace, $this->jobRecord->$rootIdField->id, $routingData); } else { $service = $this->services->get($workflow . '.Service.' . $workflow); $service->setWorkflow($wf); $action = 'init' . $state; $service->$action( $this->jobRecord, $this->namespace . '.' . $previousState, $routingData ); } } !is_null($previousState) && $this->jobRecord->previousState = $previousState; $this->jobRecord->jobState = isset($this->stateLabel[$newState]) ? $this->stateLabel[$newState] : $newState; $this->em->flush(); return $this->jobRecord->jobState; } /** * Check if a task is valid for provided context. * @param string $type * @param string $state * @param string $task * @param integer|null $jobId * @return boolean */ protected function validTask($type, $state, $task, $jobId = null) { if (!isset($this->stateMap[$state][$type][$task])) { return false; } $meta = $this->stateMap[$state][$type][$task]; if (isset($meta['ApplicationRestriction'])) { is_array($meta['ApplicationRestriction']) || $meta['ApplicationRestriction'] = array($meta['ApplicationRestriction']); if (!in_array(APPLICATION, $meta['ApplicationRestriction'])) { return false; } } return true; } /** * Retrieve a task contract. * @param string $task * @param integer|null $jobId * @param array $input * @return \Workspace\Contract\AbstractBase * @throws \Exception */ public function contractTask($task, $jobId = null, array $input = array()) { !is_null($jobId) && !isset($input['id']) && $input['id'] = $jobId; $state = is_null($jobId) ? 'Global' : $this->getState($jobId); if (!$this->validTask('Actions', $state, $task, $jobId)) { throw new \Exception('Not a valid task for specified job item. (Id: ' . $jobId . '; State: ' . $state . ')'); } list($handler, $action) = explode('.', $task); $service = $this->services->get($this->namespace . '.Service.' . $handler); $service->setWorkflow($this); $action = 'contract' . $action; return $service->$action($this->jobRecord, $input); } /** * Execute a contracted task. * @param string $task * @param integer|null $jobId * @param \Workspace\Utility\ServiceInputParams $contract * @return array */ public function executeTask($task, $jobId = null, \Workspace\Utility\ServiceInputParams $contract) { $state = is_null($jobId) ? 'Global' : $this->getState($jobId); list($handler, $action) = explode('.', $task); $service = $this->services->get($this->namespace . '.Service.' . $handler); $service->setWorkflow($this); $action = 'execute' . $action; return $service->$action($this->jobRecord, $contract); } /** * Retrieve a route contract. * @param string $route * @param integer|null $jobId * @param array $input * @return \Workspace\Contract\AbstractBase * @throws \Exception */ public function contractRoute($route, $jobId, array $input = array()) { !is_null($jobId) && $input['id'] = $jobId; $state = is_null($jobId) ? 'Global' : $this->getState($jobId); //$contract = new \Workspace\Utility\ServiceInputParams(); if (!$this->validTask('Routes', $state, $route, $jobId)) { throw new \Exception('Not a valid task for specified job item. (Id: ' . $jobId . '; State: ' . $state . ')'); } list($handler, $action) = explode('.', $route); $service = $this->services->get($this->namespace . '.Service.' . $handler); $service->setWorkflow($this); $action = 'contractRoute' . $action; return $service->$action($this->jobRecord, $input); } /** * Execute a contracted route. * @param string $route * @param integer|null $jobId * @param \Workspace\Utility\ServiceInputParams $contract * @return array */ public function executeRoute($route, $jobId, \Workspace\Utility\ServiceInputParams $contract) { #-> Establish next state for routing. $state = is_null($jobId) ? 'Global' : $this->getState($jobId); list($handler, $action) = explode('.', $route); $service = $this->services->get($this->namespace . '.Service.' . $handler); $service->setWorkflow($this); $action = 'executeRoute' . $action; $nextState = $service->$action( $this->jobRecord, $state, $contract ); #-> Resolve destination. (is_null($nextState) || is_bool($nextState) || (is_array($nextState) && !isset($nextState['Destination']))) && isset($this->stateMap[$state]['Routes'][$route]['Destination']) && $nextState = $this->stateMap[$state]['Routes'][$route]['Destination']; if (is_null($nextState) || is_bool($nextState) || (is_array($nextState) && !isset($nextState['Destination']))) { throw new \Exception('No destination state provided by workflow or contracted routing method.'); } #-> Set new state. $newState = $this->changeState($nextState); return $contract->success('State changed.', array('State' => $newState)); } /** * Execute a direct route. * @param string $route * @param integer|null $jobId * @return array * @throws \Exception */ public function directRoute($route, $jobId) { #-> Check we have a valid request since we don't have a contract. $state = is_null($jobId) ? 'Global' : $this->getState($jobId); if (!$this->validTask('Routes', $state, $route, $jobId)) { throw new \Exception('Not a valid task for specified job item. (Id: ' . $jobId . '; State: ' . $state . ')'); } list($handler, $action) = explode('.', $route); $contract = new \Workspace\Utility\ServiceInput('Contract'); $contract = $contract->pack(); #-> Are we reclaiming an item from another workflow? /* if (isset($this->stateMap[$state]['Routes'][$route]['ReclaimFrom'])) { $service = $this->services->get( $this->stateMap[$state]['Routes'][$route]['ReclaimFrom'] ); $service->setServiceLocator($this->services); $service->reclaim($this->namespace, $this->jobRecord); } */ #-> Shortcut. if ('Workflow' == $handler) { $newState = $this->changeState($this->stateMap[$state]['Routes'][$route]); return $contract->success('State changed.', array('State' => $newState)); } #-> Check if we have a routing method. $service = $this->services->get($this->namespace . '.Service.' . $handler); $service->setWorkflow($this); $action = 'directRoute' . $action; $nextState = null; method_exists($service, $action) && $nextState = $service->$action($this->jobRecord, $state); #-> Resolve destination. (is_null($nextState) || is_bool($nextState) || (is_array($nextState) && !isset($nextState['Destination']))) && isset($this->stateMap[$state]['Routes'][$route]['Destination']) && $nextState = $this->stateMap[$state]['Routes'][$route]['Destination']; if (is_null($nextState) || is_bool($nextState) || (is_array($nextState) && !isset($nextState['Destination']))) { throw new \Exception('No destination state provided by workflow or routing method.'); } #-> Set new state. $newState = $this->changeState($nextState); return $contract->success('State changed.', array('State' => $newState)); } }