31

I have this very basic block which just shows the current node's ID.

<?php

/**
 * @file
 * Contains \Drupal\mymodule\Plugin\Block\ExampleEmptyBlock.
 */

namespace Drupal\mymodule\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;

/**
 * @Block(
 *   id = "example_empty",
 *   admin_label = @Translation("Example: empty block")
 * )
 */
class ExampleEmptyBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
    $node = \Drupal::routeMatch()->getParameter('node');
    $build = array();

    if ($node) {
      $config = \Drupal::config('system.site');

      $build = array(
        '#type' => 'markup',
        '#markup' => '<p>' . $node->id() . '<p>',
        '#cache' => array(
          'tags' => $this->getCacheTags(),
          'contexts' => $this->getCacheContexts(),
        ),
      );
    }

    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    $node = \Drupal::routeMatch()->getParameter('node');
    return Cache::mergeTags(parent::getCacheTags(), ["node:{$node->id()}"]);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    return Cache::mergeContexts(parent::getCacheContexts(), ['user.node_grants:view']);
  }

}

But once cached, the block stays the same, regardless which node I visit. How do I correctly cache the result per node ID?

1
  • 2
    Look at getCacheTags() from BlockBase, you just need add a tag that represent your node (node:{nid}). Sorry I'm in a hurry now, I can explain better later,
    – Vagner
    Commented Apr 28, 2016 at 11:52

7 Answers 7

51

This is full working code with comments.

namespace Drupal\module_name\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;

/**
 * Provides a Node cached block that display node's ID.
 *
 * @Block(
 *   id = "node_cached_block",
 *   admin_label = @Translation("Node Cached")
 * )
 */
class NodeCachedBlock extends BlockBase {
  public function build() {
    $build = array();
    //if node is found from routeMatch create a markup with node ID's.
    if ($node = \Drupal::routeMatch()->getParameter('node')) {
      $build['node_id'] = array(
        '#markup' => '<p>' . $node->id() . '<p>',
      );
    }
    return $build;
  }

  public function getCacheTags() {
    //With this when your node change your block will rebuild
    if ($node = \Drupal::routeMatch()->getParameter('node')) {
      //if there is node add its cachetag
      return Cache::mergeTags(parent::getCacheTags(), array('node:' . $node->id()));
    } else {
      //Return default tags instead.
      return parent::getCacheTags();
    }
  }

  public function getCacheContexts() {
    //if you depends on \Drupal::routeMatch()
    //you must set context of this block with 'route' context tag.
    //Every new route this block will rebuild
    return Cache::mergeContexts(parent::getCacheContexts(), array('route'));
  }
}

I tested it; it works.

Just put the code in a file named NodeCachedBlock.php in your module folder, change its namespace {module_name}, clear the cache and use it.

11
  • 1
    so the trick is to remove the #cache settings in the build function and just add the public functions?
    – Alex
    Commented Apr 28, 2016 at 13:41
  • 4
    That doesn't matter where you set the cache tags and contexts.
    – 4uk4
    Commented Apr 28, 2016 at 13:43
  • 1
    Well, I think that's make more sense, because we are building a block, so block needs to be cached. If you change your block in the future (i.e put some extra render elements), your block will work.
    – Vagner
    Commented Apr 28, 2016 at 13:46
  • 1
    @4k4 url.path seemed to have worked too. whats the difference?
    – Alex
    Commented Apr 28, 2016 at 13:47
  • 4
    @Vagner: Putting cache tags/contexts in the render array is also not a bad idea, because you have it where your data is, which depend on it. And it will always bubble up, so you don't have to worry about the elements that are above. Btw. your code is great, does explain the caching issues very well.
    – 4uk4
    Commented Apr 28, 2016 at 14:00
18

The by far easiest way to do this is to rely on the plugin/block context system.

See my answer for How do I make a block that pulls the current node content?

You just have to put a node context definition in your block annotation like this:

*   context_definitions = {
*     "node" = @ContextDefinition("entity:node", label = @Translation("Node"))
*   }

And then use it like this: $this->getContextValue('node')

The nice thing about this is that Drupal will then take care of caching for you. Automatically. Because it knows that the default (and as far as core goes only) node context is the current node. And that knows where it is coming from, so the cache context and cache tags are added automatically.

Through \Drupal\Core\Plugin\ContextAwarePluginBase::getCacheContexts() and the corresponding getCacheTags() methods, BlockBase/your block class extends from that and inherits those methods.

6
  • 1
    You replace \Drupal::routeMatch()->getParameter('node') with $this->getContextValue('node') and you solve the whole caching issue with one line of code? Great!
    – 4uk4
    Commented Apr 29, 2016 at 7:00
  • 2
    thanks so far! could you provide a full code example?
    – Alex
    Commented Apr 29, 2016 at 7:58
  • 1
    @Alex: I edited your question. Please check and change the code if you find any error.
    – 4uk4
    Commented Apr 29, 2016 at 21:19
  • 1
    @4k4 I didnt try it out because the other solution works, too
    – Alex
    Commented May 2, 2016 at 7:44
  • 1
    @Alex – Full code example: drupal.stackexchange.com/a/205155/15055
    – leymannx
    Commented Oct 17, 2017 at 12:44
9

If you derive the class of your block plugin from Drupal\Core\Block\BlockBase, you will have two methods to set cache tags and contexts.

  • getCacheTags()
  • getCacheContexts()

For example, the Book module block implements those methods as follows.

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    return Cache::mergeContexts(parent::getCacheContexts(), ['route.book_navigation']);
  }

The Forum module block uses the following code.

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    return Cache::mergeContexts(parent::getCacheContexts(), ['user.node_grants:view']);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    return Cache::mergeTags(parent::getCacheTags(), ['node_list']);
  }

In your case, I would use the following code.

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    $node = \Drupal::routeMatch()->getParameter('node');
    return Cache::mergeTags(parent::getCacheTags(), ["node:{$node->id()}"]);
  }

You could also use the following method, to make the block uncacheable at all (even if I would avoid it). It could be useful in other cases, maybe.

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    return 0;
  }

Remember to add use Drupal\Core\Cache\Cache; on the top of the file, if you are going to use the Cache class.

9
  • 1
    thanks, but on /node/2 the block still outputs 1 when I visited node/1 in the first place, after clearing my cache
    – Alex
    Commented Apr 28, 2016 at 13:08
  • 3
    If you are editing a module that is enabled, you need to first uninstall it, before editing it. Clearing the cache is not enough.
    – avpaderno
    Commented Apr 28, 2016 at 13:10
  • 1
    okay, but adding the maxAge 0 works, strangely!
    – Alex
    Commented Apr 28, 2016 at 13:11
  • 1
    Also, does your block class uses the BlockBase class as parent class?
    – avpaderno
    Commented Apr 28, 2016 at 13:11
  • 1
    yes it does use it
    – Alex
    Commented Apr 28, 2016 at 13:12
8

When you build a render array, always attach the correct metadata:

use Drupal\Core\Cache\Cache;

$build['node_id'] = [
  '#markup' => '<p>' . $node->id() . '<p>',
  '#cache' => [
    'tags' => $node->getCacheTags(),
    // add a context if the node is dependent on the route match
    'contexts' => ['route'],
  ],
];

This is not block specific and the block plugins cache dependency methods getCacheTags(), getCacheContext() and getCacheMaxAge() are not a replacement. They should only be used for additional cache metadata, which can't be delivered through the render array.

See the documentation:

"It is of the utmost importance that you inform the Render API of the cacheability of a render array."

https://www.drupal.org/docs/8/api/render-api/cacheability-of-render-arrays

See this example how Drupal expects a render array to provide the necessary cache metadata when optimizing caching through auto-placeholdering and lazy-building Problem setting user specific cache tags on custom block with user context

14
  • 1
    I don't think that can set cache of Block object. '#markup' is just a Render Element object and there is no reason to set cache context or tag. The block object that needs to rebuild when cache is invalidate.
    – Vagner
    Commented Apr 28, 2016 at 12:33
  • 1
    #markup can be cached the same as any other render element. In this case it is not the markup, but the block, which is cached and here is the problem. You can't solve it with cache tags, because they only get invalidated, if the node is changed in the database.
    – 4uk4
    Commented Apr 28, 2016 at 12:48
  • 1
    @Vagner You can set the cache of a Block object; The BlockBase class has even the necessary methods.
    – avpaderno
    Commented Apr 28, 2016 at 12:50
  • 2
    For me return [ '#markup' => render($output), '#cache' => [ 'contexts' => ['url'] ] ]; works super fine for per URL caching.
    – leymannx
    Commented Nov 14, 2017 at 9:34
  • 2
    Yes, @leymannx, it's as simple as this. This threads seems to overthinking the issue.
    – 4uk4
    Commented Nov 14, 2017 at 10:04
3

The problem here is that the cache contexts are not declare at the right place in the build function:

class ExampleEmptyBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
   $node = \Drupal::routeMatch()->getParameter('node');
   $build = array();

   if ($node) {
    $config = \Drupal::config('system.site');

    $build = array(
    '#type' => 'markup',
    '#markup' => '<p>' . $node->id() . '<p>',
    '#cache' => array(
      'tags' => $this->getCacheTags(),
      'contexts' => $this->getCacheContexts(),
    ),
   );
 }
 return $build;
 }
}

If you call that block on a non node, the build function return an empty array, so there is no cache context for this block and that behaviour will be cached by drupal: the display of this block will not properly invalidate or be rendered.

Solution is just to initialise the $build with the cache contexts everytime:

class ExampleEmptyBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
   $node = \Drupal::routeMatch()->getParameter('node');

    $build = array(
    '#cache' => array(
      'tags' => $this->getCacheTags(),
      'contexts' => $this->getCacheContexts(),
    ),
   );

   if ($node) {
    $config = \Drupal::config('system.site');

    $build['#markup'] = '<p>' . $node->id() . '<p>';
    $build['#type'] = 'markup';
    }
 return $build;
 }
}
3

Have you tried implementing hook_block_view_BASE_BLOCK_ID_alter?

function hook_block_view_BASE_BLOCK_ID_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
  $build['#cache']['max-age'] = 0;
}
2

I realize that I'm late to this conversation, but the below code worked for me:

class ExampleBlock extends BlockBase
{

  public function build()
  {
    $lcContent = '';

    $loNode = \Drupal::routeMatch()->getParameter('node');

    if (!$loNode)
    {
      return (array(
        '#type' => 'markup',
        '#cache' => array('max-age' => 0),
        '#markup' => $lcContent,
      ));
    }

    $lcContent .= "<div id='example_block' style='overflow: hidden; clear: both;'>\n";
    $lcContent .= $loNode->id();
    $lcContent .= "</div>\n";

    return (array(
      '#type' => 'markup',
      '#cache' => array('max-age' => 0),
      '#markup' => $lcContent,
    ));
  }
}
0

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