Привет! Меня зовут Игорь Росляков, я технический писатель. По приглашению руководителя направления «Маркет и интеграции» Сергея Вострикова готовлю цикл статей на тему ИИ-ассистированной разработки решений для Битрикс24

Статьи из цикла можно использовать как туториалы при создании локальных приложений в свой бизнес-портал. Их можно показать своим разработчикам или использовать самому, если работаете один и хотите кастомизировать работу дополнительными удобными функциями.


Сегодня — 4-я статья из цикла. Рассказываем про работу с проектом BI-коннектора. Если упрощённо, то это инструмент для аналитики не только по показателям портала — сделкам, клиентам и другим — но и по внешним базам данных. С ним получается мост между Битрикс24 и любыми другими источниками данных.

Что было в предыдущих статьях:

  1. Пишем первое приложение с AI-стартером, чтобы видеть прибыли и убытки

  2. Добавляем в бизнес-портал Битрикс24 роботов для автоматизации

  3. Что даёт воспроизводимая среда разработки и как развернуть контейнеры на VPS.

Как и в работе с AI-стартером, работать мы будем с помощью AI-агентов. В этом проекте я использовал Codex.


Что будет в статье:

Что нужно установить перед работой

Репозиторий для старта: github.com/bitrix24/bi-connector. Мы клонируем его на локальную машину и будем работать с ним. 

Итоговый репозиторий, который подключен к порталу:
github.com/igorrosliakov-bitrix24/Bitrix24-BIConnector

Что ещё понадобится:

  • Docker и Docker Compose для контейнеризации.

  • Git для контроля версий и пуша в удалённый репозиторий.

  • make для коротких команд.

  • Публичный HTTPS-туннель через CloudPub. Получить можно через десктопный клиент или инструмент командной строки clo.

Что за BI-connector, зачем он нужен и как работает

В портале Битрикс24 есть раздел BI-конструктор, а в нём есть вкладка — «Рабочее место аналитика»:

Внутри вкладки можно создать таблицу из внешнего подключения к базе данных:

BI-коннектор нужен для создания такого внешнего подключения. 

Получается такая схема:

  • У нас есть база данных, которая может быть на отдельном сервере, в Docker-контейнере, облаке или локально на машине. 

  • BI-коннектор получает параметры подключения к этой базе.

  • И уже через коннектор Битрикс24 начинает видеть таблицы и данные.

Анализ проекта для дальнейшей работы

Я попросил агента тщательно проанализировать проект и составить план работы для подключения к порталу, оптимизации, усиления безопасности и стабильного добавления новых фич. Поэтому сначала агент разобрал репозиторий и составил исходную картину.

Задача агенту:

Проанализируй этот проект подробно: объясни, что это за сервис, для чего он нужен и как он работает в связке с Битрикс24. Сначала дай простое верхнеуровневое объяснение, потом опиши техническую архитектуру: основные файлы, точки входа, ключевые классы и поток запроса. Отдельно укажи, что в проекте уже реализовано, а какие части выглядят как кандидаты на доработку.

Для работы нам понадобится настроить подключение для портала и соединить коннектор с тестовыми базами данных. После этого мы оптимизируем приложение: усилим безопасность, покроем тестами и добавим новую возможность: нормализацию полей, чтобы в портал попадали уже подготовленные данные.

Вот что в итоге Codex рассказал о проекте.

Основные точки входа

Агент нашёл 4 основные точки входа:

  • Application.php отвечает за install-flow и регистрацию коннекторов в Битрикс24.

  • index.php или затем выделенный request-handler отвечает за входящие BI-запросы.

  • BiConnector.php отвечает за доступ к БД и за контракт BI-коннектора.

  • QueryBuilder.php отвечает за построение SQL.

Технически сервис умеет подключаться к БД, отдавать список таблиц, отдавать структуру и отдавать данные.

BI-коннектор — это отдельный серверный сервис, которое поднимается локально или на сервере и принимает запросы от Битрикс24. Портал не встраивает этот код себе внутрь, вместо этого он делает HTTP-запросы в наше приложение: «проверь подключение, покажи список таблиц, покажи структуру таблицы». В ответ на это приложение отвечает в JSON-формате.

Кроме точек входа, технически проект состоит из 3 основных частей-слоёв.

1. Слой установки

Главный серверный orchestration-класс — Application.php. Он принимает install-событие от Битрикс24, сохраняет auth и регистрирует в бизнес-портале два коннектора: MySQL и PostgreSQL.

Так мы регистрируем в Битрикс-портале контракт:

  • какой URL вызывать для проверки

  • какой URL вызывать для списка таблиц

  • какой URL вызывать для схемы

  • какой URL вызывать для данных

2. Слой HTTP-обработки

Когда Битрикс24 вызывает наш сервис, попадает в public/index.php. После оптимизации в сценарий обработки добавился ещё один скрипт — ConnectorRequestHandler.php.

Этот слой делает несколько вещей:

  • Читает query-параметры вроде action и connection_type.

  • Парсит JSON body.

  • Валидирует входные данные.

  • Создаёт нужный коннектор.

  • Вызывает нужное действие.

3. Слой работы с БД

С базой работает BiConnector.php. Он отвечает за создание подключения, проверку БД, получение списка и структуры таблиц и остальную работу.

Отдельный SQL-builder находится в QueryBuilder.php. Он собирает SQL-запрос, применяет select, filter и limit и возвращает данные в формате, который ожидает портал.

Задача 1: установка в портал

Сначала проект нужно подключить к порталу.

Задача агенту:

Помоги подключить этот проект к Битрикс24 как локальное приложение. Сначала определи, чего не хватает для установки и внешней доступности сервиса, потом распиши пошагово, что нужно настроить в проекте, в окружении и в самом портале. Объясняй в двух уровнях: сначала просто для новичка, потом технически с точными файлами, переменными окружения, URL и командами.

Первая проблема простая: Битрикс24 не может установить приложение из localhost напрямую. Если проект просто запущен локально на машине, портал не увидит его, потому что находится снаружи, а localhost виден только на компьютере. Поэтому сначала нужно решить вопрос с доступностью.

Короткий план на этот этап:

  1. Сам проект должен запускаться локально.

  2. У проекта должен появиться внешний HTTPS-адрес.

  3. Проект должен знать свой внешний адрес.

  4. Битрикс24 должен получить install URL и handler URL этого проекта. Без этого портал не сможет ни установить приложение, ни ходить в него за BI-запросами.

Поднимаем проект локально

Сначала проект должен начать работать просто как локальный HTTP-сервис. Для этого нужны Docker, файл .env, docker-compose и рабочий app-контейнер.

То есть первый минимум: клонировать репозиторий, создать .env и поднять контейнер приложения.

Добавить tunnel / внешний доступ

Битрикс24 ожидает, что endpoint любого приложения, в том числе локального, доступен как внешний HTTPS URL. Поэтому нужен либо деплой на сервер, либо локальный деплой через туннель. Мы выбрали вариант туннеля через CloudPub.

Для использования CloudPub нужно зарегистрироваться в системе и получить токен на главной странице регистрации. После этого токен можно отдать агенту, чтобы он добавил его в .env-файл. Так мы получим внешний HTTPS-адрес для регистрации в портале Битрикс24. 

Сам проект тоже должен понимать, какой у него публичный домен и какие URL он должен зарегистрировать в портале. Например, когда приложение устанавливается, оно регистрирует такие endpoint:

  • /?connection_type=mysql&action=check

  • /?connection_type=mysql&action=table_list

  • /?connection_type=postgresql&action=data
    и так далее

Но эти URL должны строиться не от localhost, а от реального внешнего адреса. То есть проекту нужен был параметр вида: APP_DOMAIN=https://something.cloudpub.ru.

Подключить приложение к порталу

Когда агент проведёт все приготовления, приложение нужно установить на свой портал по инструкции из 1-й статьи в разделе «Регистрируем приложение на портале Битрикс24»

Обратите внимание, что на последнем экране для сегодняшнего проекта нужно поставить галочку «Использует только API» и не забывать выставить права biconnector:

Что поменял и добавил агент

В .env.example расширился пример окружения, чтобы проект мог работать не только как абстрактный backend, но и как локально публикуемое приложение. Главные добавления были такие:

  • CLOUDPUB_TOKEN, нужен для самого туннеля.

  • CLOUDPUB_IMAGE, нужен для выбора Docker image CloudPub.

  • BITRIX24_PHP_SDK_APPLICATION_SCOPE=biconnector фиксирует, что приложение работает именно как BI connector app.

