Создание своего действия
Table of Contents
Ранее мы говорили что бизнес процесс состоит из шагов или действий, которые отображаются в редакторе виде блоков, и сейчас мы поговорим о том как создать свое простое действие для бизнес-процесса.
Для нетерпеливых, есть оформленный пример кода на github
Мы будем рассматривать создание своего действия на примере ‘Пример мир!’ (
helloworld): это будет простое действие бизнес-процесса, задачей которого будет вывести сообщение шаблонного вида “Привет, <обращение>! <текст сообщения>”. Параметрами будет строка (обязательная, по-умолчанию “мир”) и многострочный текст (обязательное поле). Мы так же усовершенствуем наше действие чтобы оно возвращало сгенерированное сообщение в качестве дополнительного результата.
Расположение
Активити, как и условия, могут располагаться в следующих местах (путь указан от document root):
/local/activities/local/activities/customBX_ROOT/activities/customBX_ROOT/activities/bitrixBX_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для действий всегда содержитactivityCLASS- строка с названием 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) - ассоциативный набор доступных группу