Jak poprawnie skonfigurować buforowanie mojego bloku niestandardowego wyświetlającego zawartość w zależności od bieżącego węzła?

19

Mam ten bardzo podstawowy blok, który pokazuje tylko bieżący identyfikator węzła.

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

}

Ale po buforowaniu blok pozostaje taki sam, niezależnie od tego, który węzeł odwiedzam. Jak poprawnie buforować wynik według identyfikatora węzła?

Alex
źródło
1
Spójrz na getCacheTags()BlockBase, wystarczy dodać tag reprezentujący twój węzeł (node: {nid}). Przepraszam, spieszy mi się, mogę później wyjaśnić lepiej
Vagner,

Odpowiedzi:

31

To jest pełny działający kod z komentarzami.

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

Przetestowałem to; to działa.

Wystarczy umieścić kod w pliku o nazwie NodeCachedBlock.php w folderze modułu, zmienić jego przestrzeń nazw {nazwa_modułu}, wyczyścić pamięć podręczną i użyć go.

Vagner
źródło
więc sztuczka polega na usunięciu #cacheustawień w funkcji kompilacji i dodaniu funkcji publicznych?
Alex
3
To nie ma znaczenia, gdzie ustawisz tagi i konteksty pamięci podręcznej.
4k4
Cóż, myślę, że to ma większy sens, ponieważ budujemy blok, więc blok musi być buforowany. Jeśli zmienisz swój blok w przyszłości (tj. Umieścisz dodatkowe elementy renderujące), twój blok będzie działał.
Vagner
@ 4k4 url.path też chyba działało. co za różnica?
Alex
2
@Vagner: Umieszczenie tagów / kontekstów pamięci podręcznej w tablicy renderowania nie jest złym pomysłem, ponieważ masz je tam, gdzie są twoje dane, które zależą od tego. I zawsze będzie się bulgotał, więc nie musisz się martwić o elementy powyżej. Btw. Twój kod jest świetny, bardzo dobrze wyjaśnia problemy z buforowaniem.
4k4
13

Zdecydowanie najłatwiejszym sposobem na to jest poleganie na systemie kontekstowym wtyczki / bloku.

Zobacz moją odpowiedź na temat Jak utworzyć blok, który pobiera bieżącą zawartość węzła?

Musisz tylko umieścić definicję kontekstu węzła w adnotacji bloku w następujący sposób:

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

A następnie użyj go w następujący sposób: $this->getContextValue('node')

Zaletą tego jest to, że Drupal zajmie się dla ciebie buforowaniem. Automatycznie. Ponieważ wie, że domyślnym (i jeśli chodzi o rdzeń) kontekstem węzła jest bieżący węzeł. I to wie, skąd pochodzi, więc kontekst pamięci podręcznej i tagi pamięci podręcznej są dodawane automatycznie.

Poprzez \Drupal\Core\Plugin\ContextAwarePluginBase::getCacheContexts()i odpowiednie getCacheTags()metody BlockBase / twoja klasa bloków rozszerza się i dziedziczy te metody.

Berdir
źródło
Wymieniając \Drupal::routeMatch()->getParameter('node')z $this->getContextValue('node')i rozwiązać cały problem buforowania z jednej linii kodu? Świetny!
4k4
1
dzięki do tej pory! czy możesz podać pełny przykład kodu?
Alex
@Alex: Zredagowałem twoje pytanie. Sprawdź i zmień kod, jeśli znajdziesz jakiś błąd.
4k4
@ 4k4 Nie wypróbowałem tego, ponieważ działa też inne rozwiązanie
Alex
@Alex - Przykład pełnego kodu: drupal.stackexchange.com/a/205155/15055
leymannx
7

Jeśli wywodzisz się z klasy wtyczki blokowej Drupal\Core\Block\BlockBase, będziesz mieć dwie metody ustawiania tagów pamięci podręcznej i kontekstów.

  • getCacheTags()
  • getCacheContexts()

Na przykład blok modułu Book implementuje te metody w następujący sposób.

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

Blok modułu Forum używa następującego kodu.

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

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

W twoim przypadku użyłbym następującego kodu.

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

