ContextHandlerTrait.php

Namespace

Drupal\rules\Context

File

src/Context/ContextHandlerTrait.php

View source
<?php

namespace Drupal\rules\Context;

use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Plugin\ContextAwarePluginInterface as CoreContextAwarePluginInterface;
use Drupal\rules\Exception\EvaluationException;
use Drupal\rules\Exception\IntegrityException;

/**
 * Provides methods for handling context based on the plugin configuration.
 *
 * The trait requires the plugin to use configuration as defined by the
 * ContextConfig class.
 *
 * @see \Drupal\rules\Context\ContextConfig
 */
trait ContextHandlerTrait {
    
    /**
     * The data processor plugin manager used to process context variables.
     *
     * @var \Drupal\rules\Context\DataProcessorManager
     */
    protected $processorManager;
    
    /**
     * Prepares plugin context based upon the set context configuration.
     *
     * The plugin is prepared for execution by mapping the variables from the
     * execution state into the plugin context and applying data processors.
     * In addition, it is ensured that all required context is basically
     * available as defined. This include the following checks:
     *  - Required context must have a value set.
     *  - Context may not have NULL values unless the plugin allows it.
     *
     * @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin
     *   The plugin that is populated with context values.
     * @param \Drupal\rules\Context\ExecutionStateInterface $state
     *   The execution state containing available variables.
     *
     * @throws \Drupal\rules\Exception\EvaluationException
     *   Thrown if some context is not satisfied; e.g. a required context is
     *   missing.
     *
     * @see ::prepareContextWithMetadata()
     */
    protected function prepareContext(CoreContextAwarePluginInterface $plugin, ExecutionStateInterface $state) {
        if (isset($this->configuration['context_values'])) {
            foreach ($this->configuration['context_values'] as $name => $value) {
                $plugin->setContextValue($name, $value);
            }
        }
        $selected_data = [];
        // Map context by applying data selectors and collected the definitions of
        // selected data for refining context definitions later. Note, that we must
        // refine context definitions on execution time also, such that provided
        // context gets the right metadata attached.
        if (isset($this->configuration['context_mapping'])) {
            foreach ($this->configuration['context_mapping'] as $name => $selector) {
                $typed_data = $state->fetchDataByPropertyPath($selector);
                $plugin->setContextValue($name, $typed_data);
                $selected_data[$name] = $typed_data->getDataDefinition();
            }
        }
        if ($plugin instanceof ContextAwarePluginInterface) {
            // Getting context values may lead to undocumented exceptions if context
            // is not set right now. So catch those exceptions.
            // @todo Remove once https://www.drupal.org/node/2677162 is fixed in core.
            try {
                $plugin->refineContextDefinitions($selected_data);
            } catch (ContextException $e) {
                if (strpos($e->getMessage(), 'context is required') === FALSE) {
                    throw new EvaluationException($e->getMessage());
                }
            }
        }
        // Apply data processors.
        $this->processData($plugin, $state);
        // Finally, ensure all contexts are set as expected now.
        foreach ($plugin->getContextDefinitions() as $name => $definition) {
            if ($plugin->getContextValue($name) === NULL && $definition->isRequired()) {
                // If a context mapping has been specified, the value might end up NULL
                // but valid (e.g. a reference on an empty property). In that case
                // isAllowedNull determines whether the context is conform.
                if (!isset($this->configuration['context_mapping'][$name])) {
                    throw new EvaluationException("Required context '{$name}' is missing for plugin '" . $plugin->getPluginId() . "'.");
                }
                elseif (!$definition->isAllowedNull()) {
                    throw new EvaluationException("The context for '{$name}' is NULL, but the context '{$name}' in '" . $plugin->getPluginId() . "' requires a value.");
                }
            }
        }
    }
    
