Drupal 8 : How To Avoid Block Caching

11th February 2018

I was struggling with a problem on a Drupal 8 project that was in development recently where a block used to show information to anonymous users was cached for the first user who saw it. This meant that the special message meant for the first user was then being seen by all subsequent users who visited that page. This only happened when page caching was turned on, but as it's best practice to do that I didn't want to turn that off just to solve one little problem.

After some searching around I found that quite a few people have encountered the same problem, but the solutions to the issue didn't appear to work correctly. Various people had suggested returning the a #cache attribute along with the block information, which contains a max-age setting of 0. There are actually a number of values that we can supply to the #cache array that will effect the cache of the block. So after reading these examples I ended up adding the following array items to the block build array.

  1. $build['#cache']['max-age'] = 0;
  2. $build['#cache']['contexts'] = [];
  3. $build['#cache']['tags'] = [];

This can be done in a much simpler form by using the UncacheableDependencyTrait. By adding this trait to your block code you are effectively doing all of the above without having to add a messy array to your code. When finding out if the block can be cached Drupal will ask the block class for this information by calling the functions getCacheContexts(), getCacheTags() and getCacheMaxAge(). As these functions return null values it effectively turns off caching for the module.

This looks like it should have worked. The only problem was that there appears to be a bug in the current version of Drupal (8.4 in this case) where anonymous block caches aren't being transmitted to to the page layer. This means that no matter what cache tags you set in your block it will always be subject to the page level cache. So, if you have anonymous page caching turned on then your page will force the block to cache regardless of what cache settings it has. Thankfully, there is a solution to this. By using the KillSwitch class we can trigger the page cache to be killed in certain circumstances. This class is held in the page_cache_kill_switch service so we can call its trigger() method in the following way.

\Drupal::service('page_cache_kill_switch')->trigger();

Putting this all together we can then build a block that will never be cached and will show the IP address to a user. Note that if you add this block to every page on your website then your Drupal site will effectively not be cached. I was able to use this block on the site I was developing as it would only ever appear on a single page on the site.

  1. <?php
  2.  
  3. namespace Drupal\ip_address\Plugin\Block;
  4.  
  5. use Drupal\Core\Block\BlockBase;
  6. use Drupal\Core\Cache\UncacheableDependencyTrait;
  7.  
  8. /**
  9.  * Provides an IP Address Block.
  10.  *
  11.  * @Block(
  12.  * id = "ip_address_block",
  13.  * admin_label = @Translation("IP Address"),
  14.  * category = @Translation("IP Address"),
  15.  * )
  16.  */
  17. class IpAddressBlock extends BlockBase {
  18.  
  19. use UncacheableDependencyTrait;
  20.  
  21. /**
  22.   * {@inheritdoc}
  23.   */
  24. public function build() {
  25. // Initialise the block data.
  26. $build = [];
  27.  
  28. // Do NOT cache a page with this block on it.
  29. \Drupal::service('page_cache_kill_switch')->trigger();
  30. // Get the users ip address.
  31. $userIp = \Drupal::request()->getClientIp();
  32.  
  33. if ($userIp == '192.168.88.11') {
  34. $build['content'] = [
  35. '#markup' => $this->t("Your IP address @address matches.", ['@address' => $userIp]),
  36. ];
  37. }
  38.  
  39. return $build;
  40. }
  41. }

Another good example of this in action (and perhaps an easier way to test it) is by detecting the browser that the user is using to access the site.

  1. <?php
  2.  
  3. namespace Drupal\ip_address\Plugin\Block;
  4.  
  5. use Drupal\Core\Block\BlockBase;
  6. use Drupal\Core\Cache\UncacheableDependencyTrait;
  7.  
  8. /**
  9.  * Provides an IP Address Block.
  10.  *
  11.  * @Block(
  12.  * id = "ip_address_block",
  13.  * admin_label = @Translation("IP Address"),
  14.  * category = @Translation("IP Address"),
  15.  * )
  16.  */
  17. class IpAddressBlock extends BlockBase {
  18.  
  19. use UncacheableDependencyTrait;
  20.  
  21. /**
  22.   * {@inheritdoc}
  23.   */
  24. public function build() {
  25. // Initialise the block data.
  26. $build = [];
  27.  
  28. // Do NOT cache a page with this block on it.
  29. \Drupal::service('page_cache_kill_switch')->trigger();
  30.  
  31. if (stristr($_SERVER['HTTP_USER_AGENT'], 'firefox')) {
  32. $build['content'] = [
  33. '#markup' => $this->t("User agent @browser found.", ['@browser' => $_SERVER['HTTP_USER_AGENT']]),
  34. ];
  35. }
  36. return $build;
  37. }
  38. }

 

Comments

Permalink

This is no good for a block that is dynamic and/or sits on the homepage, because the page cache is likely important and needed.

I wish only the block element could have its cache invalidated without removing the entire page's cache.

For now, JS still seems to be the best solution for a small block of dynamic content within a cached page.

Brian MindSing (Wed, 02/05/2020 - 09:40)

Permalink

I absolutely agree, it's not a good idea to put this on your home page. If, however, you want to show something on your login page that is to do with the user then this is a good way to get that done.

I also agree that using a tidy JavaScript ajax callback might be the better solution here. That way, only a small portion of the page load (ie, the ajax response) needs to be uncached.

philipnorton42 (Thu, 02/06/2020 - 09:00)

Add new comment

The content of this field is kept private and will not be shown publicly.