Drupal 8 in action, the route to the method

Juan Olalla - DrupalCamp Spain Cáceres - October 27, 2013

Drupal 8 in action,
the route to the method

@juanolalla #DrupalCampEs

about.me/juanolalla

Juan Olalla
web developer focused on drupal
at @ideup (@gowex)

Motivation
I’ve recently been contributing to Drupal 8 core and I want to share with you what I've learned

Wellcome to Drupal 8 Routing

Goodbye to hook_menu()

Goodbye to hook_menu() for routing
and for everything else...

Routing Routes defined in routing.yml and controllers
Menu links Defined in hook_default_menu_links()
Local actions Plugins defined in local_actions.yml
Local tasks Plugins defined in local_tasks.yml
Contextual links Plugins defined in contextual_links.yml
Breadcrumbs Implementing BreadcrumbBuilder plugins

Links can be related regardless of the path, managed by machine names.

Drupal 8 uses Symfony2 Routing component

And also uses Symfony CMF RoutingBundle for dynamic routing

What is a route?

A route is an object defined by

  1. Path
  2. Default values
  3. Requirements
  4. Options
  5. Host
  6. Schemes
  7. Methods
			use Symfony\Component\Routing\Route;	
			
$route = new Route(
  '/archive/{month}', // path
  array('controller' => 'showArchive'), // default values
  array(
    'month' => '[0-9]{4}-[0-9]{2}',
    'subdomain' => 'www|m',
  ), // requirements
  array(), // options
  '{subdomain}.example.com', // host
  array(), // schemes
  array() // methods
);
			
		

The Routing component gets routes from

Drupal 8 uses YAML to define routes

			foo.archive:
  path: '/archive/{month}'
  defaults:
    _content: '\Drupal\foo\Controller\FooController::showArchive'
  requirements:
    month: '[0-9]{4}-[0-9]{2}'
    subdomain: 'www|m'
  host: '{subdomain}.example.com'
		

Let’s start with a plain
“hello world”

using a /hello-world path

Defining the route in YAML

/modules/foo/foo.routing.yml

			foo.hello_world:
  path: '/hello-world'
  defaults:
    _controller: '\Drupal\foo\Controller\FooController::helloWorld'
  requirements:
    _access: 'TRUE'
		

Creating the controller

/modules/foo/lib/Controller/FooController.php

			namespace Drupal\foo\Controller;
use Symfony\Component\HttpFoundation\Response;

class FooController {
  public function helloWorld() {
    $response = new Response('Hello World!');
    $response->headers->set('Content-Type', 'text/plain');
    return $response;
  }  
}
		

Hello World!

Drupal 8 will use PSR-4 for module classes instead of PSR-0

/modules/foo/lib/Controller/FooController.php
instead of
/modules/foo/lib/Drupal/foo/Controller/FooController.php

drupal.org issue: https://drupal.org/node/1971198

Returning a JSON response

			namespace Drupal\foo\Controller;
use Symfony\Component\HttpFoundation\JsonResponse

class FooController {

  public function helloWorld() {
    return new JsonResponse(
      array('greeting' => 'Hello World!')
    );
  }  
}
		

{"greeting":"Hello World!"}

Returning "hello world" inside Drupal content

using a /hello-world path

Defining the route in YAML

			foo.hello_world:
  path: '/hello-world'
  defaults:
    _content: '\Drupal\foo\Controller\FooController::helloWorld'
    _title: 'Greeting'
  requirements:
    _permission: 'access content'
		

Returning a render array

			namespace Drupal\foo\Controller;
use Drupal\Core\Controller\ControllerBase;

class FooController extends ControllerBase {

  public function helloWorld() {
    $build = array();
    $build['#markup'] = $this->t('Hello World!');
    return $build;
  }
}

		

What about dynamic page titles?

drupal_set_title() is being removed

Use _title_callback in the route defaults

defaults:
  _title_callback: '\Drupal\foo\Controller\FooController::getTitle'

Or return it as part of the main render array

$build['#title'] = ...

Returning a "hello world" greeting configuration form

