Когда я искал информацию о журналировании (аудите событий) в Bitrix, на Хабре не было ни чего, в остальном рунете кое что было, но кто же там найдёт?
Для пополнения базы знаний я решил написать эту статью: поделиться своим опытом и предупредить о возможных граблях.
Постановка задачи
Моей задачей было разработать простейшую учётную систему рекламных конструкций, по условиям гос контракта система должна работать на базе Битрикса (версия 15).
Можно было всё навелосипедить сбоку от Битрикса, но я решил что это будет слишком нечестно по отношению к заказчику, функционал Битрикса был использован по максимуму:
- аутентификация пользователей
- система хранения данных (EAV)
- редактор данных
- хуки для аудита
- ролевая модель для авторизации действий пользователей
- управление пользователями
- работа со справочниками
Эта статья в основном о хуках аудита, также немного расскажу об авторизации и добавлении пользовательских закладок к карточке записи инфоблока.
Хуки для аудита
В разработанной системе работа с данными ведётся через API (добавление нового объекта на карту и изменение его координат), другие параметры объекта редактируются через редактор Битрикса, поскольку API обрабатывает не все запросы на изменение данных, то аудит только в API был бы не полным, значит нам необходим аудит на стороне Битрикса.
Не то что бы я не знал что такое аудит, но писать его приходиться не часто. Обычно это решается записью старой версии строки (старых данных) в отдельную таблицу, и реализация выполняется целиком на стороне СУБД. При этом у нас одновременно есть состояние до изменений и состояние после изменений.
Писать аудит на триггерах СУБД, или писать аудит на хуках Битрикса, разница не большая.
Хуки прописываются в скрипте "bitrix/php_interface/init.php", если файла нет, то его необходимо создать:
use Topliner\Scheme\Logger;
// подключаем автозагрузчик
require_once($_SERVER['DOCUMENT_ROOT'] . '/vendor/autoload.php');
// код модуля на события которого будем вешать хуки
if (!defined('IBLOCK_MODULE')) {
define('IBLOCK_MODULE', 'iblock');
}
// добавляем хук для события
AddEventHandler(IBLOCK_MODULE, 'OnIBlockElementAdd',
Array(Logger::class, 'OnAdd'));
// первый аргумент это код модуля, второй - собственно событие для обработки, третий аргумент это массив где мы указываем класс и метод который будут выполнять обработку, метод конечно статический
Это пример одного обработчика, в моей реализации их несколько, код можно посмотреть в репозитории, ссылка будет в конце статьи.
Сигнатура метода для обработки события у каждого события своя, какая конкретно смотрим в документации Битрикса или дебажим и смотрим исходники (наглядней, все хуки и моменты их срабатывания так же можно посмотреть в исходниках во время отладки).
И тут мы сталкиваемся в тем что при работе с СУБД у нас есть одновременно текущие данные и данные которые будут записаны, а при работе с Битриксом, у нас есть или старые данные или новые данные, но ни когда у нас не будет и старых и новых одновременно.
Другая беда в том, что перед изменением данных есть несколько событий и в чём между ними разница мне понять не удалось, такая же история со срабатыванием нескольких событий после изменения данных, глаза разбегаются от богатства выбора, на деле это только иллюзия выбора.
В целом Битрикс это легаси пропатченное другим легаси, ещё и написанное в разной нотации, в перемешку из снейк_кейса и кэмэлКейса, с добавками венгерской нотации (привет ребятам которые всю жизнь писали на Delphi и кто то их заставил писать на PHP), код Битрикса банально неприятно читать.
Если у большинства фреймворков не считается зазорным устаревшие методы помечать как depricated и в дальнейшем выпиливать их из «ядра», то архитекторы Битрикс считают эту практику неприемлемой.
Когда пишешь «на Битриксе» тебе в голове надо держать два (и больше) разных «алгоритма» (стратегии, философии, подхода) работы системы, а потом бизнес не понимает почему Битрикс программисты стоят дороже чем программисты других фреймворков. Потому что дорогой бизнес тебе сэкономили деньги на переписывании кода при переходе к новой версии Битрикс, потому теперь программисту в голове надо держать весь «хвост» обратной совместимости.
Я считаю каждый программист Битрикс должен сказать спасибо авторам фреймворка за свою большую зарплату, и за головную боль тоже :)
Конечно когда смотришь на исходники Битрикса ты понимаешь почему сделано так как сделано, но так же ты понимаешь что люди просто поставили подпорки и костыли, что бы по быстрому удовлетворить потребностям бизнеса, просто кто то очень попросил и вот теперь у нас два метода которые делают примерно одно и тоже, но есть нюанс, который ни в какой документации не описан, Битрикс создан для набивания шишек, а виноват кто? конечно Вася программист.
Использование глобального состояния
Что бы сохранять и передавать состояние между хуками, приходиться заводить глобальные переменные. Глобальные переменные это то что не поддаётся рефакторигу, поэтому я использую статические свойства класса:
class Logger
{
const CHANGE = 'change';
const REMOVE = 'remove';
const CREATE = 'create';
const UNDEFINED = 'undefined';
public static $operation = self::UNDEFINED;
const NO_VALUE = [];
private static $fields = self::NO_VALUE;
private static $properties = self::NO_VALUE;
private static $names = self::NO_VALUE;
}
Здесь надо сделать пояснение об использовании «fields» и «properties». Поля это параметры которые есть у каждой записи инфоблока (идентификатор, название, инфоблок «родитель»), свойства это параметры характерные для записей конкретного типа инфоблоков, в EAV модели это атрибуты и их значения.
«names» это наименования атрибутов, в одном событии мы имеем и идентификаторы атрибутов и наименования, в другом событии мы имеем только идентификаторы, и для красивой записи в журнале нам необходимо сохранить названия (можно конечно по идентификатору вычитать, но почему то не хочется).
«operation» текущая операция, для работы с «fields» Битрикс даёт конкретные события, при работе с «properties» (функции «SetPropertyValues» и «SetPropertyValuesEx»), нет события с типом операции, есть событие перед вызовом и после, при этом не известно какая операция выполняется (добавить/обновить/удалить), поэтому операцию так же необходимо передавать из обработчика в обработчик.
Возможно я в чём то не разобрался, и в Битриксе можно одновременно видеть значения как было и как будет, а может быть в журнал надо было отдельно записывать состояние перед и состояние после, но почему то я решил что запись журнала должна быть в формате «свойство %наименование% было %значение перед изменением% => стало %значение после%» и поэтому огрёб проблем с сохранением глобального состояния.
Определение необходимости регистрации изменений
Как ни странно но наш хук будет срабатывать на изменение любой записи любого инфоблока.
Поэтому в обработчике нам надо понимать запись какого инфоблока нам пришла в параметрах.
Инфоблок записи характеризуется двумя значениями это собственно инфоблок ('IBLOCK_ID') и его раздел ('IBLOCK_SECTION_ID'), при чём идентификатор раздела иногда лежит в массиве значений с индексом 'IBLOCK_SECTION_ID', а иногда 'IBLOCK_SECTION', поэтому у меня идентификатор раздела считывается по обоим ключам с приоритетом для 'IBLOCK_SECTION'.
При чём в каких то ситуациях идентификатор раздела невозможно определить в принципе, поэтому проверку на необходимость добавления записи в журнал аудита приходиться делать только на основе идентификатор инфоблока.
После определения инфоблока и раздела, мы принимаем решение о необходимости регистрации события в журнале.
Сохранение состояние перед изменениями
Каждая запись инфоблока состоит из двух частей, одна часть это «fields», и эти параметры изменяются методами:
- CIBlockElement::Add()
- CIBlockElement::Update()
- CIBlockElement::Delete()
Другая часть это «properties», методы:
- CIBlockElement::SetPropertyValues()
- CIBlockElement::SetPropertyValuesEx()
Я уже не помню почему, но от использования в своём коде CIBlockElement::SetPropertyValuesEx() лучше воздержаться.
Каждая часть обрабатывается в своём хуке отдельно от другой, поэтому по нажатию кнопки «Сохранить», может быть создано две записи в журнале аудита.
Сигнатуры событий для «fields» и «properties» различаются, в части с обработкой «fields» мы сохраняем текущее состояние для fields и устанавливаем значение operation, в части обработки «properties» мы сохраняем properties и их names.
Делаем мы это конечно в хуке «перед».
Регистрация изменений
В хуке «после» мы делаем запись в журнал.
При составлении списка различий в записях, есть проблема с атрибутами которые могут принимать множественные значения. Формат записи их состояния «перед» и состояния «после», отличается настолько, что из одного нельзя вывести другой, поэтому при определении необходимости регистрации изменений вы всегда будете делать вывод о том что регистрация необходима.
Исходный код ведения аудита в репозитории в файле src/Topliner/Scheme/Logger.php.
Добавление пользовательской вкладки в карточку инфоблока (в классификаторе Битрикса)
Для добавления карточки используется аналогичный с аудитом подход — добавляем хук:
AddEventHandler('main', 'OnAdminIBlockElementEdit',
Array(PermitTab::class, 'OnInit'));
В методе 'OnInit' мы определяем остальные методы для работы с вкладкой в редакторе Битрикса:
class PermitTab
{
const LINKS = 'links';
function OnInit($arArgs)
{
$permits = BitrixScheme::getPermits();
$pubPermits = BitrixScheme::getPublishedPermits();
$blockId = (int)$arArgs['IBLOCK']['ID'];
$letShow = $blockId === $permits->getBlock()
|| $blockId === $pubPermits->getBlock();
$result = false;
if ($letShow) {
$result = [
'TABSET' => 'LinksToConstructs',
'GetTabs' => [static::class, 'GetTabs'],
'ShowTab' => [static::class, 'ShowTab'],
'Action' => [static::class, 'Action'],
'Check' => [static::class, 'Check'],
];
}
return $result;
}
}
Сначала конечно проверяем, что для этого инфоблока надо показывать нашу пользовательскую вкладку, если надо то задаём методы.
Рассказывать тут особо не чего, смотрите исходники в репозитории src/Topliner/Scheme/PermitTab.php, читайте документацию.
Ролевая модель для авторизации действий пользователей
Методы Битрикса для авторизации работают с линейной шкалой прав, то есть сценария когда одним можно первое и второе, но нельзя третье, а другим можно второе и третье, но нельзя первое, в лоб с помощью Битрикса не реализуешь.
Для таких хитрых сценариев в Битриксе есть просто проверка некоторого разрешения, и в коде при выполнении операции, вы смотрите, есть у пользователя разрешение или нет, и одной роли вы разрешаете первое и второе, а другой роли разрешаете второе и третье.
$constSec = BitrixScheme::getConstructs();
$isAllow = CIBlockSectionRights::UserHasRightTo(
$constSec->getBlock(), $constSec->getSection(),
BitrixPermission::ELEMENT_ADD, false);
if (!$isAllow) {
$output['message'] = 'Forbidden, not enough permission;';
}
- $constSec->getBlock() — идентификатор инфоблока
- $constSec->getSection() — идентификатор раздела
- BitrixPermission::ELEMENT_ADD — код разрешения
В классификаторе (справочник инфоблоков) в нужных инфоблоках и разделах вы назначаете ролям соответствующие права. Пользователям раздаёте роли и всё будет под вашим контролем.
Аутентификация
Если вам для каких то страниц требуется аутентификация пользователя, то просто добавите:
<?php
define("NEED_AUTH", true);
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/header.php");
?>
// ваш код для формирования веб страницы
<?php
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/footer.php");
?>
Битрикс при загрузке такой страницы выдаст форму входа, если пользователь ещё не залогинился.
Если вам просто необходимо подключить Битрикс в каком то скрипте, то:
require_once($_SERVER['DOCUMENT_ROOT']
. '/bitrix/modules/main/include/prolog_before.php');
Работа со справочниками
«Новые» (на ноябрь 2019) справочники в Битриксе называются «HighloadBlock», работа с ними чуть чуть нестандартна.
Каждый Хайлоад блок можно хранить в отдельной таблице, поэтому для доступа к справочнику надо каждый раз определять имя таблицы для чтения. Что бы постоянно этого не делать, в Битриксе надо создавать экземпляр класса для доступа к данным. Создаётся экземпляр в две строки:
CModule::IncludeModule('highloadblock');
$entity = HighloadBlockTable::compileEntity('ConstructionTypes');
$reference = $entity->getDataClass();
Где 'ConstructionTypes' — код справочника.
Что бы постоянно не писать эти две строчки, можно написать класс который будет создавать нам инстансы справочников (src/Topliner/Bitrix/BitrixReference.php).
Далее с экземпляром работаем как с обычным классом ORM Битрикса (D7):
/* @var $reference DataManager */
$data = $reference::getList(array(
'select' => array('UF_NAME', 'UF_XML_ID'),
'filter' => array('UF_TYPE_ID' => 0)
));
Репозиторий (осторожно, много копипасты)
Спасибо за внимание.
mvs
В Битриксе же есть Документооборот (поверх Инфоблоков), а Журнал событий стандартно показывает все изменения элементов, если это включено в настройках.
SbWereWolf Автор
В настройках инфоблока можно включить какой то аудит, но там фиксируется только сам факт изменения без старого и нового состояния.
Модуля «Документооборот» в «Старте» нет, нам ни кто не сказал, что в Битриксе есть готовый модуль аудита, да мы и не спрашивали :)
По описанию модуля «Вы можете быть спокойны за безопасность и сохранение данных – на каждом этапе редактирования система создает копии файлов. При необходимости вы можете просто вернуться к предыдущей версии документа и продолжить работать с ней.» сразу и не догадаешься что это то что нам надо. Хотя заголовок — «История изменений», конечно кричит об этом.
Спасибо за подсказку.