This is a small case study in converting how page caching is done in the D7 version of the Optimizely module to Drupal 8.
The purpose of the module is to manage the insertion of certain <script> elements into designated pages of a site. To do so, the user creates one or more projects. Each project has one or more url paths. When a project is enabled, all pages that match one of its paths have the <script> element added.
To specify project paths, use of a trailing * wildcard is allowed, as are special page designators. For example, these are all valid paths.
/node/2
/node/*
/admin/config/system
/admin/*
*
<front>
In the case of /admin/* it would match against any of
/admin/
/admin/config/
/admin/config/system/
/admin/people
/admin/people/create
...
In the D7 version, invalidating is done through calls to cache_clear_all(), which takes three parameters. The function is used by the module in three different ways.
(1) To invalidate a particular page, e.g.
cache_clear_all('/node/2', 'cache_page', FALSE);
(2) To invalidate a path with a trailing wildcard, e.g.
cache_clear_all('/node/*', 'cache_page', TRUE);
(3) To invalidate all pages of the site,
cache_clear_all('*', 'cache_page', TRUE);
In Drupal 8 the Cache API is completely different. Function cache_clear_all() has disappeared.
After some research, it looked like using cache tags would be the way to go. The article Cacheability of render arrays was especially helpful in how to think about caching as applied to page rendering.
There are two facets to implementing this. The first is what needs to be done when a page is rendered, the second is what to invalidate when triggering changes occur.
For page rendering, I already had an implementation of hook_page_attachments() that checked for inserting the element into any page whose path matched against any of the enabled project paths.
This hook function is where cache tags could be added to the page. This turned out to be a little tricky. A page must be invalidated for two different use cases: when the page contains the element which now needs to be removed, and when the page does not contain the element but it now needs to be added.
For the first case, I decided to just use the matching project path act as the cache tag (there can only be one because overlapping project paths are not allowed).
But for the second case, I had to cover all the possible project paths that might be enabled in the future. So, for example, for the page at /node/2 there are three such possible paths.
/node/2
/node/*
*
For every page rendered, it was necessary to attach all of these possible project paths as cache tags. Here is a snippet of code that shows how this is done in hook_page_attachments().
// Site-wide wildcard.
$page['#cache']['tags'][] = 'optimizely:*';
// Non site-wide wildcards. Repeat for every directory level.
$dirname = pathinfo($current_path, PATHINFO_DIRNAME);
while ($dirname && $dirname != '/') {
$page['#cache']['tags'][] = 'optimizely:' . $dirname . '/*';
$dirname = pathinfo($dirname, PATHINFO_DIRNAME);
}
// The specific page url.
$page['#cache']['tags'][] = 'optimizely:' . $current_path;
// Finally, if there is an alias for the page, tag it.
if ($current_path_alias) {
$page['#cache']['tags'][] = 'optimizely:' . $current_path_alias;
}
The optimizely: prefix follows the convention of prefixing a group name as part of the tag where appropriate. For example, cache tags from core include node:1 and config:node.type.article.
Finally, there is the matter of what and how to invalidate when changes to the projects and their paths are submitted. This turned out to be fairly easy to implement.
An array of all relevant project paths is passed to a function that carries out the following.
$cache_tags = [];
foreach ($path_array as $path) {
$cache_tags[] = 'optimizely:' . $path;
}
\Drupal::service('cache_tags.invalidator')->invalidateTags($cache_tags);
For debugging purposes, outputting X-Drupal-Cache-Tags in HTTP headers was extremely useful. See my earlier post Enable and Use X-Drupal-Cache-Tags in HTTP headers.
This post is about the caching that is done by Drupal itself. The D7 version also checks for the presence of the varnish module and calls a function of that module if it exists. I did not pursue a replacement for that functionality, but the articles Varnish and Use Drupal 8 Cache Tags with Varnish and Purge look promising.
Sources:
Function cache_clear_all() has been removed
https://optimizely-to-drupal-8.blogspot.com/2014/07/function-cacheclearall-has-been-removed.html
Cacheability of render arrays
https://www.drupal.org/developing/api/8/render/arrays/cacheability
Cache tags
https://www.drupal.org/developing/api/8/cache/tags
Allow to set #cache metadata in hook_page_attachments() https://www.drupal.org/node/2475749
public static function Cache::invalidateTags
https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Cache!Cache.php/function/Cache%3A%3AinvalidateTags/8
No comments:
Post a Comment