Write Up: Twig theme sprint @ Chapter Three

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
You are viewing a wiki page. You are welcome to join the group and then edit it. Be bold!

On April 20-22 chx, JohnAlbin, EclipseGC, quicksketch, hefox, effulgentsia, and jenlampton sprinted on creating a new theme layer for Drupal 8, using Twig, along with help from David Needham, Garret Voorhees, and lots of sprinters from the Front End United conference.

Day 1

On day one, we studied twig and how it might integrate with Drupal.

One really great thing we learned from chx, is that the way twig templates are called is very similar to the way Drupal's theme system works already. Mainly, we need to pass it the name of a template, and an array of variables for the template to use. This means that hook_theme will need to change very little in order to use twig. YAY!

Chx suggested that we use a __toString method to flatten Twig's complex data structures into strings of HTML for printing. This is a much more elegant solution than having arrays at the preprocess level that get flattened into strings at the process level, to be printed into HTML later on.

We decided that the templating engine will need to be responsible for making the output secure, which means that it will need to be able to determine when strings should be escaped, and when they shouldn't, automatically.

Day 2

On day two, we wanted to see if we could create a working example of a single twig template, based on the decisions made on Day 1. We all started working in chx's sandbox.

We started with a fairly complex theme function: theme_item_list.

<?php
function theme_item_list($variables) {
 
$items = $variables['items'];
 
$title = $variables['title'];
 
$type = $variables['type'];
 
$attributes = $variables['attributes'];

 
// Only output the list container and title, if there are any list items.
  // Check to see whether the block title exists before adding a header.
  // Empty headers are not semantic and present accessibility challenges.
 
$output = '<div class="item-list">';
  if (isset(
$title) && $title !== '') {
   
$output .= '<h3>' . $title . '</h3>';
  }

  if (!empty(
$items)) {
   
$output .= "<$type" . drupal_attributes($attributes) . '>';
   
$num_items = count($items);
    foreach (
$items as $i => $item) {
     
$attributes = array();
     
$children = array();
     
$data = '';
      if (
is_array($item)) {
        foreach (
$item as $key => $value) {
          if (
$key == 'data') {
           
$data = $value;
          }
          elseif (
$key == 'children') {
           
$children = $value;
          }
          else {
           
$attributes[$key] = $value;
          }
        }
      }
      else {
       
$data = $item;
      }
      if (
count($children) > 0) {
       
// Render nested list.
       
$data .= theme_item_list(array('items' => $children, 'title' => NULL, 'type' => $type, 'attributes' => $attributes));
      }
      if (
$i == 0) {
       
$attributes['class'][] = 'first';
      }
      if (
$i == $num_items - 1) {
       
$attributes['class'][] = 'last';
      }
     
$output .= '<li' . drupal_attributes($attributes) . '>' . $data . "</li>\n";
    }
   
$output .= "</$type>";
  }
 
$output .= '</div>';
  return
$output;
}
?>

And recreated it as a twig template, item_list.twig:
{% if items %}
  <{{ type }} class="{{ items.attributes.class }}" {{ items.attributes }}>
  {% for item in items %}
    <li class="{{ item.attributes.class }} {{ cycle(['even', 'odd'], loop.index) }} {{ loop.first ? 'first' : '' }} {{ loop.last ? 'last' : '' }}" {{- item.attributes }}>
      {{- item -}}
    </li>
  {% endfor %}
  </{{ type }}>
{% endif %}

We were able to get the "Who's online" block, and the "User login" block to render their item lists using our twig template, proving that using Drupal and Twig together is actually possible.

Starting with this particular theme function also meant that we needed to also solve "the attributes problem", and after spending a few hours looking at HTML attributes and valid syntaxes, we came up with a suitable solution to the attributes problem, as being worked on in http://drupal.org/node/1290694.

Day 3

On day three, we decided to back up, and get out of the code. We had learned that using twig for theme functions and template files was possible, so now the big question became "how does this change things?".

We started with an overview of page rendering in Drupal 7, including how the theme() function currently generates it's output. We identified a few pain points, as clearly indicated in JohnAlbin's infamous flow chart:

