Drupal 8, Cache & Rock N' Roll
21.06.16 12:49

Вообще, мне бы самому хотелось послушать про кэширование в 8-ке. Но что-то не особо говорят. Вот и решил подсобрать инфы, чтобы подогреть интерес. Сразу скажу, что если не пишешь собственный модуль, то всё это не так и нужно. Но не зря же я втыкал в Word (вместо видосов с котами), так что все читайте, уж будьте бобры :)

Ну и чо там?

Из Symfony в Drupal 8 привита всеобщая кэшируемость. Теперь для любого объекта, который участвует в рендеринге, можно задать свои параметры кэширования.

Выглядит так:

'#cache' => [
    'keys' => ['entity_view', 'node', 5, 'teaser'],
    'contexts' => ['languages', 'timezone'],
    'tags' => ["node:5", 'user:3'],
    'max-age' => Cache::PERMANENT,
  ],

Всего 4 параметра (указывать все необязательно):

keys – массив значений, из которых будет сформировано название для кэша (ID). Типичный набора:
'keys' => array( 'entity_view',  $this->entityTypeId,  $entity->id(),  $view_mode)

contexts –зависимость от эм.. контекста, например языка и временной зоны. Для тех, кто сечет в HTTP заголовках, в качестве его аналогии приводят Vary. Еще примеры возможных значений: cookies, route, session, url, user и т.д. Можно задавать более конкретную привязку, например user.permissions – зависимость только от прав пользователя, или url.query_args:foo – зависимость от параметра foo в адресной строке запроса.При их обработке происходит оптимизация. Например:
[user, user.permissions] => [user], т.к. зависимость от user уже включает в себя user.permissions.

tags – зависимость от конкретных объектов, например от 5-ой ноды, или пользователя с id = 3. Интересно, что можно задавать значения типа config:block_list (зависимость от конфигурации блоков), или config:filter.format.basic_html. А еще node_list.
В значениях тега не должно быть пробелов, т.к. пробел используются в качестве разделителя при передаче через header (странно, конечно, что выбран пробел, ну да не о том речь). Этот самый заголовок (X-Drupal-Cache-Tags) интересен для настройки супербыстрой раздачи через систему обратного проксирования (Varnish, CDN).

max-age – время хранения кэша в секундах (0 – не хранить, Cache::PERMANENT (-1) – без ограничения)

Не надо бла-бла, давай пример

Задача: разработать блок, который будет выводить значение параметра xxx переданного через адресную строку.

При рендеринге блока нет трудоемких задач, но его результат все равно будем кэшировать (иначе нафига этот пример). Чтобы убедиться, что кэш работает, кроме значения выведем еще и время, когда это значение было сохранено.


<?php

/**
 * @file
 * Contains \Drupal\cacheable_block\Plugin\Block\CacheableBlock.
 */

namespace Drupal\cacheable_block\Plugin\Block;

use 

Drupal;
use 
Drupal\Core\Block\BlockBase;
use 
Drupal\Core\Annotation\Translation;
use 
Drupal\Core\Routing;

/**
 * Provides a 'Cacheable Block' block.
 *
 * @Block(
 *   id = "cacheable_block",
 *   admin_label = @Translation("Cacheable block"),
 * )
 */
class CacheableBlock extends BlockBase {

  

/**
   * {@inheritdoc}
   */
  
public function build() {

    

$key 'xxx';
    
$message "";
    
$value Drupal::request()->query->get($key);

    if(

$value) {
      
$time date("Y-m-d H:i:s");
      
$vars = array('@value' => $value'@time' => $time);
      
$message $this->t('@value (@time)'$vars);
    }

    return array(
      

'#markup' => $message,
      
'#cache' => [
        
'contexts' => ["url.query_args:$key"],
      ],
    );
  }
}
?>

Пример работы:

my.site?xxx=Kate                   -> Kate  (2016-02-07 11:11:11)
my.site?xxx=Ann                    -> Ann  (2016-02-07 22:22:22)
my.site/node/666?xxx=Ann           -> Ann  (2016-02-07 22:22:22)
my.site?xxx=Kate                   -> Kate  (2016-02-07 11:11:11)

Т.е. если xxx повторяется – значение берется из кэша, независимо от времени и страницы, и наоборот, стоит xxx измениться, и блок выдает другое значение (которое тоже кэширует).

Правда если зайти под другим пользователем, то будет другой кэш, хоть никакой зависимости от пользователей и не указано. И так и не понял, как задать
'cache' => DRUPAL_CACHE_GLOBAL

Но, можно делать свой кэш с нужным значением и ключами:

