Создание своего действия
Table of Contents
Ранее мы говорили что бизнес процесс состоит из шагов или действий, которые отображаются в редакторе виде блоков, и сейчас мы поговорим о том как создать свое простое действие для бизнес-процесса.
Для нетерпеливых, есть оформленный пример кода на 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 механизма возврата значений:
- От
CBPActivity
: мы должны просто изменить свойство нашего класса (например$this->Text = "value"
) - От
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 поля ввода: строка и многострочный текст.
Что мы сделаем?
- Определим в конструкторе 2 новых ключа в arParams
- Переопределим метод
getPropertiesDialogMap
классаBaseActivity
- Вынесем языко-зависимые переменные в языковой файл.
Таким образом наш код на данный момент выглядит следующим образом:
Файл 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) - ассоциативный набор доступных группу