+7 495 008 8452 пн.-пт. 10:00 – 17:00
Если у вас возникли какие либо вопросы которые вы не смогли решить по нашим публикациям самостоятельно,
то ждем ваше обращение в нашей службе тех поддержки.


ORM-обёртка для инфоблоков

Потребовалось для одного проекта скрестить инфоблоки с собственными таблицами, всяческими сквозными фильтрами и сортировками. Да и ещё и работало чтоб быстро. Первое, что приходит на ум - ORM. Но она в инфоблоках пока отсутствует (точнее, отсутствует самая нужная её часть - работа с элементами), поэтому пришлось реализовывать свою прослойку, которой и хочу поделиться - https://github.com/unnamed777/IblockOrm

Краткие характеристики
  • Доступна вся мощь getlist'а orm и sql-билдера - связывание и подтягивание данных откуда угодно и как угодно (раз, два).
  • Возможна работа как с заранее описанными сущностями (наследниками обёртки), так и с динамически создаваемыми (аналогично hlblock'ам).
  • Поддержка стандартной постраничной навигации. Пока в ORM её нет, поэтому пришлось немного костыльнуть, зато поддерживается стандартный system.pagenavigation.
  • Хелпер для получения всех свойств элементов в выборке (описано ниже).
  • Только для ИБ 2.0. При особом желании можно и под первые адаптировать, но не было необходимости.
  • Read only. Создания и модификация элементов через эту обёртку невозможны, для этого есть замечательное классическое апи.
  • Доступ к элементам по правам - на совести конечного разработчика, обёртка не предлагает никакого функционала, но предоставляет все возможности для его реализации.
Обёртка состоит из трёх классов:
  • ElementTable - главный класс, наследуется от orm'ного DataManager. Через него происходит вся работа.
  • MultiplePropertyElementTable - служебная сущность, описывающая множественные свойства инфоблока.
  • SinglePropertyElementTable - аналогично предыдущему, только для единичных.
Все они находятся под рекомендуемым namespace Vendor\Module, в моём случае - \nav\IblockOrm. Кстати говоря, нагромождение папок в обёртке может показаться странным, но предполагается, что она будет находиться в /local примерно вот так:



Как использовать

Для начала зарегистрируем классы в автолоадере (можно и просто заинклудить):
\CModule::AddAutoloadClasses('', array(
    'nav\\IblockOrm\\MultiplePropertyElementTable' => '/local/lib/nav/IblockOrm/MultiplePropertyElementTable.php',
    'nav\\IblockOrm\\SinglePropertyElementTable' => '/local/lib/nav/IblockOrm/SinglePropertyElementTable.php',
    'nav\\IblockOrm\\ElementTable' => '/local/lib/nav/IblockOrm/ElementTable.php',
));

После чего объявляем свою сущность в соответствии с соглашением:
class MyIblockTable extends \nav\IblockOrm\ElementTable
{
    static protected $_iblockId = 1;
}

Либо же динамически создаём класс сущности без его явного объявления (как в HL):
$entity = \nav\IblockOrm\ElementTable::createEntity($iblockId);
$entityClass = $entity->getDataClass();

Всё! Теперь можно работать с методом MyIblockTable::getList() (или $entityClass::getList()) как с обычной ORM, например:
MyIblockTable::getList(array(
    'order' => array('PROPERTY_POPULAR' => 'ASC'),
    'filter' => array(
        '=ACTIVE' => 'Y',
        '!PROPERTY_NEW' => false,
    ),
));

Как видно, синтаксис запросов всё тот же. Основные моменты:
  • Свойства доступны по коду PROPERTY_CODE, где CODE - символьный код свойства (как в обычном гетлисте).
  • Если свойства есть в select, то их значения в результате будут находиться в тех же ключах, если не было задано алиасов (в апи инфоблоков, как помните, они находятся в PROPERTY_CODE_VALUE).
  • Поля при получении экранируются htmlspecialcharsbx(), оригинальные значения лежат в ключах с префиксом ~ (аналогично обычному гетлисту).
  • По умолчанию ORM выбирает только скалярные поля, поэтому для получения желаемых свойств их надо явно указывать в select. Либо использовать хелпер для получения всех свойств.
  • Для получения ссылки на элемент надо в select указать DETAIL_PAGE_URL. Это динамически вычисляемое поле, поэтому по умолчанию оно не заполняется.
  • Постраничная навигация стартует автоматически, если в выборке были указаны и limit, и offset.
  • При фильтре по множественному свойству элементы могут дублироваться. Чтобы этого избежать, надо добавить в select поле DISTINCT (см. комментарии к посту).
Что внутри
Главный класс - ElementTable - это наследник от DataManager с getMap() (метод, описывающий поля сущности), который считывает свойства и строит по ним поля-референсы PROPERTY_*. Референсы эти ведут на:
  • SinglePropertyElementTable для единичных свойств. На каждый инфоблок (читай класс) присутствует только один референс на SinglePropertyElementTable и он доступен по ключу PROPERTY. Поля референса - это символьные коды свойств. Таким образом, в гетлисте мы можем обращаться к свойствам по ключу PROPERTY.CODE. Предлагаемые же PROPERTY_CODE сделаны просто для удобства и совместимости с обычным апи инфоблоков и являются референсами на поля сущности SingleProperty.
  • MultiplePropertyElementTable. Поскольку у каждого множественного свойства своя таблица в БД, то и сущностей MultipleProperty создаётся на каждое свойство по одной. Сущности доступны по ключу PROPERTY_CODE_ENTITY. Так как таблицы множественных свойств имеют разные колонки в зависимости от типа свойства (VALUE, VALUE_ENUM, VALUE_NUM), не пренебрегайте фильтр через эту сущность для достижения наилучшей производительности (вообще при работе с любой orm-сущностью советую всегда мониторить генерируемый sql, встречаются подвохи). Для сущности элемента инфоблока PROPERTY_CODE является референсом PROPERTY_CODE_ENTITY.VALUE.

Постраничная навигация
Как выше уже упоминал, постраничка стартует автоматически, если указаны limit и offset в запросе. Поскольку в ORM такое понятие отсутствует, пришлось немного извратиться. Объект \Main\DB\Result (результат работы getList()) дополняется свойством oldCDBResult - старым CDBResult, который содержит стандартную информацию о текущей странице, количестве элементов и т.п, и используется для получения кода постранички. С первого раза, полагаю, понять сложно, поэтому вот пример кодом "было/стало".

Так выглядит классическая постраничка в news.list:
$rsElement = CIBlockElement::GetList(...);
...
$arResult["NAV_STRING"] = $rsElement->GetPageNavStringEx($navComponentObject, $arParams["PAGER_TITLE"], $arParams["PAGER_TEMPLATE"], $arParams["PAGER_SHOW_ALWAYS"]);
$arResult["NAV_CACHED_DATA"] = $navComponentObject->GetTemplateCachedData();
$arResult["NAV_RESULT"] = $rsElement;

А так выглядит она же, но при выборке через обёртку:
$rsElement = MyIblockTable::getList(...);
...
$arResult['NAV_STRING'] = $rsElement->oldCDBResult->GetPageNavStringEx($navComponentObject, $arParams["PAGER_TITLE"], $arParams["PAGER_TEMPLATE"], $arParams["PAGER_SHOW_ALWAYS"]);
$arResult['NAV_CACHED_DATA'] = $navComponentObject->GetTemplateCachedData();
$arResult['NAV_RESULT'] = $rsElement->oldCDBResult;

Как видно, поменялось "практически ничего".

Хелпер для получения всех свойств
Чтобы получить свойства элемента, можно в явном виде перечислить их в select. Но если их много или же нужна дополнительная информация о них, можно получить сразу все свойства для всех элементов выборки с помощью вспомогательного метода:
$rs = MyIblockTable::getList(...);
$arItems = MyIblockTable::fetchAllWithProperties($rs);

Внутри используется магия \CIBlockElement::GetPropertyValuesArray() - та же, что применяется в catalog.section для существенного сокращения количества запросов к БД по сравнению с _CIBlockElement::GetProperties().

Хелпер для получения Query
Порой при отладке требуется залезть напрямую в объект запроса, но стандартный датаменеджер сразу возвращает объект результата выборки Bitrix\Main\Db\Result. Поэтому был добавлен ещё один метод, являющийся полной копией DataManager::getList(), но возвращающий Bitrix\Main\Entity\Query вместо Result:
$query = MyIblockTable::getQuery(...)

Пример компонента
Если вы смогли дочитать до этого места, то наверняка уже заглянули на гитхаб и увидели там компонент. Это простой пример компонента, использующий для вывода списка элементов orm-обёртку. Изначально это был компонент в рабочем проекте, из которого я выпилил всю лишнюю логику, но не стал менять общей структуры, поэтому он может выглядеть монструозно. Основная логика находится в методах getItems() и formatResult(). А ещё в качестве бонуса там остался закомментированный код для реализации постранички со своей переменной/чпу.

Вместо тысячи слов
Просто для демонстрации - пример скомпанованного запроса с фильтром по свойствам инфоблока и сторонней таблице и сортировкой по агрегированному полю сторонней таблицы. Быстро, просто и безболезненно.
$this->rs = \nav\PlaceTable::getList(array(
    'select' => array('ID', 'NAME', 'DETAIL_PICTURE', 'PREVIEW_TEXT', 'DETAIL_TEXT', 'DETAIL_PAGE_URL', 'PRICE_SORT'),
    'order' => array('PRICE_SORT' => 'DESC'),
    'filter' => array(
        '=ACTIVE' => 'Y',
        '=WF_STATUS_ID' => 1,
        'WF_PARENT_ELEMENT_ID' => false,
        '><\\nav\\PriceDayTable:PLACE.PRICE' => array(12000, 32100),
        '\\nav\\PriceDayTable:PLACE.DATE' => new \Bitrix\Main\Type\Date('01/06/15', 'd/m/y'),
        '=PROPERTY_TYPE' => array(208, 209, 5302),
    ),
    'group' => array('ID'),
    'limit' => $arNavigation['SIZEN'],
    'offset' => $arNavigation['SIZEN'] * ($arNavigation['PAGEN'] - 1),
    'runtime' => array(
        'PRICE_SORT' => array(
            'expression' => array('MIN(%s)', '\nav\PriceDayTable:PLACE.PRICE')
        ),
    ),
));

$this->items = \nav\PlaceTable::fetchAllWithProperties($this->rs);




Назад в раздел

Подписаться на новые материалы раздела:












CAPTCHA