В docker-compose.yml добавили отдельный сервис cloudpub. Смысл этого сервиса:

  • Получает TOKEN.

  • Знает, что HTTP нужно прокидывать в app:80.

  • Работает в одном docker network с приложением.

  • Поднимает внешний HTTPS URL для локального app-контейнера.

В Makefile агент добавил команды, чтобы весь сценарий можно было запускать dev-командами, а не руками. Ключевые команды:

  • dev-init

  • start-cloudpub

  • logs-cloudpub

Появился скрипт автоматизации scripts/dev-init.sh. Скрипт делает примерно следующее:

  • Запускает app.

  • Запускает cloudpub.

  • Читает из логов CloudPub реальный внешний URL.

  • Пишет этот URL в APP_DOMAIN.

  • Перезапускает приложение, чтобы оно уже использовало правильный домен.

Почему это важно: если APP_DOMAIN не совпадает с реальным публичным URL-туннелем, то Bitrix24 потом будет вызывать не те адреса, и коннектор не зарегистрируется правильно.

Application.php. Основная логика регистрации коннекторов уже была, но на этапе подключения к порталу нужно убедиться, что она строит URL из APP_DOMAIN.

Проверяем, что приложение установилось

Для проверки работоспособности коннектора он должен быть связан с базами данных. Можно попросить агента создать новые тестовые, или привязать уже существующие.

Если у вас уже есть готовая база, то в проекте обычно ничего менять не нужно, если:

  • база MySQL/PostgreSQL;

  • коннектор может до неё достучаться по сети;

  • у вас есть доступы.

Менять может понадобиться только окружение:

  • .env, если вы хотите сохранить какие-то свои dev-параметры;

  • docker-compose.yml, если захотите поднять свою БД рядом в контейнере.

В разделе BI-конструктор сначала нужно зайти в «Рабочее место аналитика» и начать создавать новое подключение:

На следующем экране нужно выбрать «Создать подключение»:

После этого выбираем тип источника данных и заполняем все значения:

Когда параметры заполнены, подключение нужно проверить. Если коннектор установился корректно, и база доступна, всё пройдёт успешно.

После создания должна быть возможность выбрать конкретную таблицу из базы данных и просмотреть её:

Второй вариант проверки — через curl-команду такого вида:

curl -X POST 'https://YOUR-CLOUDPUB-URL/?connection_type=postgresql&action=check' \
  -H 'Content-Type: application/json' \
  -d '{
    "connection": {
      "host": "YOUR_DB_HOST",
      "port": "5432",
      "database": "YOUR_DB_NAME",
      "username": "YOUR_DB_USER",
      "password": "YOUR_DB_PASSWORD"
    }
  }'

Если всё работает, в ответ придёт сообщение такого вида:

{
  "status": "OK",
  "message": "Connection successful"
}

Можно запросить список таблиц:

curl -X POST 'https://YOUR-CLOUDPUB-URL/?connection_type=postgresql&action=table_list' \
  -H 'Content-Type: application/json' \
  -d '{
    "connection": {
      "host": "YOUR_DB_HOST",
      "port": "5432",
      "database": "YOUR_DB_NAME",
      "username": "YOUR_DB_USER",
      "password": "YOUR_DB_PASSWORD"
    }
  }'

Или описание конкретной таблицы:

curl -X POST 'https://YOUR-CLOUDPUB-URL/?connection_type=postgresql&action=table_description' \
  -H 'Content-Type: application/json' \
  -d '{
    "table": "YOUR_TABLE",
    "connection": {
      "host": "YOUR_DB_HOST",
      "port": "5432",
      "database": "YOUR_DB_NAME",
      "username": "YOUR_DB_USER",
      "password": "YOUR_DB_PASSWORD"
    }
  }'

Задача 2: оптимизация безопасности и покрытие тестами

После подключения к порталу и проверки основных функций я попросил агента улучшить надёжность проекта и покрытие тестами. В проекте уже были тесты, поэтому агент просто усилил то, что было.

Задача агенту:

Проанализируй проект с точки зрения безопасности и качества кода, а затем предложи и внеси улучшения. Сначала улучши обработку входных данных, логирование и другие уязвимые места, потом покрой критические сценарии тестами и настрой удобный запуск покрытия. В конце покажи, какие тесты появились, что именно они проверяют и как запускать тесты, линтеры и coverage.

