You are here

trait ContextHandlerTrait in Rules 8.3

Provides methods for handling context based on the plugin configuration.

The trait requires the plugin to use configuration as defined by the ContextConfig class.

Hierarchy

See also

\Drupal\rules\Context\ContextConfig

1 file declares its use of ContextHandlerTrait
ContextHandlerTraitTest.php in tests/src/Unit/ContextHandlerTraitTest.php

File

src/Context/ContextHandlerTrait.php, line 18

Namespace

Drupal\rules\Context
View source
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;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ContextHandlerTrait::$processorManager protected property The data processor plugin manager used to process context variables.
ContextHandlerTrait::addProvidedContext protected function Adds provided context values from the plugin to the execution state.
ContextHandlerTrait::addProvidedContextDefinitions protected function Adds the definitions of provided context to the execution metadata state.
ContextHandlerTrait::assertMetadata protected function Asserts additional metadata.
ContextHandlerTrait::getMappedDefinition protected function Gets the definition of the data that is mapped to the given context.
ContextHandlerTrait::getSelectedData protected function Gets definitions of all selected data at configuration time.
ContextHandlerTrait::prepareContext protected function Prepares plugin context based upon the set context configuration.
ContextHandlerTrait::prepareContextWithMetadata protected function Prepares plugin context based upon the set context configuration.
ContextHandlerTrait::processData protected function Process data context on the plugin, usually before it gets executed.
ContextHandlerTrait::processValue protected function Processes a single value.