Чистый код в компонентах Битрикс: Магия class.php
Урок №3. Параметры и Кэширование. Строим надежный фундамент и не «травим» кэш
В мире Bitrix большинство разработчиков относятся к входящим параметрам ($arParams) как к «неизбежному злу», а к кэшированию — как к «черной магии», которая иногда ломает сайт. Ошибки на этом этапе — самые дорогие: от утечки персональных данных между пользователями (Cache Poisoning) до падения базы данных из-за одного некорректного ID в параметрах.
Сегодня мы научимся превращать хаос входящих данных в строгий контракт и заставим кэш работать на нас, а не против нас.
1. Теоретический фундамент: Контракт параметров и Изоляция кэша
Валидация как «Санитарный контроль»
Когда ваш компонент получает $arParams, он должен исходить из принципа Zero Trust (Нулевое доверие). Метод onPrepareComponentParams — это ваш единственный шанс очистить данные до того, как они попадут в логику и, что критично, до того, как система решит, есть ли для этих данных кэш.
Анатомия кэша в классах
В процедурном стиле вы писали $obCache->StartDataCache(). В class.php всё элегантнее. Метод $this->startResultCache() делает три вещи:
- Проверяет наличие кэша.
- Если кэш есть — выводит его и возвращает
false. - Если кэша нет — начинает запись и возвращает
true.
Но есть нюанс: кэш привязывается к состоянию параметров. Если вы не «почистили» параметры в onPrepareComponentParams, у вас может создаться 1000 копий кэша для строк "1", " 1", "001", хотя по сути это один и тот же ID.
2. Анатомия катастрофы: Отравление и взрыв кэша
Кейс А Cache Poisoning (Утечка данных)
Представьте компонент «Личный кабинет», который выводит баланс пользователя.
Ошибка в коде:
public function executeComponent()
{
// ОШИБКА: Кэш зависит только от ID компонента, но не от ID пользователя!
if ($this->startResultCache()) {
$currentUser = \Bitrix\Main\Engine\CurrentUser::get();
$this->arResult["BALANCE"] = UserTable::getBalance($currentUser->getId());
$this->includeComponentTemplate();
}
}
Как с этим бороться (2 рабочих стратегии):
- Если данные зависят от пользователя — делаем кэш «персональным». Добавляем фактор пользователя в ключ кэша через
additionalCacheId(2-й аргументstartResultCache).
public function executeComponent()
{
$currentUser = \Bitrix\Main\Engine\CurrentUser::get();
$cacheId = ["user_id" => (int)$currentUser->getId()];
if ($this->startResultCache($this->arParams["CACHE_TIME"], $cacheId)) {
$this->arResult["BALANCE"] = UserTable::getBalance($currentUser->getId());
$this->includeComponentTemplate();
}
}
- Если в компоненте есть и общие данные, и персональные — разделяем их. Общее кэшируем, персональное вычисляем после блока кэша (как в примере урока:
setPersonalData()вызывается вне кэша).
public function executeComponent()
{
$shouldCache = $this->arParams["CACHE_TYPE"] !== "N";
if (!$shouldCache || $this->startResultCache()) {
$this->arResult["PUBLIC_DATA"] = $this->getPublicData();
$this->includeComponentTemplate();
}
$this->arResult["BALANCE"] = $this->getUserBalance(); // вне кэша
}
additionalCacheId, либо данные нельзя класть в кэш вообще.Кейс Б Теневые параметры
Иногда разработчики передают в параметры огромные массивы или объекты.
Чем это плохо на практике:
- Взрыв вариаций кэша: параметры участвуют в формировании ключа кэша. Любая «мелочь» (лишний пробел, разные типы
"1"vs1, нестабильные значения) создаёт новые файлы кэша. - Нестабильные значения убивают кэш: если вы протащили в параметры текущее время, random, объект запроса и т.п. — ключ кэша будет разным на каждом запросе, кэш никогда не будет попадать.
- Тормоза на сериализации/десериализации: большие массивы в
$arParamsили «тяжёлые» структуры увеличивают стоимостьserialize/unserialize(и размер файлов кэша). - Сложнее дебажить: параметров становится слишком много, «контракт компонента» размывается.
Как с этим бороться:
- Держим
$arParamsкак контракт: только нужные, стабильные, маленькие значения (числа/строки/короткие флаги). - Нормализуем и ограничиваем варианты в
onPrepareComponentParams(типы, whitelist, лимиты). - Любые runtime-факторы (request, user, время) не «таскаем» в параметрах: либо превращаем в нормализованный флаг (
SHOW_FULL_LIST), либо добавляем вadditionalCacheId, либо вычисляем вне кэша.
Пример антипаттерна и исправления:
// ПЛОХО: нестабильно и раздувает ключ кэша
$params["NOW"] = time();
$params["REQUEST"] = \Bitrix\Main\Application::getInstance()->getContext()->getRequest();
// ХОРОШО: стабильный флаг в контракте
$params["SHOW_FULL_LIST"] = ($request->get("SHOW_FULL_LIST") === "Y") ? "Y" : "N";
3. Экономика и Техдолг
Плохое управление параметрами стоит денег:
- Инфраструктурные расходы: Лишние гигабайты в
bitrix/cacheили Redis из-за дублей кэша. - Репутационные риски: Показ чужих данных — прямой путь к жалобам в РКН и потере доверия клиентов.
- Сложность поддержки: Если кэш сбрасывается «когда захочет», разработчики начинают отключать его вовсе, что «убивает» сервер под нагрузкой.
4. Путь к спасению: Реализуем контракт и динамический кэш
Правильный onPrepareComponentParams
Здесь мы превращаем «что-то» в «строгое что-то».
// examples/lesson-03-params-cache/class.php
public function onPrepareComponentParams($params): array
{
$request = \Bitrix\Main\Application::getInstance()->getContext()->getRequest();
// 1. Приведение к типам
$params["IBLOCK_ID"] = (int)($params["IBLOCK_ID"] ?? 0);
$params["NEWS_COUNT"] = (int)($params["NEWS_COUNT"] ?? 10);
$params["CACHE_TIME"] = (int)($params["CACHE_TIME"] ?? 3600);
$params["CACHE_TYPE"] = $params["CACHE_TYPE"] ?? "A";
$params["SHOW_FULL_LIST"] = ($request->get("SHOW_FULL_LIST") === "Y") ? "Y" : "N";
// 2. Валидация критичных параметров
if ($params["IBLOCK_ID"] <= 0) {
throw new \Bitrix\Main\ArgumentException("IBLOCK_ID должен быть положительным числом");
}
// 3. Ограничение допустимых значений
$params["SORT_ORDER"] = in_array($params["SORT_ORDER"] ?? "", ["ASC", "DESC"])
? $params["SORT_ORDER"]
: "DESC";
$params["CACHE_TYPE"] = in_array($params["CACHE_TYPE"], ["A", "Y", "N"])
? $params["CACHE_TYPE"]
: "A";
// 4. Разумные лимиты
if ($params["NEWS_COUNT"] > 100) {
$params["NEWS_COUNT"] = 100;
}
return $params;
}
Почему мы читаем request здесь, а не в result_modifier.php и не в шаблоне: если отображение зависит от $_GET, но этот фактор не становится частью «контракта параметров», кэш начинает жить своей жизнью. Переносим входные данные в onPrepareComponentParams, нормализуем их, и тогда:
- состояние «показывать полный список или нет» фиксируется в
$arParamsи участвует в кэшировании; - мы не размазываем «скрытые входы» (
$_GET,$_POST, cookies) по коду компонента.
Динамическое управление ключом кэша
Если данные зависят от внешних факторов (группы пользователя, куки, валюта), используйте дополнительные идентификаторы.
// examples/lesson-03-params-cache/class.php
protected function getAdditionalCacheId(): array
{
$currentUser = \Bitrix\Main\Engine\CurrentUser::get();
return [
"user_groups" => $currentUser->getUserGroups(),
"is_authorized" => $currentUser->getId() > 0,
];
}
public function executeComponent()
{
$this->checkModules();
$shouldCache = $this->shouldCache();
$cacheId = $this->getAdditionalCacheId();
if (!$shouldCache || $this->startResultCache($this->arParams["CACHE_TIME"], $cacheId)) {
// ... получение данных ...
$this->includeComponentTemplate();
}
// Данные вне кэша - персонализация
$this->setPersonalData();
}
Выборочное кэширование через setResultCacheKeys()
Не всегда нужно «тащить наружу» весь $arResult. Метод setResultCacheKeys() позволяет указать ключи, которые должны быть доступны после кэша (например, чтобы безопасно использовать их вне блока кэширования).
// examples/lesson-03-params-cache/class.php
if ($shouldCache) {
$this->setResultCacheKeys(["ITEMS", "TOTAL_COUNT"]);
if (empty($this->arResult["ITEMS"])) {
$this->abortResultCache();
}
}
Параметр CACHE_TYPE
Стандартные компоненты Битрикса принимают параметр CACHE_TYPE со значениями:
A— авто (учитывает настройки в админке)Y— всегда кэшироватьN— никогда не кэшировать
Рекомендуется поддерживать этот параметр в своих компонентах для единообразия. Минимально корректная интерпретация: если CACHE_TYPE = "N" — не используем кэш, иначе — используем CACHE_TIME и startResultCache.
public function executeComponent()
{
$shouldCache = $this->arParams["CACHE_TYPE"] !== "N";
if (!$shouldCache || $this->startResultCache()) {
$this->getResult();
$this->includeComponentTemplate();
}
}
📂 Полный код урока: lesson-03-params-cache на GitHub
Возьмите компонент новостей из прошлого урока (step-2-lifecycle/class.php) и доработайте его так, чтобы он соответствовал тому, что мы обсудили в этом уроке:
- Превратите
onPrepareComponentParamsв строгий контракт: типы, whitelist для допустимых значений, разумные лимиты, обработка нестабильных входов из$_GET, чистка~-ключей. - Защитите кэш от Cache Poisoning — подумайте, какие факторы должны влиять на ключ кэша, и какие данные вообще нельзя класть в кэш.
- Поддержите параметр
CACHE_TYPEсо значениямиA/Y/N. - Используйте
setResultCacheKeysтак, чтобы в кэш попадало только то, что действительно нужно.
Когда закончите — сравните полученный результат с финальной версией: lesson-03-params-cache на GitHub.