Presented by Larry Garfield (@Crell)
Drupal 8 is so different,
how will I ever learn it?
—Drupal developers
I hear Drupal 8 is finally not weird,
does that mean I can learn it?
—Everyone else
The more things change,
the more they stay the same
The concepts are still Drupal
The achitecture is modernized
The APIs are more consistent
interface HttpKernelInterface {
const MASTER_REQUEST = 1;
const SUB_REQUEST = 2;
/**
* Handles a Request to convert it to a Response.
*
* @param Request $request A Request instance
* @param integer $type The type of the request
* @param Boolean $catch Whether to catch exceptions or not
*
* @return Response A Response instance
*/
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true);
}
(The concept formerly known as page callbacks and now represented by a PHP callable...)
use Symfony\Component\HttpFoundation\Response;
class MyControllers {
public function hello() {
return new Response('<html><body>Hello World</body></html>');
}
}
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
class MyControllers {
public function hello() {
return new Response('<html><body>Hello World</body></html>');
}
public function helloJson() {
$data['Hello'] = 'World';
return new JsonResponse($data);
}
}
use Symfony\Component\HttpFoundation\StreamedResponse;
class MyControllers {
public function helloCsv() {
$lots_of_data = get_lots_of_data();
$response = new StreamedResponse();
$response->headers->set('Content-Type', 'text/csv');
$response->setCallback(function() use ($lots_of_data) {
foreach ($lots_of_data as $record) {
print implode(', ', $record) . PHP_EOL;
}
});
return $response;
}
}
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class MyControllers {
public function privateFile() {
return new BinaryFileResponse('secret_plans.odt');
}
}
class MyControllers {
public function helloDrupal() {
return array(
'#theme' => 'a_drupal_render_array',
'#description' => 'Those still exist.',
);
}
}
/hello/world/{from}/{to}
use Symfony\Component\HttpFoundation\Request;
class HelloController {
public function helloDrupal($to, $from, Request $request) {
return array(
'#theme' => 'love_letter',
'#from' => $from,
'#to' => $to,
);
}
}
module.routing.yml
hello.world:
path: '/hello/world/{from}/{to}'
defaults:
_controller: '\Drupal\mymodule\Controller\HelloController::helloDrupal'
requirements:
_permission: 'access content'
from: \s+
to: \s+
Gah, enough theory, how do I do stuff?
define a module
/modules/hugs/hugs.info.yml
name: Hugs
description: Examples of hugs
type: module
core: 8.x
make a page
/modules/hugs/src/Controller/HugsController.php
namespace Drupal\hugs\Controller;
use Drupal\Core\Controller\ControllerBase;
class HugsController extends ControllerBase {
public function hug($to, $from) {
$message = $this->t('%from sends hugs to %to', [
'%from' => $from,
'%to' => $to,
]);
return ['#markup' => $message];
}
}
/modules/hugs/hugs.routing.yml
hugs.hug:
path: /hug/{from}/{to}
defaults:
_controller: 'Drupal\hugs\Controller\HugsController::hug'
_title: 'Hug!'
requirements:
_permission: 'access content'
make content themeable
/modules/hugs/hugs.module
function hugs_theme() {
$theme['hug_page'] = [
'variables' => ['from' => NULL, 'to' => NULL],
'template' => 'hug_page',
];
return $theme;
}
/modules/hugs/template/hug_page.html.twig
<section>
{% trans %}
<strong>{{ from }}</strong> hugs <em>{{ to }}</em>
{% endtrans %}
</section>
/modules/hugs/src/Controller/HugsController
namespace Drupal\hugs\Controller;
use Drupal\Core\Controller\ControllerBase;
class HugsController extends ControllerBase {
public function hug($to, $from) {
return [
'#theme' => 'hug_page',
'#from' => $from,
'#to' => $to,
];
}
}
make a config form
/modules/hugs/config/install/hugs.settings.yml
default_count: 3
/modules/hugs/config/schema/hugs.settings.yml
hugs.settings:
type: mapping
label: 'Hug module settings'
mapping:
default_count:
type: integer
label: 'Default hug count'
/modules/hugs/src/Form/ConfigForm.php
namespace Drupal\hugs\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
class ConfigForm extends ConfigFormBase {
public function getFormId() { return 'hug_config'; }
protected function getEditableConfigNames() { return ['hugs.settings']; }
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('hugs.settings');
$form['default_count'] = [
'#type' => 'number',
'#title' => $this->t('Default hug count'),
'#default_value' => $config->get('default_count'),
];
return parent::buildForm($form, $form_state);
}
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
$config = $this->config('hug.settings');
$config->set('default_count', $form_state->getValue('default_count'));
$config->save();
}
}
/modules/hugs/hugs.routing.yml
hugs.config:
path: /admin/config/system/hugs
defaults:
_form: 'Drupal\hugs\Form\ConfigForm'
_title: 'Hug configuration'
requirements:
_permission: 'configure_hugs'
/modules/hugs/hugs.permissions.yml
configure_hugs:
title: 'Configure the hugs system'
description: 'Configure default hug count'
/modules/hugs/hugs.links.menu.yml
hugs.config:
title: 'Hugs configuration'
description: 'Configure the hugs system'
route_name: hugs.config
parent: system.admin_config_system
/modules/hugs/src/Controller/HugsController
namespace Drupal\hugs\Controller;
use Drupal\Core\Controller\ControllerBase;
class HugsController extends ControllerBase {
public function hug($to, $from, $count) {
if (!$count) {
$count = $this->config('hugs.settings')->get('default_count');
}
return [
'#theme' => 'hug_page',
'#from' => $from,
'#to' => $to,
'#count' => $count
];
}
}
/modules/hugs/hugs.routing.yml
hugs.hug:
path: /hug/{from}/{to}/{count}
defaults:
_controller: 'Drupal\hugs\Controller\HugsController::hug'
_title: 'Hug!'
count: 0
requirements:
_permission: 'access content'
count: \d+
/modules/hugs/hugs.module
function hugs_theme() {
$theme['hug_page'] = [
'variables' => ['from' => NULL, 'to' => NULL, 'count' => NULL],
'template' => 'hug_page',
];
return $theme;
}
/modules/hugs/template/hug_page.html.twig
<section>
{% trans %}
<strong>{{ from }}</strong> hugs <em>{{ to }}</em> {{ count }} time.
{% plural count %}
<strong>{{ from }}</strong> hugs <em>{{ to }}</em> {{ count }} times.
{% endtrans %}
</section>
make a block
-- or --
instanceof
"Plugins abstract/automate common OO practices
Learn once, apply everywhere
—Lee Rowlands (core developer)
/modules/hugs/src/Plugin/Block/HugStatus.php
namespace Drupal\hugs\Plugin\Block;
use Drupal\block\BlockBase;
/**
* Reports on hugability status.
*
* @Block(
* id = "hugs_status",
* admin_label = @Translation("Hug status"),
* category = @Translation("System")
* )
*/
class HugStatus extends BlockBase {
public function build() {
return [
'#markup' => $this->t('This is a hug-enabled site'),
];
}
}
/modules/hugs/src/Plugin/Block/HugStatus.php
class HugStatus extends BlockBase {
public function defaultConfiguration() {
return ['enabled' => 1];
}
public function blockForm($form, FormStateInterface $form_state) {
$form['enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Hugging enabled'),
'#default_value' => $this->configuration['enabled'],
];
return $form;
}
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['enabled'] = (bool)$form_state->getValue('enabled');
}
public function build() {
$message = $this->configuration['enabled']
? $this->t('Now accepting hugs')
: $this->t('No hugs :-(');
return ['#markup' => $message];
}
}
Make a service
/modules/hugs/src/HugTracker.php
namespace Drupal\hugs;
use Drupal\Core\State\StateInterface;
class HugTracker {
protected $state;
public function __construct(StateInterface $state) {
$this->state = $state;
}
public function addHug($target_name) {
$this->state->set('hugs.last_recipient', $target_name);
return $this;
}
public function getLastRecipient() {
return $this->state->get('hugs.last_recipient');
}
}
/modules/hugs/hugs.services.yml
services:
hugs.hug_tracker:
class: Drupal\hugs\HugTracker
arguments: ['@state']
/modules/hugs/src/Controller/HugsController.php
namespace Drupal\hugs\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\hugs\HugTracker;
use Symfony\Component\DependencyInjection\ContainerInterface;
class HugsController extends ControllerBase {
protected $hugTracker;
public function __construct(HugTracker $tracker) {
$this->hugTracker = $tracker;
}
public static function create(ContainerInterface $container) {
return new static($container->get('hugs.hug_tracker'));
}
public function hug($to, $from, $count) {
$this->hugTracker->addHug($to);
return [
'#theme' => 'hug_page',
'#from' => $from,
'#to' => $to,
'#count' => $count ?: $this->config('hugs.settings')->get('default_count'),
];
}
}
/modules/hugs/src/Plugin/Block/HugStatus.php
namespace Drupal\hugs\Plugin\Block;
class HugStatus extends BlockBase implements ContainerFactoryPluginInterface {
protected $hugTracker;
public function __construct(array $configuration, $plugin_id, $plugin_definition, HugTracker $hugTracker) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->hugTracker = $hugTracker;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration, $plugin_id, $plugin_definition,
$container->get('hugs.hug_tracker')
);
}
public function build() {
$message = $this->t('No hugs :-(');
if ($this->configuration['enabled']) {
$message = $this->t('@to was the last person hugged', [
'@to' => $this->hugTracker->getLastRecipient()
]);
}
return ['#markup' => $message];
}
// ...
}
Work with content
The API is fully baked now!
/modules/hugs/hugs.routing.yml
hugs.node:
path: /node/{node}/hug
defaults:
_controller: 'Drupal\hugs\Controller\HugsController::nodeHug'
requirements:
_permission: 'access content'
class HugsController extends ControllerBase {
public function nodeHug(NodeInterface $node) {
if ($node->isPublished()) {
// These are the same!
$body = $node->body->value;
$body = $node->body[0]->value;
// But we really want...
$formatted = $node->body->processed;
foreach ($node->field_tags as $tag) {
$terms[] = $tag->entity->label();
}
$message = $this->t('Everyone hug @name because @reasons!', [
'@name' => $node->getOwner()->label(), '@reasons' => implode(', ', $terms),
]);
return [
'#title' => $node->label() . ' (' . $node->bundle() . ')',
'#markup' => $message . $formatted,
];
}
return ['#markup' => $this->t('Not published')];
}
}
That should probably all be in a template...
The wiring may vary, the approach is the same
Senior Architect, Palantir.net
Let's Make Something Good Together.
Keep tabs on our work at @Palantir