Как стать автором
Обновить
130.04
НЛМК ИТ
Группа НЛМК

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

Уровень сложностиПростой
Время на прочтение10 мин
Количество просмотров2K

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

Мы оказывались в этой ситуации не раз. Вместо того чтобы смириться с рутиной, решили действовать. Так появился наш набор 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-команд стал важным инструментом в работе. Объединив опыт команды и ежедневные трудности в одну библиотеку, мы сделали нашу работу проще и приятнее.

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

Теги:
Хабы:
Всего голосов 19: ↑19 и ↓0+20
Комментарии3

Публикации

Информация

Сайт
nlmk.com
Дата регистрации
Дата основания
2013
Численность
свыше 10 000 человек
Местоположение
Россия

Истории