How to cache correctly a render array for item list value Drupal example

Content word on wooden cubes background digital marketing concept

The Milky Way for example.. as "whole" it looks like one massive huge "Render Array" which inside contains lot of smaller but still huge Children which are Stars, then each Star contains children which are Planets, then Planets contains children such as Country, we are children of "UK" Region and when we are "rendered" in microscope inside us we also have millions of children: very good ones such as the "White Blood Cells" and recently very unlucky people may have very bad ones such as "Corona Virus or "COVID-19" which are like infiltrate rendered arrays injected by a Hacker to provoke the crash of our web site!

We will discuss about Security into another article, but Drupal is much simpler to update than real World and the Drupal Security Team is always working very hard to provide the latest security software updates! It's true that just like a Virus Hackers gets more clever, but no worries you will succeed when we know what we are doing and I hope you will understand and like all my entire article.

Actually in realities a web page generated by Drupal has got only few thousands of "Render Array" and they are very simple to handle compare to the rest of the things around us.

At the end of the days the actors are always the same: Form, View, Blocks, Content Entities of different Bundles (content types for Node, Vocabulary for Term), then their Children are Tables, Item Lists, Fields, Files, Images. And they are "spitted" (Rendered) on the screen as HTML using the Drupal Theme. Everything happens in automatically, we really do no even see it if we do not look at it in Microscope: I will write another article on how to use xdebug in phpstorm to debug it!

Note: The output of these huge "Render Arrays" is handled completely by "render pipeline" Process that Drupal uses to take information provided by modules and render it into a response, all these works comes completely for free by the Core.

The entire Project for this Web site is available in Github and you can download it for free from our how to install drupal guide page.

Here I will share with you the code of our I did for implementing my custom requirement on the Item List for rendering the Related Service fields displayed inside the Taxonomy View Page that shows all my Services, for example look at the first Service (Deploy File on Amazon S3..) in my Service page and have a look at this Block:

Related Service Block

The Requirement is to hide Links to the Current Service page, for example if the current page is Our Services & Solutions then I do not want to show the Our Services & Solutions link because you are already inside that page so the link is redundant so we want to hide. But when you look at the same Block inside another Service Page such as Custom Modules then you will see that the Custom Module Link is there because it is not Displayed and it is not rendered inside the Twig Template because I added a new Twig with a new custom logic to skip it when it item.hide is TRUE.

Let's have a look how this Rendered Array Looks like (for simplicity I show only what is relevant and it was printed using:

dpm($variables['items']);
Array
[
    [0] => Array
        (
            ['value'] => stdClass Object
                (
                    [__CLASS__] => Drupal\views\Render\ViewsRenderPipelineMarkup
                    [string:protected] => 'Our Services & Solutions'
                )
        )
    [1] => Array
        (
            ['value'] => stdClass Object
                (
                    [__CLASS__] => Drupal\views\Render\ViewsRenderPipelineMarkup
                    [string:protected] => 'Custom Modules'
                )
        )
    [2] => Array
        (
            ['value'] => stdClass Object
                (
                    [__CLASS__] => Drupal\views\Render\ViewsRenderPipelineMarkup
                    [string:protected] => 'Database Integration'
                )

                )

        )
    [3] => Array
        (
            ['value'] => stdClass Object
                (
                    [__CLASS__] => Drupal\views\Render\ViewsRenderPipelineMarkup
                    [string:protected] => 'Information Architecture'
                )
        )
    [4] => Array
        [
            ['value'] => stdClass Object
                (
                    [__CLASS__] => Drupal\views\Render\ViewsRenderPipelineMarkup
                    [string:protected] => 'Integrated Content'
                )
        ]
]

In order to Preprocess these Items we need to set up a new Class for  "item_list" Preprocess:

/**
 * Pre-processes variables for the "item_list" theme hook.
 *
 * @ingroup plugins_preprocess
 *
 * @see item-list.html.twig
 *
 * @BootstrapPreprocess("item_list")
 */
