The Drupal 8 plugin system - part 4

We defined what a plugin is, discussed some plugins used in core and wrote our own custom plugin previously. We shall tune it up a bit in this post.

Real world plugins have a lot more properties than the label property mentioned in our breakfast plugin. To make our plugin more "real world", we introduce 2 properties, image and ingredients. It makes more sense now to have a custom annotation for breakfast instead of using the default Plugin annotation.

How different are custom annotations from the usual Plugin annotation?

1) They convey more information about a plugin than what an @Plugin does. Here's a custom annotation for a views display plugin from search_api, taken from here.

/**
 * Defines a display for viewing a search's facets.
 *
 * @ingroup views_display_plugins
 *
 * @ViewsDisplay(
 *   id = "search_api_facets_block",
 *   title = @Translation("Facets block"),
 *   help = @Translation("Display the search result's facets as a block."),
 *   theme = "views_view",
 *   register_theme = FALSE,
 *   uses_hook_block = TRUE,
 *   contextual_links_locations = {"block"},
 *   admin = @Translation("Facets block")
 * )
 */

2) Custom annotations are a provision to document the metadata/properties used for a custom plugin. Check out this snippet from FieldFormatter annotation code for instance:


/** * A short description of the formatter type. * * @ingroup plugin_translatable * * @var \Drupal\Core\Annotation\Translation */ public $description; /** * The name of the field formatter class. * * This is not provided manually, it will be added by the discovery mechanism. * * @var string */ public $class; /** * An array of field types the formatter supports. * * @var array */ public $field_types = array(); /** * An integer to determine the weight of this formatter relative to other * formatter in the Field UI when selecting a formatter for a given field * instance. * * @var int optional */ public $weight = NULL;

That gave us a lot of information about the plugin and its properties. Now that you are convinced of the merits of custom annotations, let's create one.

Checkout the module code if you haven't already.

$ git clone git@github.com:badri/breakfast.git

Switch to the custom-annotation-with-properties tag.

$ git checkout -f custom-annotation-with-properties

and enable the module. The directory structure should look like this: Custom annotation directory structure

The new Annotation directory is of interest here. Custom annotations are defined here.

  /**
   * A glimspe of how your breakfast looks.
   *
   * This is not provided manually, it will be added by the discovery mechanism.
   *
   * @var string
   */
  public $image;

  /**
   * An array of igredients used to make it.
   *
   * @var array
   */
  public $ingredients = array();

Now, the plugin definition is changed accordingly. Here's the new annotation of the idli plugin.

/**
 * Idly! can't imagine a south Indian breakfast without it.
 *
 *
 * @Breakfast(
 *   id = "idly",
 *   label = @Translation("Idly"),
 *   image = "https://upload.wikimedia.org/wikipedia/commons/1/11/Idli_Sambar.JPG",
 *   ingredients = {
 *     "Rice Batter",
 *     "Black lentils"
 *   }
 * )
 */

The other change is to inform the Plugin Manager of the new annotation we are using. This is done in BreakfastPluginManager.php.

    // The name of the annotation class that contains the plugin definition.
    $plugin_definition_annotation_name = 'Drupal\breakfast\Annotation\Breakfast';

    parent::__construct($subdir, $namespaces, $module_handler, 'Drupal\breakfast\BreakfastInterface', $plugin_definition_annotation_name);

Let's tidy the plugin by wrapping it around an interface. Though this is purely optional, the interface tells the users of the plugin what properties it exposes. This also allows us to define a custom function called servedWith() whose implementation is plugin specific.

With the plugin class hierarchy looking like this now: breakfast plugin class hierarchy

The servedWith() is implemented differently for different plugin instances.

// Idly.php
  public function servedWith() {
    return array("Sambar", "Coconut Chutney", "Onion Chutney", "Idli podi");
  }
// MasalaDosa.php
  public function servedWith() {
    return array("Sambar", "Coriander Chutney");
  } 