Когда проект работает как мост между бизнес-порталом Битрикс24 и внешней базой, у него есть три главных риска:

  • сломать обработку запросов при доработках;

  • случайно пропустить опасные или кривые входные данные;

  • создать утечку чувствительных данных в логах.

Агент сделал 2 вещи.

Усилил безопасность поведения. Проект стал аккуратнее принимать запросы, валидировать их и логировать. Например:

  • Если приходит битый JSON, приложение теперь не падает «как получится», а возвращает понятный 400 Bad Request.

  • Если нет обязательных параметров подключения, оно сразу говорит, что именно не хватает.

  • Если в логах есть password, token, secret, authorization, эти значения теперь маскируются и не попадают в лог в открытом виде.

  • Для SQL-части мы усилили безопасность одного места, где имя таблицы в MySQL-описании стало проходить через корректное quoting.

Улучшил тестовое покрытие. Перед тем как модернизировать проект дальше, мы должны были зафиксировать его поведение тестами, чтобы новые изменения не ломали старое.

Мы покрыли тестами не только «счастливый путь», но и проблемные сценарии:

  • Неправильный JSON.

  • Отсутствие connection.

  • Неверный connection_type.

  • Неизвестный action.

  • Нормализацию лимита.

  • Преобразование имён полей.

  • Сборку SQL-запросов.

  • Безопасную обработку логов.

Запуск покрытия стал удобным. То есть теперь проект можно не просто тестировать, а ещё и посмотреть, какие части кода реально покрыты тестами, а какие пока нет. 

Если упрощённо, результат работы с безопасностью был такой:

  • Проект стал меньше доверять входным данным.

  • Проект перестал светить секреты в логах.

  • Проект стало безопаснее рефакторить.

  • У нас появился понятный способ показать, какие части системы уже проверяются автоматически.

Что поменял и добавил агент

Выделили обработку входящих запросов в отдельный класс.
Файл: src/ConnectorRequestHandler.php.

Раньше входная логика была менее изолированной. Теперь появился отдельный request-handler, который: 

  • Разбирает action и connection_type.

  • Вытаскивает input из form-data и JSON.

  • Валидирует обязательные параметры.

  • Переводит ошибки в контролируемые HTTP-ответы 400 или 500.

  • Централизует dispatch на check, table_list, table_description, data.

Сделали entrypoint чище.
Файл: public/index.php.

Теперь index.php не держит в себе толстую бизнес-логику, а передаёт запрос в ConnectorRequestHandler. Это архитектурно чище и проще для тестирования.

Усилили безопасность логирования.
Файл: src/Application.php.

В логгер добавлен processor, который прогоняет context и extra через санитайзер:

$logger->pushProcessor(
    static function (LogRecord $record): LogRecord {
        return $record->with(
            context: self::sanitizeLogData($record->context),
            extra: self::sanitizeLogData($record->extra)
        );
    }
);

И сам санитайзер скрывает чувствительные ключи:

$sensitiveFragments = ['password', 'token', 'secret', 'authorization'];

if (in_array($normalizedKey, ['auth'], true)) {
    return true;
}

За счёт этого в логах не торчат пароли БД, токены, client secret, authorization payload.

Добавилось безопаснее логирование внешних ответов. В  src/Application.php появился метод formatResponseForLog(), который пытается безопасно распарсить JSON-ответ и замаскировать чувствительные поля, прежде чем они попадут в лог.

Поджали SQL-часть для MySQL schema inspection в файле src/BiConnector.php

Подготовили отдельную инфраструктуру для coverage.
Файл: Dockerfile.test. 

В тестовый контейнер добавлен Xdebug в режиме coverage:

&& pecl install xdebug \
&& docker-php-ext-enable xdebug \
&& echo "xdebug.mode=coverage" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \

Добавили удобную make-команду для покрытия. Файл: Makefile.

Выполняем команду:

make test-coverage

И HTML-отчёт лежит в директории coverage/.

В конце агент добавил и расширил unit-тесты.

Задача 3: модернизируем проект: добавляем автомаппинг

Последний шаг работы на сегодня: новая фича. 

В базе данных может быть колонка такого вида: Created By

Но Битрикс24 ожидает другое: CREATED_BY

