Kim PepperCo-Founder & Tech Director
Using Drupal 8's new route controllers
As part of the Web Services and Context Core Initiative, traditional procedural page callbacks were converted to shiny new Object Oriented route controllers.
In this post, we cover the basics of creating a route controller, and how to pass in dependencies using dependency injection.
In following posts, we'll look at how to convert Drupal 7 custom access callbacks, to the new AccessCheckInterface, as well as dynamic routes.
A Basic Route Controller
Drupal 8 leverages a number of components from the Symfony2 project: HTTP Foundation, Dependency Injection container, Serializer, Event Dispatcher to name a few. Drupal 8 includes the Symfony2 Routing component.
Why did we need a new Routing component?
In prior versions of Drupal, there was only hook_menu. Hook menu actually managed a few different features. In addition to handling incoming requests, it provided menu links, access control, action links, and a number of other features that are all very tightly coupled together.
By using the Symfony2 Routing component, we are able to split out the route handling aspect, and get a much improved and feature-rich solution. Update: As Crell points out, the main reason for using Symfony2 Routing was to be able to create routes on more than just the path, for example make a request for JSON, XML or HTML while still using the same path.
Defining a Route
A simple example of a hook_menu() implementation in Drupal 7 might look like this:
function trousers_menu() {
$items['trousers'] = array(
'title' => 'Trousers',
'page callback' => 'trousers_page', \\ calls a trousers_page function that returns the content
);
return $items;
}
So how are things different in Drupal 8?
The first step is to define a route in a YAML file that sits in the root directory of your module. These YAMLfiles follow the modulename.routing.yml naming convention. If you take a look in Drupal core's module directories, you'll find a lot of good examples.
In this example, we are creating the trousers module (hat tip to the pants module). Our new module is called trousers and our trousers.routing.yml file looks like this.
trousers_list:
pattern: '/trousers'
defaults:
_content: '\Drupal\trousers\Controller\TrouserController::listTrousers'
requirements:
_permission: 'view content'
In the first line we define a unique name for our route, in this case trousers_list. We will reference this later on when creating our menu link.
The second is how we match the incoming request path. This is pretty much the same as how we keyed the $items array in our Drupal 7 hook_menu() implementation (e.g. $items['trousers']).
The defaults section lets us specify a special _content variable. This references the PHP class and the method that will be called when this route fires. In our case, the listTrousers() method of the TrouserController class. The _content key name is special because it lets Drupal know that the returned value from our controller is going to be the content for the main part of the page, and to therefore wrap it with the rest of the page layout (blocks etc).
We also specify a special _permissions requirement. This ensures that the route is only accessible if the permissions defined are met.
Creating Route Controller Class
A route controller is a plain old PHP class. As we are now using PSR-0 namespaces in Drupal 8, it should live in a Controller sub-namespace. If we are creating a new controller for our trousers module, we would place it in the follow folder structure: lib/Drupal/trousers/Controller/TrouserController.php
The next step is to create our file.
<?php
namespace Drupal\trousers\Controller;
/**
* Route controller for trousers.
*/
class TrouserController {
/**
* Displays a list of trousers.
*/
public function listTrousers() {
// return trouser list here.
}
}
As you can see, there is nothing too special about our controller class. We are simply returning a some rendered HTML, or a Drupal render array in the exact same way we do in Drupal 7.
Creating a Menu Item
As mentioned above, the new Symfony2 Routing component takes care of the route, but we still need a menu item to allow us to integrate with Drupal's menu system. This has been greatly simplified in Drupal 8. Instead of a 'page callback' we just need to define the title and route name:
/**
* Implements hook_menu().
*/
function trousers_menu() {
$items['trousers'] = array(
'title' => 'Trousers',
'route_name' => 'trousers_list',
);
return $items;
}
As you can see, route_name references the route we defined in trousers.routing.yml above.
A More Advanced Route Controller
Our example above is a bit too naive. Usually we need to get content from somewhere. This might be a web service, another PHP function, or by querying the database. This is where the power of Symfony2 Dependency Injection container (DIC) comes into play.
Dependency Injection
I wont go into too much detail on what dependency injection is, or how it works in Drupal other than to say, if you are calling another class, declare it as a contructor parameter, and the Dependency Injection Container (DIC) will pass it in for you on creation.
In our example, we need to query the database to get some trousers, so lets modify our Controller to pass a database Connection object in.
<?php
namespace Drupal\trousers\Controller;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Database\Connection;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Route controller for trousers.
*/
class TrouserController implements ContainerInjectionInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection;
*/
protected $database;
/**
* Constructs a \Drupal\trousers\Controller\TrouserController object.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/ public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('database'));
}
/**
* Displays a list of trousers
*/
public function listTrousers() {
// query the database
$this->database->query('SELECT * from {trousers}');
// handle the db results as usual
// return trouser content here.
}
}
There is a fair amount of code required to inject our dependencies. First, we need to create a protected $database property to hold the database connection, and then we need to set it via the constructor.
Because the controller isn't itself a service defined in the DIC, we have a factory method, create(ContainerInterface $container) provided by the ContainerInjectionInterface interface we are implementing, that lets us grab what we need out of the DIC, in this case, the database connection, and pass it to the constructor of this class.
Although it looks magical, if you want to use dependency injection, just know that you need to implement ContainerInjectionInterface, and follow the pattern above.
Why bother with Dependency Injection?
You may ask yourself, why would I bother adding all that code, when I can just call the procedural function db_query instead? Dependency injection de-couples our code from the code it is calling. If we want to test this class using PHPUnit tests, then we can provide mock or stub implementations of the dependent classes, and test our code in isolation from the rest of the system. This is pretty useful, especially if there are a number of different code paths that are difficult to test with end-to-end web tests. If you call procedural code, there is no way to mock or stub that code.
Summary
That is a very basic introduction into the route controllers in Drupal 8. In the next post, I'll cover how to provide custom code to control access (D7 access callbacks) as well as dynamic routes.
Update: ControllerInterface has been replaced with ContainerInjectionInterface. Updated code and docs to match.