Drupal 8 Theme System – hook_theme() to Twig template – Scott Reeves



Drupal 8 Theme System – hook_theme() to Twig template – Scott Reeves

0 0


ThemeSystemLA2015


On Github DrupalTwig / ThemeSystemLA2015

Drupal 8 Theme System

hook_theme() to Twig template

Scott Reeves & Joël Pittet

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!
@joelpittet

1:30 - 5:00

Now we can hit 'B' and gauge the personas in the audience. Ask people to raise their hand if they are:

  • Site builders
  • Themers
  • Developers
  • DevOps
  • Other!

Drupal 8 theme layer changes

Template process layer

…is gone!

5:00 - 8:00

  • The main reason for the process layer was to flatten strings into HTML.
  • In Drupal 8 we prefer "lazy" rendering things as late as possible, which usually gets called from the template.

Theme functions

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

Only 12 remaining!

8:00 - 11:00

Theme functions are so Drupal 7 (effectively deprecated, people will yell at you) Markup belongs in templates, not in PHP strings. Your IDE/Text Editor will reward you by syntax highlighting markup and much less string concatenation and quote escaping is needed. We have 20 left to convert.

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

11:00 - 14:00

In Drupal 7 you could manipulate theme suggestions during preprocess, but by this point it might already be too late, or you've already done unnecessary preprocessing. Drupal 8 splits out the manipulation of theme hook (template) suggestions into separate hooks. You can see template suggestions in action when we talk about Twig debug in a few minutes.

Goodbye theme(), hello render arrays

Drupal 7:

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

Drupal 8:

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

14:00 - 16:00

Instead of the list variable being a flat string of HTML, it's a render array that gets passed to the template and rendered from there.

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

16:00 - 19:00

Attributes contain the attributes and also know how to print them properly. If you are printing the entire set of attributes, don't prepend a space.

Attributes cont'd

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

19:00 - 21:00

Attributes cont'd

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

21:00 - 21:30

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

21:30 - 23:00

Oh yeah, and Twig!

Did you notice we snuck that in? 😉

23:00 - 25:00

  • Twig is a flexible, fast, and secure template engine for PHP.
  • The Twig engine and initial integration code was committed during BADCamp in November 2012.
  • The conversions of .tpl.php to .html.twig files were committed during DrupalCon Portland in May 2013 and Twig is now the default template engine for Drupal 8.

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' -->

25:00 - 27:00

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' -->

27:00 - 28:00

Sandwiches.

https://github.com/DrupalTwig/sandwich

28:00 - 29:00

We were hungry when we came up with this example.

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.

- 31:00

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

That's the bare minimum for most practical use cases. But what if you need to do more, or if you're altering themeable output that someone else has defined? Let's dig a bit deeper.

Don't make a template for condiments. Real life example: toolbar.

Overview of Drupal 7 rendering flow

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

33:00 - 37:00

drupal_render() is called on the render array. Any defined #pre_render callbacks are called. drupal_render() calls the now internal _theme() function.

After _theme() does its thing, #post_render callbacks can massage the resulting string.

Overview of Drupal 8 rendering flow

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

37:00 - 38:00

Overall pretty much the same thing, just drupal_render() and theme() got moved to service classes.

#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');

38:00 - 40:00

Show Kint demo.

  • checks if sandwich is an array and cheese a valid element;
  • if not, and if sandwich is an object, checks that cheese is a valid property;
  • if not, and if sandwich is an object, checks that cheese is a valid method;
  • if not, and if sandwich is an object, checks that getBar is a valid method;
  • if not, and if sandwich is an object, checks that isBar is a valid method;
  • if not, returns a null value.

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?