Development

Using Lazy Builders and Auto Placeholders in Drupal 8

Introduction

I’ve been working on a site that features a lot of user-specific customization and output. The site offers workshops (courses) using the Opigno LMS for Drupal 8. The workshops are rendered in a few different ways throughout the site. For the most part, the rendered workshops appear the same for all users. Here’s an example of a “card” view mode for a workshop:

If a user has successfully completed a workshop, a special badge will appear on the card view mode. Also, the card will be highlighted:

There are two parts of the workshop card template that have to change based on the user viewing the card:

  1. “Completed” badge appears if user completed the workshop
  2. “completed” CSS class added to the wrapper if the user completed the workshop

Potential Solutions

I was faced with the challenge of maintaining performance while showing these customizations per-user.

Some solutions (and notes about each) that were on the table:

  1. Add a user or session cache context for this entity type + view mode combination
    1. This would work but would result in a lot of repeated data in the database cache tables
  2. Don’t cache this entity type + view mode combination
    1. Obviously there would be a significant performance hit here
  3. Use default cache configuration and implement the badge/css class via Javascript
    1. There wasn’t much time to spend coding something like this
    2. You’d see things flicker while the page was loading (classes changing, badges appearing)
  4. Use Lazy Builders and Auto Placeholders to render these dynamic pieces
    1. This seemed like the best solution; 95% of the template is stored in memory, 5% is rendered on-the-fly
    2. If the dynamic pieces were hungry (meaning they take a while to run, or they overuse resources) this might not be as good as the user cache context solution above)

The Solution

Keeping with my general approach (hopefully self-explanatory code) here’s what I came up with.

Step 1: Identified which template/theme function I needed to modify

This was simple. I enabled twig debug settings in my development.services.yml file. See here.

Workshop entities are group entities in Opigno LMS. The theme override I could use was hook_preprocess_group , and the template was group--opigno-course--card-view-explore.html.twig  .

Twig debug

Step 2: Implemented hook_preprocess_group()

I could’ve done this in the theme’s .theme file, but I chose to put it in a .module file (I want the placeholders to be available no matter which theme we’re using).

There are a few important things to note here:

  • We’re using two separate placeholder variables but they both call the same function. The expensive-to-perform logic within that callback is only executed once. The results are altered based on the second passed parameter (“badge” vs “classname”).
  • In most cases you’d have a 1:1 pairing (single callback for a single placeholder)
  • The params you pass to a lazy builder have to be scalar (int, float, string, boolean) or NULL.
  • On my site I needed the badge/css class on several view modes and group types, hence the conditions at the top of this function.

Step 3: Implemented the callback function

You can put this in a .module function, a method in a class, a service, etc. For simplicity I’ve defined the callback directly below my mysite_preprocess_group function in mysite.module.

There are a few important things to note here:

  1. I’ve simplified this function a bit for the purposes of this blog post; it’s still pretty ugly and should be broken apart a bit.
  2. Because we are executing this twice (for both placeholders), I leverage drupal_static to store the results of expensive operations (everything in the if (empty($data[$group_id])) {  condition). This $data variable will persist through subsequent calls in the same request.
  3. The error_log trick is helpful while developing; it lets you see when Drupal is running the expensive calls (before static variable is populated), and when it’s able to just pull data from the static variable. Xdebug is amazing and I use it every day, but it’s nice (in this case) to throw this info into the error log and tail it (e.g., lando logs -t -f -s appserver ).

Step 4: Updated twig template

Step 5: Tested

I cleared all of my caches, made sure all caching was at production levels, then started watching my logs for my debug messages (see note about error_log usage above).

Then, I visited a listing page that had a few of these workshop cards. Here’s what I saw:

I could see here that the expensive “query” logic only happened once.

The page was rendering cards correctly.

Step 6: Kept testing

I tested over and over and over again. I worked through many workshops with many users to make sure user X wasn’t seeing badges from user Y. All the while, I monitored the logs to make sure each user was getting fresh (their own) badge/class info.

Step 7: Implemented the same placeholders in several other template files

Then I continued to test.

Additional Information about Placeholders

If you’re curious what is stored in the database, have a look at the cache_render table.

I picked one of my groups to focus on. Here’s what shows in the cache_render table:

Cache render table

The cache contexts (which I inspected via xdebug in mysite_preprocess_group()) show a context of  user.permissions. This is why we see two entries (one as I browsed anonymously, and one as I browsed as an authenticated user).

You can see the placeholders in the data field of the rendered entity cached rows. Here’s what the authenticated version’s data looks like:

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *