Picture
Minnur Yunusov Senior Drupal Developer Follow
September 18, 2015

In this blog post I will try provide an example on how you could make your modules extendable within Drupal 7.

This is a very simple module that displays buttons with labels from your custom plugins on http://localhost/my-module/plugin page and with the configuration form here: http://localhost/admin/config/development/my-module.

Hopefully this is going to be a very useful tutorial for you and help you to make your modules extendable.

Download example module

Tutorial

First lets create a custom module with the following structure:

my_module
my_module/my_module.info
my_module/my_module.module
my_module/plugins/plugin.php
my_module/plugins/example.php

In the my_module.info file implement your custom hook.

IMPORTANT: make sure you clear Drupal cache after you add new plugin to .info files[].

name = My Module
description = Description of the module
core = 7.x
configure = admin/config/development/my-module

; This is required. You have to include all plugin files.
files[] = plugins/plugin.php
files[] = plugins/example.php

In the my_module.module file implement your custom hook.

/**
 * Implements hook_menu().
 */
function my_module_menu() {

  // My module configuration page.
  $items['admin/config/development/my-module'] = array(
    'title' => 'My Module',
    'description' => 'Description of the module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('my_module_configuration_form'),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_NORMAL_ITEM,
  );

  // Path for the plugin integration page.
  // This page will display buttons from
  // all enabled plugins.
  $items['my-module/plugin'] = array(
    'title' => 'My Module Elements',
    'description' => 'Description of the module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('my_module_view_form'),
    'access callback' => TRUE,
    'type' => MENU_NORMAL_ITEM,
  );

  return $items;
}

/**
 * Implements hook_my_own_hook().
 */
function my_module_my_own_hook() {
  return array(
    // Machine name of the plugin.
    'example' => array(
      // Human readable string visible on the config page.
      'name' => t('Example plugin'),
      // PHP Class name that extends MyPluginBase class.
      'phpClassName' => 'Example',
    ),
  );
}

/**
 * View form.
 * Simple callback that implements buttons with labels
 * pulled from plugin `label()` methods.
 */
function my_module_view_form($form, &$form_state) {

  // Load all plugins.
  $plugins = my_module_load_plugins();
  foreach ($plugins as $name => $plugin) {

    // Display only enabled plugins.
    $enabled = variable_get(
      'my_module_' . $name . '_enabled',
      FALSE
    );

    if ($enabled) {
      // Get button label.
      $label = my_module_plugin_method(
        $plugin['phpClassName'],
        'label'
      );
      $form[$name] = array(
        '#type' => 'submit',
        '#value' => $label,
      );
    }

  }

  return $form;
}

/**
 * Configuration form.
 */
function my_module_configuration_form($form, &$form_state) {
  $form['settings'] = array(
    '#type' => 'vertical_tabs',
  );

  // Load all plugins.
  $plugins = my_module_load_plugins();
  foreach ($plugins as $name => $plugin) {

    $form['settings'][$name] = array(
      '#type' => 'fieldset',
      '#title' => $plugin['name'],
    );

    // Plugin status.
    $form['settings'][$name][$name . '_enabled'] = array(
      '#type' => 'checkbox',
      '#title' => t('Enable plugin'),
      '#default_value' => variable_get(
        'my_module_' . $name . '_enabled',
        FALSE
      ),
      '#description' => t("Description for the checkbox."),
    );

    // Check if plugin has configuraiton form.
    $configForm = my_module_plugin_method(
      $plugin['phpClassName'],
      'configForm',
      array($form_state)
    );
    if (is_array($configForm)) {
      $form['settings'][$name] += $configForm;
    }

  }

  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save settings'),
  );

  return $form;
}

/**
 * Form validation handler.
 */
function my_module_configuration_form_validate($form, &$form_state) {
  $plugins = my_module_load_plugins();
  foreach ($plugins as $name => $plugin) {
    // Trigger plugin validation handler method.
    my_module_plugin_method(
      $plugin['phpClassName'],
      'validationHandler',
      array($form, $form_state)
    );
  }
}

/**
 * Form submit handler.
 */
function my_module_configuration_form_submit($form, &$form_state) {
  $plugins = my_module_load_plugins();
  foreach ($plugins as $name => $plugin) {
    // Save Enable options.
    variable_get(
      'my_module_' . $name . '_enabled',
      $form_state['values'][$name . '_enabled']
    );
    // Trigger plugin submit handler method.
    my_module_plugin_method(
      $plugin['phpClassName'],
      'submitHandler',
      array($form, $form_state)
    );
  }
  drupal_set_message(t('The configuration options have been saved.'));
}

/**
 * Helper function to invoke your custom hook.
 *
 * @return array List of available plugins.
 */
function my_module_load_plugins() {

  $plugins = array();
  // Invoke `my_own_hook` implemented in all modules.
  $plugin_hooks = module_invoke_all('my_own_hook');

  foreach ($plugin_hooks as $name => $plugin) {

    // @see http://php.net/manual/en/class.reflectionclass.php
    $reflection = new ReflectionClass($plugin['phpClassName']);

    // Now make sure the plugin class is extended from
    // your abstract `MyPluginBase` class.
    if ($reflection->isSubclassOf('MyPluginBase')) {
      $plugins[$name] = $plugin;
    }

  }

  return $plugins;
}

/**
 * Helper function to call phpClass method.
 *
 * @return mixed Result of the class method.
 */
function my_module_plugin_method($class, $method, $args = array()) {

  $reflection = new ReflectionClass($class);
  $method = $reflection->getMethod($method);
  $pluginClass = new $class();
  return $method->invokeArgs($pluginClass, $args);

}

The plugin.php is going to be an abstract class for the plugin.

abstract class MyPluginBase {

  /**
   * Button label.
   */
  abstract public function label();

  /**
   * Configuration form.
   * Drupal form API elements.
   *
   * @return array.
   */
  abstract public function configForm(&$form_state);

  /**
   * Drupal form validation handler.
   */
  abstract public function validationHandler($form, &$form_state);

  /**
   * Drupal form submit handler.
   */
  abstract public function submitHandler($form, &$form_state);

}

The example.php is your first example plugin that uses MyPluginBase class.

/**
 * Example plugin.
 */
class Example extends MyPluginBase {

  /**
   * {@inheritdoc}
   */
  public function label() {
    return t('Example');
  }

  /**
   * {@inheritdoc}
   */
  public function configForm(&$form_state) {

    $form['example_setting'] = array(
      '#title' => t('Example variable'),
      '#type' => 'textfield',
      '#default_value' => variable_get('my_module_example_setting', ''),
      '#description' => t('Description of the configraiont option.'),
    );

    return $form;

  }

  /**
   * {@inheritdoc}
   */
  public function validationHandler($form, &$form_state) {
    if (empty($form_state['values']['example_setting'])) {
      form_set_error('example_setting', t('Validation error'));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitHandler($form, &$form_state) {
    variable_set(
      'my_module_example_setting',
      $form_state['values']['example_setting']
    );
  }

}

Download example module