at /admin/config/people/greeting

Defining the route in YAML

			
foo.admin_greeting:
  path: '/admin/config/people/greeting'
  defaults:
    _form: '\Drupal\foo\Form\FooForm'
  requirements:
    _permission: 'administer greeting'
		
			use Drupal\Core\Form\ConfigFormBase;

class FooForm extends ConfigFormBase {

  public function getFormId() { return 'foo_form'; }
  
  public function buildForm(array $form, array &$form_state) {
    $form['foo_greeting'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Greeting'),
      '#default_value' => 'Hellow World!',
    );
    return parent::buildForm($form, $form_state);
  }
}
		

FormBase implements FormInterface

			namespace Drupal\Core\Form;

interface FormInterface {

  public function getFormId();

  public function buildForm(array $form, array &$form_state);

  public function validateForm(array &$form, array &$form_state);

  public function submitForm(array &$form, array &$form_state);
}
		

Advanced Routing

defaults:
# Control the full response apart from Drupal extra content
			_controller: '\Drupal\book\Controller\BookController::bookExport'
		
# Put content in the main region and let Drupal do the rest
			_content: '\Drupal\book\Controller\BookController::bookRender'
		
# Get the buildForm method of a class extending a Drupal Form
			_form: '\Drupal\book\Form\BookSettingsForm'
		

Entity defaults

defaults:
  # Call buildForm for the action entity add operation Form
			  _entity_form: 'action.add'
		
  # Display a list of taxonomy_vocabulary entity objects
			  _entity_list: 'taxonomy_vocabulary'
		
  # Show the full view mode of a user entity object
			  _entity_view: 'user.full'
		

Passing parameters to the controller method

# Parameter defined in the path
			path: '/comment/{comment}/approve'
		
# Default parameter
			path: '/admin/structure/types'
defaults:
  entity_type: 'node_type'
		
# Optional (both in the path and with a default value)
			path: '/admin/config/search/path/{keys}'
defaults:
  keys: NULL
		
			node.overview_types:
  path: '/admin/structure/types'
  defaults:
    _content: '\Drupal\Core\Entity\Controller\EntityListController::listing'
    entity_type: 'node_type'
    _title: 'Content types'


class EntityListController extends ControllerBase {
  public function listing($entity_type) {
    return $this->entityManager()
      ->getListController($entity_type)
      ->render();
  }
}
		

Parameters are upcasted to entities by entity type id

			contact.personal_page
  path: '/user/{user}/contact'

contact.site_page_category:
  path: '/contact/{contact_category}'
		
      class ContactController {
  public function contactPersonalPage(UserInterface $user) {...}
  public function contactSitePage(
    CategoryInterface $contact_category = NULL) {...}
}
    

Explicitly converting parameters into entities

      
			foo.hello_user:
  path: '/{foo_user}/hello/{user}'
        # e.g. /1/hello/2
        defaults:
    _content: '\...\FooController::helloUser'
  options:
    parameters:
      foo_user:
        type: 'entity:user'
              # {foo_user} will also be passed as a user entity
		

Regular expressions requirements for parameters

# {op} parameter can only be 'enable' or 'disable'
      views_ui.operation:
  path: '/admin/structure/views/view/{view}/{op}'
  requirements:
    op: 'enable|disable'
    
# {user} parameter can only be a number
      user.view:
  path: '/user/{user}'
  requirements:
          user: \d+
    

Access Check

_you_shall_not_pass: TRUE

Drupal 8 doesn't implement Symfony2 Security component, and manages access from routing

Symfony uses a separate Security component for controlling route access that is too complex to integrate in Drupal 8.

Drupal 8 implements access checkers services, which will ignore, allow or deny the access based on a requirement on the route.

Checking Permissions

			node.overview_types:
  path: '/admin/structure/types'
  defaults:
    _content: '\Drupal\Core\Entity\Controller\EntityListController::listing'
    entity_type: 'node_type'
    _title: 'Content types'
  requirements:
    _permission: 'administer content types'
		

