Как стать автором
Поиск
Написать публикацию
Обновить

Битрикс24 Factory, Operation, Action разбираемся с новым API CRM и строим масштабируемую архитектуру для смарт-процессов

Уровень сложностиСредний
Время на прочтение18 мин
Количество просмотров441

Эта статья — практическое руководство для разработчиков, которые хотят использовать новое API CRM для работы со смарт-процессами в Bitrix24.

Раньше логику работы с сущностями (сделки, лиды, контакты и т.п.) реализовывали через обработчики событий (событийная модель). Этот подход часто приводит к сложному и запутанному коду, который тяжело поддерживать. Новое API предлагает более современный, предсказуемый и мощный объектно-ориентированный подход (ООП).

Звучит здорово, но на практике разработчики могут сталкиваются с проблемами: 

  • Скудная и сухая документация, 

  • Непонятные и не очевидные баги

  • Антипаттерны в документации

Задачи этой статьи:

  • Объяснить основы: как устроено и работает новое API CRM.

  • Подсветить проблему: показать на конкретном примере сложность добавления обработчиков для смарт-процессов.

  • Предложить решение: дать готовое, работающее решение этой проблемы.

Основной фокус мы направим на то, как правильно добавлять свои обработчики для смарт-процессов — тот самый момент, который хуже всего освещен и в официальной документации, и в статьях на Habr.

Как работает новое API CRM: основана Service Locator

Вся кастомизация в новом API CRM построена на паттерне Service Locator (Сервис-локатор). Разберем, как это устроено.

1. Регистрация сервисов

Битрикс заранее регистрирует в системе все классы-фабрики для работы с сущностями CRM. Это происходит в конфигурационных файлах, например, в bitrix/modules/crm/.settings.php:

'crm.service.factory.contact' => [
    'className' => '\\Bitrix\\Crm\\Service\\Factory\\Contact',
],

2. Получение сервиса

В коде любой модуль или компонент может получить стандартную фабрику для работы с контактами:

// Получаем стандартную фабрику контактов через Service Locator
/** @var \Bitrix\Crm\Service\Factory\Contact $factory */
$factory = \Bitrix\Main\DI\ServiceLocator::getInstance()
    ->get('crm.service.factory.contact');

3. Подмена сервиса (кастомизация)

Чтобы добавить свою логику, мы создаем собственный класс, наследуем его от стандартного и переопределяем нужные методы.

// Наш кастомный класс фабрики
class ContactFactory extends \Bitrix\Crm\Service\Factory\Contact
{
    // Здесь мы переопределим методы, например, для добавления обработчиков
}

Затем мы подменяем стандартный сервис в локаторе на наш:

// Подменяем стандартную фабрику своей
\Bitrix\Main\DI\ServiceLocator::getInstance()
    ->addInstance('crm.service.factory.contact', new ContactFactory());

4. Результат подмены

Теперь любой код в системе, который запрашивает фабрику контактов, будет получать наш экземпляр:

// Теперь этот код вернет наш кастомный ContactFactory
/** @var ContactFactory $factory */
$factory = \Bitrix\Main\DI\ServiceLocator::getInstance()
    ->get('crm.service.factory.contact');

Таким образом, наша логика будет выполняться везде, где используется фабрика.

5. Упрощенное получение через Container

Для удобства разработчики Битрикс24 добавили класс-обертку Container. Его также можно подменить. Работает он так:

// Стандартный способ получить фабрику через Container
// После нашей подмены он тоже вернет наш кастомный класс
/** @var ContactFactory $factory */
$factory = \Bitrix\Crm\Service\Container::getInstance()
    ->getFactory(\CCrmOwnerType::Contact);

Итог: Механизм подмены сервисов — это ключ к кастомизации нового API. Все дальнейшие примеры будут строиться на этой основе.

Factory, Operation и Action: основные понятия

Давайте разберемся с основными компонентами нового API, которые показаны на схеме. Это основа для добавления собственной логики.

UML Factory, Operation и Action
UML Factory, Operation и Action
Вложенность классов
Вложенность классов

1. Factory (Фабрика)

  • Аналогия: Это реализация паттерна Фабричный Метод (Factory Method).

  • Задача: Фабрика создает объекты Operation (операции) для работы с элементами CRM.

  • Пример: DinamicFactory наследуется от базового Service\Factory.

  • Что нас интересует: Три главных метода фабрики, которые возвращают объекты операций:

    • getAddOperation()Operation\Add (создание)

    • getUpdateOperation()Operation\Update (обновление)

    • getDeleteOperation()Operation\Delete (удаление)

2. Operation (Операция)

  • Задача: Инкапсулирует всю логику выполнения действия над элементом (добавление, изменение, удаление).

  • Как работает: Каждая операция содержит массив Action — отдельных шагов с бизнес-логикой. Операция последовательно выполняет эти шаги.

3. Action (Действие)

  • Задача: Это отдельный, атомарный кусок бизнес-логики (ваш кастомный код).

  • Как работает: Вас должен интересовать один главный метод — process(). Он принимает объект Bitrix\Crm\Item  (элемент, над которым проводится операция) и возвращает Bitrix\Main\Result (успех или ошибку).

Как добавить свой обработчик

Вся магия происходит внутри методов фабрики. Вы переопределяете нужный метод (например, для обновления — getUpdateOperation), получаете стандартную операцию и добавляете в нее свой Action.

// Переопределяем метод получения операции обновления
public function getUpdateOperation(Item $item, Context $context = null): Operation\Update
{
    // 1. Получаем стандартную операцию обновления
    $operation = parent::getUpdateOperation($item, $context);
    
    // 2. Добавляем свой Action на этап "ПЕРЕД сохранением"
    $operation->addAction(
        Operation::ACTION_BEFORE_SAVE,
        new ExampleAction()
    );

    return $operation;
}

Метод addAction() принимает два ключевых параметра:

  • Момент выполнения: 

    • Operation::ACTION_BEFORE_SAVE — до сохранения элемента в БД.

    • Operation::ACTION_AFTER_SAVE — после успешного сохранения элемента в БД.

  • Объект-обработчик: наследник класса Action

Итог: Чтобы добавить свою логику, вам нужно:

  1. В фабрике переопределить getAddOperation, getUpdateOperation или getDeleteOperation.

  2. Добавить свой Action в нужную операцию с помощью addAction(), указав этап выполнения (BEFORE или AFTER).

Как добавить свой обработчик в смарт-процесс: практический пример

Разберем на примере смарт-процесса «Заказы» с идентификатором 1036.

Пример СП
Пример СП

1. Создаем кастомную фабрику (OrderFactory)

Создаем класс-фабрику, которая будет возвращать операцию с вашим обработчиком.

<?php

/// файл local/modules/your.module/lib/Crm/Service/Factory/OrderFactory.php

namespace Your\Module\Crm\Service\Factory;

use Bitrix\Crm\Service\Factory\Dynamic;
use Bitrix\Crm\Service\Context;
use Bitrix\Crm\Service\Operation;
use Bitrix\Crm\Model\Dynamic\TypeTable;
use Bitrix\Crm\Item;

/**
 * Смарт-процесс: Заказы
 */
class OrderFactory extends Dynamic
{
    /** @var int */
    protected int $entityTypeId = 1036; // Идентификатор типа смарт-процесса
    
    public function __construct() 
    {
        $type = TypeTable::getByEntityTypeId($this->entityTypeId)->fetchObject();

        if (!is_null($type)) 
        {
            parent::__construct($type);
        } 
        else 
        {
            // Важно: обработайте случай, если тип не найден
            throw new \Exception("Smart process type with ID {$entityTypeId} not found.");
        }
    }

   // Переопределяем метод получения операции обновления
    public function getUpdateOperation(Item $item, Context $context = null): Operation\Update
    {
        // 1. Получаем стандартную операцию обновления
        $operation = parent::getUpdateOperation($item, $context);
        
        // 2. Добавляем свой Action на этап "ПЕРЕД сохранением"
        $operation->addAction(
            Operation::ACTION_BEFORE_SAVE,
            new \Your\Module\Crm\Service\Operation\Action\OrderAction()
        );

        return $operation;
    }
}

2. Подменяем фабрику в сервис-локаторе

Регистрируем нашу фабрику вместо стандартной. Это нужно сделать в точке входа, например, в init.php.

<?php

/// файл local/php_interface/init.php

use Your\Module\Crm\Service\Factory\OrderFactory;

// Подменяем фабрику для конкретного смарт-процесса (ID=1036)
\Bitrix\Main\DI\ServiceLocator::getInstance()->addInstance(
    'crm.service.factory.dynamic.1036', // Ключ для динамического типа
    new OrderFactory()
);

Важно: Ключ сервиса формируется по шаблону crm.service.factory.dynamic.{ID_СУЩНОСТИ}.

3. Создаем Action с бизнес-логикой

Создаем класс, в котором будет реализована ваша логика обработки.

<?php

/// файл local/modules/your.module/lib/Crm/Service/Factory/OrderAction.php

namespace Your\Module\Crm\Service\Operation\Action;

use Bitrix\Main\Result;
use Bitrix\Main\Error;
use Bitrix\Crm\Item;
use Bitrix\Crm\Service\Operation\Action;

// Ваш кастомный класс фабрики
class OrderAction extends Action
{
    /**
     * @param Item $iteml
     * @return Result
     */
    public function process(Item $item): Result
    {
        $result = new Result();
        
        // Пример: проверка поля "Сумма"
        if ($item->get('OPPORTUNITY') < 0) 
        {
            $result->addError(new Error("Сумма заказа не может быть отрицательной!"));
        }
        
        return $result; // Возвращаем результат операции
    }
}
Пример
Пример

Как это работает:

  • Сервис-локатор возвращает кастомную фабрику OrderFactory.

  • При обновлении элемента смарт-процесса система запрашивает операции через метод getUpdateOperation у фабрики.

  • Фабрика создает операцию обновления и добавляет в нее ваш OrderAction.

  • Система выполняет операцию. Перед сохранением (ACTION_BEFORE_SAVE) вызывая метод process.

  • Если process вернул успешный Result — выполнение продолжается. Если вернулись ошибки — операция прерывается, и элемент не сохраняется.

Проблемы нового API CRM

Стремление к объектно-ориентированному подходу — это безусловно хорошо. Новое API предлагает более структурированную, предсказуемую и мощную модель по сравнению с событийной.

Однако у этого подхода есть и обратная сторона. В неопытных руках разработчик может создать больше проблем, чем решить. Давайте разберем основные подводные камни.

Проблема 1: Сложность и качество примеров в документации

Главная проблема на старте — это документация, а точнее, её педагогическая составляющая.

Возьмем, к примеру, официальную страницу «Примеры кастомизации». Вместо того чтобы объяснить базовые концепции (что такое Factory, Operation, Action и как они связаны), она сразу показывает сложные конструкции.

Типичный пример из документации:

Пример из документации
Пример из документации

Почему это проблема:

  • Слишком сложно для старта. В событийной модели разработчик работал с одним классом и тремя параметрами. Здесь же ему сразу показывают три взаимосвязанных класса (Factory, Operation, Action) с высокой степенью вложенности.

  • Использование анонимных классов. Конструкция new class (...) { ... } в данном контексте — крайне плохая практика. Она предназначена для быстрых одноразовых операций, а не для бизнес-логики, которую нужно поддерживать и масштабировать.

  • «Копипаст». К сожалению, такие примеры часто бездумно копируют в реальные проекты. В результате кодовая база быстро заполняется неуправляемыми анонимными классами, которые невозможно переиспользовать, отладить или просто найти в проекте.

Итог: Отсутствие вводных, объяснений логики и примеры не самых лучших практик в официальной документации затрудняют вход в новое API и способствуют появлению плохого, трудно поддерживаемого кода.

Решение: Эта статья — и есть решение данной проблемы. Мы разбираем архитектуру по полочкам и показываем, как делать правильно — через явное объявление классов в отдельные файлы.

Проблема 2: Масштабирование на несколько смарт-процессов

Фраза «у вас будет несколько смарт-процессов» кажется очевидной. Но ключевая мысль, которую часто упускают, заключается в следующем: каждый смарт-процесс — это самостоятельная сущность, такая же как Контакт или Компания. Следовательно, для каждого из них логика должна быть инкапсулирована в своем собственном классе.

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

Плохой подход: Один класс на все сущности

Вот пример из документации, который демонстрирует проблему:

Пример из документации
Пример из документации

Почему это проблема:

  1. Нарушение OCP (Open-Closed Principle). Чтобы добавить обработчик для нового смарт-процесса, вам нужно лезть в уже работающий код и добавлять в него новую ветку if-else. Это прямой путь к ошибкам.

  2. Монолитность. Весь код для всех сущностей собран в одном классе-контейнере. Он будет раздуваться с каждым новым процессом.

  3. Сложность поддержки. Разобраться в таком коде и найти логику для конкретной сущности становится очень тяжело.

Решение: Явная регистрация фабрики для каждого смарт-процесса

Битрикс сам генерирует уникальный ключ сервиса для каждого динамического типа по шаблону crm.service.factory.dynamic.{ID_СУЩНОСТИ}

Вместо одного большого контейнера, мы явно регистрируем отдельную фабрику для каждого смарт-процесса в том же init.php:

/// Файл: local/php_interface/init.php

\Bitrix\Main\DI\ServiceLocator::getInstance()->addInstance(
    'crm.service.factory.dynamic.1036', // СП: Заказы
    new \Your\Module\Crm\Service\Factory\OrderFactory()
);

\Bitrix\Main\DI\ServiceLocator::getInstance()->addInstance(
    'crm.service.factory.dynamic.1037', // СП: Люди
    new \Your\Module\Crm\Service\Factory\PeopleFactory()
);

\Bitrix\Main\DI\ServiceLocator::getInstance()->addInstance(
    'crm.service.factory.dynamic.1038', // СП: Родители
    new \Your\Module\Crm\Service\Factory\ParentFactory()
);

Преимущества этого подхода:

  1. Соответствие принципам SOLID. Каждая фабрика отвечает только за одну сущность (принцип единственной ответственности). Код закрыт для модификаций, но открыт для расширений — чтобы добавить новую сущность, вы создаете новый класс, а не меняете существующие.

  2. Чистота и порядок. Логика для каждого смарт-процесса изолирована в своем собственном файле и классе. Это упрощает навигацию, отладку и поддержку.

  3. Масштабируемость. Добавление 11-го, 12-го или 100-го смарт-процесса не усложнит систему. Процесс останется тем же: создать класс → зарегистрировать фабрику.

Итог: Не усложняйте архитектуру там, где в этом нет необходимости. Используйте встроенную возможность регистрации сервисов по их уникальным идентификаторам. Это сделает ваш код чище и надежнее.

Проблема 3: Множество Actions

По мере развития проекта количество обработчиков (Action) будет только расти. Если для каждого нового действия вы будете просто добавлять в фабрику новый вызов $operation->addAction(), ваш класс очень быстро превратится в нечитаемого монстра.

Особенно катастрофично эта ситуация становится, если вы изначально пошли по пути условных конструкций if ($entityTypeId === ...) или анонимных классов.

Решение 1: Инкапсуляция в методе-регистраторе

Минимальное и самое простое решение — вынести логику регистрации обработчиков в отдельный метод. Это сразу делает код чище и нагляднее.

<?php
/// файл local/modules/your.module/lib/Crm/Service/Factory/OrderFactory.php

namespace Your\Module\Crm\Service\Factory;

use Bitrix\Crm\Service\Factory\Dynamic;
use Bitrix\Crm\Service\Context;
use Bitrix\Crm\Service\Operation;
use Bitrix\Crm\Model\Dynamic\TypeTable;
use Bitrix\Crm\Item;

/**
 * Смарт-процесс: Заказы
 */
