Для AJAX в модулях Битрикс до сих пор плодят /local/ajax/*.php, ручной echo json_encode и проверку sessid «на глаз». Штатный контракт ядра — Bitrix\Main\Engine\Controller: action-методы, prefilters, единый JSON-ответ и вызов из JS через BX.ajax.runAction.
Рядом по теме: AJAX в компонентах, REST API на D7, Application / Context / Request, Web\Json, Result / Error, HttpClient. Ниже — чем Controller отличается от component ajax, как настроить configureActions, вызвать action из JS и разобрать типичные 400/«пустой» ответ.
Controller vs component ajax vs старый ajax.php
Три слоя, которые часто путают:
- Отдельный
ajax.php— произвольный скрипт под document root. Нет prefilters, легко забыть CSRF и права, сложно тестировать. Для нового кода — антипаттерн. - AJAX компонента —
AJAX_MODE, веткаisAjaxRequest()илиBX.ajax.runComponentActionк контроллеру компонента. Удобно, когда логика живёт рядом с UI-блоком (см. гайд по AJAX в компонентах). Engine\Controllerмодуля — action’ы модуля, вызовBX.ajax.runAction('vendor:module.controller.action')или HTTP-маршрут. Это правильное место для переиспользуемой бизнес-логики, админки, виджетов и API «внутри сайта».
Правило: UI-перерисовка списка — компонент; сохранение сущности, права, валидация, общий API — Controller модуля. REST «наружу» с префиксом api/v1 — отдельный слой маршрутов поверх тех же контроллеров (REST API D7).
Минимальный контроллер модуля
<?php
namespace Vendor\Demo\Controller;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Engine\ActionFilter;
use Bitrix\Main\Engine\CurrentUser;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
class Item extends Controller
{
public function configureActions(): array
{
return [
'get' => [
'prefilters' => [
new ActionFilter\Authentication(),
new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_GET]),
],
],
'save' => [
'prefilters' => [
new ActionFilter\Authentication(),
new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
new ActionFilter\Csrf(),
],
],
// публичный read-only без авторизации — явно снимаем дефолтные фильтры
'publicList' => [
'prefilters' => [
new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_GET]),
],
],
];
}
public function getAction(int $id): ?array
{
if (!Loader::includeModule('vendor.demo')) {
$this->addError(new Error('Module vendor.demo is not installed'));
return null;
}
$userId = (int)CurrentUser::get()->getId();
// проверка прав — в action или через кастомный prefilter
if ($userId <= 0) {
$this->addError(new Error('Access denied', 'ACCESS_DENIED'));
return null;
}
return [
'id' => $id,
'name' => 'Demo item',
'userId' => $userId,
];
}
public function saveAction(string $name, int $id = 0): ?array
{
$name = trim($name);
if ($name === '') {
$this->addError(new Error('Name is required', 'VALIDATION'));
return null;
}
// ... сохранение ...
return ['id' => $id > 0 ? $id : 42, 'name' => $name];
}
}
Имя action в конфиге — без суффикса Action: ключ get соответствует методу getAction. Класс кладут в namespace контроллеров модуля (см. раздел про маршрутизацию ниже).
configureActions, prefilters и дефолты
Метод configureActions() возвращает карту «имя action → опции». Главное поле — prefilters: цепочка проверок до вызова метода. Базовые фильтры ядра:
ActionFilter\Authentication— пользователь должен быть авторизован;ActionFilter\HttpMethod— только указанные HTTP-методы (GET/POST/…);ActionFilter\Csrf— проверка sessid / заголовка CSRF;ActionFilter\ContentType— при необходимости ограничить Content-Type.
Если для action не задать prefilters явно, у Controller сработают дефолтные prefilters родителя (в типичной конфигурации ядра это включает аутентификацию и CSRF). Поэтому «открытый» публичный action нужно описывать осознанно: перечислить только нужные фильтры (часто — один HttpMethod), а не надеяться, что пустой массив «ничего не проверяет» во всех версиях одинаково. При сомнении смотрите getDefaultPreFilters() в вашей версии main.
Postfilters (postfilters) применяют уже к результату — реже нужны в прикладном коде. Для кастомной проверки прав удобнее свой класс, наследник ActionFilter\Base, или явная проверка CurrentUser / $USER->CanDoOperation в начале action.
Что возвращает action: массив, Result, ошибки
Успешный action обычно возвращает array — Engine сам упакует его в JSON-поле data. Можно вернуть объект Bitrix\Main\Result / наследников — контракт успеха и ошибок разобран в статье про Result и Error.
Ошибки добавляйте через $this->addError(new Error(...)) и возвращайте null (или Result с ошибками). Не бросайте «голый» Exception с HTML-текстом — фронт ждёт структуру errors[]. Для контролируемых отказов с кодом статуса иногда используют исключения Engine (в т.ч. связанные с JSON-ответом) — главное не смешивать это с ручным echo.
<?php
use Bitrix\Main\Error;
public function deleteAction(int $id): ?array
{
if ($id <= 0) {
$this->addError(new Error('Invalid id', 'VALIDATION'));
return null;
}
if (!$this->canDelete($id)) {
$this->addError(new Error('Access denied', 'ACCESS_DENIED'));
return null;
}
// delete...
return ['deleted' => true, 'id' => $id];
}
Нельзя внутри action писать echo Json::encode(...); die; — Engine уже сериализует return. Двойная кодировка и «битый» ответ — классика отладки в 2 часа ночи. Ручной Web\Json оставляйте вне Engine (свой webhook, отдача файла) — см. Web\Json.
Вызов из JS: BX.ajax.runAction
Штатный клиент — BX.ajax.runAction. Имя действия: vendor:module.controllerName.actionName (модуль с точкой в id, контроллер в camelCase/как зарегистрирован, action без суффикса Action):
BX.ajax.runAction('vendor:demo.item.get', {
data: { id: 15 },
// method: 'GET' // при необходимости; по умолчанию POST
}).then(function (response) {
// response.data — то, что вернул getAction
console.log(response.data);
}).catch(function (response) {
console.error(response.errors);
});
// Изменяющее действие — POST + CSRF закроет prefilter
BX.ajax.runAction('vendor:demo.item.save', {
data: {
id: 15,
name: 'Новое имя',
},
}).then(function (response) {
BX.UI.Notification.Center.notify({
content: 'Сохранено: #' + response.data.id,
});
}).catch(function (response) {
var msg = (response.errors || []).map(function (e) {
return e.message;
}).join('; ') || 'Ошибка сохранения';
alert(msg);
});
CSRF для браузерных запросов Bitrix подставляет сам (sessid / заголовок). Если вызываете action «голым» fetch — передайте sessid / X-Bitrix-Csrf-Token, как в примерах из AJAX в компонентах. Для server-to-server лучше отдельный токен и свой prefilter, а не отключение CSRF «для всех».
Маршрутизация: .settings.php, Controllers, routes
Чтобы Engine нашёл класс, модуль должен объявить namespace контроллеров. В типовой схеме в .settings.php модуля (или эквивалентной конфигурации) задают секцию controllers / defaultNamespace — например Vendor\Demo\Controller. Тогда короткое имя item в runAction резолвится в Vendor\Demo\Controller\Item.
HTTP-маршруты (красивые URL вида /api/...) подключают через routes.php / RoutingConfigurator — это уже слой REST, не обязательный для runAction. Краткий каркас маршрутов и связка с Controller — в статье про REST API D7. Для внутреннего AJAX админки и публички достаточно корректного namespace + Loader::includeModule.
<?php
// фрагмент идеи .settings.php модуля (уточняйте ключи под вашу версию ядра)
return [
'controllers' => [
'value' => [
'defaultNamespace' => '\\Vendor\\Demo\\Controller',
],
'readonly' => true,
],
];
CurrentUser и проверка прав
Bitrix\Main\Engine\CurrentUser::get() — удобный доступ к текущему пользователю внутри action без глобального $USER. Для прав не ограничивайтесь «авторизован = можно»: проверяйте операции модуля, принадлежность сущности, роль в проекте.
<?php
use Bitrix\Main\Engine\CurrentUser;
use Bitrix\Main\Error;
public function updateAction(int $id, array $fields): ?array
{
$user = CurrentUser::get();
if (!$user->isAdmin() && !$this->isOwner($id, (int)$user->getId())) {
$this->addError(new Error('Access denied', 'ACCESS_DENIED'));
return null;
}
// Request/контекст при необходимости:
// $request = \Bitrix\Main\Application::getInstance()->getContext()->getRequest();
// см. статью Application / Context / Request
return $this->doUpdate($id, $fields);
}
Контекст запроса (getRequest(), метод, заголовки) берите из Application/Context — подробности в гайде по HTTP-контексту. Исходящие вызовы к внешним API из action — через HttpClient с таймаутами, не через «голый» curl без обработки ошибок.
Типичные ошибки
- CSRF / 400 — забыли prefilter
Csrfна POST или наоборот вызвали без sessid из кастомного JS. Смотрите Network: статус и телоerrors. - Wrong HTTP method — action разрешён только GET, а
runActionшлёт POST (или наоборот). Явно задайтеHttpMethodи метод на клиенте. - Module not installed / class not found — не вызвали
Loader::includeModule, опечатка в id модуля, неверныйdefaultNamespaceв настройках контроллеров. - Wrong action name — в JS указали
getActionвместоget, или неверный сегмент контроллера (itemvsItemController). - Сессия / auth на публичном ajax — на кешируемой витрине или без куки сессии
Authenticationзакономерно падает. Для гостя нужен отдельный action без этого фильтра и жёсткая проверка входных данных. - Двойной JSON —
echo json_encodeвнутри action плюс сериализация Engine. Возвращайте массив/Result, не печатайте тело сами. - Ошибки «проглатываются» — на фронте читают только
response.dataи игнорируютcatch/errors. Всегда обрабатывайте reject.
Чеклист перед выкладкой
- У каждого изменяющего action есть
Authentication(если нужно) +HttpMethod+Csrf. - Публичные action перечислены явно, без лишних прав и без утечки PII.
- Имя в
runActionсовпадает с модулем/контроллером/action; модуль подключается. - В action нет
echo/dieс JSON; ошибки — черезError. - Права проверены относительно сущности, а не только «залогинен».
- Фронт показывает
errors[].message; в Network виден предсказуемый контракт.
Вывод
Bitrix\Main\Engine\Controller — базовый способ делать AJAX и внутренние API модулей: configureActions + prefilters, методы *Action, вызов BX.ajax.runAction. Для перерисовки UI компонента оставайтесь на component ajax; для REST наружу добавьте маршруты. Сериализацию в Engine не дублируйте, ошибки отдавайте через Error/Result, HTTP-контекст и исходящие запросы — через Application и HttpClient. Так проще сопровождать код и ловить 400 до продакшена, а не после.
