Creating a custom plugin type

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.

Getting ready

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.

How to do it…

  1. All plugins need to have a service that acts as a plugin manager. Create a new file in your module's src directory called UnitManager.php. This will hold the UnitManager class.
  2. Create the UnitManager class by extending the DrupalCorePluginDefaultPluginManager class:
    <?php
    
    /**
     * @file
     * Contains DrupalmymoduleUnitManager.
     */
    
    namespace Drupalmymodule;
    
    use DrupalCorePluginDefaultPluginManager;
    use DrupalCoreCacheCacheBackendInterface;
    use DrupalCoreExtensionModuleHandlerInterface;
    
    class UnitManager extends DefaultPluginManager {
    
    }
  3. When creating a new plugin type, it is recommended that the plugin manager provides a set of defaults for new plugins, if an item is missing. This is also useful to define the default class a plugin should use:
    <?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',
      ];
    
    }
  4. Later, we will create the Unit class in our module that unit plugins will be instances of.
  5. Next, we need to override the 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');
      }
    
    }
  6. We override the constructor so that we can specify a specific cache key. This allows plugin definitions to be cached and cleared properly; otherwise, our plugin manager will continuously read the disk to find plugins.
  7. We also need to override the 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;
      }
    
    }
  8. The default plugin manager implementation supports an annotated plugin discovery, such as field types, field widgets, and field formatters. By setting the discovery property to YamlDiscovery, we are telling Drupal to look for a *.units.yml file in all the module directories.
  9. The next step is to create a 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']
  10. Drupal utilizes services and dependency injection. By defining our class as a service, we are telling the application container how to initiate our class. This will allow us to retrieve the manager and access plugins even if another module replaces our defined plugin manager.
  11. Next, we will define the plugin interface that we defined in the plugin manager. The plugin manager will validate the 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);
    
    }
  12. We provide an interface so that we can guarantee that we have these expected methods when working with a Unit plugin and have an output, regardless of the logic behind each method. It pushes for encapsulation when working with plugins.
  13. Create a 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
  14. As defined in our plugin's default definition, we need to provide a 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();
      }
    }
  15. This class implements all the required methods defined in our interface. The toBase and fromBase methods allow us to convert the unit's value from its defined factor value.
  16. The Unit plugin is now implemented and can be integrated through a custom field type or another custom code.

How it works…

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

There's more

Specifying an alter hook

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.

Using a cache backend

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.

Accessing plugins through the manager

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.

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset