10.05.2026 15 мин чтения

Тихие хуки Битрикса: вспомогательные файлы, которые ядро подключает за вас (и как ими пользоваться)

Кирилл Новожилов

Кирилл Новожилов

Автор

Тихие хуки Битрикса: вспомогательные файлы, которые ядро подключает за вас (и как ими пользоваться)
Введение

Про 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(). Ядро не пойдёт дальше, шаблон не отрисуется.

Три места поиска:

  1. /local/php_interface/<LANG>/site_closed.php
  2. /local/php_interface/include/site_closed.php
  3. /bitrix/modules/main/include/site_closed.php
Что положить Автономный HTML с 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>/... в собственном модуле или компоненте.

Шаблон вывода меню. Из \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.phpmodules/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/ в порядке

Раз в полгода полезно пройтись по списку.

  1. В dbconn.php$DBDebug = false, нет ничего, что требует автозагрузки D7.
  2. В after_connect_d7.php — только SET-команды.
  3. dbconn_error.php — 503/500 + Retry-After, без БД, без кредов.
  4. init.php короче 100 строк. Если больше — надо выносить в модули.
  5. site_closed.php (любой из вариантов) автономен, без Loader::includeModule().
  6. user_lang/<LANG>/lang.php — точечный инструмент, не основная локализация.
  7. <type>.menu_template.php в php_interface/<LANG>/ — либо нужны и согласованы с актуальным шаблоном сайта, либо удалены.
  8. admin_header.php, this_site_logo.php, this_site_support.php соответствуют брендингу клиента (актуально для партнёрского аудита).
  9. Модульные хуки (im_options.php, pull.php, 1c_mutator.php...) — задокументированы в README проекта.
  10. Для любого файла в каталоге у вас есть ответ на вопрос «где в ядре это включается». Если ответа нет — либо удалите, либо разберитесь.

Закрывая тему

В Битриксе две системы расширения: явная (события, ServiceLocator, контроллеры, роуты, фильтры) и тихая — файлы под /local/php_interface/ и в шаблоне сайта. Тихая старше и часто удобнее: один файл, одна точка ответственности, никакой регистрации.

Беда в том, что незаметная система копит легаси быстрее всего. Прошёл подрядчик — оставил top.menu_template.php в php_interface/<LANG>/. Прошёл другой — раздул init.php на 2 000 строк. Третий положил dbconn_error.php с $DBPassword. Раз в полгода стоит пройтись по списку выше, и проблем заметно меньше.

Если вы знаете о хуке, которого не оказалось в карте, — напишите в комментарии или личку, добавлю.

Опубликовано 1 день назад

Комментарии (0)

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

Оставить комментарий

Пока нет ни одного комментария. Будьте первым!

Похожие статьи

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