0

I struggle with getting a custom block cached correctly:

  • we have a custom entity "Politician"
  • For that entity we have several routes where we want to render a "politician header" with some basic information about the politician
  • the politician header is a custom template which is rendered in a custom block
  • the problem is: when caching is activated the block always shows the politician data from the first politician which was rendered.

Here is the code for the block plugin - to keep it simple I do not copy the method getPoliticianFromUrl() here:

/**
 * Provides a block with the profile header for a politician. We do not render
 * it on the public edit form path
 *
 * @Block(
 *   id = "aw_frontend_politician_header",
 *   admin_label = @Translation("Politician profile header"),
 *   category = "Abgeordnetenwatch"
 * )
 */
class PoliticianProfileHeader extends BlockBase {
  /**
   * {@inheritdoc}
   */
  public function build() {
    $politician = $this->getPoliticianFromUrl();
    if ($politician == NULL) {
      return [];
    }

    return array(
      '#theme' => 'politician_header',
      '#politician' => $politician,
      '#cache' => [
        'keys' => ['politician', $politician->id()]
      ]
    );
  }

  public function getCacheContexts() {
    $politician = $this->getPoliticianFromUrl();
    if ($politician != NULL) {
      return Cache::mergeContexts(parent::getCacheContexts(), $politician->getCacheContexts());
    }
    return parent::getCacheContexts();
  }

  public function getCacheTags() {
    $politician = $this->getPoliticianFromUrl();
    if ($politician != NULL) {
      return Cache::mergeTags(parent::getCacheTags(), $politician->getCacheTags());
    }
    return parent::getCacheTags();
  }
}

Then I render the politician header - again I just put in interesting parts:

function template_preprocess_politician_header(&$variables) {
  /** @var \Drupal\pw_basic\Entity\Politician $politician */
  $politician = $variables['politician'];
  /** @var \Drupal\Core\Render\Renderer $renderer */
  $renderer = \Drupal::service('renderer');
  $renderer->addCacheableDependency($variables, $politician);
  $variables["#cache"]["contexts"][] = 'user.roles';
  $variables['#cache']['keys'] = ['politician_header', $politician->id()];
}

I thought when I define cache keys this ensures that different versions of the render array are cached. This does not happen - not for the custom politician header template (for which I do not find any cached data in cache_render tanle - is this just cached with Twig caching?) and not for the block.

My idea was to create a custom cache context based on my Politician entity but as far as I understand cache contexts they depend on more global circumstances such as the active theme, the current user and things like that. I hoped the Entity API would generate such a context in a magical way so I tried $politician->getCacheContexts() but nothing.

What do I miss?

  • The key is the function you didn't show, getPoliticianFromUrl(), but judging based on the function name you have a cache dependency on the current URL, which means you need a cache context url.path. – 4k4 Jan 17 '20 at 15:28
  • Using "key" in the previous comment is a bit misleading, this was meant as key point, not cache key, which by the way you don't need at all, because the block has already cache keys and each time you add cache keys you get another cache entry, so this results in three cache entries which contain basically the same content. – 4k4 Jan 17 '20 at 16:25
  • No, getPoliticianFromUrl() is not the key point: the block is used on 5 or 6 different urls but the content of the block keeps the same, it depends on the poltician, not the url. So the cache context should be the politician otherwise we have 5 or 6 versions of the block with the same content. With my cache keys "politician" und "$politician->id()" I thought I would get one version of the block for each politician. I will try "politician_123" (123 = id) as a key as this is what I think I need?! But as said: I defined the keys above and they are NOT used for the cid of the block – Tobias Krause Jan 18 '20 at 6:48
  • Yes, this is what I was trying to point out, the cache keys you've set are not contexts but store extra cache entries additional to the block. As context you can use a core context like url.path or route (if you retrieve the entity from a route parameter) or, to make caching more efficient, create a custom context, but for this to decide you need to know what getPoliticianFromUrl() is doing. – 4k4 Jan 18 '20 at 9:10
  • ... and what amount of data we are talking about. Caching is not an exact science, it is always a trade-off between cache efficiency and code complexity. Don't over-optimize. – 4k4 Jan 18 '20 at 9:36
1

By any reason I do not really get the block rendering to respect my cache keys set wherever. It only respects set cache contexts so I create a custom cache context:

In pw_basic.services.yml (pw_basic is the name ofthe module)

  cache_context.politician:
    class: Drupal\pw_basic\Cache\PoliticianCacheContext
    arguments: ['@current_route_match']
    tags:
      - { name: cache.context }

The politician cache context class:

class PoliticianCacheContext implements CacheContextInterface {

  use GetPoliticianFromUrlTrait;

  /**
   * @var \Drupal\pw_basic\Entity\Politician
   */
  protected $politician;

  public function __construct(CurrentRouteMatch $current_route_match) {
    $this->politician = $this->getPoliticianFromUrl();
  }


  /**
   * {@inheritdoc}
   */
  public static function getLabel() {
    return t('Politician');
  }

  /**
   * {@inheritdoc}
   */
  public function getContext() {
    $cache_context = '';
    if ($this->politician !== NULL) {
      $cache_context = $this->politician->id();
    }
    return $cache_context;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheableMetadata() {
    return new CacheableMetadata();
  }
}

Then I cleared the block plugin class from any cache logic and just added the following in template_preprocess_politician_header(), the preprocess function of the template rendered within the block:

$variables["#cache"]["contexts"][] = 'politician';

What I do not really like is the service character of the cache context classes. Why is it only possible to set the context in such a global way? Do I miss somethin? I need to use this global workaround and check for the politician from URL which makes this context not as flexible as I want it to be. Is there a way to give a variable to a cache context directly when defining it in a render array?

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.