Перегрузка классов ядра Bitrix (и не только)

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

Видеоинструкция в Telegram канале.

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

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

Пример автозагрузки классов модуля 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 (практичность сомнительна, но возможность есть).

6 комментариев

  1. пытаюсь по примеру заменить метод класса

    CAllIMContactList

    находится по пути
    /bitrix/modules/im/classes/general/im_contact_list.php

    ‘Bitrix\Im\CAllIMContactList’ => [
    ‘classPath’ => __DIR__ . ‘/../../bitrix/modules/im/classes/general/im_contact_list.php’,
    ‘overrideClass’ => ‘Bazoo\Override\ImOverrideClass’
    ],

    в файле класса
    namespace Bazoo\Override;

    use \Bitrix\Im\CAllIMContactList;

    class ImOverrideClass extends CAllIMContactList

    и метод GetList в котором просто exit()

    ваш пример работает, мой метод не переписывается

    сможете подсказать в чем дело ?

    Благодарю.

  2. Решил проблему интуитивно
    понял, что файл должен находиться именно в lib

    но сути не понял, если объясните — буду рад

    1. Указание такого неймспейса (Bitrix\Im\CAllIMContactList) говорит о том, что файл класса будет взять из директории модуля lib (bitrix/modules/im/lib). Попробуй указать просто CAllIMContactList. Класс в overrideClass лучше делать одноимённым с переопределяемым.

Добавить комментарий