Поэтому для гладкой надёжной работы в приложении коннектора не хватает слоя-переводчика. Нужен двусторонний маппинг такого вида:

  • реальное имя в БД: Created By

  • имя для Битрикс24: CREATED_BY

Задача агенту:

Добавь в проект фичу автомаппинга полей между внешней базой данных и форматом Битрикс24. Если в БД поле называется, например, "Created By" или "City / Region", коннектор должен отдавать его в Битрикс24 как "CREATED_BY" и "CITY_REGION", а при входящих запросах от Битрикс24 уметь переводить эти имена обратно в реальные имена колонок для SQL-запроса. Продумай архитектуру и добавь тесты.

Для этого понадобится слой логики, который работает так:

  • получает реальную структуру,

  • строит безопасные Bitrix-коды,

  • запоминает соответствие,

  • при запросе от Б24 подменяет CREATED_BY обратно на Created By,

  • выполняет запрос к реальной БД,

  • и затем снова отдаёт ответ в Bitrix-формате.

Визуально работать это должно так. В БД есть колонка Created By, но наружу в Битрикс24 мы отдаём:

code = CREATED_BY

name = CREATED_BY

У у себя внутри сохраняем map:

CREATED_BY -> Created By

После этого портал присылает select: ["CREATED_BY"]. Мы перед SQL-запросом меняем это на реальное поле Created By, выполняем запрос, а в ответе снова отдаём данные под кодом CREATED_BY.

Без этого могут быть проблемы с пробелами в названиях полей, смешанным регистром, дефисами, спецсимволами. То есть эта фича делает коннектор сильно более совместимым с реальными внешними БД, а не только с аккуратными таблицами, где всё уже называется как CREATED_BY.

Результат работы можно проверить не только на портале. Один из вариантов — через плагины SQL Tools в VS Code.

Здесь мы делаем два разных запроса в уже готовом приложении — в расширении и в терминале, чтобы посмотреть, как выглядят данные в БД в реальности и как их обрабатывает коннектор для передачи в портал Битрикс24.

Нюанс в том, что надо сразу продумать коллизии. Например:

  • Created By

  • Created-By

  • Created_By

Все эти названия могут превратиться в один и тот же код CREATED_BY. Значит, нужен либо детерминированный способ решить конфликт, либо fallback вроде CREATED_BY__2, либо хранение уникального внутреннего идентификатора.

Что поменял и добавил агент

Шаг 1. Codex добавил отдельный слой нормализации имён полей в FieldNameMapper.php. Он преобразует, например, Created By в CREATED_BY, City / Region в CITY_REGION. При коллизиях создаются уникальные коды вроде CREATED_BY_2.

