[Drupal] How to create a custom plugin in Context for a Drupal website?

| | 5 min read

Context is one of the most useful architectural modules for Drupal. Contexts allows a site administrator to manage contextual conditions and reactions. Context feature works by setting conditions such as a path or a node type. If that condition is present then the corresponding reactions, such as adding blocks to regions or setting an active menu, will work as configured.

Context module offers a well-built API that makes configuring our conditions and reactions quick and easy.

For creating a context we need to follow the below steps:

1. Create a Custom Module

The first step is to create a module to house our code.
For the sake of this example I'm calling this module as 'mymodule'. To set this up:

  1. create a mymodule.info file that lists node, context, and comment modules as dependencies
  2. create a blank mymodule.module file

2. Create a Context Condition

Next, we need to tell Context, about our plugin. Add our condition to the Context registry. Here we are creating the mymodule_no_comment condition and telling Context that we will be using the mymodule_no_comment_context_condition plugin.

/**
* Implementation of hook_context_registry().
*/

function mymodule_context_registry() {
  return array(
    'conditions' => array(
      'mymodule_no_comment' => array(
        'title' => t('No Comments on a node'),
        'description' => t('Set context on a node if there are NO comments on that node.'),
        'plugin' => 'mymodule_no_comment_context_condition',
      ),
    ),
  );
}

Then, add the definition for our Context plugin. Here we are telling Context where our plugin can be found and what class it uses.

/**
* Implementation of hook_context_plugins().
*/

function mymodule_context_plugins() {
  $plugins = array();
  $plugins['mymodule_no_comment_context_condition'] = array(
    'handler' => array(
      'path' => drupal_get_path('module', 'mymodule'),
      'file' => 'mymodule.context.inc',
      'class' => 'mymodule_no_comment_context_condition',
      'parent' => 'context_condition_node',
    ),
  );
  return $plugins;
}

3. Create a Context Class

Next we need to create the Context condition class. As you can see above we are extending the context_condition_node. To do so we'll make a file mymodule.context.inc and add the following:

/**
* Expose node views/node forms of nodes with no comments as a context condition.
*/

class mymodule_no_comment_context_condition extends context_condition_node {
  function condition_values() {
    return array(1 => t('Enable this context.'));
  }

  function execute($op) {
    foreach ($this->get_contexts() as $context) {
      // Check the node form option.
      $options = $this->fetch_from_context($context, 'options');
      if ($op === 'form') {
        if (!empty($options['node_form']) 
        && 
        in_array($options['node_form'], array(CONTEXT_NODE_COMMENTS_FORM, CONTEXT_NODE_COMMENTS_FORM_ONLY))) {
          $this->condition_met($context, 'no_comment');
        }
      }
      elseif (empty($options['node_form']) || $options['node_form'] != CONTEXT_NODE_COMMENTS_FORM_ONLY) {
        $this->condition_met($context, 'no_comment');
      }
    }
  }
}

It is also important to note that we are inheriting the options_form method from the context_condition_node class as well. This method creates the form shown in the image above.

4. Set the Context

Finally, we need to set the Context to execute when the correct conditions have occurred. For this example our focus is on the node. We want to set the Context when we are

  • Viewing a Node with no comments
  • Editing a Node with no comments
  • Viewing a Node with no comments

To set the Context when we are viewing the node we will use hook_nodeapi():

/**
* Implementation of hook_nodeapi().
*/

function mymodule_nodeapi(&$node, $op, $teaser, $page) {
  switch($op) {
    case 'view':
    // Check that there are comments on this node, 
    // if context exists, and we are viewing the node on a page.
    if (!$node->comment_count > 0 && module_exists('context') && $page) {
      mymodule_no_comment_context_condition('view');
    }
    break;
  }
}

/**
* Function to execute 'no comments' context condition.
*/

function mymodule_no_comment_context_condition($op)
  if ($plugin = context_get_plugin('condition', 'mymodule_no_comment')) {
    $plugin->execute($op);
  }
}

Here I am checking to see that we are viewing an actual node page (instead of a teaser) and that there are more than 0 comments on the node.

Editing a Node with no comments

Unfortunately, because there are a number of ways to access a node form we are going to have to do more than set 'prepare' on our hook_nodeapi() call above. First we are going to use a form_alter() to check that we aren't on an admin page and that we are on an node form:

/**
* Implementation of hook_form_alter().
*/

function mymodule_form_alter(&$form, $form_state, $form_id) {
  // Prevent this from firing on admin pages... damn form driven apis...
  if ($form['#id'] === 'node-form' && arg(0) != 'admin') {
    $node = node_load($form['nid']['#value']);
    // Check to make sure node has no comments.
    if (!$node->comment_count > 0) {
      mymodule_no_comment_context_condition('form');
      $form['#validate'][] = 'mymodule_form_alter_node_validate';
    }
  }
  else if ($form_id == 'system_modules') {
    context_invalidate_cache();
  }
}

Secondly we want to respect modules such as panels that use ctools page manager to decide if they are viewing or editing a node. This sets a 'view' op as well so even if we were doing a version of this module that didn't check for the node edit form it would have been best to set this anyway.

/**
* Implementation of hook_ctools_render_alter().
* Used to detect the presence of a page manager node view or node form.
*/

function mymodule_ctools_render_alter($info, $page, $args, $contexts, $task, $subtask) {
  if ($page && in_array($task['name'], array('node_view', 'node_edit'), TRUE)) {
    foreach ($contexts as $ctools_context) {
      if ($ctools_context->type === 'node' && !empty($ctools_context->data)) {
        $node = node_load(arg(1));
        if (!$node->comment_count > 0) {
          mymodule_no_comment_context_condition('form', $task['name'] === 'node_view' ? 'view' : 'form');
          break;
        }
      }
    }
  }
}

Note that an extra node_load() on every page is not great for performance. Use with discretion.

Finally, we are going to implement a fix that is still in the context issue queue that keeps the context active when a node is undergoing a validation error: http://drupal.org/node/606816

our form_alter, you may have wondered why we added the line "$form['#validate'][] = 'mymodule_form_alter_node_validate';". We set that so we could grab the node validate and make sure the context is active there as well:

/**
* Set context also on validate, otherwise forms that don't validate drop out
* of context.
*/

function mymodule_form_alter_node_validate($form, &$form_state) {
  mymodule_no_comment_context_condition('form');
}