Тихие хуки Битрикса: вспомогательные файлы, которые ядро подключает за вас (и как ими пользоваться)
Кирилл Новожилов
Автор
Содержание
Про init.php знают все. Но в Битриксе есть целый пласт неявных точек расширения — файлов, которые ядро ищет по захардкоженным путям и просто инклудит. Никаких подписок на события или ServiceLocator: положил файл с нужным именем в /local/php_interface/, и он отработал.
Эта механика размазана по исходникам и почти не задокументирована. Поэтому на проектах с историей постоянно всплывают артефакты: неработающий site_closed.php из-за ошибки в пути, слитый пароль от БД в dbconn_error.php или забытый top.menu_template.php, который ломает вёрстку нового меню.
Ниже — шпаргалка по таким файлам для ядра 22+. Разберём, в какой момент они вызываются, для чего реально нужны и чего в них делать категорически нельзя. Заодно затронем пару соседних файлов вроде urlrewrite.php и 404.php, с которыми их вечно путают.
Как ядро вообще ищет ваши файлы
Вместо жёсткого require '/bitrix/...' ядро использует getLocalPath('php_interface/...', BX_PERSONAL_ROOT) или \Bitrix\Main\Loader::getLocal/getPersonal. Эти функции сначала смотрят в /local/, потом в /bitrix/, и возвращают первый найденный путь относительно DOCUMENT_ROOT. Поэтому /local/php_interface/X.php всегда выигрывает у /bitrix/php_interface/X.php.
Канонический пример из main/include/prolog_after.php:
if (($siteClosed = getLocalPath("php_interface/" . LANG . "/site_closed.php", BX_PERSONAL_ROOT)) !== false) {
include($_SERVER["DOCUMENT_ROOT"] . $siteClosed);
} elseif (($siteClosed = getLocalPath("php_interface/include/site_closed.php", BX_PERSONAL_ROOT)) !== false) {
include($_SERVER["DOCUMENT_ROOT"] . $siteClosed);
} else {
include($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/site_closed.php");
}
Три уровня — под конкретный язык, общий, дефолт ядра — повторяются по всему фреймворку, отличаются только имена. Контракт получается простой: если ядро где-то делает getLocalPath('php_interface/X.php') или file_exists($_SERVER['DOCUMENT_ROOT'] . BX_PERSONAL_ROOT . '/php_interface/X.php'), у вас по факту есть тихий хук с именем X. Создал файл по этому пути — Битрикс его включит.
Когда именно всё это срабатывает
Куда класть код — зависит от того, в какой момент его включит ядро. Шкала одного публичного хита выглядит так:
HTTP-запрос
│
▼
/bitrix/urlrewrite.php (если включён ЧПУ)
└─ require_once /local/php_interface/dbconn.php ← [hook]
│
▼
/bitrix/header.php
└─ /bitrix/modules/main/include/prolog.php
├─ prolog_before.php
│ └─ require /bitrix/modules/main/include.php
│ ├─ старт ядра, инициализация Application/Context/Culture
│ ├─ Connection::connectInternal()
│ │ └─ include /local/php_interface/after_connect_d7.php ← [hook]
│ ├─ include /local/init.php ← [hook, редкий]
│ ├─ include /local/php_interface/init.php ← [hook]
│ └─ include /local/php_interface/<SITE_ID>/init.php ← [hook]
│
└─ prolog_after.php
├─ если site_stopped == "Y":
│ include /local/php_interface/<LANG>/site_closed.php ← [hook]
│ // или /local/php_interface/include/site_closed.php
│ // или дефолтный из /bitrix/modules/main/include/site_closed.php
│ die();
├─ include SITE_TEMPLATE_PATH/header.php ← [шаблон]
│ └─ внутри: меню (CMenu) → ищет
│ /local/templates/<T>/<type>.menu_template.php ← [hook]
│ /local/php_interface/<LANG>/<type>.menu_template.php ← [hook]
│ /local/templates/.default/<type>.menu_template.php
├─ Loc::loadUserMessages →
│ /local/php_interface/user_lang/<LANG>/lang.php ← [hook]
└─ если есть админская верхняя панель:
/local/php_interface/include/add_top_panel.php ← [hook]
│
▼
/bitrix/footer.php → SITE_TEMPLATE_PATH/footer.php
В админке (/bitrix/admin/...) дополнительно подключаются:
prolog_main_admin.php
├─ include /local/php_interface/admin_header.php
└─ include /local/php_interface/this_site_logo.php
epilog_main_admin.php
└─ include /local/php_interface/this_site_support.php
И отдельная аварийная ветка: \CDatabase::showConnectionError() подгружает dbconn_error.php, если коннект к БД не удался.
Дальше — по файлам.
dbconn.php
Самый ранний хук. Подключается в urlrewrite.php:
require_once $_SERVER["DOCUMENT_ROOT"] . getLocalPath('php_interface/dbconn.php', BX_PERSONAL_ROOT);
И в проверках окружения (site_checker.php, autocheck.php).
На этом этапе работают только нативный PHP и константы, которые вы тут же определите. Никакого use Bitrix\Main\..., никакого Composer, никаких запросов к БД — соединения ещё нет, и автозагрузки тоже.
Исторически сюда писали $DBHost/$DBLogin/$DBPassword/$DBName, $DBDebug, BX_TEMPORARY_FILES_DIRECTORY, BX_UTF, BX_CRONTAB_SUPPORT, BX_DISABLE_INDEX_PUBLIC, error_reporting(), date_default_timezone_set(), LOG_FILENAME. В свежих проектах большая часть конфигурации уехала в .settings.php (а с main 24.100.0 — также в /local/.settings.php и /local/.settings_extra.php), и в dbconn.php остаются только вещи, которые должны быть определены до старта ядра: timezone, error reporting, временные директории, BX_CRONTAB_SUPPORT.
urlrewrite.php.after_connect_d7.php (и старый after_connect.php)
Подключается из \Bitrix\Main\DB\Connection::afterConnected():
protected function afterConnected()
{
if (isset($this->configuration["include_after_connected"]) && $this->configuration["include_after_connected"] <> '')
{
include($this->configuration["include_after_connected"]);
}
}
Сам путь в конфигурацию подкладывает ConnectionPool::getConnectionParameters():
$params["include_after_connected"] = Main\Loader::getPersonal("php_interface/after_connect_d7.php");
Срабатывает сразу после успешного mysqli_real_connect() — и на каждое новое соединение, в том числе на шарды и на отдельные модульные коннекты. Поэтому единственное разумное содержание — настройка сессии MySQL/MariaDB:
$this->queryExecute("SET NAMES 'utf8mb4'");
$this->queryExecute("SET sql_mode = ''");
$this->queryExecute("SET time_zone = '+03:00'");
SELECT/UPDATE/DELETE — каждая такая строка умножится на число коннектов за хит. И, тем более, никаких SET FOREIGN_KEY_CHECKS=0 — это прямой путь к молчаливой потере целостности.after_connect.php — то же самое, но для устаревшего CDatabase старого ядра. В современных проектах это тонкий мостик из пары SET-команд, если вообще нужен.
dbconn_error.php
Включается из \CDatabase::showConnectionError(), когда ядро не смогло подключиться к дефолтной БД:
public static function showConnectionError()
{
$response = new Main\HttpResponse();
$response->setStatus('500 Internal Server Error');
$response->writeHeaders();
if (file_exists($_SERVER["DOCUMENT_ROOT"] . BX_PERSONAL_ROOT . "/php_interface/dbconn_error.php")) {
include($_SERVER["DOCUMENT_ROOT"] . BX_PERSONAL_ROOT . "/php_interface/dbconn_error.php");
} else {
echo "Error connecting to database. Please try again later.";
}
}
К моменту вызова заголовки 500 уже отправлены, тело ответа — нет. Сюда уместен вежливый HTML-экран обслуживания, заголовок Retry-After, тихая запись инцидента в файл (без БД).
Чего сюда категорически нельзя:
- Любых обращений к БД, кэшу,
Loader::includeModule(), ServiceLocator. На момент срабатывания они физически не работают. - Вывода
$DBHost/$DBLogin/$DBPassword. Этот файл показывается реальному посетителю, и любая утечка кредов — реальный взлом.
Site Checker отдельно проверяет $DBDebug = false в dbconn.php, но в dbconn_error.php подобного автотеста нет — безопасность тут держится только на дисциплине разработчика.
init.php
Самый известный хук. Из main/include.php:
if (($_fname = getLocalPath("init.php")) !== false) {
include_once $_SERVER["DOCUMENT_ROOT"] . $_fname;
}
if (($_fname = getLocalPath("php_interface/init.php", BX_PERSONAL_ROOT)) !== false) {
include_once $_SERVER["DOCUMENT_ROOT"] . $_fname;
}
if (($_fname = getLocalPath("php_interface/" . SITE_ID . "/init.php", BX_PERSONAL_ROOT)) !== false) {
include_once $_SERVER["DOCUMENT_ROOT"] . $_fname;
}
Срабатывает сразу после старта ядра, ещё до отрисовки шаблона. Уже доступны Application, Context, Culture, автозагрузка PSR-4, Loader::includeModule().
- Регистрацию старых обработчиков (
AddEventHandler('main', 'OnUserLogin', ...)), - подключение Composer проекта (
require_once /local/vendor/autoload.php), - пару глобальных функций-хелперов, переопределение
custom_mail()для альтернативной отправки писем.
- Бизнес-логику и UseCase. Их место — в сервисах модулей, а не в
init.php. - Тяжёлые операции. Файл грузится на каждый хит — включая агентов, кроны, REST-эндпойнты, админку и страницу 404. Любой фатал в
init.php— это лежащий сайт, причём чинить его придётся правкой файла на сервере, потому что админка тоже не откроется. - Прямого
$_SESSION/$_GET/$_COOKIE. В D7 для этогоApplication::getInstance()->getSession(),Context::getCurrent()->getRequest(),Cookie.
Посайтовый <SITE_ID>/init.php нужен только в многосайтовых конфигурациях — например, основной сайт s1 и партнёрский s2, у каждого свои обработчики.
init.php. Если он у вас на 100+ строк — почти наверняка половину надо разложить по модулям и обработчикам событий.site_closed.php
Включается в prolog_after.php, когда в админке стоит «Сайт остановлен» (main.site_stopped) и текущий пользователь не имеет права edit_other_settings:
if (COption::GetOptionString("main", "site_stopped", "N") == "Y" && !$USER->CanDoOperation('edit_other_settings'))
{
if (($siteClosed = getLocalPath("php_interface/" . LANG . "/site_closed.php", BX_PERSONAL_ROOT)) !== false)
include($_SERVER["DOCUMENT_ROOT"] . $siteClosed);
elseif (($siteClosed = getLocalPath("php_interface/include/site_closed.php", BX_PERSONAL_ROOT)) !== false)
include($_SERVER["DOCUMENT_ROOT"] . $siteClosed);
else
include($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/site_closed.php");
die();
}
После include — die(). Ядро не пойдёт дальше, шаблон не отрисуется.
Три места поиска:
/local/php_interface/<LANG>/site_closed.php/local/php_interface/include/site_closed.php/bitrix/modules/main/include/site_closed.php
503 + Retry-After, без обращений к БД, кэшу и модулям.И ещё одна полезная деталь: под админом страница не показывается. То есть site_closed.php срабатывает для всех, кроме вас — это удобно для отладки прямо на «остановленном» сайте.
user_lang/<LANG>/lang.php
Подключается из \Bitrix\Main\Localization\Loc::loadUserMessages():
if (($fname = Main\Loader::getLocal("php_interface/user_lang/" . $lang . "/lang.php", $documentRoot)) !== false) {
$mess = self::includeFile($fname);
foreach ($mess as $key => $val) {
$userMess[str_replace("\\", "/", realpath($documentRoot . $key))] = $val;
}
}
И аналогично — в __IncludeLang() старого tools.php.
Контракт необычный: файл возвращает массив $MESS, где ключи — пути к языковым файлам относительно DOCUMENT_ROOT, а значения — массивы переопределённых сообщений:
$MESS['/bitrix/modules/sale/lang/ru/general/order.php']['SOA_TEMPLATE_REVIEW_ORDER'] = 'Проверьте корректность заказа';
$MESS['/bitrix/modules/iblock/lang/ru/admin/iblock_edit.php']['IBLOCK_ACTIVE'] = 'Активен';
Это инструмент точечной правки сообщений ядра и модулей без правки /bitrix. Полезно, если нужно переписать корявую формулировку в админке клиента, временно потушить неудачное сообщение модуля до выхода фикса.
На этом не стоит строить основную локализацию проекта: путь к сообщению фактически становится ID, и при рефакторинге модуля переопределение тихо перестанет работать. Для своих текстов — lang/<LANG>/... в собственном модуле или компоненте.
.menu_template.php
Шаблон вывода меню. Из \CMenu::Init():
if (defined("SITE_TEMPLATE_PATH") && file_exists($_SERVER["DOCUMENT_ROOT"].SITE_TEMPLATE_PATH."/".$this->type.".menu_template.php"))
$this->template = SITE_TEMPLATE_PATH."/".$this->type.".menu_template.php";
elseif (file_exists($_SERVER["DOCUMENT_ROOT"].BX_PERSONAL_ROOT."/php_interface/".LANG."/".$this->type.".menu_template.php"))
$this->template = BX_PERSONAL_ROOT."/php_interface/".LANG."/".$this->type.".menu_template.php";
else
$this->template = BX_PERSONAL_ROOT."/templates/.default/".$this->type.".menu_template.php";
<type> — это top, left, bottom и т.п. Приоритет: шаблон сайта → php_interface/<LANG>/ → templates/.default/.
В реальности это легаси. Старая шаблонная система меню. На новых проектах используют компонент bitrix:menu со своим шаблоном внутри templates/<TEMPLATE_ID>/components/bitrix/menu/<template>/, и *.menu_template.php в php_interface оказывается случайно перебивающим артефактом.
Несколько раз встречал на проектах, что меню второго сайта рисовалось шаблоном первого сайта, потому что <LANG> совпадал, а файл лежал в php_interface/<LANG>/ ещё с 2018 года. Если унаследовали проект — пройдитесь по этому каталогу с ревизией.
admin_header.php
Подключается в prolog_main_admin.php:
if (($adminHeader = getLocalPath("php_interface/admin_header.php", BX_PERSONAL_ROOT)) !== false) {
include($_SERVER["DOCUMENT_ROOT"] . $adminHeader);
}
Срабатывает в начале каждой админской страницы, включая фреймы CKEditor, попапы выбора файлов и AJAX-обработчики. Поэтому туда — только лёгкое: глобальные <style>/<script> для админки клиента, баннер «вы на staging» с цветной полосой, тег Sentry.
Никаких LocalRedirect() и тяжёлых проверок прав. Иначе попап выбора файла превратится в редирект-фокус, и пользоваться визуальным редактором станет невозможно.
this_site_logo.php
Подключается в боковой панели админки (тоже из prolog_main_admin.php):
if (file_exists($_SERVER["DOCUMENT_ROOT"].BX_PERSONAL_ROOT."/php_interface/this_site_logo.php")) {
include($_SERVER["DOCUMENT_ROOT"].BX_PERSONAL_ROOT."/php_interface/this_site_logo.php");
}
Маленький HTML с логотипом партнёра-интегратора и ссылкой на партнёра. Часть «брендирования» админки клиента, и да, это ровно то, что вы видели в админке любого сайта, разработанного партнёром.
/bitrix/php_interface/.this_site_support.php
Подключается в подвале админки (epilog_main_admin.php) и на странице авторизации (auth/wrapper.php):
if (($siteSupport = getLocalPath("php_interface/this_site_support.php", BX_PERSONAL_ROOT)) !== false) {
include($_SERVER["DOCUMENT_ROOT"] . $siteSupport);
}
Контактная информация партнёра, телефон тех. поддержки, ссылка на хелпдеск. Этот файл специально проверяется чек-листом партнёрского аудита (QJ0030.html, QJ0040.html) — если вы интегратор, его обязательно нужно заполнить, иначе на сертификации будут вопросы.
include/add_top_panel.php
Плашка вверху страницы, которую видит залогиненный администратор. Из main/public/top_panel.php:
if (file_exists($_SERVER["DOCUMENT_ROOT"].BX_PERSONAL_ROOT . "/php_interface/include/add_top_panel.php")) {
include($_SERVER["DOCUMENT_ROOT"] . BX_PERSONAL_ROOT . "/php_interface/include/add_top_panel.php");
}
Сюда удобно добавить кастомные кнопки: «Открыть тикет», «Перейти в свою аналитику», «Сообщить о проблеме». Видно их только людям с правом на админку, поэтому можно не стесняться давать ссылки на внутренние сервисы.
/bitrix/php_interface/.Модульные хуки
Кроме «общих» точек, отдельные модули тоже ищут файлы по знакомому паттерну. Перечислю кратко — пусть будут под рукой, когда понадобятся.
/local/php_interface/im_options.php — modules/im/default_option.php. Возвращайте массив с переопределёнными опциями модуля Сообщения и Уведомления (например, ограничения на размер вложений и список разрешённых URL).
/local/php_interface/pull.php — аналогично для модуля pull (push-сервер).
/local/php_interface/include/1c_mutator.php — обмен с 1С (catalog/load_import/commerceml_run.php). Подключается перед обработкой импорта CommerceML; туда кладут «мутаторы»: нормализацию артикулов, фильтрацию номенклатуры, исправление кодировок.
/local/php_interface/subscribe/templates/ — шаблоны рассылок модуля subscribe.
/local/php_interface/lists/group_lists.php (модуль lists) и /local/php_interface/wiki/group_index.php (модуль wiki) — точки расширения интеграции с соцсетью; история про корпоративный портал.
/local/php_interface/include/catalog_import/cron_frame.php и /local/php_interface/include/catalog_export/cron_frame.php — каркасы для запуска импорта/экспорта каталога по cron, копируются туда модулем catalog при установке.
/local/php_interface/.styles.php — массив CSS-классов для WYSIWYG-редактора в админке (модуль fileman). Если хотите видеть в визуальном редакторе пункты вроде «Цитата клиента / Уточнение / Sale-баннер» — это сюда.
Как держать /local/php_interface/ в порядке
Раз в полгода полезно пройтись по списку.
- В
dbconn.php—$DBDebug = false, нет ничего, что требует автозагрузки D7. - В
after_connect_d7.php— толькоSET-команды. dbconn_error.php— 503/500 +Retry-After, без БД, без кредов.init.phpкороче 100 строк. Если больше — надо выносить в модули.site_closed.php(любой из вариантов) автономен, безLoader::includeModule().user_lang/<LANG>/lang.php— точечный инструмент, не основная локализация.<type>.menu_template.phpвphp_interface/<LANG>/— либо нужны и согласованы с актуальным шаблоном сайта, либо удалены.admin_header.php,this_site_logo.php,this_site_support.phpсоответствуют брендингу клиента (актуально для партнёрского аудита).- Модульные хуки (
im_options.php,pull.php,1c_mutator.php...) — задокументированы в README проекта. - Для любого файла в каталоге у вас есть ответ на вопрос «где в ядре это включается». Если ответа нет — либо удалите, либо разберитесь.
Закрывая тему
В Битриксе две системы расширения: явная (события, ServiceLocator, контроллеры, роуты, фильтры) и тихая — файлы под /local/php_interface/ и в шаблоне сайта. Тихая старше и часто удобнее: один файл, одна точка ответственности, никакой регистрации.
Беда в том, что незаметная система копит легаси быстрее всего. Прошёл подрядчик — оставил top.menu_template.php в php_interface/<LANG>/. Прошёл другой — раздул init.php на 2 000 строк. Третий положил dbconn_error.php с $DBPassword. Раз в полгода стоит пройтись по списку выше, и проблем заметно меньше.
Если вы знаете о хуке, которого не оказалось в карте, — напишите в комментарии или личку, добавлю.
Комментарии (0)
Пожалуйста, войдите в аккаунт, чтобы оставить комментарий
Оставить комментарийПока нет ни одного комментария. Будьте первым!
Похожие статьи