Understanding Drupal cache contexts via history and code

The Drupal cache system is just a key-value store. Say, your key is left sidebar”, the value is the HTML of the left sidebar. Very simple. But if the left sidebar contained the login block, then you have a different HTML string for anonymous and authenticad users. So now you have a cache ID left sidebar for anonymous users” pointing to one piece of HTML and left sidebar for logged in users” pointing to another. Of course Drupal is multilingual, so you will have left sidebar for anonymous users in English” and left sidebar for logged in users in French” and so on. Maybe you had customizable sidebars per users so now you had a different cache entry per users. Quite obviously many cached pieced of content changes on every page. And this was what Drupal 7 offered: the cache ID had parts set per the caller (say, block” and left_sidebar”) and then drupal_render_cid_parts added parts creating different cache IDs per role, per user, per language, per page. So’d have say block:left_sidebar:bartik:en:fr:u1234”.

In Drupal 8, there is a much bigger flexibility. Here’s an actual cache id (truncated):


The first few parts are just the same: we have a block. But then we see a very big difference: the [languages:language_content]=de part has an identifier and a value. This is a big advantage compared to the previous system where you’d only have de and basically hoped noone will manually introduce such a part causing massive confusion. And this is not all hardwired. There’s a service called cache_context.languages tagged with cache.context which implements the CacheContextInterface and the getContext() method will return the language depending on the type — all three languages present in the cache ID are calculated per the same method. Finally we see a route context. As you can guess, there’s a cache_context.route service, again tagged with cache.context and the getContext method returns the hashed route parameters appended to the route name. So if you are on a different page, the system will end up with a different cache id and so the cached content will vary per page.

Say, you have a block which is different per node type. It would be much more beneficial to solve this problem a bit more generic — let’s write a cache context which allows different blocks per the value of a field. The getContext() method is nothing more than just retrieving the entity from the route match and then converting the value of a field to a string:

public function getContext($entity_type = NULL, $field_name = NULL) {
  $entity = $this->routeMatch->get($entity_type);  
  if ($entity instanceof FieldableEntityInterface && $entity->hasField($field_name)) {
    return hash('sha256', serialize($entity->get($field_name)->getValue()));
  return '';

this could be used as the entity:node:type cache context, for example.

June 5, 2020

Let’s learn recursive CTE SQL via paragraphs

We are going through a partial relaunch and it came up twice to find the nodes which have a certain paragraph somewhere. The first one was just a request to return a list of nodes, the second however required writing an update function to do something with the node. I implemented the first using MySQL 8.0 (MariaDB 10.2 works too) installed locally but the second one required Drupal code written for a lower database version. This provides us with the opportunity to share code with you that does the same in PHP and then in a recursive CTE. Without further ado:

$db = Drupal::database();
$ids = $db->query('SELECT entity_id FROM {paragraph__field_lc_video_ref}')->fetchCol();
$all_ids = $ids;
do {
  $parents = $db->query('SELECT parent_id FROM {paragraphs_item_field_data} WHERE id IN (:ids[]) AND parent_type = :paragraph', [':ids[]' => $ids, ':paragraph' => 'paragraph'])->fetchCol();
  $all_ids = array_merge($all_ids, $parents);
  $ids = $parents;
} while ($parents);
$nids = $db->query('
  SELECT DISTINCT parent_id 
  FROM {paragraphs_item_field_data} 
  INNER JOIN {node_field_data} n ON parent_id = nid AND n.status = 1 
  WHERE id IN (:ids[]) AND parent_type = :node', [':ids[]' => $all_ids, ':node' => 'node'])->fetchCol();

And then the CTE:

SELECT entity_id FROM paragraph__field_article_section_body 
SELECT entity_id FROM paragraph__field_article_section_title;
  SELECT id, parent_id, parent_type
  FROM paragraphs_item_field_data
  WHERE id IN (SELECT entity_id FROM x)
  SELECT r.id, parent.parent_id, parent.parent_type
  FROM r
  INNER JOIN paragraphs_item_field_data parent ON parent.id = r.parent_id
  WHERE r.parent_type = 'paragraph'
INNER JOIN node_field_data n on nid = r.parent_id

May 15, 2020