Приветствую всех укротителей событий! Проникнувшись идеей разработки с использованием DDD, я решил реализовать на нем обработку событий для элементов смарт-процессов Bitrix24, коей хочу поделиться с вами.

Перевод архитектуры на DDD подробно описан в статьях:

  1. Часть первая;

  2. Часть вторая;

  3. Часть третья.

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

Шаг 1. Сервис

Так как обработчики событий элементов смарт-процессов могут пригодиться на других проектах, создадим в Shared основной сервис для дочерних менеджеров смарт-процессов под названием “DynamicManager”. Таким образом мы сможем переключаться между ними в случае, если будут создаваться дополнительные. Для этого нам понадобятся:

Интерфейс для сервисов домена Shared, если его еще не существует:

Enum с типами менеджеров смарт-процессов:

Теперь мы можем приступить к созданию самого сервиса. Назовем его BaseDynamic, так как он использует стандартный функционал битрикса без сторонних библиотек. В сервисе будут 2 класса, для которых нужно создать собственные интерфейсы, а также дополнительный интерфейс для классов обработчиков по каждому смарт процессу – Repository:

  1. Action – сборник методов переопределения операций над элементами смарт-процесса:

  1. Controller – класс для транспортировки репозиториев смарт-процессов в классы операций:

  1. Repository – отдельный класс с описанием событий для каждого типа смарт-процесса. Описываем столько событий, сколько мы хотим переопределить:

Также для полноты картины я создал пустой интерфейс для BaseDynamic:

 И теперь, используя интерфейсы, мы можем создать для сервиса BaseDynamic:

Actions:

Controller:

И сам сервис:

Таким образом, при инициализации сервиса, у нас будет доступен его контроллер, однако, Actions надо будет получать отдельно для переданного репозитория. Об этом будет ниже.

Теперь, когда у нас есть подсервис, его необходимо определить в родительском:

Вот мы и создали пустой сервис, который можно получить методом DynamicManagerService::create(ServicesEnums::BASE->value). Пора наполнить его логикой.

Шаг 2. Контейнер

Как мы уже знаем, работа со смарт-процессами происходит посредством контейнеров \Bitrix\Crm\Service\Container, получаемых статическим методом getInstance(). Если углубимся в этот метод, то заметим, что он получает crm.service.container через сервис локатор. Этим же инструментом мы можем подменить контейнер на собственный. Давайте создадим один общий, как фабрику контейнеров через сервис DDD:

Теперь у нас есть класс для подмены контейнеров, но система его еще не видит. Для этого необходимо подменить его явно в файле "/local/php_interface/init.php" (а лучше, конечно, в собственном обработчике событий, чтобы не нагромождать файл и не грузить систему на каждом хите) код:

\Bitrix\Main\DI\ServiceLocator::getInstance()->addInstanceLazy('crm.service.container', [ 'className' => '\\App\\Shared\\Services\\DynamicManager\\BaseDynamic\\Fabric\\BaseDynamicContainerFabric', ]);

Таким образом, мы подменили инициализируемый сервис получения контейнера. Другими словами, теперь при каждом выполнении метода \Bitrix\Crm\Service\Container::getInstance(); мы получим инстанс созданного нами класса BaseDynamicContainerFabric.

Стоит подметить: мы использовали метод addInstanceLazy() для того, чтобы подмена происходила лишь когда выполняется метод getInstance() контейнера, а не при каждом хите страницы.

Так как операции над событиями относятся к сервису фабрик, нам нужно переопределить у контейнера метод их получения:

При этом, только у интересующих нас смарт-процессов. Метод getFactory() должен возвращать, как ни странно, фабрику (но иногда возвращает null) – то есть, дочерний объект абстрактной фабрики со своими определениями операций, а значит, мы можем переопределить и их. Для этого необходимо создать собственную фабрику.

Шаг 3. Фабрика

Так как мы создаем фабрику для смарт-процессов, создаваемый класс должен наследоваться от \Bitrix\Crm\Service\Factory\Dynamic – предка абстрактной фабрики:

Вместо того, чтобы плодить фабрики, я решил сделать одну для всех смарт-процессов, которые мы будем подключать по собственным репозиториям. То есть, фабрика всегда будет одного класса, но с разными параметрами. Для этого нам необходимо описать репозитории для тех смарт-процессов, события над элементами которых мы хотим изменить.

Шаг 4. Репозитории

Здесь нам необходимо на время отойти от слоя Shared и переключиться на Domains, так как нам предстоит описать события, применимые к конкретным смарт-процессам конкретного проекта. Создадим логическую структуру и класс событий \Domains\Dynamic\Events\VisitEvent.php для смарт-процесса "Посещения", унаследованный от созданного нами ранее интерфейса репозитория:

