What I Learned Porting a Module to Drupal 8

  • 12 minute read

Earlier this year, I wrote a very small module to extend Views Slideshow and provide a new kind of pager. For those of you are not aware of what Views Slideshow is:

Views Slideshow can be used to create a slideshow of any content (not just images) that can appear in a View. Powered by jQuery, it is heavily customizable: you may choose slideshow settings for each View you create.

As Drupal 8 started to become more stable, I wanted to upgrade my module. Unfortunately, this relies on Views Slideshow which did not have any plan for Drupal 8. As this module is extensively used in Drupal 7, and I was looking to start a project on Drupal 8, I started a new branch 8.x.

My initial thought was, “Yeah! I can try all these new stuff as Plugins, Services, Libraries, etc.”

After few hours, I was already a little bit disappointed, and scared by the amount of work, and the number of mechanisms I was not comfortable with. So I decided to move by steps, and start working on a first, basic port which would focus on replacing hooks and features which disappeared or changed between Drupal 7 and Drupal 8.

This blog post will explain this first porting. Please note that Views Slideshow is packaged with a sub-module Views Slideshow Cycle (providing a method to display slideshows based on the jQuery Cycle plugin) which is also part of the porting.

As with Drupal 7, modules need to have an info file to be recognized by Drupal. The only difference is the format. In Drupal 7 it was almost always a simple text file; in Drupal 8 it is now a YAML file.

Drupal 7 : views_slideshow.info

name = Views Slideshow
description = Provides a View style that displays rows as a jQuery slideshow. This is an API and requires Views Slideshow Cycle or another module that supports the API.
dependencies[] = views
package = Views
core = 7.x
files[] = views_slideshow.module
files[] = theme/views_slideshow.theme.inc
files[] = views_slideshow.views.inc
files[] = views_slideshow_plugin_style_slideshow.inc

Drupal 8 : views_slideshow.info.yml

name: Views Slideshow
type: module
description: 'Provides a View style that displays rows as a jQuery slideshow. This is an API and requires Views Slideshow Cycle or another module that supports the API.'
package: Views
version: 8.1
core: 8.x
dependencies: - views

The only notable difference is the files, which are not listed anymore as these are now auto-loaded or included manually only when needed.

For example:

\Drupal::moduleHandler()->loadInclude('views_slideshow', 'inc', 'views_slideshow.theme');

As Views Slideshow is essentially providing a new style plugin for Views, it requires us to declare this plugin, and to write the class containing the logic behind it.

In Drupal 7, this was achieved by using the hook_views_plugin() which declared the plugin and defined how and where to access the class.

In Drupal 8, Views style plugins use the Annotation mechanism to be discovered and loaded.

That means plugin classes are annotated and placed in a defined namespace subdirectory. To get some examples, we can search for style plugins provided by default with Views in Drupal 8 and check how these are defined.

Drupal 7 : views_slideshow.views.inc + views_slideshow_plugin_style_slideshow.inc

/**
 * Implements hook_views_plugins().
 */
function views_slideshow_views_plugins() {
  return array(
    'style' => array(
      'slideshow' => array(
        'title' => t('Slideshow'),
        'help' => t('Display the results as a slideshow.'),
        'handler' => 'views_slideshow_plugin_style_slideshow',
        'uses options' => TRUE,
        'uses row plugin' => TRUE,
        'uses grouping' => FALSE,
        'uses row class' => TRUE,
        'type' => 'normal',
        'path' => drupal_get_path('module', 'views_slideshow'),
        'theme' => 'views_slideshow',
        'theme path' => drupal_get_path('module', 'views_slideshow') . '/theme',
        'theme file' => 'views_slideshow.theme.inc',
      ), 
    ),
  );
}

/**
 * Style plugin to render each item in a slideshow of an ordered or unordered list. 
 *
 * @ingroup views_style_plugins
 */
class views_slideshow_plugin_style_slideshow extends views_plugin_style { … }

The hook_views_plugin() is loaded because the file views_slideshow.views.inc is listed in the *.info file. The handler and path keys in the plugin definition give information for Views to find the appropriate file and class name.

Drupal 8 : src/Plugin/views/style/Slideshow.php

namespace Drupal\views_slideshow\Plugin\views\style;
use Drupal\views\Plugin\views\style\StylePluginBase;

/**
 * Style plugin to render each item in a grid cell.
 *
 * @ingroup views_style_plugins
 *
 * @ViewsStyle(
 * id = "slideshow",
 * title = @Translation("Slideshow"),
 * help = @Translation("Display the results as a slideshow."),
 * theme = "views_view_slideshow",
 * display_types = {"normal"}
 * )
 */
class Slideshow extends StylePluginBase { … }

The extended class StylePluginBase in Drupal 8 is very similar to the extended class views_plugin_style in Drupal 7, so methods previously defined are still actual and only need to be adapted by replacing functions or methods calls which have been renamed or removed. Some objects have also changed and access to attributes needs to be adapted.

The class is only in charge of providing the settings form, validation, and submission.

As Views Slideshow is not making any assumption and doesn’t provide by default any slideshow method, it provides multiple hooks to let other contributed modules define their own slideshow plugins (mostly jQuery plugins) and their own settings. The Views Slideshow module mostly consists of hook invocations. The way hooks can be invoked or implementations can be listed have changed:

Drupal 7

$slideshows = module_invoke_all('views_slideshow_slideshow_info');
$modules = module_implements('views_slideshow_slideshow_type_form');

