Many Drupal 7 sites relied upon hook_boot
and hook_init
for critical functionality that needed to be executed on every page, even pages cached by Drupal. Oftentimes these hooks were incorrectly used and an alternate approach would have been much more performant by triggering logic exactly where it was needed instead of on every page. For example, drupal_add_js
could be used to add JavaScript for a particular block or theme hook globally, but using a targeted preprocess function is often the correct approach. However, in some cases, these hooks were indeed the correct solution. Both hook_boot and hook_init were deprecated in Drupal 8, so an alternate approach will be needed.
Cacheable Logic
In Drupal 7, hook_init
offered a hook that was fired on every page that Drupal did not cache. Drupal 8 offers this same functionality, using the Event Subscriber pattern as detailed on the hook_init
change notice. These pages provide detailed examples, which walk you through the process of setting one up. How to Register an Event Subscriber in Drupal 8 also provides examples of event dispatching code.
Uncacheable Logic
Uncacheable logic should seldom be needed, as the solution is often to use proper cache settings on your render arrays. In the rare case that it is indeed needed, there are two viable options depending on the situation.
Using settings.php
The change notice page states:
A module that needs to run on cached pages should prompt its users to add code in settings.php
Note that Services are not instantiated yet when this code runs, thus severely limiting the available functionality. So while this approach is by no means ideal, it is available. The StackMiddleware
approach below is a better approach that offers deeper integration with Drupal functionality.
Using StackMiddleware
This comment on the hook_boot
change notice page provides an example of using StackMiddleware
. It provides 95% of the functionality needed to run logic on cached pages by utilizing a tagged service with the http_middleware
tag. Since the new class is a service, it will have full access to other core and contrib services, allowing for much greater functionality. The example shows the following for a module’s *.services.yml
file:
services:
http_middleware.mymodule:
class: Drupal\mymodule\StackMiddleware\MyModule
tags:
- { name: http_middleware, priority: 180, responder: true }
This is a pretty standard service definition, but note the items added to the tags
property that register our service with the http_middleware
tag and also set a priority
. In order to bypass the page cache, understanding the page_cache.services.yml
file is helpful. There, a similar definition can be found, but with a higher priority
value.
services:
http_middleware.page_cache:
class: Drupal\page_cache\StackMiddleware\PageCache
arguments: ['@cache.render', '@page_cache_request_policy', '@page_cache_response_policy']
tags:
- { name: http_middleware, priority: 200, responder: true }
Higher priority services are run first. So to trigger logic before the page cache module takes over the request, a priority greater than 200
is needed.
services:
http_middleware.mymodule:
class: Drupal\mymodule\StackMiddleware\MyModule
tags:
- { name: http_middleware, priority: 210, responder: true }
With this change in the services files, and proper setup of the service as described by the comment, the http_middleware.mymodule
service should now be called on every page load, even on fully cached pages.
namespace Drupal\example\StackMiddleware;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Performs a custom task.
*/
class ExampleStackMiddleware implements HttpKernelInterface {
/**
* The wrapped HTTP kernel.
*
* @var \Symfony\Component\HttpKernel\HttpKernelInterface
*/
protected $httpKernel;
/**
* Creates a HTTP middleware handler.
*
* @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel
* The HTTP kernel.
*/
public function __construct(HttpKernelInterface $kernel) {
$this->httpKernel = $kernel;
}
/**
* {@inheritdoc}
*/
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
// Custom logic goes here.
return $this->httpKernel->handle($request, $type, $catch);
}
}
Verifying the Results
A quick and easy way to test all of this is to simply add \Drupal::logger('test')->notice(‘not cached’);
into the functions triggered by each of the approaches above. Ensure that the Drupal cache is enabled, and simply refresh a page while watching your log (drush ws --tail
). Then verify the logic is being called as expected.