Drupal - How do I correctly setup caching for my custom block showing content depending on the current node?

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.


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 = {
*     "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.


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.

Tags:

Caching

8

Blocks