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

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

Урок №2. Переносим компонент в class.php

Текст • 40 мин
Введение

В прошлом уроке мы развернули стенд components.bitrix и положили в www/local/components/project/news.list/ процедурный компонент «список новостей». Он работает: страница открывается, новости выводятся. Плохо в нём всё остальное — выборка, форматирование, права и кэш живут в одном файле component.php, автор каждой новости берётся отдельным запросом, а часть логики спрятана в result_modifier.php. Сегодня мы переносим этот код в class.php и получаем каркас, в который дальше можно вносить правки без археологии.

Переносить будем в два приёма. Сначала миграция «в лоб»: код переезжает почти как есть, компонент продолжает работать, и у вас появляется точка отсчёта, с которой можно сравнивать. Потом раскладка по жизненному циклу. Если сделать оба шага сразу, при расхождении в выводе вы не поймёте, что его вызвало: перенос или перестановка. Промежуточное состояние, где класс уже есть, а порядка ещё нет, — шестьдесят строк в одном методе. Как этап это нормально, задерживаться в нём не стоит, иначе «переход на D7» сведётся к смене расширения файла.

Код обоих шагов лежит в lesson-02-class-basics: step-1-migration/ — миграция «в лоб», step-2-lifecycle/ — итог урока. Шаблоны в обеих папках одинаковые. У фрагментов из репозитория путь к файлу стоит в первой строке листинга.

Кто и когда вызывает ваш класс

Компонент в Битриксе запускаете не вы, его запускает ядро. Вы отдаёте класс и точки входа, а порядок вызовов задаёт CBitrixComponent. Вот что происходит после $APPLICATION->IncludeComponent('project:news.list', '', [...]):

  1. Ядро ищет class.php — сначала в /local/components/project/news.list/, потом в /bitrix/components/. Если файл есть и в нём объявлен наследник CBitrixComponent, работает класс. Если файла нет — ядро откатывается на component.php. Если нет и его, вы увидите 'project:news.list' is not a component.
  2. Параметр CACHE_TYPE нормализуется до того, как ваш код что-то увидит: всё, что не 'Y' и не 'N', ядро превращает в 'A' — «авто», то есть «как настроено в админке».
  3. Вызывается onPrepareComponentParams($arParams). То, что метод вернёт, ядро кладёт в $this->arParams.
  4. Ядро прогоняет параметры через свой фильтр: строки с символами ; & < > " экранируются.
  5. Вызывается executeComponent() — ваша точка входа.
  6. Дальше решаете вы: когда открыть блок кэша и когда позвать шаблон. includeComponentTemplate() подключает result_modifier.php, затем template.php, затем закрывает блок кэша и записывает файл.

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

ℹ️ Под капотом после onPrepareComponentParams ядро добавляет в $this->arParams копии значений с префиксом ~: в ['NAME'] лежит экранированная строка, в ['~NAME'] — исходная. Ключи с ~ в ключ кэша не попадают.

При переезде в класс меняется смысл двух переменных. В component.php $arParams и $arResult — локальные переменные области видимости include. В классе это свойства объекта: $this->arParams заполняет ядро, и именно из него собирается ключ кэша; $this->arResult ядро сериализует в файл кэша и передаёт в шаблон. Локальная переменная внутри executeComponent() умрёт вместе с методом: шаблон её не увидит, в кэш она не попадёт.

Метод Кто вызывает Когда Что внутри
onPrepareComponentParams() ядро до executeComponent() и до кэша приведение типов, значения по умолчанию
executeComponent() ядро один раз за вызов компонента оркестрация: модули, кэш, данные, шаблон
includeComponentTemplate() вы внутри блока кэша подключение шаблона и запись кэша

Шаг 1. Переносим код в class.php как есть

На первом шаге задача одна: ничего не сломать. Создайте в папке компонента файл class.php и перенесите туда содержимое component.php, заменив процедурные обращения на работу с объектом.

ℹ️ Под капотом в main 26.800 заготовку класса умеет создавать консоль: php www/bitrix/bitrix.php make:component project:news.list --no-module кладёт в www/local/components/project/news.list/ пустой class.php с onPrepareComponentParams() и executeComponent() и шаблон к нему. Кэша и проверки модулей в заготовке нет. Для нового компонента это удобный старт, а нам нужно перенести существующий код, поэтому файл пишем руками.
Было в component.php Стало в class.php
$arParams["IBLOCK_ID"] $this->arParams['IBLOCK_ID']
$arResult["ITEMS"] $this->arResult['ITEMS']
$this->IncludeComponentTemplate() $this->includeComponentTemplate()
global $USER, $APPLICATION $GLOBALS['USER'], $GLOBALS['APPLICATION']
CModule::IncludeModule("iblock") \Bitrix\Main\Loader::includeModule('iblock')
new CPHPCache() + InitCache / StartDataCache / EndDataCache if ($this->startResultCache()) { ... }
"IBLOCK_ID" 'IBLOCK_ID': строка без подстановки переменных — в одинарных кавычках

Первое, что меняется по существу, — ручной кэш. Обвязка CPHPCache (создание объекта, ключ через serialize($arParams), путь /SITE_ID/news_list/) встроена в CBitrixComponent. Тащить за собой десять строк, дублирующих базовый класс, смысла нет. Так выглядит step-1-migration/class.php, тело цикла сокращено:

        // lesson-02-class-basics/step-1-migration/class.php

class NewsListComponent extends CBitrixComponent
{
    public function executeComponent()
    {
        $this->arParams['IBLOCK_ID'] = intval($this->arParams['IBLOCK_ID']);
        $this->arParams['NEWS_COUNT'] = $this->arParams['NEWS_COUNT'] ? intval($this->arParams['NEWS_COUNT']) : 10;
        $this->arParams['CACHE_TIME'] = $this->arParams['CACHE_TIME'] ? intval($this->arParams['CACHE_TIME']) : 3600;

        \Bitrix\Main\Loader::includeModule('iblock');

        if ($this->startResultCache()) {

            $this->arResult = array();
            $this->arResult['ITEMS'] = array();

            $rsElements = \CIBlockElement::GetList(
                array('ACTIVE_FROM' => 'DESC', 'ID' => 'DESC'),
                array('IBLOCK_ID' => $this->arParams['IBLOCK_ID'], 'ACTIVE' => 'Y'),
                false,
                array('nTopCount' => $this->arParams['NEWS_COUNT']),
                array('ID', 'NAME', 'PREVIEW_TEXT', 'PREVIEW_PICTURE', 'DETAIL_PAGE_URL', 'ACTIVE_FROM', 'CREATED_BY')
            );

            while ($obElement = $rsElements->GetNextElement()) {
                $arItem = $obElement->GetFields();
                // тело цикла перенесено из component.php без изменений:
                // картинка, дата, автор отдельным запросом, проверка прав
                $this->arResult['ITEMS'][] = $arItem;
            }

            $this->arResult['TOTAL_COUNT'] = count($this->arResult['ITEMS']);

            if (empty($this->arResult['ITEMS'])) {
                $this->arResult['ERROR_MESSAGE'] = 'Новости не найдены';
                $this->abortResultCache();
            }

            $this->includeComponentTemplate();
        }

        $GLOBALS['APPLICATION']->SetTitle('Новости компании');
        $GLOBALS['APPLICATION']->SetPageProperty('description', 'Последние новости нашей компании');
        $GLOBALS['APPLICATION']->AddChainItem('Новости', '/news/');
    }
}

    

Старый component.php удалите. Правило ядра: есть class.php — работает он, а component.php не подключается вовсе, и оставленный файл становится ловушкой: следующий разработчик поправит в нём фильтр, обновит страницу и не увидит изменений. Не путайте его с .description.php — тот на выполнение не влияет, это файл-визитка, из которой визуальный редактор берёт имя компонента и место в дереве. У нашего компонента его пока нет, заведём в уроке 6.

Второе изменение по существу — шаблон: templates/.default/template.php в снапшоте урока 2 заметно отличается от версии урока 1. Вывод пропущен через htmlspecialchars() — закрыта дыра XSS, из-за которой в уроке 1 тег <script> из заголовка новости доезжал до публичной страницы. Инлайновые style="..." заменены классами. Из шаблона убраны CUser::GetByID() и CIBlockSection::GetByID(): его дело — вывести готовый $arResult, а не ходить в базу; вместе с запросом за разделом ушёл и блок [Редактировать], внутри которого тот стоял. Пустой список выводит «Новости не найдены» вместо красной рамки с ERROR_MESSAGE. Причесали и result_modifier.php: вместо «Очень много новостей (N)» из урока 1 он ставит «Новости (N)» — а на самой странице этот заголовок перекроет SetTitle('Новости компании') из класса, который вызывается после шаблона.

Проверяем: компонент ожил

Вызов на странице www/news/index.php остаётся тем же, что в уроке 1:

        $APPLICATION->IncludeComponent(
    'project:news.list',
    '',
    [
        'IBLOCK_ID' => 1,
        'NEWS_COUNT' => 10,
        'CACHE_TYPE' => 'A',
        'CACHE_TIME' => 3600,
        'CACHE_GROUPS' => 'Y',
    ]
);

    

Вместо 1 — ID вашего инфоблока.

Сбросьте кэш: админка /bitrix/ → Настройки → Настройки продукта → Автокеширование, вкладка «Очистка файлов кеша», вариант «Все», кнопка «Очистить». Дальше в курсе этот путь сокращается до «очистите кэш». Откройте http://components.bitrix/news/.

Сверяйте состав списка, а не разметку: те же новости в том же порядке и количестве, что в конце урока 1, заголовки в верхнем регистре, дата, автор, ссылка «Подробнее», внизу Всего новостей: 10. Заголовок вкладки теперь «Новости компании». HTML после чистки шаблона другой и блока [Редактировать] под новостями больше нет, так что посимвольно вывод не сравнивайте.

Не сработало?

  • Белый экран или пустая страница — фатальная ошибка PHP при подключении class.php, смотрите лог. Частая причина помимо опечаток: класс с таким именем уже объявлен — имена классов компонентов глобальные, NewsListComponent на проекте должен быть один.
  • 'project:news.list' is not a component — ядро не нашло ни наследника CBitrixComponent в class.php, ни component.php. Проверьте путь www/local/components/project/news.list/class.php и объявление класса.
  • Cannot find '.default' template with page '' — папка templates/.default/ осталась в старом месте. Шаблон лежит рядом с class.php.
  • Страница открывается, но показывает старые данные — включено автокеширование, ядро отдаёт сохранённый результат. Выключите его на время работы или чистите кэш после каждой правки.

Класс есть, а порядка нет

Компонент работает, задача формально закрыта. Посмотрите, что получилось: один метод примерно на шестьдесят строк, где подряд идут приведение типов, подключение модуля, открытие кэша, запрос к базе, форматирование, проверка прав, закрытие кэша и SEO. От component.php этот код отличается отступом и словом class в начале файла.

У этого метода три проблемы, и каждая из них однажды что-нибудь сломает.

Порядок держится на вашей аккуратности, а не на ядре. Нормализация параметров стоит выше startResultCache(), поэтому ключ кэша собирается из правильных значений — ровно до момента, пока кто-нибудь не поменяет местами две строки или не вынесет часть логики в отдельный метод. Ядро даёт готовую гарантию: onPrepareComponentParams() вызывается раньше любого кэша. Пока нормализация живёт внутри executeComponent, эта гарантия не используется.

Loader::includeModule('iblock') вызван, но результат не проверен. Если модуль выключен или не установлен, класса \CIBlockElement не существует, и вы получите Class "CIBlockElement" not found в середине открытого блока кэша вместо внятного сообщения о причине.

Переопределить одну часть невозможно. Наследнику, которому нужна другая сортировка, придётся скопировать executeComponent целиком — вместе с кэшем, шаблоном и SEO. В уроке 5 мы вынесем общую логику в BaseComponent, и с монолитным методом этот шаг не сделать.

Шаг 2. Раскладываем код по жизненному циклу

Второй шаг — разнести обязанности по методам, которые ядро и так готово вызвать в нужном порядке. Результат — step-2-lifecycle/class.php, ниже он разобран по методам.

Шаг 2.1: параметры

        // lesson-02-class-basics/step-2-lifecycle/class.php

public function onPrepareComponentParams($params): array
{
    $params['IBLOCK_ID'] = (int)($params['IBLOCK_ID'] ?? 0);
    $params['NEWS_COUNT'] = (int)($params['NEWS_COUNT'] ?? 10);
    $params['CACHE_TIME'] = (int)($params['CACHE_TIME'] ?? 3600);

    return $params;
}

    

Метод получает сырой массив из IncludeComponent() и возвращает тот, с которым будет жить компонент. Это единственное место, где параметр меняет тип и получает значение по умолчанию. Дальше по коду можно писать $this->arParams['NEWS_COUNT'] и знать, что там целое число, а не строка, не null и не пустой массив.

Есть и вторая причина: ключ кэша строится из $this->arParams, а строка '10' и число 10 сериализуются по-разному. Без нормализации два одинаковых по смыслу вызова компонента создают два разных файла кэша.

ℹ️ Если это в новинку ключ кэша ядро собирает само в getCacheID() — идентификатор сайта, язык, шаблон сайта, имя компонента, имя шаблона и все $arParams. Ничего из того, что вы прочитаете внутри executeComponent() — права пользователя, $_GET, дату — в ключ не попадает.

Шаг 2.2: модули

        // lesson-02-class-basics/step-2-lifecycle/class.php

protected function checkModules(): void
{
    if (!Loader::includeModule('iblock')) {
        throw new \Bitrix\Main\SystemException('Модуль iblock не установлен');
    }
}

    

checkModules() — не метод жизненного цикла, его придумали мы. Ради одного вызова заводить метод кажется избыточным, но причины две. Проверка результата includeModule() перестаёт быть строчкой, которую легко потерять при следующей правке. И в уроке 5 этот метод переедет в BaseComponent, а компоненты проекта будут объявлять только список нужных им модулей.

Исключение вместо return — потому что executeComponent() не должен решать, что делать с несобранной инфраструктурой. Он ловит исключение в одном месте и один раз показывает ошибку.

Шаг 2.3: точка входа

        // lesson-02-class-basics/step-2-lifecycle/class.php

public function executeComponent()
{
    try {
        $this->checkModules();

        if ($this->startResultCache()) {

            $this->arResult['ITEMS'] = [];

            $arFilter = [
                'IBLOCK_ID' => $this->arParams['IBLOCK_ID'],
                'ACTIVE' => 'Y'
            ];

            $arOrder = ['ACTIVE_FROM' => 'DESC', 'ID' => 'DESC'];

            // GetList и обход выборки — тот же код, что на шаге 1; переедут в урок 4

            $this->arResult['TOTAL_COUNT'] = count($this->arResult['ITEMS']);

            if (empty($this->arResult['ITEMS'])) {
                $this->arResult['ERROR_MESSAGE'] = 'Новости не найдены';
                $this->abortResultCache();
            }

            $this->includeComponentTemplate();
        }

    } catch (\Exception $e) {
        $this->abortResultCache();
        ShowError($e->getMessage());
    }

    $GLOBALS['APPLICATION']->SetTitle('Новости компании');
    $GLOBALS['APPLICATION']->SetPageProperty('description', 'Последние новости нашей компании');
    $GLOBALS['APPLICATION']->AddChainItem('Новости', '/news/');
}

    

Метод всё ещё великоват: выборка и форматирование останутся в нём до урока 4. Но каркас вокруг них состоит из решений, которые стоит проговорить.

startResultCache() возвращает true, когда кэша нет и его надо построить. false — попадание в кэш: ядро уже восстановило $this->arResult из файла и подтянуло CSS/JS шаблона, а template.php не выполняется вовсе — в HTML уходит сохранённый вывод. Отсюда правило: всё, что влияет на разметку, считается внутри if.

Кэш записывает не конец метода, а includeComponentTemplate(): он подключает шаблон и следом закрывает блок. Забыть вызвать шаблон внутри if — значит никогда не записать кэш; компонент будет ходить в базу на каждый хит и ничем этого не покажет.

При CACHE_TYPE = 'N' (и при 'A', если кеширование компонентов выключено в админке) startResultCache() сразу возвращает true и ничего не пишет — штатный режим разработки.

SetTitle() стоит вне блока if, потому что заголовок страницы не входит в HTML шаблона и в кэш компонента не попадает. Оставьте SetTitle() внутри if, и при первом заходе заголовок появится, а при втором исчезнет.

ℹ️ Под капотом abortResultCache() делает больше, чем «отменяет кэш»: он закрывает начатый блок, отдаёт накопленный буфер вывода как есть и не создаёт файл. Без явного вызова блок остаётся открытым до конца запроса, а результат не сохраняется.

Проверяем: каркас на месте

  1. Очистите кэш и откройте http://components.bitrix/news/. Список тот же, что после шага 1: те же новости, та же строка Всего новостей: N.
  2. Обновите страницу второй раз, не сбрасывая кэш. Список на месте, заголовок вкладки — «Новости компании». Если заголовок пропал, SetTitle() оказался внутри блока if.
  3. Временно замените в checkModules() 'iblock' на 'iblock_not_exists' и обновите страницу. Вместо белого экрана — красная строка Модуль iblock не установлен: сработала цепочка SystemException → catch → ShowError(). Верните имя модуля обратно.

Не сработало?

  • Список пустой, хотя новости в инфоблоке есть — IBLOCK_ID после (int) стал нулём. Проверьте вызов: в параметре должен быть числовой ID, а не символьный код инфоблока.
  • Изменения в class.php не видны — снова автокеширование: очистите кэш после правки.
  • Cannot find '.default' template with page '' — шаблон не нашёлся; ядро в этом случае само вызывает abortResultCache().
  • Красная строка Модуль iblock не установлен появилась без всяких правок — модуль «Информационные блоки» выключен в админке (Настройки → Настройки продукта → Модули). Проверка сработала как надо, причина в стенде.