We make use of the interface functions we wrote in the formatter while displaying details about the user's favorite breakfast item.

// BreakfastFormatter.php
    foreach ($items as $delta => $item) {
      $breakfast_item = \Drupal::service('plugin.manager.breakfast')->createInstance($item->value);
      $markup = '<h1>'. $breakfast_item->getName() . '</h1>';
      $markup .= '<img src="'. $breakfast_item->getImage() .'"/>';
      $markup .= '<h2>Goes well with:</h2>'. implode(", ", $breakfast_item->servedWith());
      $elements[$delta] = array(
        '#markup' => $markup,
      );
    }

And the user profile page now looks like this.

user profile for breakfast plugin

Derivative plugins

We have Idly plugin instance mapped to the Idly class and Uppuma instance mapped to the Uppuma class. Had all the plugin instances been mapped to a single class, we would have got derivative plugins. Derivative plugins are plugin instances derived from the same class. They are employed under these scenarios: 1. If one plugin class per instance is an overkill. There are times where you don't want to define a class for each plugin instance. You just say that it is an instance of a particular class that is already defined. 2. You need to dynamically define plugin instances. The Flag module defines a different Flag Type plugin instance for different entities. The entities in a Drupal site are not known beforehand and hence we cannot define one instance each for every entity. This calls for a plugin derivative implementation.

Lets add derivative plugins to our breakfast metaphor.

$ git checkout -f plugin-derivatives

Here's a new user story for the breakfast requirement. We are going to add desserts to our menu now. All desserts are of type Sweet. So, we define a derivative plugin called Sweet which is based on breakfast.

This calls for 3 changes as shown in the new directory structure outlined below:

derivative plugins

1) Define the Sweet plugin instance class on which all our desserts are going to be based on.

/**
 * A dessert or two whould be great!
 *
 *
 * @Breakfast(
 *   id = "sweet",
 *   deriver = "Drupal\breakfast\Plugin\Derivative\Sweets"
 * )
 */
class Sweet extends BreakfastBase {
  public function servedWith() {
    return array();
  }
}

Note the "deriver" property in the annotation.

2) Next, we define the deriver in the Derivative directory.

/**
 * Sweets are dynamic plugin definitions.
 */
class Sweets extends DeriverBase {

  /**
   * {@inheritdoc}
   */
  public function getDerivativeDefinitions($base_plugin_definition) {
    $sweets_list = drupal_get_path('module', 'breakfast') . '/sweets.yml';
    $sweets = Yaml::decode(file_get_contents($sweets_list));

    foreach ($sweets as $key => $sweet) {
      $this->derivatives[$key] = $base_plugin_definition;
      $this->derivatives[$key] += array(
        'label' => $sweet['label'],
        'image' => $sweet['image'],
        'ingredients' => $sweet['ingredients'],
      );
    }

    return $this->derivatives;
  }
}

3) The derivative gets sweets info from the sweets.yml present in the module root directory. This could even be an XML/JSON or any file format which holds metadata. I've used a YAML file for consistency's sake.

mysore_pak:
  label: 'Mysore Pak'
  image: 'https://upload.wikimedia.org/wikipedia/commons/f/ff/Mysore_pak.jpg'
  ingredients:
    - Ghee
    - Sugar
    - Gram Flour

jhangri:
  label: 'Jhangri'
  image: 'https://upload.wikimedia.org/wikipedia/commons/f/f8/Jaangiri.JPG'
  ingredients:
    - Urad Flour
    - Saffron
    - Ghee
    - Sugar

The class hierarchy for derivative plugins looks a little big bigger now.

Derivative plugins class hierarchy

Clear your cache and you must be able to see the sweets defined in the yaml file in the breakfast choice dropdown of the user profile.

new derivative plugins

Enjoy your dessert!

That concludes our tour of Drupal 8 plugins. Hope you liked it and learnt something. Stay tuned for the next series about services.