Миграции шаблонов бизнес-процессов для Битрикс24. Вот что для этого нужно

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

Управление большим числом изменяемых шаблонов бизнес-процессов — непростая задача. А необходимость поддержки актуальных версий шаблонов на нескольких копиях проекта добавляет еще больше сложностей. Делюсь своим опытом решения проблемы миграций. 

Привет, Хабр! 

Меня зовут Закаулова Дарья, и я PHP-разработчик в IBS. 

Год назад я начала работать с корпоративными порталами, в которых реализовано большое количество разнообразных бизнес-процессов. Тогда я впервые столкнулась с задачей поддержания актуальной версии шаблонов бизнес-процессов во всех копиях проекта. Довольно быстро мы приняли решение автоматизировать этот процесс, что позволило команде существенно сократить время, необходимое на поддержку и перенос изменений в шаблонах бизнес-процессов (БП). 

В этой статье я хочу поделиться своим опытом расширения миграции этих шаблонов. 

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

Но об этом позже. 

Для начала разберемся, почему не подходит стандартное решение экспорта/импорта шаблонов бизнес-процессов? 

  1. Для импорта шаблона специалисту необходимо вручную открывать шаблон бизнес-процесса, загружать экспортированный ранее файл с его описанием  и осуществлять перенос. Т.е. отсутствует простой механизм миграций, который мог бы позволить установить все изменения кликом по одной кнопке

  2. Используемая в шаблонах информация привязана к идентификаторам разных сущностей. То есть если БП добавляет запись в инфоблок с ID = 5, то после штатного импорта этого шаблона на другой сервер БП просто привяжется к инфоблоку с ID = 5, без проверки символьного кода.

Первую проблему может решить использование модуля sprint.migration. Он помогает переносить изменения между несколькими копиями проекта. Одним из его плюсов является предусмотренная возможность расширения функциональности через добавление своих типов миграций, которую мы и будем использовать. 

Разработку можно разделить на три основных блока:

  1. Создание модуля и добавление нового обработчика к миграциям модуля sprint.migration.

  2. Создание вспомогательных классов для работы с шаблонами и другими сущностями.

  3. Реализация логики миграций.

Рассмотрим каждый этап подробнее. 

1. Создание модуля и добавление нового обработчика к миграциям модуля sprint.migration

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

Общие правила работы с модулем sprint.migration можно прочитать в подробно написанном вики.

После создания модуля общая структура файлов может быть такой:

Для добавления обработчика к миграциям модуля sprint.migration нужны файлы: 

  1. WorkflowTemplateEntities.php — шаблон файла, который будет создаваться при импорте и запускаться при установке миграции. В нашем случае он выглядит следующим образом:

<?= "<?php\n" ?>

namespace Sprint\Migration;

<?= $extendUse ?>
use Ibs\Migration\WorkflowTemplate\WorkflowTemplate;

class <?= $version ?> extends <?= $extendClass ?>

{
    protected $description = "<?= $description ?>";

    protected $moduleVersion = "<?= $moduleVersion ?>";

    /**
    * @throws Exceptions\HelperException
    * @return bool|void
    */
    public function up()
    {
        $workflowTemplate = new WorkflowTemplate();
        <?php foreach ($entities as $entity): ?>
            $workflowTemplate->importWorkflowTemplate(<?= var_export($entity, true) ?>);
        <?php endforeach; ?>
    }
}
  1. Файл workflowtemplatebuilder.php содержит описание класса WorkflowTemplateBuilder. Он унаследован от Sprint\Migration\VersionBuilder и является описанием обработчиков миграций. В нем необходимо переопределить два основных метода (вспомогательные описаны в комментариях): 

    a. метод initialize() отвечает за добавление кастомного типа миграций с меню выбора:

protected function initialize(): void
    {
        // Добавляет переводы фраз, которые будут использоваться в модуле
        $this->addLocalization();

        // Шаблоны бизнес-процессов
        $this->setTitle(Loc::getMessage('WORKFLOW_TEMPLATE')); 
        $this->setGroup('CRM');

        // Определение полей, описывающих миграцию. Метод можно не переопределять и использовать базовый, который включает в себя префикс (для именования файла и класса) и описание.  
        $this->addVersionFields();
    }

b. метод excute(), который отвечает за вывод списка шаблонов и формирование файла с миграциями. Описание методов для экспорта и получения списка шаблонов БП будет ниже

protected function execute()
    {
        // $workflowTemplate содержит список выбранных шаблонов БП
        $workflowTemplates = $this->addFieldAndReturn(
            'workflowTemplate',
            [
                'title' => Locale::getMessage('BUILDER_WorkflowTemplateEntities_EntityId'),
                'placeholder' => '',
                'width' => 250,
                'multiple' => 1,
                'items' => $this->getWorkflowTemplates(), // список шаблонов БП
                'value' => [],
            ]
        );

        $entities = [];

        $workflowTemplateEntity = new WorkflowTemplate();
        // Запись в $entities экспортированных шаблонов БП
        foreach ($workflowTemplates as $workflowTemplate) {
            $entities[] = $workflowTemplateEntity->exportWorkflowTemplateEntity($workflowTemplate);
        }

        // Создание файла миграции с описаниями шаблонов БП
        $this->createVersionFile(
            Config::getPathToLib() . '/templates/WorkflowTemplateEntities.php',
            [
                'entities' => $entities,
            ]
        );
    }

