drupal 8 in action, the route to the method
Post on 21-Oct-2014
593 Views
Preview:
DESCRIPTION
TRANSCRIPT
Drupal 8 in action,the route to the method
@juanolalla #DrupalCampEs
about.me/juanolalla
Juan Olallaweb developer focused on drupalat @ideup (@gowex)
MotivationI’ve recently been contributing to Drupal 8 coreand I want to share with you what I've learned
Wellcome to
Drupal 8 Routing
Goodbye tohook_menu()
Goodbye to hook_menu() for routingand 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 Routingcomponent
And also uses Symfony CMFRoutingBundle 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
• PHP code:
• a RouteCollection object
• closures
• class annotations
• External files:
• YAML
• XML
• PHP
Drupal 8 uses YAML to define routesfoo.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 insteadof PSR-0
/modules/foo/lib/Controller/FooController.phpinstead of/modules/foo/lib/Drupal/foo/Controller/FooController.php
drupal.org issue: https://drupal.org/node/1971198
Returning a JSON responsenamespace 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" insideDrupal content
using a /hello-world path
Defining the route in YAMLfoo.hello_world:
path: '/hello-world'
defaults:
_content:
'\Drupal\foo\Controller\FooController::helloWorld'
_title: 'Greeting'
requirements:
_permission: 'access content'
Returning a render arraynamespace 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 defaultsdefaults:
_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 format /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 FormInterfacenamespace 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 idcontact.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 entitiesfoo.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 Securitycomponent, and manages access from routing
Symfony uses a separate Security component for controlling route accessthat is too complex to integrate in Drupal 8.
Drupal 8 implements access checkers services, which will ignore, allow ordeny the access based on a requirement on the route.
Checking Permissionsnode.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.ymlrequirements:
_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 classclass 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 requirementsnode.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 RoutesImplement a route subscriber class
/core/modules/system/Routing/RouteSubscriber.phpnamespace 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 injectionKeep your controllers clean and thin
Our classes shouldn't have hard-coded dependenciesclass 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 injecteduse 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!
top related