Всем привет! Меня зовут Дмитрий, я разработчик в Битрикс24. В этой статье хочу рассказать о том, как можно создавать кастомные действия в коробке и сравнить способы их создания.
В официальной документации Битрикс24 разработчикам предлагается создавать кастомные действия вручную. Я в свое время, еще до работы в Битриксе, изучал кастомные действия больше по исходному коду. А когда обнаружил, что есть еще и другой способ, шаблонный, был приятно удивлен этому. Оказывается, многие собственные действия можно реализовывать гораздо проще. Для начинающих шаблонная реализация подойдет лучше, чем подходы, описанные в документации.
Для начала несколько слов о самих кастомных действиях.
Действия в Битрикс24 — это строительные блоки для автоматизации бизнес-процессов. В коробочную версию продукта входит большой набор стандартных действий, из которых в «Дизайнере бизнес-процессов» можно собрать свой алгоритм в виде блок-схемы. Стандартные действия покрывают основной объем типовых задач, таких как изменение полей элемента, создание задач, отправка уведомлений, работа с CRM и т.д.
Кастомные действия нужны тогда, когда в коробочной версии не хватает функционала для автоматизации, например:
вам нужно интегрироваться с очень узкими, специализированными внешними системами: подключить трекинговую систему, сверить данные с государственными реестрами или отправить данные в малоизвестную отраслевую ERP;
автоматизировать сложные или специфичные для вашего бизнеса операции, для которых нет готовых решений: рассчитать оптимальный маршрут доставки, собрать статистику из разных источников, сформировать отчет по уникальному шаблону;
оптимизировать рутинные задачи, которые требуют индивидуального подхода: проверить на дубли по сложным правилам, сохранить кастомные сущности.
Кастомные действия могут быть разной сложности. Обычные — выполняют одну конкретную операцию, не содержат внутренней логики ветвления. Они имеют линейную структуру и наследуются от базового класса CBPActivity
. Их используют чаще всего.
Составные действия могут содержать в себе дочерние действия, реализуют сложную логику с ветвлениями и циклами. Наследуются от CBPCompositeActivity
. Составные кастомные действия нужны для организации нестандартных потоков выполнения, поэтому используются редко. Например, если необходимо отслеживать статус выполнения дочернего действия.
Как создавать свои действия
Как я уже говорил, в документации подробно изложена ручная реализация, но начинать проще с шаблонной. Разберем разницу на примере — наше кастомное действие будет сохранять полученные данные из процесса в БД.
Важно: название папки с действием должно совпадать с именем файла, в котором будет находится класс действия.
Далее я рассмотрю шаги, которые нужно будет сделать, приведу примеры кода и покажу различия между ними.
Создаем описание действий - общий шаг для обоих подходов. Мы создаем файл
.description.php
, его содержание я приведу ниже.Создаем класс действия.
Этот шаг отличает методы друг от друга. В ручном варианте мы наследуем класс кастомного действия от CBPActivity
или CBPCompositeActivity
, если мы будем реализовывать составное действие.
На этом шаге мы определяем 5 методов:
конструктор класса;
метод
execute()
, который начинает выполнение действия;метод
getPropertiesDialog()
для вывода диалога настроек параметров действия;метод
getPropertiesDialogValues()
для сохранения настроек;метод
validateProperties()
для проверки параметров.
В шаблонном варианте мы также определяем конструктор, но класс будем наследовать от базового класса BaseActivity
. Он предоставляет готовые методы для работы со свойствами действия, их валидации, логирования и выполнения, но подходит только для обычного действия. Для выполнения используем метод internalExecute()
, а определять оставшиеся три метода нам не потребуется, они уже определены в BaseActivity
. Вместо них используем один метод getPropertiesDialogMap()
. Это заметно уменьшит объем кода, избавит от дублирования отдельных фрагментов и сократит риск ошибок.
На третьем шаге создаем формы настроек в файле
properties_dialog.php
— это общий шаг для обоих подходов.И в финале описываем основную логику нашего кастомного действия в методе
execute()
илиinternalExecute()
.
Дальше рассмотрим каждый подход с примерами реализации.
Ручная реализация
Для начала ознакомимся со стандартным способом создания своих действий в коробке. Такие действия следует размещать в каталоге /local/activities
или в своем кастомном модуле /mymodule/install/activities
. Наше кастомное действие будет сохранять полученные данные из процесса в БД. Давайте разберемся со структурой действия:
.description.php
— файл описания действия;savecustomeractivity.php
— файл с классом действия, в котором будем описывать логику действия;properties_dialog.php
— форма настроек действия;savecustomeractivity.js
— необязательный файл, скрипт для дополнительной логики визуального интерфейса полей;savecustomeractivity.css
— необязательный файл со стилями;icon.gif
— необязательный файл, иконка действия размером 24x24 для конструктора;lang/
— каталог файлов локализации дляLoc::getMessage()
.
Шаг 1. Создание описания действия
Создайте файл .description.php
в папке /local/activities/savecustomeractivity
:
<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
use Bitrix\Main\Localization\Loc;
$arActivityDescription = [
'NAME' => Loc::getMessage('MY_CUSTOM_ACTIVITY_NAME'), //Название действия
'DESCRIPTION' => Loc::getMessage('MY_CUSTOM_ACTIVITY_DESC'), //Описание действия
'TYPE' => 'activity', //Тип - действие
'CLASS' => 'SaveCustomerActivity', //Название класса действия без префикса "CBP".
'JSCLASS' => 'BizProcActivity', //Стандартная JS библиотека, которая будет рисовать Activity
'CATEGORY' => [
'ID' => 'document',
'OWN_ID' => 'my_cat', //Создание своей категории
'OWN_NAME' => Loc::getMessage('MY_CUSTOM_OWN_NAME') //Название своей категории
//'ID' => 'other', //Можно указать ID существующей категории, в данном случае категория "Прочее"
],
'RETURN' => [ //Возвращаемый результат
'CustomerId' => [
'NAME' => Loc::getMessage('MY_CUSTOM_RETURN_ITEM_ID'),
'TYPE' => 'int',
],
],
];
Шаг 2: Создание класса действия
Создайте файл savecustomeractivity.php
в папке /local/activities/savecustomeractivity
и определите необходимые методы.
В этом файле декларируем класс кастомного действия, в котором опишем его логику. Название класса строится из двух частей: префикса CBP и значения $arActivityDescription['CLASS']
. В нашем случае класс будет называться CBPSaveCustomerActivity
, так как действие сохраняет клиента в БД.
Поскольку мы разрабатываем «обычное действие», которое не будет содержать дочерних действий, наш класс должен быть наследован от CBPActivity
. Если же вам понадобится разработать действие, которое должно содержать дочерние действия, как, например, ветвления и циклы, наследуйте класс от CBPCompositeActivity
.
В конструкторе класса нужно определить параметры действия:
public function __construct($name)
{
parent::__construct($name);
$this->arProperties = [
'Title' => '', //Заголовок, нужно обязательно определить
'CustomerName' => '', //Настраиваемый параметр действия - имя клиента
'CustomerPhone' => '', //Настраиваемый параметр действия - телефон клиента
'CustomerEmail' => '', //Настраиваемый параметр действия - Email клиента
//Возвращаемые параметры
'CustomerId' => null,
];
}
Вывод диалога с настройками параметров действия:
public static function getPropertiesDialog(
$documentType,
$activityName,
$workflowTemplate,
$workflowParameters,
$workflowVariables,
$currentValues = null,
$formName = '',
$popupWindow = null,
$siteId = ''
): PropertiesDialog|bool
{
if (!Loader::includeModule('my_module'))
{
return false;
}
$dialog = new \Bitrix\Bizproc\Activity\PropertiesDialog(__FILE__, [
'documentType' => $documentType,
'activityName' => $activityName,
'workflowTemplate' => $workflowTemplate,
'workflowParameters' => $workflowParameters,
'workflowVariables' => $workflowVariables,
'currentValues' => $currentValues,
'formName' => $formName,
'siteId' => $siteId
]);
$dialog->setMapCallback([self::class, 'getPropertiesDialogMap']);
return $dialog;
}
public static function getPropertiesDialogMap(?PropertiesDialog $dialog = null): array
{
return [
'CustomerName' => [
'Name' => Loc::getMessage('MY_CUSTOM_CUSTOMER_NAME'),
'FieldName' => 'customer_name',
'Type' => \Bitrix\Bizproc\FieldType::STRING,
'Required' => true,
],
'CustomerPhone' => [
'Name' => Loc::getMessage('MY_CUSTOM_CUSTOMER_PHONE'),
'FieldName' => 'customer_phone',
'Type' => \Bitrix\Bizproc\FieldType::STRING,
'Required' => true,
],
'CustomerEmail' => [
'Name' => Loc::getMessage('MY_CUSTOM_CUSTOMER_EMAIL'),
'FieldName' => 'customer_email',
'Type' => \Bitrix\Bizproc\FieldType::STRING,
'Required' => false,
],
];
}
Сохранение настроек, сделанных в диалоге с настройками действия:
public static function getPropertiesDialogValues(
$documentType,
$activityName,
&$workflowTemplate,
&$workflowParameters,
&$workflowVariables,
$currentValues,
&$errors
): bool
{
$properties = [];
foreach (self::getPropertiesDialogMap() as $propertyKey => $fieldProperties)
{
$properties[$propertyKey] = $currentValues[$fieldProperties['FieldName']];
}
$errors = self::validateProperties(
$properties,
new CBPWorkflowTemplateUser(CBPWorkflowTemplateUser::CurrentUser)
);
if ($errors)
{
return false;
}
$currentActivity = &CBPWorkflowTemplateLoader::FindActivityByName(
$workflowTemplate,
$activityName
);
$currentActivity['Properties'] = $properties;
return true;
}
Проверка параметров действия:
public static function validateProperties(
$testProperties = [],
CBPWorkflowTemplateUser $user = null
): array
{
$errors = [];
foreach (self::getPropertiesDialogMap() as $propertyKey => $fieldProperties)
{
if (
CBPHelper::getBool($fieldProperties['Required'])
&& CBPHelper::isEmptyValue($testProperties[$propertyKey])
)
{
$errors[] = [
'code' => 'NotExist',
'parameter' => 'FieldValue',
'message' => Loc::getMessage('MY_CUSTOM_EMPTY_PROP', ['#PROPERTY#' => $fieldProperties['Name']])
];
}
}
return array_merge($errors, parent::validateProperties($testProperties, $user));
}
Шаг 3: Создание формы настроек
Создайте файл properties_dialog.php
в папке /local/activities/savecustomeractivity
:
<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
/** @var \Bitrix\Bizproc\Activity\PropertiesDialog $dialog */
foreach ($dialog->getMap() as $fieldId => $field): ?>
<tr>
<td align="right" width="40%"><?=htmlspecialcharsbx($field['Name'])?>:</td>
<td width="60%">
<?php $filedType = $dialog->getFieldTypeObject($field);
echo $filedType->renderControl(
[
'Form' => $dialog->getFormName(),
'Field' => $field['FieldName']
],
$dialog->getCurrentValue($field['FieldName']),
true,
\Bitrix\Bizproc\FieldType::RENDER_MODE_DESIGNER
);
?>
</td>
</tr>
<?php
endforeach;
После этого можно перейти в «Дизайнер бизнес-процессов» и найти это действие по названию раздела, который вы указали ранее, либо в разделе “Прочее”. Перетащить его в блок-схему и кликнуть по нему дважды, откроется окно с диалогом настроек действия. Введите любые значения, сохраните, проверьте, что код сохранения и проверки значений формы работает корректно.
Шаг 4: Описание логики действия
Теперь можно перейти к описанию основной логики нашего действия, оно будет сохранять полученные данные в БД, используя наш сервис из кастомного модуля 'my_module'
с клиентами:
public function execute(): int
{
if (!Loader::includeModule('my_module'))
{
return CBPActivityExecutionStatus::Closed;
}
$customerService = \MyModule\Service\CustomerService::getInstance();
$result = $customerService->save([
$this->CustomerName,
$this->CustomerPhone,
$this->CustomerEmail,
]);
if ($result->isSuccess())
{
$this->CustomerId = $result->getId();
}
else
{
$this->trackError($result->getErrorMessages());
}
return CBPActivityExecutionStatus::Closed;
}
Добавим файлы локализации, и наше обычное кастомное действие готово. Для его отладки можно использовать запись в отчет или уведомление в колокольчик.
Шаблонная реализация
Если у вас есть опыт написания кастомных действий, вы могли заметить, что класс CBPActivity
требует реализации стандартной логики, которая зачастую схожа в различных действиях. Поэтому для упрощения разработки кастомных действий доступен абстрактный базовый класс BaseActivity
, который предоставляет готовые методы для работы со свойствами действия, их валидации, логирования и выполнения, чтобы разработчик мог сфокусироваться на основной логике действия. Разработчику не нужно каждый раз реализовывать базовую логику, достаточно наследовать BaseActivity
и переопределить необходимые методы. Это вносит стандартизацию в создание действий, что упрощает поддержку и расширение функциональности.
Давайте, рассмотрим некоторые методы этого класса:
метод
prepareProperties()
автоматически преобразует значения свойств действия в нужные типы (например,int
,bool
,string
), это избавляет разработчика от необходимости вручную обрабатывать свойства;метод
checkProperties()
позволяет легко проверять параметры действия на корректность;методы
logError()
иlog()
упрощают запись сообщений в журнал (трекинг) бизнес-процесса;методы
getPropertiesDialog()
,renderPropertiesDialog()
иgetPropertiesDialogValues()
упрощают создание и обработку диалогов настроек действия;метод
checkModules()
проверяет, подключены ли необходимые модули для работы действия.
Класс предоставляет абстрактные методы (например, getFileName()
), которые должны быть реализованы в дочерних классах. Это делает класс гибким и позволяет адаптировать его под конкретные задачи. Большая часть стандартной логики уже реализована в BaseActivity
. Единый подход к созданию действий упрощает их поддержку и расширение. Класс легко адаптируется под различные задачи благодаря абстрактным методам. Автоматическая проверка свойств и модулей снижает вероятность ошибок.
Давайте на предыдущем примере реализуем на практике действие с помощью BaseActivity
.
Создание описания остается прежним. Перейдем сразу к созданию класса действия:
<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
use Bitrix\Main\Loader;
use Bitrix\Main\Localization\Loc;
use Bitrix\Bizproc\Activity\BaseActivity;
class CBPSaveCustomerActivity extends BaseActivity
{
protected static $requiredModules = ['my_module'];
}
Шаг 1: Наследование базового класса
Теперь наш класс должен быть наследован от BaseActivity
.
Так как мы используем API нашего модуля 'my_module'
, укажем protected static $requiredModules = ['my_module'];
и тогда BaseActivity
за нас автоматически проверит, подключен ли модуль и вернет ошибку, если не подключен.
Конструктор и метод getPropertiesDialogMap()
остается без изменений, в нем уже указаны наши поля.
Шаг 2: Определение метода getFileName
Нам обязательно нужно определить метод getFileName()
для корректной работы диалога с настройками действия.
Методы getPropertiesDialog()
, getPropertiesDialogValues()
, validateProperties()
теперь нам не нужны, так как они определены в BaseActivity
и нам достаточно их базовой логики.
Шаг 3: Определение метода internalExecute
Раньше нужно было вручную проверять модули и вызывать writeToTrackingService()
для логирования ошибок, теперь это делается автоматически в execute()
, а основная логика вынесена в internalExecute()
. Обратите внимание, что метод должен вернуть $errorCollection
.
В итоге, получаем гораздо меньше кода, весь код помещается на 80 строчках:
<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
use Bitrix\Main\Error;
use Bitrix\Main\Localization\Loc;
use Bitrix\Bizproc\Activity\PropertiesDialog;
use Bitrix\Bizproc\Activity\BaseActivity;
class CBPSaveCustomerActivity extends BaseActivity
{
protected static $requiredModules = ['my_module'];
public function __construct($name)
{
parent::__construct($name);
$this->arProperties = [
'Title' => '', //Заголовок
'CustomerName' => '', //Передаем имя клиента
'CustomerPhone' => '', //Передаем телефон клиента
'CustomerEmail' => '', //Передаем Email клиента
//Возвращаемые параметры
'CustomerId' => null,
];
}
protected static function getFileName(): string
{
return __FILE__;
}
protected function internalExecute(): \Bitrix\Main\ErrorCollection
{
$errorCollection = parent::internalExecute();
$customerService = \MyModule\Service\CustomerService::getInstance();
$result = $customerService->save([
$this->CustomerName,
$this->CustomerPhone,
$this->CustomerEmail,
]);
if ($result->isSuccess())
{
$this->CustomerId = $result->getId();
}
else
{
$errorCollection->setError(new Error($result->getErrorMessages()));
}
return $errorCollection;
}
public static function getPropertiesDialogMap(?PropertiesDialog $dialog = null): array
{
return [
'CustomerName' => [
'Name' => Loc::getMessage('MY_CUSTOM_CUSTOMER_NAME'),
'FieldName' => 'customer_name',
'Type' => \Bitrix\Bizproc\FieldType::STRING,
'Required' => true,
],
'CustomerPhone' => [
'Name' => Loc::getMessage('MY_CUSTOM_CUSTOMER_PHONE'),
'FieldName' => 'customer_phone',
'Type' => \Bitrix\Bizproc\FieldType::STRING,
'Required' => true,
],
'CustomerEmail' => [
'Name' => Loc::getMessage('MY_CUSTOM_CUSTOMER_EMAIL'),
'FieldName' => 'customer_email',
'Type' => \Bitrix\Bizproc\FieldType::STRING,
'Required' => false,
],
];
}
}
Таким образом, получаем меньше кода, убраны дублирующиеся части кода (например, создание диалога, обработка ошибок). Все действия будут работать по единому шаблону, что упрощает поддержку. Многие рутинные задачи (например, подготовка свойств, логирование) выполняются автоматически. Можно легко расширять функциональность, переопределяя методы.
Использование BaseActivity
значительно упрощает разработку действий:
уменьшается объем кода;
упрощается поддержка и расширение;
снижается вероятность ошибок.
Когда какой подход использовать
Ручная реализация подходит для сложных действий, где требуется нестандартная логика или тонкая настройка.
Шаблонная реализация идеальна для типичных задач, где можно использовать готовые решения и минимизировать объем кода.