Still setting permissions in hook_permission()

			function node_permission() {
  return array(
    'administer content types' => array(
      'title' => t('Administer content types'),
      'restrict access' => TRUE,
    ),
  );
}
		

I asked for hook_permission() in IRC #drupal-contribute

<timplunkett> juanolalla: no plans to replace that AFAIK

Common access checkers provided by Drupal core

# Always pass, or 'FALSE' to always fail and block access
			requirements:
  _access: 'TRUE'
		
# Role IDs separated by "," (any role) or "+" (all roles)
			requirements:
  _role: 'authenticated,admin'
		
# Passes if the user is logged in
    
			requirements:
  _user_is_logged_in: 'TRUE'
		

Other useful access checkers provided

# Checks if any/all (,/+) the specified modules are enabled
			requirements:
  _module_dependencies: 'node + search'
		
# Checks access to the specified entity_type.operation
			requirements:
  _entity_access: 'node.edit'
		
# Checks access to the specified entity_type:entity_bundle
			requirements:
  _entity_create_access: 'taxonomy_term:{taxonomy_vocabulary}'
		

Declaring your own custom access checker

Declare your requirement checker in .routing.yml

			requirements:
  _you_shall_not_pass: 'TRUE'
		

Register an access checker class as a service (in .services.yml)

			services:
  access_check.foo.gandalf:
    class: Drupal\foo\Access\GandalfAccessCheck
    tags:
      - { name: access_check }
		

Writting your own custom access checker class

			class GandalfAccessCheck implements StaticAccessCheckInterface {
  public function appliesTo() {
    return array('_you_shall_not_pass');
  }
  
  public function access(Route $route, Request $request) {
    $requirement = $route->getRequirement('_you_shall_not_pass');
    if (!in_array('https', $route->getSchemes())
        && $requirement == 'TRUE') {
      return static::DENY; // or return static::ALLOW
    } // No return means other checkers will decide
  }
}
		

Access mode options, checking any or all requirements

			node.add_page:
  path: '/node/add'
  defaults:
    _title: 'Add page'
    _content: '\Drupal\node\Controller\NodeController::addPage'
  options:
    _access_mode: 'ANY'
  requirements:
    _permission: 'administer content types'
    _node_add_access: 'node'
		

Building Dynamic Routes

Implement a route subscriber class

/core/modules/system/Routing/RouteSubscriber.php

			namespace Drupal\system\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

/**
 * Provides dynamic routes for theme administration.
 */
class RouteSubscriber extends RouteSubscriberBase {
  protected function routes(RouteCollection $collection) {...}
}
		
			protected function routes(RouteCollection $collection) {
  foreach (list_themes() as $theme) {
    $route = new Route(
      'admin/appearance/settings/' . $theme->name,
      array(
        '_form' => '\Drupal\system\Form\ThemeSettingsForm',
        'theme_name' => $theme->name
      ),
      array('_permission' => 'administer themes'),
    );
    $collection->add(
      'system.theme_settings_' . $theme->name, $route);
  }
}
		

Dependency injection

Keep your controllers clean and thin

Our classes shouldn't have hard-coded dependencies

			class SongController {
      
  public function getRandomSong() {
    // db_query() is a hard-coded dependency
    $songs = db_query('SELECT name FROM {songs}')->fetchCol();
    $song = $songs[rand(0, count($songs) - 1)];
    return new JsonResponse(
      array('song' => $song)
    );
  }
}
		

Decoupling the dependency

			
  protected $database; // Drupal\Core\Database\Connection;

  public function getRandomSong() {
    $songs = $this->database
      ->query('SELECT name FROM {songs}')->fetchCol();
    return $songs[rand(0, count($songs) - 1)];
  }

		

Getting the dependency injected

			use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class SongController implements ContainerInjectionInterface {
  protected $database;
  public function __construct(Connection $database) {
    $this->database = $database;
  }
  public static function create(ContainerInterface $container) {
    // Instantiate the controller with a database object
    return new static($container->get('database'));
  }

		

Want to see more coming?

/join #drupal-contribute

Thank you!