How to use Hooks for building Drupal 8 themes

woman typing on laptop viewing drupal 8 screen

For this article, we enlisted front end developer, Abby Milberg, to give us a little inspiration for Drupal 8 theme building. Abby is the expert here, so I'll let her take it away.

What is a preprocess function?

A preprocess function creates or modifies variables and render arrays (arrays of data structured in specific ways that Drupal knows how to render as HTML) before they’re rendered and/or passed to a template.

Why not just use Twig?

In many cases, it's possible to achieve the same final output by using either a preprocess function or putting logic directly in the relevant Twig template. For many themers like myself, especially those coming from a front-end background unrelated to Drupal and PHP, the latter can seem like the path of least resistance. It's important to consider the pros and cons of each approach for each use case, though. While Twig has an important role to play, preprocess functions offer a wide range of advantages: they're easily reusable, performant, and by separating programmatic logic from your templates, you can easily access the full power of the Drupal API. Furthermore, the data structures you modify or create will remain intact and accessible from other parts of your Drupal project, such as custom modules.

Example #1:

Adding bundle and view-mode classes to entities

If you're using a base theme, whether a core option like Classy or contrib option like Bootstrap, these classes probably exist out of the box. I prefer to start with a blank slate (no base theme), though, which means that most entities have no classes unless I add them. On the downside, I have to spend a few minutes adding them myself. On the upside, though, these functions are completely reusable from project to project and let me set up the classes precisely how I choose.

/**
 * Implements hook_preprocess_node().
 */
function mytheme_preprocess_node(&$variables) {
  // Get the node's content type
  $type = $variables['node']->getType();

  // Get its view mode
  $mode = $variables['view_mode'];

  // Make sure we have a class array
  if (!isset($variables['attributes']['class'])) {
    $variables['attributes']['class'] = [];
  }

  // Add our classes
  $variables['attributes']['class'][] = 'node--type-' . $type; // ex: node--type-article
  $variables['attributes']['class'][] = 'node--mode-' . $mode; // ex: node--mode-teaser
  $variables['attributes']['class'][] = 'node--type-' . $type . '--mode-' . $mode; // ex: node--type-article--mode-teaser
}

This isn't only useful for nodes. We can follow this same basic model for any type of entity: media, taxonomy terms, bricks, paragraphs, custom entities, you name it. Here are two more examples:

/**
 * Implements hook_preprocess_media().
 */
function mytheme_preprocess_media(&$variables) {
  // Get the media entity's bundle (such as video, image, etc.)
  $mediaType = $variables['media']->bundle();

  // Make sure we have a class array, just like with the nodes
  if (!isset($variables['attributes']['class'])) {
    $variables['attributes']['class'] = [];
  }

  // Add a class
  $variables['attributes']['class'][] = 'media--type-' . $mediaType; // ex: media--type-video
}

/**
 * Implements template_preprocess_block().
 */
function mytheme_preprocess_block(&$variables) {
  // Custom block type helper classes.
  if (!isset($variables['elements']['content']['#block_content'])) {
    // This checks whether we actually have a custom, fielded block type, or if
    // we're working with a generic out-of-the-box block.
    return;
  }

  // Get the block type name
  $bundle = $variables['elements']['content']['#block_content']->bundle();

  // Make sure we have a class array
  if (!isset($variables['attributes']['class'])) {
    $variables['attributes']['class'] = [];
  }

  // Add our class
  $variables['attributes']['class'][] = 'block--bundle-' . $bundle;
}

Example #2:

Building a link (the "Drupal way")

The Drupal API has its own ways of handling lots of data structures, including links. Why would we want to preprocess a link? The following example builds a link using hard-coded values, but the real value of preprocessing links comes from pulling in values from various fields on your node (text, link, images, etc.) and building the exact markup that suits your use case. These fields and how you wish to combine them will likely be different on every project.

// These are required at the top of your file in order to use Link and Url later
// in the preprocess function.
use Drupal\Core\Link;
use Drupal\Core\Url;

/**
 * Implements hook_preprocess_node().
 */
function mytheme_preprocess_node(&$variables) {
  // Use URL::fromUri to turn a plain uri into a Drupal URL object.
  // It might need another URL::fromXYZ function, depending on input.
  // See https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Url.php/class/Url/8.9.x
  // for more possibilities.
  // If you pull your URL from a Drupal link field, it will probably be a URL object already.
  $my_url = URL::fromUri('https://www.some-url.com/');

  // Set the text or markup you want inside the link. Note the use of t() to
  // ensure that Drupal's multilingual functionality recognizes the text.
  $my_text = t('Click Me!');

  // Set any attributes you want as an array.
  $link_options = [
    'attributes' => [
      'target' => '_blank',
      'class' => ['btn', 'btn--large'],
    ],
  ];

  // Apply the options to the URL
  $my_url->setOptions($link_options);

  // Build the final link.
  // See https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Link.php/function/Link%3A%3AfromTextAndUrl/8.9.x
  $variables['my_full_link'] = Link::fromTextAndUrl($my_text, $my_url)->toString();
  // You can now render this link in a Twig template by calling {{ my_full_link }}
}

Example #3:

Adding placeholder text to the Drupal search block

I find myself using this one on every project - when's the last time you got a design that didn't call for the search bar to have placeholder text? Luckily, it's short and usually requires no modifications.

/**
 * Implements hook_form_FORM_ID_alter().
 */
function mytheme_form_search_block_form_alter(&$form, $form_state) {
  // Adds placeholder text to the search field.
  $form['keys']['#attributes']['placeholder'] = t('Search');
}

Example #4:

Changing field template suggestions

Out of the box, Drupal gives us lots of options for templates that each field can use. Sometimes, though, you may want to point multiple different (programmatically unrelated) fields to the same Twig template. A common use case that I personally employ on most projects is something like field__bare - a field template I create that has no wrapper divs, labels, or classes, but simply renders the field's contents. There are plenty of other design reasons that you might want different fields to point to the same template, though.

/**
 * Implements hook_theme_suggestions_HOOK_alter().
 */
function mytheme_theme_suggestions_field_alter(array &$suggestions, array $variables) {
  // Set custom template suggestions for fields.
  // Switch based on field names
  switch ($variables['element']['#field_name']) {
    case 'field_something':
    case 'field_something_else':
      // Add this to the list of templates that those fields will look for
      // The file field--bare.html.twig would go in
      // mytheme/templates/field
      $suggestions[] = 'field__bare';
      break;
    case 'field_another_one':
      // The file field--another-template.html.twig would go in
      // mytheme/templates/field
      $suggestions[] = 'field__another_template';
      break;
  }
}

Example #5:

Helpful page-level classes

Last but not least, by using hook_preprocess_html, we can apply a variety of useful classes to a page's <body> element This can be particularly useful if we need to change something in global elements, like the header and footer, based on a page's content or a user's role. Here are a few examples:

/**
 * Implements hook_preprocess_html().
 */
function mytheme_preprocess_html(&$variables) {
  // Apply a special class to the homepage, which often has no distinct content
  // type but often needs special design treatment.
  $variables['attributes']['class'][] = \Drupal::service('path.matcher')->isFrontPage() ? 'frontpage' : '';

  // Apply a class at the page level based on content type if we're on a node
  // page. Also helpful when you need to modify global elements that appear
  // outside the node itself.
  $variables['attributes']['class'][] = isset($variables['node_type']) ? 'page-node-type--' . $variables['node_type'] : '';

  // Check whether the viewer is logged in.
  $variables['attributes']['class'][] = $variables['logged_in'] ? 'logged-in' : 'logged-out';
}