Skip to main content

Services in Drupal 8

Submitted by on Wed, 08/05/2015 - 08:42

Altering REST response data in Drupal 8

The article is a part of an ongoing series about rebuilding thegoodwood.dk into a single page application with Drupal 8 and AngularJS. You can find the main article here.

This article will show you two ways of overriding or extending services. What is a service? The definition from Symfony docs puts it like this:

Put simply, a Service is any PHP object that performs some sort of “global” task. It’s a purposefully-generic name used in computer science to describe an object that’s created for a specific purpose (e.g. delivering emails).

Services and dependency injection in Drupal 8 builds on top of the concept from Symfony. The concept is powerful. It makes us able to easily extend or override existing services provided by other modules and to define our own. And this is exactly what needs to be done to change the response of a REST request.

ServiceProvider: Overriding an existing service

While building a page of products for THEGOODWOOD, we needed a specific image style for the first image per product. By doing some research in core (var_dump() and die()), we figured out that the HAL module has a bunch of services that normalizes entities to arrays. So to change the response, we had to extend the right one. By more researching, we figured out that the FileEntityNormalizer was normalizing images. Guess that makes sense.

So how to overwrite this? Easy. First we need to create a new custom ServiceProvider. It must be a camel cased name of your module suffixed with “ServiceProvider”. Our module is called tgw_app, so the ServiceProvider is named TgwAppServiceProvider.php. The class must reside in the Drupal\your_module namespace. The following is the content of the ServiceProvider:

/**
 * @file
 * Contains \Drupal\tgw_app\TgwAppServiceProvider.
 */

namespace Drupal\tgw_app;

use \Drupal\Core\DependencyInjection\ServiceProviderBase;
use \Drupal\Core\DependencyInjection\ContainerBuilder;

/**
 * Overrides the class for the file entity normalizer from HAL.
 */
class TgwAppServiceProvider extends ServiceProviderBase {
  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    $definition = $container->getDefinition('serializer.normalizer.file_entity.hal');
    $definition->setClass('Drupal\tgw_app\Normalizer\FileEntityNormalizer');
  }

}

Next we need to create the actual implementation of the normalizer:

/**
 * @file
 * Contains \Drupal\tgw_app\Normalizer\FileEntityNormalizer.
 */

namespace Drupal\tgw_app\Normalizer;

use Drupal\hal\Normalizer\FileEntityNormalizer as HalFileEntityNormalizer;
use Drupal\image\Entity\ImageStyle;

/**
 * Converts the Drupal entity object structure to a HAL array structure.
 */
class FileEntityNormalizer extends HalFileEntityNormalizer {
 
  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\file\FileInterface';

  /**
   * {@inheritdoc}
   */
  public function normalize($entity, $format = NULL, array $context = array()) {
    $data = parent::normalize($entity, $format, $context);
    if ($entity->getEntityTypeId() === 'file' && strpos($entity->getMimeType(), 'image') === 0) {
      $data['_links']['self']['href_list'] = ImageStyle::load('list')->buildUrl($entity->getFileUri());
    }
    return $data;
  }

}

As you might notice, this is just an extension of the existing FileEntityNormalizer from the HAL module. The $supportedInterfaceOrClass property defines what interface or class it supports.
So what’s going on here? Basically the normalize method looks for file entities with image as part of the mime type, and for all these it adds a link to the required image style.

Now my image style is available in the response:

"http://thegoodwooddk.dev/rest/relation/node/product/field_images": [
  {
    "href": "http://thegoodwooddk.dev/sites/default/files/images/products/thegoodwood-filippa-bord1_0.jpg",
    "href_list": "http://thegoodwooddk.dev/sites/default/files/styles/list/public/images/products/thegoodwood-filippa-bord1_0.jpg?itok=j7k6IPbd"
  },
  {
    "href": "http://thegoodwooddk.dev/sites/default/files/images/products/thegoodwood-filippa-bord2_0.jpg",
    "href_list": "http://thegoodwooddk.dev/sites/default/files/styles/list/public/images/products/thegoodwood-filippa-bord2_0.jpg?itok=qw5swWXR"
  }
],

