Preprocessors

Sometimes you need to adjust the DOM in a way you can’t do just via overwriting Twig templates. Thats where preprocessors come in handy.

Lets assume we already have a custom theme called “mytheme” and created a mytheme.theme file in web/themes/custom/mytheme

Even though this file has a .theme extension you can write normal PHP code in there.

Preprocessor functions are usually in the following structure: THEMENAME_preprocess_HOOK()

Our THEMENAME here is mytheme and you can see the available hooks via enabling the Twig Debugging Mode in the frontend HTML comments.

So therefore you can create custom preprocessor function like

  • mytheme_preprocess_node()
    • All nodes
  • mytheme_preprocess_node__article()
    • All article nodes
  • mytheme_preprocess_menu()
    • All menus

What needs to be inside these functions?

Lets say we want to add a class to a specific menu.

/**
* Implements hook_preprocess_HOOK() for menu.html.twig.
*/
function mytheme_preprocess_menu(&$variables) {
  if ($variables['menu_name'] == 'main') {
    if (!isset($variables['attributes']['class'])) {
      $variables['attributes']['class'] = [];
    }
    $variables['attributes']['class'] = array_merge(
      $variables['attributes']['class'],
      ['my-main-menu']
    ); 
  }
}

First of all: $variables is passed through by reference, therefore all the changes we do inside $variables gets passed onto the template side.

Therefore, as you can see above, we check on which menu we currently are and if we are on the ‘main’ menu we add the class ‘my-main-menu’ to the attributes class array.

This results in the following DOM:

The hook “menu__main” here is only a more specific hook as we used above.

We could have also got the same result with the following function:

function mytheme_preprocess_menu__main(&$variables) {
  if (!isset($variables['attributes']['class'])) {
    $variables['attributes']['class'] = [];
  }
  $variables['attributes']['class'] = array_merge(
    $variables['attributes']['class'],
    ['my-main-menu']
  ); 
}

As you can see the function name has an appended __main at the end but now we don’t need the outer most if query to check which menu we currently try to preprocess.

One important notice here: The preprocessor mytheme_preprocess_menu only adjusts values for the menu.html.twig, not any other Twig files like page.html.twig or block.html.twig

If you want to adjust values inside the page.html.twig or any other Twig file you should check its hook first and create a separate preprocessor function for this hook.

So therefore you can only adjust values inside a preprocessor for its assigned element connected via the hook. If you want to adjust an element because of another element thats more complicated.

Developer Tools & Debugging

Requirement: Working Drupal site which has already been installed via a local instance or on a server.

Add a settings.local.php for your environment specific settings

It’s always a good idea to have a config file specific to your currently active environment (e.g. stagging, dev and live server). Drupal already preps that in your web/sites/default/settings.php with the following lines:

# if (file_exists($app_root . '/' . $site_path . '/settings.local.php')) {
#   include $app_root . '/' . $site_path . '/settings.local.php';
# }

You only have to uncomment these 3 lines of code and create a web/sites/default/settings.local.php

In there you can add your environment specific settings like the database array already present in your settings.php or the ones I will explain further in this post:

Exclude settings.local.php from GIT

Please check your .gitignore so the settings.local.php doesn’t get commited into GIT.

Disable render caching and CSS/JS aggregation

Drupal has the following “caching” system:

Source: https://drupalize.me/tutorial/render-api-overview?p=2766

To disable this render cache and the automatic CSS/JS aggregation add this to you settings.local.php

$settings['cache']['bins']['render'] = 'cache.backend.null';
$config['system.performance']['css']['preprocess'] = false;
$config['system.performance']['js']['preprocess'] = false;

Personal experience

I have experienced some weird caching issues if your not logged in. Even though I was sure i had correctly regenerated my CSS & JS and force refreshed my browser cache I still got old styling.

The solution: Log in

Twig Debugging

Twig has its own “config file” in which you can tweak how Twig is handling your template files. This file is located in web/sites/default/services.yml

services.yml not present?

If the services.yml is not present just copy the default.services.yml and rename it into services.yml

After that you should perform a drush cr!

In this services.yml you should adjust the following values to have a better Twig experience:

parameters:
  # --- Some comments ---
  twig.config:
    # --- Some comments ---
    debug: true
    # --- Some comments ---
    auto_reload: true
    # --- Some comments ---
    cache: false

With that you should see HTML comments in the frontend showing you several information about which template Drupal used to generate this DOM, how you can overwrite these templates and which hook you can use to adjust parts if the DOM in preprocessors. More on that in another chapter!

Twig Developer Tools

The easiest way to know your current Twig environment is via outputting all your available variables:

{{ dump() }}

I have tried using kint but only ran into PHP memory size issues regularly due to the fact, that Drupal likes to have circular references and therefore create an infinite loop.

So my best experiences were just using the default Twig debugging tool dump()

Drupal – The advanced one

Drupal is another very popular CMS which has a pretty large community and many “modules” which extend the CMS the way you like.

In my opinion Drupal does the following aspects of being a CMS very good:

  • Creation of Content Types, Taxonomies and fields via backend
  • Built in caching system
  • Built in multilingual support
  • Built in backend error log viewer (Watchdog)
  • Very mighty permission system
  • You recieve e-mail notifications if the core or a module has security issues (if you subscribe to the security newsletter)
  • Built on Symfony and therefore you have TWIG as a template engine
  • The “Webform” module is the most advanced form module I have ever seen

Neutral aspects:

  • Requires PHP composer to install/update core and modules therefore you need SSH but you have dependency management. If you want to learn more about PHP composer see HERE
  • Backend content structure not as user-friendly as WordPress

Negative aspects:

  • If you want to do more than the provided functionality in the backend you will have a pretty steep learning curve on how to e.g. adjust the DOM the way you want it, learn what preprocessors are and how to adjust one element due to the configuration of another.
  • If a update goes wrong (either composer updates or database updates) the whole site will be down till you fixed it. It is very common that you have to add a patch to a module or the core so a specific problem doesn’t occur on your website till this patch has been merged into its own release version by the core team or the module developer.

Some Drupal facts

As per W3Techs Drupal is currently used for 2.6% of all CMS based websites and drives 1.5% of all websites available on the internet. (July 26th, 2020)
Source: https://w3techs.com/technologies/overview/content_management

Drupal starts of with 87 database tables, unlike WordPress with just 12 tables. There are many reasons why that is the fact. One of them is the fact, that each field gets its own table and the whole caching system requires its own tables.
See database diagramm here: https://www.drupal.org/files/Drupal8_UPsitesWeb_Schema_10-19-2013.png

So what I want to say is

Drupal is a CMS which is easy to use if you need exactly what it offers in the backend. If you need more customization of specific elements which are not already provided or are extendable by modules you will have a pretty hard time.

I love the fact, that multilingual support is built in and there is also no problem adding multilingual support after you have created the website because the whole system has that covered.

If you want to learn more about Drupal the site drupalize.me has very good descriptions and trainings.

Custom Views Display Style Plugin

What do we want to achieve?

Say you have built a custom view and now want to output the given fields as well as the wrapper for the whole view in a custom DOM.

Thats where custom view display style plugins come in hand.

Creating the custom Drupal module

First of all lets start with a basic custom Drupal module:

Create the folder docroot/modules/custom/sunlime_custom_view_display

In there create a file “sunlime_custom_view_display.info.yml“:

name: Sunlime Custom Display View
type: module
description: 'Provides a views display style for listings.'
package: Views
core: '8.x'
dependencies:
  - views

With that alone you should see the module appear in the backend under “Extend”

e4054546762eb0ae42d34182bb636b77

Adding the views display style plugin code

Now lets create the rest of the files with the following file system structure:

99b45280bff3f926b0f5c71b03bbf661

The file “SunlimeCustomViewDisplay.php” contains:

<?php
/**
 * @file
 * Definition of Drupal\sunlime_custom_view_display\Plugin\views\style\SunlimeCustomViewDisplay.
 */
namespace Drupal\sunlime_custom_view_display\Plugin\views\style;
use Drupal\core\form\FormStateInterface;
use Drupal\views\Plugin\views\style\StylePluginBase;

/**
 * Style plugin to render listing.
 *
 * @ingroup views_style_plugins
 *
 * @ViewsStyle(
 *   id = "sunlime_custom_view_display",
 *   title = @Translation("Sunlime New"),
 *   help = @Translation("Render a listing of view data."),
 *   theme = "views_view_sunlime_custom_view_display",
 *   display_types = { "normal" }
 * )
 *
 */
class SunlimeCustomViewDisplay extends StylePluginBase {

  /**
   * Set default options
   */
  protected function defineOptions() {
    $options = parent::defineOptions();
    $options['height'] = array('default' => '354');
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    parent::buildOptionsForm($form, $form_state);
    $form['height'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Height'),
      '#description' => $this->t('Default height of header.'),
      '#size' => '6',
      '#default_value' => $this->options['height'],
      '#required' => TRUE,
      '#field_suffix' => 'px',
    );
  }
}

With this PHP class you will now see the following option popup in the view formatter selection:

34174f57596a81284de6a7a51ceb6318

The buildOptionsForm(&$form, FormStateInterface $form_state) function defines which fields should be present in the “Settings” modal of the view formatter.

The defineOptions() function defines the default values for the given fields in the buildOptionsForm(&$form, FormStateInterface $form_state) function.

e78cecddb207ccae0fa17c72a5f9ee6f
717c5533368d03da4280da639d037f88

One very important part here is the theme definition in the @ViewsStyle comments of the class. This text defines how the template file has to be named.

Basically as you can see its 1:1 the same name but with the _ replaced with –

I don’t know why that has to be like that but I guess it has some Drupal historical reason.

3d4b20efe6e3d61755187912c6e06949

Outputting the view

So now lets open views-view-sunlime-custom-view-display.html.twig and add the following code:

<h1>Start of view-view-sunlime-custom-view-display.html.twig</h1>

<h2>Settings of View</h2>
{# Options from View Settings #}
{{ dump(view.style_plugin.options) }}

<h2>Rows</h2>
{# View Result #}
{{ dump(rows) }}

<h2>Logged in user</h2>
{# Logged in user #}
{{ dump(user) }}

With that code (and a place where your view is outputted in Drupal like a block or a page) you should see something like this:

dcb6f65f263934249217db89ef30cc34

Preproccessing the view

Just like you can preprocess many other data in Drupal you can of course preprocess your views data as well.

For that first of all open the sunlime_custom_view_display.module file and add the following content:

<?php
/**
 * @file
 * Sunlime Custom Views module help and theme functions.
 */
// Theme functions in a separate .inc file.
\Drupal::moduleHandler()->loadInclude('sunlime_custom_view_display', 'inc', 'sunlime_custom_view_display.theme');

/**
 * Implements hook_theme().
 */
function sunlime_custom_view_display_theme($existing, $type, $theme, $path) {
  return array(
    'sunlime_view_display_module' => array(
      'file' => 'sunlime_custom_view_display.theme.inc',
    ),
  );
}

Basically the first line dictates that the file sunlime_custom_view_display.theme.inc should also be included. Lets add the following code inside the sunlime_custom_view_display.theme.inc

<?php
/**
 * Prepares variables for view template.
 *
 * Default template: views-view-sunlime-custom-view-display.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - view: A ViewExecutable object.
 *   - rows: The raw row data.
 */
function template_preprocess_views_view_sunlime_custom_view_display(&$variables) {
  // View options set by user.
  $options = $variables['view']->style_plugin->options;
  $rows = $variables['rows'];
  echo '<h2> $options in sunlime_custom_view_display.theme.inc</h2>';
  var_dump($options);
  echo '<h2> $rows in sunlime_custom_view_display.theme.inc</h2>';
  var_dump($rows);
  // Apply required logic and add variables here. We can define variable in
  // our hook_theme and use it here.
}

With that in place you should now see the following additional output:

33f443c08a0f25cdc627b02977c7bd02

Depending on your Drupal cache settings as well as your TWIG debug settings you will probably have to do a drush cr more often or not.

I want to use the fields defined in the view backend. How do I do that?

The above example just shows you how to create a basic views formatter and requires you to iterate over the rows object in TWIG to access the nodes data.

But this “version” will not work with the defined fields in the Drupal backend.

The following template file shows a basic view formatter twig output for a grid output:

{% set wrapper_classes = "l-row" %}

<div class="{{ wrapper_classes }}">

  {% if rows|length %}
    {% for row in rows %}
      <div class="l-col {{view.style_plugin.options.grid_class}}">
        {{- row.content -}}
      </div>
    {% endfor %}

  {% elseif empty %}

    <div class="view-empty">
      {{ empty }}
    </div>

  {% endif %}

</div>

As you can see {{- row.content -}} should theoretically output the fields which are defined in the backend.

Instead you get the following error:

Exception: Object of type Drupal\views\ResultRow cannot be printed.

After some researching I found the following Bootstrap issue: https://www.drupal.org/project/views_bootstrap/issues/2871454

Basically you have to do the following modifications to your View Formatter:

class GridDisplay extends StylePluginBase {
   protected $usesOptions = true;
   protected $usesRowPlugin = true;
...

At the start of your custom PHP class you have to add the following 2 variables so you “somehow” tell Drupal to “use the row plugin” which changes “something”.

But not only that, you will also have to add the following preprocessor to your <module>.theme.inc

/**
 * Prepares variables for the view grid display template.
 *
 * Template: views-view-grid-display.html.twig
 *
 * @param array $variables
 *   An associative array containing:
 *   - view: The view object.
 *   - rows: An array of row items. Each row is an array of content.
 */
function template_preprocess_views_view_grid_display(&$variables) {
  template_preprocess_views_view_unformatted($variables);
}

The name of the function has to be adapted to your view display name, but I hope this is clear since it is basically a preprocessor.

I have no idea why this is needed and we just accidentally happened to stumble upon that but as you can see, I have no idea whats happening here. With that, it works, and thats all you will need.

If someone reads this who has a deeper understanding of this and can explain to me why this is needed I would be very happy to adjust this post and cite you.

Multilingual adjustments

Using custom view style plugins in a multilingual page need a little bit of customization to work.

If you read out the fields value hard coded via {{ row._entity.fields.title.value }} you will need to adjust your code to something like that:

{% set current_language = row.node_field_data_langcode %}
{% if row._entity.hastranslation(current_language) %}
  {% set entity = row._entity.translation(current_language) %}
{% else %}
  {# No translation available for this node #}
{% endif %}
{{ entity.fields.title.value }}

A Drupal Core Issue has been created here because in my opinion I don’t see the need for that here: https://www.drupal.org/project/drupal/issues/3111717

Source: https://www.drupal.org/docs/creating-custom-modules/building-a-views-display-style-plugin-for-drupal