Создание своего действия

Ранее мы говорили что бизнес процесс состоит из шагов или действий, которые отображаются в редакторе виде блоков, и сейчас мы поговорим о том как создать свое простое действие для бизнес-процесса.

Для нетерпеливых, есть оформленный пример кода на github

Мы будем рассматривать создание своего действия на примере ‘Пример мир!’ (helloworld): это будет простое действие бизнес-процесса, задачей которого будет вывести сообщение шаблонного вида “Привет, <обращение>! <текст сообщения>”. Параметрами будет строка (обязательная, по-умолчанию “мир”) и многострочный текст (обязательное поле). Мы так же усовершенствуем наше действие чтобы оно возвращало сгенерированное сообщение в качестве дополнительного результата.

Расположение

Активити, как и условия, могут располагаться в следующих местах (путь указан от document root):

  • /local/activities
  • /local/activities/custom
  • BX_ROOT/activities/custom
  • BX_ROOT/activities/bitrix
  • BX_ROOT/modules/bizproc/activities

Порядок поиска является приоритетным, т.е. директории будут перебираться последовательно пока не будет найдена директория с действием. BX_ROOT - это константа содержащая путь к директории битрикса. По-умолчанию равна /bitrix.

Например, в стандартном окружении BitrixEnv класс нашего действия Битрикс будет искать по следующим путям:

  • /home/bitrix/www/local/activities/helloworldactivity/helloworldactivity.php
  • /home/bitrix/www/local/activities/custom/helloworldactivity/helloworldactivity.php
  • /home/bitrix/www/bitrix/activities/custom/helloworldactivity/helloworldactivity.php
  • /home/bitrix/www/bitrix/activities/bitrix/helloworldactivity/helloworldactivity.php
  • /home/bitrix/www/bitrix/modules/bizproc/activities/helloworldactivity/helloworldactivity.php

Файловая структура

По своему строению и расположению действия похожи на условия, за исключением того, что используют ключевое слово acitivty вместо condition. Для нашего случая будет использоваться название активити helloworldactivity (от англ. hello world activity) и располагать мы будем ее в /local/activities/custom.

Структура нашего действия:

/local/activites/custom/
| -> helloworldactivity
| -> | -> .description.php
| -> | -> properties_dialog.php
| -> | -> helloworldactivity.php
| -> | -> lang
| -> | -> | -> ru
| -> | -> | -> | -> .description.php
| -> | -> | -> | -> properties_dialog.php
| -> | -> | -> | -> helloworldactivity.php

Рассмотрим подробнее содержимое директории: /local/activites/custom/helloworldactivity.

Файл .description.php будет содержать мета-информацию описывающую наше действие (аналог .description.php в компонентах) Файл properties_dialog.php будет содержать код для визуального отображения (аналог templates/.default/template.php в компонентах) Файл helloworldactivity.php будет содержать основную логику нашего активити (аналог class.php в компонентах) Директория lang с языковыми фразами.

Файл .description.php

Основная задача файла - установить переменную $arActivityDescription как массив описывающий действие.

Содержимое файла:

<? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();

use \Bitrix\Main\Localization\Loc;

$arActivityDescription = [
    "NAME" => Loc::getMessage("HELLOWORLD_DESCR_NAME"),
    "DESCRIPTION" => Loc::getMessage("HELLOWORLD_DESCR_DESCR"),
    "TYPE" => "activity",
    "CLASS" => "HelloWorldActivity",
    "JSCLASS" => "BizProcActivity",
    "CATEGORY" => [
        "ID" => "other",
    ],
    "RETURN" => [
        "Text" => [
            "NAME" => Loc::getMessage("HELLOWORLD_DESCR_FIELD_TEXT"),
            "TYPE" => "string",
        ],
    ],
];

Структура файла подробно разобрана в главе “Действия”, поэтому мы всего-лишь пробежимся по нашим параметрам:

  • NAME и DESCRIPTION это отображаемые значения, чтобы тому что настраивал действие была понятна его суть
  • TYPE для действий всегда содержит activity
  • CLASS - строка с названием php класса-обработчика (без приставки CBP) который выполняет работу
  • JSCLASS - строка с названием js класса-обработчика (по-умолчанию BizProcActivity), который отвечает за отрисовку в редакторе
  • CATEGORY - структура описывающая раздел в котором находится наше действие. Поскольку
  • RETURN - структура описывающая возвращаемые значения. В нашем случае возвращается строка в ключе Text

Сразу же создадим lang-файл lang/ru/.description.php:

<? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
$MESS['HELLOWORLD_DESCR_NAME']  = 'Привет мир!';
$MESS['HELLOWORLD_DESCR_DESCR'] = 'Генерирует привественное сообщение';
$MESS['HELLOWORLD_DESCR_FIELD_TEXT'] = 'Текст сообщения';

Файл helloworldactivity.php

Поскольку мы создаем простое действие, то есть наше не будет ждать наступления какого-либо события, то родительским классом в данном случае будет являться \Bitrix\Bizproc\Activity\BaseActivity, а наш класс должен иметь префикс CBP.

Первоначальное содержимое файла:

<?php if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

use Bitrix\Bizproc\Activity\BaseActivity;
use Bitrix\Bizproc\FieldType;

class CBPHelloWorldActivity extends BaseActivity
{
	/**
	 * @see parent::_construct()
	 * @param void
	 */
	public function __construct($name)
	{
		parent::__construct($name);

		$this->arProperties = [
			'Title' => '',

			// return
			'Text' => null,
		];

		$this->SetPropertiesTypes([
			'Text' => ['Type' => FieldType::STRING],
		]);
	}

	/**
	 * Return activity file path
	 * @return string
	 */
	protected static function getFileName(): string
	{
		return __FILE__;
	}
}

После этого можно увидеть в редакторе бизнес процесса в блоке “Прочее” наше действие “Привет мир!”. Его можно разместить в шаблоне процесса, сохранить и даже запустить бизнес-процесс. Ничего конечно же не произойдет, так как мы не придали нашему коду никакой бизнес логики - он запуститься, проверит условия и завершится. За выполнение действия отвечает метод internalExecute - он не имеет аргументов на входе, но в результате своей работы обязан вернуть объект с коллекцией ошибок (Bitrix\Main\ErrorCollection, но не пугайтесь это просто объект - коллекция может быть пуста).

Абстрактный класс Bitrix\Bizproc\Activity\BaseActivity имеет несколько методов синтаксического сахара для записи в журнал бизнес процесса. Воспользуемся методами-абстракциями над $this->WriteToTrackingService чтобы записать в журнал сообщения.

Добавим к нашему классу метод:

/**
 * @return ErrorCollection
 */
protected function internalExecute(): ErrorCollection
{
	$errors = parent::internalExecute();

	$this->logError( 'Some error text here (if needed)');
	$this->log("Привет мир!");

	return $errors;
}

Таким образом при выполнении активити в журнале бизнес процесса появятся записи.

Теперь научимся возвращать данные. В конструкторе класса мы изобразили причудливой формы массив визуально разделив элементы комментарием return. Таким образом мы явно подметили для себя какими данными мы оперируем в коде, а какие хотим вернуть. Наследуясь от BaseActivity у нас есть 2 механизма возврата значений:

  1. От CBPActivity: мы должны просто изменить свойство нашего класса (например $this->Text = "value")
  2. От BaseActivity: мы должны сохранить возвращаемое значение в preparedProperties $this->preparedProperties['Text'] = "value".

Можно использовать оба варианта, но только при соблюдении определенных условий: в случае если вы хотите возвращать данные как CBPActivity, то в конструкторе в arProperties значения возвращаемых переменных должны быть null (в противном случае то что вы напишите в конструкторе, то вы и получите в результате активити).

Мы воспользуемся вариантом предлагаемым BaseActivity, тогда наш код будет выглядеть так:

/**
 * @return ErrorCollection
 */
protected function internalExecute(): ErrorCollection
{
	$errors = parent::internalExecute();

	$this->preparedProperties['Text'] = "Привет, мир!";

	$this->log($this->preparedProperties['Text']);

	return $errors;
}

Теперь мы можем выполнить активити и даже использовать возвращаемое значение в других действиях, например в блоке “Запись в отчет”.

По условию задачи мы должны генерировать текст вида “Привет, <обращение>! <текст сообщения>”, и указанные маркеры должны быть настраиваемыми параметрами. Чтобы описать параметры действия, необходимо переопределить метод getPropertiesDialogMap полученный от нашего родительского BaseActivity. Напомню, что для конфигурации нам необходимо 2 поля ввода: строка и многострочный текст. Что мы сделаем?

  1. Определим в конструкторе 2 новых ключа в arParams
  2. Переопределим метод getPropertiesDialogMap класса BaseActivity
  3. Вынесем языко-зависимые переменные в языковой файл.

Таким образом наш код на данный момент выглядит следующим образом:

Файл helloworldactivity.php:

<?php if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

use Bitrix\Bizproc\Activity\BaseActivity;
use Bitrix\Bizproc\FieldType;
use Bitrix\Main\ErrorCollection;
use Bitrix\Main\Localization\Loc;
use Bitrix\Bizproc\Activity\PropertiesDialog;