Services.yml: Defining a new service

This next example is not from THEGOODWOOD, but some might find this method useful anyway.

I’m working on another project, some sort of webforms, but for Drupal 8. The data model is basically that we have “Form element” entities (checkboxes, textareas, selects etc), and these entities can be referenced on a “Step” node. This way editors can create multistep forms from the backend.
This means that each step node, can have a lot of Form Elements referenced. The app is being built with AngularJS and each of these steps are loaded asynchronously through AJAX as JSON.

The default response of the reference field on a Step node from the GET request was this:

"field_step_elements": [
  {
    "target_id": "1"
  },
  {
    "target_id": "3"
  },
  {
    "target_id": "2"
  }
],

To get the content of all the referenced elements, it would in this case require three more requests. And since a step can have as many elements as the editor likes, the amount of additional requests could potentially be very big.

Not the best solution. A better solution would be if these entities were already normalized in the first request.

To solve this I created a new normalizer service. This is defined in a my_module.services.yml file, in my case steps.services.yml:

services:
  steps.normalizer.entity_reference_item.json:
    class: Drupal\steps\Normalizer\EntityReferenceItemNormalizer
    arguments: ['@rest.link_manager', '@serializer.entity_resolver']
    tags:
      - { name: normalizer, priority: 10 }

The important thing to notice here is the tags. To register this service as a normalizer, the service must be tagged with the name “normalizer”. The priority tag is optional (defaults to 0). I do not know the internals, but trial and error shows me that the highest prioritized normalizer is used to resolve the request. So if we where to activate the HAL module (which is disabled for this project), the normalizer from that module would compete with my custom normalizer.
The normalizer with the highest priority wins this battle.

Then the implementation of the new normalizer must be created:

/**
 * @file
 * Contains \Drupal\steps\Normalizer\EntityReferenceItemNormalizer.
 */

namespace Drupal\steps\Normalizer;

use Drupal\serialization\Normalizer\ComplexDataNormalizer;

/**
 * Converts the Drupal entity reference item object to HAL array structure.
 */
class EntityReferenceItemNormalizer extends ComplexDataNormalizer {
  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';

  /**
   * Implements \Symfony\Component\Serializer\Normalizer\NormalizerInterface::normalize()
   */
  public function normalize($field_item, $format = NULL, array $context = array()) {
    // If the target entity is not a Step Element, make the default normalize handle it.
    $target_entity = $field_item->get('entity')->getValue();
    if ($target_entity->getEntityTypeId() !== 'step_element') {
      return parent::normalize($field_item, $form, $context);
    }
    // If the parent entity passed in a langcode, unset it before normalizing
    // the target entity. Otherwise, untranslatable fields of the target entity
    // will include the langcode.
    $langcode = isset($context['langcode']) ? $context['langcode'] : NULL;
    unset($context['langcode']);
    $context['included_fields'] = array('uuid');
    // Normalize the target entity.
    return $this->serializer->normalize($target_entity, $format, $context);
  }

}

So this checks all entity reference fields. If the target entity is not a Form Element (actually machine name is "step_element"), then it just passes the normalization to the default normalizer, which would result in a target_id reference.
Otherwise it normalizes the entity right away.

This is a part of the new response. The point being that the referenced entity is now available in the same request:

"field_step_elements": [
  {
    "seid": [
      {
        "value": "1"
      }
    ],
    "uuid": [
      {
        "value": "b7ee80b3-c883-4b98-a03c-60af54e713f1"
      }
    ],
    "type": [
      {
        "target_id": "text"
      }
    ],
    "langcode": [
      {
        "value": "en"
      }
    ],

Conclusion

The concept of services is powerful. As the Symfony documentations so well put it - this is not a Symfony or even a PHP concept, but a common programming concept used in many languages. And no wonder why. With very few lines of code, we can easily change behavior throughout an entire app. And we can define services, which can be re-used across our code in a flexible, decoupled and easy testable way.
Not much more to say, other than the introduction of services (and Symfony) into Drupal is a big leap forward both for Drupal and you as a developer.

Comment? Tweet me