Juan Olalla - DrupalCamp Spain Cáceres - October 27, 2013
@juanolalla #DrupalCampEs
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
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.
And also uses Symfony CMF RoutingBundle for dynamic routing
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
);
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'
using a /hello-world path
/modules/foo/foo.routing.yml
foo.hello_world:
path: '/hello-world'
defaults:
_controller: '\Drupal\foo\Controller\FooController::helloWorld'
requirements:
_access: 'TRUE'
/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!
/modules/foo/lib/Controller/FooController.php
instead of
/modules/foo/lib/Drupal/foo/Controller/FooController.php
drupal.org issue: https://drupal.org/node/1971198
namespace Drupal\foo\Controller;
use Symfony\Component\HttpFoundation\JsonResponse
class FooController {
public function helloWorld() {
return new JsonResponse(
array('greeting' => 'Hello World!')
);
}
}
{"greeting":"Hello World!"}
using a /hello-world path
foo.hello_world:
path: '/hello-world'
defaults:
_content: '\Drupal\foo\Controller\FooController::helloWorld'
_title: 'Greeting'
requirements:
_permission: 'access content'
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;
}
}
drupal_set_title() is being removed
defaults:
_title_callback: '\Drupal\foo\Controller\FooController::getTitle'
$build['#title'] = ...
at /admin/config/people/greeting
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);
}
}
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);
}
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'
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'
# 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(); } }
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) {...}
}
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
# {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+
_you_shall_not_pass: TRUE
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.
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'
function node_permission() {
return array(
'administer content types' => array(
'title' => t('Administer content types'),
'restrict access' => TRUE,
),
);
}
<timplunkett> juanolalla: no plans to replace that AFAIK
# 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'
# 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}'
requirements:
_you_shall_not_pass: 'TRUE'
services: access_check.foo.gandalf:
class: Drupal\foo\Access\GandalfAccessCheck tags: - { name: access_check }
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
}
}
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'
Implement a route subscriber class
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);
}
}
Keep your controllers clean and thin
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)
);
}
}
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)];
}
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'));
}
/join #drupal-contribute