19.12.2025 12 мин чтения

AJAX-запросы в Битрикс: контроллеры в модулях и компонентах

AJAX-запросы — неотъемлемая часть современного веб-приложения. Они позволяют обновлять данные на странице без перезагрузки, создавать интерактивные интерфейсы и улучшать пользовательский опыт. В 1С-Битрикс для обработки AJAX-запросов используется механизм контроллеров — удобный и безопасный способ связать JavaScript-код на фронтенде с PHP-логикой на сервере.

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

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

Автор

AJAX-запросы в Битрикс: контроллеры в модулях и компонентах

В этой статье мы рассмотрим:

🎯 Контроллеры в модулях — создание отдельных классов для обработки AJAX

🧩 Контроллеры в компонентах — интеграция AJAX прямо в компоненты

🛡️ Безопасность — фильтры аутентификации, CSRF-защита, валидация

📦 Практические примеры — готовые решения для типовых задач

Что такое контроллер

Контроллер в Bitrix Framework — это PHP-класс, который обрабатывает AJAX-запросы от браузера. Он получает данные, выполняет бизнес-логику и формирует ответ в формате JSON.

Контроллеры состоят из действий (actions), которые представляют собой публичные методы с суффиксом Action. Например, метод saveAction обрабатывает запрос на сохранение данных.

Преимущества контроллеров

  1. Автоматическая маршрутизация — система сама находит нужный метод по имени действия
  2. Встроенная безопасность — проверка CSRF-токенов, аутентификации, HTTP-методов
  3. Внедрение зависимостей — параметры автоматически извлекаются из запроса
  4. Стандартный формат ответа — 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 канале. Подписывайтесь, чтобы не пропустить новые материалы!

Опубликовано 1 час назад

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