Problem/Motivation

Cache tags are brilliant when they work well, and can be obtuse and difficult to use when they don't.

In the case of preprocessing nodes, it's quite difficult to find a way to make sure that node cache tags bubble correctly. Consider this use case (seemingly pretty common):

  1. "Parent" nodes have one "Child" node associated with it.
  2. The Parent node should render the Parent node, then render the Child node in 'Parent teaser' view mode.
  3. Themer decides to use a preprocess function to preprocess the Parent node, then get the render array using something like $view_builder->viewMultiple($child_node, 'parent_teaser'), then the Twig template prints the teaser.
  4. Content admins complain that when they save updates to the "Child" node, the "Parent" doesn't reflect changes until the entire site's cache is cleared.

Apparently Views' viewRenderable() method sets all the cache contexts/tags correctly—we need something similar for getting the render arrays for nodes in general...

Proposed resolution

Either document how to add the appropriate cache contexts/tags when rendering a node via preprocess functions, or add functionality to make this easier to do.

Remaining tasks

TBD

User interface changes

N/A

API changes

TBD

Data model changes

N/A

Comments

geerlingguy created an issue. See original summary.

wim leers’s picture

Title: Bubbling cache tag metadata when preprocessing nodes is difficult » Bubbling cache tag metadata when rendering nodes in preprocess functions is difficult

Rendering things correctly in preprocess is always difficult. It's why it's actively discouraged to do very complex things in preprocess functions.

Anything we would do "to make this easier to do" would just be fighting symptoms. The root cause is using the preprocess layer for things it's not designed to do.

geerlingguy’s picture

Be that as it may, it seems the majority of FE developers tend to stick to preprocess functions and templates. What alternative route would you recommend? I haven't seen any simpler way of rendering one node inside another yet in D8 :-/

sarahjean’s picture

You don't have to be doing preprocessing, you can see this issue if you simply use twig and don't print {{content}}, like the example here: https://www.previousnext.com.au/blog/ensuring-drupal-8-block-cache-tags-...

geerlingguy’s picture

berdir’s picture

In which direction is the reference, child to parent?

can you share the full relevant snippets of how you get the child, prepare it and then display it in twig? There are complicated cases, but an actually viewed entity should just work as far as I see, also in preprocess.

I would recommend doing things in hook_node_view() and to use extra fields, so you can control if things are done/processed per view mode, but preprocess should still work.

geerlingguy’s picture

@Berdir - here's a high-level overview of what we're doing, in code:

/**
 * Implements hook_preprocess_hook().
 *
 * We need to look up related component data for this product for its display.
 */
function beagov_theme_preprocess_node(&$variables) {
  $view_builder = \Drupal::entityTypeManager()->getViewBuilder('node');
  $node = $variables['node'];
  $product_id = $node->id();

  // Get the related component.
  $query = \Drupal::entityQuery('node')
    ->condition('status', 1)
    ->condition('type', 'component')
    ->condition('field_product', $product_id)
    ->sort('created', 'DESC')
    ->range(0, 1);
  $result = $query->execute();
  $component = Node::loadMultiple($result);

  // Get a renderable teaser view.
  $variables['component_teaser'] = $view_builder->viewMultiple($component, 'teaser');
}

Then, inside node--product.html.twig, we have:

<div class="component">
  {{ component_teaser }}
</div>
<div class="product">
  {{ content }}
</div>

Not exactly the code we're using, but you get the gist.

berdir’s picture

And what kind of changes are not being updated? Anything?

The only thing I can think of is adding new children, that makes sense, you need to add the node_list or a more specific cache tag, or invalidate the correspending parent cache tag when saving a children, but that's by design and not related to preprocess.

Rendering those things in the template should result in adding the necessary cache tags, I don't see why that wouldn't work.

geerlingguy’s picture

The only thing I can think of is adding new children, that makes sense, you need to add the node_list or a more specific cache tag, or invalidate the correspending parent cache tag when saving a children, but that's by design and not related to preprocess.

Thinking about it with my backend dev hat on, this does make sense. But it's (a) not obvious why it doesn't work out of the box (like it did in Drupal < 8, when all the page cache was cleared when you saved a new node...), and (b) difficult to find an example of how to clear the parent when the child is saved/updated.

I mean, in one sense this is just a documentation issue. But it's not the first time I've run into a theme-related cache tag issue where google searches are fruitless and the examples in the Cache/Render API documentation are not specific enough to quickly find a solution. I can only imagine someone who's more of a front end developer/site builder and doesn't dig deeper into the question will end up disabling the dynamic page cache entirely because of these kinds of issues...

For the site in question, we'll likely find and clear the parent node's cache tag manually, but it took too much time for us to go from 'why isn't this working like we thought it would?' to 'we may have a working solution'. I want to reduce that time for others in our situation in the future!

geerlingguy’s picture

In the interest of assisting others who will likely hit this issue in the future, here's how we fixed it on our site (so we can use the method in #2848158-8: Bubbling cache tag metadata when rendering nodes in preprocess functions is difficult to print a teaser display of a child content type in a parent content type page that the child references:

/**
 * Implements hook_node_insert().
 *
 * NOTE: You should likely also do this in hook_node_update()!
 */
function mymodule_node_insert(EntityInterface $entity) {
  if ($entity->bundle() == 'mynodetype') {
    if (!empty($entity->field_node_reference->getValue())) {
      $reference_value = $entity->field_node_reference->getValue();
      _mymodule_invalidate_node_cache_tags($reference_value);
    }
  }
}

/**
 * Invalidate a node by an entity reference field value.
 *
 * @param array $entity_reference_value
 *   An entity reference field value array.
 */
function _mymodule_invalidate_node_cache_tags($entity_reference_value) {
  $node_id = array_shift($entity_reference_value);
  $node = Node::load($node_id['target_id']);
  $tags = $node->getCacheTagsToInvalidate();
  Cache::invalidateTags($tags);
}

This seems to work okay in our instance. Any way to make it more efficient? It would be nice if there were a way in core to just say "invalidate cache tags for this node" and pass it an ID...

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

mxh’s picture

This is a more common problem, isn't it? I had to embed a block inside another block and did it via preprocessing, the embedded block had further cache contexts defined. The result was that the renderer created the generated cache entry for the "parent" block both with and without the additional cache context by the "child" block. Even setting explicitly $variables['#cache'] didn't solve it.

It's pretty frustrating that a themer can't rely on the caching system the way it's described. There's always a caveat somewhere which costs time to work around it, and can only be solved outside of a themer's scope.

wim leers’s picture

@mxh

The result was that the renderer created the generated cache entry for the "parent" block both with and without the additional cache context by the "child" block.

This sounds like that first cache entry is a "redirecting" cache entry. Does it start with something like a:2:{s:15:"#cache_redirect";[…]?

mxh’s picture

This sounds like that first cache entry is a "redirecting" cache entry. Does it start with something like a:2:{s:15:"#cache_redirect";[…]?

Yes, it stores it as a cache redirect, and thus leads to the wrong cache hit for the parent block on pages where an other cache context should be present.

mxh’s picture

The root of problem in #13 was a custom cache context implementation, which always calculated the same value as the key (example: [customcontext:abc]=abc). It obviously caused wrong cache redirections. Thanks @Wim Leers for pointing me to the right track to identify this.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev
cilefen’s picture

Status: Active » Postponed (maintainer needs more info)

I am triaging old support requests. This one hasn't had a comment in four years. It it still a "thing"?

cilefen’s picture

Status: Postponed (maintainer needs more info) » Closed (outdated)
joachim’s picture

For future reference: in a preprocess hook, you can do:

  $additional_cacheable_metadata = new CacheableMetadata();

  $additional_cacheable_metadata->applyTo($variables);