Франкеншнейне — Laravel с админкой Битрикс, ватефа? шнейна?
Франкеншнейне — Laravel с админкой Битрикс, ватефа? шнейна?

Делюсь опытом разработки проекта, где потребовалось совместить Laravel и «Битрикс Управление Сайтом» в одной системе: подробно описал путь от настройки окружения и выбора инструментов до внедрения CI/CD и решения возникавших проблем. Если вы стоите перед выбором: быть или не быть, то сперва прочитайте эту статью.

Оглавление

Техническое задание
Инфраструктура
Документация
Битрикс как сервис
Классы сущностей для инфоблоков
JWT-авторизация
Организация маршрутов API: принципы и структура
Контроллеры: структура и принципы организации
Сервисы
Репозитории
Выводы и предостережения

Техническое задание

Техническое задание жёстко требовало использования CMS «Битрикс Управление Сайтом» для административной части. Однако, оценив сложность предстоящего REST API и имеющийся опыт команды в Laravel, я выбрал гибридную архитектуру: подход позволил соблюсти формальные требования ТЗ, но вести разработку API в предсказуемой и знакомой экосистеме.

Мой первый опыт работы с Битрикс сводился к классической разработке — внедрению верстки в готовые компоненты. Самым сложным испытанием тогда стала кастомизация процесса оформления заказа. Система казалась невероятно громоздкой. Сейчас, конечно, меня ей не напугать, потому что знаю, что всё можно переопределить или написать собственную реализацию. Но лёгкая тревога осталась: а справлюсь ли я?

 Небольшая тревожность в начале проекта: а справлюсь ли я с Битрикс?
Небольшая тревожность в начале проекта: а справлюсь ли я с Битрикс?

Изучая опыт рынка, я встречал кейсы успешной интеграции Laravel и Битрикс в крупных студиях. Этот подход позволял оставить мощную админку Битрикс для клиента, но вести разработку по современным стандартам. Учитывая требования ТЗ и положительный опыт коллег, такое решение казалось оптимальным компромиссом.

Инфраструктура

Перед деплоем на тестовый сервер, я локально подготовил окружение и определил список сервисов, которые потребуются для работы проекта. Я не нашёл официальной информации от разработчиков Битрикс о рекомендуемом образе docker: встретил парочку репозиториев, но они показались нестабильными. Поэтому я решил собрать свой контейнер на базе php-8.4, написав инструкцию по созданию в Dockerfile.

Во время установки Bitrix выполняется проверка установленных расширений PHP, необходимых для корректной работы модулей Битрикс. Используя установщик Битрикс и немного Google, нашёл всё необходимое для добавления в свой PHP-образ.

php/Dockerfile

FROM php:8.4-fpm

RUN apt-get update && apt-get install -y \
    libfreetype6-dev \
    libjpeg62-turbo-dev \
    libpng-dev \
    libzip-dev \
    libonig-dev \
    libxml2-dev \
    curl \
    git

RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) gd \
    && docker-php-ext-install pdo_mysql \
    mysqli \
    mbstring \
    xml \
    zip

# Install Composer globally
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Add Composer to the PATH
ENV PATH="$PATH:/usr/local/bin"

WORKDIR /app/laravel

EXPOSE 9000
CMD ["php-fpm"]

Для базы данных я использовал образ MySQL из официального репозитория. Образ поставляется со всем необходимым для работы. Достаточно указать данные пользователя, имя базы данных и root-пароль. После скачивания система автоматически создаст базу данных и пользователя. Хост подключения будет доступен внутри сети по имени сервиса, а чтобы подключиться снаружи, нужно пробросить порт.

Уточнил версию Node у фронтенд-разработчика, чтобы избежать возможного конфликта, и загрузил из официального репозитория образ. Настроил автоматический npm install при создании контейнера.

Сделал маршрутизацию в nginx, настроив доступ к сервисам на одном домене.

схема nginx
схема nginx

Конфигурация NGINX

По умолчанию Laravel использует /public для точки входа, поэтому указал root /app/laravel/public. Там же разместил ядро Битрикс и upload для доступа к загружаемым файлам. Маршрут с /api обрабатывал Laravel, поэтому его тоже отдаём php-сервису. Все остальные маршруты обслуживает сервер Node с Next.js-фронтендом.

server {
    listen 80;
    root /app/laravel/public;
    index index.php index.html;
    client_max_body_size 25m;
    
    location /upload/ {
        try_files $uri $uri/;
    }

    # Обработка /bitrix/
    location /bitrix/ {
        try_files $uri $uri/ @bitrix;
    }

    # Обработка /api/
    location /api/ {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    # Проксирование на фронтенд
    location / {
        proxy_pass http://frontend:3000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location @bitrix {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/bitrix/urlrewrite.php;
        fastcgi_pass backend:9000;
        fastcgi_index index.php;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
    }

    # Обработка PHP-запросов
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass backend:9000;
        fastcgi_index index.php;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_buffers 16 16k;
        fastcgi_buffer_size 32k;
    }
}

Деплой и CI/CD

Когда появился каркас проекта, руководство запросило желаемые характеристики тестового сервера. На старте высоких нагрузок не предвиделось, поэтому взял скромный VPS: 20 ГБ SSD, 1 ГБ RAM.

Для настройки CI/CD мне рекомендовали использовать Drone.io, так как у команды уже был опыт работы с этим self-hosted решением. Drone.io поставляется как docker-образ и имеет множество плагинов, облегчающих интеграцию с другими системами. У сервиса есть удобная панель управления (Dashboard). Интерфейс отображает репозитории, за которыми следит Drone, логи и статусы обновлений. Секреты тоже настраиваются через веб-интерфейс.

Скриншот интерфейса drone.io
Скриншот интерфейса drone.io

После настройки панели управления нужно настроить runner. Runner — это приложение, которое будет подключаться к серверу и выполнять инструкции. Существует несколько способов подключения, я выбрал доступ по SSH. Есть официальный образ, но мы использовали сторонний:

docker pull appleboy/drone-ssh

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

Уведомление в телеграм
Уведомление в телеграм

В каждый репозиторий добавил файл конфигурации drone. В файле описан pipeline, который срабатывает каждый раз, когда была обновлена main-ветка. Drone подключается к серверу, выполняет обновление локального репозитория, перезапускает контейнер и отправляет уведомление в Telegram. В рамках пайплайна доступны переменные окружения, содержащие информацию о коммите, авторе и SHA-идентификаторе. По идентификатору можно собрать ссылку на GitHub.

.drone.yml

kind: pipeline
type: docker
name: default

steps:
- name: backend
  image: appleboy/drone-ssh
  settings:
    host:
      from_secret: SSH_HOST
    username:
      from_secret: SSH_USER
    key:
      from_secret: SSH_KEY
    port: 22
    script:
      - echo "Run backend update"
      - cd /app/backend/laravel
      - git reset --hard HEAD
      - git pull origin main
      - echo "Backend updated!"
      - cd /app
      - git pull origin main
      - docker compose restart
      - echo "Backend config updated!"
      - echo "Containers restarted!"

- name: notify
  image: appleboy/drone-telegram:1.3.10
  settings:
    token:
      from_secret: TELEGRAM_TOKEN
    to:
      from_secret: TELEGRAM_CHAT_ID
    message: |
      ✅ Успешно развернуто приложение после коммита на ветке main!
      📝 Коммит: ${DRONE_COMMIT_MESSAGE}
      👤 Автор: ${DRONE_COMMIT_AUTHOR}
      🔗 ${DRONE_COMMIT_PATH}/${DRONE_COMMIT_SHA}

До этого я не настраивал CI/CD. Есть несколько популярных инструментов, и, безусловно, у каждого есть свои преимущества, но мне понравилось использование Drone.io. Во время первой настройки пришлось потратить время, чтобы разобраться, но оно того стоило. Реализация CI/CD-пайплайна в другом проекте заняла значительно меньше времени.

CI/CD-пайплайн готов. Теперь разработчики локально ведут разработку, а при pull в main-ветку обновляется удалённое тестовое окружение, уведомление об обновлении приходит в группу Telegram.

Схема настроенных docker сервисов
Схема настроенных docker сервисов

После начала работы с тестовым сервером обнаружили, что в процессе сборки проекта Next.js сервер падает.

Нехватка оперативной памяти

Краткая инструкция по созданию файла подкачки (SWAP) на Linux

Гадать долго не пришлось — системе не хватало оперативной памяти. По умолчанию SWAP отключён в Ubuntu. Вспомнил, что можно использовать файл подкачки (SWAP), добавляя возможность использовать свободное место на SSD для работы с оперативной памятью.

Перед началом и после завершения выполнения команд проверяем доступную оперативную и SWAP-память в системе, используя команду free -h.

# free -h
              total        used        free      shared  buff/cache   available
Mem:            15G        7.1G        1.0G        1.0G        7.4G        7.0G
Swap:          511M          0B        511M

Затем выполняем последовательно команды:

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Теперь, когда в системе закончится оперативная память, будет использован SWAP-файл. Использование файла подкачки менее эффективно, чем оперативной памяти, но предотвращает падение процесса во время сборки, что достаточно для тестового сервера без нагрузки.


После подготовки окружения, я принялся к подготовке к работе с документированием API.

Документация

