Transition.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • application/
  • workflow/
  • models/
  • Transition.php
  • View
  • Commits
  • Open Download .zip Download (11 KB)
<?php
/**
 * Model for a transition (permits moving from one state to another).
 *
 * Transitions are defined by the workflow. Each state may have zero or
 * more transitions to other states. Transitions can also define conditions
 * that govern whether or not the transition is allowed. 
 * 
 * See Workflow_Model_Workflow for more details.
 * 
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 */
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;
    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/application/workflow/models/Transition.php
#1 8972 Matt Attaway Initial add of the Chronicle source code