Чистый код в компонентах Битрикса: class.php
Урок №2. Переносим компонент в class.php
В прошлом уроке мы развернули стенд 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', '', [...]):
- Ядро ищет
class.php— сначала в/local/components/project/news.list/, потом в/bitrix/components/. Если файл есть и в нём объявлен наследникCBitrixComponent, работает класс. Если файла нет — ядро откатывается наcomponent.php. Если нет и его, вы увидите'project:news.list' is not a component. - Параметр
CACHE_TYPEнормализуется до того, как ваш код что-то увидит: всё, что не'Y'и не'N', ядро превращает в'A'— «авто», то есть «как настроено в админке». - Вызывается
onPrepareComponentParams($arParams). То, что метод вернёт, ядро кладёт в$this->arParams. - Ядро прогоняет параметры через свой фильтр: строки с символами
; & < > "экранируются. - Вызывается
executeComponent()— ваша точка входа. - Дальше решаете вы: когда открыть блок кэша и когда позвать шаблон.
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() делает больше, чем «отменяет кэш»: он закрывает начатый блок, отдаёт накопленный буфер вывода как есть и не создаёт файл. Без явного вызова блок остаётся открытым до конца запроса, а результат не сохраняется.Проверяем: каркас на месте
- Очистите кэш и откройте
http://components.bitrix/news/. Список тот же, что после шага 1: те же новости, та же строкаВсего новостей: N. - Обновите страницу второй раз, не сбрасывая кэш. Список на месте, заголовок вкладки — «Новости компании». Если заголовок пропал,
SetTitle()оказался внутри блокаif. - Временно замените в
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 «Контракт параметров и безопасный кэш» мы сделаем её заметной: вернём в шаблон блок [Редактировать], посчитаем ссылки в ответе гостя, а потом построим контракт параметров и починим ключ кэша.
AI Домовой История
на связи
Привет! Я AI Домовой
Помогу с вопросами по 1С-Битрикс: D7, ORM, компоненты, события.
Чем подробнее задача — тем точнее ответ. Иногда готовлю развёрнутое решение.
Дневной лимит исчерпан. Сброс завтра.
Увеличить лимит с поддержкой