Нормализация имён полей в FieldNameMapper
public function buildMappings(array $fieldDefinitions): array
{
    $mappings = [];
    $usedCodes = [];

    foreach ($fieldDefinitions as $fieldDefinition) {
        // Берём реальное имя колонки из БД, например "Created By"
        // и превращаем его в нормализованный код, например "CREATED_BY"
        $baseCode = $this->normalizeFieldCode($fieldDefinition['originalName']);

        // Если такой код уже занят, делаем его уникальным:
        // CREATED_BY -> CREATED_BY_2 -> CREATED_BY_3 ...
        $code = $this->makeUniqueCode($baseCode, $usedCodes);
        $usedCodes[] = $code;

        // Сохраняем полное соответствие:
        // что увидит Битрикс24, и как это поле называется в реальной БД
        $mappings[$code] = [
            'code' => $code,
            'name' => $code,
            'originalName' => $fieldDefinition['originalName'],
            'type' => $fieldDefinition['type'],
        ];
    }

    return $mappings;p

Этот код:

  • получает список реальных полей таблицы;

  • для каждого поля строит “bitrix-safe” код;

  • сохраняет связь нормализованное имя -> реальное имя.

Как выглядит механизм нормализации normalizeFieldCode
public function normalizeFieldCode(string $fieldName): string
{
    // Убираем лишние пробелы по краям
    $normalizedFieldName = trim($fieldName);

    // Если имя было не в ASCII, пробуем транслитерировать.
    // Это нужно, чтобы символы стали предсказуемыми для кода.
    $normalizedFieldName = $this->transliterateToAscii($normalizedFieldName);

    // Переводим всё в верхний регистр:
    // "Created By" -> "CREATED BY"
    $normalizedFieldName = strtoupper($normalizedFieldName);

    // Всё, что не A-Z и не цифры, заменяем на "_":
    // "CITY / REGION" -> "CITY_REGION"
    $normalizedFieldName = (string)preg_replace('/[^A-Z0-9]+/', '_', $normalizedFieldName);

    // Убираем "_" по краям
    $normalizedFieldName = trim($normalizedFieldName, '_');

    // Если получилось несколько "_" подряд, схлопываем их в один
    $normalizedFieldName = (string)preg_replace('/_+/', '_', $normalizedFieldName);

    // Если после очистки имя оказалось пустым, даём безопасное имя по умолчанию
    if ($normalizedFieldName === '') {
        $normalizedFieldName = 'FIELD';
    }

    // Если имя начинается с цифры, Bitrix-стиль делаем безопасным через префикс
    // "2024 TOTAL" -> "FIELD_2024_TOTAL"
    if (preg_match('/^[0-9]/', $normalizedFieldName) === 1) {
        $normalizedFieldName = 'FIELD_' . $normalizedFieldName;
    }

    return $normalizedFieldName;
}
Как выглядит защита от коллизий makeUniqueCode
private function makeUniqueCode(string $baseCode, array $usedCodes): string
{
    // Если такого кода ещё не было, используем как есть
    if (!in_array($baseCode, $usedCodes, true)) {
        return $baseCode;
    }

    $suffix = 2;

    // Иначе добавляем 2, 3, 4...
    do {
        $candidate = $baseCode . '' . $suffix;
        $suffix++;
    } while (in_array($candidate, $usedCodes, true));

    return $candidate;
}

Шаг 2. Этот маппинг подключили в BiConnector.php: table_description теперь отдаёт Bitrix-безопасные имена, а getData() сначала получает mapping, потом передаёт его в query layer.

Как выглядит чтение реальных полей из базы
private function fetchTableFields(Connection $connection, string $tableName): array
{
    if ($this->connectionType === 'mysql') {
        // Для MySQL читаем схему через DESCRIBE
        $sql = 'DESCRIBE ' . $connection->quoteIdentifier($tableName);
    } else {
        // Для PostgreSQL читаем схему через information_schema.columns
        $sql = "SELECT column_name, data_type, is_nullable
               FROM information_schema.columns
               WHERE table_name = :table_name
               AND table_schema = 'public'";
    }

    $stmt = $connection->prepare($sql);

    if ($this->connectionType === 'postgresql') {
        $stmt->bindValue('table_name', $tableName);
    }

    $result = $stmt->executeQuery();
    $fieldDefinitions = [];

    while ($row = $result->fetchAssociative()) {
        if ($this->connectionType === 'mysql') {
            // Берём реальное имя поля из MySQL
            $fieldDefinitions[] = [
                'originalName' => (string)$row['Field'],
                'type' => $this->mapMySQLTypeToBitrix($row['Type'])
            ];
        } else {
            // Берём реальное имя поля из PostgreSQL
            $fieldDefinitions[] = [
                'originalName' => (string)$row['column_name'],
                'type' => $this->mapPostgreSQLTypeToBitrix($row['data_type'])
            ];
        }
    }

    // Вот здесь и происходит автомаппинг:
    // реальные имена колонок превращаются в Bitrix-safe поля
    $fields = array_values($this->fieldNameMapper->buildMappings($fieldDefinitions));

    return $fields;
}

В этом коде:

  • БД отдаёт, например, Created By;

  • FieldNameMapper превращает это в CREATED_BY;

  • дальше коннектор уже работает с этой нормализованной схемой.

Как выглядит tableDescription
public function tableDescription(string $tableName): Response
{
    if (empty($tableName)) {
        return new Response(
            json_encode(['error' => 'Table name is required']) ?: '{"error":"Table name is required"}',
            400,
            ['Content-Type' => 'application/json']
        );
    }

    try {
        // Получаем уже нормализованную структуру полей
        $fields = $this->getTableDescriptionFields($tableName);

        // И отдаём её Битрикс24
        return new Response(
            json_encode($fields) ?: '[]',
            200,
            ['Content-Type' => 'application/json']
        );
    } catch (\Throwable $e) {
        return new Response(
            json_encode(['error' => $e->getMessage()]) ?: '{"error":"Unknown error"}',
            500,
            ['Content-Type' => 'application/json']
        );
    }
}
Как подготавливается именно mapping-словарь getFieldMappings для getData():
private function getFieldMappings(string $tableName): array
{
    $connection = $this->getConnection();

    // Сначала читаем поля таблицы и нормализуем их
    $fields = $this->fetchTableFields($connection, $tableName);

    $fieldMappings = [];

    foreach ($fields as $field) {
        // Строим словарь вида:
        // "CREATED_BY" => [ ... originalName => "Created By" ... ]
        $fieldMappings[$field['code']] = $field;
    }

    return $fieldMappings;
}
Где этот mapping идёт в query layer:
public function getData(string $tableName, array $select, array $filter, int $limit): Response
{
    if (empty($tableName)) {
        return new Response(
            json_encode(['error' => 'Table name is required']) ?: '{"error":"Table name is required"}',
            400,
            ['Content-Type' => 'application/json']
        );
    }

    try {
        $connection = $this->getConnection();
        $queryBuilder = new QueryBuilder($connection, $this->logger);

        // Получаем mapping между Bitrix-полями и реальными колонками БД
        $fieldMappings = $this->getFieldMappings($tableName);

        // Передаём этот mapping в слой построения SQL
        $data = $queryBuilder->buildAndExecuteQuery(
            $tableName,
            $select,
            $filter,
            $limit,
            $fieldMappings
        );

        return new Response(
            json_encode($data) ?: '[]',
            200,
            ['Content-Type' => 'application/json']
        );
    } catch (\Throwable $e) {
        return new Response(
            json_encode(['error' => $e->getMessage()]) ?: '{"error":"Unknown error"}',
            500,
            ['Content-Type' => 'application/json']
        );
    }
}

Шаг 3. QueryBuilder.php обновили, чтобы запросы от Битрикс24 работали по нормализованным полям, а внутри SQL использовались реальные имена колонок. То есть select: ["CREATED_BY"] идёт в БД как Created By, а в ответе снова возвращается CREATED_BY.

Где происходит обратный перевод в реальные SQL-имена: QueryBuilder
public function buildAndExecuteQuery(
    string $tableName,
    array $select,
    array $filter,
    int $limit,
    array $fieldMappings = []
): array {
    $queryBuilder = $this->connection->createQueryBuilder();

    if (empty($select)) {
        // Если Bitrix не указал select, берём все доступные поля
        $selectExpressions = $this->buildSelectExpressions([], $fieldMappings);

        if ($selectExpressions === ['*']) {
            $queryBuilder->select('*');
        } else {
            $queryBuilder->select(...$selectExpressions);
        }
    } else {
        // Если select есть, строим выражения с учётом mapping
        $queryBuilder->select(...$this->buildSelectExpressions($select, $fieldMappings));
    }

    $queryBuilder->from($this->quoteIdentifier($tableName));

    // Здесь фильтры тоже идут через mapping
    $this->applyFilters($queryBuilder, $filter, $fieldMappings);

    if ($limit > 0) {
        $queryBuilder->setMaxResults($limit);
    }

    $result = $queryBuilder->executeQuery();
    $rows = $result->fetchAllAssociative();

    // На выходе приводим результат к формату Битрикс24
    return $this->formatDataForBitrix($rows, $select);
}
Как CREATED_BY превращается в Created By.
private function resolveFieldName(string $field, array $fieldMappings): string
{
    // Если mapping не передали, работаем с именем как есть
    if (empty($fieldMappings)) {
        return $field;
    }

    // Если Bitrix запросил нормализованное имя,
    // возвращаем настоящее имя колонки в БД
    if (isset($fieldMappings[$field])) {
        return $fieldMappings[$field]['originalName'];
    }

    // Если вдруг уже пришло реальное имя колонки, тоже позволяем его использовать
    foreach ($fieldMappings as $fieldMapping) {
        if ($fieldMapping['originalName'] === $field) {
            return $fieldMapping['originalName'];
        }
    }

    // Если поле вообще неизвестно, это ошибка
    throw new \InvalidArgumentException('Unknown field requested: ' . $field);
}
Как строится SELECT с alias
private function buildSelectExpressions(array $select, array $fieldMappings): array
{
    if (empty($select) && empty($fieldMappings)) {
        return ['*'];
    }

    // Если select пустой, но mapping есть, берём все нормализованные поля
    $requestedFields = !empty($select) ? $select : array_keys($fieldMappings);
    $selectExpressions = [];

    foreach ($requestedFields as $requestedField) {
        // Превращаем Bitrix-имя в реальное имя колонки
        $realField = $this->resolveFieldName($requestedField, $fieldMappings);

        // Строим SQL по реальному имени
        $expression = $this->quoteIdentifier($realField);

        // Но если имя для Bitrix отличается от реального,
        // добавляем alias, чтобы в ответе колонка снова называлась CREATED_BY
        if ($requestedField !== $realField) {
            $expression .= ' AS ' . $this->quoteIdentifier($requestedField);
        }

        $selectExpressions[] = $expression;
    }

    return $selectExpressions;
}
Фильтры

Автомаппинг работает не только для SELECT, но и для:

  • filter

  • LIKE

  • IN

  • BETWEEN

  • обычного =

private function applyFieldFilter(
    DBALQueryBuilder $queryBuilder,
    string $field,
    mixed $condition,
    array $fieldMappings = []
): void {
    // И здесь тоже сначала переводим Bitrix-имя в реальную колонку
    $realField = $this->resolveFieldName($field, $fieldMappings);
    $quotedField = $this->quoteIdentifier($realField);

    // Имя SQL-параметра строим отдельно и безопасно
    $paramName = $this->buildParameterName($field);

    if (is_array($condition)) {
        if (isset($condition['operator'])) {
            $operator = strtoupper($condition['operator']);
            $value = $condition['value'] ?? null;

            switch ($operator) {
                case 'LIKE':
                    // Пример:
                    // CITY_REGION -> "City / Region"
                    // SQL будет фильтровать по реальной колонке
                    $queryBuilder->andWhere($quotedField . ' LIKE :' . $paramName);
                    $queryBuilder->setParameter($paramName, '%' . $value . '%');
                    break;

                case 'BETWEEN':
                    if (isset($condition['from']) && isset($condition['to'])) {
                        $queryBuilder->andWhere(
                            $quotedField . ' BETWEEN :' . $paramName . '_from AND :' . $paramName . '_to'
                        );
                        $queryBuilder->setParameter($paramName . '_from', $condition['from']);
                        $queryBuilder->setParameter($paramName . '_to', $condition['to']);
                    }
                    break;
            }
        } else {
            $queryBuilder->andWhere($quotedField . ' IN (:' . $paramName . ')');
            $queryBuilder->setParameter($paramName, $condition, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
        }
    } else {
        // Обычное равенство:
        // CREATED_BY = 'Alice'
        // превратится в SQL по колонке "Created By"
        $queryBuilder->andWhere($quotedField . ' = :' . $paramName);
        $queryBuilder->setParameter($paramName, $condition);
    }
}

Дополнительно агент создал demo-таблицу friendly_columns с колонками с реальными названиями: Created By, Office Name, City / Region, Opened At.

Всё это было покрыто тестами в FieldNameMapperTest.php и QueryBuilderTest.php.

Результат

Агент проанализировал существующий проект и успешно модернизировал его трижды: 

  1. Для подключения к порталу по HTTPS-туннелю.

  2. Для усиления безопасности и тестового покрытия.

  3. Для внесения оптимизации автомаппинга полей.

В отличие от предыдущих опытов, на этот раз ИИ нигде не зависал надолго и сделал всю работу достаточно быстро. Самой сложной частью было сформулировать задачу и план работы.

Что будем делать дальше

Мы уже добавляли UI-приложения, автоматизировали сделки с роботами, деплоили проекты на сервер и оптимизировали существующие сервисы. В следующий раз попробуем поработать с чат-ботами на бэкенде. 

Если вам интересна какая-то тема про кастомизацию портала — напишите в комментариях, а мы постараемся раскрыть её в следующих статьях.

Содержание цикла статей про создание приложений с AI-агентами

  1. Пишем первое приложение с AI-стартером, чтобы видеть прибыли и убытки

  2. Добавляем в бизнес-портал Битрикс24 роботов для автоматизации

  3. Что даёт воспроизводимая среда разработки и как развернуть контейнеры на VPS.

  4. Анализ и модернизация коннектора баз данных с помощью AI-агентов

Комментарии (0)