Service extending
Description
Easy service extending is one of [link=13972404]service[/link] mechanism existing advantages.
Service extending provides developer with higher level of app manageability without disrupting the source code.
However, developer must execute high level of caution, responsibility and understanding of actions performed.
Due to this, replacing original services with a customized update, you need to adhere to the following rules:
- Implemented changes shall not disturb existing app logic. The more complex improvements are introduced, the more difficult is to support them in the future.
- It's not recommended to introduce changes to method signatures at the level of added arguments.
- Changes must be implemented in a manner for them to be simply and painlessly removed (for debugging / checking of various development hypothesis).
These rules are specially pertinent at the earlier stage of service development. The "younger" the code, the more likelihood that it will change.
Preparation
This section details services for CRM. Service extending requires an available original class and mandatory connected CRM module.
In an ideal case, you should have a subscription to an event, occurring when connecting CRM module for subsequent extending inside it.
Now such event is not present and CRM module must be connected on each hit and services extended.
File /local/php_interface/include/crm_services.php
if (\Bitrix\Main\Loader::includeModule('crm')) { // here we can use crm module api }
File /local/php_interface/init.php
define('CRM_USE_CUSTOM_SERVICES', true); if (defined('CRM_USE_CUSTOM_SERVICES') && CRM_USE_CUSTOM_SERVICES === true) { $fileName = __DIR__ . '/include/crm_services.php'; if (file_exists($fileName)) { require_once ($fileName); } }
All examples below are applicable to the crm_services.php
file content without accounting of first branch with condition.
Now service customization can be disabled at any moment, by annotating constant declaration.
crm_services.php
.
Simple example for service extending
This example extends [link=13982518]localization[/link] service.
use Bitrix\Crm\Service; use Bitrix\Main\DI; $localization = new class extends Service\Localization { public function loadMessages(): array { $messages = parent::loadMessages(); $messages['CRM_MY_NEW_COMMON_MESSAGE'] = 'Some Messages'; return $messages; } }; DI\ServiceLocator::getInstance()->addInstance( 'crm.service.localization' , $localization );
This example places a re-defined service directly to DI\ServiceLocator
, to be retrieved by Service\Contianer
.
In majority of cases it's sufficient to extend Service\Container
.
Container extending
Extend [link=13976638]Service\Container[/link].
use Bitrix\Crm\Service; use Bitrix\Main\DI; $container = new class extends Service\Container { }; DI\ServiceLocator::getInstance()->addInstance('crm.service.container', $container);
The same is repeated as in the previous example. However, a container is placed into DI\ServiceLocator
.
Now any container method can be re-defined, by returning your own implementation to a service query.
All subsequent examples presuppose that you already have container extending code. Examples below are listed with container implemented in a separate file.
Factory extending
For more complex extending actions, sooner or later you'll have to extend the factory [link=13981986]Service\Factory[/link].
In this case, factory is to be extended not for all smart processes, but only for a single one.
For example, this SPA was created previously and its ID was included into the constant:
define('SUPER_ENTITY_TYPE_ID', 150);
Below is the example of code, when factory is extended only for SPA with listed ID:
use Bitrix\Crm\Service; class MyContainer extends Service\Container { public function getFactory(int $entityTypeId): ?Service\Factory { if (defined('SUPER_ENTITY_TYPE_ID') && $entityTypeId === SUPER_ENTITY_TYPE_ID) { $type = $this->getTypeByEntityTypeId($entityTypeId); $factory = new class($type) extends Service\Factory\Dynamic { // here some additional logic }; return $factory; } return parent::getFactory($entityTypeId); } };
Set as [link=13986312]поле[/link] read only
Let's suppose that factory implementation for separate SPA is located in a separate file.
Now you need to add custom logic within this factory.
For example, you have custom field with code UF_CRM_150_STRING
to be available as read only (can be changed only via API).
use Bitrix\Crm\Service; class MyFactory extends Service\Factory\Dynamic { public function getUserFieldsInfo(): array { $fields = parent::getUserFieldsInfo(); $fields['UF_CRM_150_STRING']['ATTRIBUTES'][] = \CCrmFieldInfoAttr::Immutable; return $fields; } }
Adding the attribute \CCrmFieldInfoAttr::Immutable
doesn't allow user to update this field via interface.
In similar fashion, you can add the following attributes:
\CCrmFieldInfoAttr::NotDisplayed
- hides field from details.\CCrmFieldInfoAttr::Required
- sets field as required independently from settings.
Operation extending
Standard case - make adjustments in [link=14005980]operations[/link] executed for an element.
Event mechanism allows to enter into process of before saving and after saving the changes.
Adding via factory of [link=14006646]additional actions[/link] - similar to events.
Legacy event handlers for "old" entities are implemented using the same additional actions. However, it's preferable to use this mechanism directly.
For example, we want to implement logging for deletion of SPA elements:
use Bitrix\Main\Result; use Bitrix\Crm\Item; use Bitrix\Crm\Service; use Bitrix\Crm\Service\Operation; class MyFactory extends Service\Factory\Dynamic { public function getDeleteOperation(Item $item, Service\Context $context = null): Operation\Delete { $operation = parent::getDeleteOperation($item, $context); return $operation->addAction( Operation::ACTION_AFTER_SAVE, new class extends Operation\Action { public function process(Item $item): Result { $userId = Service\Container::getInstance()->getContext()->getUserId(); \AddMessage2Log(Json::encode([ 'userId' => $userId, 'entityTypeId' => $item->getEntityTypeId(), 'id' => $item->getId(), ])); return new Result(); } } ); } }
Restrict stage change for a specific user
A slightly more complex case. For example, we want to move an element from stage D150_3:PREPARATION to the stage D150_3:CLIENT for user with 222.
use Bitrix\Main\Error; use Bitrix\Main\Result; use Bitrix\Crm\Item; use Bitrix\Crm\Service; use Bitrix\Crm\Service\Operation; class MyFactory extends Service\Factory\Dynamic { public function getUpdateOperation(Item $item, Context $context = null): Operation\Update { $operation = parent::getUpdateOperation($item, $context); return $operation->addAction( Operation::ACTION_BEFORE_SAVE, new class extends Operation\Action { public function process(Item $item): Result { $result = new Result(); $userId = Service\Container::getInstance()->getContext()->getUserId(); if ( $userId === 222 && $item->isChangedStageId() && $item->getStageId() === 'D150_3:CLIENT' && $item->remindActual(Item::FIELD_NAME_STAGE_ID) === 'D150_3:PREPARATION' ) { $result->addError(new Error('Change stage is prohibited')); } return $result; } } ); } }