class RulesState

The rules evaluation state.

A rule element may clone the state, so any added variables are only visible for elements in the current PHP-variable-scope.

Hierarchy

Expanded class hierarchy of RulesState

File

includes/rules.state.inc, line 14

View source
class RulesState {
    
    /**
     * Globally keeps the ids of rules blocked due to recursion prevention.
     *
     * @var array
     */
    protected static $blocked = array();
    
    /**
     * The known variables.
     *
     * @var array
     */
    public $variables = array();
    
    /**
     * Holds info about the variables.
     *
     * @var array
     */
    protected $info = array();
    
    /**
     * Keeps wrappers to be saved later on.
     */
    protected $save;
    
    /**
     * Holds the arguments while an element is executed.
     *
     * May be used by the element to easily access the wrapped arguments.
     */
    public $currentArguments;
    
    /**
     * Variable for saving currently blocked configs for serialization.
     */
    protected $currentlyBlocked;
    
    /**
     * Constructs a RulesState object.
     */
    public function __construct() {
        // Use an object in order to ensure any cloned states reference the same
        // save information.
        $this->save = new ArrayObject();
        $this->addVariable('site', FALSE, self::defaultVariables('site'));
    }
    
    /**
     * Adds the given variable to the given execution state.
     */
    public function addVariable($name, $data, $info) {
        $this->info[$name] = $info + array(
            'skip save' => FALSE,
            'type' => 'unknown',
            'handler' => FALSE,
        );
        if (empty($this->info[$name]['handler'])) {
            $this->variables[$name] = rules_wrap_data($data, $this->info[$name]);
        }
    }
    
    /**
     * Runs post-evaluation tasks, such as saving variables.
     */
    public function cleanUp() {
        // Make changes permanent.
        foreach ($this->save
            ->getArrayCopy() as $selector => $wrapper) {
            $this->saveNow($selector);
        }
        unset($this->currentArguments);
    }
    
    /**
     * Block a rules configuration from execution.
     */
    public function block($rules_config) {
        if (empty($rules_config->recursion) && $rules_config->id) {
            self::$blocked[$rules_config->id] = TRUE;
        }
    }
    
    /**
     * Unblock a rules configuration from execution.
     */
    public function unblock($rules_config) {
        if (empty($rules_config->recursion) && $rules_config->id) {
            unset(self::$blocked[$rules_config->id]);
        }
    }
    
    /**
     * Returns whether a rules configuration should be blocked from execution.
     */
    public function isBlocked($rule_config) {
        return !empty($rule_config->id) && isset(self::$blocked[$rule_config->id]);
    }
    
    /**
     * Get the info about the state variables or a single variable.
     */
    public function varInfo($name = NULL) {
        if (isset($name)) {
            return isset($this->info[$name]) ? $this->info[$name] : FALSE;
        }
        return $this->info;
    }
    
    /**
     * Returns whether the given wrapper is saveable.
     */
    public function isSavable($wrapper) {
        return $wrapper instanceof EntityDrupalWrapper && entity_type_supports($wrapper->type(), 'save') || $wrapper instanceof RulesDataWrapperSavableInterface;
    }
    
    /**
     * Returns whether the variable with the given name is an entity.
     */
    public function isEntity($name) {
        $entity_info = entity_get_info();
        return isset($this->info[$name]['type']) && isset($entity_info[$this->info[$name]['type']]);
    }
    
    /**
     * Gets a variable.
     *
     * If necessary, the specified handler is invoked to fetch the variable.
     *
     * @param string $name
     *   The name of the variable to return.
     *
     * @return
     *   The variable or a EntityMetadataWrapper containing the variable.
     *
     * @throws RulesEvaluationException
     *   Throws a RulesEvaluationException in case we have info about the
     *   requested variable, but it is not defined.
     */
    public function &get($name) {
        if (!array_key_exists($name, $this->variables)) {
            // If there is handler to load the variable, do it now.
            if (!empty($this->info[$name]['handler'])) {
                $data = call_user_func($this->info[$name]['handler'], rules_unwrap_data($this->variables), $name, $this->info[$name]);
                $this->variables[$name] = rules_wrap_data($data, $this->info[$name]);
                $this->info[$name]['handler'] = FALSE;
                if (!isset($data)) {
                    throw new RulesEvaluationException('Unable to load variable %name, aborting.', array(
                        '%name' => $name,
                    ), NULL, RulesLog::INFO);
                }
            }
            else {
                throw new RulesEvaluationException('Unable to get variable %name, it is not defined.', array(
                    '%name' => $name,
                ), NULL, RulesLog::ERROR);
            }
        }
        return $this->variables[$name];
    }
    