Стоит подметить: для определения событий мы будем добавлять действия к операциям событий, что всегда требует объект Item, над которым производится действие, и зафиксированный (обработанный нами) результат. Так что эти аргументы обязательны и всегда будут участвовать в наших переопределениях событий.

Определим одно событие для примера – onBeforeAdd:

Мы модифицировали результат выполнения события, но еще никуда его не подключили. Так как метод getFactory() контейнера получает идентификатор типа сущности (ENTITY_TYPE_ID) в качестве аргумента (его можно найти в таблице b_crm_dynamic_type или в ссылке открытого смарт-процесса), он и будет определять, нужно ли подключать репозиторий событий. Не придумал варианта лучше, чем создать список констант с соотношениями кодов смарт-процессов и их репозиториев, так как ID – нестабильная величина, а код всегда зависит от создающего смарт-процесс разработчика.

 Сделаем такой список и определим в нем созданный репозиторий:

Также в Enum добавил метод tryFromName($name), позволяющий найти константу в списке по имени, если она существует.

Теперь, используя ID типа сущности, переданный в getFactory(), мы можем найти код смарт-процесса и проверить, существует ли у нас для него репозиторий. Создадим в контроллере 2 метода:

Получение кода смарт-процесса по его type ID:

Получение репозитория событий смарт-процесса по его коду:

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

Шаг 5. Actions и Controller

Теперь нам необходимо описать каким образом методы из репозиториев будут попадать в операции. Для этого расширим классы Actions и Controller.

Добавим контроллеру свойство, имплементированное от RepositoryInterface, а также опишем для него унаследованные геттер и сеттер:

В Actions добавим конструктор, где мы будем запоминать переданный репозиторий:

А далее, тут же, в Actions, опишем новые действия для операций, полученные от интерфейса:

Поговорим о них немного подробнее. У родной фабрики битрикса есть методы определения операции, например: getAddOperation() (вернет \Bitrix\Crm\Service\Operation\Add). Но у нас есть возможность модифицировать операцию действиями – ее наследниками. Таким образом, вызывая у операции метод addAction(), мы добавляем ей новое действие, указав его тип (в нашем примере константа \Bitrix\Crm\Service\Operation::ACTION_BEFORE_SAVE) и экземпляр действия – анонимный класс-наследник Action, в котором переопределен метод process(). Здесь нам достаточно вызвать у сохраненного конструктором репозитория соответствующий метод с переданными Item-объектом, над которым совершается операция, и объектом результата выполнения, который может быть модифицирован. Именно поэтому в методах (которые так же, как и process(), возвращают объект Result) интерфейса репозитория важно соблюсти аргументы и возвращаемое значение.

Таким образом, можно добавить действия для следующих операций:

  1. Добавление – \Bitrix\Crm\Service\Operation\Add;

  2. Обновление – \Bitrix\Crm\Service\Operation\Update;

  3. Удаление – \Bitrix\Crm\Service\Operation\Delete;

  4. Копирование – \Bitrix\Crm\Service\Operation\Copy;

  5. Конвертация – \Bitrix\Crm\Service\Operation\Conversion.

Шаг 6. Соединяем

Настало время подключить обработчики. Для начала опишем класс фабрики BaseDynamicFactoryFabric.

  1. Добавляем свойство action, а также его геттер и сеттер:

  1. Переопределяем методы родителя для определения операций. В нашем примере хватит getAddOperation():

То есть, мы получили стандартную операцию добавления и дважды изменили ее, вызвав методы класса Actions (мы определим свойство actions, которое возвращается методом $this->getActions(), позже).

  1. Если необходимо переопределить больше операций, действуем по той же логике:

И, наконец, переходим к определению фабрики (метод getFactory() в классе BaseDynamicContainerFabric):

  1. Пытаемся получить наш кастомный репозиторий для сущности, по которой собирается фабрика:

  1. Сервисы в битриксе собираются часто, а исключение срабатывает, когда не получается найти для него кастомный репозиторий. Чтобы не спамить бессмысленными логами, просто возвращаем не интересующую нас фабрику как есть;

  2. Получаем Actions по полученному репозиторию с помощью нашего сервиса:

  1. Теперь у нас есть определения операций над сущностью с переданным типом. Проверяем, что мы можем взаимодействовать с сервисом (штатными методами контейнера):

  1. Если мы все же можем добавить инстанс для сервиса, то определяем нашу фабрику, установив ей полученный по репозиторию класс Actions как соответствующее свойство, идентифицируем ее и отдаем подмененную:

Вот и всё! Теперь события будут подгружаться лишь для элементов тех смарт-процессов, репозитории которых были описаны.

Стоит подметить: перед переопределением событий необходимо подключить модуль crm.

Завершение

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

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

Ещё больше материала можно найти в канале DD Planet, где публикуются как мои, так и замечательные статьи моих коллег!

Комментарии (1)