2

Goal:

Custom block with dynamic non-entity content that varies with the URL.

Specifically, the block has a form with select elements, and the selected items should be determined by the current URL path.

Problem:

The rendered block intermittently displays incorrectly, as though it was rendered for another URL different than the current URL.

Code:

Plugin:

<?php
/**
 * @file
 * Contains \Drupal\my_module\Plugin\Block\ViewsFiltersResources.
 */

namespace Drupal\my_module\Plugin\Block;

  use Drupal\Core\Block\BlockBase;
  use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
  use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a 'ViewsFiltersResources' block.
 *
 * @Block(
 *   id = "my_module_views_filters_resources",
 *   admin_label = @Translation("Resources: filters"),
 * )
 */
class ViewsFiltersResources extends BlockBase implements ContainerFactoryPluginInterface {

  /**
   * Constructs a new ViewsFiltersResources.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param array $plugin_definition
   *   The plugin implementation definition.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition
    );
  }

  public function getCacheContexts(){
    return ['url.path'];
  }

  /**
   * {@inheritdoc}
   */
  public function build() {

    $request_url = \Drupal::request()->getRequestUri();
    $url_parts = explode('/', $request_url);

    return array(
      '#theme'       => 'my_module_block_views_filters_resources',
      '#issue_areas' => array(
        'all' => 'All Issue Areas',
        'area-one' => 'Area One',
        'area-two' => 'Area Two',
        ),
      '#resource_types' => array(
        'all' => 'All Resource Types',
        'news_story' => 'News Stories',
        'press_release' => 'Press Releases',
      ),
      '#active_area' => isset($url_parts[2]) ? strtolower($url_parts[2]) : NULL,
      '#active_type' => isset($url_parts[3]) ? strtolower($url_parts[3]) : NULL,
      '#active_keywords' => isset($url_parts[4]) ? urldecode(str_replace('+', ' ', $url_parts[4])) : NULL,
      '#attached' => array(
        'library' =>  array(
          'my_module/my_module-resource-filters'
        )
      ),
    );
  }
}

Theme function:

function my_module_theme() {
  return array(
    'my_module_block_views_filters_resources' => array(
      'variables' => array('issue_areas' => NULL, 'resource_types' => NULL, 'active_area' => NULL, 'active_type' => NULL, 'active_keywords' => NULL),
      'template' => 'my-module-block-views-filters-resources',
    ),
  );
}

Template:

<section{{ attributes.addClass('filters resources-filters bg-light-80') }}>
  {{ title_prefix }}
  {{ title_suffix }}
  {% block content %}
    <div class="row">
      <div class="column">
        <form id="resources-filters-form" class="filters-form resources-filters-form clearfix views-exposed-form" method="get" accept-charset="UTF-8">
          <fieldset class="filters-fields resources-filters-fields">
            <label class="filters-label">Show Me</label>
            <div class="filters-issue-area filters-item form-item">
              <select class="filters-issue-area-select filters-select form-select" id="edit-area" name="area">
                {% for value, name in issue_areas %}
                  <option value="{{ value }}"{% if value == active_area %} selected="selected"{%endif %}>{{ name }}</option>
                {% endfor %}
              </select>
            </div>
            <div class="filters-resource-type filters-item form-item">
              <select class="filters-resource-type-select filters-select form-select" id="edit-type" name="type">
                {% for value, name in resource_types %}
                  <option value="{{ value }}"{% if value == active_type %} selected="selected"{%endif %}>{{ name }}</option>
                {% endfor %}
              </select>
            </div>
            <div class="filters-keywords filters-item form-item">
              <input type="text" class="filters-keywords-text filters-text form-text" id="edit-body-value" name="body_value" value="{{ active_keywords }}" maxlength="128" placeholder="Enter optional keywords...">
            </div>
          </fieldset>
          <div class="filters-actions form-actions" id="edit-actions">
            <input type="submit" class="button large filters-submit js-form-submit form-submit" id="edit-submit-resources" name="" value="Update">
          </div>
        </form>
      </div><!-- column -->
    </div><!-- row -->

  {% endblock %}
</section>

Questions:

  1. Any thoughts on what could cause incorrect caching here? Could this be related to https://www.drupal.org/node/2529980?
  2. Is using getCacheContexts() supposed to result in the desired behavior? What's the difference between using getCacheContexts() vs using '#cache' in the render array?
    I have searched and read the API docs on getCacheContexts(), but it's still not clear to me what exactly it does.
  3. If getCacheContexts() is appropriate for this purpose, is it used correctly here?
    In core, I've seen cases that look like this one, and other cases that merge a new value into the result of parent::getCacheContexts().

1 Answer 1

2

Answers

  1. No, your block is not empty

  2. #cache is bubbleable metadata the same as #attached. So if you use one of them you can use both.

  3. If you use getCacheContexts() then you extend the parent method and it's correct OOP to merge the result from the parent.

Merge contexts:

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

Question 2. + 3. in more details

If you build a render array then the straightforward thing to do is to attach the correct metadata, because the render array should not be restricted to a block. It should contain all metadata it needs to work in all places. On the other hand if you build a block plugin and you don't know what kind of render array it contains if any, then it's better to use getCacheContexts(). This is part of the cacheable interface all object have, that are cacheable in Drupal:

https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Cache%21CacheableDependencyInterface.php/interface/CacheableDependencyInterface/8.2.x

Debugging

Look in the database table cache_render. After clearing the cache as you visit pages with different urls there should be cache entries for the block that vary by the cache context [cache_context].

entity_view:block:block_name:[cache_context]=...
3
  • Thx for the detailed answer. In testing, I do have a separate entry in cache_render for the different URLs. However, there are still intermittent cases on prod where the block doesn't correctly render according to the URL. I guessed that it was a caching issue because it changes when I cr -- sometimes the problem disappears, sometimes it moves to another page.
    – kentr
    Mar 1, 2017 at 19:21
  • If you see the context in the cid's of the cache table your block seems to be OK. Debugging an intermittent case, that's difficult. Shouldn't be the internal page cache, because here the full url is used as cache key by default. More likely the dynamic page cache. Normally this cache respects the contexts, but you can try to disable this module for testing, to see if there is a problem on the way from the block up to the page level.
    – 4uk4
    Mar 1, 2017 at 19:49
  • Using #cache instead of getCacheContexts() and / or disabling the dynamic page cache appears to have solved the problem with incorrect block content. Makes me wonder if there's a bug in core.
    – kentr
    Mar 8, 2017 at 4:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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