<?php
$value 
Drupal::request()->query->get($key);
$message Drupal::cache()->get("cacheable_block-$key-$value")->data;
if(
$value && !$message) {
  
$time date("Y-m-d H:i:s");
  
$vars = array('@value' => $value'@time' => $time);
  
$message $this->t('@value (@time)'$vars);
}
Drupal::cache()->set("cacheable_block-$key-$value"$message);
?>

Помни! Если кто-то вобьёт мильярд разных вариантов xxx – ничем хорошим это не закончится. Прямая зависимость кэша от столь легко меняющейся (и бесконечновариантной) зависимости – крайне не рекомендуется.

Еще пример

В блоге Acquia Dev есть пример, в котором рассматривается конвертация изображения в asci-символы. Вот краткая выжимка:

Как тебе такое решение?

<?php
$build 
= array(
  
'#theme' => 'ascii_art',   
  
'#attributes' => array('class' => 'ascii-art'),   
  
'#caption' => 'My favorite animal',   
  
'content' => array(
    
'#markup' => generate_ascii_art('llama.png')
  ), 
);
?>

Здесь клёвая картинка ламы будет преобразована в ASCII символы. И, будь уверен, преобразование просто прекрасно! Но сейчас разговор не об искусстве ASCII. Динамическая генерация данных на основе изображения, разве это не медленно, спросишь ты. Ясен день! В конце-то концов, как следует преобразовать пушистую ламу в набор символов не так-то и просто. Нет, без шуток, можно придумать кучу случаев затратного рендеринга (требующего сложных вычислений и кучи запросов к бд). Так что кэширование результатов здорово бы помогло. И вот как это можно устроить:

<?php
$build 
= array(
  
'#theme' => 'ascii_art',
  
'#attributes' => array('class' => 'ascii-art'),
  
'#caption' => 'My favorite animal',
  
'#cache' => array(
    
'keys' => array('ascii-art''llama'),
  ),
  
'#pre_render' => array('ascii_art_pre_render'),
  
'#ascii_art_image' => 'llama.png',
);

function 

ascii_art_pre_render($build) {   
  
$build['content'] = generate_ascii_art($build['#ascii_art_image']);   
  return 
$build
}
?>

Что за магические значения?

Завязывать кэш на конкретные значения (5-ую ноду, 3-го пользователя) – это, конечно, не солидно. А главное, вряд ли нужные значения известны. Поэтому в реальности, конечно, всё описывается программно.

Например, нужно связать кэш со списком нод из какой-то выборки

<?php
$nids 
= [];
foreach (
$result as $row) {
    
$nids[] = $row->nid;
}
$cache_tags  Cache::buildTags('node'$nids));
'#cache' => array(
    
'keys' => $cache_tags,
),
?>

Яма, яма, яма, ямллл…

Кстати, простые настройки можно указывать и через YML

renderer.config:
    auto_placeholder_conditions:
      max-age: 0
      contexts: ['session', 'user']
      tags: []

Настройка через API:

$metadata = new CacheableMetadata();
$metadata->setCacheContexts(['qux'])
      ->setCacheTags(['foo:bar'])
      ->setCacheMaxAge(600);

Хочу свой рендеринг с кешем и зависимостями

Вот, пожалуйста:


<?php
$renderer 
= \Drupal::service('renderer');

$config = \Drupal::config('system.site');
$current_user = \Drupal::currentUser();

$build = [
  
'#prefix' => '<aside>',
  
'#markup' => t('Hi, %name, welcome back to @site!', [
    
'%name' => $current_user->getUsername(),
    
'@site' => $config->get('name'),
  ]),
  
'#suffix' => '</aside>',
  
'#cache' => [
    
'contexts' => [
      
// The "current user" is used above, which depends on the request,
      // so we tell Drupal to vary by the 'user' cache context.
      
'user',
    ],
  ],
];

// Merges the cache contexts, cache tags and max-age of the config object
// and user entity that the render array depend on.
$renderer->addCacheableDependency($build$config);
$renderer->addCacheableDependency($build, \Drupal\user\Entity\User::load($current_user->id()));
?>

Какие есть еще API?

Теги и контексты – обычные массивы, и ничто не мешает их склеить обычным сложением
$tags = $account->getCacheTags() + $comment->getCacheTags()
Но престижней использовать специально обученные метод, ведь это и инкапсуляция и избавления от повторок
Cache::mergeTags($main_tags, $add_tags);
Все это верно и для contexts, и для max-age.
Если в запросе нет подходящего параметра, чтобы подставить в keys, можно использовать его hash (sha256), через метод keyFromQuery.
Еще есть validateTags, invalidateTags и getBins (bins – контейнеры, в которых хранятся кэши, обычно по названию модуля, к которому принадлежат их значения)

Entity и Config – готовы, а ты?

У всех entity или configuration уже настроены параметры кэширования. Получить их можно так:

\Drupal::entityManager()->getDefinition('node')->getListCacheTags();
Или через конкретные объекты:
$node->getEntityType()->getListCacheTags()
(вместо $node, может быть $user, $view…)

Аналогично для contexts и max-age
$this->entityType->getListCacheContexts()

Что там есть в Examples/cache_example

Хоть там todo больше, чем кода, но можно глянуть как вручную сохранять/получать/удалять кэш:

<?php
cacheBackend
->set('название_кэша''содержимое_кэша'параметры_кэширования);
$cache cacheBackend->get('название_кэша'); // и потом $cache->data
cacheBackend->delete('название_кэша');
?>

Где cacheBackend - это объект реализующий интерфейс CacheBackendInterface.
Там же, в качестве примера, подсчитывают количество файлов в ядре, угадаешь сколько? А вот и нет, всего лишь 5928.

Так кому, что и почему надо знать?

При описании «революционных изменений» Drupal 8, часто упоминают «кэширование по умолчанию». Но это не просто кэширование. Два модуля, Internal Page Cache и Dynamic Page Cache затащили это дело на новый уровень. Вот как описывает историю их создания ведущий разработчик Вим Лирс (вкратце):

Как-то Дрис начал парить мне голову вопросами о ESI. Я сказал, что то, что у нас называется "поддержкой ESI" - бред, но есть реальный шанс сделать хорошо. Дриса это так проняло, что он даже выложил пост :) А мне пришлось изрядно напрячь башку, чтобы реализовать то, что посулил в запале.

Сначала наша команда довела до ума кэш тэги, благодаря чему Page Cache (ныне Internal Page Cache) стал работать так хорошо, что мы включили его в сборку и включили по умолчанию. А затем добили и контексты кэша. Это далось не просто, пробовали и так и сяк. В резульате на пару с Фабьеном запили через пузырёк. Потом еще 7 месяцев просыхали фиксили, но это того стоило. И теперь Dynamic Page Cache (бывший Smart Cache) тоже в сборке, и тоже по умолчанию.

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

И еще раз

То, что эти модули включены по умолчанию, означает, что каким бы ты ни был лохом, кэширование на сайте Drupal 8 будет огонь! Только не выключай их. Хотя, если для анонимных пользователей нужны разные результаты (например, с учетом их сессии), то Internal Page Cache придется отключить (либо мутить через AJAX), но это уже другая история.

А в этой истории речь о том, что теперь больше не нужны никакие танцы с Authcache, где нужно учитывать весь код сайта. Более того, опять пошли разговоры, что владелец сайта (админ, маускликер, сайт-билдер) может забыть, что значить фраза «очистить кэш». Поскольку теперь разработчики модулей могут и должны сами настроить зависимости кэша. Контриб-модули даже обязаны предоставить тесты на годность в режиме кэширования.

Но если Dynamic Page Cache мешает запалу разработки, можно временно его отключить, для этого в папке sites/название_сайта/ создается settings.local.php с кодом:

$settings['cache']['bins']['render'] = 'cache.backend.null';

Когда это будет в 7-ке?

Никогда. В Drupal 7 не появится мгновенного обновления кэша (в Drupal 8 кэш обновляется сразу по факту устаревания, не дожидаясь запроса, ога). Не появится Dynamic Cache Page. И не будет BigPipe. Как-то так.

А где же та фраза?

«There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton»

Склад ссылок, которых здесь еще не было (хотя быть должны)

https://www.drupal.org/developing/api/8/cache
https://www.drupal.org/developing/api/8/render/pipeline
https://www.drupal.org/developing/api/8/render/arrays/cacheability/auto-placeholdering
http://wimleers.com/talk/making-drupal-fly-fastest-drupal-ever-here
http://wimleers.com/blog/drupal-8-page-caching-enabled-by-default
https://drupalwatchdog.com/volume-4/issue-1/automagic-speed-cache
http://tech.dichtlog.nl/php/2015/08/03/lazy-builder-callback.html
http://www.sitepoint.com/exploring-cache-api-drupal-8/
https://github.com/upchuk/cache_demo_d8
http://slides.com/mikkeschiren-1/d8-cache#/1
http://chimera.labs.oreilly.com/books/1230000000845/ch01.html#_queues_and_workers

Read Full Article