In the previous chapter we showed how Compony extended theme-specific functions to component-specific functions. But what if this fine-grained component-specific functions are not fine-grained enough? 

The problem #

Let's take the example of the preprocess functions. If we would preprocess all nodes it would look like this:

components
node
node.theme

function my_theme_preprocess_node(&$variables, $hook) {
  // Add a "rendered_author" variable to be used in node templates
  $account = $variables['node']->getOwner();
  $variables['rendered_author'] = user_view($account, 'compact');
}

Twig Created with Sketch.
node.html.twig
node--article
Twig Created with Sketch.
node--article.html.twig
node--blog
Twig Created with Sketch.
node--blog.html.twig

The function prepares a new variable that can be used in each "node" template.

But what if you also need to do something custom in a preprocess hook for article nodes only?

The your function could look like this:

components
node
node.theme

function my_theme_preprocess_node(&$variables, $hook) {
  // Add a "rendered_author" variable to be used in node templates
  $account = $variables['node']->getOwner();
  $variables['rendered_author'] = user_view($account, 'compact');

  if($variables['node']->getType() == "article") {
    // do something custom for article nodes only
    $variables['article_image_url'] = $variables['node']->field_main_image->entity->getFileUri();
  }
}

Twig Created with Sketch.
node.html.twig
node--article
Twig Created with Sketch.
node--article.html.twig
node--blog
Twig Created with Sketch.
node--blog.html.twig

But this breaks the independent component principle, as you are preprocessing article nodes, outside of node--article component.

So we could add a .theme file to node--article component in which we split up the original PHP function, which would give us back the independent individual components:

components
node
node.theme

function my_theme_preprocess_node(&$variables, $hook) {
  // Add a "rendered_author" variable to be used in node templates
  $account = $variables['node']->getOwner();
  $variables['rendered_author'] = user_view($account, 'compact');
}

Twig Created with Sketch.
node.html.twig
node--article
node--article.theme

function my_theme_preprocess_node(&$variables, $hook) {
  // do something custom for article nodes only
  $variables['article_image_url'] = $variables['node']->field_main_image->entity->getFileUri();
}

Twig Created with Sketch.
node--article.html.twig
node--blog
Twig Created with Sketch.
node--blog.html.twig

This looks cleaner, but we are not there yet. PHP has a limitation that it will break if it finds multiple functions with exactly the same name. In the above setup we define the my_theme_preprocess_node function twice, which will give us a white screen of death when you try this code out.

However the splitting up of the php function is the way to go in a component world, so how can we make this work? Drupal by default gives us individual preprocess functions for each component. But considers each template suggestion not as a new component, but merely as a new variation of an existing component. That means a new template suggestion doesn't bring it's own preprocess hook.

We could fix that by extending the already existing preprocess functions.

Content-type specific hooks #

If you want to extend functions, you have to extend from within the most specific function. So in the use case for node--articles, the most specific function is HOOK_preprocess_node in node.theme.

From this function we will tell Drupal of the possibility of other functions existing that we might want to write in the future. It looks like this for nodes:

components
node
node.theme

function my_theme_preprocess_node(&$variables, $hook) {
  // Add a "rendered_author" variable to be used in node templates
  $account = $variables['node']->getOwner();
  $variables['rendered_author'] = user_view($account, 'compact');

  // Create the possibility to use different preprocess function for different content types.
  $function = __FUNCTION__ . '__' . $variables['node']->getType();
  if (function_exists($function)) {
    $function($variables, $hook);
  }
}

Twig Created with Sketch.
node.html.twig
node--article
node--article.theme

function my_theme_preprocess_node__article(&$variables, $hook) {
  // do something custom for article nodes only
  $variables['article_image_url'] = $variables['node']->field_main_image->entity->getFileUri();
}

Twig Created with Sketch.
node--article.html.twig
node--blog
Twig Created with Sketch.
node--blog.html.twig

So let’s break down the new lines of code in the my_theme_preprocess_node function:

$variables['node']->getType(); this will be article or blog depending on what type of node gets preprocessed by this function.

__FUNCTION__ means the current function name that we are in, which would be my_theme_preprocess_node in this example.

__FUNCTION__ . '__' . $variables['node']->getType(); therefor will be the string my_theme_preprocess_node__article

if (function_exists($function)) will check in any other .theme file, if the function with name my_theme_preprocess_node__article exists.

If the function exists, execute it: $function($variables, $hook); with all the same variables available in the current function.

And boom, we can now write functions in different folders depending on the content-type!

Note: this process is iterative, there is nothing stopping you for making this type of behaviour work in deeper folders or for not-node components.

View-mode specific hooks #

If In the above example we would have access to 3 different hooks, assuming we only have 2 content-types:

  • HOOK_preprocess_node 
  • HOOK_preprocess_node__article 
  • HOOK_preprocess_node__blog

But what if we want to take it yet another step further? Let's assume for the sake of an example that we would like to also have access to a separate preprocess hook for each view-mode of blog nodes? We can follow the same principle of implementing this in the most specific hook we can find. In our example that would be inside my_theme_preprocess_node__blog function.

If we extend the previous example, it would look like this:

components
node
node.theme

function my_theme_preprocess_node(&$variables, $hook) {
  // Add a "rendered_author" variable to be used in node templates
  $account = $variables['node']->getOwner();
  $variables['rendered_author'] = user_view($account, 'compact');

  // Create the possibility to use different preprocess function for different content types.
  $function = __FUNCTION__ . '__' . $variables['node']->getType();
  if (function_exists($function)) {
    $function($variables, $hook);
  }
}

Twig Created with Sketch.
node.html.twig
node--article
node--article.theme

function my_theme_preprocess_node__article(&$variables, $hook) {
  // do something custom for article nodes only
  $variables['article_image_url'] = $variables['node']->field_main_image->entity->getFileUri();
}

Twig Created with Sketch.
node--article.html.twig
node--blog
node--blog
node--blog.theme

function my_theme_preprocess_node__blog(&$variables, $hook) {
  // Create the possibility to use different preprocess function for different view modes.
  $function = __FUNCTION__ . '__' . $variables['view_mode'];
  if (function_exists($function)) {
    $function($variables, $hook);
  }
}

node--blog--full
node--blog--full.theme

function my_theme_preprocess_node__blog__full(&$variables, $hook) {
  // Do something custom here
}

Twig Created with Sketch.
node--blog--full.html.twig
node--blog--teaser
node--blog--teaser.theme

function my_theme_preprocess_node__blog__teaser(&$variables, $hook) {
  // Do something custom here
}

Twig Created with Sketch.
node--blog--teaser.html.twig

The my_theme_preprocess_node__blog function follows the exact same extending principles like the my_theme_preprocess_node function. Let's go over what the PHP means:

__FUNCTION__ is the name of the current function, so that is the string "my_theme_preprocess_node__blog".

$variables['view_mode'] will be the name of the view mode of the node. (in this example it assumes it will contain the string full or teaser).

The if checks inside any .theme file, if one of these functions exists:

  • my_theme_preprocess_node__blog__full
  • my_theme_preprocess_node__blog__teaser

And if it does, excecute it if a blog node is being rendered in the full or teaser view mode.

Naming #

Using a random name for your new functions will get quickly get out of hand, therefor the naming convention of the new functions should follow the naming convention of the folders. In turn the naming conventions are named after the .html.twig file they contain.