Skip to content

Instantly share code, notes, and snippets.

@drubb
Last active January 11, 2023 16:39
Show Gist options
  • Save drubb/06feabcada785fa79b154290814bb7db to your computer and use it in GitHub Desktop.
Save drubb/06feabcada785fa79b154290814bb7db to your computer and use it in GitHub Desktop.
Invalidate Drupal cache tags on full hours / at midnight using stack middleware

While it's possible to invalidate Drupal cache entries time-based, e.g. every 10 minutes, using max-age, this strategy doesn't always fit:

  • Drupal's internal page cache doesn't respect max-age
  • Some sites, e.g. event portals, need to update things at fixed spots (e.g. every full hour or at midnight) rather than interval-based.

At first glance this requirement can be implemented using cron runs, but this might be no option in some cases, e.g. if the invalidation needs to be done more often than cron runs take place. Here's a way to implement cache invalidations on full hours and at midnight using Drupal's stack middleware.

<?php
namespace Drupal\mymodule\StackMiddleware;
use Drupal\Core\Cache\CacheTagsInvalidator;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\State\State;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Provides and invalidates cache tags for hourly / daily changes.
*
* Example:
* Blocks needing a refresh every full hour should be tagged using
* the cache tag defined in constant CacheTimeout::HOURLY.
*/
class CacheTimeout implements HttpKernelInterface {
// Some constants to use elsewhere.
public const HOURLY = 'mymodule:hourly';
public const DAILY = 'mymodule:daily';
public const STATE = 'mymodule.last_invalidated';
/**
* The wrapped HTTP kernel.
*
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
protected HttpKernelInterface $httpKernel;
/**
* The date formatter.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected DateFormatter $dateFormatter;
/**
* The state service.
*
* @var \Drupal\Core\State\State
*/
protected State $state;
/**
* The cache tags invalidator.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidator
*/
protected CacheTagsInvalidator $cacheTagsInvalidator;
/**
* Constructs a new CacheTimeout instance.
*
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel
* The wrapped HTTP kernel.
* @param \Drupal\Core\Datetime\DateFormatter $date_formatter
* The date formatter.
* @param \Drupal\Core\State\State $state
* The state service.
* @param \Drupal\Core\Cache\CacheTagsInvalidator $cache_tags_invalidator
* The cache tags invalidator.
*/
public function __construct(HttpKernelInterface $http_kernel, DateFormatter $date_formatter, State $state, CacheTagsInvalidator $cache_tags_invalidator) {
$this->httpKernel = $http_kernel;
$this->dateFormatter = $date_formatter;
$this->state = $state;
$this->cacheTagsInvalidator = $cache_tags_invalidator;
}
/**
* {@inheritdoc}
*/
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
// We're taking care of master requests only.
if ($type !== self::MASTER_REQUEST) {
return $this->httpKernel->handle($request, $type, $catch);
}
// Get the state variable indicating the last invalidation (YYYYMMDDHH)
$last_invalidation = $this->state->get(self::STATE, '0000000000');
// Get the current request time.
$current_time = $this->dateFormatter->format(
time(), 'custom', 'YmdH', 'Europe/Berlin'
);
// Handle daily invalidation (implies hourly invalidation, too).
if (strncmp($last_invalidation, $current_time, 8) < 0) {
$this->cacheTagsInvalidator->invalidateTags([
self::DAILY,
self::HOURLY,
]);
$this->state->set(self::STATE, $current_time);
return $this->httpKernel->handle($request, $type, $catch);
}
// Handle hourly invalidation.
if ($last_invalidation < $current_time) {
$this->cacheTagsInvalidator->invalidateTags([self::HOURLY]);
$this->state->set(self::STATE, $current_time);
return $this->httpKernel->handle($request, $type, $catch);
}
// No need to invalidate cache tags.
return $this->httpKernel->handle($request, $type, $catch);
}
}
<?php
/**
* @file
* Contains install and update functions for the module.
*/
use Drupal\mymodule\StackMiddleware\CacheTimeout;
/**
* Implements hook_uninstall().
*/
function mymodule_uninstall() {
// Remove the module's state variable.
\Drupal::state()->delete(CacheTimeout::STATE);
}
services:
http_middleware.cache_timeout:
class: Drupal\mymodule\StackMiddleware\CacheTimeout
# This looks invalid, but is correct! The reason is there's a service injected implicitly
# at the second position, which can't be specified here. See core's ban module to compare!
arguments: ['@date.formatter', '@state', '@cache_tags.invalidator']
tags:
# This needs to run before Drupal's internal page cache middleware!
- { name: http_middleware, priority: 220 }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment