Porting Expression Plugins

Last updated on
11 December 2021

Drupal 8 Plugins replace Drupal 7 Rules' hook_rules_plugin_info() and allow you to extend Rules with other expressions, like other variants of loops, etc.

To implement a Rules Expression plugin in Drupal 8 you will need a custom module, which we'll refer to as {module_name}. Your plugin code needs to be located in the directory {module_name}/src/Plugin/RulesExpression and needs to declare a namespace of \Drupal\{module_name}\Plugin\RulesExpression. Rules Expression plugins use the @RulesExpression annotation, as you will see in the example below.

Let's look at an example from core Rules. The AND condition in Drupal 7 is defined in hook_rules_plugin_info() and implemented in a class called RulesAnd:

function hook_rules_plugin_info() {
  return array(
    'and' => array(
      'label' => t('Condition set (AND)'),
      'class' => 'RulesAnd',
      'embeddable' => 'RulesConditionContainer',
      'component' => TRUE,
      'extenders' => array(
        'RulesPluginUIInterface' => array(
          'class' => 'RulesConditionContainerUI',
        ),
      ),
    ),
  );
}

The important things to note are:

  • The 'and' key in the array returned by hook_rules_plugin_info(), which is the equivalent of the plugin id in Drupal 8.
  • The 'class' value. This is the class in D7 that implements the condition, which is equivalent to the plugin class name in D8.

In Drupal 7 the class implementing this expression looks like this:

/**
 * A logical AND.
 */
class RulesAnd extends RulesConditionContainer {

  /**
   * @var string
   */
  protected $itemName = 'and';

  public function evaluate(RulesState $state) {
    foreach ($this->children as $condition) {
      if (!$condition->evaluate($state)) {
        rules_log('%condition evaluated to %bool.', array(
          '%condition' => $this->label(),
          '%bool' => 'FALSE',
        ));
        return $this->negate;
      }
    }
    rules_log('%condition evaluated to %bool.', array(
      '%condition' => $this->label(),
      '%bool' => 'TRUE',
    ));
    return !$this->negate;
  }

  public function label() {
    return !empty($this->label) ? $this->label : ($this->negate ? t('NOT AND') : t('AND'));
  }

}

In Drupal 8, both of these are replaced by a RulesExpression plugin, implemented by a class called \Drupal\rules\Plugin\RulesExpression\AndExpression:

<?php

namespace Drupal\rules\Plugin\RulesExpression;

use Drupal\rules\Context\ExecutionStateInterface;
use Drupal\rules\Engine\ConditionExpressionContainer;

/**
 * Evaluates a group of conditions with a logical AND.
 *
 * @RulesExpression(
 *   id = "rules_and",
 *   label = @Translation("Condition set (AND)"),
 *   form_class = "\Drupal\rules\Form\Expression\ConditionContainerForm"
 * )
 */
class AndExpression extends ConditionExpressionContainer {

  /**
   * Returns whether there is a configured condition.
   *
   * @return bool
   *   TRUE if there are no conditions, FALSE otherwise.
   */
  public function isEmpty() {
    return empty($this->conditions);
  }
  /**
   * {@inheritdoc}
   */
  public function evaluate(ExecutionStateInterface $state) {
    // Use the iterator to ensure the conditions are sorted.
    foreach ($this as $condition) {
      /* @var \Drupal\rules\Engine\ExpressionInterface $condition */
      if (!$condition->executeWithState($state)) {
        $this->rulesDebugLogger->info('%label evaluated to %result.', [
          '%label' => $this->getLabel(),
          '%result' => 'FALSE',
        ]);
        return FALSE;
      }
    }
    $this->rulesDebugLogger->info('%label evaluated to %result.', [
      '%label' => $this->getLabel(),
      '%result' => 'TRUE',
    ]);
    // An empty AND should return FALSE. Otherwise, if all conditions evaluate
    // to TRUE we return TRUE.
    return !empty($this->conditions);
  }

  /**
   * {@inheritdoc}
   */
  protected function allowsMetadataAssertions() {
    // If the AND is not negated, all child-expressions must be executed - thus
    // assertions can be added it.
    return !$this->isNegated();
  }

}

We don't have a separate hook and class in D8. The D8 plugin class is 'AndExpression' whereas the D7 hook declared a 'class' of 'RulesAnd'. Other information that was previously returned by the D7 hook is now contained in the @RulesExpression annotation in our plugin. Specifically, the plugin 'id', 'label' and 'form_class' in the annotation correspond to D7's key ('and'), label ('Condition set (AND)'), and 'embeddable' ( 'RulesConditionContainer') values.

As far as the functionality, you can see that in both D7 and D8 the work is done in the evaluate() function. An important part of this work is the use of the logger - rules_log() calls in Drupal 7 have become $this->rulesDebugLogger->log() calls in Drupal 8, but notice that we are logging exactly the same way. Logging debug information in RulesExpression plugins is extremely important - these calls do nothing unless logging is turned on so they don't cost anything, but the information they provide when logging is on is extremely useful when it comes to figuring out how Rules are executing on your site.

Help improve this page

Page status: No known problems

You can: