Блог Горошко Андрея Bitrix24,PHP Создание своего действия: Подробное руководство

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

Эффективная интеграция и кастомизация функционала в Битрикс24 часто требует работы на уровне API. Сегодня мы подробно рассмотрим «Создание своего действия: Подробное руководство». Вы узнаете экспертные методики работы с битрикс и научитесь избегать типичных ошибок начинающих разработчиков.

—
keywords: «activity, cbpactivity, baseactivity, bitrix, 1c-bitrix, активити, действия, бизнес, процессы, бизнес-процессы, свое»
—

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

[TOC]

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

>Для нетерпеливых, есть оформленный пример кода на 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):

ПараметрТипОписание
TypestringМнемонический код типа поля
NamestringОтображаемое название поля
DescriptionstringПримечание к полю (нужно выводить вручную)
RequiredboolФлаг обязательного поля
MultipleboolФлаг множественного поля
Optionsstring/arrayСтрока либо массив с дополнительными параметрами (в зависимости от типа поля)
SettingsarrayМассив с дополнительными параметрами (в зависимости от типа поля)
DefaultmixedЗначение по-умолчанию (в зависимости от типа поля)

Немного о параметре 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) — ассоциативный набор доступных группу

💡 Экспертное мнение и рекомендации

Советы эксперта по интеграции:

  • Всегда используйте кэширование при частых запросах к битрикс, чтобы разгрузить базу данных.
  • Соблюдайте стандарты PSR при написании обработчиков для «Создание своего действия: Подробное руководство».
  • Регулярно проводите аудит производительности системы с помощью встроенного монитора 1С-Битрикс.

Похожая задача в бизнесе?

Помогу автоматизировать процессы и внедрить ИИ

Меня зовут Андрей Горошко. Я Full-stack разработчик и эксперт по 1С-Битрикс. Настраиваю CRM Битрикс24, разрабатываю защищенные API и создаю умных ИИ-агентов для автоматизации рутины.

Related Post

Открыть всплывающее окно BX24.openApplicationОткрыть всплывающее окно BX24.openApplication

Каждый PHP-разработчик, работающий с экосистемой Bitrix Framework, должен глубоко понимать работу её ключевых компонентов. В этой статье мы проведем подробный аудит темы «Открыть всплывающее окно BX24.openApplication», проанализируем лучшие практики и

Получить значения всех полей группы свойств по id sale.propertygroup.getПолучить значения всех полей группы свойств по id sale.propertygroup.get

Оптимизация производительности и безопасности — залог успешного проекта на 1С-Битрикс. Данный материал посвящен практическому применению «Получить значения всех полей группы свойств по id sale.propertygroup.get». Мы подготовили подробный разбор принципов работы

Изменить контакт crm.contact.updateИзменить контакт crm.contact.update

Каждый PHP-разработчик, работающий с экосистемой Bitrix Framework, должен глубоко понимать работу её ключевых компонентов. В этой статье мы проведем подробный аудит темы «Изменить контакт crm.contact.update», проанализируем лучшие практики и покажем,