Как правильно настроить кэширование для моего пользовательского блока, отображающего содержимое в зависимости от текущего узла?

19

У меня есть очень простой блок, который просто показывает идентификатор текущего узла.

<?php

/**
 * @file
 * Contains \Drupal\mymodule\Plugin\Block\ExampleEmptyBlock.
 */

namespace Drupal\mymodule\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;

/**
 * @Block(
 *   id = "example_empty",
 *   admin_label = @Translation("Example: empty block")
 * )
 */
class ExampleEmptyBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
    $node = \Drupal::routeMatch()->getParameter('node');
    $build = array();

    if ($node) {
      $config = \Drupal::config('system.site');

      $build = array(
        '#type' => 'markup',
        '#markup' => '<p>' . $node->id() . '<p>',
        '#cache' => array(
          'tags' => $this->getCacheTags(),
          'contexts' => $this->getCacheContexts(),
        ),
      );
    }

    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    $node = \Drupal::routeMatch()->getParameter('node');
    return Cache::mergeTags(parent::getCacheTags(), ["node:{$node->id()}"]);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    return Cache::mergeContexts(parent::getCacheContexts(), ['user.node_grants:view']);
  }

}

Но после кэширования блок остается неизменным независимо от того, какой узел я посещаю. Как правильно кешировать результат по идентификатору узла?

Alex
источник
1
Посмотрите на getCacheTags()BlockBase, вам просто нужно добавить тег, который представляет ваш узел (node: {nid}). Извините, я сейчас тороплюсь, позже объясню лучше,
Вагнер,

Ответы:

31

Это полный рабочий код с комментариями.

namespace Drupal\module_name\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;

/**
 * Provides a Node cached block that display node's ID.
 *
 * @Block(
 *   id = "node_cached_block",
 *   admin_label = @Translation("Node Cached")
 * )
 */
class NodeCachedBlock extends BlockBase {
  public function build() {
    $build = array();
    //if node is found from routeMatch create a markup with node ID's.
    if ($node = \Drupal::routeMatch()->getParameter('node')) {
      $build['node_id'] = array(
        '#markup' => '<p>' . $node->id() . '<p>',
      );
    }
    return $build;
  }

  public function getCacheTags() {
    //With this when your node change your block will rebuild
    if ($node = \Drupal::routeMatch()->getParameter('node')) {
      //if there is node add its cachetag
      return Cache::mergeTags(parent::getCacheTags(), array('node:' . $node->id()));
    } else {
      //Return default tags instead.
      return parent::getCacheTags();
    }
  }

  public function getCacheContexts() {
    //if you depends on \Drupal::routeMatch()
    //you must set context of this block with 'route' context tag.
    //Every new route this block will rebuild
    return Cache::mergeContexts(parent::getCacheContexts(), array('route'));
  }
}

Я проверил это; оно работает.

Просто поместите код в файл с именем NodeCachedBlock.php в папке вашего модуля, измените его пространство имен {имя_модуля}, очистите кеш и используйте его.

Vagner
источник
так хитрость в том, чтобы удалить #cacheнастройки в функции сборки и просто добавить публичные функции?
Алекс
3
Неважно, где вы устанавливаете теги и контексты кеша.
4k4
Ну, я думаю, что это имеет больше смысла, потому что мы строим блок, поэтому блок должен быть кэширован. Если вы измените свой блок в будущем (то есть добавите несколько дополнительных элементов рендеринга), ваш блок будет работать.
Вагнер
@ 4k4 url.path, похоже, тоже сработало. какая разница?
Алекс
2
@Vagner: Размещение тегов / контекстов кеша в массиве рендеринга также неплохая идея, потому что вы располагаете там, где находятся ваши данные, которые зависят от них. И он всегда будет пузыриться, так что вам не придется беспокоиться о вышеперечисленных элементах. Btw. Ваш код великолепен, действительно хорошо объясняет проблемы кеширования.
4k4
13

