Third & GroveThird & Grove
Jun 9, 2017 - Spencer Leopold

A high-level overview of the Plugin system in Drupal 8

By now you've probably had the chance to get your feet wet with some Drupal 8, and have probably heard the term "Plugins," but just aren't quite sure what they are. If you've looked through the Drupal backend hoping to find any to shed some light on it, chances are you still have no clue.

The easiest way to describe Plugins are that they're "Entity Types" for your code. For instance, you have your Node (Content) entity type, and then all of your different bundles (article, basic page, etc) extending them. The same could be said for plugins. You have a "Plugin Type" and different plugins extending them.

Think of an e-commerce checkout page. A page with a bunch of different panes to collect information, and each pane kind of works the same, but they might need to do things behind the scenes very differently. Now you could create custom blocks for each pane, but the problem there is you're going to end up duplicating all of the core functionality each time. A better approach would be to leverage the Plugin system, and create a single plugin type that each pane would extend upon. We know each pane is going to have a form inside, and maybe a heading, and they're all definitely going to have to display a summary on the checkout review page. So using blocks here would just be way too much work.

The first step in creating a plugin is to first create its manager, src/MyFormPaneManager.php. The manager is used so Drupal knows how to instantiate your plugin. If you need to do anything special to load each plugin, you’d do that in here.

<?php
 
namespace Drupal\my_module;
 
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
 
class MyFormPaneManager extends DefaultPluginManager {
 
  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    parent::__construct('Plugin/MyFormPane, $namespaces, $module_handler, 'Drupal\my_module\Plugin\MyFormPane\MyFormPaneInterface', 'Drupal\my_module\Annotation\MyFormPane);
    $this->alterInfo('my_module_my_form_pane_info');
    $this->setCacheBackend($cache_backend, 'my_module_my_form_pane_plugins');
  }
 
}

 

The next step is defining a plugin interface, src/Plugin/MyFormPane/MyFormPaneInterface.php. All this is, is a definition of what each plugin should look like. You define all common method signatures, but no logic just yet. Each plugin of this type will have to implement this interface:

<?php
 
namespace Drupal\my_module\Plugin\MyFormPane;
 
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
 
interface MyFormPaneInterface extends PluginInspectionInterface, DerivativeInspectionInterface {
 
  public function getId();
 
  public function getLabel();
 
  public function getWrapperElement();
 
  public function buildPaneSummary();
 
  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form);
 
  public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form);
 
  public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form);
 
}

 

After that you would create a base class with all of the core functionality, src/Plugin/MyFormPane/MyFormPaneBase.php:

<?php
 
namespace Drupal\my_module\Plugin\MyFormPane;
 
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
 
abstract class MyFormPaneBase extends PluginBase implements MyFormPaneInterface {
 
  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->setConfiguration($configuration);
  }
 
  public function getConfiguration() {
    return $this->configuration;
  }
 
  public function setConfiguration(array $configuration) {
    $this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration);
  }
 
  public function defaultConfiguration() {
    return [];
  }
 
  public function getId() {
    return $this->pluginId;
  }
 
  public function getLabel() {
    return $this->pluginDefinition['label'];
  }
 
  public function getWrapperElement() {
    return $this->pluginDefinition['wrapper_element'];
  }
 
  public function buildPaneSummary() {
    return [];
  }
 
  public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {}
 
  public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {}
 
}

 

The last step (or the first step depending on your preference) is to define your plugin type with an annotation class, src/Annotation/MyFormPane.php. Without this, Drupal won't be able to discover your custom plugin types:

<?php
 
namespace Drupal\my_module\Annotation;
 
use Drupal\Component\Annotation\Plugin;
 
class MyFormPane extends Plugin {
 
  /**
   * The plugin ID.
   *
   * @var string
   */
  public $id;
 
  /**
   * The label of the plugin.
   *
   * @var \Drupal\Core\Annotation\Translation
   *
   * @ingroup plugin_translatable
   */
  public $label;
 
  /**
   * The wrapper element to use when rendering the pane's form.
   *
   * E.g: 'container', 'fieldset'. Defaults to 'container'.
   *
   * @var string
   */
  public $wrapper_element;
 
}

 

From here you can create new plugins, like src/Plugins/MyFormPane/BillingInformation.php:

<?php
 
namespace Drupal\my_module\Plugin\MyFormPane;
 
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\Entity\User;
 
/**
 * Provides the contact information pane.
 *
 * @MyFormPane(
 *   id = "billing_information",
 *   label = @Translation("Billing information"),
 *   wrapper_element = "fieldset",
 * )
 */
class BillingInformation extends MyFormPaneBase implements MyFormPaneInterface {
 
  public function buildPaneSummary() {
  }
 
  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
  }
 
  public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
  }
 
  public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
  }
 
}

 

What's left out of this example is how these plugins would actually be added to a page. For that to work you'd need to build a controller that would handle finding each plugin definition (pluginManager->getDefinitions()) and creating them (pluginManager->createInstance()). Or even having another plugin type for the form itself, so you can create single page or multi-step forms. Hopefully this was enough to help you better understand what plugins are, how they work, and when to use them.