What to Expect from Drupal 8

(For developers)

Presented by Larry Garfield (@Crell)

Palantir.net Pays me!

@Crell

Palantir.net Pays me!
  • Senior Architect, Palantir.net
  • Drupal 8 Web Services Lead
  • Drupal Representative, PHP-FIG
  • Advisor, Drupal Association
  • Loveable pedant

http://bit.ly/palantir-d8-devs

https://github.com/palantirnet/hugs

Drupal 8 is so different,
how will I ever learn it?

The more things change,
the more they stay the same

Many APIs are new

The concepts are much the same

New concepts are there to make life easier

Just enough theory

PHP 4 architecture

HTTP Architecture

Symfony/HttpKernel architecture


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);
}
            

HttpKernel pipeline

Controllers

(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;
  }
}
            

class MyControllers {

  public function helloString() {
    return "The view event will turn this into a response.";
  }
}

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 MyControllers {

  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:
        _content: '\Drupal\mymodule\Controller\HelloController::helloWorld'
      requirements:
        _permission: 'access content'
        from: \s+
        to: \s+
    

Gah, enough theory, how do I do stuff?

Drupal in 2 steps:

  1. Build a tool
  2. Wire it up

Drupal in 2 (other) steps:

  1. Extend a base class/interface
  2. Tell Drupal about it

How do I...?

define a module

/modules/hugs/hugs.info.yml


name: Hugs
description: Examples of hugs
type: module
core: 8.x
            
The modules page, showing the hug module.

How do I...?

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 $message;
  }
}
            

/modules/hugs/hugs.routing.yml


hugs.hug:
  path: /hug/{from}/{to}
  defaults:
    _content: 'Drupal\hugs\Controller\HugsController::hug'
    _title: 'Hug!'
  requirements:
    _permission: 'access content'
            
The simple hug controller.

How do I...?

make content themeable

/modules/hugs/hugs.module


function hugs_theme() {
  $theme['hug_page'] = [
    'variables' => array('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,
    ];
  }
}
            
Now the page is themed.

How do I...?

make a 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;

class ConfigForm extends ConfigFormBase {
  public function getFormId() {
    return 'hug_config';
  }

  public function buildForm(array $form, array &$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, array &$form_state) {
    parent::submitForm($form, $form_state);
    $config = $this->config('hug.settings');
    $config->set('default_count', $form_state['values']['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.menu_links.yml


hugs.config:
  title: 'Hugs configuration'
  description: 'Configure the hugs system'
  route_name: hugs.config
  parent: system.admin_config_system
              

/modules/hugs/hugs.module


function hugs_permission() {
  $perms['configure hugs'] = [
    'title' => t('Configure the hugs system'),
  ];

  return $perms;
}
            
The menu item is now created
The form has the field we specified

/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:
    _content: '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' => array('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>
              
We now see the default hug count.
The hug count is overridden by the URI.

How do I...?

make a block

Definitions

  • Plugin: Swappable, user-defined piece of functionality
  • Plugin Type: A category of inter-changeable plugins
  • Discovery: Collecting/cataloging plugins

-- or --

  • Plugin Type: Interface
  • Plugin: Implementing class
  • Discovery: "find all instanceof"

Plugins abstract/automate common OO practices

Drupal 7 extensibility

  • Info hooks
  • Little else in common
  • ctools plugins: PHP 4 yuck
  • No actual commonality

Drupal 8 extensibility

  • Want user-configurable logic choices? Plugin
  • Define interface, define manager class, done

Learn once, apply everywhere

—Lee Rowlands (core developer)

Pluginified systems in Drupal 8

  • Actions
  • Blocks
  • Text formats
  • Fields
  • Widgets
  • Formatters
  • Image effects
  • Conditions
  • Mail backends
  • Archiver
  • Image toolkits
  • Aggregator (Fetcher, Parser, Processor)
  • Editor extensions
  • EntityReference selections
  • Migration
  • REST Resources
  • Search backends
  • Tours
  • All of Views…

/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'),
    ];
  }
}
            
The blocks configuration page
The hug block configuration page
The hug block shown on a page

/modules/hugs/src/Plugin/Block/HugStatus.php


class HugStatus extends BlockBase {

  public function defaultConfiguration() {
    return array('enabled' => 1);
  }

  public function blockForm($form, &$form_state) {
    $form['enabled'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Hugging enabled'),
      '#default_value' => $this->configuration['enabled'],
    ];

    return $form;
  }

  public function blockSubmit($form, &$form_state) {
    $this->configuration['enabled'] = (bool)$form_state['values']['enabled'];
  }

  public function build() {
    $message = $this->configuration['enabled']
      ? $this->t('Now accepting hugs')
      : $this->t('No hugs :-(');
    return [
      '#markup' => $message,
    ];
  }
}            
Hug configuration form, now with more options

How do I...?

Make a service

Services

  • "Object that does something"
  • Stateless
  • Usually only one of

/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);
    if (!$count) {
      $count = $this->config('hugs.settings')->get('default_count');
    }
    return [
      '#theme' => 'hug_page',
      '#from' => $from,
      '#to' => $to,
      '#count' => $count
    ];
  }
}
            

/modules/hugs/src/Plugin/Block/HugStatus.php


namespace Drupal\hugs\Plugin\Block;

// use ...

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,
    ];
  }
  // ...
}
            
The block is now populated by our service.

How do I...?

Work with content

Entities

  • Storable ID'd content
  • Content entities: "Fieldable"
  • Configuration entities: Useful shortcut

The API is fully baked now!

Content entities

  • Nodes
  • Users
  • Taxonomy terms
  • Files
  • Comments
  • ...
An article with a few fields.

/modules/hugs/hugs.routing.yml


hugs.node:
  path: /node/{node}/hug
  defaults:
    _content: 'Drupal\hugs\Controller\HugsController::nodeHug'
  requirements:
    _permission: 'access content'
            

/modules/hugs/src/Plugin/Block/HugStatus.php


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;

      // Someone convert this function please.
      $formatted = check_markup($body, $node->body->format);

      $terms = [];
      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 $this->t('Not published');
  }
}
            

Actually that should probably all be in a template...

Our extra page.

Drupal in 2 steps:

  1. Build a tool
  2. Wire it up

The wiring may vary, the approach is the same

Guidelines

  • Small classes, smaller methods
  • Thin controllers
  • Logic in services
  • Not a DB query in sight...
  • If it's hard to unit test, you're probably doing it wrong
Drupal 8

Larry Garfield

Senior Architect, Palantir.net

Making the Web a Better Place

Keep tabs on our work at @Palantir

Want to hear about what we're doing?