    /**
     * Apply permanent changes provided the wrapper's data type is saveable.
     *
     * @param $selector
     *   The data selector of the wrapper to save or just a variable name.
     * @param $wrapper
     * @param bool $immediate
     *   Pass FALSE to postpone saving to later on. Else it's immediately saved.
     */
    public function saveChanges($selector, $wrapper, $immediate = FALSE) {
        $info = $wrapper->info();
        if (empty($info['skip save']) && $this->isSavable($wrapper)) {
            $this->save($selector, $wrapper, $immediate);
        }
        elseif (empty($info['skip save']) && isset($info['parent']) && !$wrapper instanceof EntityDrupalWrapper) {
            // Cut of the last part of the selector.
            $selector = implode(':', explode(':', $selector, -1));
            $this->saveChanges($selector, $info['parent'], $immediate);
        }
        return $this;
    }
    
    /**
     * Remembers to save the wrapper on cleanup or does it now.
     */
    protected function save($selector, EntityMetadataWrapper $wrapper, $immediate) {
        // Convert variable names and selectors to both use underscores.
        $selector = strtr($selector, '-', '_');
        if (isset($this->save[$selector])) {
            if ($this->save[$selector][0]
                ->getIdentifier() == $wrapper->getIdentifier()) {
                // The entity is already remembered. So do a combined save.
                $this->save[$selector][1] += self::$blocked;
            }
            else {
                // The wrapper is already in there, but wraps another entity. So first
                // save the old one, then care about the new one.
                $this->saveNow($selector);
            }
        }
        if (!isset($this->save[$selector])) {
            // In case of immediate saving don't clone the wrapper, so saving a new
            // entity immediately makes the identifier available afterwards.
            $this->save[$selector] = array(
                $immediate ? $wrapper : clone $wrapper,
                self::$blocked,
            );
        }
        if ($immediate) {
            $this->saveNow($selector);
        }
    }
    
    /**
     * Saves the wrapper for the given selector.
     */
    protected function saveNow($selector) {
        // Add the set of blocked elements for the recursion prevention.
        $previously_blocked = self::$blocked;
        self::$blocked += $this->save[$selector][1];
        // Actually save!
        $wrapper = $this->save[$selector][0];
        $entity = $wrapper->value();
        // When operating in hook_entity_insert() $entity->is_new might be still
        // set. In that case remove the flag to avoid causing another insert instead
        // of an update.
        if (!empty($entity->is_new) && $wrapper->getIdentifier()) {
            $entity->is_new = FALSE;
        }
        rules_log('Saved %selector of type %type.', array(
            '%selector' => $selector,
            '%type' => $wrapper->type(),
        ));
        $wrapper->save();
        // Restore the state's set of blocked elements.
        self::$blocked = $previously_blocked;
        unset($this->save[$selector]);
    }
    
    /**
     * Merges info from the given state into the existing state.
     *
     * Merges the info about to-be-saved variables from the given state into the
     * existing state. Therefore we can aggregate saves from invoked components.
     * Merged-in saves are removed from the given state, but not-mergeable saves
     * remain there.
     *
     * @param $state
     *   The state for which to merge the to be saved variables in.
     * @param $component
     *   The component which has been invoked, thus needs to be blocked for the
     *   merged in saves.
     * @param $settings
     *   The settings of the element that invoked the component. Contains
     *   information about variable/selector mappings between the states.
     */
    public function mergeSaveVariables(RulesState $state, RulesPlugin $component, $settings) {
        // For any saves that we take over, also block the component.
        $this->block($component);
        foreach ($state->save
            ->getArrayCopy() as $selector => $data) {
            $parts = explode(':', $selector, 2);
            // Adapt the selector to fit for the parent state and move the wrapper.
            if (isset($settings[$parts[0] . ':select'])) {
                $parts[0] = $settings[$parts[0] . ':select'];
                $this->save(implode(':', $parts), $data[0], FALSE);
                unset($state->save[$selector]);
            }
        }
        $this->unblock($component);
    }
    
