Entendiendo el sistema de plugins de Chaos Tools

June 18, 2021

Existen muy pocos modulos que otorgan tanta funcionalidad para programadores expertos en Drupal como CTools, gracias a este existen módulos como Views, Panels, Context, Display Suite, Password Policy, entre otros.

Como su nombre indica, Chaos Tools es un módulo de API con varias herramientas para desarrolladores, contienen algunas funciones que facilitan el desarrollo, permite exportar elementos en bases de datos hacia codigo, crear formularios de varios pasos, crear formularios o páginas en diálogos modales, y muchas otras cosas más, sin embargo la que nos enfocaremos en este blog es en el sistema de plugins.

El sistema de plugins de CTools permite crear espacios en el código que permiten que otros módulos extiendan la funcionalidad, como si se tratasen de hooks, pero usando un concepto mas avanzado basado en clases y objetos, que permiten que el codigo sea mas ordenado y mantenible por otros desarrolladores.

A continuación se mostrará un ejemplo sobre donde podría necesitarse un sistema de plugins. El ejemplo consistirá en una calculadora basada en operaciones que un usuario puede escoger para 2 numeros, el módulo por defecto podría contar con algunas y si un módulo lo necesitase, podrá crear más sin necesidad de modificar una sola linea del modulo original.

Este código sirve para declarar en que folder CTools debería buscar plugins


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

Usaremos un formulario de Drupal donde el usuario elegirá una operación y llenara un formulario al respecto para que posteriormente Drupal realize una operación con los numeros ingresados. No nos enfocaremos en la lógica detrás de estas operaciones, sino que observaremos donde se usa cada elemento de CTools para permitir el uso de los plugins declarados.


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;
}

Luego en el submit en vez de siempre usar la misma lógica para procesar los submissions, dejemos que el sistema de plugins deje elegir a otros modulos como llevar a cabo la generación de 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;
    }
  }
}

La función _mymodule_get_instance se encargará de retornar una instancia de la clase cuyos métodos contienen lo necesario tanto como para construir el formulario como para realizar la operación.


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];
}

Hasta este punto ya CTools es capaz de buscar plugins de cierto tipo en nuestro módulo, sin embargo no encontrara ninguno, pues aún debemos crear e implementar los plugins que necesitemos. Para ello, creamos el directorio que usamos arriba "plugins/password_condition" y creamos un archivo que contendrá la lógica de nuestro 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 necesita que las clases que se invoquen a través de ctools_plugin_get_class esten todas estructuradas de la misma manera y que todas tengan los mismos atributos y metodos, para eso usamos una clase abstracta, que será la clase padre de todas las clases (llamese plugins) que declaremos


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(),
    ));
  }
}

Finalmente ya es posible implementar más operadores alrededor de este modulo y otros modulos, y cada uno el formulario de operación los reconocerá por igual.

Espero que este ejemplo haya servido para ayudar a entender como funciona el sistema de plugins de CTools y sobre todo, en que situaciones sirve. Esto tambien puede ayudar a entender como se podrian crear plugins para otros modulos que ya esten usando este sistema.