Ядро D7 в 1С-Битрикс - Application, запрос, контроллеры
Ядро D7 - это пространство имён Bitrix\Main, на котором держится всё
современное в 1С-Битрикс. Вместо суперглобальных массивов и функций с префиксом
C здесь объекты: приложение, контекст хита, запрос, ответ, контроллеры и
события. Разберём базовые классы, которые нужны в любой задаче.
Как это работает
Приложение и контекст - разные вещи. Application - это singleton и точка
входа к глобальным сущностям ядра: соединения с БД, кеш, документ-рут. Он не
зависит от конкретного запроса. Context, наоборот, привязан к хиту и хранит
запрос, ответ, серверные параметры, язык, культуру и SITE_ID. Схема доступа
всегда одна: Application::getInstance() → getContext() →
getRequest()/getResponse(). Для контекста есть короткая форма
Context::getCurrent().
Запрос и ответ - объекты. Вместо $_GET, $_POST и $_COOKIE в D7
работают с HttpRequest: типизированные геттеры getQuery(), getPost(),
getFile(), getCookie(), getHeader() и признаки запроса isPost(),
isAjaxRequest(), isHttps(), isAdminSection(). Ответ - это HttpResponse и
его специализированные наследники: Json, AjaxJson, File, Redirect.
Как запрос доходит до кода. Современный путь - контроллеры
Bitrix\Main\Engine\Controller. Методы вида *Action() принимают типизированные
аргументы прямо из запроса, вокруг них работают пре- и постфильтры (CSRF,
HTTP-метод, аутентификация), а вызывается действие либо через AJAX-эндпоинт
BX.ajax.runAction, либо через объявленный маршрут. Физические страницы .php
с IncludeComponent - легаси-подход.
Слабая связанность. Модуль подключают через Loader::includeModule().
Настройки модуля в базе живут в Config\Option, файловая конфигурация ядра - в
.settings.php через Config\Configuration. Расширяемость обеспечивают события
(Event, EventManager, EventResult) и контейнер зависимостей
DI\ServiceLocator.
Контракт результата. Операции возвращают Result - объект, который несёт
одновременно данные и коллекцию ошибок. Это лучше и массива со служебными
флагами, и исключения: исключение не умеет передать несколько ошибок сразу.
Примеры
1. Приложение, контекст, запрос
use Bitrix\Main\Application;use Bitrix\Main\Context;
$request = Context::getCurrent()->getRequest();
$page = (int)$request->getQuery('page'); // только из query string$name = (string)$request->getPost('NAME'); // только из тела формы$token = $request->getHeader('X-Api-Token'); // заголовок, не $_SERVER
if ($request->isPost() && $request->isAjaxRequest()) { // ...}
// документ-рут - статический метод, контекст - нестатический$root = Application::getDocumentRoot();$siteId = Context::getCurrent()->getSite(); // 's1'Ключевое правило: читайте точный источник данных. getQuery() и getPost()
разделены намеренно, а общий get() смешивает их - это compatibility-путь,
уместный только когда эндпоинт по контракту принимает merged-параметры.
Частая путаница: у класса Application часть методов статические
(getConnection, getDocumentRoot, isUtfMode), а часть - нет
(getContext, getCache, getManagedCache, addBackgroundJob). Последние
доступны только через getInstance().
2. Подключение модуля
use Bitrix\Main\Loader;
// зависимость обязательна - пусть падает сразу и с понятным сообщениемLoader::requireModule('iblock');
// зависимость реально опциональна - есть осмысленная веткаif (Loader::includeModule('crm')) { $this->syncWithCrm();}Разница принципиальная. includeModule() возвращает bool, и конструкция
if (Loader::includeModule('iblock')) вокруг кода, который без модуля вообще не
имеет смысла, просто прячет проблему: сценарий молча ничего не сделает. Когда
без модуля продолжать нельзя, берите requireModule().
3. Тонкое действие контроллера
use Bitrix\Main\Engine\Controller;use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Authentication;use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\HttpMethod;use Bitrix\Main\Result;
final class ExampleController extends Controller{ #[Authentication] #[HttpMethod([HttpMethod::METHOD_POST])] public function saveAction(ExampleService $service, int $id): ?array { /** @var Result $result */ $result = $service->save($id);
if (!$result->isSuccess()) { foreach ($result->getErrors() as $error) { $this->addError($error); } return null; // клиент получит {"status":"error","errors":[...]} }
return $result->getData(); }}Что здесь происходит:
- Аргументы
$idи$serviceзаполняются автоматически: скаляр берётся из запроса по имени и типу, сервис приходит автоварингом. Если обязательный аргумент не пришёл, действие вернёт ошибкуCould not find value for parameter. - Доступ и HTTP-метод заданы атрибутами-фильтрами декларативно. По умолчанию каждое действие и так требует CSRF-токен, метод и аутентификацию - подробный разбор этих механизмов в статье об основах безопасности.
- Возвращённый массив ядро само обернёт в
Response\AjaxJsonс конвертомstatus,data,errors. Для чистого JSON естьResponse\Json, для HTML или заголовков -HttpResponse. - Текущего пользователя берут через
CurrentUser, а не через global$USER.
Вызывают действие из JS по имени, а не по URL:
BX.ajax.runAction('vendor:module.Example.save', { data: { id: 42 } });4. Маршруты
<?phpuse Bitrix\Example\Infrastructure\Controller\DocumentController;use Bitrix\Main\Routing\RoutingConfigurator;
return static function (RoutingConfigurator $routes) { $routes ->prefix('api/v1/documents') ->group(function (RoutingConfigurator $routes) { $routes->get('{documentId}/', [DocumentController::class, 'get']) ->where('documentId', '[0-9]+') ->name('Example.Document.Get');
$routes->post('/', [DocumentController::class, 'create']) ->name('Example.Document.Create'); });};Файл кладут в /local/routes/web.php и подключают в .settings.php в секции
routing. Три вещи, которые ломают маршруты чаще всего: сопоставление идёт
first-match-wins, поэтому catch-all-маршрут обязан объявляться последним; правила
из legacy-файла urlrewrite.php проверяются раньше современного роутинга; имена
маршрутов должны быть уникальны среди всех файлов, иначе дубликат перезапишет
предыдущий и сломает генерацию ссылок.
5. Результат операции: Result и Error
use Bitrix\Main\Result;use Bitrix\Main\Error;
public function save(int $id): Result{ $result = new Result();
$user = UserTable::getByPrimary($id)->fetchObject(); if ($user === null) { // стабильный прикладной код, а не 0 и не текст сообщения return $result->addError(new Error('Пользователь не найден', 'USER_NOT_FOUND')); }
$nested = $this->repository->persist($user); if (!$nested->isSuccess()) { return $result->addErrors($nested->getErrors()); // пробрасываем как есть }
return $result->setData(['id' => $user->getId()]);}Проверяйте isSuccess() перед getData() и делайте ранний возврат на
ошибочном пути. Вложенный сбой пробрасывайте через addErrors(), не схлопывая
список ошибок в одну строку. И не отдавайте наружу сырое $e->getMessage() -
сначала преобразуйте его в безопасный Error.
6. События
// отправка$event = new \Bitrix\Main\Event('mymodule', 'OnMacrosProductCreate', [$basketId]);$event->send();
// разбор результатов обработчиковforeach ($event->getResults() as $eventResult) { if ($eventResult->getResultType() === \Bitrix\Main\EventResult::ERROR) { return false; // обработчик запретил операцию }}// обработчикfunction OnMacrosProductCreate(\Bitrix\Main\Event $event) { $params = $event->getParameters(); return new \Bitrix\Main\EventResult(\Bitrix\Main\EventResult::SUCCESS, $basketId);}Обработчики модуля регистрируют один раз при установке через
EventManager::registerEventHandler() - они хранятся в базе и снимаются при
удалении модуля. Динамический addEventHandler() действует только на текущий
хит и обычно ставится в init.php.
Отдельно про старые события вроде OnBeforeUserAdd: у них нет объекта Event,
поэтому нужен registerEventHandlerCompatible(). Новые и *Compatible-методы
дают обработчику разные сигнатуры, и путаница здесь - типичная причина ошибок.
7. Куки, сессия, HTTP-клиент
use Bitrix\Main\Web\Cookie;use Bitrix\Main\Context;
$cookie = new Cookie('example_cookie', 'value', time() + 3600);$cookie->setPath('/');Context::getCurrent()->getResponse()->addCookie($cookie); // запись - в ответ
$value = Context::getCurrent()->getRequest()->getCookie('example_cookie'); // чтение - из запросаТолько что установленная cookie на текущем хите ещё не видна: сервер прочитает
её со следующего запроса клиента. Для данных, которые нельзя раскрывать клиенту,
есть шифрованные Web\CryptoCookie.
$session = \Bitrix\Main\Application::getInstance()->getSession();$session->set('foo', 'bar');
// сессионный кеш с изоляцией по session_id, без блокировки основной сессии$storage = \Bitrix\Main\Application::getInstance()->getLocalSession('someCategory');$storage->set('productIds', [1, 2, 100]);use Bitrix\Main\Web\HttpClient;use Bitrix\Main\Web\Json;
$http = new HttpClient(['socketTimeout' => 5, 'streamTimeout' => 10]);$http->setHeader('Content-Type', 'application/json');
$body = $http->post($url, Json::encode(['id' => 42]));
if ($body === false) { $this->log($http->getError()); // транспортная ошибка} elseif ($http->getStatus() !== 200) { $this->log('HTTP ' . $http->getStatus()); // непустое тело - не признак успеха}Таймауты задавайте явно, а транспортную ошибку и HTTP-статус проверяйте
раздельно. Если URL приходит от пользователя, включайте SSRF-safe режим
setPrivateIp(false).
Справочник API
Приложение, контекст, запрос
| API | Назначение | Особенности |
|---|---|---|
Application::getInstance() | singleton приложения | нестатические: getContext, getCache, getTaggedCache, addBackgroundJob |
Application::getDocumentRoot() | корень сайта | статический; вместо $_SERVER['DOCUMENT_ROOT'] |
Context::getCurrent() | контекст хита | даёт getRequest, getResponse, getServer, getSite, getLanguage, getCulture |
HttpRequest::getQuery() / getPost() / getFile() | точные источники данных | общий get() смешивает query и body |
HttpRequest::getHeader() / getCookie() | заголовки и куки | вместо $_SERVER['HTTP_*'] |
HttpRequest::isPost() / isAjaxRequest() / isAdminSection() | признаки запроса | |
HttpRequest::getInput() | сырое тело запроса | единственный статический геттер запроса |
Application::getInstance()->getSession() | сессия как объект | вместо $_SESSION; есть getLocalSession() для кеша |
Ответ
| API | Назначение | Особенности |
|---|---|---|
HttpResponse::addHeader() / setStatus() | заголовки и код ответа | вместо ручного header() |
Response\Json | чистый JSON | без конверта |
Response\AjaxJson | JSON с конвертом status, data, errors | формат, который ждёт BX.ajax |
Response\File | отдача файла | вместо ручных Content-Type и Content-Disposition |
Response\Redirect | редирект | вместо ручного заголовка Location |
Web\Cookie / Web\CryptoCookie | куки, в том числе шифрованные | CryptoCookie требует crypto_key в .settings.php |
Контроллеры и роутинг
| API | Назначение | Особенности |
|---|---|---|
Engine\Controller | базовый класс контроллера | публичный контракт - методы *Action() |
ActionFilter\Attribute\Rule\* | фильтры доступа как атрибуты | по умолчанию требуются CSRF, метод, аутентификация |
getAutoWiredParameters() | подстановка своих объектов в действие | штатно подставляются CurrentUser, PageNavigation, JsonPayload |
$this->addError(new Error(...)) | ошибка из действия | вернуть null, клиент получит status: error |
Engine\UrlManager::getInstance()->create() | ссылка на действие | вместо ручной сборки URL |
RoutingConfigurator | объявление маршрутов | /local/routes/web.php, подключается в секции routing |
Router::route('name', [...]) | ссылка по имени маршрута | имена уникальны среди всех файлов |
Конфигурация, события, сервисы
| API | Назначение | Особенности |
|---|---|---|
Loader::requireModule() / includeModule() | подключение модуля | первый - fail-fast, второй - для опциональной зависимости |
Config\Option::get() / set() | настройки модуля в базе | хранит строки; set() дорогой, не для часто меняющегося состояния |
Option::getRealValue() | отличить сохранённое от значения по умолчанию | вернёт null, если в базе ничего нет |
Config\Configuration | чтение .settings.php | после add() обязателен saveConfiguration() |
EventManager::registerEventHandler() | постоянный обработчик | хранится в базе, снимается при удалении модуля |
EventManager::addEventHandler() | обработчик на текущий хит | обычно в init.php |
EventResult | ответ обработчика | типы SUCCESS, ERROR, UNDEFINED |
DI\ServiceLocator::get() | получение сервиса | по умолчанию кеширует singleton |
Result / Error / ErrorCollection | результат операции | getErrorByCode() может вернуть null |
Web\HttpClient | внешние HTTP-запросы | вместо file_get_contents() и curl_* |
Type\Date / Type\DateTime | даты с учётом часовых поясов | tryParse() для пользовательского ввода |
Localization\Loc::getMessage() | языковые фразы | вне компонентов нужен Loc::loadMessages(__FILE__) |
Частые ошибки
Статический вызов нестатического метода. Симптом: фатальная ошибка на
Application::getContext() или HttpRequest::getCookie(). У Application
статические только getConnection, getDocumentRoot, isUtfMode и ещё
несколько; у запроса статический только getInput(). Всё остальное - через
экземпляр.
Только что установленная cookie не читается. Симптом: записали и тут же пытаетесь прочитать - пусто. Так и должно быть: значение придёт со следующим запросом клиента.
Изменения сессии пропадают. Симптом: записали в сессию, на следующем хите
данных нет. Причина - режим BX_SECURITY_SESSION_READONLY или вызов
CloseSession: они ускоряют работу, но запись в сессию после этого не
сохраняется.
Маршрут не срабатывает. Три причины по частоте: тот же URL перекрыт
правилом в urlrewrite.php, который проверяется первым; catch-all-маршрут
объявлен раньше узких; не сброшен кеш роутинга после правки файла.
Настройки ведут себя странно. Симптом: Option::get() возвращает не то, что
записали. Помните, что имя параметра приводится к нижнему регистру, значения
хранятся строками, а длины ограничены: 50 символов для имени модуля и параметра,
2000 для значения.
HTTP-запрос считается успешным по непустому телу. Симптом: интеграция
«работает», но данные не те. Проверять нужно раздельно: === false плюс
getError() для транспортной ошибки и getStatus() для HTTP-кода. В
PSR-18-режиме клиент к тому же не переходит по редиректам сам.
Дата в логах не серверная. При включённых часовых поясах echo $dateTime
выводит время в поясе пользователя. Для логов и системных событий вызывайте
disableUserTime(). И учтите, что Date::add() мутирует сам объект, а не
возвращает новый.
Частые вопросы
Чем Application отличается от Context?
Application - это singleton приложения, он живёт независимо от конкретного запроса и даёт доступ к соединениям с базой, кешу и корню сайта. Context привязан к текущему хиту и хранит запрос, ответ, сервер, язык, культуру и идентификатор сайта. Для HTTP-хитов используются HttpApplication и HttpContext, для командной строки - CliContext.
Можно ли читать $_GET и $_POST напрямую?
Технически да, но в новом коде не нужно. Объект запроса даёт типизированные геттеры и явно разделяет источники: getQuery() для query string, getPost() для тела формы, getHeader() для заголовков, getCookie() для кук. Это снимает целый класс ошибок, когда параметр случайно приходит не оттуда, откуда ожидали. Важно понимать и обратное: значение из запроса проходит фильтры безопасности, но само по себе безопасным не становится - экранирование зависит от того, куда вы его выводите.
Контроллер или физическая страница с компонентом?
Для нового кода - контроллер. Он даёт типизированные аргументы действий, декларативные фильтры (CSRF, HTTP-метод, аутентификация), единый формат ответа и работает как через AJAX, так и через маршруты. Физические страницы с IncludeComponent остаются легаси-подходом, хотя продолжают работать.
Зачем нужен Result, если есть исключения?
Исключение передаёт ровно одну проблему и прерывает поток. Result несёт данные и коллекцию ошибок одновременно, поэтому подходит там, где нужно вернуть сразу несколько ошибок валидации или частичный результат. Правило простое: проверяйте isSuccess() до getData(), а вложенные сбои пробрасывайте через addErrors(), не превращая их в строку.
Где регистрировать обработчик события - в init.php или при установке модуля?
Обработчики модуля правильнее регистрировать один раз при установке через registerEventHandler(): они хранятся в базе, их проще найти и они снимаются при удалении модуля. Динамический addEventHandler() действует только на текущий хит и подходит для разовых обработчиков и отдельных точек расширения; обычно его ставят в init.php.
Связанные темы
- D7 ORM - работа с базой через сущности
- Компоненты 2.0 - вывод данных на страницу
- Основы безопасности - фильтрация ввода, CSRF, права
- Подсистемы ядра - готовые сервисы поверх описанного фундамента
- Пользователи, группы и права - текущий пользователь и события авторизации
- Обработчик события: регистрация, аргументы, отмена действия - события на практике
- События на практике - решения по обработчикам и журналу
- AJAX-запрос: контроллер, свой файл и ответ в JSON - контроллеры на практике
- Раздел Ядро D7
- Файлы в коде - хранилище платформы и свои каталоги
- Даты и время - хранение, пояса и границы периодов