Drupal 8 coding compared to Drupal 7

There is a great article for developers familiar with Drupal 7 that explains how to create modules for Drupal 8: http://effulgentsia.drupalgardens.com/content/drupal-8-hello-oop-hello-world

Just in case that article ever gets taken down, I've recopied it here. Also, see the Pants module.

Drupal 8: Hello OOP, Hello world!

 
 
Submitted by effulgentsia 
on August 2nd, 2013 at 6:50:32 PM

Hello world. This is my first blog post. For those of you who don't know me, I'm effulgentsia on drupal.org. I work within Acquia's OCTO group, and I work a lot on Drupal core, both writing and reviewing patches.

In the course of working on Drupal 8 and attending various Drupal events, I've met quite a few Drupal 7 module developers curious about what they'll need to learn to be successful Drupal 8 module developers. Several people in the Drupal community have started writing blog posts about that, including one earlier this week by Joe Shindelar on writing a Hello World module.

In this post, I'd like to dive a little deeper into the first thing you'll probably notice if you watch that video and write the module: that you're now writing namespaced PHP classes instead of global functions, even for very simple stuff. If you first learned PHP within the last couple years, or have worked with any modern object-oriented PHP project, this might all be second nature to you. However, if you're like me, and only learned PHP in order to develop for Drupal 7 or earlier, it might take a bit to learn and adjust to the best practices of OOP in PHP. But hey, learning and adjusting is what programming is all about. Just look around at how much has changed in HTML, CSS, and JS best practices over the last 3 years. Why should the server-side stay stagnant?

To start with, let's look at what a hello.module would look like in Drupal 7:

hello.info

name = Hello
core = 7.x

hello.module

<?php

function hello_menu() {
  return array(
    'hello' => array(
      'title' => 'Hello',
      'page callback' => 'hello_page',
      'access callback' => 'user_access', 
      'access arguments' => array('access content'),
    ), 
  ); 
} 

function hello_page() { 
  return array( 
    '#type' => 'markup', 
    '#markup' => t('Hello.'), 
  ); 
} 

Pretty simple so far, right? There's a .info file that lets Drupal know about the module, and within the hello.module file, you implement hook_menu(), specify that you want a menu link (that shows up in your Navigation menu by default) at the URL "hello" whose link title is "Hello". Visiting that URL (whether by clicking that link or typing into the browser's address bar) should return the contents of the hello_page() function. But only to someone with "access content" permission.

However, there are two things here that should be improved even for Drupal 7. The first is that byhaving hello_page() in hello.module, PHP needs to load that function into memory for every single page request, even though most page requests will probably be for some URL other than /hello. One extra function to load isn't so bad, but on a site with a lot of modules, if every module did things this way, it would add up. So, it's better to move the function to a file that can be loaded only when needed:

hello.info

name = Hello 
core = 7.x 

hello.module

<?php

function hello_menu() {
  return array(
    'hello' => array( 
      'title' => 'Hello', 
      'page callback' => 'hello_page', 
      'access callback' => 'user_access', 
      'access arguments' => array('access content'), 
      'file' => 'hello.pages.inc', 
    ), 
  ); 
} 

hello.pages.inc

<?php

function hello_page() {
  return array(
    '#type' => 'markup', 
    '#markup' => t('Hello.'), 
  ); 
} 

The second problem is that there are no automated tests for this module. It is almost guaranteed that at some point, I will introduce bugs into this module. Or, if I contribute this module to drupal.org, that other people will submit patches to it that introduce bugs. Even very smart and diligent developers make mistakes. And frankly, I'm lazy. I don't want to have to manually test this module every time I make a change or review someone's patch. I'd rather have a machine do that for me. If I write tests, then drupal.org will automatically run them for every submitted patch that needs review, and if a test fails, automatically set the issue to "needs work" with a report of which test failed, all while I'm sleeping. So here's the module again, with a test:

hello.info

name = Hello 
core = 7.x 
files[] = hello.test 

hello.module

<?php

function hello_menu() {
  return array(
    'hello' => array( 
      'title' => 'Hello', 
      'page callback' => 'hello_page', 
      'access callback' => 'user_access', 
      'access arguments' => array('access content'), 
      'file' => 'hello.pages.inc', 
    ), 
  ); 
} 

hello.pages.inc

<?php

function hello_page() {
  return array(
    '#type' => 'markup', 
    '#markup' => t('Hello.'),
  ); 
} 

