There are times when a module needs an easy way for other modules to provide some additional kind of functionality or task to the main feature. There are a number of Drupal 7 contrib modules (such as Key, Encrypt, Feeds) which are dependent on the CTools module's plugin management tool. CTools plugin management can be replicated by using the equivalent Backdrop contrib module Plugin Manager. This module is actually just a direct extraction of the CTools plugin.inc file into a standalone module. It is the easiest approach and can usually be accomplished by replacing with equivalent functions. The downside is that it adds a dependency. So the other approach, which the rest of this article will describe in greater detail, is to convert the plugins to regular Backdrop-style *_info() hooks and related functions.
In order to convert, we first need to know how CTools implements plugins. Here is how CTools describes it:
A module which uses plugins can implement a hook describing the plugin (which is not necessary, as defaults will be filled in) and then calls a function which loads either all the known plugins (used for listing/choosing) or a specific plugin (used when it's known which plugin is needed). From the perspective of the plugin system, a plugin is a packet of data, usually some printable info and a list of callbacks. It is up to the module implementing plugins to determine what that info means and what the callbacks do.
A module would implement hook_ctools_plugin_type() to describe the info and callbacks for the plugin, like this example from Encrypt:
function encrypt_ctools_plugin_type() {
$plugins = array();
$plugins['encryption_methods'] = array(
'cache' => TRUE,
'cache table' => 'cache',
'process' => '_encrypt_plugin_process',
'defaults' => array(
'title' => '',
'description' => '',
'encrypt callback' => NULL,
'dependency callback' => NULL,
'dependency errors' => NULL,
'settings form' => NULL,
),
);
return $plugins;
}
Note that Encrypt currently uses Plugin Manager, but it could be converted to using info hooks without too much effort.
This plugin type hook can be converted to a function to describe the defaults for the encryption methods plugin and then another function to implement a new encryption_method_info hook:
function encrypt_encryption_method_plugin_defaults() {
return array(
'title' => '',
'description' => '',
'encrypt callback' => NULL,
'dependency callback' => NULL,
'dependency errors' => NULL,
'settings form' => NULL,
'file' => NULL,
);
}
Note that it only includes the defaults. The cache and _encrypt_plugin_process callback can be dealt with when calling the hook in a function which fetches either one or all encryption method plugins, which would look like this:
function encrypt_encryption_method_plugin($plugin_id = NULL) {
$encryption_methods = &backdrop_static(__FUNCTION__);
if (!isset($encryption_methods)) {
foreach (module_implements('encrypt_encryption_method_info') as $module) {
$module_encryption_methods = module_invoke($module, 'encrypt_encryption_method_info');
foreach ($module_encryption_methods as $module_encryption_method_name => $encryption_method_info) {
$encryption_method_info += encrypt_encryption_method_plugin_defaults();
// Calculate dependencies and attach any errors. (This replaces _encrypt_plugin_process()).
if ($encryption_method_info['dependency callback'] && $dep_function = key_get_function($encryption_method_info, 'dependency callback')) {
$key_provider_info['dependency errors'] = call_user_func($dep_function);
}
$encryption_methods[$module_encryption_method_name] = $encryption_method_info;
}
}
backdrop_alter('encryption_method_info', $encryption_methods);
backdrop_sort($encryption_methods);
}
if (!isset($plugin_id)) {
return $encryption_methods;
}
elseif (isset($encryption_methods[$plugin_id])) {
return $encryption_methods[$plugin_id];
}
else {
return FALSE;
}
}
Then encrypt_encryption_method_plugin() can be called wherever the plugin(s) are needed.
This approach can be repeated for each plugin type defined with CTools.
Next there needs to be an implementation of the new hook hook_encrypt_encryption_method_info():
function encrypt_encrypt_encryption_method_info() {
$plugins['basic'] = array(
'title' => t('Basic'),
'description' => t('This is the basic default encryption type that does not require any special extensions.'),
'encrypt callback' => 'encrypt_encryption_methods_basic',
'settings form' => 'encrypt_encryption_methods_basic_form',
'file' => backdrop_get_path('module', 'encrypt') . 'plugins/encryption_methods/default.inc',
);
return $plugins;
}
This replaces the plugin definition which is found in the plugins/encryption_methods directory. The callbacks in the plugin can remain there since there's now a file parameter to say where to find them. This replaces the need for implementing hook_ctools_plugin_directory().
For the callbacks it useful to add a helper function for fetching the function and ensuring it exists (this is similar to how ctool does it):
function encrypt_get_function(array $plugin_definition, $function_name) {
if (!empty($plugin_definition['file'])) {
require_once $plugin_definition['file'];
}
if (!isset($plugin_definition[$function_name])) {
return NULL;
}
$function = $plugin_definition[$function_name];
if (function_exists($function)) {
return $function;
}
return NULL;
}
Then encrypt_get_function() can get called before executing any of a plugin's callbacks, such as in an admin form. E.g.
$settings_form_function = encrypt_get_function(encrypt_encryption_method_plugin('basic'), 'settings form');
$form = $settings_form_function(array(), $plugin_form_state);
This covers the majority of the functionality of CTools plugin management. Since the conversion is custom-made for a module, it's possible to expand this further. For example, the plugin may need to also load a class which contains methods defined as callbacks in the plugin. For that the existing hook_autoload_info() Backdrop hook needs to be added to load the class along with custom integration into the plugin. For an example of this look at the Entity Reference module in core.