We then evaluated how page rendering in Drupal 8 might look, along with a new simplified version of the theme() function.

The most exciting revelation for me, was that we'd be able to eliminate several parts of this confusing puzzle, since flattening arrays (and objects) into HTML strings is now something that twig can do as part of theme(). If twig can also replace render() John's flowchart starts to look much more manageable:

So, what about PHPTemplate? Well, it turns out the PHPTemplate engine is only about 30 lines of code:

<?php
/<strong>
* @
file
* Handles integration of PHP templates with the Drupal theme system.
*/

/</
strong>
* Implements
hook_init().
*/
function
phptemplate_init($template) {
 
$file = dirname($template->filename) . '/template.php';
  if (
file_exists($file)) {
    include_once
DRUPAL_ROOT . '/' . $file;
  }
}

/**
* Implements hook_theme().
*/
function phptemplate_theme($existing, $type, $theme, $path) {
 
$templates = drupal_find_theme_functions($existing, array($theme));
 
$templates += drupal_find_theme_templates($existing, '.tpl.php', $path);
  return
$templates;
}
?>

And chx thought he could be convinced to leave this in core. This means that all those people who love PHP and want to be able to execute PHP in their template files (and promise that they will do it securely) would still be able to use PHPTemplate.

However, if you want PHP - you're going to get PHP. We are not going to dumb it down for you. We are not going to provide you with any variables that are previously sanitized and ready for output, all you get are the raw data objects - the ones that were also passed to twig for rendering. This means that every time you need to print anything in a PHPtemplate, you need to sanitize it yourself.

When thinking more about how to render data for use in a PHPTemplate, it became clear that we could use the same engine that renders data for twig templates - by extending it. We could create a PHPTemplate version of the render() method that would always need to be called in order to render anything into a template file.

The huge upside here is that 100% of Drupal themes would become secure. Twig themes would be secure - automatically. And for PHPTemplate themes - the rules finally become crystal clear. Always, always, always sanitize anything you are printing, and here's the method you use to do it.

So, what would a PHPTemplate look like without those pretty variables?

<?php if ($items) { ?>
  <<?php print $type->render(); ?> class="<?php print $items['attributes']['class']->render(); ?>" <?php print $items['attributes']->render(); ?>>
  <?php foreach $items as $item { ?>
    <li class="<?php print $item['attributes']['class']->render(); ?> <?php print $items->even() ? 'even' : 'odd' ?> <?php print $items->first() ? 'first' : '' ?> <?php $items->last() ? 'last' : '' ?>" <?php print $item['attributes']->render(); ?>>
      <?php print $item->render(); ?>
    </li>
  <?php } ?>
  </<?php print $type->render(); ?>>
<?php } ?>

Developers today are worried about how long it takes for Drupal to render pages, as well as how much memory Drupal needs to hold these massive arrays. How would that change by using Twig? The real answer is that we won't really know until we replace everything, but we suspect that things are only going to get better.

Effulgentsia pointed out that for a node with 50 comments, with PHPTemplate, PHP needs to call "include" for the comment.tpl.php file 50 times, which requires accessing the disk 50 times (even with APC enabled, under default settings APC must check the modification timestamp of the file, and for shared hosts not using APC, a full read and code parse is needed). With or without APC, this can be time consuming.

The twig approach would read the comment.twig file from disk once, and compile it into a PHP class, which it would then hold in memory. As the page with 50 comments is being rendered, the class is simply instantiated 50 times with 50 different sets of data. This should mean that these kinds of pages can render more efficiently using Twig.

Overall

This sprint was a wild success for several reasons.

  1. We proved that using Twig with Drupal is actually Possible.
  2. We think that using by using Twig we may make Drupal faster.
  3. We discovered that by using Twig, we could make 100% of all themes more secure.
  4. We discovered that by using Twig, we could reduce the complexity of the theme layer.
  5. And Lastly, we realized that we could find a way to keep PHPTemplate around for those who don't like change. (probably)
AttachmentSize
theme-layer.png482.96 KB
theme-layer-revamp.png481.69 KB

Comments

Effulgentsia pointed out that

moshe weitzman's picture

