A new mechanism for registering routes has been introduced, based on Symfony2's Routing component. At this time the new routing system is still under development and the old one, based on hook_menu(), still exists. The hook_menu() version will be removed before launch, however.
The new routing system allows for matching on more than just path; it supports matching by path, HTTP method, scheme (http vs. https), and host. HTTP Accept headers are in progress. This change notice will be updated as time goes on, and once proper documentation is developed.
Drupal 8 now incorporates the Symfony2 Routing component and the Symfony2 CMF Routing component extension. As a result, the way routes (URL path to page/access callback mappings formerly provided in hook_menu()) are defined in Drupal 8 has changed.
The key differences are:
- Rather than defining routes in hook_menu(), they are now defined in module.routing.yml files, in YAML format.
- Routes are identified by a machine name. By convention, route machine names should be prefixed with the name of the module that defines them.
- Each route consists of a
pattern(formerly "path", e.g. /admin/content/book, including preceding slash). All wildcard parameters (formerly '%' or '%name') are named and displayed in the format of{node}. The names within curly brackets map to the argument names of the _content/_controller (see below) — e.g. {node} maps to $node innode_view($node). - "page callback" becomes, in most cases, when what's being returned is content that will be rendered inside an HTML layout with other blocks, a
_contentkey pointing to the callable name. - The _controller or _content keys may not point to functions. They should reference a method of a class. That class will be instantiated when needed and the specified method called. Ex:
_content: \Drupal\mymodule\Controller\MyClass::myMethod - The _controller or _content keys may also refer to registered services. To do so, specify the name of the service as registered in the service container, followed by a single colon and then the method to use. Ex:
_controller: mymodule_controller:someMethod. The service will be instantiated as needed. - In the case of a callback that wants to return the entire response (not HTML, or even if HTML, we don't want additional blocks around it), a result that is not a fully-themed HTML page,
_controlleris used instead. (see book_export) - Access checks may now be stacked, and multiple access checks may be run on a single route. See http://drupal.org/node/1851490 for more details.
- @TODO: How to deal with dynamic paths like node/add/book, node/add/page, node/add/...?
- @TODO: What happens to other properties of hook_menu() like title, description, type, weight, context....
Here are some more specific examples from Drupal 7.
Static paths
Here's a basic default example, with a standard static router path that outputs a fully-themed HTML page.
Drupal 7
in modules/book/book.module:
<?php
function book_menu() {
$items['admin/content/book'] = array(
'title' => 'Books',
'description' => "Manage your site's book outlines.",
'page callback' => 'book_admin_overview',
'access arguments' => array('administer book outlines'),
'type' => MENU_LOCAL_TASK,
'file' => 'book.admin.inc',
);
...
return $items;
}
?>Drupal 8
in modules/book/book.routing.yml
book_admin:
pattern: '/admin/content/book'
defaults:
_content: '\Drupal\book\Controller\BookController::adminOverview'
requirements:
_permission: 'administer book outlines'in modules/book/book.module
<?php
function book_menu() {
$items['admin/content/book'] = array(
'title' => 'Books',
'description' => "Manage your site's book outlines.",
'route_name' => 'book_admin', // this links the menu item to the route.
'type' => MENU_LOCAL_TASK,
);
...
return $items;
}
?>in modules/book/lib/Drupal/book/Controller/BookController.php
<?php
namespace Drupal\book\Controller;
use Drupal\Core\ControllerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class BookController implements ControllerInterface {
protected $database;
/**
* This method lets us inject the services this class needs.
*
* Only inject services that are actually needed. Which services
* are needed will vary by the controller.
*/
public static function create(ContainerInterface $container) {
return new static($container->get('database');
}
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* This is the method that will get called, with the services above already available.
*/
public function adminOverview() {
// ...
}
}
?>Forms
Forms are their own special type of controller. Forms can now be objects, which allows them to be lazy-loaded and injected, just like controllers.
Drupal 7
in modules/book/book.module:
<?php
function book_menu() {
$items['admin/content/book/%node'] = array(
'title' => 'Re-order book pages and change titles',
'page callback' => 'drupal_get_form',
'page arguments' => array('book_admin_edit', 3),
'access callback' => '_book_outline_access',
'access arguments' => array(3),
'type' => MENU_CALLBACK,
'file' => 'book.admin.inc',
);
...
return $items;
}
?>Drupal 8
in modules/book/book.routing.yml
book_admin_edit:
pattern: 'admin/content/book/{node}'
defaults:
_form: '\Drupal\book\Form\BookAdminEditForm'
requirements:
_permission: 'administer book outlines'in modules/book/book.module
<?php
function book_menu() {
$items['admin/content/book/%node'] = array(
'title' => 'Re-order book pages and change titles',
'route_name' => 'book_admin_edit',
'type' => MENU_CALLBACK,
);
...
return $items;
}
?>in modules/book/lib/Drupal/book/Form/BookAdminEditForm.php
<?php
namespace Drupal\book\Form;
use Drupal\Core\ControllerInterface;
use Drupal\Core\Form\FormInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class BookAdminEditForm implements ControllerInterface, FormInterface {
protected $database;
/**
* This method lets us inject the services this class needs.
*
* Only inject services that are actually needed. Which services
* are needed will vary by the controller.
*/
public static function create(ContainerInterface $container) {
return new static($container->get('database'));
}
public function __construct(Connection $database) {
$this->database = $database;
}
public function getFormID() {
return 'book_admin_edit';
}
public function buildForm(array $form, array &$form_state, Node $node = NULL) {
// ...
}
public function validateForm(array &$form, array &$form_state) {
// ...
}
public function submitForm(array &$form, array &$form_state) {
// ...
}
}
?>Wildcard paths
Drupal 7
in modules/book/book.module:
<?php
function book_menu() {
$items['book/export/%/%'] = array(
'page callback' => 'book_export',
'page arguments' => array(2, 3),
'access arguments' => array('access printer-friendly version'),
'type' => MENU_CALLBACK,
'file' => 'book.pages.inc',
);
$items['node/%node/outline'] = array(
'title' => 'Outline',
'page callback' => 'book_outline',
'page arguments' => array(1),
'access callback' => '_book_outline_access',
'access arguments' => array(1),
'type' => MENU_LOCAL_TASK,
'weight' => 2,
'file' => 'book.pages.inc',
);
...
return $items;
}
?>Drupal 8
in modules/book/book.routing.yml:
book_export:
pattern: '/book/export/{type}/{node}'
defaults:
# What's being returned is the entire response (not HTML, or even if HTML, we don't want additional blocks around it), so use _controller instead of _content.
_controller: '\Drupal\book\Controller\BookController::export'
requirements:
_permission: 'access printer-friendly version'
book_node_outline:
pattern: '/node/{node}/outline'
defaults:
_controller: '\Drupal\book\Controller\BookController::outline'
requirements:
# Indicates that the book outline access access system should be invoked.
_book_outline_access: TRUEThe nid passed in the URL will be automatically converted to a node object. This can be done the same with all entity types, for example {user}.
If you have parameters for the controller which aren't named like entity type, you can use the following example:
paramconverter_test_node_set_parent:
pattern: '/paramconverter_test/node/{node}/set/parent/{parent}'
requirements:
_access: 'TRUE'
defaults:
_content: '\Drupal\paramconverter_test\TestControllers::testNodeSetParent'
options:
# Here you specify the entity type a certain parameter maps to.
converters:
parent: 'node'If you want to handle access logic on the controller, as you might need some values of the URL use '_access: TRUE' and throw exceptions on the controller
function.
Dynamic paths
Drupal 7
in modules/node/node.module:
<?php
function node_menu() {
...
foreach (node_type_get_types() as $type) {
$type_url_str = str_replace('_', '-', $type->type);
$items['node/add/' . $type_url_str] = array(
'title' => $type->name,
'title callback' => 'check_plain',
'page callback' => 'node_add',
'page arguments' => array($type->type),
'access callback' => 'node_access',
'access arguments' => array('create', $type->type),
'description' => $type->description,
'file' => 'node.pages.inc',
);
}
...
return $items;
}
?>Drupal 8
In Drupal 8 we use Subscriber classes to handle dynamic routes
module/lib/Drupal/module/NodeSubscriber.php
<?php
/**
* Definition of \Drupal\node\NodeSubscriber.
*/
namespace Drupal\node;
use \Drupal\Core\Routing\RouteBuildEvent;
use \Drupal\Core\Routing\RoutingEvents;
use \Symfony\Component\EventDispatcher\EventSubscriberInterface;
use \Symfony\Component\Routing\Route;
/**
* Listens to the dynamic route event and add a test route.
*/
class RouteTestSubscriber implements EventSubscriberInterface {
/**
* Implements EventSubscriberInterface::getSubscribedEvents().
*/
static function getSubscribedEvents() {
$events[RoutingEvents::DYNAMIC] = 'createNodeRoutes';
return $events;
}
/**
* Adds dynamic node routes.
*
* I'm pretty sure the _access syntax is wrong in this case. And have no idea how to pass arguments to the access callback.
* This is pretty much a copy/paste of RouteTestSubscriber, and it probably doesn't work fully.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The route building event.
*
* @return \Symfony\Component\Routing\RouteCollection
* The route collection that contains the new dynamic route.
*/
public function createNodeRoutes(RouteBuildEvent $event) {
$collection = $event->getRouteCollection();
foreach (node_type_get_types() as $type) {
$type_url_str = str_replace('_', '-', $type->type);
$route = new Route('/node/add/' . $type_url_str, array(
'_content' => '\Drupal\node\NodeController::add',), array(
'_node_access' => 'create',
));
$collection->add('node_add_' . $type->type, $route);
}
}
?>
Comments
It should be noted that
It should be noted that adding a '_form' to .routing.yml will automatically load the file that contains the name of the class referenced by '_form'.
Australian Drupal Dev - http://danielph.in/
Custom wildcards
We can add custom implementation example with warning that it is not recommended as below:
Wildcard paths
Drupal 7
in modules/book/book.module:
<?phpfunction book_menu() {
$items['book/export/%/%'] = array(
'page callback' => 'book_export',
'page arguments' => array(2, 3),
'access arguments' => array('access printer-friendly version'),
'type' => MENU_CALLBACK,
'file' => 'book.pages.inc',
);
$items['node/%node/outline'] = array(
'title' => 'Outline',
'page callback' => 'book_outline',
'page arguments' => array(1),
'access callback' => '_book_outline_access',
'access arguments' => array(1),
'type' => MENU_LOCAL_TASK,
'weight' => 2,
'file' => 'book.pages.inc',
);
...
return $items;
}
?>
Drupal 8
in modules/book/book.routing.yml:
book_export:
pattern: '/book/export/{type}/{node}'
defaults:
# What's being returned is the entire response (not HTML, or even if HTML, we don't want additional blocks around it), so use _controller instead of _content.
_controller: '\Drupal\book\BookController::export'
requirements:
_permission: 'access printer-friendly version'
book_node_outline:
pattern: '/node/{node}/outline'
defaults:
_controller: '\Drupal\book\BookController::outline'
requirements:
# Indicates that the book outline access access system should be invoked.
_book_outline_access: TRUE
If you want to handle access logic on the controller, as you might need some values of the URL use '_access: TRUE' and throw exceptions on the controller
function.
If you want to define custom wildcard, refer converters section
converters
It is not recommended to define customer converters in D8 as most of them will be covered as entity or configentity.
There are two steps to create a custom converter (apart from entity and configentity)
1. Implement ParamConverterInterface
File: core/modules/views/views_ui/lib/Drupal/views_ui/ParamConverter/ViewUIConverter.php2. Register converter in a bundle
file: core/modules/views/views_ui/lib/Drupal/views_ui/ViewsUiBundle.php