The big move to Drupal 8 presents many challenges and opportunities to developers community. Naturally, we like new things and meddling with code. But we also don't want leave behind what worked well in D7. In this blog, I investigated porting modules from D7 to D8 and tested my approach on a simple use case. 

As I wanted to explore Drupal 8 a bit more, and especially module development, I wanted to update a very simple module to Drupal 8. 

The module I ported over was ‘image_max_size_crop’ a small module i once created for Drupal 7. It allows you to only specify one dimension, and doesn’t do anything when the image has a smaller size.

As a starting point, hook_image_effect_info() is gone, those are replaced by plugins. So I began my search on https://www.drupal.org/list-changes/drupal the change record page. This pointed me to https://www.drupal.org/node/2072699. The change record describing the conversion to plugins.

I’ve also looked at the core image effects (scale / crop / resize) in the image module, and to Image effects, a D8 module which adds more image effects.

First task described in the change record was:

  • hook_effect_info() → replaced by annotation-based plugin discovery, using the \Drupal\image\Annotation\ImageEffect annotation class. 

I created my class in the /src/Plugin/ImageEffect folder of my module, added the annotation, and it was discovered on the the image style overview page. 

<?php

namespace Drupal\image_max_size_crop\Plugin\ImageEffect;

use Drupal\Core\Image\ImageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\image\ConfigurableImageEffectBase;

/**
 * Crop an image resource with respect for maximum size, and with only one dimension required.
 *
 * @ImageEffect(
 *   id = "image_max_size_crop",
 *   label = @Translation("Maximum size crop"),
 *   description = @Translation("Cropping will remove portions of an image to make it the specified dimensions. This style only resizes when the image dimension(s) is larger than the spedified dimension(s).")
 */
class MaxSizeCropImageEffect extends ConfigurableImageEffectBase {

  /**
   * {@inheritdoc}
   */
  public function applyEffect(ImageInterface $image) {
    return TRUE;
  }
}

It is important to know is that you have to clear the cache, even when you have some types of caching disabled for development. Also, you need to add the applyEffect method, since that’s required by the base class. We start with an empty method, and will implement it later. The change record didn’t mention the type of the $image object passed to it. This is ImageInterface, and requires the use Drupal\Core\Image\ImageInterface; statement.

Next step - style and behavior:

Now, let’s make the image style do something, and behave as desired. This is actually where the OOP used in D8 plugin’s made the module easier. This module actually acts exactly like the crop image style. Only 2 differences actually. Only one of the dimension sizes is required, and it doesn’t upcrop. So instead of extending the base class, we can better extend the CropImageEffect class and only make the adjustments needed. So we can adjust to code to be like:

/**
 * Crop an image resource with respect for maximum size, and with only one dimension required.
 *
 * @ImageEffect(
 *   id = "image_max_size_crop",
 *   label = @Translation("Maximum size crop"),
 *   description = @Translation("Cropping will remove portions of an image to make it the specified dimensions. This style only resizes when the image dimension(s) is larger than the specified dimension(s).")
 */
class MaxSizeCropImageEffect extends CropImageEffect {}

First up, is making only one of the dimensions required. This includes two adjustments, removing the required on the form element, and add an extra validation to check at least one of them is filled in. So we need to adjust the form and add an extra validation. This can be done by implementing / adjusting the following methods:

/**
 * {@inheritdoc}
 */
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
 $form = parent::buildConfigurationForm($form, $form_state);
 unset($form['width']['#required']);
 unset($form['height']['#required']);
 return $form;
}

/**
 * {@inheritdoc}
 */
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
 parent::validateConfigurationForm($form, $form_state);
 if ($form_state->isValueEmpty('width') && $form_state->isValueEmpty('height')) {
   $form_state->setErrorByName('data', $this->t('Width and height can not both be blank.'));
 }
}

The only thing left is to adjust the effect itself. What we need to do is ‘determine the sizes which the image has to become, taking the max of the current image in effect, and only then do the cropping itself.

/**
 * {@inheritdoc}
 */
public function applyEffect(ImageInterface $image) {
  // Set the desired dimensions, if the width is empty or not high enough.
  $original_width = $this->configuration['width'];
  if (!$this->configuration['width'] || $this->configuration['width'] > $image->getWidth()) {
    $this->configuration['width'] = $image->getWidth();
  }

  // Set the desired dimensions, if the height is empty or not high enough.
  $original_height = $this->configuration['height'];
  if (!$this->configuration['height'] || $this->configuration['height'] > $image->getHeight()) {
    $this->configuration['height'] = $image->getHeight();
  }

  $result = parent::applyEffect($image);

  // Restore configuration so that settings screen is shown correctly.
  $this->configuration['width'] = $original_width;
  $this->configuration['height'] = $original_height;

  return $result;
}

Key benefits and differences between D7 and D8 modules:

At the moment, the D8 module is already much simpler since we can use the power of extending classes, resulting in usage of already existing code. In line numbers the D8 module has 66 LOC, versus 214 LOC in the D7 module. (That’s a 70% reduction). But perhaps what's even more interesting, it’s only 3 methods now (make it 4 if we add the annotation code) while the D7 module has 7 functions implemented.

Overall I’m mostly excited about how this is going to make customizations easier. If you want to have a plugin which behaves slightly different, there is no need to copy it and adjust it anymore, just extend and override the needed methods. And since lots of things have become plugins in D8, we’re going to be able to use this in many more places.

Get in touch to discuss more Drupal with the Wunder Group.

Tim Mallezie, Developer at Wunderkraut Belgium