Drupal 8 isn't Scary, Pt. 1: Introduction & Creating a Hello World Module
The Drupal 8 development cycle has introduced a massive shift in many of the underlying concepts which have traditionally provided the foundations for Drupal development. Chief among these concepts is the move to a Object-Oriented architecture based upon Symfony components rather than proceed with the previous procedural codebase.
While the intention of these moves has been to improve the developer experience (DX) of Drupal 8, these changes have provoked a consequential backlash (and fork) due to the scale of the changes being introduced.
I was myself initially skeptical of the changes incoming in Drupal 8. After spending a week reviewing the changes and helping out with some WSCCI issues, I've come to the conclusion that that changes in D8 are a net benefit for developers, and will go a long way to simplify the writing and maintenance of Drupal code.
At this point, the Drupal 8 learning curve is quite steep; this is due in a large part to the lack of consistent and updated documentation about how to work with the new concepts. This series of blog posts is an attempt to address that in part by covering some of the new module development concepts in D8, and to demonstrate that the changes in D8 aren't that difficult to understand once explained in a logical, progressive fashion.
This first post will cover creating a basic module to display a 'Hello World' page, and will cover the new concepts of menu routes and controller classes. Successive posts will build upon this foundation as we look at other new aspects of Drupal 8 module development.
This post series assumes that you are already familiar with the concepts of Object Oriented programming, and PSR-0 namespacing.
Disclaimer: This blog post series represents the state of Drupal 8 at the time this post was written; D8 is now in the 'API Completion' phase which means that for the most part, the information below should not change drastically. I will attempt to keep this post reasonably up-to-date as new developments or code changes make their way into Drupal.
Starting a module
In Drupal 8, the first step to creating a new module is to create a directory for your module in the /modules directory of your Drupal installation. For this example, we will create /modules/hello.
In Drupal 7, the /modules path contained all the core modules; in Drupal 8, core files have been moved into the core/ subdirectory. Core modules now exist at /core/modules.
In Drupal 8, .info files are no longer used; instead, we now use YAML-formatted .info.yml files. The format is very similar to the Drupal 7 .info files:
name: Hello World!<br>type: module<br>description: 'Displays a Hello World page.'<br>package: Example Modules<br>version: VERSION<br>core: 8.x
Create a hello.info.yml file similar to the above in your module folder.
As in Drupal 7, your next step is to create a hello.module file to hold hook implementations and supporting code.
Creating our menu items and routes
As in Drupal 7, your first step towards creating a new page for your module is to define a hook_menu()
implementation. One of the changes introduced along with the Symfony routing system in Drupal 8 is the separation of the 'Menu' and 'Router' concepts.
In Drupal 8, the menu as defined in hook_menu()
implementations describes the presentation of the menu. It defines the menu hierarchy/tree, breadcrumbs, and other 'user-visible' aspects of the menu system.
Note: There is an issue in the queue to replace hook_menu()
with a new function named hook_default_menu_links()
. See #2047633: Move definition of menu links to hook_default_menu_links(), decouple key name from path, and make 'parent' explicit.
Routes are declarations defined in a mymodule.routing.yml file which define which controller should respond for a request for a specific path, and defines any required access checks.
A controller is a method defined in a Controller class which roughly corresponds to a Drupal 7 menu callback. It defines how Drupal responds to a given request.
Our Drupal 8 hook_menu()
implementation for our simple hello_world page looks like this:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * Implements hook_menu().<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">hello_menu</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$items</span><span style="color: #007700">[</span><span style="color: #DD0000">'hello_world'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'title' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Hello'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'description' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'The Hello World page'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'route_name' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'hello.hello_world'</span><span style="color: #007700">,<br> );<br> return </span><span style="color: #0000BB">$items</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
This should be reasonably familiar to any Drupal 7 developer; we define our menu items by path as before. We also specify a title and description for this menu page.
However, there is now an unfamiliar 'route_name' key. This key contains the machine name of a route we've defined in our module's routing file.
For our example, you should create a hello.routing.yml file in the module directory with the following contents:
hello.hello_world:<br> path: '/hello_world'<br> defaults:<br> _content: '\Drupal\hello\Controller\HelloController::helloWorld'<br> requirements:<br> _permission: 'view hello world'
This route definition has a few simple parts to it:
- hello.hello_world is the machine name assigned to this route. The best practice is to prefix your routes with the name of your module. The route name (including it's prefix) must match what was specified in the 'route_name' key of your
hook_menu()
item. - The '_content' key describes the Controller class and method used to return the page content for this callback. By specifying '_content', we are specifying that the output of this controller is for the main content area of the page, and should be returned wrapped in all the other blocks and regions.
- The '_permission' key defines a permission (defined in
hook_permissions()
as per Drupal 7) used to control access to this route.
Menu items which would have been created as menu callbacks in Drupal 7 only need a routing.yml entry in Drupal 8. They can be omitted from the hook_menu()
implementation entirely.
Creating our Controller
Our next step is to create the Controller class and method that defines our callback.
In our route declaration, we declared that our controller class is called '\Drupal\hello\Controller\HelloController'. Currently, all module classes provided in Drupal 8 must live in PSR-0 directories under /modules/<module>/lib. This means that our new Controller class must live in /modules/hello/lib/Drupal/hello/Controller/.
Note: There is currently an issue in the Drupal 8 issue queue to use PSR-4 for module classes to remove some of the path redundancy, so a class like our controller class can live at /modules/hello/lib/Controller/HelloController.php. See #1971198: [meta] Drupal and PSR-0/PSR-4 Class Loading.
Create a new file called 'HelloController.php' in /modules/hello/lib/Drupal/hello/Controller/, so the final path of the controller class file is /modules/hello/lib/Drupal/hello/Controller/HelloController.php.
This first controller will be simple:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">/**<br> * @file<br> * Contains \Drupal\hello\Controller\HelloController.<br> */<p></p></span><span style="color: #007700">namespace </span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">hello</span><span style="color: #007700">\</span><span style="color: #0000BB">Controller</span><span style="color: #007700">;<p>use </p></span><span style="color: #0000BB">Drupal</span><span style="color: #007700">\</span><span style="color: #0000BB">Core</span><span style="color: #007700">\</span><span style="color: #0000BB">Controller</span><span style="color: #007700">\</span><span style="color: #0000BB">ControllerBase</span><span style="color: #007700">;<p></p></span><span style="color: #FF8000">/**<br> * Controller routines for hello module routes.<br> */<br></span><span style="color: #007700">class </span><span style="color: #0000BB">HelloController </span><span style="color: #007700">extends </span><span style="color: #0000BB">ControllerBase </span><span style="color: #007700">{<p> </p></span><span style="color: #FF8000">/**<br> * Return the 'Hello World' page.<br> *<br> * @return string<br> * A render array containing our 'Hello World' page content.<br> */<br> </span><span style="color: #007700">public function </span><span style="color: #0000BB">helloWorld</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$output </span><span style="color: #007700">= array();<p> </p></span><span style="color: #0000BB">$output</span><span style="color: #007700">[</span><span style="color: #DD0000">'hello'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#markup' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Hello World!'</span><span style="color: #007700">,<br> );<p> return </p></span><span style="color: #0000BB">$output</span><span style="color: #007700">;<br> }<p>}<br></p></span><span style="color: #0000BB">?></span></span>
Our new controller extends the ControllerBase class. ControllerBase was created to hide a lot of boilerplate code that applies to most page-based controllers.
The helloWorld() function returns a render array that contains our 'Hello World!' text.
And Finally...
At this point, enabling the module and visiting the /hello_world page should now show a page titled 'Hello' with the contents of 'Hello World'.
In the next installment, we will look at adding a configuration form to our example module to demonstrate how to use the new SystemConfigFormBase and FormBase controllers for Drupal 8 forms.