The plugin system provides means to create specialized objects in Drupal that do not require the robust features of the entity system.
In this recipe, we will create a new plugin type called Unit
that will work with units of measurement and conversions. We will create a plugin manager, default plugin interface, YAML discovery
method, base class, and plugin definition.
This recipe is based on the work being done to export the Physical
module to Drupal 8. The Physical
module provides a way to work with units of volume, weight, and dimensions and attaches them to entities. It discovers unit plugins in the same way that the Breakpoint
module discovers breakpoint plugins.
Create a new module like the one existing in the first recipe. We will refer to the module as mymodule
throughout the recipe. Use your module's appropriate name.
src
directory called UnitManager.php
. This will hold the UnitManager
class.UnitManager
class by extending the DrupalCorePluginDefaultPluginManager
class:<?php /** * @file * Contains DrupalmymoduleUnitManager. */ namespace Drupalmymodule; use DrupalCorePluginDefaultPluginManager; use DrupalCoreCacheCacheBackendInterface; use DrupalCoreExtensionModuleHandlerInterface; class UnitManager extends DefaultPluginManager { }
<?php /** * @file * Contains DrupalmymoduleUnitManager. */ namespace Drupalmymodule; use DrupalCorePluginDefaultPluginManager; use DrupalCoreCacheCacheBackendInterface; use DrupalCoreExtensionModuleHandlerInterface; class UnitManager extends DefaultPluginManager { /** * Default values for each unit plugin. * * @var array */ protected $defaults = [ 'id' => '', 'label' => '', 'unit' => '', 'factor' => 0.00, 'type' => '', 'class' => 'DrupalmymoduleUnit', ]; }
Unit
class in our module that unit plugins will be instances of.DrupalCorePlugin|DefaultPluginManager
class constructor to define the module handler and cache backend:<?php /** * @file * Contains DrupalmymoduleUnitManager. */ namespace Drupalmymodule; use DrupalCorePluginDefaultPluginManager; use DrupalCoreCacheCacheBackendInterface; use DrupalCoreExtensionModuleHandlerInterface; class UnitManager extends DefaultPluginManager { /** * Default values for each unit plugin. * * @var array */ protected $defaults = [ 'id' => '', 'label' => '', 'unit' => '', 'factor' => 0.00, 'type' => '', 'class' => 'DrupalphysicalUnit', ]; /** * Constructs a new DrupalmymoduleUnitManager object. * * @param DrupalCoreCacheCacheBackendInterface $cache_backend * Cache backend instance to use. * @param DrupalCoreExtensionModuleHandlerInterface $module_handler * The module handler to invoke the alter hook with. */ public function __construct(CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { $this->moduleHandler = $module_handler; $this->setCacheBackend($cache_backend, 'physical_unit_plugins'); } }
getDiscovery
method. We need to implement a YAML discovery
method:<?php /** * @file * Contains DrupalmymoduleUnitManager. */ namespace Drupalmymodule; use DrupalCorePluginDefaultPluginManager; use DrupalCoreCacheCacheBackendInterface; use DrupalCoreExtensionModuleHandlerInterface; class UnitManager extends DefaultPluginManager { /** * Default values for each unit plugin. * * @var array */ protected $defaults = [ 'id' => '', 'label' => '', 'unit' => '', 'factor' => 0.00, 'type' => '', 'class' => 'DrupalmymoduleUnit', ]; /** * Constructs a new DrupalmymoduleUnitManager object. * * @param DrupalCoreCacheCacheBackendInterface $cache_backend * Cache backend instance to use. * @param DrupalCoreExtensionModuleHandlerInterface $module_handler * The module handler to invoke the alter hook with. */ public function __construct(CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { $this->moduleHandler = $module_handler; $this->setCacheBackend($cache_backend, 'physical_unit_plugins'); } /** * {@inheritdoc} */ protected function getDiscovery() { if (!isset($this->discovery)) { $this->discovery = new YamlDiscovery('units', $this->moduleHandler->getModuleDirectories()); $this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery); } return $this->discovery; } }
YamlDiscovery,
we are telling Drupal to look for a *.units.yml
file in all the module directories.mymodule.services.yml
in your module's directory. This will describe our plugin manager to Drupal, allowing a plugin discovery:services: plugin.manager.unit: class: DrupalmymoduleUnitManager arguments: ['@container.namespaces', '@cache.discovery', '@module_handler']
Unit
plugins that implement this interface. Create a UnitInterface.php
file in your module's src
directory to hold the interface:<?php /** * @file * Contains DrupalmymoduleUnitInterface. */ namespace Drupalmymodule; /** * Interface UnitInterface. */ interface UnitInterface { /** * Returns the unit's label. * * @return string * The unit's label. */ public function getLabel(); /** * Returns the unit abbreviation. * * @return string * The abbreviation. */ public function getUnit(); /** * Returns the factor amount for conversions. * * @return int|float * The factor amount. */ public function getFactor(); /** * Converts a value to the base unit. * * @param int|float $value * The amount to convert. * * @return int|float * The converted amount. */ public function toBase($value); /** * Converts value from base unit to current unit. * * @param int|float $value * The amount to convert. * * @return int|float * The converted amount. */ public function fromBase($value); /** * Rounds a value. * * @param int|float $value * The value to round. * * @return int|float * The rounded value. */ public function round($value); }
Unit
plugin and have an output, regardless of the logic behind each method. It pushes for encapsulation when working with plugins.mymodule.units.yml
file to provide default unit plugin definitions:centimeters: label: Centimeters unit: cm factor: 1E-2 type: dimensions meters: label: Meters unit: m factor: 1 type: dimensions feet: label: Feet unit: ft factor: 3.048E-1 type: dimensions inches: label: Inches unit: in factor: 2.54E-2 type: dimensions
Unit
class. Create Unit.php
in your module's src
directory. This class will implement our UnitInterface
interface:<?php /** * @file * Contains DrupalmymoduleUnit. */ namespace Drupalmymodule; use DrupalCorePluginPluginBase; /** * Class Unit. */ class Unit extends PluginBase implements UnitInterface { /** * {@inheritdoc} */ public function getFactor() { return (float) $this->pluginDefinition['factor']; } /** * {@inheritdoc} */ public function getLabel() { return $this->t($this->pluginDefinition['label'], array(), array('context' => 'unit')); } /** * {@inheritdoc} */ public function getUnit() { return $this->pluginDefinition['unit']; } /** * {@inheritdoc} */ public function toBase($value) { return $this->round($value * $this->getFactor()); } /** * {@inheritdoc} */ public function fromBase($value) { return $this->round($value / $this->getFactor()); } /** * {@inheritdoc} */ public function round($value) { return round($value, 5); } /** * Returns the unit's label. * * @return string * Unit label. */ public function __toString() { return $this->getLabel(); } }
toBase
and fromBase
methods allow us to convert the unit's value from its defined factor
value.Unit
plugin is now implemented and can be integrated through a custom field type or another custom code.Drupal 8 implements a service container, a concept adopted from the Symfony framework. In order to implement a plugin, there needs to be a manager who can discover and process plugin definitions. This manager is defined as a service in a module's services.yml
with its required constructor parameters. This allows the service container to initiate the class when it is required.
In our example, the UnitManager plugin manager discovers the Unit
plugin definitions in YAML files that modules provide. After the first discovery, all the known plugin definitions are then cached under the physical_unit_plugins
cache key.
Plugin managers also provide a method for returning these definitions or creating an object instance based on an available definition. The instance is created from the class
key that we defined in our plugin's default definition. This also allows a developer to use a custom class to provide an extended Unit
plugin as long as it extends the default Unit
class or implements the UnitInterface
interface.
An example usage would be to create a custom form that allows users to convert values. The following code can be placed in the submit
method and will allow us to load our plugin for feet
and return the value in meters:
// Load the manager service. $unit_manager = Drupal::service('plugin.manager.unit'); // Create a class instance through the manager. $feet_instance = $unit_manager->createInstance('feet'); // Convert 12ft into meters. $meters_value = $feet_instance->toBase(12);
Plugin managers have the ability to define an alter hook. The following line of code will be added to the UnitManager
class's constructor to provide hook_physical_unit_alter
. This is passed to the module handler service for invocations:
/**
* Constructs a new DrupalmymoduleUnitManager object.
*
* @param DrupalCoreCacheCacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param DrupalCoreExtensionModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
$this->moduleHandler = $module_handler;
$this->alterInfo('physical_unit');
$this->setCacheBackend($cache_backend, 'physical_unit_plugins');
}
Modules implementing hook_physical_unit_alter
in the .module
file have the ability to modify all the discovered plugin definitions. Modules have the ability to remove defined plugin entries or alter any information provided for the annotation definition.
Plugins can use a cache backend to improve performance. This can be done by specifying a cache backend with the setCacheBackend
method in the manager's constructor. The following line of code will allow the Unit
plugins to be cached and only discovered on a cache rebuild.
The $cache_backend
variable is passed to the constructor. The second parameter provides the cache key. The cache key will have the current language code added as a suffix.
There is an optional third parameter that takes an array of strings to represent cache tags that will cause the plugin definitions to be cleared. This is an advanced feature and plugin definitions should normally be cleared through the manager's clearCachedDefinitions
method. The cache tags allow the plugin definitions to be cleared when a relevant cache is cleared as well.
Plugins are loaded through the manager service, which should always be accessed through the service container. The following line of code will be used in your module's hooks or classes to access the plugin manager:
$unit_manager = Drupal::service('plugin.manager.unit');
Plugin managers have various methods to retrieve plugin definitions, which are as follows:
getDefinitions
: This method will return an array of plugin definitions. It first makes an attempt to retrieve cached definitions, if any, and sets the cache of discovered definitions before returning them.getDefinition
: This takes an expected plugin ID and returns its definition.createInstance
: This takes an expected plugin ID and returns an initiated class for the plugin.getInstance
: This takes an array that acts as a plugin definition and returns an initiated class from the definition.