Joris Vercammen

Lazy builder callback

This week, I’ve been working on a couple of issues in the Drupal cacheability issue queue. In a couple of those issues, I’ve had the opportunity to work with lazy builders. This is a really interesting new concept in drupal 8, and I’m going try to explain a little bit more about how this works here.

We’re going to do this by creating a block that will display a random icecream flavor.

Now, there are 2 options to resolve this issue:

  1. Make every page with the block on it uncacheable. This is horrible for performance but it’ll work. This is simple to do: $build['#cache]['max-age'] = 0;
  2. The second solution is introducing a placeholder and attaching a lazy builder to it. This is a little bit more complex but it’s the best for cacheability because you can have the rest of the render array be cacheable and only have one part of it being uncacheable.

A lazy builder, is a piece callback function that gets executed every request, so that the surrounding code is can still be cached. This is a way to make sure that all your pages can be cached and keep all the required complexity. First of all, we need to implement a callback for the lazy builder.

<?php

namespace Drupal\icecream;

class Icecream {

  /**
   * Returns a renderable array containing an ice cream flavour.
   *
   * @return array
   */
  public function lazyCallback() {
    $flavors = [
      'chocolate',
      'vanilla',
      'chocolate chip cookie dough',
      'grape',
      'straciatella',
      'rocky road',
    ];

    // This is never cacheable, because of the max_age 0.
    return [
      '#markup' => $flavors[array_rand($flavors)],
      '#cache' => ['max_age' => 0],
    ];
  }
}

So, we’ve created a new class that has one public method, the lazyCallback method. This can be as complex as you want, as long as you’re returning a renderable array. In this example the renderable array is explicitly non-cacheable, but because this is just another render-array, it can have it’s own set of cache-tags / cache-contexts.

If you want to have a different flavor based on every page, but it can be cached per url, the only change needed would be to change the #cache part of the returned render array.

<?php

return [
  '#cache' => [
    'contexts' => ['url']
  ]
];

Now, here’s the block that will use this callback:

<?php

  /**
   * {@inheritdoc}
   */
  public function build() {
    $build = [];

    // This placeholder is just a unique string, it has no other requirements
    // other than being injected in the places than where you want to have the
    // #lazy_builder take over.
    $placeholder = 'iceCream' . crc32('lazy icecream block');

    // Makes sure the placeholder will be replaced by an ice cream flavor by
    // attaching a lazy builder that will make the rest of this block cacheable.
    $build['#attached']['placeholders'][$placeholder] = [
      '#lazy_builder' => ['icecream:lazyCallback', []]
    ];

    // This is really intensive to calculate, so that's why we're caching the
    // entire block and having the lazy builder take care of the uncacheable
    // part of the block.
    $build['#markup'] = 'This is being generated with a placeholder.
    It is time for ' . $placeholder . ' ice cream. Hurray for ice cream!';


    // Returns the renderable array with attached placeholder.
    return $build;
  }

You can see in the code, that we’ve set #lazy_builder as an array containing a string and another array. If you don’t specify the second array, Drupal will give you an error with the following message: #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback. The callback is set as 'icecream:lazyCallback'. This works because we’ve defined the class in services.yml as icecream

## services.yml

services:
    icecream:
        class: Drupal\icecream\Icecream

These are all the pieces that you need to make a #lazy_builder callback. You can use this solution every time you have a small portion of a page that’s too complex to be cached. Lazy builders make sure that the rest of the page is still cacheable, and that can help move quite some load off the server.

If you want to have a look at the complete code, you can check it out on my bitbucket: https://bitbucket.org/botchcake/icecream-lazybuilder/src.


Update - note that the placeholders that are currently being created in the block aren’t going to be needed once https://www.drupal.org/node/2499157 lands.