AJAX-запросы в Битрикс: контроллеры в модулях и компонентах
AJAX-запросы — неотъемлемая часть современного веб-приложения. Они позволяют обновлять данные на странице без перезагрузки, создавать интерактивные интерфейсы и улучшать пользовательский опыт. В 1С-Битрикс для обработки AJAX-запросов используется механизм контроллеров — удобный и безопасный способ связать JavaScript-код на фронтенде с PHP-логикой на сервере.
Кирилл Новожилов
Автор
Содержание
В этой статье мы рассмотрим:
🎯 Контроллеры в модулях — создание отдельных классов для обработки AJAX
🧩 Контроллеры в компонентах — интеграция AJAX прямо в компоненты
🛡️ Безопасность — фильтры аутентификации, CSRF-защита, валидация
📦 Практические примеры — готовые решения для типовых задач
Что такое контроллер
Контроллер в Bitrix Framework — это PHP-класс, который обрабатывает AJAX-запросы от браузера. Он получает данные, выполняет бизнес-логику и формирует ответ в формате JSON.
Контроллеры состоят из действий (actions), которые представляют собой публичные методы с суффиксом Action. Например, метод saveAction обрабатывает запрос на сохранение данных.
Преимущества контроллеров
- Автоматическая маршрутизация — система сама находит нужный метод по имени действия
- Встроенная безопасность — проверка CSRF-токенов, аутентификации, HTTP-методов
- Внедрение зависимостей — параметры автоматически извлекаются из запроса
- Стандартный формат ответа — JSON с полями
status,data,errors
Контроллеры в модулях
Структура модуля с контроллером
Для работы контроллеров в модуле создайте следующую структуру:
/local/modules/vendor.example/
├── install/
│ ├── index.php # Установщик модуля
│ └── version.php # Версия модуля
├── lib/
│ ├── Controller/
│ │ └── ItemController.php # Контроллер
│ └── Services/
│ └── ItemService.php # Сервис бизнес-логики
├── lang/
│ └── ru/
│ └── install/
│ └── index.php
└── .settings.php # Настройка пространства имен
Скачать полный исходный код всех примеров можно в нашем Telegram канале. Подписывайтесь, чтобы не пропустить новые материалы!
Шаг 1: Настройка .settings.php
Файл .settings.php обязателен для работы контроллеров. Он указывает системе, где искать классы контроллеров:
<?php
// /local/modules/vendor.example/.settings.php
return [
'controllers' => [
'value' => [
'defaultNamespace' => '\\Vendor\\Example\\Controller',
],
'readonly' => true,
],
];
Важные моменты:
defaultNamespaceдолжен соответствовать пространству имен ваших контроллеров- Двойные обратные слеши
\\— экранирование в PHP-строках readonly: trueзапрещает изменение настроек через интерфейс
Шаг 2: Создание контроллера
Создайте файл контроллера /lib/controller/item.php:
<?php
namespace Vendor\Example\Controller;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Engine\ActionFilter;
use Bitrix\Main\Error;
class Item extends Controller
{
/**
* Настройка фильтров для всех действий контроллера
*/
protected function getDefaultPreFilters(): array
{
return [
// Разрешает запросы только авторизованным пользователям
new ActionFilter\Authentication(),
// Принимает только POST-запросы
new ActionFilter\HttpMethod([
ActionFilter\HttpMethod::METHOD_POST,
]),
// Проверяет CSRF-токен
new ActionFilter\Csrf(),
];
}
/**
* Добавление элемента
*/
public function addAction(array $fields): ?array
{
// Валидация
if (empty($fields['NAME'])) {
$this->addError(new Error('Поле NAME обязательно', 'EMPTY_NAME'));
return null;
}
// Бизнес-логика
$item = $this->createItem($fields);
if (!$item) {
$this->addError(new Error('Не удалось создать элемент', 'CREATE_FAILED'));
return null;
}
return $item;
}
/**
* Получение элемента по ID
*/
public function getAction(int $id): ?array
{
$item = $this->findItem($id);
if (!$item) {
$this->addError(new Error('Элемент не найден', 'NOT_FOUND'));
return null;
}
return $item;
}
/**
* Удаление элемента
*/
public function deleteAction(int $id): ?array
{
$result = $this->removeItem($id);
if (!$result) {
$this->addError(new Error('Не удалось удалить элемент', 'DELETE_FAILED'));
return null;
}
return ['deleted' => true, 'id' => $id];
}
private function createItem(array $fields): ?array
{
// Логика создания элемента
return ['ID' => 1, 'NAME' => $fields['NAME']];
}
private function findItem(int $id): ?array
{
// Логика поиска элемента
return ['ID' => $id, 'NAME' => 'Тестовый элемент'];
}
private function removeItem(int $id): bool
{
// Логика удаления
return true;
}
}
Шаг 3: Вызов из JavaScript
Для вызова действий контроллера используйте BX.ajax.runAction:
// Добавление элемента
BX.ajax.runAction('vendor:example.item.add', {
data: {
fields: {
NAME: 'Новый элемент',
DESCRIPTION: 'Описание элемента'
}
}
}).then(function(response) {
// Успешный ответ
console.log('Создан элемент:', response.data);
}).catch(function(response) {
// Ошибка
console.error('Ошибки:', response.errors);
});
// Получение элемента
BX.ajax.runAction('vendor:example.item.get', {
data: { id: 1 }
}).then(function(response) {
console.log('Элемент:', response.data);
});
// Удаление элемента
BX.ajax.runAction('vendor:example.item.delete', {
data: { id: 1 }
}).then(function(response) {
if (response.data.deleted) {
console.log('Элемент удалён');
}
});
Соглашение об именовании
Система связывает JavaScript-вызов и PHP-метод по имени:
| Вызов в JavaScript | PHP-класс | PHP-метод |
|---|---|---|
vendor:example.item.add |
\Vendor\Example\Controller\Item |
addAction() |
vendor:example.item.get |
\Vendor\Example\Controller\Item |
getAction() |
bitrix:disk.folder.get |
\Bitrix\Disk\Controller\Folder |
getAction() |
Формат имени: vendor:module.Controller.action
vendor— первая часть кода модуляmodule— вторая часть кода модуляController— имя класса контроллера (в нижнем регистре)action— имя метода без суффиксаAction
Контроллеры в компонентах
Способ 1: Контроллер в class.php
Для обработки AJAX прямо в компоненте реализуйте интерфейс Controllerable:
<?php
// /local/components/vendor/example/class.php
use Bitrix\Main\Engine\Contract\Controllerable;
use Bitrix\Main\Engine\ActionFilter;
use Bitrix\Main\Error;
use Bitrix\Main\ErrorCollection;
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
class ExampleComponent extends \CBitrixComponent implements Controllerable, \Bitrix\Main\Errorable
{
protected ErrorCollection $errorCollection;
/**
* Настройка действий (можно вернуть пустой массив для отключения всех префильтров)
*/
public function configureActions(): array
{
return [
'save' => [
'prefilters' => [
new ActionFilter\Authentication(),
new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
new ActionFilter\Csrf(),
],
],
'load' => [
'prefilters' => [
new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_GET]),
],
],
];
}
public function onPrepareComponentParams($arParams): array
{
$this->errorCollection = new ErrorCollection();
$arParams['ITEM_ID'] = (int)($arParams['ITEM_ID'] ?? 0);
return $arParams;
}
public function executeComponent(): void
{
// Этот код НЕ выполняется при AJAX-запросах!
$this->includeComponentTemplate();
}
/**
* Действие сохранения
*/
public function saveAction(array $fields): ?array
{
if (empty($fields['NAME'])) {
$this->errorCollection[] = new Error('Укажите название');
return null;
}
// Сохраняем данные
return [
'success' => true,
'message' => 'Данные сохранены',
];
}
/**
* Действие загрузки
*/
public function loadAction(int $id): ?array
{
if ($id <= 0) {
$this->errorCollection[] = new Error('Неверный ID');
return null;
}
return [
'ID' => $id,
'NAME' => 'Элемент #' . $id,
];
}
public function getErrors(): array
{
return $this->errorCollection->toArray();
}
public function getErrorByCode($code): ?\Bitrix\Main\Error
{
return $this->errorCollection->getErrorByCode($code);
}
}
Вызов действий компонента
Используйте BX.ajax.runComponentAction с указанием имени компонента:
// Вызов действия из компонента
BX.ajax.runComponentAction('vendor:example', 'save', {
mode: 'class',
data: {
fields: {
NAME: 'Тест',
VALUE: 123
}
}
}).then(function(response) {
console.log('Сохранено:', response.data);
}).catch(function(response) {
console.error('Ошибка:', response.errors);
});
Передача подписанных параметров
Чтобы использовать в AJAX те же параметры, что при рендере компонента:
В class.php:
class ExampleComponent extends \CBitrixComponent implements Controllerable
{
/**
* Список параметров для подписи
*/
protected function listKeysSignedParameters(): array
{
return [
'ITEM_ID',
'IBLOCK_ID',
'SHOW_DELETED',
];
}
public function updateAction(array $fields): ?array
{
// Получаем подписанные параметры
$params = $this->getUnsignedParameters();
$itemId = $params['ITEM_ID'];
// Обновляем элемент
return ['updated' => true, 'id' => $itemId];
}
}
В шаблоне template.php:
<script>
BX.ready(function() {
var component = new BX.MyComponent({
signedParameters: '<?= $this->getComponent()->getSignedParameters() ?>',
componentName: '<?= $this->getComponent()->getName() ?>'
});
});
</script>
В JavaScript:
BX.ajax.runComponentAction(this.componentName, 'update', {
mode: 'class',
signedParameters: this.signedParameters,
data: {
fields: { NAME: 'Новое название' }
}
});
Способ 2: Контроллер в ajax.php
Альтернативно можно создать отдельный файл ajax.php в корне компонента:
<?php
// /local/components/vendor/example/ajax.php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
class ExampleAjaxController extends \Bitrix\Main\Engine\Controller
{
public function sayHelloAction(string $name = 'Гость'): string
{
return "Привет, {$name}!";
}
public function getDataAction(int $limit = 10): array
{
return [
'items' => [],
'limit' => $limit,
];
}
}
Вызов аналогичен, но без указания mode:
BX.ajax.runComponentAction('vendor:example', 'sayHello', {
data: { name: 'Иван' }
}).then(function(response) {
console.log(response.data); // "Привет, Иван!"
});
Фильтры безопасности
Основные фильтры
| Фильтр | Назначение |
|---|---|
Authentication |
Проверяет авторизацию пользователя |
Csrf |
Проверяет CSRF-токен |
HttpMethod |
Ограничивает HTTP-методы (GET, POST) |
Scope |
Ограничивает контекст (AJAX, REST) |
CloseSession |
Закрывает сессию для производительности |
Настройка фильтров для действий
class Item extends Controller
{
public function configureActions(): array
{
return [
// Публичное действие без авторизации
'list' => [
'prefilters' => [
new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_GET]),
],
],
// Защищённое действие
'delete' => [
'prefilters' => [
new ActionFilter\Authentication(),
new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
new ActionFilter\Csrf(),
],
],
];
}
public function listAction(): array
{
// Доступно всем
return ['items' => []];
}
public function deleteAction(int $id): ?array
{
// Только авторизованным
return ['deleted' => true];
}
}
Использование атрибутов PHP 8
В PHP 8+ можно использовать атрибуты вместо метода configureActions:
use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\HttpMethod;
use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Authentication;
use Bitrix\Main\Engine\ActionFilter\Attribute\Rule\Csrf;
class Item extends Controller
{
#[HttpMethod('POST')]
#[Authentication]
#[Csrf]
public function deleteAction(int $id): ?array
{
return ['deleted' => true];
}
#[HttpMethod('GET')]
public function listAction(): array
{
return ['items' => []];
}
}
Отключение фильтров по умолчанию
По умолчанию все действия защищены фильтрами Authentication, Csrf и HttpMethod. Для изменения переопределите getDefaultPreFilters:
protected function getDefaultPreFilters(): array
{
return [
// Только проверка HTTP-метода, без авторизации
new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
];
}
Формат ответов
Успешный ответ
{
"status": "success",
"data": {
"ID": 1,
"NAME": "Элемент"
},
"errors": []
}
Ответ с ошибкой
{
"status": "error",
"data": null,
"errors": [
{
"message": "Элемент не найден",
"code": "NOT_FOUND"
}
]
}
Обработка ошибок
Добавляйте ошибки через метод addError и возвращайте null:
public function updateAction(int $id, array $fields): ?array
{
if ($id <= 0) {
$this->addError(new Error('Неверный ID', 'INVALID_ID'));
return null;
}
if (empty($fields['NAME'])) {
$this->addError(new Error('Поле NAME обязательно', 'EMPTY_NAME'));
$this->addError(new Error('Заполните обязательные поля', 'VALIDATION_ERROR'));
return null;
}
return ['updated' => true];
}
Внедрение зависимостей
Автоматическое извлечение параметров
Параметры методов автоматически извлекаются из REQUEST:
// Параметры будут получены из $_POST['userId'] или $_GET['userId']
public function getUserAction(int $userId, string $format = 'short'): ?array
{
// $userId — обязательный, $format — необязательный со значением по умолчанию
}
Внедрение объектов
Можно внедрять стандартные объекты:
use Bitrix\Main\Engine\CurrentUser;
use Bitrix\Main\UI\PageNavigation;
public function listAction(CurrentUser $user, PageNavigation $nav): array
{
$userId = $user->getId();
return [
'items' => $this->getItems($nav->getLimit(), $nav->getOffset()),
'userId' => $userId,
];
}
Постраничная навигация
Объект PageNavigation автоматически внедряется в метод контроллера, если передать параметр navigation в JavaScript-вызове:
use Bitrix\Main\UI\PageNavigation;
public function listAction(PageNavigation $pageNavigation): array
{
// Получаем все элементы (в реальном проекте — запрос к БД с LIMIT/OFFSET)
$allItems = $this->getAllItems();
$totalCount = count($allItems);
// Применяем пагинацию
$items = array_slice(
$allItems,
$pageNavigation->getOffset(),
$pageNavigation->getLimit()
);
// Возвращаем данные вместе с информацией о пагинации
return [
'items' => $items,
'totalCount' => $totalCount,
'currentPage' => $pageNavigation->getCurrentPage(),
'pageSize' => $pageNavigation->getLimit(),
'totalPages' => (int)ceil($totalCount / $pageNavigation->getLimit()),
];
}
Вызов с пагинацией:
BX.ajax.runAction('vendor:example.item.list', {
navigation: {
page: 1, // Номер страницы
size: 10 // Элементов на странице
}
}).then(function(response) {
console.log('Элементы:', response.data.items);
console.log('Всего:', response.data.totalCount);
console.log('Страница:', response.data.currentPage, 'из', response.data.totalPages);
});
Важно: Класс Page из Bitrix\Main\Engine\Response\DataType\Page не добавляет totalCount в ответ автоматически. Рекомендуется возвращать обычный массив с явным указанием всех параметров пагинации.
Отдача файлов
Скачивание файла из таблицы b_file
use Bitrix\Main\Engine\Response\BFile;
public function downloadAction(int $fileId): BFile
{
return BFile::createByFileId($fileId);
}
Скачивание сгенерированного файла
use Bitrix\Main\Engine\Response\File;
use Bitrix\Main\Web\MimeType;
public function exportAction(): File
{
$path = $this->generateExport();
return new File(
$path,
'export.xlsx',
MimeType::getByFileExtension('xlsx')
);
}
Показ изображения inline
use Bitrix\Main\Engine\Response\BFile;
public function avatarAction(int $userId): BFile
{
$imageId = $this->getUserAvatar($userId);
return BFile::createByFileId($imageId)->showInline(true);
}
Практический пример: Избранное
Рассмотрим полный пример системы добавления товаров в избранное.
Структура модуля
/local/modules/vendor.favorites/
├── install/
│ ├── index.php
│ └── version.php
├── lib/
│ ├── controller/
│ │ └── favorite.php
│ └── Services/
│ └── FavoriteService.php
└── .settings.php
Файл .settings.php
<?php
return [
'controllers' => [
'value' => [
'defaultNamespace' => '\\Vendor\\Favorites\\Controller',
],
'readonly' => true,
],
];
Сервис FavoriteService.php
<?php
namespace Vendor\Favorites\Services;
use Bitrix\Main\Context;
use Bitrix\Main\Web\Cookie;
use Bitrix\Main\Web\Json;
class FavoriteService
{
private const COOKIE_NAME = 'favorites';
public function isFavorite(int $productId): bool
{
return in_array($productId, $this->getFavorites(), true);
}
public function addToFavorites(int $productId): void
{
$favorites = $this->getFavorites();
$favorites[] = $productId;
$this->setFavorites(array_unique($favorites));
}
public function removeFromFavorites(int $productId): void
{
$favorites = array_filter(
$this->getFavorites(),
fn($id) => $id !== $productId
);
$this->setFavorites($favorites);
}
public function getFavorites(): array
{
$cookie = Context::getCurrent()->getRequest()->getCookie(self::COOKIE_NAME);
if (empty($cookie)) {
return [];
}
try {
$value = Json::decode($cookie);
return is_array($value) ? array_map('intval', $value) : [];
} catch (\Exception $e) {
return [];
}
}
public function getCount(): int
{
return count($this->getFavorites());
}
private function setFavorites(array $favorites): void
{
Context::getCurrent()->getResponse()->addCookie(
new Cookie(
self::COOKIE_NAME,
Json::encode(array_values($favorites)),
time() + 60 * 60 * 24 * 365
)
);
}
}
Контроллер favorite.php
<?php
namespace Vendor\Favorites\Controller;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Engine\ActionFilter;
use Bitrix\Main\Error;
use Vendor\Favorites\Services\FavoriteService;
class Favorite extends Controller
{
protected function getDefaultPreFilters(): array
{
return [
new ActionFilter\HttpMethod([
ActionFilter\HttpMethod::METHOD_POST,
]),
new ActionFilter\Csrf(),
];
}
public function toggleAction(FavoriteService $service, int $productId): ?array
{
if ($productId <= 0) {
$this->addError(new Error('Неверный ID товара', 'INVALID_ID'));
return null;
}
$isFavorite = $service->isFavorite($productId);
if ($isFavorite) {
$service->removeFromFavorites($productId);
} else {
$service->addToFavorites($productId);
}
return [
'productId' => $productId,
'isFavorite' => !$isFavorite,
'count' => $service->getCount(),
];
}
public function listAction(FavoriteService $service): array
{
return [
'favorites' => $service->getFavorites(),
'count' => $service->getCount(),
];
}
public function clearAction(FavoriteService $service): array
{
foreach ($service->getFavorites() as $productId) {
$service->removeFromFavorites($productId);
}
return [
'cleared' => true,
'count' => 0,
];
}
}
JavaScript для работы с избранным
class FavoritesManager {
constructor() {
this.bindEvents();
this.updateCounter();
}
bindEvents() {
document.querySelectorAll('.js-favorite-toggle').forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
this.toggle(button);
});
});
}
toggle(button) {
const productId = button.dataset.productId;
button.classList.add('loading');
BX.ajax.runAction('vendor:favorites.favorite.toggle', {
data: { productId: parseInt(productId, 10) }
}).then(response => {
button.classList.remove('loading');
button.classList.toggle('active', response.data.isFavorite);
this.updateCounterValue(response.data.count);
}).catch(response => {
button.classList.remove('loading');
console.error('Ошибка:', response.errors);
});
}
updateCounter() {
BX.ajax.runAction('vendor:favorites.favorite.list', {
data: {}
}).then(response => {
this.updateCounterValue(response.data.count);
});
}
updateCounterValue(count) {
const counter = document.querySelector('.js-favorites-count');
if (counter) {
counter.textContent = count;
counter.classList.toggle('hidden', count === 0);
}
}
}
BX.ready(() => {
new FavoritesManager();
});
Отладка контроллеров
Включение режима отладки
В .settings.php добавьте:
return [
'exception_handling' => [
'value' => [
'debug' => true,
],
],
];
Логирование ошибок
use Bitrix\Main\Diag\Debug;
public function complexAction(array $data): ?array
{
Debug::writeToFile($data, 'input data', '/local/logs/ajax.log');
try {
$result = $this->processData($data);
Debug::writeToFile($result, 'result', '/local/logs/ajax.log');
return $result;
} catch (\Exception $e) {
Debug::writeToFile($e->getMessage(), 'error', '/local/logs/ajax.log');
$this->addError(new Error($e->getMessage()));
return null;
}
}
Советы и рекомендации
1.Разделяйте логику: Контроллер должен только принимать запрос и возвращать ответ. Бизнес-логику выносите в отдельные сервисы.
2.Валидируйте данные: Всегда проверяйте входящие параметры перед использованием.
3.Используйте фильтры: Не пренебрегайте CSRF-защитой и проверкой авторизации.
4.Типизируйте параметры: Указывайте типы для автоматической валидации (int $id, array $fields).
5.Обрабатывайте ошибки: Возвращайте понятные сообщения об ошибках с уникальными кодами.
6.Кэшируйте результаты: Для тяжёлых операций используйте кэширование.
Заключение
Контроллеры в Битрикс — мощный и удобный инструмент для обработки AJAX-запросов. Они обеспечивают:
- ✅ Чистую архитектуру с разделением ответственности
- ✅ Встроенную безопасность
- ✅ Автоматическую маршрутизацию
- ✅ Гибкую настройку фильтров
- ✅ Стандартный формат ответов
Используйте контроллеры в модулях для общей логики и в компонентах для специфичной функциональности.
Скачать полный исходный код всех примеров можно в нашем Telegram канале. Подписывайтесь, чтобы не пропустить новые материалы!
Теги:
Похожие статьи