/ */ class Workflow_Model_Transition extends P4Cms_Model { protected static $_fields = array( 'label' => array( 'accessor' => 'getLabel' ), 'workflow' => array( 'accessor' => 'getWorkflow' ), 'fromState' => array( 'accessor' => 'getFromState', 'mutator' => 'setFromState' ), 'toState' => array( 'accessor' => 'getToState', 'mutator' => 'setToState' ), 'conditions' => array( 'accessor' => 'getConditions' ), 'actions' => array( 'accessor' => 'getActions' ), ); /** * Get the current label. * * @return string the current label or id if label is empty or not a string. */ public function getLabel() { $label = $this->_getValue('label'); return is_string($label) && strlen($label) ? $label : (string) $this->getId(); } /** * Return state model that this transition moves from. * * @return Workflow_Model_State state that this transition moves from. */ public function getFromState() { if (!$this->_getValue('fromState') instanceof Workflow_Model_State) { throw new Workflow_Exception( "Cannot get state the transition moves from. No 'from' state has been set." ); } return $this->_getValue('fromState'); } /** * Set state this transition moves from. * * @param Workflow_Model_State $state state model this transition moves from. */ public function setFromState(Workflow_Model_State $state) { // set workflow from the state model $this->_setWorkflowFromState($state); return $this->_setValue('fromState', $state); } /** * Return state model that this transition moves to. * * @return Workflow_Model_State state that this transition moves to. */ public function getToState() { if (!$this->_getValue('toState') instanceof Workflow_Model_State) { throw new Workflow_Exception( "Cannot get state the transition moves to. No 'to' state has been set." ); } return $this->_getValue('toState'); } /** * Set state this transition moves to. * * @param Workflow_Model_State $state state model this transition moves to. */ public function setToState(Workflow_Model_State $state) { // set workflow from the state model $this->_setWorkflowFromState($state); return $this->_setValue('toState', $state); } /** * Determine if conditions are satisfied for this transition and record. * * If this transition has no conditions specified it evaluates as true, * except for content records for which we also verify that the user has * permission to publish content if to-state is 'published' * * There is the option of passing an array of pending values to * consider when evaluating conditions. These would typically come * from a request as the user makes changes to the record. * * @param P4Cms_Record $record the record to evaluate for context. * @param array|null $pending optional - updated values to consider. * @return bool true if conditions for this transition are * satisfied for given record, false otherwise. */ public function areConditionsMetFor(P4Cms_Record $record, array $pending = null) { // perform special checks for content type records if ($record instanceof P4Cms_Content) { // deny if no active user if (!P4Cms_User::hasActive()) { return false; } $user = P4Cms_User::fetchActive(); $toPublish = $this->getToState()->getId() === Workflow_Model_State::PUBLISHED; // deny if to-state is published and user doesn't have permission to publish if ($toPublish && !$user->isAllowed('content', 'publish')) { return false; } // deny if to-state is not published and user doesn't have access // to unpublished content // limit this check to only those entries having the owner, as otherwise // some of valid transitions will be filtered out when adding a new content // (assuming that current user will become the owner of an 'un-owned' content // after save) if (!$toPublish && strlen($record->getOwner()) && !$user->isAllowed('content', 'access-unpublished') && $record->getOwner() !== $user->getId() ) { return false; } } // loop through all conditions and evaluate them in context foreach ($this->getConditions() as $condition) { // early exit if condition is not met if (!$condition->evaluate($this, $record, $pending)) { return false; } } // all conditions are met (or this transition has no conditions) return true; } /** * Invoke actions for this transition on the given record. * * @param P4Cms_Record $record the record to invoke actions on. * @return Workflow_Model_Transition provides fluent interface. */ public function invokeActionsOn(P4Cms_Record $record) { // loop through all actions and invoke them in context foreach ($this->getActions() as $action) { $action->invoke($this, $record); } return $this; } /** * Get a workflow that this transition is part of. * * @return Workflow_Model_Workflow workflow this transition is part of. * @throws Workflow_Exception if no valid workflow has been set. */ public function getWorkflow() { if (!$this->_getValue('workflow') instanceof Workflow_Model_Workflow) { throw new Workflow_Exception( "Cannot get workflow for transition. No workflow has been set." ); } return $this->_getValue('workflow'); } /** * Set a workflow governing this transition from the given state. If * workflow has been already set to this model, it also checks if given * state is goverened by this workflow. * * @param Workflow_Model_State $state state to get workflow from. * @throws Workflow_Exception if workflow governing the state * and this transition are not same. */ protected function _setWorkflowFromState(Workflow_Model_State $state) { // get workflow from the state model and check if it is the same // as workflow set to this transition (if applicable) $workflow = $state->getWorkflow(); if ($this->_getValue('workflow') instanceof Workflow_Model_Workflow && $this->_getValue('workflow') !== $workflow ) { throw new Workflow_Exception( "State must refer to the same workflow as the transition." ); } return $this->_setValue('workflow', $workflow); } /** * Get the conditions placed on this transition. * * @return array a list of condition objects */ public function getConditions() { return $this->_getPlugins('condition'); } /** * Get the actions to invoke when this transition occurs. * * @return array a list of action objects */ public function getActions() { return $this->_getPlugins('action'); } /** * Get the defined plugins of the given type for this transition. * * @param string $type the type of plugins to get * @return array instances of the defined plugins of the given type */ protected function _getPlugins($type) { $definitions = $this->_getValue($type . 's'); if (!is_array($definitions)) { return array(); } $plugins = array(); $loader = Workflow_Module::getPluginLoader($type); foreach ($definitions as $key => $definition) { // get the short name of the plugin class - four cases: // 1. definition is a string, we take that to be the name. // 2. definition is an array with an explicit $type element // (e.g. 'condition' or 'action'), we use this value. // 3. definition is an array without an explicit $type element // and key is a string, we take the key to be the name. // 4. none of the above - we skip it. if (is_string($definition)) { $name = $definition; } else if (isset($definition[$type])) { $name = $definition[$type]; } else if (is_array($definition) && is_string($key)) { $name = $key; } else { continue; } // attempt to resolve the condition class. $class = $loader->load($name, false); // skip invalid plugin classes $interface = 'Workflow_' . ucfirst($type) . 'Interface'; if (!$class || !class_exists($class) || !in_array($interface, class_implements($class)) ) { continue; } // pull options from the definition - options can be specified at // the top-level of the definition, or under 'options' (latter wins) $options = array(); if (is_array($definition)) { $definition['options'] = isset($definition['options']) ? (array) $definition['options'] : array(); $exclude = array($type => null, 'options' => null); $options = array_diff_key($definition, $exclude); $options = array_merge($options, $definition['options']); } // make the plugin object $plugins[] = new $class($options); } return $plugins; } }