Working With Plugins in Drupal 8

Justin Langley
|
November 10, 2017
Image
Drupal 8 logo with a plug

Drupal 8 has overhauled the way you add pieces of custom functionality, or the way you extend current functionality. With Plugins, creating new features in Drupal 8 is a breeze!

Plugins in Drupal 8

Drupal 8 has replaced quite a few internal "hook" pieces (let's be real, more like most of it) in favor of implementing an OO class-based system. Plugins are the new hook_{something}_info. If you have used hook_block_info, hook_extra_fields_info, etc etc any other info hook then plugins will seem vaguely familiar.

Plugins are the way to make swappable pieces of functionality for a particular system. Field Widgets are plugins, blocks are plugins, views filters/fields/relationships etc etc are all Plugins now! And Plugins are awesome.

The Plugin System has 3 primary components:
Plugin Types
Plugin Discovery
Plugin Factory

Plugin Types:
Plugin types are the way you can define a "system" of plugins. Some example are Field Widgets, Field Formatters, Views Filters, and so on.

Plugin Discovery:
These are the definitions of how Plugins of a certain type are discovered in your code base. I.e. the Field Formatter plugin type will find any Plugins placed in the src/Plugin/Field/FieldFormatter directory of any enabled module.

Plugin Factory:
The Factory is used to instantiate a specific plugin for a given use case. I.e. when you use the Entity Reference field formatter you can edit the options (view mode, wrapper classes, etc etc) and save those options. What happens is you are saving an instance of the Entity Reference plugin with the chosen options.

Let's run through a quick example to show how awesome plugins are.


 

What to do and where to start?

Let's say we want to make a custom field formatter for entity reference fields. Our ideal functionality is to let an admin configure the plugin to do these things:

  1. Render out the entities as links where the clicked link will make an AJAX request to retrieve that node and inject it into the page.
  2. Decide on what display mode the entities should be rendered as.

Sounds like a neat piece of functionality right? Or least... it sounds neat to me. Moving on!

First, let's spin up a fresh Drupal 8 site (I'll assume you can already do this) using 8.4! Once that's done let's start by using Drupal console to scaffold a new module!

drupal gm (generate:module)

And let's go with these options through the walkthrough:

  1. Module name: AJAX Load Entity
  2. Machine name: (use recommended)
  3. Module path: (use recommended)
  4. Module description: (Put whatever you want here!)
  5. Package name: (use recommended)
  6. Core version: (use recommended)
  7. Generate module file: yes
  8. Feature?: no
  9. Composer.json?: yes
  10. Dependencies?: no
  11. Unit test class?: yes
  12. Themeable template?: no
  13. Confirm generation?: yes

Great! Now we have a nice blank module ready for us to start using!


 

Generate more things!

Now that we have an empty module ready for using, let's use Drupal Console to also generate an empty plugin for us to start using.

drupal gpff (generate:plugin:fieldformatter)

Let's put in these options:

  1. Module name: ajax_load_entity (the module machine name from step 1, should autocomplete for you)
  2. Class name: AjaxLoadEntityFormatter
  3. Plugin label: (use recommended)
  4. Plugin ID: (use recommended)
  5. Field type: entity_reference
  6. Confirm?: yes

And now you will see that a new file was created in [module_name]/src/Plugin/Field/FieldFormatter/AjaxLoadEntityFormatter.php This is located here because the Field Formatter Plugin Discovery, provided by Core, looks inside the src/Plugin/Field/FieldFormatter directory of any enabled module to find FieldFormatter plugins.

Now let's crack open that AjaxLoadEntityFormatter.php class and see what's inside!

