← Вернуться к курсу

Чистый код в компонентах Битрикс: Магия class.php

Урок №3. Параметры и Кэширование. Строим надежный фундамент и не «травим» кэш

Текст
Введение

В мире Bitrix большинство разработчиков относятся к входящим параметрам ($arParams) как к «неизбежному злу», а к кэшированию — как к «черной магии», которая иногда ломает сайт. Ошибки на этом этапе — самые дорогие: от утечки персональных данных между пользователями (Cache Poisoning) до падения базы данных из-за одного некорректного ID в параметрах.

Сегодня мы научимся превращать хаос входящих данных в строгий контракт и заставим кэш работать на нас, а не против нас.

1. Теоретический фундамент: Контракт параметров и Изоляция кэша

Валидация как «Санитарный контроль»

Когда ваш компонент получает $arParams, он должен исходить из принципа Zero Trust (Нулевое доверие). Метод onPrepareComponentParams — это ваш единственный шанс очистить данные до того, как они попадут в логику и, что критично, до того, как система решит, есть ли для этих данных кэш.

Анатомия кэша в классах

В процедурном стиле вы писали $obCache->StartDataCache(). В class.php всё элегантнее. Метод $this->startResultCache() делает три вещи:

  1. Проверяет наличие кэша.
  2. Если кэш есть — выводит его и возвращает false.
  3. Если кэша нет — начинает запись и возвращает 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(); // вне кэша
}

    
⚠️ Критерий выбора
Если значение меняется от пользователя (ID / группы / валюта / регион / язык / cookie) — этот фактор обязан попасть либо в additionalCacheId, либо данные нельзя класть в кэш вообще.

Кейс Б Теневые параметры

Иногда разработчики передают в параметры огромные массивы или объекты.

Чем это плохо на практике:

  • Взрыв вариаций кэша: параметры участвуют в формировании ключа кэша. Любая «мелочь» (лишний пробел, разные типы "1" vs 1, нестабильные значения) создаёт новые файлы кэша.
  • Нестабильные значения убивают кэш: если вы протащили в параметры текущее время, random, объект запроса и т.п. — ключ кэша будет разным на каждом запросе, кэш никогда не будет попадать.
  • Тормоза на сериализации/десериализации: большие массивы в $arParams или «тяжёлые» структуры увеличивают стоимость serialize/unserialize (и размер файлов кэша).
  • Сложнее дебажить: параметров становится слишком много, «контракт компонента» размывается.

Как с этим бороться:

  1. Держим $arParams как контракт: только нужные, стабильные, маленькие значения (числа/строки/короткие флаги).
  2. Нормализуем и ограничиваем варианты в onPrepareComponentParams (типы, whitelist, лимиты).
  3. Любые 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. Экономика и Техдолг

Плохое управление параметрами стоит денег:

  1. Инфраструктурные расходы: Лишние гигабайты в bitrix/cache или Redis из-за дублей кэша.
  2. Репутационные риски: Показ чужих данных — прямой путь к жалобам в РКН и потере доверия клиентов.
  3. Сложность поддержки: Если кэш сбрасывается «когда захочет», разработчики начинают отключать его вовсе, что «убивает» сервер под нагрузкой.

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) и доработайте его так, чтобы он соответствовал тому, что мы обсудили в этом уроке:

  1. Превратите onPrepareComponentParams в строгий контракт: типы, whitelist для допустимых значений, разумные лимиты, обработка нестабильных входов из $_GET, чистка ~-ключей.
  2. Защитите кэш от Cache Poisoning — подумайте, какие факторы должны влиять на ключ кэша, и какие данные вообще нельзя класть в кэш.
  3. Поддержите параметр CACHE_TYPE со значениями A / Y / N.
  4. Используйте setResultCacheKeys так, чтобы в кэш попадало только то, что действительно нужно.

Когда закончите — сравните полученный результат с финальной версией: lesson-03-params-cache на GitHub.

Навигация

Уроки курса

1
Архитектурный кризис компонентов. Почему процедурный подход убивает ваш проект?
2
Анатомия class.php. Собираем идеальный каркас и управляем жизненным циклом
3
Параметры и Кэширование. Строим надежный фундамент и не «травим» кэш
4
Декомпозиция и Чистота. Выносим бизнес-логику в методы (Скоро)
5
Наследование и Трейты. Масштабируем архитектуру без боли и копипаста (Скоро)
6
Финальный Бой. Рефакторинг реального компонента: от «лапши» к архитектуре (Скоро)

Ваш прогресс

Войдите в аккаунт, чтобы отслеживать свой прогресс

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