Как мы приручили рутину в 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/articles/868300/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *