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:
name = Hellocore = 7.x
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:
name = Hello core = 7.x
array( 'title' => 'Hello', 'page callback' => 'hello_page', 'access callback' => 'user_access', 'access arguments' => array('access content'), 'file' => 'hello.pages.inc', ), ); }
'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:
name = Hello core = 7.x files[] = hello.test
array( 'title' => 'Hello', 'page callback' => 'hello_page', 'access callback' => 'user_access', 'access arguments' => array('access content'), 'file' => 'hello.pages.inc', ), ); }
'markup', '#markup' => t('Hello.'), ); }
'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:
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:
Whew. Ok, with all that explained, here's the Drupal 8 version of hello.pages.inc:
'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:
name: Hello core: 8.x type: module
hello: path: '/hello' defaults: _content: '\Drupal\hello\Controller\HelloController::content' requirements: _permission: 'access content'
array( 'title' => 'Hello', 'route_name' => 'hello', ), ); }
'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:
If this content did not answer your questions, try searching or contacting our support team for further assistance.
Fri Sep 12 2025 07:10:40 GMT+0000 (Coordinated Universal Time)