Effulgentsia pointed out that for a node with 50 comments, with PHPTemplate, PHP needs to call "include" for the comment.tpl.php file 50 times, which requires accessing the disk 50 times (even with APC enabled, under default settings APC must check the modification timestamp of the file, and for shared hosts not using APC, a full read and code parse is needed). /code>

Modern operating systems cache stat information so this is false AFAIK. No disk access is needed to determine whether cached file is stale or not.

The twig approach would read the comment.twig file from disk once, and compile it into a PHP class, which it would then hold in memory

I'm pretty sure PHP holds the contents of comment.tpl.php in memory as well so I don't get how the situation is improved with Twig. 49 of those include() statements are basically free (with or without APC).

Needs testing on some common hosts

effulgentsia's picture

When I benchmark on MAMP, neither PHP nor Mac OS behave as though any intelligent file system or PHP code caching is happening. At one point a few years ago, I also tested a cheap shared hosting server and found the same. This would be worth testing on modern Linux hosting setups though.

That would be the case if it

sun's picture

That would be the case if it was an include_once. But it's an include.

Daniel F. Kudwien
netzstrategen

Can you elaborate?

moshe weitzman's picture

Can you elaborate? include_once() is more expensive than include(). See http://stackoverflow.com/questions/4326435/php-include-vs-include-once-speed

I think I get it now. include() is roughly like an eval() of all the code in the already-in-memory tpl file. Instantating a class and calling render() method might be a faster way of doing the same thing for some reasons known only to php devs.

hook_page_alter

totten's picture
  1. Comparing the two diagrams, there appear to be two different forms of "X" -- blue and red. Are these supposed to indicate different things?

  2. Is it really a good idea to eliminate hook_page_alter? In D6, page content was flattened into HTML -- the addition of structured pages and hook_page_alter in D7 seemed to me (as a relative outsider) like an enhancement. This page suggests that it's used several times.

Colors

jenlampton's picture

1) The blue Xs are the removal of the rendering system. The red Xs are the removal of the process layer. The pink Xs are the removal of all theme functions in favor of template files.

2) Pages in D8 will still be structured - just very differently. What will change most about rendering is when it hppens. In D7 most of the HTML was generated long before it's printed - occasionally wasting rendering power for HTML that is later thrown away - and always consuming massive amounts of memory while that gigantic $page array is carried around before rendering. in D8 we don't want anything generated until we know it's needed. When the template file says "print author" that's when "author" gets created. Not before.

"Pages" and page layouts will be handled very differently in Drupal 8, so all we are really talking about for the new theme layer are blocks and elements smaller than blocks. For more on how the Blocks everywhere initiative will affect theme development (most of which we still don't know for sure) see the second twig sprint write up.

Thanks guys for this write

podarok's picture

Thanks guys for this write up
As for me Twig logic is much more simple and consistens against PHPtemplate

Leaving possibility of using *.tpl.ph files in core is good. Old way is not bad way sometimes


Andriy Podanenko
web: http://druler.com

Simplify with css 3

dillix's picture

I think we can simplify this code:

{% if items %}
  <{{ type }} class="{{ items.attributes.class }}" {{ items.attributes }}>
  {% for item in items %}
    <li class="{{ item.attributes.class }} {{ cycle(['even', 'odd'], loop.index) }} {{ loop.first ? 'first' : '' }} {{ loop.last ? 'last' : '' }}" {{- item.attributes }}>
      {{- item -}}
    </li>
  {% endfor %}
  </{{ type }}>
{% endif %}

to twig:

{% if items %}
  <{{ type }} class="{{ items.attributes.class }}" {{ items.attributes }}>
  {% for item in items %}
    <li class="{{ item.attributes.class }}" {{- item.attributes }}>
      {{- item -}}
    </li>
  {% endfor %}
  </{{ type }}>
{% endif %}

and CSS3:

ul.item_block_class li:first-child {
...
}
ul.item_block_class li:last-child {
...
}
ul.item_block_class li:nth-child(odd) {
...
}
ul.item_block_class li:nth-child(even) {
...
}

We are Dillix Media Group and we like to support FreeBSD Community