class OrderFactory extends Dynamic
{
    /** @var int */
    protected int $entityTypeId = 1036; // Идентификатор типа смарт-процесса
    
    public function __construct() 
    {
        $type = TypeTable::getByEntityTypeId($this->entityTypeId)->fetchObject();

        if (!is_null($type)) 
        {
            parent::__construct($type);
        } 
        else 
        {
            // Важно: обработайте случай, если тип не найден
            throw new \Exception("Smart process type with ID {$entityTypeId} not found.");
        }
    }

    // Переопределяем метод получения операции обновления
    public function getUpdateOperation(Item $item, Context $context = null): Operation\Update
    {
        // 1. Получаем стандартную операцию обновления
        $operation = parent::getUpdateOperation($item, $context);
        
        // Выносим добавление действий в отдельный метод
        $this->registerBeforeSaveActions($operation);

        return $operation;
    }
    
    // Метод-регистратор для действий перед сохранением
    protected function registerBeforeSaveActions(Operation\Update &$operation): void
    {
        $actions = [
            new \Your\Module\Crm\Service\Operation\Action\ValidateOrderSumAction(),
            new \Your\Module\Crm\Service\Operation\Action\GenerateOrderTitleAction(),
            new \Your\Module\Crm\Service\Operation\Action\SendNotificationAction(),
            // Новый Action добавляется одной строкой здесь
        ];

        foreach ($actions as $action) 
        {
            $operation->addAction(Operation::ACTION_BEFORE_SAVE, $action);
        }
    }
    
    // По аналогии можно создать registerAfterSaveActions()
}

Преимущества подхода:

  • Чистота: Основной метод getUpdateOperation не загромождается.

  • Наглядность: Все обработчики собраны в одном месте, их список легко просматривать и редактировать.

  • Простота: Новый Action добавляется одной строкой в массив.

Решение 2: Использование Реестра (Registry)

Первый подход отлично работает для 5-10 обработчиков. Но если их становится больше (десятки), массив в фабрике снова начнет раздуваться.

Более масштабируемое решение — вынести управление обработчиками в отдельный класс-Реестр.

protected function registerBeforeSaveActions(Operation\Update $operation): void
{
    $actions = \Your\Module\Crm\Service\Operation\ActionRegistry::getBeforeSaveActions($this->entityTypeId);

    foreach ($actions as $action)
    {
        $operation->addAction(Operation::ACTION_BEFORE_SAVE, $action);
    }
}

Итог: Не позволяйте вашим фабрикам разрастаться. Используйте методы-регистраторы для группировки логики, а для сложных случаев заведите центральный Реестр, который будет возвращать нужный набор обработчиков для каждой сущности. Это сделает архитектуру гибкой и готовой к масштабированию.

Проблема 4: Каскад наследований и хрупкая архитектура

Одна из самых коварных проблем возникает, когда разработчики, пытаясь избежать модификации существующего кода, создают длинные цепочки наследования.

Пример наследования
Пример наследования

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

Почему это проблема:

  1. Хрупкость архитектуры. Цепочка наследования создает жесткую связь между всеми классами. Изменение в одном из промежуточных классов (например, CustomDinamicFactory) может неожиданно сломать все последующие.

  2. Сложность отладки. Крайне сложно отследить, какая именно реализация метода getUpdateOperation должна вызваться и вызывается ли она вообще. Легко ошибиться и унаследоваться не от того класса, потеряв часть функциональности.

  3. Нарушение LSP (Liskov Substitution Principle). Длинные цепочки наследования часто приводят к тому, что классы-потомки перестают быть на 100% совместимыми с своими родителями, что ведет к тонким и сложным для обнаружения багам.

Решение: Композиция вместо наследования

Ключ к решению — отказ от глубоких цепочек наследования в пользу композиции. Вместо того чтобы создавать нового потомка CustomDinamicFactory, мы должны проектировать систему из переиспользуемых, независимых компонентов.

Практическое применение:

  1. Отказ от наследования фабрик. Ваша фабрика должна наследоваться только от стандартного класса Битрикс (Dynamic). Всю кастомизацию нужно внедрять не через создание новых потомков, а через внедрение зависимостей (например, того же Реестра Actions из описания предыдущей проблемы).

  2. Принцип единственной ответственности. Создавайте множество небольших, независимых классов Action, каждый из которых отвечает за одну и только одну операцию. Их комбинация даст нужную бизнес-логику.

  3. Агрегация. Ваша фабрика не должна знать все о всех возможных действиях. Её задача — получить готовый набор действий извне (из Конфига или Реестра) и применить их.

class OrderFactory extends Dynamic
{
    // Переопределяем метод получения операции обновления
    public function getUpdateOperation(Item $item, Context $context = null): Operation\Update
    {
        // 1. Получаем стандартную операцию обновления
        $operation = parent::getUpdateOperation($item, $context);
        
        // Реестр — независимый сервис. Мы не наследуем его, а используем.
        $actions = ActionRegistry::make($this->entityTypeId);
        
        foreach ($actions->getOnBeforeUpdate() as $action)
        {
            $operation->addAction(Operation::ACTION_BEFORE_SAVE, $action);
        }

        return $operation;
    }
}

Итог: Стремитесь к проектированию, при котором новую функциональность можно добавить созданием нового независимого класса Action и его регистрацией в системе (например, в том же Реестре), а не путем создания нового класса-фабрики. Это делает систему гибкой, понятной и действительно соответствующей принципам SOLID и DRY.

Изучайте и применяйте паттерны проектирования — именно они дают инструменты для решения таких сложных архитектурных задач.

Моё решение: Библиотека для качественной архитектуры

Обозначенные выше проблемы на маленьких проектах могут быть неочевидны, но с ростом масштаба вероятность их появления резко возрастает. Чтобы изначально заложить качественную архитектуру, я вынес её в публичную библиотеку.

Библиотека dimayadikin1997/bitrix24-crm позволяет добавлять обработчики, избегая всех описанных проблем: хрупких цепочек наследования, раздувания фабрик и сложности масштабирования.

Установка

Установите библиотеку через Composer:

composer require dimayadikin1997/bitrix24-crm

Шаг 1. Создаем фабрику для смарт-процесса

Создаем класс фабрики для конкретного смарт-процесса. Он наследуется от стандартного Dynamic и реализует интерфейс FactoryInteface.

<?php
  
/// файл local/modules/your.module/lib/Crm/Service/Factory/OrderFactory.php

namespace Your\Module\Crm\Service\Factory;

use Bitrix\Crm\Service\Factory\Dynamic;
use Bitrix\Crm\Service\Container;
use Bitrix\Crm\Service\Context;
use Bitrix\Crm\Service\Operation;
use Bitrix\Crm\Model\Dynamic\TypeTable;
use Bitrix\Crm\Item;

use Yadikin\Bitrix24\Crm\Factory\Intefaces\FactoryInteface;

/**
 * Смарт-процесс: Заказы
 */
class OrderFactory extends Dynamic implements FactoryInteface
{
    /** @var int */
    protected int $entityTypeId = 1036; // Идентификатор типа смарт-процесса
    
    public function __construct() 
    {
        $type = TypeTable::getByEntityTypeId($this->entityTypeId)->fetchObject();

        if (!is_null($type)) 
        {
            parent::__construct($type);
        } 
        else 
        {
            // Важно: обработайте случай, если тип не найден
            throw new \Exception("Smart process type with ID {$entityTypeId} not found.");
        }
    }
    
    /**
     * return string
     */
    public function getInstanceCode() : string
    {
        /// Вернет "crm.service.factory.dynamic.1036"
        $identifier = Container::getIdentifierByClassName(Dynamic::class, [$this->entityTypeId]);
        return $identifier;
    }

    /// Переопределить родительский метод
    public function getAddOperation(Item $item, Context $context = null): Operation\Add
    {
        $operation = parent::getAddOperation($item, $context);

        // добавляем Action`s

        return $operation;
    }

    /// Переопределить родительский метод
    public function getUpdateOperation(Item $item, Context $context = null): Operation\Update
    {
        $operation = parent::getUpdateOperation($item, $context);

        // добавляем Action`s

        return $operation;
    }

    /// Переопределить родительский метод
    public function getDeleteOperation(Item $item, Context $context = null): Operation\Delete
    {
        $operation = parent::getDeleteOperation($item, $context);

        // добавляем Action`s

        return $operation;
    }
}

Шаг 2. Регистрируем фабрику в Service Locator

Явно регистрируем фабрику для нашего смарт-процесса по его уникальному ключу.

<?php

/// Файл: local/php_interface/init.php
  
$orderFactory = new Your\Module\Crm\Service\Factory\OrderFactory();

// Подменяем фабрику для конкретного смарт-процесса (ID=1036)
\Bitrix\Main\DI\ServiceLocator::getInstance()->addInstance(
    $orderFactory->getInstanceCode(), // Ключ для динамического типа
    $orderFactory
);

Шаг 3. Создаем Action с бизнес-логикой

Создаем любой класс с методом, который будет содержать вашу логику. Это не наследник Operation\Action.

<?php
  
/// файл local/modules/your.module/lib/Crm/Service/Factory/ExampleFactory.php

namespace Your\Module\Crm\Service\Operation\Action;

use Bitrix\Main\Result;
use Bitrix\Main\Error;
use Bitrix\Crm\Item;

class OrderAction
{
    /**
     * @param Item $iteml
     * @return Result
     */
    public function run(Item $item): Result
    {
        $result = new Result();
        
        // Пример: проверка поля "Сумма"
        if ($item->get('OPPORTUNITY') < 0) 
        {
            $result->addError(new Error("Сумма заказа не может быть отрицательной!"));
        }
        
        return $result; // Возвращаем результат операции
    }
}

Шаг 4. Регистрируем Action в Реестре

Связываем наш Action с конкретной операцией (добавление, обновление) и этапом (до или после сохранения).

<?php
  
/// ... Далее в файле: local/php_interface/init.php

$orderRegistry = Yadikin\Bitrix24\Crm\Action\Actions::make($orderFactory);

$orderRegistry->onBeforeAdd(new Your\Module\Crm\Service\Operation\Action\OrderAction, 'run');
$orderRegistry->onBeforeUpdate(new Your\Module\Crm\Service\Operation\Action\OrderAction, 'run');

Шаг 5. Интегрируем Реестр в фабрику

Модифицируем методы операций в фабрике, чтобы они автоматически подтягивали все зарегистрированные действия.

<?php
/// файл local/modules/your.module/lib/Crm/Service/Factory/OrderFactory.php

namespace Your\Module\Crm\Service\Factory;

use Bitrix\Crm\Service\Factory\Dynamic;
use Bitrix\Crm\Service\Container;
use Bitrix\Crm\Service\Context;
use Bitrix\Crm\Service\Operation;
use Bitrix\Crm\Model\Dynamic\TypeTable;
use Bitrix\Crm\Item;

use Yadikin\Bitrix24\Crm\Factory\Intefaces\FactoryInteface;

/**
 * Смарт-процесс: Заказы
 */
class OrderFactory extends Dynamic implements FactoryInteface
{
    /** @var int */
    protected int $entityTypeId = 1036; // Идентификатор типа смарт-процесса
    
    public function __construct() 
    {
        $type = TypeTable::getByEntityTypeId($this->entityTypeId)->fetchObject();

        if (!is_null($type)) 
        {
            parent::__construct($type);
        } 
        else 
        {
            // Важно: обработайте случай, если тип не найден
            throw new \Exception("Smart process type with ID {$entityTypeId} not found.");
        }
    }
    
    /**
     * return string
     */
    public function getInstanceCode() : string
    {
        /// Вернет "crm.service.factory.dynamic.1036"
        $identifier = Container::getIdentifierByClassName(Dynamic::class, [$this->entityTypeId]);
        return $identifier;
    }

