4

I have a custom module with a block plugin:

SliderBlock.php

class SliderBlock extends BlockBase {
  public function build() {
    $build = [];
    $build['#theme'] = 'slider_block';

    return $build;
  }
}

I want to render a list of nodes of type 'slides' in the block (it will eventually be a custom slider feature)

mymodule.module

function mymodule_theme() {
  return [
    'slides_block' => [
      'variables' => [
        'slides' => get_slides(),
      ],
    ],
  ];
}

function get_slides() {
  // Load all slides nodes.
  $nids = \Drupal::entityQuery('node')
    ->condition('type','slides')
    ->condition('status','1') // Only published slides nodes.
    ->execute();
  $nodes =  \Drupal\node\Entity\Node::loadMultiple($nids);

  return $nodes;
}

and I loop over each in the template to get the markup I need:

slider-block.html.twig

{# markup for slider here #}
{% for slide in slides %}
  <p>{{ slide.label }}</p>
{% endfor %}

The above template shows me the title field for each Slides node, which is what I expect. The problem is that if I un-publish a Slides node, the markup is not updated unless I clear caches.

Alright, this sounds like a cache issue, so I dove into a google search mission and discovered a series of ways to disable caching for the block. None of which seem to work, for anonymous users or otherwise:

// Unsuccessful:
public function getCacheMaxAge() {
  return 0;
}

// Unsuccessful:
$build['#cache'] = [
  'max-age' => 0,
];

I looked at cache contexts, as far as I could tell, there was no 'node' based cache context. If I understood them correctly, cache tags wouldn't help, since I don't know the ids of any given node of type 'Slides'.

I 'only want to' disable caching for this block. This block will be rendered only on a specific page, so I could disable caching on that page using the modules route yml file (am I right?), but it feels wrong to disable that whole page for the sake of one block.

What is the right way to solve this, and what's the methodology in reaching that conclusion?

Note: I could use views to collect the nodes, but to customise the view markup is more challenging.

4
  • 2
    Adding a tag of node_list would do it, but it would also invalidate the cache when any node was created or updated, not just ones that would be involved in the slider. If you need to get more granular, use your own custom cache tag, and implement hook_node_insert etc to invalidate it under the right conditions (probably content type == slider)
    – Clive
    Commented Jan 20, 2020 at 11:18
  • Nice question! Actually quite a useful feature to be requested from core: a node type cache tag.
    – leymannx
    Commented Jan 20, 2020 at 11:29
  • 1
    @leymannx I think there's a core issue open for it somewhere. There's also Handy cache tags which provides it. But I hate to add another module for something that can be done in a few lines of code ;)
    – Clive
    Commented Jan 20, 2020 at 11:41
  • @Clive – Ah yes! And 4k4 now nailed it down.
    – leymannx
    Commented Jan 20, 2020 at 12:47

2 Answers 2

4

As @Clive commented you need a node list cache tag.

Put the slides and the cache tag when you invoke the template in the build array (not in a hook_theme(), this hook is only called when you install the module):

class SliderBlock extends BlockBase {
  public function build() {
    $build = [
     '#theme' => 'slider_block',
     '#slides' => getSlides(),
     '#cache' => ['tags' => ['node_list']],
    ];
    return $build;
  }
}

Put a TODO annotation in your code to change the list tag to a content type specific node_list:slides, once you have updated to Drupal 8.9.x. See this change record: Added an ENTITY_TYPE_list:BUNDLE cache tag.

2

As mentioned on the comments by @Clive and @leymannx, you need to use cache tags to do the job. The problem is with your current approach will be tricky since your nodes are being loaded by your theme hook. Would be much cleaner and Drupal-way of doing things if you refactor your code and make your nodes loaded by the block, so your theme hook can focus on his work: prepare data and render it. Your code could be as follow (dependency injects ommited on purpose, please read more about this here):

SliderBlock.php

class SliderBlock extends BlockBase {
  private $slides;

  public function build() {
    $this->slides = $this->getSlides();

    if (empty($this->slides)) {
      // Feel free to change to your use case when
      // no slides available.
      // Keep in mind that any non-empty render array
      // could/would be cached.
      return [];
    }

    $build = [];
    $build['#theme'] = 'slider_block';
    $build['#slides'] = $this->slides;

    return $build;
  }

  function getSlides() {
    // Load all slides nodes.
    $nids = \Drupal::entityQuery('node')
      ->condition('type','slides')
      ->condition('status','1') // Only published slides nodes.
      ->execute();
    return \Drupal\node\Entity\Node::loadMultiple($nids);
  }

  public function getCacheTags() {
    $cache_tags = parent::getCacheTags();
    $cache_tags[] = 'block:' . $this->getDerivativeId();

    if (empty($this->slides)) {
       return $cache_tags;
    }

    $node_cache_tags = [];

    foreach ($this->slides as $node) {
      $node_cache_tags[] = $node->getCacheTags();
    }

    $node_cache_tags = array_merge([], ...$node_cache_tags);
    $cache_tags = \Drupal\Core\Cache\Cache::mergeTags($cache_tags, $node_cache_tags);

    return $cache_tags;
  }
}

If you are concerned about ... a.k.a spread operator and why I merged the tags like that, check this post.

mymodule.module

function mymodule_theme() {
  return [
    'slides_block' => [
      'variables' => [
        'slides' => NULL,
      ],
    ],
  ];
}

Also check this post about block cache tags.

3
  • 1
    As a side note, getCacheTags() is only in case if you don't return a rendered result, as explained above the code in the linked post, otherwise the build array should contain the cache tags.
    – 4uk4
    Commented Jan 20, 2020 at 16:55
  • Duly noted, but the template provided by the OP does no render the entities only prints out their label in plain text so we could use the cache tags. But I think your answer is more efficient and I wasn't aware of this change, is indeed a handy tool.
    – d70rr3s
    Commented Jan 20, 2020 at 22:25
  • 1
    My side note is not about individual tags vs list tags, it's about where to add cache metadata in general, and this should be the render array when you have a render array. See why this can be an issue drupal.stackexchange.com/questions/288881/…
    – 4uk4
    Commented Jan 20, 2020 at 23:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.