Выбор шаблонов БП может выглядеть таким образом: 

  1. В local/php_interface/ необходимо добавить файл migrations.cfg.php, подключающий к модулю sprint.migration обработчики новых типов миграций:

<?php

use Bitrix\Main\Loader;
use Ibs\Migration\WorkflowTemplate\WorkflowTemplateBuilder;
use Sprint\Migration\VersionConfig;

/*
 * Для миграций к дефолтным обработчикам добавляем свои.
 */

Loader::includeModule('ibs.migration');

return [
    'version_builders' => array_merge(
        VersionConfig::getDefaultBuilders(),
        [
            'WorkflowTemplateBuilder' => WorkflowTemplateBuilder::class
        ]
    )
];

2. Создание вспомогательных классов для работы с шаблонами и другими сущностями

Для решения проблемы с привязкой логики шаблона к сущностям по идентификатору необходимо реализовать функциональность подмены идентификаторов на символьные коды и наоборот. 

Множество классов, описывающих вспомогательные сущности, может отличаться в разных проектах. Для нашего решения было достаточно описать замены идентификаторов кодами для:

  1. Групп пользователей (group_g).

  2. Подразделений компании (group_d).

  3. Инфоблоков (iblock_).

  4. Пользователей (user_).

Вспомогательные классы (ibs.migration/lib/helpers/..) содержат методы для получения списка экземпляров соответствующих сущностей. В качестве ключа может быть использован либо ID, либо символьный код. Также к ключу добавляется префикс, который нужен для поиска в шаблонах тех записей, которые необходимо заменить. Штатные префиксы для описанных сущностей представлены в списке выше. 

Пример такого метода:

/**
     * Функция для получения списка пользователей с заданными в параметрах префиксами.
     */

public static function getUsersForImport(
        string $prefixUserId,
        string $prefixUserLogin,
        bool $groupById = true
    ): array {
        $query = UserTable::query();
        $query->addSelect('ID');
        $query->addSelect('LOGIN');
        $query->addFilter('ACTIVE', 'Y');
        $usersResult = $query->exec();
        $users = [];
        while ($user = $usersResult->fetch()) {
            if ($groupById) {
                $users[$prefixUserId . $user['ID']] = $prefixUserLogin . $user['LOGIN'];
            } else {
                $users[$prefixUserLogin . $user['LOGIN']] = $prefixUserId . $user['ID'];
            }
        }
        return $users;
    }

Подробнее стоит остановиться на вспомогательных методах класса WorkflowTemplate. 

a. Список шаблонов БП, который выводится для выбора экспортируемых шаблонов, может быть представлен в таком формате: 

Такой массив описывает множество сущностей, к которым относятся шаблоны БП. В качестве ключа в массиве выступает ИД этой сущности, а в значениях лежит ее название и список шаблонов БП. 

Для формирования списка шаблонов достаточно данных, полученных таким образом:

public function getList(array $select = ['ID', 'MODULE_ID', 'ENTITY', 'DOCUMENT_TYPE', 'NAME', 'DESCRIPTION'], array $filter = []): array
    {
        $query = WorkflowTemplateTable::query()
            ->setSelect($select)
            ->setFilter($filter);
        $templatesResult = $query->exec();

        return $templatesResult->FetchAll();
    }

3. Реализация логики миграций

А теперь приступим к самому интересному: рассмотрим, как же можно реализовать  экспорт и импорт миграций. Вся эта логика находится в классе WorkflowTemplate.

Если в шаблоне БП используется привязка к каким-то сущностям, то префиксы после экспорта будут совпадать с системными (их мы описывали ранее). После замены данных с ID на символьные коды необходимо использовать другие префиксы, например:

 private string $prefixUserLogin = 'IMPORT_USER_LOGIN_';
    private string $prefixIblockCode = 'IMPORT_IBLOCK_CODE_';
    private string $prefixGroupCode = 'IMPORT_GROUP_CODE_';
    private string $prefixDepartmentCode = 'IMPORT_DEPARTMENT_CODE_';

В листинге ниже представлен метод для экспорта шаблона БП по его ID. Его описание можно увидеть в комментариях. Отмечу, что при получении массива заменяемых данных в качестве строк для поиска будут выступать идентификаторы сущностей с системными префксами, а в качестве значения – символьные коды с соответствующими префиксами, например, ‘iblock_1’ будет заменяться на 'IMPORT_IBLOCK_CODE_catalog’.  

/**
     * Функция возвращает описание шаблона бп для экспорта по идентификатору шаблона.
     *
     * @throws Exception
     */
    public function exportWorkflowTemplateEntity(int $templateId): array
    {
        // метод получает массив всех замен, которые были определены во вспомогательных методах
        $this->replace = $this->getParamsForReplace(true);
        // получение шаблона БП
        $exportTemplate = CBPWorkflowTemplateLoader::exportTemplate($templateId);
        // приведение шаблона БП к массиву
        $data = unserialize(gzuncompress($exportTemplate));

        // рекурсивный обход массива с заменой переменных с идентификаторами переменными с символьными кодами
        return $this->recursivelyVariableReplace(
            [
                'templateParams' => $this->getInfoAboutTemplateById($templateId),
                'templateData' => $data,
            ]
        );
    }

