You are here

RulesComponent.php in Rules 8.3

File

src/Engine/RulesComponent.php
View source
<?php

namespace Drupal\rules\Engine;

use Drupal\Core\Entity\DependencyTrait;
use Drupal\rules\Context\ContextDefinition;
use Drupal\rules\Context\ContextDefinitionInterface;
use Drupal\rules\Context\ExecutionMetadataState;
use Drupal\rules\Context\ExecutionState;
use Drupal\rules\Exception\LogicException;

/**
 * Handles executable Rules components.
 */
class RulesComponent {
  use DependencyTrait;

  /**
   * The rules execution state.
   *
   * @var \Drupal\rules\Context\ExecutionStateInterface
   */
  protected $state;

  /**
   * Definitions for the context used by the component.
   *
   * @var \Drupal\rules\Context\ContextDefinitionInterface[]
   */
  protected $contextDefinitions = [];

  /**
   * List of context names that is provided back to the caller.
   *
   * @var string[]
   */
  protected $providedContext = [];

  /**
   * The expression.
   *
   * @var \Drupal\rules\Engine\ExpressionInterface
   */
  protected $expression;

  /**
   * Constructs the object.
   *
   * @param \Drupal\rules\Engine\ExpressionInterface $expression
   *   The expression of the component.
   *
   * @return static
   */
  public static function create(ExpressionInterface $expression) {
    return new static($expression);
  }

  /**
   * Creates a component based on the given configuration array.
   *
   * @param array $configuration
   *   The component configuration, as returned from ::getConfiguration().
   *
   * @return static
   */
  public static function createFromConfiguration(array $configuration) {
    $configuration += [
      'context_definitions' => [],
      'provided_context_definitions' => [],
    ];

    // @todo Can we improve this use dependency injection somehow?
    $expression_manager = \Drupal::service('plugin.manager.rules_expression');
    $expression = $expression_manager
      ->createInstance($configuration['expression']['id'], $configuration['expression']);
    $component = static::create($expression);
    foreach ($configuration['context_definitions'] as $name => $definition) {
      $component
        ->addContextDefinition($name, ContextDefinition::createFromArray($definition));
    }
    foreach ($configuration['provided_context_definitions'] as $name => $definition) {
      $component
        ->provideContext($name);
    }
    return $component;
  }

  /**
   * Constructs the object.
   *
   * @param \Drupal\rules\Engine\ExpressionInterface $expression
   *   The expression of the component.
   */
  protected function __construct(ExpressionInterface $expression) {
    $this->state = ExecutionState::create();
    $this->expression = $expression;
  }

  /**
   * Gets the expression of the component.
   *
   * @return \Drupal\rules\Engine\ExpressionInterface
   *   The expression.
   */
  public function getExpression() {
    return $this->expression;
  }

  /**
   * Gets the configuration array of this component.
   *
   * @return array
   *   The configuration of this component. It contains the following keys:
   *   - expression: The configuration of the contained expression, including a
   *     nested 'id' key.
   *   - context_definitions: Array of context definition arrays, keyed by
   *     context name.
   *   - provided_context: The names of the context that is provided back.
   */
  public function getConfiguration() {
    return [
      'expression' => $this->expression
        ->getConfiguration(),
      'context_definitions' => array_map(function (ContextDefinitionInterface $definition) {
        return $definition
          ->toArray();
      }, $this->contextDefinitions),
      'provided_context_definitions' => $this->providedContext,
    ];
  }

  /**
   * Gets the execution state.
   *
   * @return \Drupal\rules\Context\ExecutionStateInterface
   *   The execution state for this component.
   */
  public function getState() {
    return $this->state;
  }

  /**
   * Adds a context definition.
   *
   * @param string $name
   *   The name of the context to add.
   * @param \Drupal\rules\Context\ContextDefinitionInterface $definition
   *   The definition to add.
   *
   * @return $this
   */
  public function addContextDefinition($name, ContextDefinitionInterface $definition) {
    $this->contextDefinitions[$name] = $definition;
    return $this;
  }

  /**
   * Gets definitions for the context used by this component.
   *
   * @return \Drupal\rules\Context\ContextDefinitionInterface[]
   *   The array of context definitions, keyed by context name.
   */
  public function getContextDefinitions() {
    return $this->contextDefinitions;
  }

  /**
   * Adds the available event context for the given events.
   *
   * @param string[] $event_names
   *   The (fully qualified) event names; e.g., as configured for a reaction
   *   rule.
   *
   * @return $this
   */
  public function addContextDefinitionsForEvents(array $event_names) {
    foreach ($event_names as $event_name) {

      // @todo Correctly handle multiple events to intersect available context.
      // @todo Use setter injection for the service.
      $event_definition = \Drupal::service('plugin.manager.rules_event')
        ->getDefinition($event_name);
      foreach ($event_definition['context_definitions'] as $context_name => $context_definition) {
        $this
          ->addContextDefinition($context_name, $context_definition);
      }
    }
    return $this;
  }

  /**
   * Marks the given context to be provided back to the caller.
   *
   * @param string $name
   *   The name of the context to provide.
   *
   * @return $this
   */
  public function provideContext($name) {
    $this->providedContext[] = $name;
    return $this;
  }