Для согласования API по опыту команды решено использовать Swagger. Существует библиотека OpenAPI, которая умеет генерировать Swagger на основе аннотаций. Для Laravel есть обёртка, которая делает работу с OpenAPI ещё удобнее.

darkaonline/l5-swagger

Устанавливаем библиотеку через composer и добавляем файл конфигурации в наш проект:

composer require "darkaonline/l5-swagger"
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"

Затем регистрируем сервис-провайдер в AppServiceProvider:

$this->app->register(\L5Swagger\L5SwaggerServiceProvider::class);

Теперь в приложении зарегистрирован /api/documentation endpoint, где будет размещаться Swagger-сайт с нашим API. Позже я закрыл к нему доступ посторонних глаз через авторизацию Битрикс.

Конфигурация генератора происходит в файле /config/l5-swagger.php. Можно настроить маршруты, авторизацию и директорию для сканирования аннотаций. Я использовал аннотации только в контроллерах, так как там ими проще управлять.

Перед началом описания ручек необходимо описать @Info, где содержится основная информация о проекте и контактные данные автора. Так как я использую аннотации к контроллерам, то добавил @Info к наследуемому классу Controller.

...
use OpenApi\Annotations as OA;
/**
 * @OA\Info(
 *     description="",
 *     version="1.0.0",
 *     title="REST API Документация",
 *     @OA\Contact(
 *         email="example@example.com"
 *     ),
 * )
 * 
 */
 class Controller
 ...

Затем для каждого action, который отвечает за endpoint, подготавливаю описание для Swagger в стиле аннотаций. Иногда получается громоздко, особенно при описании POST-методов. Допустимо сворачивать описание отдельных объектов и хранить их отдельно.

Пример описания endpoint:

...
/**
* @OA\Get(
*     path="/api/news/{id}",
*     tags={"news"},
*     summary="Получить новость по ID",
*     @OA\Parameter(
*         name="id",
*         in="path",
*         required=true,
*         @OA\Schema(type="integer")
*     ),
*     @OA\Response(
*         response=200,
*         description="Успешный ответ",
*         @OA\JsonContent(
*              @OA\Property(property="message", type="string", example="Новость"),
*              @OA\Property(property="data", ref="#/components/schemas/NewsItemDetail"),
*          )
*     ),
*     @OA\Response(
*         response=404,
*         description="Страница не найдена",
*         @OA\JsonContent(
*             @OA\Property(property="message", type="string", example="The given data was invalid."),
*             @OA\Property(property="error", type="boolean", example="true"),
*             @OA\Property(property="data", type="array", example="[]", @OA\Items()),
*         )
*     )
* )
*/
public function detail(int $id)
...

Параметр tags={"news"} группирует маршруты по разделам.

Подготовив необходимые аннотации, необходимо сгенерировать документацию. Выполнить команду:

php artisan l5-swagger:generate

Теперь по указанному в файле конфигурации пути будет доступен сайт Swagger с нашими ручками. В аннотациях указываются все доступные GET-параметры, ожидаемые POST-ключи, токены доступа для закрытых маршрутов — всё, что может быть необходимо для тестирования HTTP-запроса.

Пример API документации Swagger
Пример API документации Swagger

Выкручивая пальцы при написании аннотаций, обнаружил, что ИИ может генерировать их. Правда, приходилось много править и разбираться в том, как они устроены. Гораздо проще было бы описать OpenAPI в YAML вручную, я думаю. Не советую данный подход.

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

В POSTMAN уже добавили встроенные инструменты для генерации HTTP-тестов при помощи ИИ. Можно импортировать запрос как CURL, JS fetch туда и обратно, использовать параметры. Одна из крутых вещей — Interceptor: подключается к браузеру через плагин и тянет все Cookie. Так что закрытые авторизацией ручки перестают быть проблемой во время разработки.

Битрикс как сервис

В процессе работы над проектом стало понятно, что для работы с инфоблоками лучше использовать API Битрикс. Его классы автоматически подтягивают данные о таблице свойств, которая может быть настроена по-разному: в общей либо отдельной таблице. Значит, что Bitrix используется не только как админка, но и как полноценный компонент системы. Для использования Битрикс я создал отдельный сервис-провайдер и зарегистрировал его в приложении. Теперь у нас есть доступ к кодовой базе Битрикс.

Класс BitrixService содержит описание загрузки ядра. Добавляем в сервис-контейнер приложения.

namespace App\Providers;

use App\Services\Bitrix\BitrixService;
use Illuminate\Support\ServiceProvider;

class BitrixProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register(): void
    {
        $this->app->singleton('bitrix', function () {
            return new BitrixService();
        });
...

Получение инстанса приложения Битрикс доступно через обращение к сервис-контейнеру app('bitrix')->getInstance(); в любом месте приложения. Использование сервис-провайдеров позволяет загружать ядро Битрикс только тогда, когда это необходимо. Я использую инъекцию зависимости, чтобы указать Laravel использовать BitrixService в классе, где ожидается использование Bitrix API.

Классы сущностей для инфоблоков

Битрикс предоставляет мощный механизм для генерации ORM-классов на основе инфоблоков. Для его активации необходимо в настройках инфоблока указать «Символьный код API». Например, после установки кода news для инфоблока новостей система автоматически создаёт класс ElementNewsTable, предоставляющий объектный доступ к данным и свойствам инфоблока без дополнительных запросов.

Пример базового использования:

$list = \Bitrix\Iblock\Elements\ElementNewsTable::getList()->fetchAll();

Помимо класса для работы с таблицей генерируются два вспомогательных класса: EO_ElementNews (объект сущности) и EO_ElementNews_Collection (коллекция объектов). Они служат основой для расширения функциональности в проекте.

Расширение стандартных классов

В проекте стандартные классы были унаследованы для добавления необходимой бизнес-логики и унификации работы с данными.

1. Кастомный Table-класс
Был создан класс NewsTable, который наследует сгенерированный ElementNewsTable. В нём реализованы два ключевых метода, возвращающих пути к собственным классам сущности и коллекции, что позволяет ORM Битрикс использовать расширенные классы вместо стандартных.

class NewsTable extends ElementNewsTable
{
    const ENTITY_CLASS = 'App\Models\News\NewsEntity';
    const ENTITY_COLLECTION_CLASS = 'App\Models\News\NewsCollection';

    public static function getObjectClass(): string {
        return self::ENTITY_CLASS;
    }

    public static function getCollectionClass(): string {
        return self::ENTITY_COLLECTION_CLASS;
    }
}

Этот же класс содержит методы для типичных операций: получения, создания, обновления и удаления записей, учитывающие специфику проекта.

2. Кастомный Entity-класс
Класс NewsEntity наследует сгенерированный EO_ElementNews и отвечает за представление и поведение отдельной новости. В нём инкапсулирована логика работы с данными.

  • Преобразование данных: Метод getGallery() получает значения свойства «MORE_PHOTO» (привязанные изображения), запрашивает через FileTable соответствующие файлы и возвращает массив публичных URL-адресов.

  • Сохранение данных: Метод setNewGalleryImages() принимает массив загруженных файлов (объекты UploadedFile из Laravel), преобразует их в формат, понятный для CIBlockElement::SetPropertyValues, и сохраняет в свойство инфоблока.

Важный момент: вся валидация входных данных (проверка типов, размеров, MIME-типов) выполняется на более ранних слоях приложения (HTTP-запрос, сервисный слой). На уровне Entity-класса предполагается работа уже с проверенными и безопасными данными.

3. Кастомный Collection-класс
Класс NewsCollection наследует EO_ElementNews_Collection и расширяет его единственным методом toArray(). Этот метод проходит по всем объектам коллекции и вызывает их собственный метод toArray(), который в NewsEntity реализует конечное преобразование данных в структуру, готовую для отдачи по API.


class NewsCollection extends EO_ElementNews_Collection
{
    public function toArray(): array {
        return array_map(fn($item) => $item->toArray(), iterator_to_array($this));
    }
}

Итог подхода

Представленная трёхслойная структура (Table → Entity → Collection) позволяет:

  • Максимально использовать встроенные ORM-возможности Битрикс для работы с инфоблоками.

  • Чётко разделить ответственность: Table работает с базой, Entity содержит бизнес-логику элемента, Collection — логику работы с набором элементов.

  • Легко расширять функциональность в одном месте, не вмешиваясь в кодогенерацию Битрикс.

  • Получать на выходе удобные для API структуры данных.

Этот паттерн был последовательно применён ко всем ключевым инфоблокам проекта, что обеспечило единообразие кода и упростило его поддержку.

Архитектурное решение задает тон всему проекту...
Архитектурное решение задает тон всему проекту...

JWT-авторизация

В стандартной поставке Битрикс используется авторизация на основе сессий, что не подходит для REST API. Хотя в Битрикс существует собственная реализация JWT, в данном проекте было принято решение использовать проверенный пакет из экосистемы Laravel, адаптировав его под модель пользователя Битрикс.

AuthController: управление аутентификацией

За логику входа, выхода и обновления токенов отвечает контроллер AuthController. Для валидации входных данных используется механизм Laravel Validator, что обеспечивает чистый код и стандартизированные сообщения об ошибках.

Ключевые особенности реализации метода login():

  1. Валидация данных: Проверяется наличие и минимальная длина полей email и password.

  2. Гибкие учётные данные: Система пытается аутентифицировать пользователя, используя переданное значение как LOGIN, а затем как EMAIL, что обеспечивает совместимость с данными Битрикс.

  3. Единый ответ: При успешной аутентификации вызывается метод respondWithToken(), который возвращает стандартизированный JSON-ответ с JWT-токеном, временем его жизни и типом.

  4. Обработка ошибок: Все исключения (неудачная валидация, неверные учётные данные) перехватываются и выбрасываются как BaseException с соответствующим HTTP-кодом (401).

Прочие методы контроллера:

  • me() — возвращает данные текущего аутентифицированного пользователя.

  • validateToken() — проверяет актуальность токена.

  • logout() — выполняет выход (инвалидация токена на стороне клиента).

  • refresh() — обновляет истёкший токен.

BitrixUser: адаптация модели пользователя под JWT

Чтобы пакет JWT для Laravel мог работать с таблицей b_user Битрикс, создана модель BitrixUser, которая наследуется от Authenticatable и реализует интерфейс JWTSubject.

Основные настройки модели:

  • Указана таблица b_user и первичный ключ ID.

  • Отключены временные метки Laravel (timestamps), так как Битрикс использует свои поля.

  • Определены fillable и hidden атрибуты для корректной работы с массивом данных.

JWT-интеграция:

  • Метод getJWTIdentifier() возвращает ID пользователя (ключ модели).

  • Метод getJWTCustomClaims() позволяет добавить в токен дополнительные данные (в текущей реализации не используется).

Расширенная бизнес-логика:

  1. Определение ролей: Методы isExpert() и isOrganization() проверяют принадлежность пользователя к соответствующим группам Битрикс (кэшируя результат для производительности). Для этого настроено отношение «многие-ко-многим» с таблицей групп (b_user_group).

  2. Управление группами: Реализованы методы addToGroup() и removeFromGroup() для динамического управления членством.

  3. Связь с профилем организации: Метод getOrganization() через сервис NkoService получает связанные данные об организации, используя PERSONAL_ICQ как поле для хранения ИНН.

  4. Кастомное преобразование в массив: Метод toArray() формирует ответ API, включая флаги ролей и, при необходимости, данные организации.

BitrixUserProvider: провайдер аутентификации

Для корректной работы механизма аутентификации Laravel с нестандартной структурой таблицы пользователей (b_user) создан кастомный провайдер BitrixUserProvider, расширяющий EloquentUserProvider.

Главная задача провайдера — «объяснить» Laravel, как искать пользователя и проверять его пароль, учитывая специфику Битрикс.

Ключевые отличия от стандартного провайдера:

  • Поиск по учётным данным: Метод retrieveByCredentials корректно фильтрует массив credentials, исключая поле пароля перед построением запроса, что позволяет искать по LOGIN или EMAIL.

  • Валидация пароля: Метод validateCredentials использует поле PASSWORD (а не стандартное password) для получения хэша и проверяет его с помощью хэшера Laravel, который предварительно настроен на алгоритм Битрикс.

Итог: эта трёхкомпонентная архитектура (Контроллер, Модель, Провайдер) обеспечивает бесшовную интеграцию мощного JWT-механизма Laravel с унаследованной базой пользователей Битрикс, сохраняя при этом гибкость и соблюдая стандарты REST API.

Конфигурация аутентификации

Код конфигурации, поможет расставить все на свои места.

<?php

return [
    'defaults' => [
        'guard' => 'api',
        'passwords' => 'bitrix_users',
    ],

    'guards' => [
        'api' => [
            'driver' => 'jwt',
            'provider' => 'bitrix_users',
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => env('AUTH_MODEL', App\Models\BitrixUser::class),
        ],
        'bitrix_users' => [
            'driver' => 'bitrix_users_driver',
            'model' => App\Models\BitrixUser::class,
        ],
    ],

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

Организация маршрутов API: принципы и структура

Архитектура маршрутизации построена с соблюдением REST-принципов и модульности Laravel. Все endpoint'ы сконцентрированы в файле routes/api.php, который сохраняет высокую читаемость благодаря логической группировке и middleware-ориентированному подходу.

Ключевые архитектурные решения

  1. Логическая группировка по доменам — маршруты сгруппированы по бизнес-направлениям (новости, проекты, аутентификация)

  2. Middleware-цепочки — вся логика контроля доступа вынесена в отдельные middleware

  3. Единая обработка ошибок — централизованный обработчик для несуществующих маршрутов

  4. Полное документирование — все endpoint'ы описаны через OpenAPI/Swagger-аннотации

Примеры структурной организации

1. Публичный контент (без аутентификации)


// Статические страницы
Route::get('/content/faq', [FaqController::class, 'index']);
Route::get('/content/about', [AboutController::class, 'index']);
Route::get('/content/documents', [DocumentController::class, 'index']);

// Новости
Route::get('/news', [NewsController::class, 'index']);
Route::get('/news/{id}', [NewsController::class, 'detail']);

2. Система аутентификации и регистрации


Route::group([
    'middleware' => 'api',
    'prefix' => 'auth'
], function ($router) {
    Route::post('login', [AuthController::class, 'login']);
    Route::post('register', [RegistrationController::class, 'register']);
    Route::post('logout', [AuthController::class, 'logout']);
    Route::get('me', [AuthController::class, 'me']);
});

3. Защищённые ресурсы с ролевым доступом


// Доступ только для организаций
Route::middleware([AuthOrganization::class])->group(function() {
    Route::post('/news/add', [NewsController::class, 'add']);
    Route::post('/projects/add', [ProjectsController::class, 'add']);
});

// Дополнительная проверка владения ресурсом
Route::middleware([AuthOrganization::class, CheckNewsOwner::class])->group(function() {
    Route::post('/news/update/{id}', [NewsController::class, 'update']);
    Route::delete('/news/delete/{id}', [NewsController::class, 'delete']);
});

4. Интеграция с внешними сервисами


// Валидация данных через DaData API
Route::post('/data/address', [DaDataController::class, 'address']);
Route::post('/data/company', [DaDataController::class, 'company']);

5. Специализированный функционал для экспертов


Route::middleware([AuthExpert::class])->group(function () {
    Route::post('/rating/vote', [RatingController::class, 'vote']);
    Route::get('/rating/vote/{id}', [RatingController::class, 'getVote']);
});

Кастомные Middleware для контроля доступа

Система использует специализированные middleware, каждый из которых отвечает за определённый аспект безопасности:

  • AuthOrganization — проверка принадлежности к группе организаций

  • AuthExpert — проверка роли эксперта

  • CheckResourceOwner — проверка прав владения конкретным ресурсом

Универсальная обработка ошибок маршрутизации

Route::fallback(function () {
    throw new BaseException("Маршрут не существует", 404);
});

Преимущества выбранного подхода

  1. Масштабируемость — новые endpoint'ы добавляются в соответствующие логические группы

  2. Безопасность — многоуровневая система проверок (аутентификация → роль → владение)

  3. Поддерживаемость — чёткая структура упрощает навигацию и рефакторинг

  4. Тестируемость — группы маршрутов могут тестироваться изолированно

  5. Документированность — аннотации OpenAPI обеспечивают автоматическую генерацию документации

Контроллеры: структура и принципы организации

Архитектура контроллеров построена на принципах модульности и единообразия. Все API-контроллеры наследуются от модифицированного базового класса, который обеспечивает унифицированный формат ответов. Это решение значительно упрощает интеграцию с фронтендом, предоставляя предсказуемую структуру данных.

Базовый контроллер

namespace App\Http\Controllers;

use \App\Http\Response;
use Illuminate\Http\JsonResponse;
use OpenApi\Annotations as OA;

class Controller
{
    private $service;

    /**
     * Метод форматирует HTTP Response
     * {
     *      "message": string,
     *      "data": any?,
     *      "error": string?
     * }
     */
    public function res(string $message, array|null $data, int $status = 200): JsonResponse {
        return response()
            ->json([
                'message' => $message,
                'data' => $data,
            ], $status);
    }
}

Ключевые особенности организации:

Централизованная обработка ответов
Базовый метод res() формирует все ответы API в едином формате, включающем сообщение, данные и статус. Это обеспечивает консистентность и упрощает клиентскую логику.

Логическая группировка по функциональности
Контроллеры сгруппированы по бизнес-доменам. Особое внимание уделено аутентификации — она вынесена в отдельный модуль с тремя специализированными контроллерами: для JWT-операций, регистрации и восстановления пароля.

Интеграция документации в код
Swagger-аннотации размещены непосредственно в контроллерах, а не в маршрутах. Это позволяет разработчику видеть документацию рядом с кодом и упрощает поддержку при изменениях. Хотя это спорный момент, и я бы не стал так делать впредь.

Многоуровневая валидация
Все контроллеры используют встроенный механизм валидации Laravel. Простые проверки выполняются непосредственно в методах действий, сложные — выносятся в отдельные методы или классы. Кастомные сообщения об ошибках локализованы для удобства пользователей.

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

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

Базовый подход: валидация в методе действия

public function register(Request $request): JsonResponse
{
    $validator = Validator::make($request->only(['email', 'inn']), [
        'email' => 'required|email|unique:b_user,EMAIL',
        'inn' => 'required|digits_between:10,12'
    ], [
        'email.required' => 'Поле email обязательно для заполнения',
        'email.unique' => 'Пользователь с таким email уже зарегистрирован',
        'inn.required' => 'Поле ИНН обязательно для заполнения',
        'inn.digits_between' => 'ИНН должен содержать от 10 до 12 цифр',
    ]);

    if ($validator->fails()) {
        throw new BaseException(
            json_encode($validator->errors()->all()),
            422 // Unprocessable Entity
        );
    }
    
    // Логика регистрации
    return $this->res('Регистрация успешна', $userData);
}

Расширенный подход: вынесение валидации

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

protected function validateRegistrationData(array $data): array
{
    $rules = [
        'email' => 'required|email|unique:b_user,EMAIL',
        'inn' => 'required|digits_between:10,12',
        'password' => 'required|min:8|confirmed',
        'organization_name' => 'required_if:type,organization|max:255',
        'expert_qualification' => 'required_if:type,expert|max:500',
    ];
    
    $messages = [
        // Кастомные сообщения для всех правил
    ];
    
    return Validator::validate($data, $rules, $messages);
}

// В методе контроллера
public function register(Request $request)
{
    $validated = $this->validateRegistrationData($request->all());
    // Дальнейшая обработка
}

Сервисы

Схема движения запроса по API
Схема движения запроса по API

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

Типы сервисов в проекте:

  1. Сервисы инфоблоков — тонкие прослойки над ORM Битрикс, сохраняющие архитектурную целостность.

  2. Бизнес-сервисы — содержат сложную логику с множеством зависимостей.

  3. Сервисы-интеграторы (DaData) — взаимодействие с внешними API.

Ключевые архитектурные решения:

  • Внедрение зависимостей через конструктор для тестируемости.

  • Базовые классы для устранения дублирования кода.

  • Сохранение простых сервисов как точек расширения для будущей логики.

Сервисный слой обеспечивает соблюдение принципа единой ответственности, лёгкое тестирование и гибкую архитектуру, готовую к эволюции по мере роста проекта.

Сервисы как координаторы: логика без доступа к данным

В проекте сервисы играют строго отведённую роль: они координируют вызовы репозиториев и содержат сквозную бизнес-логику (валидацию, преобразование данных, простую агрегацию), но не обращаются к БД, внешним API или файловой системе напрямую. Этот принцип сделал их предсказуемыми, тестируемыми и независимыми от изменений в источниках данных.

Ключевой паттерн: каждый сервис — это фасад для определённого бизнес-домена (DaData, проекты, пользователи). Он получает репозитории через Dependency Injection и делегирует им всю работу с данными.

<?php
namespace App\Services;

use App\Repositories\DaDataRepository;
use App\Repositories\DataDirectoryRepository;
use App\Exceptions\BaseException;

class DaDataService
{
    public function __construct(
        protected DaDataRepository $repository,
        protected DataDirectoryRepository $directory
    ) {}

    public function getCompanyByInn(string $inn_value): array
    {
        $companyList = $this->repository->getCompanyByInn($inn_value);

        if (!isset($companyList[0])) {
            throw new BaseException("Организация с ИНН {$inn_value} не найдена", 404);
        }
        return $companyList[0];
    }
}

Репозитории: специализированные адаптеры к данным

Репозитории в проекте — тонкие прослойки, единственная задача которых — общаться с конкретным источником данных и возвращать результат в сыром, предсказуемом виде. Они не содержат бизнес-логики.

Типы репозиториев в проекте:

  1. API-репозитории (как DaDataRepository) — инкапсулируют работу с внешними HTTP-API.

  2. Локальные репозитории (как DataDirectoryRepository) — работают с внутренними справочниками через Eloquent.

  3. Bitrix-репозитории — адаптеры к ORM-классам инфоблоков, скрывающие сложность работы со свойствами.

Ключевые преимущества подхода:

  • Изоляция изменений — модификации схемы данных локализованы в одном месте.

  • Унификация интерфейсов — сервисы работают с данными через стандартные методы.

  • Гибкость — возможность кэширования, оптимизации запросов, работы с несколькими источниками.

  • Тестируемость — лёгкое мокирование в юнит-тестах.

Репозитории особенно важны в контексте гибридной архитектуры, где часть данных хранится в инфоблоках Битрикс, а часть — в собственных таблицах Laravel. Они выступают в роли «переводчиков» между разными парадигмами.

<?php
namespace App\Repositories;

use Dadata\DadataClient;

class DaDataRepository
{
    public function __construct(
        private DadataClient $dadata
    ) {}

    public function getCompanyByInn(string $inn): array
    {
        return $this->dadata->findById("party", $inn);
    }
}

Внедрение репозиториев в сервисы

Репозитории внедряются в сервисы через dependency injection, что обеспечивает лёгкое тестирование и замену реализации:

class ProjectService
{
    public function __construct(
        private ProjectRepository $projectRepository,
        private ExpertRepository $expertRepository, // Добавлено
        private ApplicationRepository $applicationRepository,
        private NotificationRepository $notificationRepository
    ) {}
    
    public function assignExpert(int $projectId, int $expertId): void
    {
        $project = $this->projectRepository->findOrFail($projectId);
        $expert = $this->expertRepository->findOrFail($expertId);
        
        if (!$project->canAcceptApplications()) {
            throw new ProjectClosedException();
        }
        
        $application = $this->applicationRepository->create([
            'project_id' => $projectId,
            'expert_id' => $expertId,
            'status' => 'pending'
        ]);
        
        $this->notificationRepository->createForExpertAssignment($expert, $project);
    }
}

Такое разделение ответственности создало чистую, модульную архитектуру. Сервис определяет «что» нужно сделать, а репозиторий знает «как» это сделать технически.


Выводы и предостережения: боль гибридной разработки

Этот проект стал для меня важным уроком. После полутора месяцев работы над 50+ endpoint'ами я пришёл к выводу, который, возможно, вызовет гнев у сторонников гибридных решений, но, надеюсь, найдёт отклик у тех, кто прошёл через подобное.

Не повторяйте этот опыт.

Для эффективной разработки не нужен дополнительный фреймворк. Битрикс сегодня — это не CMS образца 2010 года. Он интегрирует Composer, обрастает поддержкой Docker и постепенно перенимает лучшие практики. Весь мой путь с Laravel в ретроспективе выглядит как сложный обходной манёвр там, где была прямая дорога.

Главный инсайт: когнитивная цена скрещивания

Основная проблема — не в технической совместимости. Её можно обеспечить. Проблема — в постоянном контекст-свитчинге. Мозг разработчика вынужден каждые пять минут переключаться между философией Битрикс (модули, инфоблоки, D7) и философией Laravel (Eloquent, Service-Repository, Middleware). Это выматывает, приводит к ошибкам и убивает ско��ость.

Логичнее было бы:

  1. Либо найти/обучить сильного Битрикс-разработчика, способного расширить нативное API под наши нужды.

  2. Либо реализовать требуемую логику в рамках самой CMS, не выходя за её границы.

Laravel в этой связке оказался лишним. Он не решил проблем, а создал новый слой сложности.

Жёсткая правда, к которой я пришёл

Даже если вы не умеете проектировать идеальную архитектуру с нуля, пишите на чистом PHP в рамках одной экосистемы. Со временем структура встанет на свои места в ходе рефакторинга. Но у вас не будет того слоистого бардака и конфликтующих зависимостей, который неизбежно возникает при интеграции двух монстров.

Laravel — большая, зрелая экосистема. Битрикс — тоже. Каждая требует глубокого погружения в свои правила и конвенции. Попытка знать обе на достаточном уровне — путь к тому, чтобы быть вечным Junior в обеих.

Итог для коллег

Если вы стоите перед выбором «Битрикс + Laravel», задайте себе вопрос: действительно ли проблема в ограничениях Битрикс, или в недостатке знаний о его возможностях? В 90% случаев верно второе.

С точки зрения долгосрочной поддержки и производительности, проще и надёжнее, когда разработчик работает в одной, пусть и неидеальной, но целостной системе. Поддержка гибрида — это асимптотический рост сложности с каждым новым feature.

Этот проект научил меня простой вещи: иногда нужно не искать сложные архитектурные ответы, а честно признать, что ты пытаешься закрыть пробел в знаниях костылём из другой технологии. И лучшее решение в такой ситуации — не костыль, а возврат к основам и изучение инструмента, который уже есть в проекте.

P.S. Возможно, лет через десять, оглядываясь на этот текст, я посмеюсь над своим максимализмом. Но сегодня, с горячими следами от баттхерта на клавиатуре, я уверен: скрещивать ежа и ужа — плохая идея, даже если очень хочется доказать, что ты это можешь.

Жду вашего мнения в комментариях. Голосуйте, думаю будет интересно узнать как на этот вопрос смотрит сообщество.

Изображение для голосования. "Только так и это бигмак" vs "Не дай бог никому"
Изображение для голосования. "Только так и это бигмак" vs "Не дай бог никому"
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Совмещение 2 фреймворков в одном проекте:
6.67%Только так и это…1
93.33%Ни дай бог никому14
Проголосовали 15 пользователей. Воздержались 2 пользователя.