Плотно работаю с ORM битрикса, все замечательно, но вот понадобилось работать с иерархическими структурами. Единственным методом работы с деревьями являлся старый добрый класс CIBlockSection, но он меня не устраивает по многим причинам:
Решение вроде бы лежит на поверхности. Решено было создать свой класс - потомок от Bitrix\Main\Entity\DataManager и реализовать в нем логику работы с деревьями из CIBlockSection. Способы реализации виделись следующие:
Готовый модуль можно взять по адресу , а здесь я рассмотрю некоторые особенности.
Итак создаем класс
Для регистрации наших событий создадим функцию:
Обратите внимание на строчки с комментариями classic и modern. Дело в том, что в DataManager событие отсылается дважды. Например
Чтобы наш обработчик среагировал на первое событие, его надо регистрировать с типом
Чтобы на второе, соответственно с типом
От выбора типа зависит порядок выполнения обработчиков. Собственные методы onBefore… onAfter… реагируют на modern, а classic посылается раньше. Я выбрал classic, поэтому сначала выполнится мой обработчик, а затем собственный, унаследованный от DataManager
Чтобы наш обработчик не регистрировался повторно, добавим свойство класса
и выставим флаг
Теперь переписываем методы add, update, delete следующим образом:
Далее пишем наши обработчики, в которых уже и реализуем логику работы с деревом
Примеры работы
Пример ORM-класса находится в файле lib/nstest.php
все поля, кроме NAME являются обязательными
Добавление новой записи в корень дерева
Добавление записи в существующую ветку
Перемещение записи или целой ветки в новую ветку
Получение всего упорядоченного дерева, начиная от корня
Получение только корневых элементов
Получение всех потомков конкретной ветки дерева
Получение всех предков конкретной ветки дерева
Транзакции
Во избежание разрушения структуры дерева при совместном доступе желательно блокировать таблицу на запись и откатывать изменения при возникновении ошибок. Для этого создаем методы lockTable() и unlockTable(). В mySQL нельзя использовать одновременно LOCK TABLE и START TRANSACTION () поэтому метод lockTable() выполняет:
А unlockTable()
Алгоритм работы с NSDataManager при использовании транзакций примерно такой:
Буду рад критике
- Все деревья находятся в одной таблице
- Деревья привязаны к инфоблокам
- Собственные поля можно добавить только через UF_
- Ну и т.д.
Решение вроде бы лежит на поверхности. Решено было создать свой класс - потомок от Bitrix\Main\Entity\DataManager и реализовать в нем логику работы с деревьями из CIBlockSection. Способы реализации виделись следующие:
- Полностью переписать методы add, update, delete. Сей способ показался мне довольно геморройным. Неизвестно, что изменится в родителе при очередном обновлении и не будет ли из-за этого глючить потомок
- Реализовать логику в обработчиках событий onBefore… onAfter… Но тогда пришлось бы жестко контролировать, чтобы в обработчиках событий потомков нашего класса вызывался parent::
- Вызывать собственные обработчики событий, а после из них вызывать стандартные методы onBefore… onAfter… Не вышло, т.к. константы с именами обработчиков в DataManager вызываются как self::, а не static::, поэтому переписывать их в наследнике бесполезно
- Регистрировать дополнительные обработчики событий, отрабатывающие независимо от того, используется обработка событий в потомках или нет. Вот на этом способе я и остановился
Готовый модуль можно взять по адресу , а здесь я рассмотрю некоторые особенности.
Итак создаем класс
class NSDataManager extends Entity\DataManager
{
} |
Для регистрации наших событий создадим функцию:
protected static function handleEvent($eventName, $eventHandler)
{
$entity = static::getEntity();
// classic
$eventType = $entity->getName() . $eventName;
// modern
// $eventType = $entity->getNamespace() . $entity->getName() . '::' . $eventName;
if (!static::$eventHandlers[$eventType]) {
$eventManager = Main\EventManager::getInstance();
$eventManager->addEventHandler(
$entity->getModule(),
$eventType,
array(
Entity\Base::normalizeEntityClass($entity->getNamespace() . $entity->getName()),
$eventHandler
),
false,
10
);
static::$eventHandlers[$eventType] = true;
}
} |
Обратите внимание на строчки с комментариями classic и modern. Дело в том, что в DataManager событие отсылается дважды. Например
//event after adding
$event=newEvent($entity,self::EVENT_ON_AFTER_ADD,array("id"=>$id,"fields"=>$data));
$event->send();
//event after adding(modern withn amespace)
$event=newEvent($entity,self::EVENT_ON_AFTER_ADD,array("id"=>$id,"primary"=>$primary,"fields"=>$data),true);
$event->send();
|
Чтобы наш обработчик среагировал на первое событие, его надо регистрировать с типом
//classic $eventType=$entity->getName().$eventName; |
Чтобы на второе, соответственно с типом
//modern $eventType=$entity->getNamespace().$entity->getName().'::'.$eventName; |
От выбора типа зависит порядок выполнения обработчиков. Собственные методы onBefore… onAfter… реагируют на modern, а classic посылается раньше. Я выбрал classic, поэтому сначала выполнится мой обработчик, а затем собственный, унаследованный от DataManager
Чтобы наш обработчик не регистрировался повторно, добавим свойство класса
protected static $eventHandlers = array(); |
и выставим флаг
static::$eventHandlers[$eventType]=true; |
Теперь переписываем методы add, update, delete следующим образом:
public static function add(array $data)
{
static::handleEvent(self::EVENT_ON_BEFORE_ADD, 'treeOnBeforeAdd');
static::handleEvent(self::EVENT_ON_AFTER_ADD, 'treeOnAfterAdd');
return parent::add($data);
}
public static function upd ate($primary, array $data)
{
static::handleEvent(self::EVENT_ON_BEFORE_UPDATE, 'treeOnBeforeUpdate');
static::handleEvent(self::EVENT_ON_AFTER_UPDATE, 'treeOnAfterUpdate');
return parent::update($primary, $data);
}
public static function delete($primary)
{
static::handleEvent(self::EVENT_ON_DELETE, 'treeOnDelete');
return parent::delete($primary);
} |
Далее пишем наши обработчики, в которых уже и реализуем логику работы с деревом
public static function treeOnBeforeAdd(Entity\Event $event){}
public static function treeOnAfterAdd(Entity\Event $event){}
public static function treeOnBeforeUpdate(Entity\Event $event){}
public static function treeOnAfterUpdate(Entity\Event $event){}
public static function treeOnDelete(Entity\Event $event){}
|
Примеры работы
Пример ORM-класса находится в файле lib/nstest.php
все поля, кроме NAME являются обязательными
namespace Bars46\NSTree;
use Bars46\NSTree;
class NSTestTable extends NSTree\NSDataManager {
public static function getTableName()
{
return 'test_tree';
}
public static function getMap()
{
return array(
'ID' => array(
'data_type' => 'integer',
'primary' => true,
'autocomplete' => true,
),
'PARENT_ID' => array(
'data_type' => 'integer',
),
'LEFT_MARGIN' => array(
'data_type' => 'integer',
),
'RIGHT_MARGIN' => array(
'data_type' => 'integer',
),
'DEPTH_LEVEL' => array(
'data_type' => 'integer',
),
'ACTIVE' => array(
'data_type' => 'boolean',
'values' => array('N', 'Y'),
),
'GLOBAL_ACTIVE' => array(
'data_type' => 'boolean',
'values' => array('N', 'Y'),
),
'SORT' => array(
'data_type' => 'integer',
),
'NAME' => array(
'data_type' => 'string',
'required' => true,
),
);
}
} |
Добавление новой записи в корень дерева
Bars46\NSTree\NSTestTable::add( array( 'NAME' => 'ROOT ROW' ) ); |
Добавление записи в существующую ветку
Bars46\NSTree\NSTestTable::add( array( 'PARENT_ID' => $parent_node_id, 'NAME' => 'CHILD ROW' ) ); |
Перемещение записи или целой ветки в новую ветку
Bars46\NSTree\NSTestTable::update( $id, array( 'PARENT_ID' => $new_parent_node_id ) ); |
Получение всего упорядоченного дерева, начиная от корня
$res = Bars46\NSTree\NSTestTable::getList( array( 'select' => array( 'ID', 'NAME' ), 'order' => array( 'LEFT_MARGIN' => 'ASC' ) ) ); |
Получение только корневых элементов
$res = Bars46\NSTree\NSTestTable::getList( array( 'select' => array( 'ID', 'NAME' ), 'filter' => array( '=DEPTH_LEVEL' => 1 ), 'order' => array( 'LEFT_MARGIN' => 'ASC' ) ) ); |
Получение всех потомков конкретной ветки дерева
$node = Bars46\NSTree\NSTestTable::getRow( array( 'select' => array( 'LEFT_MARGIN', 'RIGHT_MARGIN' ), 'filter' => array( '=ID' => $node_id ) ) ); $res = Bars46\NSTree\NSTestTable::getList( array( 'select' => array( 'ID', 'NAME' ), 'filter' => array( '>LEFT_MARGIN' => $node['LEFT_MARGIN'], '<RIGHT_MARGIN' => $node['RIGHT_MARGIN'] ), 'order' => array( 'LEFT_MARGIN' => 'ASC' ) ) ); |
Получение всех предков конкретной ветки дерева
$node = Bars46\NSTree\NSTestTable::getRow( array( 'select' => array( 'LEFT_MARGIN', 'RIGHT_MARGIN' ), 'filter' => array( '=ID' => $node_id ) ) ); $res = Bars46\NSTree\NSTestTable::getList( array( 'select' => array( 'ID', 'NAME' ), 'filter' => array( '<LEFT_MARGIN' => $node['LEFT_MARGIN'], '>RIGHT_MARGIN' => $node['RIGHT_MARGIN'] ), 'order' => array( 'LEFT_MARGIN' => 'ASC' ) ) ); |
Транзакции
Во избежание разрушения структуры дерева при совместном доступе желательно блокировать таблицу на запись и откатывать изменения при возникновении ошибок. Для этого создаем методы lockTable() и unlockTable(). В mySQL нельзя использовать одновременно LOCK TABLE и START TRANSACTION () поэтому метод lockTable() выполняет:
SET AUTOCOMMIT = 0; LOCK TABLES… |
А unlockTable()
UNLOCK TABLES SE T AUTOCOMMIT = 1 |
Алгоритм работы с NSDataManager при использовании транзакций примерно такой:
$connection = Bitrix\Main\Application::getConnection();
Bars46\NSTree\NSTestTable::lockTable();
try {
Bars46\NSTree\NSTestTable::add(
array(
'PARENT_ID' => $parent_node_id,
'NAME' => 'CHILD ROW'
)
);
$connection->commitTransaction();
} catch (\Exception $e) {
$connection->rollbackTransaction();
Bars46\NSTree\NSTestTable::unlockTable();
echo($e->getMessage() . "\n");
} |
Буду рад критике
((