Что делать с result_modifier.php и component_epilog.php

Эти два файла регулярно становятся тем местом, где логика прячется от следующего разработчика. Разница между «уместно» и «спрятано» определяется моментом выполнения.

Файл Когда выполняется Что туда класть
result_modifier.php внутри блока кэша, перед template.php; при попадании в кэш не выполняется подготовку данных под конкретный шаблон: CSS-классы, склейку строк, порядок вывода
component_epilog.php после отдачи HTML — и при промахе, и при попадании в кэш то, что нельзя кэшировать: счётчики просмотров, заголовок и хлебные крошки, зависящие от запроса

Всё, что делает result_modifier.php, кэшируется вместе с arResult. Если модификатор читает $_GET или текущего пользователя, результат этого чтения попадает в общий файл кэша, а ключ кэша об этом не знает. Так и утекают данные между пользователями — разберём в следующем уроке. А в component_epilog.php $arResult приходит урезанным: если вы вызвали setResultCacheKeys(), ядро оставляет в нём только перечисленные ключи (это тоже урок 3).

В нашем компоненте result_modifier.php делает три лишние для него вещи: читает $_GET['SHOW_FULL_LIST'], ходит в базу за свойством SOURCE в цикле и меняет заголовок страницы через $GLOBALS['APPLICATION']. На этом уроке уходит одна строка — CModule::IncludeModule('iblock'): за модуль теперь отвечает checkModules(). Остальное чинится в уроках 3 и 4.

Проверка «уместно или спрятано»: мысленно удалите result_modifier.php. Компонент должен продолжать отдавать корректный $arResult — без косметики шаблона, но полный. Если без модификатора шаблон падает на отсутствующем ключе, логика уехала не туда.

🛠 Практическая работа

Работаем с компонентом, который вы разобрали в аудите урока 1.

Шаг 1. Миграция «в лоб». Создайте class.php рядом с component.php, перенесите код по таблице замен, поменяйте CPHPCache на startResultCache(). Ничего не переписывайте по существу. Откройте страницу и убедитесь, что состав списка не изменился: те же элементы, тот же порядок, то же количество. Удалите component.php.

Шаг 2. Жизненный цикл. Вынесите нормализацию всех параметров в onPrepareComponentParams(), подключение модулей — в checkModules() с исключением, оберните тело executeComponent() в try/catch с abortResultCache(). Убедитесь, что SetTitle() и хлебные крошки стоят вне блока кэша, и проверьте это вторым заходом на страницу.

Контрольный вопрос. Посчитайте, сколько строк осталось в executeComponent(), и выпишите всё, что не является оркестрацией. Оркестрация — вызовы checkModules(), startResultCache(), includeComponentTemplate() и обработка ошибок. Остальное — построение фильтра, обход выборки, форматирование дат и картинок, работа с правами — кандидаты на переезд в отдельные методы. Сохраните список: в уроке 4 будете сверяться с ним, а не искать заново.

Задание со звёздочкой. Добавьте в onPrepareComponentParams() параметр SORT_ORDER с приведением к ASC или DESC и значением по умолчанию DESC — через белый список, а не через приведение строки: подставлять пользовательский ввод в порядок сортировки нельзя.

Заключение

Каркас есть. Параметры готовятся в одном месте и до кэша, модули проверяются с понятной ошибкой, вместо ручного CPHPCache работает встроенный кэш, а executeComponent() читается как последовательность шагов. class.php полезен ровно настолько, насколько вы используете его точки расширения с фиксированным порядком вызова; сложить в него тот же код можно и без пользы.

Незакрытого осталось два места. $arParams приведены к типам, но компонент по-прежнему верит, что IBLOCK_ID — существующий инфоблок, а NEWS_COUNT — вменяемое число. И ключ кэша собирается механически из параметров: ни $GLOBALS['USER']->IsAdmin(), ни $_GET из result_modifier.php в него не попадают. Первый администратор, открывший страницу, положит в кэш свою версию $arResult с CAN_EDIT и EDIT_LINK, и следующему гостю ядро отдаст именно её. На стенде этого не видно: шаблон урока 2 эти ключи не печатает, утечка есть, но невидима.

В уроке 3 «Контракт параметров и безопасный кэш» мы сделаем её заметной: вернём в шаблон блок [Редактировать], посчитаем ссылки в ответе гостя, а потом построим контракт параметров и починим ключ кэша.

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

AI Домовой История

на связи

пишет…
Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти