Drupal 8 : How To Avoid Block Caching

Sunday, February 11, 2018 - 18:21

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
2
3
$build['#cache']['max-age'] = 0;
$build['#cache']['contexts'] = [];
$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 it's trigger() method in the following way.

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

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

Philip Norton

Phil is the founder and administrator of #! code and is an IT professional working in the North West of the UK.
Google+ | Twitter

Add new comment