¿Cómo configuro correctamente el almacenamiento en caché para mi bloque personalizado que muestra contenido dependiendo del nodo actual?

19

Tengo este bloque muy básico que solo muestra la ID del nodo actual.

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

}

Pero una vez en caché, el bloque permanece igual, independientemente del nodo que visite. ¿Cómo guardo correctamente en caché el resultado por ID de nodo?

Alex
fuente
1
Mire getCacheTags()desde BlockBase, solo necesita agregar una etiqueta que represente su nodo (nodo: {nid}). Lo siento, tengo prisa ahora, puedo explicarlo mejor más tarde,
Vagner

Respuestas:

31

Este es un código de trabajo completo con comentarios.

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

Lo probé; funciona.

Simplemente coloque el código en un archivo llamado NodeCachedBlock.php en la carpeta de su módulo, cambie su espacio de nombres {module_name}, borre el caché y úselo.

Vagner
fuente
¿Entonces el truco es eliminar la #cacheconfiguración de la función de compilación y simplemente agregar las funciones públicas?
Alex
3
Eso no importa dónde establezca las etiquetas de caché y los contextos.
4k4
Bueno, creo que eso tiene más sentido, porque estamos construyendo un bloque, por lo que el bloque debe almacenarse en caché. Si cambia su bloque en el futuro (es decir, coloca algunos elementos de renderización adicionales), su bloque funcionará.
Vagner
@ 4k4 url.path parecía haber funcionado también. ¿cual es la diferencia?
Alex
2
@Vagner: poner etiquetas / contextos de caché en la matriz de renderizado tampoco es una mala idea, porque lo tienes donde están tus datos, que dependen de ellos. Y siempre burbujeará, por lo que no tiene que preocuparse por los elementos que se encuentran arriba. Por cierto. su código es excelente, explica muy bien los problemas de almacenamiento en caché.
4k4
13

La forma más fácil de hacerlo es confiar en el sistema de contexto de complemento / bloque.

Vea mi respuesta para ¿Cómo hago un bloque que extrae el contenido del nodo actual?

Solo tiene que poner una definición de contexto de nodo en su anotación de bloque como esta:

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

Y luego úsalo así: $this->getContextValue('node')

Lo bueno de esto es que Drupal se encargará del almacenamiento en caché por usted. Automáticamente. Porque sabe que el contexto de nodo predeterminado (y en lo que respecta al núcleo únicamente) es el nodo actual. Y eso sabe de dónde viene, por lo que el contexto de caché y las etiquetas de caché se agregan automáticamente.

Mediante \Drupal\Core\Plugin\ContextAwarePluginBase::getCacheContexts()y los getCacheTags()métodos correspondientes , BlockBase / su clase de bloque se extiende a partir de eso y hereda esos métodos.

Berdir
fuente
¿Reemplaza \Drupal::routeMatch()->getParameter('node')con $this->getContextValue('node')y resuelve todo el problema de almacenamiento en caché con una línea de código? ¡Excelente!
4k4
1
gracias hasta ahora! ¿podría proporcionar un ejemplo de código completo?
Alex
@ Alex: edité tu pregunta. Verifique y cambie el código si encuentra algún error.
4k4
@ 4k4 No lo probé porque la otra solución también funciona
Alex
@Alex - Ejemplo de código completo: drupal.stackexchange.com/a/205155/15055
leymannx
7

Si deriva la clase de su complemento de bloque Drupal\Core\Block\BlockBase, tendrá dos métodos para establecer etiquetas y contextos de caché.

  • getCacheTags()
  • getCacheContexts()

Por ejemplo, el bloque del módulo Libro implementa esos métodos de la siguiente manera.

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

El bloque del módulo Forum utiliza el siguiente 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']);
  }

En su caso, usaría el siguiente código.

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

También podría usar el siguiente método para hacer que el bloque no se pueda almacenar en caché (incluso si lo evitaría). Podría ser útil en otros casos, tal vez.

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

Recuerda agregar use Drupal\Core\Cache\Cache; en la parte superior del archivo, si va a usar la Cacheclase.

kiamlaluno
fuente
gracias, pero en / nodo / 2, el bloque aún genera 1 cuando visité nodo / 1 en primer lugar, después de borrar mi caché
Alex
2
Si está editando un módulo que está habilitado, primero debe desinstalarlo, antes de editarlo. Borrar el caché no es suficiente.
kiamlaluno
Está bien, pero agregar el maxAge 0 funciona, ¡extrañamente!
Alex
Además, ¿su clase de bloque usa la BlockBaseclase como clase principal?
kiamlaluno
sí, lo usa
Alex
3

Cuando crea una matriz de renderizado, siempre adjunte los metadatos correctos:

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

Esto no es específico del bloque y los métodos de dependencia de caché de complementos de bloque getCacheTags (), getCacheContext () y getCacheMaxAge () no son un reemplazo. Solo deben usarse para metadatos de caché adicionales, que no se pueden entregar a través de la matriz de representación.

Ver la documentación:

"Es de suma importancia que informe a la API de Render de la capacidad de almacenamiento en caché de una matriz de render"

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

Vea este ejemplo de cómo Drupal espera que una matriz de renderización proporcione los metadatos de caché necesarios al optimizar el almacenamiento en caché mediante la colocación automática de marcadores de posición y la creación diferida. Problema al configurar etiquetas de caché específicas del usuario en un bloque personalizado con el contexto del usuario

4k4
fuente
No creo que eso pueda establecer el caché del objeto Block. '#markup' es solo un objeto Render Element y no hay ninguna razón para establecer el contexto o la etiqueta de la memoria caché. El objeto de bloque que necesita reconstruirse cuando la memoria caché está invalidada.
Vagner
#markupse puede almacenar en caché igual que cualquier otro elemento de representación. En este caso, no es el marcado, sino el bloque, que se almacena en caché y aquí está el problema. No puede resolverlo con etiquetas de caché, porque solo se invalidan si el nodo se cambia en la base de datos.
4k4
@Vagner Puede configurar el caché de un objeto Block; La BlockBaseclase tiene incluso los métodos necesarios.
kiamlaluno
1
Para mí return [ '#markup' => render($output), '#cache' => [ 'contexts' => ['url'] ] ];funciona muy bien para el almacenamiento en caché por URL.
leymannx
1
Sí, @leymannx, es tan simple como esto. Este hilo parece pensar demasiado el problema.
4k4
0

El problema aquí es que los contextos de caché no se declaran en el lugar correcto en la función de compilación:

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

Si llama a ese bloque en un no nodo, la función de construcción devuelve una matriz vacía, por lo que no hay contexto de caché para este bloque y ese comportamiento será almacenado en caché por drupal: la visualización de este bloque no se invalidará o representará correctamente.

La solución es simplemente inicializar $ build con los contextos de caché cada vez:

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;
 }
}
Engaño
fuente
0

Me doy cuenta de que llego tarde a esta conversación, pero el siguiente código funcionó para mí:

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
fuente
mejor tarde que nunca :)
Alex
0

¿Has intentado implementar hook_block_view_BASE_BLOCK_ID_alter?

función hook_block_view_BASE_BLOCK_ID_alter (array & $ build, \ Drupal \ Core \ Block \ BlockPluginInterface $ block) {$ build ['# cache'] ['max-age'] = 0; }

Bolleddula Sambasiva Rao
fuente