    /**
     * Prepares plugin context based upon the set context configuration.
     *
     * The configuration is applied as far as possible without having execution
     * time data. That means, the configured context values are set and context is
     * refined while leveraging the definitions of selected data.
     *
     * @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin
     *   The plugin that is prepared.
     * @param \Drupal\rules\Context\ExecutionMetadataStateInterface $metadata_state
     *   The metadata state, prepared for the current expression.
     *
     * @throws \Drupal\Component\Plugin\Exception\ContextException
     *   Thrown if the plugin tries to access some not-defined context. As this is
     *   a developer error, this should not be caught.
     *
     * @see ::prepareContext()
     */
    protected function prepareContextWithMetadata(CoreContextAwarePluginInterface $plugin, ExecutionMetadataStateInterface $metadata_state) {
        if (isset($this->configuration['context_values'])) {
            foreach ($this->configuration['context_values'] as $name => $value) {
                $plugin->setContextValue($name, $value);
            }
        }
        if ($plugin instanceof ContextAwarePluginInterface) {
            $selected_data = $this->getSelectedData($metadata_state);
            // Getting context values may lead to undocumented exceptions if context
            // is not set right now. So catch those exceptions.
            // @todo Remove once https://www.drupal.org/node/2677162 is fixed in core.
            try {
                $plugin->refineContextDefinitions($selected_data);
            } catch (ContextException $e) {
                if (strpos($e->getMessage(), 'context is required') === FALSE) {
                    throw $e;
                }
            }
        }
    }
    
    /**
     * Gets definitions of all selected data at configuration time.
     *
     * @param \Drupal\rules\Context\ExecutionMetadataStateInterface $metadata_state
     *   The metadata state.
     *
     * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
     *   An array of data definitions for context that is mapped using a data
     *   selector, keyed by context name.
     */
    protected function getSelectedData(ExecutionMetadataStateInterface $metadata_state) {
        $selected_data = [];
        // Collected the definitions of selected data for refining context
        // definitions.
        if (isset($this->configuration['context_mapping'])) {
            // If no state is available, we need to fetch at least the definitions of
            // selected data for refining context.
            foreach ($this->configuration['context_mapping'] as $name => $selector) {
                try {
                    $selected_data[$name] = $this->getMappedDefinition($name, $metadata_state);
                } catch (IntegrityException $e) {
                    // Ignore invalid data selectors here, such that context gets refined
                    // as far as possible still and can be respected by the UI when fixing
                    // broken selectors.
                }
            }
        }
        return $selected_data;
    }
    
    /**
     * Gets the definition of the data that is mapped to the given context.
     *
     * @param string $context_name
     *   The name of the context.
     * @param \Drupal\rules\Context\ExecutionMetadataStateInterface $metadata_state
     *   The metadata state containing metadata about available variables.
     *
     * @return \Drupal\Core\TypedData\DataDefinitionInterface|null
     *   A data definition if the property path could be applied, or NULL if the
     *   context is not mapped.
     *
     * @throws \Drupal\rules\Exception\IntegrityException
     *   Thrown if the data selector that is configured for the context is
     *   invalid.
     */
    protected function getMappedDefinition($context_name, ExecutionMetadataStateInterface $metadata_state) {
        if (isset($this->configuration['context_mapping'][$context_name])) {
            return $metadata_state->fetchDefinitionByPropertyPath($this->configuration['context_mapping'][$context_name]);
        }
    }
    
    /**
     * Adds provided context values from the plugin to the execution state.
     *
     * @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin
     *   The context aware plugin of which to add provided context.
     * @param \Drupal\rules\Context\ExecutionStateInterface $state
     *   The Rules state where the context variables are added.
     */
    protected function addProvidedContext(CoreContextAwarePluginInterface $plugin, ExecutionStateInterface $state) {
        // If the plugin does not support providing context, there is nothing to do.
        if (!$plugin instanceof ContextProviderInterface) {
            return;
        }
        $provides = $plugin->getProvidedContextDefinitions();
        foreach ($provides as $name => $provided_definition) {
            // Avoid name collisions in the rules state: provided variables can be
            // renamed.
            if (isset($this->configuration['provides_mapping'][$name])) {
                $state->setVariableData($this->configuration['provides_mapping'][$name], $plugin->getProvidedContext($name)
                    ->getContextData());
            }
            else {
                $state->setVariableData($name, $plugin->getProvidedContext($name)
                    ->getContextData());
            }
        }
    }
    
