17

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
    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 Apr 28 '16 at 11:52
29

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.

  • so the trick is to remove the #cache settings in the build function and just add the public functions? – Alex Apr 28 '16 at 13:41
  • 3
    That doesn't matter where you set the cache tags and contexts. – 4k4 Apr 28 '16 at 13:43
  • 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 Apr 28 '16 at 13:46
  • @4k4 url.path seemed to have worked too. whats the difference? – Alex Apr 28 '16 at 13:47
  • 2
    @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. – 4k4 Apr 28 '16 at 14:00
13

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.

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

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.

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

If you want to add #cache from the node automatically:

use Drupal\Core\Cache\Cache;

$build = array(
  ...
  '#cache' => array(
    'tags' => Cache::mergeTags($this->getCacheTags(), $node->getCacheTags()),
    'contexts' => $node->getCacheContexts(),
    'max-age' => $node->getCacheMaxAge(),
  ),
);

The context is the critical one here.

To set contexts manually:

$build = array(
      ...
      '#cache' => array(
        'contexts' => array('url.path'),
      ),
    );
  • 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 Apr 28 '16 at 12:33
  • #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. – 4k4 Apr 28 '16 at 12:48
  • @Vagner You can set the cache of a Block object; The BlockBase class has even the necessary methods. – kiamlaluno Apr 28 '16 at 12:50
  • 1
    For me return [ '#markup' => render($output), '#cache' => [ 'contexts' => ['url'] ] ]; works super fine for per URL caching. – leymannx Nov 14 '17 at 9:34
  • 1
    Yes, @leymannx, it's as simple as this. This threads seems to overthinking the issue. – 4k4 Nov 14 '17 at 10:04
0

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;
 }
}
0

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,
    ));
  }
}
  • better late than never :) – Alex Jul 7 '17 at 7:25
0

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; }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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