    /**
     * Returns an entity metadata wrapper as specified in the selector.
     *
     * @param string $selector
     *   The selector string, e.g. "node:author:mail".
     * @param string $langcode
     *   (optional) The language code used to get the argument value if the
     *   argument value should be translated. Defaults to LANGUAGE_NONE.
     *
     * @return EntityMetadataWrapper
     *   The wrapper for the given selector.
     *
     * @throws RulesEvaluationException
     *   Throws a RulesEvaluationException in case the selector cannot be applied.
     */
    public function applyDataSelector($selector, $langcode = LANGUAGE_NONE) {
        $parts = explode(':', str_replace('-', '_', $selector), 2);
        $wrapper = $this->get($parts[0]);
        if (count($parts) == 1) {
            return $wrapper;
        }
        elseif (!$wrapper instanceof EntityMetadataWrapper) {
            throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not wrapped correctly.', array(
                '%selector' => $selector,
            ));
        }
        try {
            foreach (explode(':', $parts[1]) as $name) {
                if ($wrapper instanceof EntityListWrapper || $wrapper instanceof EntityStructureWrapper) {
                    // Make sure we are using the right language. Wrappers might be cached
                    // and have previous langcodes set, so always set the right language.
                    if ($wrapper instanceof EntityStructureWrapper) {
                        $wrapper->language($langcode);
                    }
                    $wrapper = $wrapper->get($name);
                }
                else {
                    throw new RulesEvaluationException('Unable to apply data selector %selector. The specified variable is not a list or a structure: %wrapper.', array(
                        '%selector' => $selector,
                        '%wrapper' => $wrapper,
                    ));
                }
            }
        } catch (EntityMetadataWrapperException $e) {
            // In case of an exception, re-throw it.
            throw new RulesEvaluationException('Unable to apply data selector %selector: %error', array(
                '%selector' => $selector,
                '%error' => $e->getMessage(),
            ));
        }
        return $wrapper;
    }
    
    /**
     * Magic method. Only serialize variables and their info.
     *
     * Additionally we remember currently blocked configs, so we can restore them
     * upon deserialization using restoreBlocks().
     */
    public function __sleep() {
        $this->currentlyBlocked = self::$blocked;
        return array(
            'info',
            'variables',
            'currentlyBlocked',
        );
    }
    
    /**
     * Magic method. Unserialize variables and their info.
     */
    public function __wakeup() {
        $this->save = new ArrayObject();
    }
    
    /**
     * Restores the before-serialization blocked configurations.
     *
     * Warning: This overwrites any possible currently blocked configs. Thus
     * do not invoke this method if there might be evaluations active.
     */
    public function restoreBlocks() {
        self::$blocked = $this->currentlyBlocked;
    }
    
    /**
     * Defines always-available variables.
     *
     * @param $key
     *   (optional)
     */
    public static function defaultVariables($key = NULL) {
        // Add a variable for accessing site-wide data properties.
        $vars['site'] = array(
            'type' => 'site',
            'label' => t('Site information'),
            'description' => t("Site-wide settings and other global information."),
            // Add the property info via a callback making use of the cached info.
'property info alter' => array(
                'RulesData',
                'addSiteMetadata',
            ),
            'property info' => array(),
            'optional' => TRUE,
        );
        return isset($key) ? $vars[$key] : $vars;
    }

}

Members

Title Sort descending Modifiers Object type Summary
RulesState::$blocked protected static property Globally keeps the ids of rules blocked due to recursion prevention.
RulesState::$currentArguments public property Holds the arguments while an element is executed.
RulesState::$currentlyBlocked protected property Variable for saving currently blocked configs for serialization.
RulesState::$info protected property Holds info about the variables.
RulesState::$save protected property Keeps wrappers to be saved later on.
RulesState::$variables public property The known variables.
RulesState::addVariable public function Adds the given variable to the given execution state.
RulesState::applyDataSelector public function Returns an entity metadata wrapper as specified in the selector.
RulesState::block public function Block a rules configuration from execution.
RulesState::cleanUp public function Runs post-evaluation tasks, such as saving variables.
RulesState::defaultVariables public static function Defines always-available variables.
RulesState::get public function Gets a variable.
RulesState::isBlocked public function Returns whether a rules configuration should be blocked from execution.
RulesState::isEntity public function Returns whether the variable with the given name is an entity.
RulesState::isSavable public function Returns whether the given wrapper is saveable.
RulesState::mergeSaveVariables public function Merges info from the given state into the existing state.
RulesState::restoreBlocks public function Restores the before-serialization blocked configurations.
RulesState::save protected function Remembers to save the wrapper on cleanup or does it now.
RulesState::saveChanges public function Apply permanent changes provided the wrapper's data type is saveable.
RulesState::saveNow protected function Saves the wrapper for the given selector.
RulesState::unblock public function Unblock a rules configuration from execution.
RulesState::varInfo public function Get the info about the state variables or a single variable.
RulesState::__construct public function Constructs a RulesState object.
RulesState::__sleep public function Magic method. Only serialize variables and their info.
RulesState::__wakeup public function Magic method. Unserialize variables and their info.