
Представьте: новый проект, сжатые сроки, десятки задач. Нужно создать компоненты, модули, классы, подготовить документацию — и всё это с нуля. Всё кажется стандартным, но на практике такие процессы забирают массу времени и сил.
Мы оказывались в этой ситуации не раз. Вместо того чтобы смириться с рутиной, решили действовать. Так появился наш набор CLI-команд для автоматизации разработки на 1C-Битрикс. Это не просто утилиты, а инструмент, который ускорил выполнение типичных задач, сделал процессы предсказуемыми и уменьшил вероятность ошибок.
Меня зовут Артур Низамов, я ведущий разработчик в НЛМК ИТ. В этой статье я расскажу, что нас мотивировало на изменения, какие команды мы добавили, как они работают и какой эффект это принесло.
Если вы архитектор или разработчик, и у вас были похожие трудности, эта статья может быть для вас полезной.
Предпосылки: почему мы создали инструмент?
Большинство проектов на 1C-Битрикс сопровождаются рутинными задачами, которые мало чем отличаются от проекта к проекту. Среди них:
-
Создание компонентов: настройка
class.php
, реализация методов (включая AJAX) и добавлениеlang
-файлов. -
Создание шаблонов для компонентов: ручное создание папки
templates
, файловtemplate.php
,result_modifier.php
и их локализаций. -
Создание модулей: повторяющаяся работа по созданию файлов
install.php
,include.php
,options.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
После выполнения команды с помощью вопросов уточняются дополнительные настройки


Сгенерированный класс компонента
<?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.php
,styles.css
иresult_modifier.php
с минимально необходимой структурой (как видно из примера выше, так же является частью команды по созданию класса компонента).
А ещё bitrix.mate был разработан с учетом возможности расширения функционала. Это позволяет добавлять свои команды в модуль, адаптируя инструмент под конкретные требования проекта. В процессе формирования итогового массива команд модуль обрабатывает подписчиков события OnCollectCommands. Они могут возвращать массив дополнительных команд, что делает bitrix.mate универсальным и гибким решением.
Результаты автоматизации
После внедрения этих команд мы заметили существенные улучшения:
-
Экономия времени: команда экономила как минимум 1-2 часа за рабочую неделю. В более сложных проектах эта экономия увеличивается.
-
Стандартизация: теперь эффективнее соблюдаются единые стандарты создания компонентов и модулей.
-
Меньше ошибок: уменьшилось количество ошибок, связанных с ручными процессами — всё создаётся предсказуемо и корректно.
Наш набор CLI-команд стал важным инструментом в работе. Объединив опыт команды и ежедневные трудности в одну библиотеку, мы сделали нашу работу проще и приятнее.
Есть идеи? Если у вас есть предложения, делитесь ими в комментариях! Надеемся, наш опыт вдохновит вас на создание своих инструментов или внедрение уже готовых автоматизаций. Мы все стремимся к одному — меньше рутины, больше интересной работы!
ссылка на оригинал статьи https://habr.com/ru/articles/868300/
Добавить комментарий