You will notice it's mostly boilerplate code with not much going on. The good news is that with classes we can simply extend a class that has most of the functionality we want to get started. There is already a field formatter for Entity Reference field where you can display the label of the entity and even link the label to the entity. That's like half of what we need to do already! So find the EntityReferenceLabelFormatter.php file inside {your local site's root folder}/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter directory and copy all of the code inside that file. Then paste it inside our new file.

Now let's update the namespace declaration at the top of the file from:
namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
to
namespace Drupal\ajax_load_entity\Plugin\Field\FieldFormatter;

And then let's update the class declaration from:
class EntityReferenceLabelFormatter extends EntityReferenceFormatterBase
to
class AjaxLoadEntityFormatter extends EntityReferenceLabelFormatter

And finally we should update the @FieldFormatter spec block from:

/**
 * Plugin implementation of the 'entity reference label' formatter.
 *
 * @FieldFormatter(
 *   id = "entity_reference_label",
 *   label = @Translation("Label"),
 *   description = @Translation("Display the label of the referenced entities."),
 *   field_types = {
 *     "entity_reference"
 *   }
 * )
 */

to

/**
 * Plugin implementation of the 'ajax load entity' formatter.
 *
 * @FieldFormatter(
 *   id = "ajax_load_entity",
 *   label = @Translation("Ajax load entity"),
 *   description = @Translation("Display the labels of the referenced entities that when clicked will AJAX inject the entity onto the page."),
 *   field_types = {
 *     "entity_reference"
 *   }
 * )
 */

And now we have our new Formatter that does exactly the same thing as the Entity Reference Label Formatter. Now we do the fun stuff! Let's define a new route that will accept several parameters to fetch a node in a specific view mode.

Add a ajax_load_entity.routing.yml file to our module. Inside this file let's add this route (if you haven't used routing in D8 yet, here are some neat docs on how routing works in D8 (routing, params, upcasting ). It replaces the use of hook_menu essentially.)

ajax_load_entity.load_entity:
  path: '/ajax_load_entity/{method}/{entity_type}/{entity}/{view_mode}'
  defaults:
    _controller: '\Drupal\ajax_load_entity\Controller\AjaxLoadEntityController::getEntity'
  requirements:
    _permission: 'access content'
    method: 'nojs|ajax'
  options:
    parameters:
      entity:
        type: entity:{entity_type}

So here we are defining a route that accepts multiple parameters in the URL, which we will then use to fetch the proper entity in the needed view mode. The couple "special" pieces going on here is the {method} parameter and the {entity_type} and {entity}. By default this parameter will be nojs in the URL and will dynamically get converted to ajax with the Drupal AJAX library if a user has Javascript enabled. This allows us to handle the situation where someone has Javascript turned off so we cannot make an AJAX call to request the node, we can instead simply redirect the user to the node.

For the {entity_type} and {entity} using upcasting we can tell Drupal to load a full entity into the {entity} parameter (this is some fancy magic using a ParamCoverter that Drupal provides out of the box. The documentation for it is here). In the options section of the route we are telling it that the parameter {entity} is going to be of the type defined by the {entity_type} parameter.

So now let's implement the controller we defined in the defaults key of that YAML file.

Inside ajax_load_entity/src/Controller directory (create that directory if it doesn't exist) let's create a file called AjaxLoadEntityController.php. And then let's add a function like so:

  /**
   * This function will fetch a loaded entity of the requested type in the requested view mode.
   *
   * @param $method
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   * @param \Drupal\Core\Entity\EntityInterface $entity
   * @param \Drupal\Core\Entity\EntityViewModeInterface $view_mode
   *
   * @return mixed $response
   */
  public function getEntity($method, $entity_type, EntityInterface $entity, $view_mode) {
    // If nojs is the method we will need to redirect the user.
    $redirect = $method === 'nojs';

    if (!$redirect) {
      // We have javascript so let's grab the entityViewBuilder service
      $view_builder = $this->entityTypeManager()->getViewBuilder($entity_type);

      // Get the render array of this entity in the specified view mode.
      $render = $view_builder->view($entity, $view_mode);

      // To workaround the issue where the ReplaceCommand actually REMOVES the HTML element
      // selected by the selector given to the ReplaceCommand, we need to wrap our content
      // in a div that same ID, otherwise only the first click will work. (Since the ID will
      // no longer exist on the page)
      $build = [
        '#type' => 'container',
        '#attributes' => [
          'id' => 'ajax-load-entity',
        ],
        'entity' => $render,
      ];

      // Now we return an AjaxResponse with the ReplaceCommand to place our entity on the page.
      $response = new AjaxResponse();
      $response->addCommand(new ReplaceCommand('#ajax-load-entity', $build));
    } else {
      // Javascript is not working/disabled so let's just route the person to the canonical
      // route for the entity.
      $response = new RedirectResponse(Url::fromRoute("entity.{$entity_type}.canonical", ["{$entity_type}" => $entity->id()]), 302);
    }

    return $response;
  }

The comments should describe what we are doing there. So now, when this route is hit, it will return the entity to be injected onto the page, sweet!

Now the last thing to do, is to update our formatter to render out the links as paths to our new route, and then create an HTML wrapper where the entities will be injected in to.

So in our src/Plugin/Field/FieldFormatter/AjaxLoadEntityFormatter.php let's open it up and update the viewElements function:

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    $elements = [];
    // Get the view mode picked on the manage display page.
    $view_mode = $this->getSetting('view_mode');

    // Now we need to loop over each entity to be rendered and create a link element
    // for each one.
    foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
      if (!$entity->isNew()) {
        // Set up the options for our route, we default the method to 'nojs' since
        // the drupal ajax library will replace that for us.
        $options = [
          'method' => 'nojs',
          'entity_type' => $entity->getEntityTypeId(),
          'entity' => $entity->id(),
          'view_mode' => $view_mode
        ];

        // Now we create the path from our route, passing the options it needs.
        $uri = Url::fromRoute('ajax_load_entity.load_entity', $options);

        // And create a link element. We need to add the 'use-ajax' class so that
        // Drupal's core AJAX library will detect this link and ajaxify it.
        $elements[$delta] = [
          '#type' => 'link',
          '#title' => $entity->uuid(),
          '#url' => $uri,
          '#options' => $uri->getOptions() + [
            'attributes' => [
              'class' => [
                'use-ajax'
              ],
            ]
          ]
        ];

        if (!empty($items[$delta]->_attributes)) {
          $elements[$delta]['#options'] += ['attributes' => []];
          $elements[$delta]['#options']['attributes'] += $items[$delta]->_attributes;
          // Unset field item attributes since they have been included in the
          // formatter output and shouldn't be rendered in the field template.
          unset($items[$delta]->_attributes);
        }
      } else {
        continue;
      }
      $elements[$delta]['#cache']['tags'] = $entity->getCacheTags();
    }

    // Now we add the container to render after the links. This is where the AJAX
    // loaded content will be injected in to.
    $elements[] = [
      '#type' => 'container',
      '#attributes' => [
        'id' => 'ajax-load-entity',
      ],
    ];

    // Make sure the AJAX library is attached.
    $elements['#attached']['library'][] = 'core/drupal.ajax';
    return $elements;
  }

And next we will update the settings form to allow the administrator to pick the view mode. We can actually pick the functionality from the core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php file.

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form, FormStateInterface $form_state) {
    $elements['view_mode'] = [
      '#type' => 'select',
      '#options' => $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type')),
      '#title' => t('View mode'),
      '#default_value' => $this->getSetting('view_mode'),
      '#required' => TRUE,
    ];

    return $elements;
  }

  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    $summary = [];

    $view_modes = $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type'));
    $view_mode = $this->getSetting('view_mode');
    $summary[] = t('Rendered as @mode', ['@mode' => isset($view_modes[$view_mode]) ? $view_modes[$view_mode] : $view_mode]);

    return $summary;
  }

The settingsForm function is called when a plugin is going to be configured. I.e. when you go to Manage Display and click the gear icon to configure the settings for the formatter. Then the settingsSummary function is called when you save the configurations and you get the textual output of configured options. I.e. when using the Rendered entity formatter after selecting the view mode you see the text Rendered as {view_mode}.

However, if you are using an IDE with predictive capabilities it should point out that $this->entityDisplayRepository does not exist. That's because the formatter we are extending from doesn't inject a service we need. So, instead of injecting the service ourselves (which we could!), we can switch which formatter we extend from!

Update the class definition from:

class AjaxLoadEntityFormatter extends EntityReferenceLabelFormatter

to

class AjaxLoadEntityFormatter extends EntityReferenceEntityFormatter

And then update this use statement:

use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceLabelFormatter;

to

use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter;


 

Wrapping up

You should now install the module and see if it all worked! (I used Devel to generate some content and added an entity reference field to Basic Pages to reference Articles). You should see a rendered list of links and if you click the link it should AJAX load that entity onto the page!

Plugins in Drupal 8 make extending and creating functionality considerably easier than ever before. Now get to creating!

Want to talk about how we can work together?

Ryan can help

Ryan Wyse
CEO