Обработчики событий

Урок 4 из 8

45 мин

Обработчики событий

Система «1С-Битрикс: Управление сайтом» не замкнутая и допускает несколько способов расширения и изменения своей логики. В процессе работы сайта внутри фреймворка возникает множество т.н. «событий»: событие «сразу после добавления элемента инфоблока», событие «непосредственно перед началом страницы», событие «сразу после удаления пользователя».

У разработчиков есть возможность создавать свои php-функции и методы, которые будет вызывать фреймворк в момент срабатывания определенного события. Такие методы называются «обработчиками событий».

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

// Пример: Обработчик события завершения страницы
public static function OnEndBufferContent(&$content)
{
	$content = str_replace('#DATE#', date('d.m.Y'), $content);
}

После объявления метода фреймворку нужно сообщить, какое событие он должен обрабатывать. Это можно сделать при установке модуля с помощью Bitrix\Main\EventManager::registerEventHandler или «на хитах» с помощью itrix\Main\EventManager::addEventHandler.

Мы рекомендуем после окончания процесса разработки все свои доработки размещать в модуле и обработчики событий регистрировать при помощи registerEventHandler.

При регистрации обработчика нужно указать, модуль и событие, которое «прослушивается», а также класс и метод-обработчик. В каждом модуле есть свой список событий, он приведен в документации по продукту.

Теперь в процессе работы сайта, при возникновении события, система будет вызывать обработчики (на каждое событие их может быть несколько), передавая им входные аргументы, необходимые для их работы.

В аргументах для обработчика может быть объект вида \Bitrix\Main\Event (для современных событий), внутри которого переданы все нужные параметры события, либо непосредственно параметры события (для устаревших событий).

Возвращать обработчик должен либо объект класса \Bitrix\Main\EventResult (для современных событий), либо другие типы данных (разные для разных устаревших событий).

OnBefore и OnAfter

События можно разделить на OnBefore - событие перед фактическим выполнением действия, и OnAfter - событие после фактического выполнения действия. В большинстве случаев это отражено прямо в названии события: OnBeforeProlog, OnAfterEpilog, OnBeforeUserAdd, OnAfterUserLogin.

К примеру, метод CIBlockElement::Update($id, $fields) в ходе своего выполнения:

  1. Обрабатывает свои аргументы и готовится выполнить изменения в БД (обновить элемент Инфоблока)
  2. Вызывает обработчики события OnBeforeIBlockElementUpdate(&$fields)
  3. Вносит изменения в БД
  4. Вызывает обработчики события OnAfterIBlockElementUpdate($fields)

Событие «до обновления» (OnBeforeIBlockElementUpdate) находится в старом ядре и обработчик принимает аргументы по ссылке. Обработчик может изменить данные или отменить сам вызываемый метод Update. В таком случае метод CIBlockElement::Update завершится с ошибкой. Устаревшие обработчики чтобы прервать исполнение события должны вернуть false.

Каждое событие может «слушать» несколько обработчиков. То есть для корректного обновления элемента инфоблока в нашем примере нужно, чтобы все обработчики не стали отменять событие.

К моменту вызова обработчика события «после обновления» (OnAfterIBlockElementUpdate) обновление уже завершено, изменить данные или отменить действие невозможно, в БД записаны новые данные элемента ИБ.

К «третьей» группе событий можно отнести события OnLocalRedirect, OnIBlockDelete и пр., в названии которых нет подсказки и которые в документации описаны словами «вызывается в момент». Обычно такие события испускаются до фактического выполнения, но от обычных событий OnBefore они отличаются тем, что не могут повлиять на данные или отменить срабатывание события. Это не строгое правило, всегда в таких сложных случаях сверяйтесь с документацией или даже исходным кодом фреймворка.

Важно понимать, в каких ситуациях какой обработчик лучше использовать.

Обработчики событий OnBefore помогут:

  • подменить данные на лету (например, вырезать из свойства «Телефон» все символы кроме цифр),
  • отменить событие (например, если такой телефон уже был указан в другом элементе Инфоблока),
  • или запоминить предыдущую версию данных.