Самым простым способом сделать это является использование системы контекста плагин / блок.

Смотрите мой ответ для Как сделать блок, который извлекает содержимое текущего узла?

Вам просто нужно поместить определение контекста узла в аннотацию вашего блока следующим образом:

*   context = {
*     "node" = @ContextDefinition("entity:node", label = @Translation("Node"))
*   }

И затем используйте это так: $this->getContextValue('node')

Приятно то, что Drupal позаботится о кешировании для вас. Автоматически. Потому что он знает, что по умолчанию (и насколько ядро ​​идет только) контекст узла является текущим узлом. И он знает, откуда он, поэтому контекст кеша и теги кеша добавляются автоматически.

Через \Drupal\Core\Plugin\ContextAwarePluginBase::getCacheContexts()и соответствующие getCacheTags()методы BlockBase / ваш класс блоков расширяется от этого и наследует эти методы.

Berdir
источник
Вы заменяете \Drupal::routeMatch()->getParameter('node')на $this->getContextValue('node')и решаете всю проблему кеширования одной строкой кода? Большой!
4k4
1
пока спасибо! Не могли бы вы предоставить полный пример кода?
Алекс
@ Алекс: Я редактировал твой вопрос. Пожалуйста, проверьте и измените код, если вы обнаружите какую-либо ошибку.
4k4
@ 4k4 Я не пробовал, потому что другое решение тоже работает
Alex
@Alex - Пример полного кода: drupal.stackexchange.com/a/205155/15055
leymannx
7

Если вы наследуете класс вашего блочного плагина Drupal\Core\Block\BlockBase, у вас будет два метода для установки тегов и контекстов кеша.

  • getCacheTags()
  • getCacheContexts()

Например, блок модуля Book реализует эти методы следующим образом.

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    return Cache::mergeContexts(parent::getCacheContexts(), ['route.book_navigation']);
  }

Блок модуля Форум использует следующий код.

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    return Cache::mergeContexts(parent::getCacheContexts(), ['user.node_grants:view']);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    return Cache::mergeTags(parent::getCacheTags(), ['node_list']);
  }

В вашем случае я бы использовал следующий код.

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    $node = \Drupal::routeMatch()->getParameter('node');
    return Cache::mergeTags(parent::getCacheTags(), ["node:{$node->id()}"]);
  }

Вы также можете использовать следующий метод, чтобы сделать блок вообще не кэшируемым (даже если бы я этого избегал). Это может быть полезно в других случаях, возможно.

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    return 0;
  }

Не забудьте добавить use Drupal\Core\Cache\Cache;в начало файла, если вы собираетесь использовать Cacheкласс.

киамалуно
источник
спасибо, но в / node / 2 блок по-прежнему выводит 1, когда я впервые посетил node / 1, после очистки моего кэша
Alex
2
Если вы редактируете модуль, который включен, вам необходимо сначала удалить его, прежде чем редактировать. Очистки кеша недостаточно.
kiamlaluno
хорошо, но добавление maxAge 0 работает, как ни странно!
Алекс
Кроме того, ваш класс блоков использует BlockBaseкласс в качестве родительского класса?
kiamlaluno
да, он использует это
Алекс
3

При создании массива рендеринга всегда прикрепляйте правильные метаданные:

use Drupal\Core\Cache\Cache;

$build['node_id'] = [
  '#markup' => '<p>' . $node->id() . '<p>',
  '#cache' => [
    'tags' => $node->getCacheTags(),
    // add a context if the node is dependent on the route match
    'contexts' => ['route'],
  ],
];

Это не относится к конкретным блокам, и методы зависимостей кэшей плагинов блоков getCacheTags (), getCacheContext () и getCacheMaxAge () не являются заменой. Их следует использовать только для дополнительных метаданных кэша, которые нельзя доставить через массив визуализации.

Смотрите документацию:

«Крайне важно сообщить API рендеринга о кешируемости массива рендеринга».

