Ultimate Guide to Drupal 8: Episode 7 - Code changes in Drupal 8

  • 18 minute read

Welcome to the 7th installment of an 8-part blog series we're calling "The Ultimate Guide to Drupal 8." Whether you're a site builder, module or theme developer, or simply an end-user of a Drupal website, Drupal 8 has tons in store for you! This blog series will attempt to enumerate the major changes in Drupal 8.

Please note that since Drupal 8 is still under active development, some of the details below may change prior to its release. Still, since Drupal 8 is now feature-frozen, hopefully most info should remain relevant. Where applicable, Drupal 7 contrib equivalents of Drupal 8 features will be noted.

"Proudly Found Elsewhere"

As a counter-point to "Not Invented Here", "Proudly Found Elsewhere" represents a mind-shift among Drupal core developers to attempt to find the best tool for the job and incorporate it into the software, versus creating something custom and specific to Drupal which only we benefit from.

You'll see this philosophy change in many aspects of Drupal 8. Among the external libraries we've pulled in are PHPUnit for unit testing, Guzzle for performing HTTP (web service) requests, a variety of Symfony components (Create your own framework on top of the Symfony2 Components is an excellent tutorial for learning more about those), Composer for pulling in external dependencies and class autoloading, and more.

But this philosophy change also extends to the code base itself. We made big architecture changes in Drupal 8 in order to embrace the way the rest of the world is writing code: decoupled, object-oriented, and embracing modern language features of PHP such as namespaces and traits.

Getting OOP-y with it

Let's look at a few code samples to illustrate Drupal 8's "Proudly Found Elsewhere" architecture in action.

Drupal 7: example.info

name = Example
description = An example module.
core = 7.x
files[] = example.test
dependencies[] = user

All modules in Drupal need a .info file in order to register themselves with the system. The example above is typical of a Drupal 7 module. The file format is "INI-like" for ease of authoring, but also includes some "Drupalisms" such as the array[] syntax so standard PHP functions for reading/writing INI files can't be used. The files[] key, which triggers Drupal 7's custom class autoloader to add code to the "registry" table, is particularly "Drupalish" and module developers writing OO code must add a files[] line for each file that defines a class, which can get a little bit silly.

Drupal 8: example.info.yml

name: Example
description: An example module.
core: 8.x
dependencies:
  - user 
# Note: New property required as of Drupal 8!
type: module

In embracing "Proudly Found Elsewhere," info files in Drupal 8 are now simple YAML files, same as those used by other languages and frameworks. The syntax is very similar (mostly ":" instead of "=" everywhere, and arrays formatted differently), and it still remains very easy to both read and write these files. The awkward files[] key is gone, in favour of the PSR-4 standard for automatic class autoloading, via Composer. The "English" version of that sentence is that by following a specific class naming/folder convention (modules/example/src/ExampleClass.php), Drupal can pick OO code up automatically without manual registration being required. :)

Drupal 7: hook_menu()

example.module

<?php
/**
 * Implements hook_menu().
 */
function example_menu() {
  $items['hello'] = array(
    'title' => 'Hello world',
    'page callback' => '_example_page',
    'access callback' => 'user_access',
    'access arguments' => 'access content',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Page callback: greets the user.
 */
function _example_page() {
  return array('#markup' => t('Hello world.'));
}
?>

This is a pretty basic "hello world" module in Drupal 7, which defines a URL at /hello that when accessed checks to make sure the user has "access content" permissions before firing the code inside _example_page() which prints "Hello world." to the screen as a fully-themed page. hook_menu() is an example of what is pejoratively known as an "ArrayPI," a common pattern in Drupal 7 and earlier. The problem with ArrayPIs is that they are difficult to type (for example, ever forgotten the return $items and then spent the next 30 minutes bashing your face against the table?), have no IDE autocompletion for what properties are available, and the documentation must be manually updated as keys are changed/added. As documentation for hook_menu() will show, it also suffers from trying to do too many things. It's used for registering path to page/access callback mappings, but also used to expose links in the UI in a variety of ways, swapping out the active theme, and much more.

Drupal 8: Routes + Controllers

example.routing.yml

example.hello:
  path: '/hello'
  defaults:
    _content: '\Drupal\example\Controller\Hello::content'
  requirements:
    _permission: 'access content'

src/Controller/Hello.php

<?php
namespace Drupal\example\Controller;

use Drupal\Core\Controller\ControllerBase;

/**
 * Greets the user.
 */
class Hello extends ControllerBase {
  public function content() {
    return array('#markup' => $this->t('Hello world.'));
  }
}
?>

In Drupal 8's new routing system, the path-to-page/access-check logic now lives in a YAML file using the same syntax as the Symfony routing system. The page callback logic now lives in a "Controller" class (as in the standard model-view-controller pattern) in a specially named folder, per the PSR-4 standard. It's declared in the example module's "namespace" in order to allow example module to name its classes whatever it wants without worry of conflicting with other modules that might also want to say Hello (Drupal is very friendly, so it's possible!). And finally, the class pulls in the logic from the ControllerBase class in via the use statement, and extends it, which gives the Hello controller access to all of ControllerBase's convenient methods and capabilities, such as $this->t() (the OO way of calling the t() function). And, because ControllerBase is a standard PHP class, all of its methods and properties will autocomplete in IDEs, so there's no guessing at what it can and can't do for you.

Drupal 7: hook_block_X()

block.module

<?php
/**
 * Implements hook_block_info().
 */
function example_block_info() {
  $blocks['example'] = array(
    'info' => t('Example block'),
  );
  return $blocks;
}

/**
 * Implements hook_block_view().
 */
function example_block_view($delta = '') {
  $block = array();
  switch ($delta) {
    case 'example':
      $block['subject'] = t('Example block');
      $block['content'] = array(
        'hello' => array(
          '#markup' => t('Hello world'),
        ),
      );
      break;
  }
  return $block;
}
?>

Here's an example of a typical way in which you define "pluggable thingies" in Drupal (blocks, image effects, text formats, etc.): some kind of _info() hook, along with one or more other hooks to perform additional actions (view, apply, configure, etc.). In addition to these largely being "ArrayPIs," this time they're actually even worse "mystery meat" APIs, because the overall API itself is completely undiscoverable except by very careful inspection of various modules' .api.php files (provided they exist, which is not a guarantee) to discover which magically named hooks you need to define in order to implement this or that behaviour. Some are required, some aren't. Can you guess which is which? :)