Листинг с определением метода импорта представлен в ниже. Теперь при получении массива заменяемых данных в качестве строк для поиска будут выступать символьные коды с префиксами. То есть 'IMPORT_IBLOCK_CODE_catalog’ будет заменяться на ‘iblock_1’

/**
     * Метод импортирует шаблон БП по описанию его шаблона.
     */
    public function importWorkflowTemplate(array $template): void
    {
     // определение параметров для замены
        $this->replace = $this->getParamsForReplace(false);
        // рекурсивная замена символьных кодов идентификаторами
        $template = $this->recursivelyVariableReplace($template, false);
        // перевод шаблона БП из массива в формат импорта
        $templateData = gzcompress(serialize($template['templateData']));
        // получение шаблона по его параметрам
        $templateInfo = $this->getInfoAboutTemplateByParams($template['templateParams']);

        // Если такой шаблон уже существует, то перезаписать его
        if ($templateInfo) {
            CBPWorkflowTemplateLoader::importTemplate(
                $templateInfo['ID'],
                $templateInfo['DOCUMENT_TYPE'],
                $templateInfo['AUTO_EXECUTE'],
                $templateInfo['NAME'],
                $template['templateParams']['DESCRIPTION'],
                $templateData,
            );
        } else { // иначе создать новый шаблон
            CBPWorkflowTemplateLoader::importTemplate(
                0, // для создания новой сделки необходимо в параметр id отправить 0
                $template['templateParams']['DOCUMENT_TYPE'],
                $template['templateParams']['AUTO_EXECUTE'],
                $template['templateParams']['NAME'],
                $template['templateParams']['DESCRIPTION'],
                $templateData,
            );
        }
    }

Вызываемый при импорте и экспорте метод для рекурсивной замены данных реализован с помощью обхода в глубину (DFS).

/**
     * Функция для обхода ДФСом массива с шаблоном БП и замены жестко заданных переменных.
     *
     * @throws Exception
     */
    public function recursivelyVariableReplace(array $data, bool $isExport = true): array
    {
        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $data[$key] = $this->recursivelyVariableReplace($value, $isExport);
            } elseif (is_string($value)) {
                $data[$key] = $this->replaceAll($value);
            }
        }

        // Для активити с запуском шаблона БП сделаем замену
        if ($data['Type'] === 'StartWorkflowActivity') {
            if ($isExport) {
                $data['Properties']['IMPORT_WORKFLOW_TEMPLATE'] =
                    $this->getInfoAboutTemplateById($data['Properties']['TemplateId']);
            } else {
                $templateParams = $this->getInfoAboutTemplateByParams($data['Properties']['IMPORT_WORKFLOW_TEMPLATE']);
                if (!$templateParams) {
                    throw new Exception(
                        Loc::getMessage('WORKFLOW_TEMPLATE_ERROR_VARIABLE_NOT_FOUND')
                        . json_encode($data['Properties']['IMPORT_WORKFLOW_TEMPLATE'])
                    );
                }
                $data['Properties']['TemplateId'] = $templateParams['ID'];
                $data['Properties']['IMPORT_WORKFLOW_TEMPLATE'] = null;
                unset($data['Properties']['IMPORT_WORKFLOW_TEMPLATE']);
            }
        }

        return $data;
    }

Основные моменты описали, остальные детали реализации в ваших умелых руках!

Спасибо, что уделили время! Надеюсь, моя статья была вам полезна.

Если у вас возникнут вопросы по реализации, спрашивайте в комментариях. 

Если вы встретите в своих задачах другие типы строго заданных данных, поделитесь, пожалуйста. А если у вас уже был опыт миграций шаблонов БП, расскажите, очень интересно изучить альтернативные варианты :)

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


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

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

В отличие от нашего прошлого героя, Михаил сделал выбор не в пользу Scala, а предпочел Rust, так как этот язык обеспечивает безопасное использование данных и ресурсов. На нём можно управлять памятью и...
Недавно мы публиковали на Хабре целую серию статей о карьере в IT. Теперь собрали ключевые советы и полезные ссылки из этих материалов. Статью можно использовать в качестве краткого помощника для тех,...
На просторах интернета часто встречается информация о платформе Postman. Большинство статей включают информацию о переменных, различных скриптах и автоматизации при тестировании. Но на самом деле Post...
Привет! Я Алексей Рыбаков, руководитель направления в Sber AR/VR Lab. И, прежде чем рассказать о том, что мы делали, нужно коротко пояснить смысл заголовка. А точнее – смысл слова «метавселенная»...
Екатерина Вебер когда-то начинала в «Яндексе» и Росатоме, а сейчас работает Program manager в Google Youtube, в подразделении, занимающемся поиском абьюзивного контента. Она рассказал...