Home / Comment permalink

Drupal 8: Hello OOP, Hello world!

"Cross Posted from http://effulgentsia.drupalgardens.com"

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 by having 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" in MVC 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 the namespace. 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 choosing HelloController, 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 Joe's post, you’ll see that there, he enhanced the HelloController class a bit to implement a Drupal core provided interface, ControllerInterface. Doing that is not necessary for very simple controllers like this one that don’t call any outside code. It does, however, allow for one of OOP’s coolest features: dependency injection. But let’s leave a deep dive into interfaces and dependency injection to a future blog post. Also, as covered in that post, in Drupal 8, the .info file changed to .info.yml, and parts of hook_menu() are now in a .routing.yml file. So adding those in, the entire module (without tests) becomes:

hello.info.yml

name: Hello 
core: 8.x 
type: module 

hello.routing.yml

hello: 
  path: '/hello' 
  defaults: 
    _content: '\Drupal\hello\Controller\HelloController::content' 
  requirements: 
    _permission: 'access content' 

hello.module

<?php

function hello_menu() {
  return array(
    'hello' => array( 
      'title' => 'Hello', 
      'route_name' => 'hello', 
    ), 
  ); 
} 

lib/Drupal/hello/Controller/HelloController.php

<?php

namespace Drupal\hello\Controller;

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

As far as the tests go, those classes also need to be namespaced, be in PSR-0 compatible file names (which also means one class per file, not all lumped into a single .test file), and there are some other changes for porting tests to Drupal 8, which you can read about in these change notices. So, there you have it. To sum up:

  • A lot of your module code will now be in classes. Possibly not all (for example, hook implementations like hello_menu() can still be functions in the .module file), but most. This sets the stage for defining interfaces, type hinting, injecting dependencies, and other best practice OOP techniques for making software more maintainable, testable, robust, and scalable. Stay tuned for future blog posts covering each of these topics.
  • The classes should be within a namespace that starts with Drupal\YOUR_MODULE_NAME. This allows for your classes to not conflict with any other code in any other Drupal module, and even any other code outside of Drupal. While you might not think that other projects will want to use your classes, don’t completely write off that possibility. After all, open source is all about sharing, so why not open the door to sharing more broadly!
  • Within that namespace, you can create more sub-namespaces, as desired to achieve your ideal level of organization.
  • Each class goes into its own file, with the directory structure matching the namespace structure.

Comments

Posted on by plaon.com (not verified).

Thanks for a nice article! Some correction: please, replace ControllerInterface to ContainerInjectionInterface according to https://drupal.org/node/2079895

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

Filtered HTML

  • Use [acphone_sales], [acphone_sales_text], [acphone_support], [acphone_international], [acphone_devcloud], [acphone_extra1] and [acphone_extra2] as placeholders for Acquia phone numbers. Add class "acquia-phones-link" to wrapper element to make number a link.
  • To post pieces of code, surround them with <code>...</code> tags. For PHP code, you can use <?php ... ?>, which will also colour it based on syntax.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <h4> <h5> <h2> <img>
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.