Last updated August 14, 2016. Created on February 19, 2013.
Edited by chanchal2002, rodtatham, cj_schrunk, jemandy. Log in to edit this page.

In order to make Drupal 8 theming as performant as possible and allow for more customization in Twig templates, please follow these best practices:

Return render arrays from preprocess functions

Always return render arrays instead of calling theme() or drupal_render() within preprocess functions.

Twig renders everything automatically so there is no need to call drupal_render() or theme() within a preprocess function. Instead, render arrays should be passed to the template since this allows for much more customization than an already-rendered HTML string.

Removing theme() call from a preprocess function:

// Before - passing a string of rendered HTML to the template.
$variables['table'] = theme('table', array('header' => $header, 'rows' => $rows));

// After - passing a render array to the template.
$variables['table'] = array(
  '#theme' => 'table',
  '#header' => $header,
  '#rows' => $rows,
);

Removing drupal_render() from a preprocess function is just a matter of removing the call:

// Before, unnecessary call to drupal_render().
$variables['teaser'] = drupal_render($node_teaser);

// After, with drupal_render() removed.
$variables['teaser'] = $node_teaser;

Common is that drupal_render() was called when adding to table data.

// Before, unnecessary call to drupal_render().
$row[] = drupal_render($display['title']);

// After, with drupal_render() removed.  
$row[]['data'] = $display['title'];

Call filters and utility functions in templates

While render arrays provide an addressable, alterable structure for data all the way through to the template, not all variables require render arrays. To provide raw data to templates for as long as possible, theme developers should call filters such as t and utility functions such as url() from within Twig templates. Calling these functions in the Twig template rather than the preprocess function can cut down on function calls since variables passed to the template might not get printed in the template at all.

Before:

In the preprocess function:

$variables['no_content_text'] = t('You have not created any content types yet. Go to the <a href="@create-content">content type creation page</a> to add a new content type.', array('@create-content' => url('admin/structure/types/add')));

In the template:

<p>{{ no_content_text }}</p>

After:

In the template:

<p>{{ 'You have not created any content types yet. Go to the <a href="@create-content">content type creation page</a> to add a new content type.'|t({'@create-content': url('admin/structure/types/add')}) }}</p>

Show/Hide & removing drupal_render_children and element_children

If hide() was called in the original template, and drupal_render_children was used to render "the rest" of the data, we'll need to separate this all out into separate variables in preprocess.

Before (PHPTemplate file):

<?php
hide($form['advanced']);
hide($form['actions']);
?>
<div class="layout-node-form clearfix">
<div class="layout-region layout-region-node-main">
<?php print drupal_render_children($form); ?>
</div>
<div class="layout-region layout-region-node-secondary">
<?php print render($form['advanced']); ?>
</div>
<div class="layout-region layout-region-node-footer">
<?php print render($form['actions']); ?>
</div>
</div> 

Preprocess everything into separate variables, and pass those into the template. You may need to unset the things you render into variables from the whole element (in this case form) before rendering the rest. Print the content exactly as intended into the template.

After: (preprocess)

function template_preprocess_node_edit_form(&$variables) {
  $form = $variables['form'];
  
  // @todo Update this once drupal.org/node/1920886 is resolved.
  $variables['advanced'] = $form['advanced'];
  $variables['actions'] = $form['actions'];
  unset($form['advanced'], $form['actions']);
  $variables['form'] = drupal_render_children($form);
}

After: (Twig template)

<div class="layout-node-form clearfix">
  <div class="layout-region layout-region-node-main">
    {{ form|without('advanced', 'actions') }}
  </div>
  <div class="layout-region layout-region-node-secondary">
    {{ form.advanced }}
  </div>
  <div class="layout-region layout-region-node-footer">
    {{ form.actions }}
  </div>
</div>

Looking for support? Visit the Drupal.org forums, or join #drupal-support in IRC.

Comments

wheelercreek’s picture

Just to be clear, you can add preprocess functions to your theme's .theme file, which takes the place of template.php in Drupal 8. Also, this might not be obvious but this is a PHP file and needs to begin with <?php.

So for example, in a theme named "foo", the file would be called "foo.theme", and a complete call to a preprocess function might look something like:

<?php 
   function foo_preprocess_page(&vars) {
      //put code here, and modify the $vars array
      //for example: 
      $vars['some_new_var'] = 'some value'; 
   } 

This variable some_new_var should now be available inside page.html.twig. The functions doesn't need to actually return the array, since it was passed by reference.

I'm not sure, maybe there are other places where these preprocess functions can go (like in a module), but I always used them in the template.php file (now THEMENAME.theme).

Yanivs’s picture

Thanks for the example, it helped me, but...

There is a missing "$" before vars, should be:
function foo_preprocess_page(&$vars)

thecelavi’s picture

Twig is executed in top-down manner without possibility to preprocess chunks of code in Twig directly.

However, there is an Twig extension which allows you to set order of execution of chunks of Twig template: https://github.com/RunOpenCode/twig-bufferized-template

zapo’s picture

I've been working on a Drupal 8 project for a few weeks now. Everything was great until it came time to get into preprocess functions to add classes to certain fields.

I could not figure out why none of my preprocess functions were working. Nothing was happening. I copy+pasted code from core/themes to try and debug but nothing would happen. I looked around at some other Drupal 8 themes and noticed I was missing 1 very specific line: package: custom

I added package: custom to my *.info.yml, cleared the cache and my preprocess functions actually started doing something.