Możesz także użyć poniższej metody, aby blok był w ogóle nieusuwalny (nawet gdybym tego uniknął). Może to może być przydatne w innych przypadkach.

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

Pamiętaj, aby dodać use Drupal\Core\Cache\Cache;na górze pliku, jeśli zamierzasz korzystać z Cacheklasy.

kiamlaluno
źródło
dzięki, ale na / node / 2 blok nadal wyświetla 1, kiedy odwiedziłem węzeł / 1, po wyczyszczeniu mojej pamięci podręcznej
Alex
2
Jeśli edytujesz moduł, który jest włączony, musisz go najpierw odinstalować przed edycją. Wyczyszczenie pamięci podręcznej nie wystarczy.
kiamlaluno
okej, ale dodanie maxAge 0 działa, co dziwne!
Alex
Czy twoja klasa blokowa używa tej BlockBaseklasy jako klasy nadrzędnej?
kiamlaluno
tak, używa go
Alex
3

Podczas budowania tablicy renderowania zawsze dołączaj poprawne metadane:

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

Nie jest to specyficzne dla bloku, a metody zależności buforowania wtyczek blokowych getCacheTags (), getCacheContext () i getCacheMaxAge () nie są zamiennikami. Powinny być używane tylko do dodatkowych metadanych pamięci podręcznej, których nie można dostarczyć przez tablicę renderującą.

Zobacz dokumentację:

„Jest niezwykle ważne, aby poinformować interfejs API renderowania o możliwości buforowania tablicy renderowania”.

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

Zobacz ten przykład, w jaki sposób Drupal spodziewa się, że tablica renderująca zapewni niezbędne metadane pamięci podręcznej podczas optymalizacji buforowania poprzez automatyczne zastępowanie i budowanie z opóźnieniem Problem z ustawieniem specyficznych dla użytkownika znaczników pamięci podręcznej w niestandardowym bloku z kontekstem użytkownika

4k4
źródło
Nie sądzę, że można ustawić pamięć podręczną obiektu Block. „#markup” to tylko obiekt Render Element i nie ma powodu, aby ustawiać kontekst pamięci podręcznej lub znacznik. Obiekt bloku, który musi zostać odbudowany, gdy pamięć podręczna jest nieważna.
Vagner
#markupmoże być buforowany tak samo jak każdy inny element renderujący. W tym przypadku nie jest to znacznik, ale blok, który jest buforowany i tutaj jest problem. Nie można go rozwiązać za pomocą tagów pamięci podręcznej, ponieważ są unieważniane tylko wtedy, gdy węzeł zostanie zmieniony w bazie danych.
4k4
@Vagner Możesz ustawić pamięć podręczną obiektu Block; BlockBaseKlasa ma jeszcze niezbędnych metod.
kiamlaluno
1
Dla mnie return [ '#markup' => render($output), '#cache' => [ 'contexts' => ['url'] ] ];działa bardzo dobrze dla buforowania adresów URL.
leymannx
1
Tak, @leymannx, to takie proste. Te wątki wydają się przesadzać z problemem.
4k4
0

Problem polega na tym, że konteksty pamięci podręcznej nie są deklarowane we właściwym miejscu w funkcji kompilacji:

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

Jeśli wywołasz ten blok w innym węźle, funkcja kompilacji zwróci pustą tablicę, więc nie ma kontekstu pamięci podręcznej dla tego bloku, a to zachowanie zostanie buforowane przez drupal: wyświetlanie tego bloku nie zostanie poprawnie unieważnione lub wyrenderowane.

Rozwiązaniem jest po prostu zainicjowanie kompilacji $ za pomocą kontekstów pamięci podręcznej za każdym razem:

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;
 }
}
Podstęp
źródło
0

Zdaję sobie sprawę, że spóźniłem się na tę rozmowę, ale poniższy kod działał dla mnie:

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
źródło
lepiej późno niż wcale :)
Alex
0

Czy próbowałeś wdrożyć hook_block_view_BASE_BLOCK_ID_alter?

funkcja hook_block_view_BASE_BLOCK_ID_alter (tablica i $ build, \ Drupal \ Core \ Block \ BlockPluginInterface $ block) {$ build ['# cache'] ['max-age'] = 0; }

Bolleddula Sambasiva Rao
źródło