10

I have run into an issue where a block that should be unique per page isn't for logged-out users. The issue is a custom block plugin I have on a views search page that contains custom filters (sort of like a custom replacement for exposed filters. the block placed through /admin/structure/block).

Based on what I have learned about Drupal 8, I added the cache contexts to my build array:

  public function build() {

    $search_form = \Drupal::formBuilder()->getForm('Drupal\mymodule\Form\SearchForm');
    return [
      'search_form' => $search_form,
      '#cache' => ['contexts' => ['url.path', 'url.query_args']]
    ];

  }

But it seems this must be incorrect because when logged-out, the block would get cached on the first view, and when the url changed, it wasn't showing a new version of the block.

I thought it might be the view page that was causing the issue, but even when I turned caching off on the view page, the issue remained.

I was able to fix the issue several ways, for example, by using a preprocess_block hook:

function mymodule_preprocess_block__mycustomsearchblock(&$variables) {
  $variables['#cache']['contexts'][] = 'url.path';
  $variables['#cache']['contexts'][] = 'url.query_args';
}

But it bothered me I couldn't just put the cache contexts into my block's build array.

Since my block extends BlockBase, I decided to try out the getCacheContexts() method, especially since I saw some modules within core are doing it this way.

  public function getCacheContexts() {
    return Cache::mergeContexts(parent::getCacheContexts(), ['url.path', 'url.query_args']);
  }

This is also fixed the issue, but interestingly, when I output the variables in the preprocess block function, these are not showing in $variables['#cache']['contexts'], but they do show in $variables['elements']['#cache']['contexts']

array:5 [▼
  0 => "languages:language_interface"
  1 => "theme"
  2 => "url.path"
  3 => "url.query_args"
  4 => "user.permissions"
]

I'm trying to figure out how this works, and why it wasn't working from the build function.

Looking at /core/modules/block/src/BlockViewBuilder.php at the viewMultiple() function, it looks like it pulls the cache tags from the entity and the plugin:

'contexts' => Cache::mergeContexts(
  $entity->getCacheContexts(),
  $plugin->getCacheContexts()
),

So that explains why adding a getCacheContexts() method to my block plugin adds the contexts to my block. Also, looking at the preRender method in the same class, it looks like it doesn't use the cache array in the blocks build function, which confuses me, as it seems the way to add caching in Drupal 8 is to add a #cache element to render elements.

So my question is,

1) Are cache contexts added directly on the array in a block plugin ignored?

2) If so, is there a way around that, do we need to add it to a child element of the build array?

3) If context added directly is ignored, is adding a getCacheContexts() the way to go for block plugins in custom modules?

  • 1) No, your block content is actually a level down and should be merged in later. 2) Not needed because 1, 3) Implementing getCacheContexts() can be easier/cleaner but should not be required. You explicitly mention anonymous users, are you sure that it also doesn't affect normal authenticated users? Does the problem go away if you disable dynamic_page_cache? Something strange must happen if it only affects anon users, as the internal page cache always varies by url/query args anyway. – Berdir Dec 19 '16 at 18:42
  • Disabling dynamic page cache doesn't fix the issue. – oknate Dec 19 '16 at 19:15
  • Hm, It might be a problem with the fact that your top level element doesn't contain anything except #cache. Have you tried to simply set #cache within you form? It's the form that needs to vary by those, and since cache tags bubble up, that should just work. And if you ever use your form in a different block or other place, it should also just work there. – Berdir Dec 19 '16 at 19:20
  • 1
    I quickly hacked SyndicateBlock and used this build() method: gist.github.com/Berdir/33a31b1e98caf080dae78adb731dba4c. Placing that on my site works just fine, the cache contexts are visible in cache_render table and the correct request URI is displayed. Can you try the same? Seems to me like something very strange is going on on your site – Berdir Dec 19 '16 at 19:46
  • 1
    Do you use the standard block template or a custom one? See drupal.stackexchange.com/questions/217884/… – 4k4 Dec 19 '16 at 20:18
8

In most cases, you just set the cache context directly on the render array you return in your build() method.

I have finally found what my issue was, with the help of @Berdir and @4k4. If you are using a custom template, such as block--myblock.html.twig and you output the variables individually, such as {{ content.foo }} instead of all at the same time like {{ content }}, it ignores your cache contexts passed directly into your block build array, when logged out. See What is the correct way to set cache contexts on custom blocks?

So, to answer the original question:

1) Cache contexts passed directly into a custom block plugin are sometimes ignored. You can test this by altering the SyndicateBlock, and then creating a custom template in your theme block--syndicate.html.php in which you output the variables individually like this:

{% block content %}
  {{ content.foo }}
{% endblock %}

As you change the url arguments, you'll see the block doesn't respect the cache context.

Now if you change it output all the content as a piece, it works:

{% block content %}
  {{ content }}
{% endblock %}

Now, it respects the cache context, and the block is unique per page.

2) For now, to work around this, you can just output what's in your block in it's own template.

 public function build() {

    $search_form = \Drupal::formBuilder()->getForm('Drupal\mymodule\Form\SearchForm');
    return [
      '#theme' => 'mycustomtemplate',
      '#search_form' => $search_form,
      '#cache' => ['contexts' => ['url.path', 'url.query_args']]
    ];

  }

This circumvents the block module's esoteric caching exceptions and your form is now unique per page when logged out.

3) Should you create your own theme template to fix this, or just add a method for getCacheContexts() in your custom Block plugin? It's better to create a new theme template, rather than add a getCacheContexts() method that overrides the natural order of bubbling up of cache contexts, and may break metadata deeper in your build array.

  • This is a very good summary of the problem. But I think the conclusion in 3) is problematic, because you not only break that your own cache metadata can bubble up, but also that which may be deeper inside of the render array and you may not be aware of. – 4k4 Dec 19 '16 at 21:33
  • So you would suggest creating a new theme template? To preserve clear bubbling? – oknate Dec 19 '16 at 21:34
  • Yes, use the block template only to add things to the outside. Build the inside of the block in build(). Use custom templates for this or use render elements, like a table or a core template like links. – 4k4 Dec 19 '16 at 21:37
  • OK, I'll update the answer. – oknate Dec 19 '16 at 21:38
  • Ugh, I didn't realize that Twig's drilling in would ignore the cacheable metadata. This might mean that we need to use our own custom method in the end to drill in (which renders the twig extension useless) so that we preserve metadata while going down into lower levels. Good find! – LionsAd Apr 27 '18 at 22:12
2

I also ran into this issue and the workaround I came up with was to create a new block_content variable based on a filtered version of the main content variable excluding any custom fields I want to render manually:

{% set block_content = content|without('field_mycustomfield', 'field_mycustomfield2') %}

Then instead of rendering the "content.body" variable directly later, I call:

{{ block_content }}

If you want to render every field individually, you can just keep adding them to the "without" filter so that rendering block_content doesn't do anything except fix caching.

2

For anyone else who finds this...

The reason that rendering content (or content|without()) works is that there is an element in the render array content['#cache'] that contains all of the cacheable metadata for the content.

If you do not allow this to be rendered in twig, either content or {{'#cache': content['#cache']|render }}, the page does not know it has cacheable metadata (eg. it never bubbles up).

Sounds like you are not doing custom twig though. If you are using something like Varnish, that could also be a culprit for anonymous users.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.