Como configuro corretamente o cache do meu bloco personalizado que mostra o conteúdo, dependendo do nó atual?

19

Eu tenho esse bloco muito básico que apenas mostra o ID do nó atual.

<?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']);
  }

}

Porém, uma vez armazenado em cache, o bloco permanece o mesmo, independentemente de qual nó eu visito. Como faço para armazenar em cache corretamente o resultado por ID do nó?

Alex
fonte
1
Olhe no getCacheTags()BlockBase, você só precisa adicionar uma tag que representa seu nó (nó: {nid}). Desculpe, eu estou com pressa agora, eu posso explicar melhor mais tarde,
Vagner

Respostas:

31

Este é um código de trabalho completo com comentários.

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'));
  }
}

Eu testei; funciona.

Basta colocar o código em um arquivo chamado NodeCachedBlock.php na pasta do módulo, alterar o espaço para nome {module_name}, limpar o cache e usá-lo.

Vagner
fonte
então o truque é remover as #cacheconfigurações na função de compilação e apenas adicionar as funções públicas?
Alex
3
Não importa onde você define as tags e os contextos de cache.
4k4 28/04
Bem, acho que faz mais sentido, porque estamos construindo um bloco, portanto, o bloco precisa ser armazenado em cache. Se você alterar seu bloco no futuro (por exemplo, colocar alguns elementos extras de renderização), seu bloco funcionará.
precisa
@ 4k4 url.path parecia ter funcionado também. qual é a diferença?
Alex
2
@ Wagner: Colocar tags / contextos de cache no array de renderização também não é uma má idéia, porque você os tem onde estão seus dados, que dependem deles. E sempre borbulha, para que você não precise se preocupar com os elementos que estão acima. Btw. seu código é ótimo, explica muito bem os problemas de armazenamento em cache.
4k4 28/04
13

A maneira mais fácil de fazer isso é confiar no sistema de contexto de plug-in / bloco.

Veja minha resposta em Como faço para criar um bloco que puxa o conteúdo atual do nó?

Você só precisa colocar uma definição de contexto de nó na anotação de bloco, como segue:

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

E então use-o assim: $this->getContextValue('node')

O bom disso é que o Drupal cuidará do cache para você. Automaticamente. Porque ele sabe que o contexto do nó padrão (e no que diz respeito apenas ao núcleo) é o nó atual. E como sabe de onde vem, o contexto e as tags de cache são adicionados automaticamente.

Através \Drupal\Core\Plugin\ContextAwarePluginBase::getCacheContexts()dos getCacheTags()métodos correspondentes , BlockBase / sua classe de bloco se estende disso e herda esses métodos.

Berdir
fonte
Você substituir \Drupal::routeMatch()->getParameter('node')com $this->getContextValue('node')e você resolver todo o problema de cache com uma linha de código? Ótimo!
4k4 29/04
1
obrigado até agora! você poderia fornecer um exemplo de código completo?
Alex
@ Alex: eu editei sua pergunta. Verifique e altere o código se encontrar algum erro.
4k4 29/04
@ 4k4 eu não experimentá-lo, porque as outras obras da solução, também
Alex
@Alex - Exemplo de código completo: drupal.stackexchange.com/a/205155/15055
leymannx
7

Se você derivar a classe do seu plug-in de bloco Drupal\Core\Block\BlockBase, você terá dois métodos para definir tags e contextos de cache.

  • getCacheTags()
  • getCacheContexts()

Por exemplo, o bloco do módulo Livro implementa esses métodos da seguinte maneira.

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

O bloco do módulo Fórum usa o seguinte código.

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

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

No seu caso, eu usaria o seguinte código.

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

Você também pode usar o método a seguir, para tornar o bloco inalcançável, mesmo que eu o evite. Pode ser útil em outros casos, talvez.

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

Lembre-se de adicionar use Drupal\Core\Cache\Cache;na parte superior do arquivo, se você for usar a Cacheclasse.

kiamlaluno
fonte
obrigado, mas em / node / 2 o bloco ainda produz 1 quando visitei node / 1, em primeiro lugar, depois de limpar o cache
Alex
2
Se você estiver editando um módulo que está ativado, primeiro desinstale-o antes de editá-lo. Limpar o cache não é suficiente.
Kiamlaluno
ok, mas adicionar o maxAge 0 funciona, estranhamente!
Alex
Além disso, sua classe de bloco usa a BlockBaseclasse como classe pai?
Kievlaluno
sim que não usá-lo
Alex
3

Ao criar uma matriz de renderização, sempre anexe os metadados corretos:

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'],
  ],
];

Isso não é específico de um bloco e os métodos de dependência de cache de plug-ins de bloco getCacheTags (), getCacheContext () e getCacheMaxAge () não são um substituto. Eles devem ser usados ​​apenas para metadados de cache adicionais, que não podem ser entregues através da matriz de renderização.

Veja a documentação:

"É da maior importância que você informe a API Render da capacidade de cache de uma matriz de renderização".

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

Veja este exemplo de como o Drupal espera que uma matriz de renderização forneça os metadados de cache necessários ao otimizar o armazenamento em cache por meio de reserva automática e construção lenta Problema ao definir tags de cache específicas do usuário no bloco personalizado com o contexto do usuário

4k4
fonte
Eu não acho que isso pode definir o cache do objeto Block. '#markup' é apenas um objeto Render Element e não há motivo para definir o contexto ou a tag do cache. O objeto de bloco que precisa ser reconstruído quando o cache é inválido.
Vagner
#markuppode ser armazenado em cache da mesma forma que qualquer outro elemento de renderização. Nesse caso, não é a marcação, mas o bloco, que é armazenado em cache e aqui está o problema. Você não pode resolvê-lo com tags de cache, porque elas são invalidadas apenas se o nó for alterado no banco de dados.
4k4 28/04
@Vagner Você pode definir o cache de um objeto Block; A BlockBaseclasse possui até os métodos necessários.
Kiamlaluno
1
Para mim, return [ '#markup' => render($output), '#cache' => [ 'contexts' => ['url'] ] ];funciona super bem para o cache por URL.
Leymannx #
1
Sim, @leymannx, é tão simples quanto isso. Esta discussão parece exagerar na questão.
4k4 14/11
0

O problema aqui é que os contextos de cache não são declarados no lugar certo na função de construção:

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;
 }
}

Se você chamar esse bloco em um não nó, a função de compilação retornará uma matriz vazia, para que não haja contexto de cache para esse bloco e esse comportamento será armazenado em cache por drupal: a exibição desse bloco não invalidará ou será renderizada corretamente.

A solução é apenas inicializar o $ build com os contextos de cache sempre:

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;
 }
}
Astúcia
fonte
0

Percebo que estou atrasado para esta conversa, mas o código abaixo funcionou para mim:

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,
    ));
  }
}
Eddie Fann
fonte
melhor tarde que nunca :)
Alex
0

Você já tentou implementar hook_block_view_BASE_BLOCK_ID_alter?

função hook_block_view_BASE_BLOCK_ID_alter (matriz & $ build, \ Drupal \ Core \ Block \ BlockPluginInterface $ block) {$ build ['# cache'] ['max-age'] = 0; }

Bolleddula Sambasiva Rao
fonte