Understanding Chaos System Tools plugins.

June 18, 2021

There are very few modules that provide much functionality for experienced Drupal developers as CTools, thanks to it, there are modules such as Views, Panels, Context, Display Suite, Password Policy, among others.

As the name suggests, Chaos Tools is a module with several API developer tools, which contain some features that facilitate the developmen. It allows exporting elements to code databases, creating forms in several steps, create forms or pages in modal dialogs, and many other things, but what we will focus on on this blog is in the plugin system.

The plugin system of CTools allows you to create spaces within the code that allow other modules to extend their functionality as if they were hooks, but using a more advanced concept based on classes and objects, which allow the code to be more organized and maintainable by other developers.

Following is an example in which a system plugins may be needed. The example consists of a calculator based on operations from where a user can choose between 2 numbers, the default module could have some operations and if a module is needed, it can be created without changing a single line of the original module.

This code is to indicate in which folders should CTools search for plugins.


/**
 * Implements hook_ctools_plugin_directory().
 */
function sum_ctools_plugin_directory($module, $plugin) {
  if (($module == mymodule) && ($plugin == calc_operation)) {
    return 'plugins/calc_operation;
  }
}

We will now use a Drupal form where the user will select an operation and fill out another form so that Drupal will make the operation with the entered numbers. We do not focus on the logic behind these operations, but observe where each of the CTools elements are how the plugins interact.


function mymodule_advanced_calculator_form($form, $form_state) {
  // Load all plugins type "operation".
  ctools_include('plugins');
  $operations = ctools_get_plugins(mymodule, calc_operation);
  $operation_options = array();  
  foreach ($operations as $id => $operation) {
    $operation_options[$id] = $operation['label'];
  }
  if (empty($operation_options)) {
    $form['message'] = array(
      '#markup' => t('There are no modules enabled with operations available.'),
    );
    return $form;
  }  
  $form['operations'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Choose an operation'),
    '#options' => $operation_options,
  );
  foreach ($operations as $id => $operation) {
    $form[$id] = array(
      '#type' => 'fieldset',
      '#title' => $operation['label'],
    );
    // Load class to load the configuration form
    if ($instance = _example_get_instance($id)) {
      $operation_form = $instance->getForm();
      // Put operation form fields inside current fieldset
      array_merge($form[$id], $operation_form);
    }
  }
  // More form elements
  return $form;
}

Then in the submit, instead of always using the same logic to process submissions, let the plugin system allow other modules to choose how to carry out the generation of passwords


function mymodule_calculation_submit($form, &$form_state) {
  foreach ($form_state['values'] as $id => $value) {
    if ($value['operations'] == $id) {
      if ($instance = _mymodule_get_instance($id)) {
        $form_state['result'] = $instance->solveOperation($value);
      }
      break;
    }
  }
}

The _mymodule_get_instance function is responsible for returning an instance of the class whose methods contain as much as necessary to build the form as to perform the operation.


function _mymodule_get_instance($id) {
  // Cache at page load level
  $instances = &drupal_static(__FUNCTION__);

  if (!isset($instances[$id])) {
    ctools_include('plugins');
    $plugin = ctools_get_plugins('mymodule', 'calc_operation', $id);
    $class = ctools_plugin_get_class($plugin, 'handler');
    $instances[$id] = new $class();
    // Check if obtained class inherits from AdvancedOperation class
    if (!is_subclass_of($instances[$id], 'AdvancedOperation')) {
      $instances[$id] = NULL;
    }
  }

  return $instances[$id];
}

Up to this point, you are able to search CTools plugins of certain type in our module, but find none, because we still have to build and deploy the plugins we need. To do this, create the directory used above "plugins/password_condition" and create a file containing the logic of our plugin.


/**
 * Operation plugin for My Module.
 *
 * Calculates the Riemann Integral.
 */

$plugin = array(
  'label' => t('Riemann Integral'),
  'handler' => array(
    'class' => 'IntegralOperation',
  ),
);

class IntegralOperation extends AdvancedOperation {
  public function buildForm() {
    $form['equation'] = array(
      '#type' => 'textfield'
      '#title' => 'Equation'
    )
    $form['dx'] = array(
      '#type' => 'textfield'
      '#title' => 'dX variable'
    )
    $form['a'] = array(
      '#type' => 'textfield'
      '#title' => 'A'
    )
    $form['b'] = array(
      '#type' => 'textfield'
      '#title' => 'B'
    )

    return $form;
  }
  public function solveOperation($values) {
    // Logic to get values and resolve the integral through numeric methods
  }
}

Ctools needs that the classes that are invoked through ctools_plugin_get_class be all structured in the same way and they all have the same attributes and methods, for that use an abstract class, which is the parent class of all classes (plugins).


abstract class AdvancedOperation {
  /**
   * Retrieves a form builded on plugin.
   */
  public function buildForm() {}

  /**
   * Calculate operation and return the result.
   */
  public function solveOperation() {}

  /**
   * Return result string for the operation.
   */
  public function resultMessage() {
    return t('Result of !operation is !result.', array(
      '!operation' => get_class($this),
      '!result' => $this->calculate(),
    ));
  }
}

Finally it is possible to implement more operators around this module and other modules, each operation form will recognize its alikes.

I hope that this example is used to help understand how the system works and how the CTools plugins can help in an specific situation. It may also help to understand how could we create plugins for other modules that are already using this system.