https://www.drupal.org/docs/8/api/render-api/cacheability-of-render-arrays

Посмотрите в этом примере, как Drupal ожидает, что массив рендеринга предоставит необходимые метаданные кэша при оптимизации кэширования с помощью автозаполнения и отложенного построения. Проблема установки пользовательских тегов кэша в пользовательском блоке в контексте пользователя

4k4
источник
Я не думаю, что может установить кэш объекта Block. «#markup» - это просто объект элемента рендеринга, и нет причин устанавливать контекст или тег кеша. Блочный объект, который необходимо перестроить, когда кеш недействителен.
Вагнер
#markupможно кэшировать так же, как любой другой элемент рендеринга. В данном случае это не разметка, а блок, который кэшируется, и здесь возникает проблема. Вы не можете решить это с тегами кэша, потому что они становятся недействительными, только если узел изменяется в базе данных.
4k4
@Vagner Вы можете установить кэш объекта Block; У BlockBaseкласса есть даже необходимые методы.
kiamlaluno
1
Для меня return [ '#markup' => render($output), '#cache' => [ 'contexts' => ['url'] ] ];работает очень хорошо для кеширования URL.
leymannx
1
Да, @leymannx, это так просто. Эта тема, кажется, переосмысливает проблему.
4k4
0

Проблема здесь в том, что контексты кэша не объявляются в нужном месте в функции сборки:

class ExampleEmptyBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
   $node = \Drupal::routeMatch()->getParameter('node');
   $build = array();

   if ($node) {
    $config = \Drupal::config('system.site');

    $build = array(
    '#type' => 'markup',
    '#markup' => '<p>' . $node->id() . '<p>',
    '#cache' => array(
      'tags' => $this->getCacheTags(),
      'contexts' => $this->getCacheContexts(),
    ),
   );
 }
 return $build;
 }
}

Если вы вызываете этот блок не на узле, функция сборки возвращает пустой массив, так что для этого блока нет контекста кэширования, и это поведение будет кэшироваться drupal: отображение этого блока не будет корректно аннулировано или визуализировано.

Решение состоит в том, чтобы каждый раз инициализировать $ build с контекстами кэша:

class ExampleEmptyBlock extends BlockBase {

  /**
   * {@inheritdoc}
   */
  public function build() {
   $node = \Drupal::routeMatch()->getParameter('node');

    $build = array(
    '#cache' => array(
      'tags' => $this->getCacheTags(),
      'contexts' => $this->getCacheContexts(),
    ),
   );

   if ($node) {
    $config = \Drupal::config('system.site');

    $build['#markup'] = '<p>' . $node->id() . '<p>';
    $build['#type'] = 'markup';
    }
 return $build;
 }
}
коварство
источник
0

Я понимаю, что опоздал на этот разговор, но приведенный ниже код работал для меня:

class ExampleBlock extends BlockBase
{

  public function build()
  {
    $lcContent = '';

    $loNode = \Drupal::routeMatch()->getParameter('node');

    if (!$loNode)
    {
      return (array(
        '#type' => 'markup',
        '#cache' => array('max-age' => 0),
        '#markup' => $lcContent,
      ));
    }

    $lcContent .= "<div id='example_block' style='overflow: hidden; clear: both;'>\n";
    $lcContent .= $loNode->id();
    $lcContent .= "</div>\n";

    return (array(
      '#type' => 'markup',
      '#cache' => array('max-age' => 0),
      '#markup' => $lcContent,
    ));
  }
}
Эдди Фанн
источник
лучше поздно, чем никогда :)
Алекс
0

Вы пытались реализовать hook_block_view_BASE_BLOCK_ID_alter?

function hook_block_view_BASE_BLOCK_ID_alter (массив & $ build, \ Drupal \ Core \ Block \ BlockPluginInterface $ block) {$ build ['# cache'] ['max-age'] = 0; }

Болледдула Самбасива Рао
источник