Для чего подходят обработчики событий OnAfter:

  • выполнение каскадных изменений в БД,
  • отправка уведомлений,
  • фиксирование в лог-файлах или отчетах.

Когда нужно обрабатывать сразу два события, и OnBefore и OnAfter

Рассмотрим еще один пример (задание из видеокурса): отправка уведомления о добавлении пользователя в группу «Администраторы».

Можно ли это сделать только через событие «до» обновления пользователя?

Нет, так как E-mail должен отправляться только по факту добавления пользователя в соответствующую группу.

А через событие «после» обновления?

Снова нет, так как фреймворк не хранит историю изменений пользователей, и после обновления уже неизвестно, был ли он добавлен в «Администраторы» только что, или месяц назад.

Это как раз тот случай, когда нужны оба обработчика. В обработчике «до» нужно получить текущий список групп пользователя, а в обработчике «после» проверить: если сейчас пользователь присутствует в группе, а раньше его там не было, то можно отправить E-mail.

Зацикливание

Начиная создавать свои обработчики, легко допустить ошибку «зацикливания».

Нельзя допускать зацикливание!

Например, если в обработчике OnBeforeIBlockElementUpdate вызвать CIBlockElement::Update, это приведет снова к вызову OnBeforeIBlockElementUpdate

Эту ошибку можно избежать, если взять за правило не вносить изменения БД в событиях OnBefore, тем более в ту же сущность.

Если изменения все же необходимо внести, то рекомендуем ознакомиться со способом избежания зацикливания обработчиков событий Зацикливание обработчиков событий.

Примеры

Рассмотрим несколько примеров обработчиков событий.

OnProlog

Дано: требуется фиксировать в журнале событий все POST-запросы в административном разделе.

Для решения этой задачи нужно обрабатывать одно из первых событий страницы: OnPageStart, OnBeforeProlog или OnProlog.

В этом примере обработчик настроен на событие onProlog, которое вызывается в начале визуальной части пролога сайта. Этот обработчик не имеет входных аргументов и не должен ничего возвращать.

\Bitrix\Main\EventManager::getInstance()->addEventHandler('main', 'OnProlog', [
	EventHandlers::class,
	'onProlog'
]);

class EventHandlers
{
	public static function onProlog()
	{
		global $APPLICATION;
		$method = \Bitrix\Main\Context::getCurrent()->getServer()->getRequestMethod();
		if (defined('ADMIN_SECTION') && $method == 'POST')
		{
			CEventLog::add([
				'SEVERITY' => 'INFO',
				'AUDIT_TYPE_ID' => 'ACADEMY_ADMIN_POST',
				'DESCRIPTION' => 'POST-запрос в административный раздел',
			]);
		}
	}
}

С помощью метода \Bitrix\Main\Server::getRequestMethod обработчик проверяет тип запроса (проверка на POST). Проверка административного раздела проводится с помощью системной константы ADMIN_SECTION — она будет инициализирована только в подразделах /bitrix/.

Если оба условия выполнены, обработчик с помощью CEventLog::add сохраняет информацию о POST-запросе в журнал событий. В метод передается только 3 аргумента: уровень, тип и текст сообщения. Сам механизм журнала событий дополнит данные IP-адресом, адресом текущей страницы и ID пользователя.

OnAdminContextMenuShow

Дано: нужно дать возможность сотрудникам, работающим в административном разделе, задать вопрос администратору. Задавать вопрос допустимо через E-mail-клиент на ПК, но получатель, заголовок и тело письма должны быть заданы автоматически.

Для этой задачи подходит обработчик события OnAdminContextMenuShow, которое вызывается на всех страницах с панелью кнопок, а это почти все страницы с формами редактирования данных.

\Bitrix\Main\EventManager::getInstance()->addEventHandler('main', 'OnAdminContextMenuShow', [
	EventHandlers::class,
	'onAdminContextMenuShow'
]);

