1С Битрикс · 7 мин чтения

GET параметры в шаблоне компонента Битрикс: как принять и не сломать кеш

GET параметры в шаблоне компонента Битрикс читать легко: $_REQUEST['цвет'] или $_GET['sort'] работают в любом файле и сразу выводят результат на экран. Проблема начинается на втором запросе с другим значением параметра: закешированный компонент отдаёт HTML, собранный для первого посетителя, а не для текущего. За несколько лет работы с Битриксом я разбирал этот баг на карточках товаров с фильтром по цвету, на калькуляторах доставки СДЭК и на баннерах с UTM-меткой в тексте, и причина всегда одна: кеш компонента не знает о параметрах, которые шаблон читает в обход $arParams.

Почему GET-параметр в шаблоне ломает кеш компонента

Кеш компонента в Битриксе хранит уже готовый HTML, а ключ этого кеша строится по CACHE_TYPE, времени жизни и набору параметров, которые компонент явно получил в $arParams. GET-параметры туда не попадают, если их не добавить руками. Компонент вроде catalog.element с CACHE_TYPE="A" закеширует карточку товара один раз, а дальше будет отдавать один и тот же файл кеша и для /catalog/product-1/?color=red, и для /catalog/product-1/?color=blue, потому что для системы кеширования это один и тот же вызов компонента с одинаковыми входными параметрами.

На практике баг выглядит так: первый посетитель открывает карточку с ?color=red, компонент кладёт в кеш HTML с выделенным красным цветом. Второй посетитель заходит по ссылке с ?color=blue и получает тот же файл кеша с красным вариантом, потому что чтение $_REQUEST['color'] в шаблоне никак не связано с моментом, когда система решает, отдавать готовый кеш или считать компонент заново.

Где принимать параметр: component.php или template.php

Первое, что я меняю, разбирая такой баг, это место, где читается параметр. Если значение забирается прямо в template.php, оно физически не может повлиять на решение о кеше, потому что к этому моменту компонент уже определился, что показывать. Правильная точка входа - component.php, до вызова StartResultCache. Там я валидирую значение, привожу к нужному типу и уже дальше передаю либо в $arResult, либо в дополнительный ключ кеша.

Если компонент вызывается статически через #include или собственный класс на базе CBitrixComponent, GET-параметр всё равно не долетает до него автоматически, его нужно явно прочитать и передать в массив параметров при вызове $APPLICATION->IncludeComponent или в логике самого класса компонента. Ниже пример, как не стоит делать, чтобы был понятен масштаб проблемы:

// template.php - так делать не стоит
if ($_REQUEST['color']) {
    $color = htmlspecialcharsbx($_REQUEST['color']);
    echo '<div class="selected-color">' . $color . '</div>';
}

Этот код честно выведет нужный цвет один раз, при первой генерации кеша, а на всех следующих показах будет молчать про реальный GET-параметр текущего запроса.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Способ 1: добавляем GET-параметр в ключ кеша

Если параметр даёт ограниченное число вариантов (цвет из десяти значений, тип сортировки, размер), проще всего завести его в ключ кеша через StartResultCache. Тогда под каждую комбинацию значений система заведёт отдельный файл кеша, и подмены между посетителями не будет.

// component.php
$color = isset($_REQUEST['color']) ? trim($_REQUEST['color']) : '';
$allowedColors = array('red', 'blue', 'green', 'black');
if (!in_array($color, $allowedColors, true)) {
    $color = '';
}

if ($this->StartResultCache(3600, array($color))) {
    $this->arResult['SELECTED_COLOR'] = $color;
    $this->arResult['ITEMS'] = $this->getItems($color);

    if (empty($this->arResult['ITEMS'])) {
        $this->AbortResultCache();
    }

    $this->IncludeComponentTemplate();
}

Важный момент здесь - список $allowedColors. Значение из GET сначала проверяется по белому списку и только потом идёт в ключ кеша. Без этой проверки любой параметр со случайным значением породит новый файл кеша, и за неделю их может накопиться десятки тысяч. У меня так было на одном каталоге: фильтр по артикулу принимал произвольную строку, и боты, перебирая параметры в ссылках, за несколько дней нагенерили около сорока тысяч файлов кеша, большинство из которых никогда больше не запрашивалось.

Способ 2: отключаем кеш точечно через AbortResultCache

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

// component.php
if ($this->StartResultCache()) {
    if (isset($_REQUEST['preview']) && $_REQUEST['preview'] === 'Y') {
        $this->AbortResultCache();
        $this->arResult['ITEMS'] = $this->getPreviewItems();
    } else {
        $this->arResult['ITEMS'] = $this->getItems();
    }

    $this->IncludeComponentTemplate();
}

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

Способ 3: выносим переменную часть за пределы кеша

