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