class ItemList extends PreprocessBase implements PreprocessInterface, ContainerFactoryPluginInterface {

This Class also use Dependency Injection to re-use the MariaCustomService, but in here I will focus on the Preprocess.

As I said I want to modify the Rendered Array for "item_list" to exclude all links to the current page, but only on Taxonomy Service Page (Vocabulary is "tags") this is the function:

public function preprocessVariables(Variables $variables) {
    /** @var ContentEntityInterface $term */
    if ($term = $this->route_match->getParameter('taxonomy_term')) {
      if ($term->bundle() == 'tags') {

        static $all_links = [];
        $current_url = $term->toUrl()->toString();
        $variables->addCacheContexts(['url.path']);

        $tot = count($variables['items']);
        for ($delta = 0; ($delta < $tot); $delta++) {
          /** @var ViewsRenderPipelineMarkup $myItem */
          $myItem = $variables['items'][$delta]['value'];

          $alias = $this->customService->findURLfromHTML($myItem->jsonSerialize());
          if (!empty($alias)) {

            // We do not show link to the current page.
            if ($alias == $current_url) {
              $variables['items'][$delta]['hide'] = true;
            }
            // Show the link only on the first occurrence.
            elseif(($my_term = $this->customService->getEntityByAlias($alias)) && $my_term instanceof ContentEntityInterface) {
              $my_tid = $my_term->id();
              if (!empty($all_links[$my_tid])) {
                $variables['items'][$delta]['hide'] = true;
              }
              else {
                $all_links[$my_tid] = 1;
              }
            }

          }

        }
      }
    }
  }

We know the Values of each Link is an HTML link which looks like:

<a href="/services" hreflang="en">Our Services &amp; Solutions</a>

We extract the URL Alias (/service) using findURLfromHTML() function from the Service which returns us the alias which is passed to getEntityByAlias() which returns us the Term, then we compare if the Term ID versus the Current Term ID, if they are identical then we hide this item:

$variables['items'][$delta]['hide'] = true;

Now we must also create a custom Twig that overwrite the Default  item-list.html.twig in order to hide the items which we set to hide, because the default Theme for Item List always display them all. 

This is the easiest part, it require simple Overwriting the item-list.html.twig file, first thing we need to do it is to tell the Theme Preprocessor that we want to Suggest a new Twig file for this Theme HOOK:

namespace Drupal\maria_consulting\Plugin\Alter;

// .... all code is in my Github Repository

/**
 * Implements hook_theme_suggestions_alter().
 *
 * @ingroup plugins_alter
 *
 * @BootstrapAlter("theme_suggestions")
 */
class ThemeSuggestions extends \Drupal\bootstrap\Plugin\Alter\ThemeSuggestions
{

  /**
   * {@inheritdoc}
   */
  public function alter(&$suggestions, &$variables = [], &$hook = NULL)
  {
    if ($hook == 'item_list' && ($taxonomy = \Drupal::routeMatch()->getParameter('taxonomy_term'))) {
      $vocabularyId = $taxonomy->getVocabularyId();
      $suggestions[] = $variables['theme_hook_original'] . '__taxonomy__term__' . $vocabularyId;
    }

Basically it is telling Drupal Theme Processor to suggest other Twig Templates more specific to use for Item_list when they are Displayed inside a Taxonomy Service Term page. In my case this is the file name which Overwrite the Default:

item-list--taxonomy--term--tags.html.twig

And this is the content of this file:

{% if context.list_style %}
  {%- set attributes = attributes.addClass('item-list__' ~ context.list_style) %}
{% endif %}
{% if items or empty %}
  {%- if title is not empty -%}
    <h3>{{ title }}</h3>
  {%- endif -%}

  {%- if items -%}
    <{{ list_type }}{{ attributes }}>
      {%- for item in items -%}
        {%- if not item.hide -%}
        <li{{ item.attributes }}>{{ item.value }}</li>
        {%- endif -%}
      {%- endfor -%}
    </{{ list_type }}>
  {%- else -%}
    {{- empty -}}
  {%- endif -%}
{%- endif %}

It is almost identical to the Original, I only added the extra IF condition to Display the Link when item.hide is FALSE!

It is not finished yet, I must explain you now how Drupal Know how to cache correctly all these modified "Rendered Arrays"

This is the most important part of this Article: Drupal Cache system by Default does not know that we are altering the "Rendered Arrays" for these Item List, so what it does it cache the rendered results when the Item List Links are rendered for the first time and then in the successive Requests re-use the Cached HTML rendered result.

But this is not what we really want because our requirement is to have different values on different Service Page. Basically I want to modify again this "Rendered Arrays" for Item List on specific Page URLs, then I must tell Drupal the Context when to cache my Values. That's why I added this line inside:

  public function preprocessVariables(Variables $variables) {
// All code is inside the Paragraph above
                        $variables->addCacheContexts(['url.path']);

addCacheContexts tells Drupal to cache this Rendered array on our specific Context which is different for each Page Path. For example our specific paths are: /services, /services/custom-modules and so on. Drupal will know to cache this values on path specific because we added this context.

There is a lot to talk about Caching and the way Drupal 8 handle contexts. For now what you need to know is that you can modify Default Context on "Rendered Arrays" to make your Content more specific on specific custom context such as: when URL is front page, when is parent URL, when URL contains certain Query Parameters, when the user is super user or has got specific roles.

url
  .path
    .is_front // Available in 8.3.x or higher.
    .parent
  .query_args
    :key
    .pagers
      :pager_id
  .site
user
  .is_super_user
  .node_grants
    :operation
  .permissions
  .roles
    :role

There is lot to learn about Custom Context. If you want to learn more about how to create your own custom Cache Contexts, I really recommend you to read Cache contexts from Drupal.org.

1. Integration

dark servers center room with computers and storage systems

Drupal can be integrated with complex system that uses other databases such as PostgreSQL, MongoDB, Oracles or even other Custom MySQL Servers.

2. Architecture

data analysis for business and finance concept

Drupal 8 Architecture is very robust, fast and Object Oriented. The Drupal Core rely on many Symfony base Components that give a great entity abstraction.

3. Strategy

creative web designer developing template layout

Do you need a very simple marketing brochure website or a very big robust intranet system? Plan which modules you need to install and keep it simple!

4. Custom

businessman using tablet

If you cannot find a module that does exactly what you need then need to write a custom module. First try to find the same functionality..