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()

BUT!!!

If you have a local setup with working Xdebug then I can highly recommend https://www.drupal.org/project/twig_xdebug

Basically put {{ breakpoint() }} anywhere in your Twig template you would like to debug.

Then check the $context variable in your debugger. In there you have every variable present in that Twig template.

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.

WordPress – The common one

WordPress is definitely the most well-known CMS in the general public and therefore the first choice for most people to start with.

And in my opinion thats totally fine because WordPress has in my opinion:

  • the best backend to manage content in a user-friendly way,
  • the easiest setup to start with,
  • has a huge repository of plugins (which can be a downside as well, see below),
  • huge community + tons of documentation,
  • is lightweight and can run on pretty much anything,
  • easy to update with a 1 click button in the backend

But there are some downsides to WordPress which can lead to many headaches and frustration:

  • No multi-lingual support per default WordPress, only with (mostly) paid plugins
  • There are so many free plugins that its hard to decide which is the one you really need and most of the time (after you decide on a plugin) it comes with so much functionality you will not need which creates overhead
  • Interoperability between plugins regularly creates problems (for me)

And here some neither positive nor negative aspects:

  • No dependency management makes it easy to update but creates dependency problems if plugin versions don’t work together
  • No PHP template engine like TWIG produces no consistent way to connect PHP logic (aka Controller) with the template (aka View) and makes it harder to reuse template parts but removes the overhead and need of configuration for TWIG

Some WordPress facts

As per W3Techs WordPress is currently used for 63.6% of all CMS based websites and drives 37.6% of all websites available on the internet. (June 28th, 2020)
Source: https://w3techs.com/technologies/overview/content_management

WordPress starts of with just 12 database tables, unlike Drupals 87 tables. This makes the database structure of WordPress very approachable and easy to understand if you need to dig deeper.

WooCommerce, a E-Commerce Plugin for WordPress, powers nearly 12% of all Webshops available on the internet. Source: https://trends.builtwith.com/shop/traffic/Entire-Internet

So what I want to say is

WordPress is a CMS which is fairly easy to understand, can be setup and used pretty quickly and has tons of plugins and support available. Because of these reasons it became the leading CMS worldwide.

BUT

WordPress also has its limitations and drawbacks. For me the main reasons to not use WordPress are:

  • Multilingual Website
  • Huge amount of data and/or rapidly increasing amount of data (> 200k entities)
  • Custom Search Implementation like SOLR

About the “Huge amount of data (> 200k entities)”:
This is just a number I wrote down by gut feeling. The fact that every field of every entity in WordPress is saved in the same wp_postmeta table just increases the size of this table so that it has to get slow after “some amount” of entities.
In Drupal each field gets its own table and therefore is much more performant handling large amounts of data.

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

What is a CMS?

A content management system (short CMS) allows you to structure your content into bits and pieces which can be displayed, searched, filtered or used in any way you need it.

How to structure content

The basic concept behind a CMS is always the same:

Content Types

Content Types (or sometimes called Post Types or Collections, in short CT) are ways to differentiate single entries of your content into separate “areas” so you can handle these types of content better.

Examples for Content Types are:

  • Blog-Posts
  • Team-Members
  • Projects

Each Content Type usually has its own set of fields, in which data is saved per entity.

Lets take “Team-Members” as an example. Therefore we could have the following fields inside the Team-Members Content Type:

  • First name
  • Last name
  • Birthdate
  • Image
  • Job Title

So therefore we can create 2 “entities” inside the Team-Members Content-Type:

Kevin Pfeifer

  • First name => Kevin
  • Last name => Pfeifer
  • Birthdate => 08-05-1993
  • Image => <Path-to-image>
  • Job Title => Full Stack Developer

Jamil Bates

  • First name => Jamil
  • Last name => Bates
  • Birthdate => 23-03-1991
  • Image => <Path-to-image>
  • Job Title => Frontend Developer

Taxonomies

Taxonomies (short tax) are ways to categorise your entities inside your content type into more separate areas.

If we stick with the “Team-Member” Content Type an easy example for a Taxonomy is “Project-Team“.

Inside our Taxonomy we now create terms called “Website Devguide” and “Website DASVok” which we now can connect to our Team-Members.

Therefore we can assign Team-Member “Kevin Peifer” to the Project “Website DASVok” and the Team-Member “Jamil Bates” to the Project “Website Devguide”.

Common CMS systems and their default Content Types and Taxonomies

Lets take 2 of the most common CMS and explain their default Content Types and Taxonomies:

WordPress

Content Types in WordPress are called “Post Types” because WordPress originated as a blogging system and therefore pretty much every “entity” is a “post” inside WordPress.

As per WordPress 5.4.2 (June 2020) you get the following backend structure per default configured:

Posts” (CT) with 2 Taxonomies: “Categories” and “Tags
Pages” (CT) with no Taxonomies

Technically Media is a CT as well, because each uploaded media gets its own “attachment” entity but thats too far in the WordPress structure.

Of course, dependent on your used Plugins, you will get more CT and/or Taxonomies or even install Plugins where you can create your own CT and Taxonomies in the backend.

Drupal 8/9

In Drupal Taxonomies are called “Vocabularies“. Currently I don’t know why but I guess it has some historical reason, same like Content Types in WordPress are called Post Types.

As per Drupal 9 (June 2020) you get the following backend structure per default configured:

Article” (CT) with 1 Taxonomy “Tags
Basic page” (CT) with no Taxonomies

You can also enable the “Media” module which allows you to basically do the same with your uploaded files (images, documents etc.)