Drupal 8: Blocks (and many other things) as Plugins

In Drupal 8, these "mystery meat" APIs have now largely moved to the new Plugin system, which looks something like this:

src/Plugin/Block/Example.php

<?php
namespace Drupal\example\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides the Example block.
 *
 * @Block(
 *   id = "example",
 *   admin_label = @Translation("Example block")
 * )
 */
class Example extends BlockBase {
  public function build() {
    return array('hello' => array(
      '#markup' => $this->t('Hello world.')
    ));
  }
}
?>

Most of this is very similar to the Controller example; a plugin is a class that in this case extends from a base class (BlockBase), which takes care of a bunch of underlying things for you. The Block API itself is housed in the BlockPluginInterface, which the BlockBase class implements.

Note that interfaces in general both expose and document various APIs in a discoverable and IDE-friendly way. A great way to learn about the new APIs in Drupal 8 is by browsing through the various interfaces that are provided.

The comments above the class are called annotations. At first it might seem strange for PHP comments to be used for specifying meta data that affects the logic of the software, but this is a technique that is now widely used by many modern PHP libraries and accepted by the PHP community. Its benefit is that it keeps the class metadata in the same file as and right next to the class definition.

Drupal 7: Hooks

In Drupal 7 and earlier, the extension mechanism used is the concept of hooks. As an API author, you can declare a hook by using functions like module_invoke_all(), module_implements(), drupal_alter(), etc. For example:

<?php
  // Compile a list of permissions from all modules for display on admin form.
  foreach (module_implements('permission') as $module) {
    $modules[$module] = $module_info[$module]['name'];
  }
?>

If a module wanted to respond to this event, it would create a function called modulename_hookname() and declare its output in a way that the given hook implementation expected. For example:

<?php
/**
 * Implements hook_permission().
 */
function menu_permission() {
  return array(
    'administer menu' => array(
      'title' => t('Administer menus and menu items'),
    ),
  );
}
?>

While this is a clever hack for extension that is mostly the result of Drupal's age (Drupal started in 2001, when PHP3 was all the rage, and when there was no support for object-oriented code and the like), there are several tricky bits:

  • This "name a function in a special way" extension mechanism is very much a "Drupalism" and developers coming to Drupal struggle to understand it at first.
  • There are at least four different functions that can trigger a hook: module_invoke(), module_invoke_all(), module_implements(), drupal_alter(), etc. This makes finding all of the available extension points in Drupal very difficult.
  • There is also no consistency between what hooks expect. Some are "info" style hooks that want an array (sometimes an array of arrays of arrays of arrays), others are "event" type hooks that respond when a particular thing happens like cron run or a node is saved, etc. You need to read the docs of each hook in order to understand what input and output it expects.

Drupal 8: Events

While hooks are definitely still prevalent in Drupal 8 for most event-driven behaviour (the "info" style hooks have largely moved to YAML or Plugin annotations), the portions of Drupal 8 that are more closely aligned to Symfony (for example, bootstrap/exit, routing system) have largely moved to Symfony's Event Dispatcher system. In this system, events are dispatched at run-time when certain logic occurs, and modules can subscribe classes to the events they want to react to.

