Привет, Хабр! Меня зовут Евгений Жуков, я работаю в Битриксе и отвечаю за правильную работу торгового каталога, а также инфоблоков — именно они являются базой для товаров.
Разбирая запросы партнеров, вижу, что часто описываемые ими проблемы — это не ошибки в коде наших модулей, а неправильное использование возможностей. Так, например, мы регулярно получаем пожелание — внедрить возможность отключения механизма событий.
Как оказалось, это связано с одной распространенной задачей: при изменении одного элемента инфоблока нужно модифицировать другой. Она встречается в разных кейсах — логирование, деактивация основного товара, когда нет активных предложений, изменение даты активности связанного элемента.
Никаких проблем, скажете вы. За 10 минут пишется обработчик, использующий метод CIBlockElement::Update, вешается на событие OnBeforeIBlockElementUpdate / OnAfterIBlockElementUpdate, вызывается тестовый пример, сервер падает... Epic fail в чистом виде...

После непродолжительной (или очень продолжительной) отладки выясняется то, о чем следовало бы подумать сразу — идет рекурсивный вызов обработчика. Поэтому к нам и обращаются с запросом на отключение механизма событий.
На самом деле, ничего отключать не нужно.
Ниже приведен код, который избавляет от подобных проблем в рамках одного хита. В качестве примера взят обработчик OnAfterIBlockElementUpdate.
class MyClass
{
private static bool $handlerDisallow = false;
public static function iblockElementUpdateHandler(&$fields): void
{
/* проверяем, что обработчик уже запущен */
if (self::$handlerDisallow)
{
return;
}
/* взводим флаг запуска */
self::$handlerDisallow = true;
/* наш код, приводящий к вызову CIBlockElement::Update */
...
$element = new \CIBlockElement();
$element->Update (..., ...);
/* вновь разрешаем запускать обработчик */
self::$handlerDisallow = false;
}
}
Поясню. За основу взят класс, т.к. в основном речь о собственных модулях. В классе имеется статическая булева переменная — $handlerDisallow. По умолчанию она имеет значение false — нет запрета. В самом начале обработчика необходимо проверять ее значение. Если обработчик уже запущен, она будет равна true и выполнение необходимо прервать. Если же выполнять обработчик можно, необходимо присвоить этой переменной true на время выполнения все обработчика. В конце необходимо флаг ($handlerDisallow) сбросить, иначе до конца хита ваш обработчик не выполнится больше ни разу.
Если вы используете в качестве обработчика обычную функцию, не класс - не беда. Создайте статическую переменную внутри функции.
Если нужна возможность управления обработчиком «снаружи»
Такой функционал скорее подходит для тиражных решений, предоставляющих api для различных задач (Маркетплейс), чем для реализации своей логики на конкретном проекте.
Типичный пример, взятый из штатного функционала — операции массового импорта товаров, когда выгоднее делать пересчёт доступности не для каждого товара отдельно, а для перечня — по окончании импорта.
Для реализации вынесем работу с флагом блокировки в отдельные публичные методы и поменяем тип переменной-флага:
class MyManagedClass
{
private static int $handlerDisallow = 0;
public static function disableHandler(): void
{
self::$handlerDisallow--;
}
public static function enableHandler(): void
{
self::$handlerDisallow++;
}
public static function isEnabledHandler(): bool
{
return (self::$handlerDisallow >= 0);
}
public static function iblockElementUpdateHandler(&$fields): void
{
/* проверяем, что обработчик уже запущен */
if (!static::isEnabledHandler())
{
return;
}
/* взводим флаг запуска */
static::disableHandler();
/* наш код, приводящий к вызову CIBlockElement::Update */
...
$element = new \CIBlockElement();
$element->Update (..., ...);
/* вновь разрешаем запускать обработчик */
static::enableHandler();
}
}
Необходимость новых методов в пояснении не нуждается — она вытекает из самой задачи и базовых принципов ООП (инкапсуляция). А вот причина смены флага на счётчик не столь очевидна.
Для тиражных решений (модулей) заранее неизвестно, где и как именно будет использован api модуля. На примере рассмотрим потенциальную ошибку, связанную с использованием булева флага.
Итак, берем класс из первого примера, расширив его публичными методами включения/выключения обработчика.
class MyPsevdoManagedClass
{
private static bool $handlerDisallow = false;
public static function disableHandler(): void
{
self::$handlerDisallow = true;
}
public static function enableHandler(): void
{
self::$handlerDisallow = false;
}
public static function isEnabledHandler(): bool
{
return self::$handlerDisallow;
}
public static function iblockElementUpdateHandler(&$fields): void
{
/* проверяем, что обработчик уже запущен */
if (!static::isEnabledHandler())
{
return;
}
/* взводим флаг запуска */
static::disableHandler();
/* наш код, приводящий к вызову CIBlockElement::Update */
...
$element = new \CIBlockElement();
$element->Update (..., ...);
/* вновь разрешаем запускать обработчик */
static::enableHandler();
}
}
Добавим два примитивных класса, имитирующих логику конкретного проекта.
Класс А внутри своего метода явно блокирует, а потом снимает блокировку обработчика из MyPsevdoManagedClass:
class A
{
public function execute(): void
{
MyPsevdoManagedClass::disableHandler();
/* работа с элементом инфоблока */
$element = new \CIBlockElement();
$element->Update (..., ...);
MyPsevdoManagedClass::enableHandler():
}
}
Класс B вообще ничего не знает про MyPsevdoManagedClass и никак с ним не взаимодействует.
class B
{
public function execute(): void
{
/* работа с элементом инфоблока */
$element = new \CIBlockElement();
$element->Update (..., ...);
}
}
И наконец мы добрались до примера, иллюстрирующего — почему нельзя в случае публичным методов включения-выключения блокировки использовать булев флаг:
class C
{
public static execute(): void
{
MyPsevdoManagedClass::disableHandler();
A::execute();
B::execute();
MyPsevdoManagedClass::enableHandler();
}
}
C::execute();
Что мы ожидаем? Что в процессе работы метода C::execute обработчик из класса MyPsevdoManagedClass не будет работать вообще. Однако это не так!
Пройдемся по этапам выполнения:
C::execute
/* выключаем обработчик - MyPsevdoManagedClass::$handlerDisallow === true */
MyPsevdoManagedClass::disableHandler();
A::execute
/* еще раз выключаем обработчи
MyPsevdoManagedClass::$handlerDisallow === true
*/
MyPsevdoManagedClass::disableHandler();
// логика метода A::execute
/* включаем обратно - MyPsevdoManagedClass::$handlerDisallow === false */
MyPsevdoManagedClass::enableHandler();
B::execute
/* на этом этапе блокировка уже отключена
И хоть мы не ожидали этого - обработчик сработает
*/
MyPsevdoManagedClass::enableHandler();
Этот пример, хотя и достаточно условный, четко показывает, почему нельзя использовать булев тип для флага в случае публичных методов блокировки.
Конечно, можно возразить, что возможна обратная ситуация — вызвали метод disableHandler(), забыли вызвать enableHandler(). Да, подобная ошибка возможна. Тем не менее, культура программирования разработчика — залог ее отсутствия. Да и тесты никто не отменял.
Надеюсь, эта статья поможет в решении достаточно простой, но не вполне очевидной проблемы.
freezemage0
Ваши коллеги по цеху придумали другое решение, которое является более самостоятельной и переиспользуемой версией "флага защиты от рекурсии" - реализация интерфейса
Bitrix\HumanResources\Contract\Service\SemaphoreService
. По сути, этот сервис предоставляет средства консультативной блокировки, который держит все блокировки в памяти.Использование выглядит примерно так:
Помимо того, что с помощью
SemaphoreService
можно гораздо явнее обозначить защиту от рекурсии, вы также избавляете себя от нескольких головных болей:Решение единообразное и может быть переиспользовано в нескольких проектах, в отличие от необходимости каждый раз внедрять статическое поле в класс, который может столкнуться с проблемами рекурсии;
Нет необходимости предоставлять публичное API внутри вашего класса. Те разработчики, которые хотят управлять этим поведением извне, будут использовать
SemaphoreService
, а не API вашего класса; вы, таким образом, снимаете с себя (а именно, со своего класса) ответственность, всё "неожиданное" поведение оказывается виной того, кто нарушил инкапсуляцию вашего обработчика.Странно, что подобного сервиса всё ещё нет в главном модуле.