class EventHandlers
{
	/**
	 * @param @var array $items
	 */
	public static function onAdminContextMenuShow(&$items)
	{
		$request = \Bitrix\Main\Context::getCurrent()->getRequest();
		$protocol = $request->isHttps() ? 'https://' : 'http://';
		$host = $request->getHttpHost();
		$currentPage = $protocol . $host . \Bitrix\Main\Context::getCurrent()->getServer()->getRequestUri();

		$email = \Bitrix\Main\Config\Option::get('main', 'email_from');
		$uri = new \Bitrix\Main\Web\Uri('mailto:' . $email);
		$uri->addParams([
			'subject' => 'Проблема на странице',
			'body' => 'Проблема на странице ' . PHP_EOL . $currentPage,
		]);

		$items[] = [
			'TEXT' => 'Задать вопрос',
			'LINK' => $uri->getUri(),
			'TITLE' => 'Отправить ссылку на текущую страницу администратору',
		];
	}
}

Событие испускается при отрисовке панели с кнопками, обработчик получает на вход массив кнопок. В обработчике можно изменить состав кнопок (добавить свою, удалить или изменить существующую), так как массив передается по ссылке.

Решение заключается в добавлении дополнительной кнопки в панель на странице. Кнопки в этой панели — обычные href-ссылки, а значит, можно создать mailto-ссылку на написание письма.

Нужно только корректно ее сформировать. Для этого подходит класс \Bitrix\Main\Web\Uri. Получатель письма находится в БД в настройке главного модуля «Email администратора сайта (отправитель по умолчанию)».

Заголовок письма задается фиксированный: «Проблема на странице», а для формирования полной ссылки на страницу требуется соединить 3 подстроки:

  • Протокол (проверяется через \Bitrix\Main\HttpRequest::isHttps)
  • Хост (\Bitrix\Main\HttpRequest::getHttpHost)
  • Относительный путь

Завершается обработчик добавлением новой кнопки в массив.

OnBeforeAdd HL-блока

Подробнее Highload-блоки и их события будут рассмотрены в следующих уроках.

\Bitrix\Main\EventManager::getInstance()->addEventHandler('', 'PagesOnBeforeAdd', [
	HlbEventHandlers::class,
	'pagesOnBeforeAdd'
]);

class HlbEventHandlers
{
	public static function pagesOnBeforeAdd(\Bitrix\Main\Entity\Event $event)
	{
		$result = new \Bitrix\Main\Entity\EventResult();
		$entity = $event->getEntity();
		$object = $event->getParameter('object');
		$link = $object->get('UF_LINK');

		if ($_SESSION['ALLOWED_URL'])
		{
			if (!substr_count($link, $_SESSION['ALLOWED_URL']))
			{
				$result->addError(
					new \Bitrix\Main\Entity\FieldError(
						$entity->getField('UF_LINK'),
						'Вы можете добавлять только страницы домена: ' . $_SESSION['ALLOWED_URL']
					)
				);
			}
		}

		return $result;
	}
}

OnBeforeAdd ORM-сущности

Подробнее ORM-сущности и их события будут рассмотрены в следующих уроках

class BookTable extends \Bitrix\Main\Entity\DataManager
{
	// ...
}

$ormEventManager = \Bitrix\Main\ORM\EventManager::getInstance();

$ormEventManager->addEventHandler(
	BookTable::class,
	\Bitrix\Main\Entity\DataManager::EVENT_ON_BEFORE_ADD,
	[
		OrmEventHandlers::class,
		'bookOnBeforeAdd'
	]
);

class OrmEventHandlers
{
	public static function bookOnBeforeAdd(\Bitrix\Main\Entity\Event $event)
	{
		$result = new \Bitrix\Main\Entity\EventResult;
		$data = $event->getParameter('fields');

		if (isset($data['ISBN']))
		{
			$result->addError(new \Bitrix\Main\Entity\FieldError(
				$event->getEntity()->getField('ISBN'),
				'Запрещено менять ISBN код у существующих книг'
			));
		}

		return $result;
	}
}

Практика

Обработчики событий. Пример №1

10 мин

Обработчики событий. Пример №2

10 мин

Обработчики событий. Пример №3

9 мин