To demonstrate this, let's take a look at Drupal 8's configuration API, helpfully housed in core/lib/Drupal/Core/Config/Config.php. It defines a variety of "CRUD" methods such as save(), delete(), and so on. Each of these dispatches an event when finished with their work, so other modules can react. For example, here's Config::save():

<?php
  public function save() {
    // Validate the incoming information.

    // Save data to Drupal, then tell other modules this was just done so they can react.
    $this->storage->write($this->name, $this->data);
    // ConfigCrudEvent is a class that extends from Symfony's "Event" base class.
    $this->eventDispatcher->dispatch(ConfigEvents::SAVE, new ConfigCrudEvent($this));
  }
?>

As it turns out, there is at least one module that needs to react when configuration is saved: the core Language module. Because if the configuration setting that was just changed was the default site language, it needs to clear out compiled PHP files so the change takes effect.

In order to do this, Language module does three things:

  1. Register an event subscriber class in its language.services.yml file (this is a configuration file for the Symfony Service Container for registering re-usable code):
    language.config_subscriber:
      class: Drupal\language\EventSubscriber\ConfigSubscriber
      tags:
        - { name: event_subscriber }
  2. In the referenced class, implement the EventSubscriberInterface and declare a getSubscribedEvents() method, which itemizes the events that it should be alerted to, and provides each with one or more callbacks that should be triggered when the event happens, along with a "weight" to ensure certain modules that need to can get the first/last crack at the can (heads-up: Symfony's weight scoring is the opposite of Drupal's!):
        <?php
    class ConfigSubscriber implements EventSubscriberInterface {
      static function getSubscribedEvents() {
        $events[ConfigEvents::SAVE][] = array('onConfigSave', 0);
        return $events;
      }
    }
    ?> 
    	
  3. Define the callback method itself, which contains the actual logic that should happen when configuration is saved:
        <?php
      public function onConfigSave(ConfigCrudEvent $event) {
        $saved_config = $event->getConfig();
        if ($saved_config->getName() == 'system.site' && $event->isChanged('langcode')) {
          // Trigger a container rebuild on the next request by deleting compiled
          // from PHP storage.
          PhpStorageFactory::get('service_container')->deleteAll();
        }
      }
    ?>

Overall this buys us a more explicit registration utility so that a single module can subscribe multiple classes to individual events. This allows us to avoid situations in the past where we had switch statements in hooks, or multiple unrelated sets of logic competing for the developer’s attention in a single code block and instead affords us the ability to separate this logic into separate and distinct classes. This also means that our event logic is lazy loaded when it needs to be executed, not just sitting in PHP's memory at all times.

Debugging events and finding their implementations is also pretty straight forward. Instead of a handful of various procedural PHP functions that may or may not have been used to call to your hook, the same Event Dispatcher is used through out the system. In addition to this, finding implementations is as simple as grepping for the appropriate Class Constant, e.g. ConfigEvents::SAVE.

Logically, the event system really rounds out the transition to an Object Oriented approach. Plugins handle info-style hooks and hooks that were called subsequent to an info hook, YAML stands in place for many of our explicit registration systems of old, and the event system covers event-style hooks and introduces a powerful subscription methodology for extending various portions of Drupal core.

...and much, much more!

A great introduction to Drupal 8's API changes can be found at the revamped D8 landing page of api.drupal.org where you'll find a list of grouped topics to help orient you to Drupal 8:

api.drupal.org landing page

Also see https://drupal.org/list-changes for the full list of API changes between Drupal 7 and Drupal 8. Maybe grab a drink first. ;) Each API change record includes before/after code examples to help you migrate, as well as pointers off to which issue(s) introduced the change, and why.

List of API change records on Drupal.org

So. Much. Typing.

It's true that moving to modern, OO code does generally involve more verbosity than procedural code. To help you over the hurdles, check out the following projects:

  • Drupal Module Upgrader: If you're looking to port your modules from Drupal 7 to Drupal 8, look no further than this project, which can either tell you what you need to change (with pointers to the relevant change notices) or automatically convert your code in-place to Drupal 8. You can learn more about DMU in this podcast interview with the maintainer.
  • Console: For new modules, this project is a Drupal 8 scaffolding code generator which will automatically generate .module/.info files, PSR-4 directory structures, YAML and class registrations for routes, and more!
  • Most Drupal 8 core developers swear by the PhpStorm IDE, and the latest version has lots of great features for Drupal developers. Additionally, if you're one of the top contributors to the Drupal ecosystem, you can get it for free! (Note that this isn't product placement. Seriously, join #drupal-contribute at any time of the day or night and see if you can go longer than an hour without someone mentioning PhpStorm. ;))

Whew! That's a wrap!

This article offered a taste of some of the under-the-hood code changes coming to Drupal 8. Join us next time in our final installment, when we'll answer some of your most burning frequently asked questions about Drupal 8, its release timeline, and what to do about upgrades. Stay tuned!