Drupal 8

$slideshows = \Drupal::moduleHandler()->invokeAll('views_slideshow_slideshow_info');
$modules = \Drupal::moduleHandler()->getImplementations('views_slideshow_options_form');

Another thing which must be adapted is the inclusion of javascript and css files. In our case, it is required for the settings form, which requires some styling to improve the UX but also requires during the preprocessing to include jQuery plugins to deal with the animations.

In Drupal 7, the recommended way is to add Javascript or CSS to a component through the use of #attached in a render array. However, many developers used drupal_add_js(), drupal_add_css(), and drupal_add_library().

This is the case in the Drupal 7 version of Views Slideshow. In Drupal 8, only #attached is supported. Because of this, it is recommended that all Javascript and CSS be defined as a library which can then be attached to a render array. Libraries can be seen as a package containing multiple JS or CSS files and can be attached all together to elements. As with many things in Drupal 8, libraries are defined through YAML files.

Drupal 8 : views_slideshow.libraries.yml + Slideshow.php

form:
 version: VERSION
 css:
   theme:
     css/views_slideshow.css: {}

class Slideshow extends StylePluginBase {
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    … 
    $form['#attached']['library'] = array('views_slideshow/form');
  }
}

Views Slideshow Cycle requires a third party library (jQuery Cycle) but may also use some optional libraries for advanced options for example. As I did not want to provide the third party libraries with the module, the path to files must be root-relative so files can be downloaded and placed in the appropriate directory as we did in Drupal 7 by using the Libraries API module and sites/all/libraries/.

Drupal 8: views_slideshow_cycle.libraries.yml

jquery_cycle:
 remote: http://malsup.github.io/jquery.cycle.all.js
 version: 3.0.3
 license:
   name: MIT
   url: http://jquery.malsup.com/license.html
   gpl-compatible: true
 js:
   /libraries/jquery.cycle/jquery.cycle.all.js: {}
 dependencies:
   - core/jquery

As explained previously, some third-party libraries are optional so the advanced settings form (for example) should be displayed only if this library is correctly installed. In Drupal 7, it was achieved by using the libraries_get_path() and then a test to check if the expected javascript file exists in the given path. In Drupal 8, I followed the same process but it doesn’t require a contributed module as a service library.discovery exists.

Drupal 7

$json2_path = libraries_get_path('json2');
if (empty($json2_path) || !file_exists($json2_path . '/json2.js')) {
  ...
}

Drupal 8

$json2 = \Drupal::service('library.discovery')->getLibraryByName('views_slideshow_cycle', 'json2');
if (!isset($json2['js'][0]['data']) || !file_exists($json2['js'][0]['data'])) {
  …
}

The last part of the porting was theming and processing. As defined in the plugin annotation, the theme function used by Views is template_preprocess_views_view_slideshow(). The process is then quite simple. We browse all widgets one by one and call the appropriate theme function based on the widget type. We call the appropriate theme function to render the slides based on the selected slideshow plugin. We finally call the last theme function to render the slideshow globally and make the glue between all widgets and slides.

Theme functions are still defined using the hook_theme(). In Drupal 7, if we wanted to associate a template to the theme function, it must be noticed using the template key. In Drupal 8, the template name is now automatically built based on the theme function name. It is still possible to suggest a custom name using the template key. If the theme function is not supposed to be associated to any template, the function key must be used and the system will not search for any template file (otherwise a debug message notify the developer of a missing template file).

foreach ($weight as $location => $order) {
 $vars[$location . '_widget_rendered'] = '';
 foreach ($order as $order_num => $widgets) {
   if (is_array($widgets)) {
     foreach ($widgets as $widget_id) {
       $vars[$location . '_widget_rendered'][] = array(
         '#theme' => $view->buildThemeFunctions($widget_id . '_widget_render'),
         '#vss_id' => $vss_id,
         '#view' => $view,
         '#settings' => $options['widgets'][$location][$widget_id],
         '#location' => $location,
         '#rows' => $rows,
       );
     }
   }
 }
}

// Process Slideshow. 
$slides = array(
 '#theme' => $view->buildThemeFunctions($main_frame_module . '_main_frame'),
 '#vss_id' => $vss_id, '#view' => $view,
 '#settings' => $settings,
 '#rows' => $rows,
);
$vars['slideshow'] = array(
 '#theme' => $view->buildThemeFunctions('views_slideshow_main_section'),
 '#vss_id' => $vss_id,
 '#slides' => $slides,
 '#plugin' => $main_frame_module,
);

The use of the buildThemeFunctions method allows developers to create their own specific theme function to override the default one as Views will automatically suggest some theme function names based on the views name, the display, etc.

Finally, template files must be adapted. It is here that we see one of the major changes in Drupal 8. In Drupal 7, the default template engine was PHPTemplate and template files was using *.tpl.php extension. In Drupal 8, the new template engine is Twig and template files now use *.html.twig extension. Twig uses its own syntax but it is very easy to understand and makes template much easier to read from my point of view.

For example:

{% if top_widget_rendered %}
  {{ top_widget_rendered }}
{% endif %}
{{ slideshow }}
{% if bottom_widget_rendered %}
  {{ bottom_widget_rendered }}
{% endif %}

This first beta1 has been released on drupal.org and can be tested right now.

I am now working on the 8.x-4.x branch, to handle the development of plugins for skin, slides and widgets to follow Drupal 8 good practices.