Очень большой проблемой Bitrix24 является его монолитность. Несмотря на то, что от релиза к релизу разработчики решают проблемы сильной связанности, остаётся много легаси кода, поведение которого не всегда поддаётся модификации с помощью событий.
Видеоинструкция в Telegram канале.
Bitrix является, в каком-то смысле, модульной системой. При вызове классов какого-либо модуля происходит его загрузка через автоматическую загрузку классов. Регистрация автолоадера может происходить несколькими способами:
- С помощью стандартной функции php spl_autoload_register
- Используя функционал Bitrix CModule::AddAutoloadClasses
Нужна автозагрузка для подключения необходимого файла, в котором описан вызываемый класс.
Пример автозагрузки классов модуля CRM:
CModule::AddAutoloadClasses( 'crm', array( 'CAllCrmLead' => 'classes/general/crm_lead.php', 'CCrmLead' => 'classes/'.$DBType.'/crm_lead.php', 'CCrmLeadWS' => 'classes/general/ws_lead.php', 'CCRMLeadRest' => 'classes/general/rest_lead.php', 'CAllCrmDeal' => 'classes/general/crm_deal.php', 'CCrmDeal' => 'classes/'.$DBType.'/crm_deal.php', 'CAllCrmCompany' => 'classes/general/crm_company.php', 'CCrmCompany' => 'classes/'.$DBType.'/crm_company.php', 'CAllCrmContact' => 'classes/general/crm_contact.php', 'CCrmContact' => 'classes/'.$DBType.'/crm_contact.php', 'CCrmContactWS' => 'classes/general/ws_contact.php', 'CCrmPerms' => 'classes/general/crm_perms.php', 'CCrmRole' => 'classes/general/crm_role.php', ... ) );
Таким образом при обращении к классу CCrmRole будет подключен файл classes/general/crm_role.php из модуля crm.
Перегрузка и расширение функционала
Как происходить подключение нужных файлов мы разобрались. Но что делать, если потребуется изменить или расширить стандартный функционал? Напоминаю, что вносить правки в ядро системы категорически не рекомендуется!
Мы знаем, что есть 1 или несколько зарегистрированных автозагрузок классов, значит мы можем сделать еще один и указать, что он должен быть выполнен раньше всех (для этого у стандартной функции есть параметр prepend). Файл init.php идеально подойдёт для регистрации нашей функции автозагрузки.
Шаг 1 Регистрация автоматической загрузки классов
Добавим в директорию php_interface файл override.php и подключим его в init.php.
require_once(__DIR__.'/override.php');
Содержимое файла override.php
<?php
/**
* Перегрузка классов
*/
$classDirectoryPath = __DIR__ . '/classes';
/**
* Конфигуратор переопределяемых классов
*/
$config = [];
/**
* Регистрация автозагрузчика
*/
spl_autoload_register(function ($baseClassName) use ($config, $classDirectoryPath) {
if (!empty($config[$baseClassName])) {
$classParts = explode('\\', $baseClassName);
$className = array_pop($classParts);
$namespace = implode('\\', array_filter($classParts));
$virtualClassName = "___Virtual{$className}";
if (file_exists($config[$baseClassName]['classPath'])) {
$classContent = file_get_contents($config[$baseClassName]['classPath']);
$classContent = preg_replace('#^<\?(?:php)?\s*#', '', $classContent);
$classContent = str_replace("class {$className}", "class {$virtualClassName}", $classContent);
if(!empty($config[$baseClassName]['replace'])) {
foreach ($config[$baseClassName]['replace'] as $from => $to) {
$classContent = str_replace($from, $to, $classContent);
}
}
eval($classContent);
}
$classFilePath = $classDirectoryPath . '/' . str_replace('\\', '/', $config[$baseClassName]['overrideClass']) . '.php';
if (file_exists($classFilePath)) {
$overrideClassContent = file_get_contents($classFilePath);
$overrideClassContent = preg_replace('#^<\?(?:php)?\s*#', '', $overrideClassContent);
$overrideClassContent = preg_replace('#extends ([^\s]+)#', "extends {$virtualClassName}", $overrideClassContent);
$overrideClassContent = preg_replace('#namespace ([^\s]+);#',
$namespace ? "namespace {$namespace};" : "",
$overrideClassContent);
eval($overrideClassContent);
return;
}
}
}, true, true);Конфигуратор — массив, содержащий элементы следующего вида:
'Имя исходного класса' => [
'classPath' => 'абсолютный путь к файлу исходного класса',
'overrideClass' => 'имя класса, которым будем подменять исходный',
'replace' => массив значений для замены вида ['подстрока для замены в классе (например "private function foo")' => 'новое значение (например "protected function foo")']
]
Таким образом, для перегрузки классов CCrmDeal и Bitrix\Crm\DealTable массив $config будет такой:
$config = [
'CCrmDeal' => [
'classPath' => __DIR__ . '/../../bitrix/modules/crm/classes/mysql/crm_deal.php',
'overrideClass' => 'Aclips\Override\CCrmDeal'
],
'Bitrix\Crm\DealTable' => [
'classPath' => __DIR__ . '/../../bitrix/modules/crm/lib/deal.php',
'overrideClass' => 'Aclips\Override\DealTable'
]
];Шаг 2 Переопределение
Для примера попробуем запретить использовать метод Bitrix\Crm\DealTable::delete() и расширим Bitrix\Crm\DealTable::getRow(). В директории local/php_interface/classes/Aclips/Override (по умолчанию наши) создадим файл DealTable.php:
<?php
namespace Aclips\Override;
/**
* Класс для переопределения поведения Bitrix\Crm\DealTable
*/
class DealTable extends \Bitrix\Crm\DealTable
{
public static function getRow($params)
{
print 'Метод переопределён';
return parent::getRow($params);
}
public static function delete($id)
{
throw new \Exception('Метод больше не используется');
}
}
Шаг 3 Проверка
Вызов метода Bitrix\Crm\DealTable::getRow()
Bitrix\Main\Loader::includeModule('crm');
$row = \Bitrix\Crm\DealTable::getRow([
'filter' => [
'ID' => 26
]
]);
print("\nid = " . $row['ID']);
Вызов метода Bitrix\Crm\DealTable::delete()
try {
\Bitrix\Crm\DealTable::delete(1);
} catch (\Throwable $e) {
print $e->getMessage();
}
Итог
Теперь везде, где использовались переопределяемые методы будет отрабатывать новая логика, таким образом можно изменить поведение любых функций системы. Если появится необходимость получить доступ к защищённым свойствам и методам исходного (родительского) класса, то можно расширить автозагрузчик и модифицировать класс с помощью Reflection API.
Описанный способ подойдёт для перегрузки любых классов, подгружаемых через автоматическую загрузку классов, например библиотеки установленные через composer (практичность сомнительна, но возможность есть).