Перейти к содержимому

Ядро 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. Маршруты

<?php
use 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\AjaxJsonJSON с конвертом 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.

Связанные темы

Первоисточники