Перейти к содержимому

Запись в инфоблок через ORM - побочные эффекты старого ядра

Разбираем, что платформа делает при записи элемента старым ядром и чего не делает объектная запись. Дальше смотрим, какие вызовы старого ядра приходится дописывать руками сразу после сохранения объекта.

Механика

Запись элемента инфоблока - это не одна строка в таблице, а цепочка связанных изменений. Старое ядро выполняет их молча внутри одного вызова, и разработчик обычно их даже не замечает. Объектный слой пишет ровно то, что описано в карте сущности инфоблока.

Базовая сущность \Bitrix\Iblock\ElementTable запись не поддерживает вовсе, ни в одном из трёх её видов. Методы добавления, обновления и удаления в ядре переопределены и возвращают ошибку с советом вызвать старое ядро.

Писать объектом можно только через скомпилированную сущность конкретного инфоблока, а не через общую. Она собирается при заполненном коде для программного интерфейса, а её объект заводят фабричным методом, а не оператором new.

Поисковый индекс после объектной записи не обновляется ни при добавлении, ни при удалении. Переиндексацию элемента запускают вызовом \CIBlockElement::UpdateSearch($id) сразу после сохранения объекта или после его удаления.

Фасетный индекс умного фильтра тоже живёт своей жизнью и сам не пересчитывается. Его обновляют вызовом PropertyIndex\Manager по конкретному элементу, иначе товар пропадает из выдачи фильтра при живой карточке.

Разделы хранятся вложенными множествами, и границы дерева считает только старое ядро. Поля LEFT_MARGIN, RIGHT_MARGIN, DEPTH_LEVEL и GLOBAL_ACTIVE при объектной записи остаются прежними. Удаление раздела объектом рвёт дерево: подразделы и элементы теряют привязку и остаются в поиске.

Картинки при объектной записи не ресайзятся и сохраняются в исходном размере. Уменьшенные копии создают вызовом \CFile::ResizeImage() сразу после \CFile::SaveFile(), отдельным шагом того же скрипта.

События старого ядра в объектный механизм не проброшены, и обратный проброс тоже отсутствует. Через ORM\EventManager подписка идёт только на ORM-сущность, а наследование сущностей инфоблока доступно с версии модуля iblock 21.500.0.

SEO-шаблоны мета-полей объектом не задаются: поле IPROPERTY_TEMPLATES объектная запись не поддерживает. Шаблоны задают через CIBlockElement::Add() при создании элемента либо отдельным вызовом InheritedProperty\ElementTemplates::set() у уже сохранённого элемента.

Шаги

  1. Заполнить инфоблоку код для программного интерфейса и символьные коды всем нужным свойствам.
  2. Перечислить побочные эффекты, которые задача реально задевает: поиск, фасеты, картинки, дерево, SEO.
  3. Писать элемент объектом скомпилированной сущности, а не базовой таблицей элементов инфоблока.
  4. Дописать вызовы старого ядра сразу после сохранения: переиндексацию поиска и обновление фасетного индекса.
  5. Правку разделов и SEO-шаблоны отдать старому ядру целиком, объекту оставить обычные поля.

Код

Проверяем, что базовая сущность не пишет:

\Bitrix\Main\Loader::includeModule('iblock');
$result = \Bitrix\Iblock\ElementTable::add(['IBLOCK_ID' => $iblockId, 'NAME' => 'Тест']);
print_r($result->getErrorMessages());
// «Для добавления элементов инфоблоков используйте вызов CIBlockElement::Add()»
// add(), update() и delete() базовой сущности переопределены в самом ядре
// причина - свойства лежат в отдельных таблицах и в двух вариантах хранения

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

Создаём элемент объектом скомпилированной сущности:

$elementClass = \Bitrix\Iblock\Iblock::wakeUp($iblockId)->getEntityDataClass();
$element = $elementClass::createObject() // new вместо фабрики даёт ошибку про первичный ключ
->setName('Новость дня')
->setCode('novost-dnya')
->set('AUTHOR', 'Редакция'); // свойство по символьному коду
$saveResult = $element->save(); // пишет элемент и его свойства одним вызовом
if (!$saveResult->isSuccess()) {
print_r($saveResult->getErrorMessages());
}

Дальше идут вызовы, которые старое ядро сделало бы само. Их порядок значения не имеет, но выполнить надо все, иначе элемент окажется наполовину записанным.

Дописываем переиндексацию поиска после сохранения:

\CIBlockElement::UpdateSearch($element->getId()); // и после save(), и после delete()
// вторым аргументом true запись поискового индекса перезаписывается целиком
// без вызова новый элемент не появится в поиске по сайту
// удалённый объектом элемент останется в выдаче со старым заголовком и адресом

Обновляем фасетный индекс умного фильтра:

use Bitrix\Iblock\PropertyIndex;
PropertyIndex\Manager::updateElementIndex($iblockId, $element->getId());
PropertyIndex\Manager::deleteElementIndex($iblockId, $oldElementId); // после удаления
PropertyIndex\Manager::markAsInvalid($iblockId); // после правки состава свойств

Индекс не пересчитывается сам и при переносе разделов, и при выгрузке с новыми свойствами. Пометка индекса невалидным обходится дешевле поэлементного обновления, когда правок за один проход много.

Сохраняем картинку и ресайзим её вручную:

use Bitrix\Iblock\ORM\PropertyValue;
$fileId = \CFile::SaveFile(\CFile::MakeFileArray($path), 'iblock');
\CFile::ResizeImage($fileId, ['width' => 300, 'height' => 300], BX_RESIZE_IMAGE_EXACT, true);
$element->set('PHOTO', new PropertyValue($fileId, 'Главное фото')); // объект, а не число
$element->addTo('GALLERY', new PropertyValue($otherId, 'Доп. снимок')); // множественное свойство
$element->save();

Файловое свойство заполняется объектом значения с описанием, а не голым идентификатором файла. Переданное голое число тоже сохранится, но описание к картинке при этом теряется без предупреждения.

Удаляем раздел вызовом старого ядра:

\CIBlockSection::Delete($sectionId); // рекурсивно: подразделы, элементы, кеши, индексы
// объектное удаление снимет одну строку и оставит подразделы без родителя
// $section->delete() границы вложенных множеств заново не пересчитывает
// пересчёт границ дерева и признака активности делает только классическое API

Задаём SEO-шаблоны отдельным механизмом:

use Bitrix\Iblock\InheritedProperty;
$templates = new InheritedProperty\ElementTemplates($iblockId, $element->getId());
$templates->set(['ELEMENT_META_TITLE' => '{=this.NAME}']); // объектом это поле не задать
$values = new InheritedProperty\ElementValues($iblockId, $element->getId());
print_r($values->getValues()); // вычисленные значения с наследованием
(new InheritedProperty\IblockValues($iblockId))->clearValues();

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

Подписываемся на событие объектной сущности:

$iblock = \Bitrix\Iblock\Iblock::wakeUp($iblockId);
\Bitrix\Main\ORM\EventManager::getInstance()->registerEventHandler(
$iblock->getEntityDataClass(),
\Bitrix\Main\ORM\Data\DataManager::EVENT_ON_BEFORE_ADD,
'mymodule', 'MyClass', 'method'
);
// OnBeforeIBlockElementAdd старого ядра сюда не приходит, и наоборот тоже
// наследование ORM-сущностей инфоблока доступно с модуля iblock 21.500.0

Ограничения

Массовый импорт объектной записью не выигрывает ничего. Единственный полный путь добавления элемента - CIBlockElement::Add(), и на больших объёмах его вызывают с отключённой переиндексацией поиска.

Переопределить поведение сущности инфоблока наследованием собственного класса получится не на каждой версии модуля. Наследование ORM-сущностей инфоблока доступно с версии модуля iblock 21.500.0, а на более старых сборках этот путь закрыт.

Свойство попадает в карту сущности только с заполненным символьным кодом, набранным латиницей. Без кода объект его не видит совсем, и записывать такое свойство приходится старым ядром.

Обработчики старого ядра о записи через объект не узнают вообще, ни до неё, ни после. Модули и решения, которые ждут события инфоблока, при объектной записи просто не срабатывают и своих действий не выполняют.

Правку структуры разделов объектному слою не отдают вовсе, даже ради единообразия кода. Перенос, слияние и удаление раздела задевают границы дерева, а считает их только классическое API.

Типичные проблемы

Запись падает с советом вызвать CIBlockElement::Add().

Вызов идёт в базовую сущность элементов, у которой методы записи в ядре закрыты намеренно. Писать объектом можно только через скомпилированную сущность своего инфоблока с заполненным кодом интерфейса.

Ошибка о требуемом первичном ключе при сохранении нового элемента.

Объект создан оператором new вместо фабричного метода, который заводит новую запись сущности. Прямое создание объекта считает элемент уже существующим и ищет у него первичный ключ.

Запись значения падает на пустом свойстве элемента.

Чтение пустого свойства возвращает пустоту, и вызвать у неё запись значения не получается. Значение пишут прямой установкой по символьному коду, минуя чтение объекта значения.

После удаления объектом элемент остаётся в поиске по сайту.

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

Товар записан объектом, а из умного фильтра пропал.

Фасетный индекс после объектной записи не пересчитывается и хранит прежние значения свойств. Индекс обновляют вручную по элементу либо помечают невалидным целиком для инфоблока.

Удалили раздел через объект - подразделы осиротели.

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

Частые вопросы

Почему не работает запись через ElementTable?

Методы добавления, обновления и удаления у базовой сущности элементов переопределены в ядре и возвращают ошибку. Причина архитектурная: элемент живёт не только строкой таблицы, но и свойствами, индексами и кешами.

Как импортировать в инфоблок несколько тысяч элементов?

Через CIBlockElement::Add() порциями, с отключённой переиндексацией поиска на каждом шаге. Аналога пакетной записи у объектного слоя нет, а поиск переиндексируют одним проходом в конце.

Как изменить SEO-свойства элемента через API?

Объектная запись поле шаблонов мета-тегов не поддерживает. Шаблоны задают через InheritedProperty\ElementTemplates::set() либо передают массивом шаблонов в вызов старого ядра.

Почему картинка сохранилась в оригинальном размере?

Объектная запись изображение не уменьшает: ресайза в ней просто нет. Уменьшенную копию создают вызовом CFile::ResizeImage() сразу после сохранения файла.

Почему не срабатывает обработчик OnAfterIBlockElementAdd?

События старого ядра инфоблоков в объектный механизм событий не проброшены. Через ORM\EventManager подписка идёт только на ORM-сущность, а наследование сущностей инфоблока доступно с iblock 21.500.0.

Смежное

Первоисточник