Привет, Хабр! Меня зовут Евгений Жуков, я работаю в Битриксе и отвечаю за правильную работу торгового каталога, а также инфоблоков — именно они являются базой для товаров. 

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

Как оказалось, это связано с одной распространенной задачей: при изменении одного элемента инфоблока нужно модифицировать другой.  Она встречается в разных кейсах — логирование, деактивация основного товара, когда нет активных предложений, изменение даты активности связанного элемента.

Никаких проблем, скажете вы. За 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(). Да, подобная ошибка возможна. Тем не менее, культура программирования разработчика — залог ее отсутствия. Да и тесты никто не отменял.

Надеюсь, эта статья поможет в решении достаточно простой, но не вполне очевидной проблемы.

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