class CBPHelloWorldActivity extends BaseActivity
{
    /**
     * @see parent::_construct()
     * @param $name string Activity name
     */
    public function __construct($name)
    {
        parent::__construct($name);

        $this->arProperties = [
            'Title' => '',
            'Subject' => '',
            'Comment' => '',

            // return
            'Text' => null,
        ];

        $this->SetPropertiesTypes([
            'Text' => ['Type' => FieldType::STRING],
        ]);
    }

    /**
     * Return activity file path
     * @return string
     */
    protected static function getFileName(): string
    {
        return __FILE__;
    }

    /**
     * @return ErrorCollection
     */
    protected function internalExecute(): ErrorCollection
    {
        $errors = parent::internalExecute();

        $this->preparedProperties['Text'] = Loc::getMessage(
            'HELLOWORLD_ACTIVITY_TEXT',
            [
                '#SUBJECT#' => $this->Subject,
                '#COMMENT#' => $this->Comment
            ]
        );
        $this->log($this->preparedProperties['Text']);

        return $errors;
    }

    /**
     * @param PropertiesDialog|null $dialog
     * @return array[]
     */
    public static function getPropertiesDialogMap(?PropertiesDialog $dialog = null): array
    {
        $map = [
            'Subject' => [
                'Name' => Loc::getMessage('HELLOWORLD_ACTIVITY_FIELD_SUBJECT'),
                'FieldName' => 'subject',
                'Type' => FieldType::STRING,
                'Required' => true,
                'Default' => Loc::getMessage('HELLOWORLD_ACTIVITY_DEFAULT_SUBJECT'),
                'Options' => [],
            ],
            'Comment' => [
                'Name' => Loc::getMessage('HELLOWORLD_ACTIVITY_FIELD_COMMENT'),
                'FieldName' => 'comment',
                'Type' => FieldType::TEXT,
                'Required' => true,
                'Options' => [],
            ],
        ];
        return $map;
    }
}

Файл lang/ru/hellowolrdactivity.php:

<? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
$MESS['HELLOWORLD_ACTIVITY_FIELD_SUBJECT']  = 'Объект';
$MESS['HELLOWORLD_ACTIVITY_FIELD_COMMENT'] = 'Комментарий';
$MESS['HELLOWORLD_ACTIVITY_DEFAULT_SUBJECT'] = "мир";
$MESS['HELLOWORLD_ACTIVITY_TEXT'] = 'Привет, #SUBJECT#! #COMMENT#';

На этом разработка нашего действия завершена, готовый код вы можете посмотреть на github.

Дополнительные возможности

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

Подключение модулей

Наше действие может зависеть от установленных модулей в системе. Для того чтобы потребовать установку модуля не обязательно вносить изменения в конструктор или internalExecute, для этого в базовом классе есть свойство $requiredModules содержащее список модулей которые нам потребуются.

Давайте подключим требование по наличию модуля к нашему активити:

// ...

class CBPHelloWorldActivity extends BaseActivity
{
    protected static $requiredModules = ["crm"];

    // ..other code here
}

Почему это работает?

Как нам известно, любое активити должно быть наследником абстрактного класса CBPActivity и реализовывать метод execute(), но абстрактный класс BaseActivity поставляемый Битриксом является его наследником и перекрывает этот метод.

Таким образом, за нас уже написали примерно следующий код:

public function execute()
{
    if (!static::checkModules())
    {
        return \CBPActivityExecutionStatus::Closed;
    }
    
    // ....
    
    $errorCollection = $this->internalExecute();

    // ....

    return \CBPActivityExecutionStatus::Closed;
}

Статический метод checkModules итерируется по массиву requiredModules и подключает каждый модуль через includeModule-метод. В случае если хотя бы один модуль не может быть подключен - возвращается false и все последующие методы не выполняются.

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

Возврат ошибок

Мы уже затрагивали тему про методы log и logError, однако прибегать к ним стоит только если это вспомогательная ошибка в ходе всего процесса. В случае ошибки основного процесса лучше возвращать ErrorCollection.

Например:

/**
 * @return ErrorCollection
 */
protected function internalExecute(): ErrorCollection
{
    $errors = parent::internalExecute();

    // ... code

    if ( $hasError )
    {
        $errors->setError(
            new \Bitrix\Main\Error("Some error")
        );
        return $errors;
    }

    // ... code

    return $errors;
}

Дополнительные проверки

Если вам нужно проверить значения каких-либо параметров во время выполнения действия, то для этого подойдет метод checkProperties. Например: например вам нужно что-то сделать, но только если сегодня НЕ выходные. Или отправить уведомление пользователю с ролью “Ответственный за процесс”, но только если при установке этот параметр был сконфигурирован.

Поведение метода checkProperties аналогично internalExecute, т.е. весь его наследованный код выглядит так:

protected function checkProperties(): ErrorCollection
{
    return new ErrorCollection();
}

В этом методе вы можете достучаться до свойства $preparedProperties, которое хранит значения всех параметров переданных в действие при настройке.

Своя отрисовка

Что делать если вам нужна более сложная отрисовка чем предлагает стандартный механизм? Например, вам нужны зависимые поля или какой-то ввод по маске? В таком случае, для отрисовки вы можете использовать более классический механизм предлагаемый файлом properties_dialog.php со всеми его преимуществами.

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

<?php if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

use Bitrix\Bizproc\FieldType;

/** @var \Bitrix\Bizproc\Activity\PropertiesDialog $dialog */
foreach ($dialog->getMap() as $field)
{
    $controlHtml = $dialog->renderFieldControl(
        $field,
        $dialog->getCurrentValue($field),
        true,
        FieldType::RENDER_MODE_DESIGNER
    );

    echo sprintf(
        '<tr><td align="right" width="40%%">%s:</td><td width="60%%">%s</td></tr>',
        htmlspecialcharsbx($field['Name']),
        $controlHtml
    );
}

Как видите, мы используем цикл по возвращаемым значениям из $dialog->getMap(), однако мы так же можем явно отрисовать наши параметры, используя следующий прием:

<?$subjectField = $dialog->getMap()['Subject'];?>
<tr>
    <td align="right" width="40%"><?=htmlspecialcharsbx($subjectField['Name'])?>:</td>
    <td width="60%">
        <?=
        $dialog->getFieldTypeObject($subjectField)->renderControl(
            [
                'Form' => $dialog->getFormName(),
                'Field' => $subjectField['FieldName']
            ],
            $dialog->getCurrentValue($subjectField['FieldName']),
            true,
            0
        )
        ?>
    </td>
</tr>

Поля диалога

В нашем действии мы использовали типы полей Строка и Текст (в getPropertiesDialogMap), однако мы ими не ограничены и возможности PropertiesDialog намного шире чем эти два типа. Давайте разберем структуру одного поля (Bitrix\Bizproc\FieldType):

Параметр Тип Описание
Type string Мнемонический код типа поля
Name string Отображаемое название поля
Description string Примечание к полю (нужно выводить вручную)
Required bool Флаг обязательного поля
Multiple bool Флаг множественного поля
Options string/array Строка либо массив с дополнительными параметрами (в зависимости от типа поля)
Settings array Массив с дополнительными параметрами (в зависимости от типа поля)
Default mixed Значение по-умолчанию (в зависимости от типа поля)

Немного о параметре Type: на момент написания статьи типы могут быть как базовыми, так и пользовательскими. Пользовательские типы определяются документом над которым запущен бизнес процесс и могут меняться в зависимости от документа. Базовые типы для всех документов одинаковы. Список базовых типов:

  • FieldType::BOOL (значение bool)
  • FieldType::DATE (значение date)
  • FieldType::DATETIME (значение datetime)
  • FieldType::DOUBLE (значение double)
  • FieldType::FILE (значение file)
  • FieldType::INT (значение int)
  • FieldType::SELECT (значение select)
  • FieldType::INTERNALSELECT (значение internalselect)
  • FieldType::STRING (значение string)
  • FieldType::TEXT (значение text)
  • FieldType::USER (значение user)
  • FieldType::TIME (значение time)

Поле типа FieldType::SELECT

Позволяет быть сконфигурированным с настройками (Settings):

  • ShowEmptyValue (bool) - показывать пустое значение
  • Groups (array) - структура позволяющая использовать группировку в отображаемых полях (на основании optiongroup тега) Структура Groups:
'Groups' => [
    [
        'name'  => 'Group one',
        'items' => [
            'key1' => 'Display value',
            'key2' => 'Another display value',
        ]
    ],
    [
        'name'  => 'Group two',
        'items' => [
            'key3' => 'Display value 3',
            'key4' => 'Display value 4',
        ]
    ],
]

В случае если в Settings нет ключа Groups, то отображаемые опции будут получены из Options ключа:

'Options' => [
    'key1' => 'Display value',
    'key2' => 'Another display value',
    'key3' => 'Display value 3',
    'key4' => 'Display value 4',
],

Поле типа FieldType::USER

Позволяет быть сконфигурированным с настройками (Settings):

  • ExternalExtract (bool) - позволяет получить сразу обработанный результат (т.е. вместо user_1 получите 1)
  • allowEmailUsers (bool) - позволить выбирать email-пользователей
  • groups (array) - ассоциативный набор доступных группу