Запись в инфоблок через 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() у уже сохранённого элемента.
Шаги
- Заполнить инфоблоку код для программного интерфейса и символьные коды всем нужным свойствам.
- Перечислить побочные эффекты, которые задача реально задевает: поиск, фасеты, картинки, дерево, SEO.
- Писать элемент объектом скомпилированной сущности, а не базовой таблицей элементов инфоблока.
- Дописать вызовы старого ядра сразу после сохранения: переиндексацию поиска и обновление фасетного индекса.
- Правку разделов и 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.
Смежное
- Выборки из инфоблоков - оглавление подтемы
- Инфоблоки - устройство хранилища целиком
- Инфоблок через ORM: API_CODE, класс элементов, свойства - как получить класс сущности
- Выборки из инфоблоков: GetList, ORM и разделы - те же два API на чтении
- Значение свойства не сохраняется: разбор причин - когда значение не доходит до базы
- Правка структуры разделов: перенос, слияние, удаление - дерево разделов и его границы
- Умный фильтр: настройка, свойства и адреса фильтрации - зачем нужен фасетный индекс
- Поиск не находит товары: индекс, переиндексация, выдача - что делает поисковый индекс
- Заголовки и метатеги каталога: шаблоны для разделов и товаров - куда попадают SEO-шаблоны
- Обработчик события: регистрация, аргументы, отмена действия - как подписываются на события ядра
- Объекты ORM: выборка объектами, ленивая загрузка, сохранение - тот же стиль на своих таблицах
- Массовая правка товаров: обновление свойств скриптом - запись на больших объёмах
- Картинки товара: загрузка, размеры и вывод галереи - ресайз и размеры изображений