Using Drupal 8 Condition Plugins API

Although Drupal 8 has had a Conditions Plugin API for a several months, it wasn't until during DrupalCon Austin sprint we managed to get blocks to use the Conditions Plugin API for block visibility.

The great thing about Condition Plugins, is they are re-usable chunks of code, and many contrib projects will be able to take advantage of them (Page Manager, Panels, Rules anyone?)

In this post, I show how you can create an example Page Message module that uses a RequestPath condition plugin to show a message on a configured page.

In our example, we're going to create an administration form that lets a user enter a list of paths for a message to appear on. 

The list of paths should sound familiar? It's exactly the same thing the block visibility forms use. So why not re-use it? Good news is, block visibility is using condition plugins, so we can too.

Step 1: Set up a plugin instance

In order to allow users to set the paths, we need to create an admin form. There are a number of posts on how to do this in Drupal 8 already, so I will just focus on the Condition Plugin parts.

class PathMessageAdminForm extends ConfigFormBase {

  protected $condition;

  public function __construct(ConfigFactoryInterface $config_factory, FactoryInterface $plugin_factory) {
    parent::__construct($config_factory);
    $this->condition = $plugin_factory->createInstance('request_path');
  }

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('plugin.manager.condition')
    );
  }

The first step is pass in our required dependencies. We'll need a ConfigFactory for saving our settings which is passed to the parent constructor. We also need a Condition plugin factory to create an instance of a Drupal\system\Plugin\Condition\RequestPath condition plugin. In most cases, we prefer to use interfaces for loose coupling and ease of unit testing. We know that Drupal\Core\Condition\ConditionManager implements FactoryInterface so lets use that as we only need to access the createInstance() method.

When creating our RequestPath condition, we pass the unique ID request_path to the factory->createInstance() method. You will find this in the plugin annotation on that class (id = "request_path"). This is a standard way of creating plugin instances for all Drupal plugins.

Step 2: Build our form

  public function buildForm(array $form, array &$form_state) {
    // Load our default configuration.
    $config = $this->config('path_message.settings');

    // Set the default condition configuration.
    $this->condition->setConfiguration($config->get('request_path'));

    $form['message'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Message'),
      '#description' => $this->t('Enter the message you want to appear'),
      '#default_value' => $config->get('message'),
    );

    $form += $this->condition->buildConfigurationForm($form, $form_state);

    return parent::buildForm($form, $form_state);
  }

We have two fields in our form: one for a message, and the other is created by the RequestPath plugin. We simply ask the plugin to build its configuration form, and add it to our form. Because we are extending ConfigFormBase we need to also call parent::buildForm().

Step 3: Save the form values

  public function submitForm(array &$form, array &$form_state) {

    $this->condition->submitConfigurationForm($form, $form_state);
    $this->config('path_message.settings')
      ->set('message', String::checkPlain($form_state['values']['message']))
      ->set('request_path', $this->condition->getConfiguration())
      ->save();

    parent::submitForm($form, $form_state);
  }

Just like any other form, we need to save the user preferences when the form is submitted. In our case, we first need to call submitConfigurationForm() on the RequestPath condition plugin. This lets the plugin process the form values, and validate the input.

Once we have done that, the next step is just saving our form values, including the plugin configuration, to our config.

Step 4: Evaluate the condition

Now that we have some configuration stored in config, how do we use it? Well, it depends! In the case of block visibility, you would use it to show or hide a block. In our simple example, we could create a an event subscriber that listens for Request events that gets fired on every page request, check if it passes the RequestPath condition, and print the configured message if it does.

  public function setMessage(FilterResponseEvent $event) {
    /* @var \Drupal\system\Plugin\Condition\RequestPath $condition */
    $condition = $this->conditionManager->createInstance('request_path');

    $condition->setConfiguration($this->config->get('request_path'));

    if ($condition->evaluate()) {
      drupal_set_message($this->config->get('message'));
    }
  }

In order to check if a message should be seny, first, we create an instance of the RequestPath pluging, then we pass it the configuration we loaded. As the RequestPath plugin implements \Drupal\Core\Condition\ConditionInterface when can simply call evaluate() which returns TRUE or FALSE.

What's Next?

This is pretty simple example, and it only uses a single condition plugin. The real power comes from the number and variety of core condition plugin types: RequestPath, NodeType, UserRole, Vocabulary, CurrentTheme, Language as well as the simplicity of adding more plugins in contrib. For example, Rules has a lot!

In previous versions of Drupal, you're main mechanism for extension was modules. Plugins, including Condition Plugins, give us another way to extend Drupal in a clean, structured and re-usable way!

PS: I've posted the sample code used here to https://github.com/kimpepper/path_message. Feedback welcome!

Drupal 8 Condition Plugins
Back to top

Comments

Posted by Brian | July 23, 2014

Hi Kim,

Great timing, as this is something I'm looking to add to a port of a module to D8. I think this tutorial is missing an explanation of config entities though? You have a schema file in your github repo here: https://github.com/kimpepper/path_message/blob/master/config/schema/path_message.schema.yml

kim's picture
Posted by kim | July 23, 2014

Thanks Brian. Yes, I purposely tried to keep it focussed on just the condition plugins part. There are plenty of other posts (some on this site!) that deal with routing, config, and config entities.

BTW config schema is needed for all config, not just config entities.

Posted by Brian | July 24, 2014

I got this working pretty well, but found it to be a bit inflexible in certain areas. Using the plugin to build the configuration form for the condition seems limiting because you can't control the title of the paths textarea of the "negate" checkbox. I suppose I could override that after calling buildConfigurationForm, but seems like there should be a different way. Maybe not.

kim's picture
Posted by kim | July 24, 2014

Yes, you're really deferring the whole form building to the plugin. While this may be inflexible, it does add consistency across the site. Especially if you are using multiple plugins.

Post a comment