    public function getAddOperation(Item $item, Context $context = null): Operation\Add
    {
        $operation = parent::getAddOperation($item, $context);

        $orderRegistry = \Yadikin\Bitrix24\Crm\Action\Actions::make(/** @var OrderFactory */$this);
        
        foreach ($orderRegistry->getOnBeforeAdd() as $action) 
        {
            $operation->addAction(Operation::ACTION_BEFORE_SAVE, /** @var Action */$action);
        }
        
        foreach ($orderRegistry->getOnAfterAdd() as $action) 
        {
            $operation->addAction(Operation::ACTION_AFTER_SAVE, /** @var Action */$action);
        }

        return $operation;
    }

    public function getUpdateOperation(Item $item, Context $context = null): Operation\Update
    {
        $operation = parent::getUpdateOperation($item, $context);

        $orderRegistry = \Yadikin\Bitrix24\Crm\Action\Actions::make(/** @var OrderFactory */$this);
        
        foreach ($orderRegistry->getOnBeforeUpdate() as $action) 
        {
            $operation->addAction(Operation::ACTION_BEFORE_SAVE, /** @var Action */$action);
        }
        
        foreach ($orderRegistry->getOnAfterUpdate() as $action) 
        {
            $operation->addAction(Operation::ACTION_AFTER_SAVE, /** @var Action */$action);
        }

        return $operation;
    }

    /// Переопределить родительский метод
    public function getDeleteOperation(Item $item, Context $context = null): Operation\Delete
    {
        $operation = parent::getDeleteOperation($item, $context);

        $orderRegistry = \Yadikin\Bitrix24\Crm\Action\Actions::make(/** @var OrderFactory */$this);
        
        foreach ($orderRegistry->getOnBeforeDelete() as $action) 
        {
            $operation->addAction(Operation::ACTION_BEFORE_SAVE, /** @var Action */$action);
        }
        
        foreach ($orderRegistry->getOnAfterDelete() as $action) 
        {
            $operation->addAction(Operation::ACTION_AFTER_SAVE, /** @var Action */$action);
        }

        return $operation;
    }
}

Какие проблемы это решает

  • Масштабируемость: Новый Action для существующего смарт-процесса добавляется в две строки кода в init.php без изменений самих фабрик.

  • Чистота кода: Вся логика обработки изолирована в отдельных классах. Фабрики остаются чистыми и выполняют только свою задачу.

  • Избегание наследования: Нет необходимости создавать цепочки классов. Вся кастомизация происходит через регистрацию в реестре.

  • Тестируемость: Action — это простые классы с одним методом. Их легко протестировать.

  • Явная регистрация: Для каждого смарт-процесса создается своя фабрика и свой набор действий, что делает архитектуру предсказуемой и легкой для понимания.

Эта библиотека — не конечная истина, а отправная точка для построения качественной и легко поддерживаемой архитектуры в ваших проектах на Bitrix24.

Заключение

Переход от событийной модели к новому объектно-ориентированному API CRM в Bitrix24 — это значительный шаг вперёд для платформы. Новый подход предлагает разработчикам более структурированный, предсказуемый и мощный инструмент для работы с данными, особенно со смарт-процессами.

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

Ключ к успешной работе приложения — это качественная архитектура с самого начала. Важно не просто копировать примеры из документации, а применять современные практики разработки, например:

  • Следование принципам SOLID для создания поддерживаемого и тестируемого кода.

  • Композиция вместо наследования — использование реестров (Registry) для управления зависимостями делает систему гибкой и масштабируемой.

Предложенное решение в виде библиотеки — это лишь один из возможных путей, который иллюстрирует, как можно систематизировать работу с обработчиками. Вы можете реализовать аналогичный подход самостоятельно или использовать готовые инструменты, чтобы сосредоточиться на бизнес-логике.

Новое API CRM — это будущее разработки под Bitrix24. Освоив его и применяя правильные архитектурные паттерны, вы сможете создавать сложные, надежные и легко поддерживаемые интеграции, которые будут расти и развиваться вместе с вашим проектом.

Теги:
Хабы:
+3
Комментарии2

Публикации

Ближайшие события