  /**
   * Returns the names of context that is provided back to the caller.
   *
   * @return string[]
   *   The names of the context that is provided back.
   */
  public function getProvidedContext() {
    return $this->providedContext;
  }

  /**
   * Sets the value of a context.
   *
   * @param string $name
   *   The name.
   * @param mixed $value
   *   The context value.
   *
   * @return $this
   *
   * @throws \Drupal\rules\Exception\LogicException
   *   Thrown if the passed context is not defined.
   */
  public function setContextValue($name, $value) {
    if (!isset($this->contextDefinitions[$name])) {
      throw new LogicException("The specified context '{$name}' is not defined.");
    }
    $this->state
      ->setVariable($name, $this->contextDefinitions[$name], $value);
    return $this;
  }

  /**
   * Executes the component with the previously set context.
   *
   * @return mixed[]
   *   The array of provided context values, keyed by context name.
   *
   * @throws \Drupal\rules\Exception\EvaluationException
   *   Thrown if the Rules expression triggers errors during execution.
   */
  public function execute() {

    // @todo Use injection for the service.
    $rulesDebugLogger = \Drupal::service('logger.channel.rules_debug');
    $rulesDebugLogger
      ->info('RulesComponent: Rule %label fires.', [
      '%label' => $this->expression
        ->getLabel(),
      'element' => $this->expression,
      'scope' => TRUE,
    ]);
    $this->expression
      ->executeWithState($this->state);
    $rulesDebugLogger
      ->info('RulesComponent: Rule %label has fired.', [
      '%label' => $this->expression
        ->getLabel(),
      'element' => $this->expression,
      'scope' => FALSE,
    ]);
    $this->state
      ->autoSave();
    $result = [];
    foreach ($this->providedContext as $name) {
      $result[$name] = $this->state
        ->getVariableValue($name);
    }
    return $result;
  }

  /**
   * Executes the component with the given values.
   *
   * @param mixed[] $arguments
   *   The array of arguments; i.e., an array keyed by name of the defined
   *   context and the context value as argument.
   *
   * @return mixed[]
   *   The array of provided context values, keyed by context name.
   *
   * @throws \Drupal\rules\Exception\LogicException
   *   Thrown if the context is not defined.
   * @throws \Drupal\rules\Exception\EvaluationException
   *   Thrown if the Rules expression triggers errors during execution.
   */
  public function executeWithArguments(array $arguments) {
    $this->state = ExecutionState::create();
    foreach ($arguments as $name => $value) {
      $this
        ->setContextValue($name, $value);
    }
    return $this
      ->execute();
  }

  /**
   * Verifies that the given expression is valid with the defined context.
   *
   * @return \Drupal\rules\Engine\IntegrityViolationList
   *   A list object containing \Drupal\rules\Engine\IntegrityViolation objects.
   */
  public function checkIntegrity() {
    $metadata_state = $this
      ->getMetadataState();
    return $this->expression
      ->checkIntegrity($metadata_state);
  }

  /**
   * Gets the metadata state with all context definitions as variables in it.
   *
   * Describes the metadata state before execution - only context definitions
   * are set as variables.
   *
   * @return \Drupal\rules\Context\ExecutionMetadataStateInterface
   *   The execution metadata state populated with context definitions.
   */
  public function getMetadataState() {
    $data_definitions = [];
    foreach ($this->contextDefinitions as $name => $context_definition) {
      $data_definitions[$name] = $context_definition
        ->getDataDefinition();
    }
    return ExecutionMetadataState::create($data_definitions);
  }

  /**
   * Calculates dependencies for the component.
   *
   * @return array
   *   An array of dependencies grouped by type (config, content, module,
   *   theme).
   *
   * @see \Drupal\Component\Plugin\DependentPluginInterface::calculateDependencies()
   */
  public function calculateDependencies() {

    // @todo Complete implementation and add test coverage.
    $this
      ->addDependency('module', 'rules');
    $this
      ->addDependencies($this
      ->getExpression()
      ->calculateDependencies());
    return $this->dependencies;
  }

  /**
   * PHP magic __clone function.
   */
  public function __clone() {

    // Implement a deep clone.
    $this->state = clone $this->state;
    $this->expression = clone $this->expression;
  }

  /**
   * Returns autocomplete results for the given partial selector.
   *
   * Example: "node.uid.e" will return ["node.uid.entity"].
   *
   * @param string $partial_selector
   *   The partial data selector.
   * @param \Drupal\rules\Engine\ExpressionInterface $until
   *   The expression in which the autocompletion will be executed. All
   *   variables in the execution metadata state up to that point are available.
   *
   * @return array[]
   *   A list of autocomplete suggestions - valid property paths for one of the
   *   provided data definitions. Each entry is an array with the following
   *   keys:
   *   - value: the data selecor property path.
   *   - label: the human readable label suggestion.
   */
  public function autocomplete($partial_selector, ExpressionInterface $until = NULL) {

    // We use the integrity check to populate the execution metadata state with
    // available variables.
    $metadata_state = $this
      ->getMetadataState();
    $this->expression
      ->prepareExecutionMetadataState($metadata_state, $until);
    return $metadata_state
      ->autocomplete($partial_selector);
  }

}

Classes

Namesort descending Description
RulesComponent Handles executable Rules components.