    /**
     * Adds the definitions of provided context to the execution metadata state.
     *
     * @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin
     *   The context aware plugin of which to add provided context.
     * @param \Drupal\rules\Context\ExecutionMetadataStateInterface $metadata_state
     *   The execution metadata state to add variables to.
     */
    protected function addProvidedContextDefinitions(CoreContextAwarePluginInterface $plugin, ExecutionMetadataStateInterface $metadata_state) {
        // If the plugin does not support providing context, there is nothing to do.
        if (!$plugin instanceof ContextProviderInterface) {
            return;
        }
        foreach ($plugin->getProvidedContextDefinitions() as $name => $context_definition) {
            if (isset($this->configuration['provides_mapping'][$name])) {
                // Populate the state with the new variable that is provided by this
                // plugin. That is necessary so that the integrity check in subsequent
                // actions knows about the variable and does not throw violations.
                $metadata_state->setDataDefinition($this->configuration['provides_mapping'][$name], $context_definition->getDataDefinition());
            }
            else {
                $metadata_state->setDataDefinition($name, $context_definition->getDataDefinition());
            }
        }
    }
    
    /**
     * Asserts additional metadata.
     *
     * @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin
     *   The context aware plugin.
     * @param \Drupal\rules\Context\ExecutionMetadataStateInterface $metadata_state
     *   The execution metadata state.
     */
    protected function assertMetadata(CoreContextAwarePluginInterface $plugin, ExecutionMetadataStateInterface $metadata_state) {
        // If the plugin does not implement the Rules-enhanced interface, skip this.
        if (!$plugin instanceof ContextAwarePluginInterface) {
            return;
        }
        $changed_definitions = $plugin->assertMetadata($this->getSelectedData($metadata_state));
        // Reverse the mapping and apply the changes.
        foreach ($changed_definitions as $context_name => $definition) {
            $selector = $this->configuration['context_mapping'][$context_name];
            // @todo Deal with selectors matching not a context name.
            if (strpos($selector, '.') === FALSE) {
                $metadata_state->setDataDefinition($selector, $definition);
            }
        }
    }
    
    /**
     * Process data context on the plugin, usually before it gets executed.
     *
     * @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin
     *   The plugin to process the context data on.
     * @param \Drupal\rules\Context\ExecutionStateInterface $rules_state
     *   The current Rules execution state with context variables.
     */
    protected function processData(CoreContextAwarePluginInterface $plugin, ExecutionStateInterface $rules_state) {
        if (isset($this->configuration['context_processors'])) {
            foreach ($this->configuration['context_processors'] as $context_name => $processors) {
                $definition = $plugin->getContextDefinition($context_name);
                $value = $plugin->getContextValue($context_name);
                if ($definition->isMultiple()) {
                    foreach ($value as &$current) {
                        $current = $this->processValue($current, $processors, $rules_state);
                    }
                }
                else {
                    $value = $this->processValue($value, $processors, $rules_state);
                }
                $plugin->setContextValue($context_name, $value);
            }
        }
    }
    
    /**
     * Processes a single value.
     *
     * @param mixed $value
     *   The current value.
     * @param array $processors
     *   An array mapping processor plugin IDs to their configuration.
     * @param \Drupal\rules\Context\ExecutionStateInterface $rules_state
     *   The current Rules execution state with context variables.
     *
     * @return mixed
     *   THe processed value.
     */
    protected function processValue($value, array $processors, ExecutionStateInterface $rules_state) {
        foreach ($processors as $processor_plugin_id => $configuration) {
            $data_processor = $this->processorManager
                ->createInstance($processor_plugin_id, $configuration);
            $value = $data_processor->process($value, $rules_state);
        }
        return $value;
    }

}

Traits

Title Deprecated Summary
ContextHandlerTrait Provides methods for handling context based on the plugin configuration.