hello.test

<?php

class HelloTest extends DrupalWebTestCase {

  public static function getInfo() {
    return array(
      'name' => 'Hello functionality', 
      'group' => 'Hello', 
    ); 
  } 

  public function setUp() {
    parent::setUp('hello'); 
  } 

  public function testPage() { 
    $this->drupalGet('hello'); 
    $this->assertText('Hello.'); 
  } 

} 

So hey, how about that, if you're adding tests to your modules in Drupal 7, about half your code is already object-oriented! Ok, so what changes for Drupal 8? For starters, we're going to reorganize our hello.pages.inc file into a class. Which means, we need to pick a name for the class. Excuse the verbosity, but here's the name I'm going to pick: Drupal\hello\Controller\HelloController. What? Why so long? Here's why:

  • Before naming my module "hello", I first checked to see if that module name is already taken on drupal.org. I wouldn't want to write a module that conflicts with one already there. So great, I have a name that's available within the Drupal world. But what about the world at large? There are other open source PHP projects out there besides Drupal. What if people working on those projects discover my class and want to incorporate it into their project? Those projects might already have their own subcomponents named "hello". Including "Drupal" in the name ensures no conflict with those projects. You might be thinking, yeah right, what other project will find any value in my silly little class that just has one function that returns a Drupal render array? Ok, fair point, but maybe this will evolve into a more interesting module, where some of the classes really do solve some interesting problems in a generic way and are useful to other projects. I think that it's actually simpler (and more consistent) to just name all your classes as though they could be useful to other projects than to have to decide on a case by case basis whether to use a complete name or a short name.
  • After Drupal\hello\, I then added another Controller piece to the name. "Controller" is the "C" inMVC terminology, and within web frameworks, commonly refers to the top-level code that runs in response to a particular URL being requested. At some point, my module might grow to include a bunch more classes that have other responsibilities, so adding a "Controller" component to my name lets me group all my controller classes and keep them separate from non-controller classes.
  • The part of the name upto the final "\", Drupal\hello\Controller, is known in PHP as thenamespace. Per PHP’s documentation, namespaces are conceptually similar to file directories in that they serve to group related items. Each part along the way is itself a namespace and serves to group, at each level becoming more specific. "Drupal" is a namespace, grouping all classes that are part of the Drupal project (in the broad sense, including core and contrib). "Drupal\hello" is a namespace, grouping all classes that are part of the "hello" module I’m creating for Drupal. "Drupal\hello\Controller" is a namespace, grouping all classes within this module that are controllers. Once I’ve reached the most specific namespace (group) that I consider to be useful, I still need to name the class itself that goes into this namespace. For now, I’m choosingHelloController, despite the fact that "hello" and "Controller" are already included in my namespace. When my module grows to include more pages, I'll probably want to organize them into multiple controller classes, and then I’ll be able to name each one in a meaningful way. But for now, I just have the one class and need to name it something, so I accept the redundancy.

Now that we have a fully namespaced class name, we need to decide the name of the file in which to put that class. For now, Drupal 8 requires (unless you want to write your own custom registration/loading code) the file name to match the class name as so:lib/Drupal/hello/Controller/HelloController.php. Yep, the file needs to be four levels deep within your module's directory. Here's why:

  • The lib directory is needed to organize your PHP classes separately from your other module files (YML files (more on that later), CSS files, etc.).
  • The Drupal/hello part of the directory path is needed to comply with PSR-0, a standard that says that the complete class name must be represented on the file system. This is an annoying standard, and the PHP standards group responsible for it are in the process of considering creating a new standard that will not require that. If they do so prior to accepting any other new standards, it will be named PSR-4. There is currently a Drupal core issue to switch to what will hopefully become PSR-4, at which time, we'll all be able to celebrate shallower directory paths within Drupal 8 modules.
  • The Controller directory is useful though. The whole point of adding it as a sub-namespace was to help organize the classes/files once the module grows to have many more classes.

Whew. Ok, with all that explained, here's the Drupal 8 version of hello.pages.inc:

lib/Drupal/hello/Controller/HelloController.php

<?php

namespace Drupal\hello\Controller;

class HelloController {
  public function content() {
    return array(
      '#type' => 'markup', 
      '#markup' => t('Hello.'), 
    ); 
  } 
} 

If you watched the video in 

Internal References

Article Type

General