Replacing hook_boot and hook_init Functionality in Drupal 8

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.