Как мы приручили рутину в 1C-Битрикс: автоматизация разработки CLI-командами

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Мы оказывались в этой ситуации не раз. Вместо того чтобы смириться с рутиной, решили действовать. Так появился наш набор CLI-команд для автоматизации разработки на 1C-Битрикс. Это не просто утилиты, а инструмент, который ускорил выполнение типичных задач, сделал процессы предсказуемыми и уменьшил вероятность ошибок.

Меня зовут Артур Низамов, я ведущий разработчик в НЛМК ИТ. В этой статье я расскажу, что нас мотивировало на изменения, какие команды мы добавили, как они работают и какой эффект это принесло.

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

Предпосылки: почему мы создали инструмент?

Большинство проектов на 1C-Битрикс сопровождаются рутинными задачами, которые мало чем отличаются от проекта к проекту. Среди них:

  • Создание компонентов: настройка class.php, реализация методов (включая AJAX) и добавление lang-файлов.

  • Создание шаблонов для компонентов: ручное создание папки templates, файлов template.phpresult_modifier.php и их локализаций.

  • Создание модулей: повторяющаяся работа по созданию файлов install.phpinclude.phpoptions.php и их структуры.

С каждым новым проектом мы яснее понимали, что с увеличением задач всё чаще возникают типичные проблемы:

  • Потеря времени: однотипные действия могли занимать часы.

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

  • Несогласованность: каждый разработчик вносил что-то своё в структуру, что мешало единообразию.

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

Вскоре мы поняли: нам нужно универсальное решение для всей команды. Так родилась идея создать набор CLI-команд, который мы назвали bitrix.mate — он стал нашим "помощником и напарником" в работе над проектами.


Как работает bitrix.mate: пример команд

Далее, используя команды doc:orm-plant-uml и component:make, я покажу, как реализованы наши утилиты и какую пользу они приносят.

Команда doc:orm-plant-uml

Одной из задач, которую мы хотели автоматизировать, стало создание документации. Команда doc:orm-plant-uml генерирует PlantUML-диаграммы из ORM-сущностей, позволяя визуализировать структуру данных и их связи, что значительно облегчает понимание и поддержку проекта.

Основные технологии:

Мы использовали Symfony Console — это дало нам удобный интерфейс для настройки аргументов и команд.

Пример:

bitrix-mate doc:orm-plant-uml /path/to/MyEntity.php -r

Как это реализовано:

CreateOrmPlantUmlCommand отвечает за обработку входных параметров и управление процессом генерации. Основная логика сосредоточена в OrmPlantUmlAction, который анализирует ORM-сущности, построение их структуры и связей, а также сохранение результата в формат PlantUML.

Упрощенный код команды
use Bitrix\Main\Localization\Loc;
use NLMK\Bitrix\Mate\Action\Doc\OrmPlantUmlAction;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
 
class CreateOrmPlantUmlCommand extends Command
{
 
    protected function configure()
    {
        $this
            ->setName('doc:orm-plant-uml')
            ->setDescription('Генерирует plantuml')
            ->setHelp('Эта команда позволяет сгенерировать plantuml для orm')
            ->addArgument('path', InputArgument::REQUIRED, 'Путь до файла класса')
            ->addOption('recursive', 'r', InputOption::VALUE_NONE, 'Рекурсивно отразить все связи')
        ;
    }
 
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        try {
            $path = $input->getArgument('path');
            $isRecursive = $input->getOption('recursive') ?? false;
 
            if (!$path) {
                throw new RuntimeException('Path is required');
            }
 
            $filePath = (new OrmPlantUmlAction())->run($path, $isRecursive);
 
            $output->writeln(Loc::getMessage('NLMK_BITRIX_MATE_COMMAND_DOC_PLANT_UML_SUCCESS', [
                '#DOC_PATH#' => $filePath,
            ]));
 
            return Command::SUCCESS;
 
        } catch (Throwable $throwable) {
            $output->writeln(Loc::getMessage('NLMK_BITRIX_MATE_COMMAND_DOC_PLANT_UML_ERROR', [
                '#ERROR#' => $throwable->getMessage()
            ]));
        }
 
        return Command::FAILURE;
    }
 
}

Упрощенный код OrmPlantUmlAction
use Bitrix\Main\Entity\ScalarField;
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use NLMK\Bitrix\Mate\Service\PathService;
use RuntimeException;
 
class OrmPlantUmlAction
{
 
    protected PathService $pathService;
 
    public function __construct()
    {
        $this->pathService = new PathService();
    }
 
    public function run(string $pathClass, bool $isRecursive): string
    {
        //получаем полный путь к файлу
        $filePath = sprintf('%s/%s', $this->pathService->root(), $pathClass);
 
        if (!file_exists($filePath)) {
            throw new RuntimeException('The file does not exist');
        }
 
        require_once $filePath;
 
        //получаем класс из файла
        $classes = get_declared_classes();
        $className = end($classes);
 
        if (!is_subclass_of($className, DataManager::class)) {
            throw new RuntimeException('The file does not contain a DataManager class');
        }
 
        //собираем информацию о полях
        $entities = [$className => $this->processEntity($className)];
 
        $umlContent = ["@startuml"];
        $referenceContent = [];
        foreach ($entities as $content) {
            $umlContent = [...$umlContent, ...$content['structure']];
 
            if (!$isRecursive) {
                continue;
            }
 
            $this->processReferences(
                content: $content,
                umlContent: $umlContent,
                entities: $entities,
                referenceContent: $referenceContent
            );
        }
 
        $umlContent = [...$umlContent, ...$referenceContent];
 
        $umlContent[] = "@enduml";
 
        //puml складываем рядом с ORM-классом
        $outputPath = preg_replace('/\.php$/', '.puml', $filePath);
        file_put_contents($outputPath, implode("\n", $umlContent));
 
        return $outputPath;
    }
 
    protected function processEntity(string $className): array
    {
        /** @var DataManager $entity */
        $entity = new $className();
        $entityName = $entity::getTableName();
        $structure = ["entity $entityName {"];
        $references = [];
 
        foreach ($entity::getMap() as $field) {
            //собираем информацию об обычных полях
            if ($field instanceof ScalarField) {
                $fieldLine = "  {$field->getName()} : {$field->getDataType()}";
                if ($field->isPrimary()) {
                    $fieldLine .= " <<PK>>";
                }
                if (!$field->isNullable()) {
                    $fieldLine .= " <<Mandatory>>";
                }
                if (!$field->getTitle()) {
                    $fieldLine .= " -- {$field->getTitle()}";
                }
                $structure[] = $fieldLine;
                continue;
            }
 
            //собираем информацию о рефересных полях
            if ($field instanceof Reference) {
                $refEntity = $field->getRefEntity();
                $references[] = [
                    'class' => $refEntity->getDataClass(),
                    'field' => $field->getName(),
                    'thisEntity' => $entityName,
                    'refEntity' => $refEntity->getDataClass()::getTableName(),
                ];
            }
        }
 
        $structure[] = "}";
 
        return ['structure' => $structure, 'references' => $references];
    }
 
    protected function processReferences(array $content, array &$umlContent, array &$entities, array &$referenceContent): void
    {
        //проходимся по всем рефересным полям
        foreach ($content['references'] as $reference) {
            if (!isset($entities[$reference['class']])) {
                //получаем информацию о сущности
                $referenceEntityContent = $this->processEntity($reference['class']);
                //добавялем в общий массив чтобы так же отразить всю структуру и ее референсные сущности
                $entities[$reference['class']] = $referenceEntityContent;
                $umlContent = [...$umlContent, ...$entities[$reference['class']]['structure']];
                $this->processReferences($referenceEntityContent, $umlContent, $entities, $referenceContent);
            }
            $referenceContent[] = "{$reference['thisEntity']}::{$reference['field']} ||--|| {$reference['refEntity']}";
        }
    }
}

Результат использования:

Класс ORM
use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\IntegerField;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Fields\StringField;
use Bitrix\Main\ORM\Query\Join;
 
class MyDictionaryEntityTable extends DataManager
{
    public static function getTableName()
    {
        return 'my_dictionary_entity';
    }
 
    public static function getMap()
    {
        return [
            (new IntegerField('ID'))->configurePrimary()->configureAutocomplete(),
            (new StringField('NAME'))->configureRequired()->configureTitle('Наименование'),
        ];
    }
}
 
class MyParentEntityTable extends DataManager
{
    public static function getTableName()
    {
        return 'my_parent_entity';
    }
 
    public static function getMap()
    {
        return [
            (new IntegerField('ID'))->configurePrimary()->configureAutocomplete(),
            (new StringField('NAME'))->configureRequired()->configureTitle('Наименование'),
            (new IntegerField('DICTIONARY_ID'))->configureNullable()->configureTitle('Справочник'),
            (new Reference(
                'DICTIONARY',
                MyDictionaryEntityTable::class,
                Join::on('this.DICTIONARY_ID', 'ref.ID')
            ))
        ];
    }
}
 
class MyChildEntityTable extends DataManager
{
    public static function getTableName()
    {
        return 'my_child_entity';
    }
 
    public static function getMap()
    {
        return [
            (new IntegerField('ID'))->configurePrimary()->configureAutocomplete(),
            (new StringField('NAME'))->configureRequired()->configureTitle('Наименование'),
            (new IntegerField('PARENT_ID'))->configureNullable()->configureTitle('Родитель'),
            (new Reference(
                'PARENT',
                MyParentEntityTable::class,
                Join::on('this.PARENT_ID', 'ref.ID')
            ))
        ];
    }
}

Результат в PlantUML
@startuml
entity my_child_entity {
  ID : integer <<PK>> <<Mandatory>> -- ID
  NAME : string <<Mandatory>> -- Наименование
  PARENT_ID : integer -- Родитель
}
entity my_parent_entity {
  ID : integer <<PK>> <<Mandatory>> -- ID
  NAME : string <<Mandatory>> -- Наименование
  DICTIONARY_ID : integer -- Справочник
}
entity my_dictionary_entity {
  ID : integer <<PK>> <<Mandatory>> -- ID
  NAME : string <<Mandatory>> -- Наименование
}
my_parent_entity::DICTIONARY ||--|| my_dictionary_entity
my_child_entity::PARENT ||--|| my_parent_entity
@enduml

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

Команда component:make

Одной из главных задач, которую мы хотели упростить, было создание компонентов. В типичном проекте на Bitrix это занимает немало времени: нужно создавать class.php, локализацию, шаблоны, методы для AJAX и, конечно же, следовать единой структуре. Всё это мы автоматизировали с помощью команды component:make.

Пример:

bitrix-mate component:make nlmk:test

После выполнения команды с помощью вопросов уточняются дополнительные настройки

При передаче флага -f или --full все вопросы, требующие подтверждения, будут отмечены как "y"
При передаче флага -f или --full все вопросы, требующие подтверждения, будут отмечены как "y"
Получившаяся структура
Получившаяся структура
Сгенерированный класс компонента
<?php
 
namespace Nlmk\Components;

use Bitrix\Main\Engine\ActionFilter\HttpMethod;
use Bitrix\Main\Engine\ActionFilter\Csrf;
use Bitrix\Main\Engine\Contract\Controllerable;
use Bitrix\Main\Engine\Response\AjaxJson;
use Bitrix\Main\Error;
use Bitrix\Main\Errorable;
use Bitrix\Main\ErrorCollection;
use Bitrix\Main\Localization\Loc;
use CBitrixComponent;
use Throwable;
 
class Test extends CBitrixComponent implements Controllerable, Errorable
{
    protected ErrorCollection $errorCollection;
 
    public function __construct($component = null)
    {
        $this->errorCollection = new ErrorCollection();
        parent::__construct($component);
    }
     
    public function executeComponent(): void
    {
        try {
            //@TODO: your code
        } catch (Throwable $throwable) {
            //@TODO: log $throwable->getMessage()
            //@TODO $this->setError(Loc::getMessage('YOUR_SYSTEM_LANG_CODE');
        }
        if (!$this->errorCollection->isEmpty()) {
            $this->arResult['ERRORS'] = $this->getErrors();
        }
        $this->includeComponentTemplate();
    }
     
     public function configureActions(): array
    {
        return [
            /** @see self::getAction() */
            'get' => [
                'prefilters' => [
                    new HttpMethod([HttpMethod::METHOD_POST]),
                    new Csrf(),
                ],
            ],
        ];
    }
     
    public function getAction(): AjaxJson
    {
        $returnData = [];
        try {
            //@TODO: your code
        } catch (Throwable $throwable) {
            //@TODO: log $throwable->getMessage()
            //@TODO $this->setError(Loc::getMessage('YOUR_SYSTEM_LANG_CODE');
        }
 
        return $this->responseAjax($returnData);
    }
     
     /**
     * @param string $message
     *
     * @return void
     */
    protected function setError(string $message): void
    {
        $this->errorCollection->setError(new Error($message));
    }
     
    /**
     * @inheritDoc
     */
    public function getErrors(): array
    {
        return $this->errorCollection->toArray();
    }
 
    /**
     * @inheritDoc
     */
    public function getErrorByCode($code): Error
    {
        return $this->errorCollection->getErrorByCode($code);
    }
     
    protected function listKeysSignedParameters(): array
    {
        return [
            //@TODO your params
        ];
    }
     
    /**
     * @param array|null $data
     *
     * @return AjaxJson
     */
    protected function responseAjax(?array $data = []): AjaxJson
    {
        return new AjaxJson(
            $data,
            $this->errorCollection->isEmpty() ? AjaxJson::STATUS_SUCCESS : AjaxJson::STATUS_ERROR,
            $this->errorCollection
        );
    }
}

В среднем выполнение ручных операций по созданию компонента может занимать от 15 до 50 минут в зависимости от сложности и опыта разработчика. Используя эту команду, мы снизили это время до 1-2 минут, освобождая 10-48 минут на каждый компонент.


Что ещё умеет bitrix.mate

Кроме генерации компонентов, наш инструмент предлагает такие полезные утилиты:

  • Создание модулей: автоматическое создание структуры с файлами install.php,  options.php, default_option.php и lang, готовой для работы.

  • Шаблоны компонентов: генерация файлов template.phpstyles.css и result_modifier.php с минимально необходимой структурой (как видно из примера выше, так же является частью команды по созданию класса компонента).

А ещё bitrix.mate был разработан с учетом возможности расширения функционала. Это позволяет добавлять свои команды в модуль, адаптируя инструмент под конкретные требования проекта. В процессе формирования итогового массива команд модуль обрабатывает подписчиков события OnCollectCommands. Они могут возвращать массив дополнительных команд, что делает bitrix.mate универсальным и гибким решением.


Результаты автоматизации

После внедрения этих команд мы заметили существенные улучшения:

  • Экономия времени: команда экономила как минимум 1-2 часа за рабочую неделю. В более сложных проектах эта экономия увеличивается.

  • Стандартизация: теперь эффективнее соблюдаются единые стандарты создания компонентов и модулей.

  • Меньше ошибок: уменьшилось количество ошибок, связанных с ручными процессами — всё создаётся предсказуемо и корректно.

Наш набор CLI-команд стал важным инструментом в работе. Объединив опыт команды и ежедневные трудности в одну библиотеку, мы сделали нашу работу проще и приятнее.

Есть идеи? Если у вас есть предложения, делитесь ими в комментариях! Надеемся, наш опыт вдохновит вас на создание своих инструментов или внедрение уже готовых автоматизаций. Мы все стремимся к одному — меньше рутины, больше интересной работы!

Источник: https://habr.com/ru/companies/nlmk/articles/868300/


Интересные статьи

Интересные статьи

Около 10 лет назад у меня возникла идея написать игру именно на Java, поскольку я использую этот язык в работе. Это был своего рода челлендж. Хотел попробовать себя, посмотреть, возможно ли это. И спо...
Задача - сделать AMP версию всего сайта на 1С-Битрикс, чтобы содержание AMP страниц всегда соответствовало оригинальным и изменялось при изменении оригинальных.
Всем привет! Меня зовут Илья!Если вы читали мою прошлую статью, то наверное уже знаете что я увлекаюсь любительским ракетостроением. Это сложная и долгая тема. Давайте сейчас не будем строить большую ...
Если вы последние лет десять следите за обновлениями «коробочной версии» Битрикса (не 24), то давно уже заметили, что обновляется только модуль магазина и его окружение. Все остальные модули как ...
Как обновить ядро 1С-Битрикс без единой секунды простоя и с гарантией работоспособности платформы? Если вы не можете закрыть сайт на техобслуживание, и не хотите экстренно разворачивать сайт из бэкапа...