Если параметр меняется почти на каждом запросе, город доставки, сессия, UTM-метка, заводить его в ключ кеша бессмысленно: кеш перестанет работать как кеш и будет пересчитываться заново для каждого уникального значения. В таких случаях я оставляю основной блок компонента кешируемым, а динамическую часть выношу в отдельный вызов с CACHE_TYPE => 'N' прямо из шаблона родительского компонента.

// template.php родительского компонента, основной блок уже закеширован выше
$region = isset($_REQUEST['region']) ? htmlspecialcharsbx($_REQUEST['region']) : '';

$APPLICATION->IncludeComponent(
    'my:delivery.calculator',
    '',
    array(
        'CACHE_TYPE' => 'N',
        'REGION' => $region,
    ),
    false
);

Так я обычно делаю виджет расчёта доставки СДЭК на карточке товара: сам каталог кешируется по CACHE_TYPE=“A”, а блок с адресом и стоимостью доставки живёт отдельным некешируемым компонентом и пересчитывается на каждый запрос. Альтернатива, которая иногда удобнее, это вообще не гонять параметр через PHP, а забирать его на клиенте через URLSearchParams и подставлять в вёрстку JS-ом после загрузки страницы, тогда серверная часть остаётся полностью кешируемой.

Способ Когда использую Что происходит с кешем
Доп. ключ в StartResultCache Параметр даёт немного вариантов: цвет, сортировка, размер Каждая комбинация значений кешируется отдельным файлом
AbortResultCache Параметр редкий и разовый: предпросмотр, отладка, админ-режим Кеш для этого запроса не создаётся вообще
Вынос за пределы кеша Параметр меняется почти на каждом запросе: город, UTM, сессия Основной блок кешируется, динамическая часть считается каждый раз

Композитный кеш и GET-параметры: отдельная история

Композитный кеш работает поверх кеша компонентов и хранит целиком HTML страницы для анонимных посетителей. Если правильно завести GET-параметр в ключ кеша конкретного компонента, но страница при этом участвует в композите, для первого анонимного визита всё равно зафиксируется один вариант разметки, а следующие анонимные посетители получат именно его, пока композитный кеш не истечёт или не сбросится по правилам модуля. Для таких страниц обычно либо исключаю URL с нужным параметром из правил композита в настройках модуля, либо выношу зависящий от параметра блок в отдельную некешируемую область, как в способе 3 выше, чтобы композит подтягивал её отдельным запросом.

Если кеш на проекте уже настроен под композит и нужно аккуратно завести туда GET-параметр без просадки производительности всего сайта, такие правки обычно беру отдельной задачей на поддержку и сопровождение сайтов, потому что цена ошибки здесь не баг на одной карточке, а неправильный HTML для всех анонимных пользователей сайта сразу.

Фильтрация GET-параметра, который идёт в кеш и в вёрстку

Любое значение из GET, прежде чем попасть в ключ кеша или в HTML, проходит у меня через простую проверку. Для конечного набора вариантов, цвет, сортировка, категория, сверяю значение с белым списком и при несовпадении обнуляю параметр. Для числовых идентификаторов использую intval, для вывода в вёрстку всегда htmlspecialcharsbx, даже если параметр уже проверен по списку, чтобы не зависеть от изменений в этом списке в будущем.

Без такой фильтрации GET-параметр, попавший в ключ кеша, превращается в источник неограниченного числа файлов на диске: каждое новое значение строки создаёт новый файл, и при переборе параметров ботами кеш перестаёт выполнять свою работу, а сервер начинает тратить ресурсы на постоянную запись мусорных файлов вместо отдачи готового HTML.

Частые вопросы

Можно ли просто читать $_REQUEST в шаблоне и не трогать кеш?

Можно, если у компонента CACHE_TYPE="N", тогда каждый показ считается заново и рассинхрона между посетителями не возникает. Для кешируемого компонента с CACHE_TYPE «A» или «Y» такое чтение сработает только для того запроса, который сформировал кеш, поэтому параметр нужно завести в ключ кеша одним из способов выше.

Как передать GET-параметр в компонент, вызванный через #include или собственный класс?

Статический вызов компонента не передаёт GET-параметры автоматически. Читаю значение из $_REQUEST в самом начале component.php, до инициализации результата, и уже оттуда передаю его дальше в StartResultCache или в массив параметров при вызове дочернего компонента.

Что делать, если параметр нужен только для JS, а не для PHP-вывода?

Такой параметр вообще не завожу в кеш компонента. Читаю его в браузере через URLSearchParams и передаю в JS-обработчик после загрузки страницы, тогда серверная часть компонента остаётся полностью кешируемой и не зависит от значения в адресной строке.

Ломает ли один такой параметр композитный кеш всего сайта?

Сам по себе нет, ломает только тот компонент, который его неправильно читает. Но если этот компонент подключён на страницах, участвующих в композите, и параметр не обработан ни одним из трёх способов, для анонимных пользователей зафиксируется HTML первого визита. Тут нужно либо исключить URL с параметром из правил композита, либо вынести зависящий от него блок в отдельную некешируемую область.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно