Drupal 8
Theme System

hook_theme() to
Twig template

Scott Reeves & Joël Pittet

Scott Reeves

Scott Reeves
@Cottser
  • Developer at Digital Echidna
  • Drupal 8 theme system
    co-maintainer
  • Drupal core mentor
  • Likes beans

Joël Pittet

  • Making Websites since 2001ish
  • Full stack developer
  • Drupal 8 Core Theme System Maintainer
  • And <3 perogies this much!
Joël Pittet
@joelpittet

Drupal 8
theme layer changes

Template process layer

…is gone!

Theme functions

…are being converted to templates…
(and other formats).

Only 12 remaining!

Theme suggestion hooks

Drupal 7:


/**
 * Implements hook_preprocess_HOOK() for node templates.
 */
function MYTHEME_preprocess_node(&$variables) {
  $variables['theme_hook_suggestions'][] = 'node__' . 'my_first_suggestion';
  $variables['theme_hook_suggestions'][] = 'node__' . 'my_second_more_specific_suggestion';
}

Drupal 8:


/**
 * Implements hook_theme_suggestions_HOOK_alter() for node templates.
 */
function MYTHEME_theme_suggestions_node_alter(array &$suggestions, array $variables) {
  $suggestions[] = 'node__' . 'my_first_suggestion';
  $suggestions[] = 'node__' . 'my_second_suggestion';
}

Goodbye theme(), hello
render arrays

Drupal 7:


$variables['list'] = theme('item_list', array(
  'items' => $items,
));

Drupal 8:


$variables['list'] = [
  '#theme' => 'item_list',
  '#items' => $items,
];

Attributes

All the HTML attributes:
<div{{ attributes }}>
Please don't do this, you will end up with yucky whitespace:
<div {{ attributes }}>
Please don't do this, you will end up with yucky whitespace:
<div✖{{ attributes }}>
Splitting out the class attribute:
<div class="myclass {{ attributes.class }}"{{ attributes|without('class') }}>

Attributes cont'd

Class manipulation:
<div{{ attributes.addClass('hello').removeClass('goodbye') }}>
Testing:
{% if attributes.hasClass('field-label-inline') %}
  {# Do something special here. #}
{% endif %}

Attributes cont'd

Set attribute:
<div{{ attributes.setAttribute('id', 'eye-d') }}>
Remove attribute:
<div{{ attributes.removeAttribute('id') }}>

Print what you want,
when you want

Drupal 7:


// We hide the comments and links
// to print them later.
hide($content['comments']);
hide($content['links']);
print render($content);
// Render calls show() on the element.
print render($content['links']);
// To get back links with the content.
show($content['links']);
// Prints content with links yet
// without comments :(
print render($content);

Drupal 8:

{# Print without comments and links #}
{{ content|without('comments', 'links') }}

{# Print only links #}
{{ content.links }}

{# Print everything without comment! #}
{{ content|without('comments') }}

{# Print everything YAY :) #}
{{ content }}

Oh yeah, and Twig!

Did you notice we snuck that in? 😉

services.yml:

parameters:
  twig.config:
    debug: true

Example output:


<!-- THEME DEBUG -->
<!-- THEME HOOK: 'block' -->
<!-- FILE NAME SUGGESTIONS:
   * block--bartik-powered.html.twig
   * block--system-powered-by-block.html.twig
   * block--system.html.twig
   x block.html.twig
-->
<!-- BEGIN OUTPUT from 'core/modules/block/templates/block.html.twig' -->
<div class="block block-system contextual-region" id="block-bartik-powered" role="complementary">
  <div data-contextual-id="block:block=bartik_powered:"></div>
  <div class="content">
    <span>Powered by <a href="http://drupal.org">Drupal</a></span>
  </div>
</div>
<!-- END OUTPUT from 'core/modules/block/templates/block.html.twig' -->

Drupal 7.33+

settings.php:

$conf['theme_debug'] = TRUE;

Example output:


<!-- THEME DEBUG -->
<!-- CALL: theme('block') -->
<!-- FILE NAME SUGGESTIONS:
   * block--system--powered-by.tpl.php
   * block--system.tpl.php
   * block--footer.tpl.php
   x block.tpl.php
-->
<!-- BEGIN OUTPUT from 'modules/block/block.tpl.php' -->
<div id="block-system-powered-by" class="block block-system">
  <div class="content">
    <span>Powered by <a href="https://www.drupal.org">Drupal</a></span>
  </div>
</div>
<!-- END OUTPUT from 'modules/block/block.tpl.php' -->

Sandwiches.

https://github.com/DrupalTwig/sandwich

Define with hook_theme()


/**
 * Implements hook_theme().
 */
function sandwich_theme() {
  return [
    'sandwich' => [
      'variables' => [
        'attributes' => [],
        'name' => '',
        'bread' => '',
        'cheese' => '',
        'veggies' => [],
        'protein' => '',
        'condiments' => [],
      ],
    ],
  ];
}

Build your render array data


/**
 * Builds a sandwich.
 */
public function build() {
  return [
    '#theme' => 'sandwich',
    '#name' => $this->t('Chickado'),
    '#attributes' => [
      'id' => 'best-sandwich',
      'style' => 'float: left;',
      'class' => ['left', 'clearfix'],
    ],
    '#bread' => $this->t('Sourdough'),
    '#cheese' => $this->t('Gruyère'),
    '#veggies' => [$this->t('Avocado'), $this->t('Red onion'), $this->t('Romaine')],
    '#protein' => $this->t('Chicken'),
    '#condiments' => [$this->t('Mayo'), $this->t('Dijon')],
  ];
}

Pass in variables using #-prefixed keys.

Markup your Twig template


<section{{ attributes }}>
  <h2>{{ name }}</h2>
  {% if bread %}
    <p><strong>Bread:</strong> {{ bread }}</p>
  {% endif %}

  <strong>Vegetables:</strong>
  <ul>
    {% for veg in veggies %}
      <li>{{ veg }}</li>
    {% endfor %}
  </ul>

  {% if condiments %}
    <strong>Condiments:</strong>
    <ul>
      {% for condiment in condiments %}
        <li>{{ condiment }}</li>
      {% endfor %}
    </ul>
  {% endif %}
</section>

Voilà!

Themable data

Rendered Chickado Sandwich

Overview of Drupal 7
rendering flow

  1. drupal_render()
    1. #pre_render
    2. theme()
      1. Preprocess functions (and suggestions)
      2. Template/theme function is rendered
    3. #post_render

Overview of Drupal 8
rendering flow

  1. \Drupal::service('renderer')->render()
    1. #pre_render
    2. \Drupal::theme()->render()
      1. Theme suggestion hooks
      2. Preprocess functions
      3. Template is rendered
    3. #post_render

#type “Coles Notes”

Yes, that's Canadian eh!

Discoverable Differences

Drupal 7:

/**
 * Implements hook_element_info().
 */
function your_module_element_info() {
  $types['html_tag'] = array(
    '#theme' => 'html_tag',
    '#pre_render' => array(
      'drupal_pre_render_conditional_comments'
    ),
    '#attributes' => array(),
    '#value' => NULL,
  );
  return $types;
}
Drupal 8:

/**
 * Provides a render element for any HTML tag, with properties and value.
 *
 * @RenderElement("html_tag")
 */
class HtmlTag extends RenderElement {
  public function getInfo() {
    return [
      '#theme' => 'html_tag',
      '#pre_render' => [
        [HtmlTag, 'preRenderConditionalComments'],
      ],
      '#attributes' => [],
      '#value' => NULL,
    ];
  }
}

Defaults and callbacks on a reusable type of render array

Example #type render array:


$variables['link'] = [
  '#type' => 'link',
  '#title' => t('Example'),
  '#url' => 'http://example.com',
];

Calls #pre_render to build up it's #markup from a link generator.


\Drupal::service('link_generator');
$generated_link = $link_generator->generate($element['#title'], $element['#url']->setOptions($options), TRUE);
$element['#markup'] = $generated_link->getGeneratedLink();

Twig magic

{{ sandwich.cheese }}


// Array key.
$sandwich['cheese'];
// Object property.
$sandwich->cheese;
// Also works for magic get (provided you implement magic isset).
$sandwich->__isset('cheese'); && $sandwich->__get('cheese');
// Object method.
$sandwich->cheese();
// Object get method convention.
$sandwich->getCheese();
// Object is method convention.
$sandwich->isCheese();
// Method doesn't exist/dynamic method.
$sandwich->__call('cheese');

Autoescape


              $user->field_first_name = "<script>alert('XSS')</script>";

Drupal 7:

BAD   <?php print $user->field_first_name; ?>
GOOD  <?php print check_plain($user->field_first_name); ?>

Drupal 8:

GOOD {{ user.field_first_name }}
BAD  {{ user.field_first_name|raw }}

What did you think?

Evaluate our session:
https://events.drupal.org/node/321

Thanks!

@Cottser

@joelpittet

drupaltwig.org

IRC: #drupal-twig

Twitter: #drupaltwig

Questions?