jiscariot / iblock-element-reader
Bitrix iblock element reader: load fields and properties from DB with batch resolve pipeline
Package info
github.com/JIscariot/iblock-element-reader
pkg:composer/jiscariot/iblock-element-reader
Requires
- php: >=8.2
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-29 14:58:42 UTC
README
Bitrix iblock element reader: загрузка полей и свойств из БД и batch-резолв в типизированные значения.
Установка
composer require jiscariot/iblock-element-reader:^0.2
Требования: PHP 8.2+, Bitrix CMS с модулем iblock (классы Bitrix\Iblock\*, Bitrix\Main\*).
IblockElementReader → IblockElementFetcher → ElementLoader → ResolvePipeline → IblockElementCollection
↓
Repository/*
Класс immutable: каждый filter / withFields / withProps / withResolvers возвращает клон.
Reader не ходит в b_iblock: scope — через filter(iblock: ...) или IBLOCK_ID из загруженных элементов; storage mode — VERSION из b_iblock_property.
Быстрый старт
use IblockElementReader\IblockElementReader; $elements = (new IblockElementReader()) ->filter(id: [10, 20, 30], iblock: [5]) ->withFields(['ID', 'NAME', 'PREVIEW_PICTURE.VALUE']) ->withProps(['MAKE_ID.VALUE', 'PRICE.VALUE', 'VIN']) ->get(); foreach ($elements->elements as $element) { $element->id; // int $element->name; // string|null $element->value('MAKE_ID'); // resolved первого значения, при null — raw $element->value('PRICE'); // int|float|null $element->property('VIN')?->first()?->raw; // сырое значение из БД } $elements->elements[0] ?? null; // первый элемент $elements->elements; // IblockElement[] // один элемент $element = (new IblockElementReader()) ->filter(id: 10, iblock: 5) ->withFields(['*']) ->first();
filter — scope элементов и инфоблоков
// id + iblock ->filter(id: [215674], iblock: [33]) // только id — iblock из IBLOCK_ID в row ->filter(id: [215674, 218572]) // все элементы iblock ->filter(iblock: [33]) // несколько iblock ->filter(id: [100, 200], iblock: [5, 10]) // ошибка: scope не определён (new IblockElementReader())->get();
iblock |
id |
Поведение |
|---|---|---|
[5] |
[10, 20] |
элементы iblock 5 с указанными id |
[5, 10] |
[1, 2, 3] |
элементы из iblock 5 и 10 по id |
[5, 10] |
не задан / null |
все элементы iblock 5 и 10 |
| не задан | [10, 20] |
загрузка по id, iblock из row (один или несколько) |
| любой | [] |
пустой результат, запросов к БД нет |
| не задан | не задан / null |
exception |
id: null и id: [] — разные вещи
id: nullили аргумент не передан — фильтра по ID нет: загружаются все элементы инфоблока изfilter(iblock: ...).id: []— заведомо пустая выборка:get()возвращает пустую коллекцию,first()—null, к БД не уходит ни одного запроса. Остальные условия фильтра на это не влияют.
Поэтому список ID из поиска или выборки можно передавать как есть, без проверки if ($ids === []) return [];:
$ids = $search->findIds($query); // может оказаться [] $elements = (new IblockElementReader()) ->filter(id: $ids, iblock: [5]) ->withProps(['PRICE.VALUE']) ->get(); // при $ids === [] — пустая коллекция, без запросов
ID нормализуются: значения <= 0 и дубликаты отбрасываются. Если валидных ID не осталось (например, [0]), это тоже пустая выборка. ID, которых нет в базе, запрашиваются обычным образом и дают пустой результат.
Исключение для id: [] не бросается намеренно: пустой список ID — нормальная ситуация, а не ошибка вызывающего кода.
Повторный filter() меняет только переданные аргументы:
->filter(id: [10, 20]) ->filter(iblock: [5]) // id остаётся [10, 20]
Несколько инфоблоков
$elements = (new IblockElementReader()) ->filter(id: [100, 200, 300], iblock: [5, 10]) ->withProps(['NAME.VALUE', 'ADDRESS.VALUE']) // код должен существовать в каждом iblock ->get(); foreach ($elements->elements as $element) { $element->iblockId; // 5 или 10 }
Главное правило: суффикс .VALUE
Без .VALUE |
С .VALUE |
|
|---|---|---|
| withFields | сырое значение (id файла, строка) | File для PREVIEW_PICTURE / DETAIL_PICTURE |
| withProps | сырое значение из prop_s |
авто-резолв по типу свойства |
Исключение — простые S и N без USER_TYPE: у них resolved заполнен всегда, с .VALUE и без (см. Простые типы S и N).
Примеры:
// id файла ->withFields(['PREVIEW_PICTURE']) // объект File { id, subdir, filename, src, size, width, height } ->withFields(['PREVIEW_PICTURE.VALUE']) // id связанного элемента ->withProps(['STORE']) // IblockElement связанного элемента (поля из withFields родительского запроса) ->withProps(['STORE.VALUE']) // id enum-значения ->withProps(['STATUS']) // Enumerate { id, value, xmlId } ->withProps(['STATUS.VALUE']) // id раздела ->withProps(['SECTION_LINK']) // Section { id, iblockId, name, code, sort, parentId, isActive } ->withProps(['SECTION_LINK.VALUE'])
withFields
// withoutFields — в SELECT только ID (withFields не вызывался) (new IblockElementReader())->filter(id: [10])->withProps([...])->get(); // все поля по умолчанию ->withFields(['*']) // явный список ->withFields(['ID', 'NAME', 'PREVIEW_PICTURE.VALUE'])
| Вызов | SELECT |
|---|---|
без withFields |
ID (+ IBLOCK_ID служебно) |
withFields(['*']) |
ID, NAME, CODE, ACTIVE, SORT, PREVIEW_PICTURE, DETAIL_PICTURE, PREVIEW_TEXT, DETAIL_TEXT |
withFields([]) |
только ID |
| явный список | указанные поля + ID |
ID добавляется в SELECT автоматически, если не указан явно.
withProps
// свойства не загружаются ($props = null) (new IblockElementReader())->filter(iblock: [5])->get(); // все свойства с авто-резолвом (.VALUE для каждого кода) ->withProps([]) ->withProps(['*.VALUE']) // все свойства как есть (сырые строки / id) ->withProps(['*']) // явный список ->withProps(['MAKE_ID.VALUE', 'PRICE', 'PHOTOS.VALUE'])
| Вызов | Что происходит |
|---|---|
без withProps |
свойства не читаются из БД |
withProps([]) |
все коды, каждый с .VALUE |
withProps(['*']) |
все коды, без резолва |
withProps(['*.VALUE']) |
все коды с резолвом + можно добавить явные пути |
Авто-резолв свойств (без кастомных резолверов)
При .VALUE pipeline выбирает стратегию в таком порядке:
- USER_TYPE resolver — встроенный (
Date,DateTime) или изwithResolvers - Nested E — если путь вида
STORE.VALUE.ADDRESS.VALUE - Тип свойства (PROPERTY_TYPE):
E→ связанныйIblockElementF→FileL→EnumerateN→int/floatG→SectionS→ как есть
Простые типы S и N
Для свойств S и N без USER_TYPE resolved заполняется всегда — независимо от .VALUE, * / *.VALUE и того, на каком уровне вложенности загружено свойство (в том числе у связанных элементов E). Писать resolved ?? raw не нужно.
| Тип | resolved |
|---|---|
S |
raw как есть |
N |
int / float / null через NumberValueParser |
NumberValueParser:
- обрезает пробелы по краям, включая неразрывные;
- убирает пробелы-разделители тысяч:
"1 500"→1500; - заменяет десятичную запятую точкой:
"1,6"→1.6; - целое →
int(в том числе"15000.0000"из числовых колонок), дробное →float; - пустое или нечисловое значение →
null, без исключений и предупреждений.
raw |
resolved |
|---|---|
"1,6" |
1.6 |
"1 500" |
1500 |
" 42 " |
42 |
"0" |
0 |
"abc" |
null |
"" |
null |
Свойства с USER_TYPE (встроенные Date / DateTime, кастомные из withResolvers, любые другие) и свойства, которые забрал SupportsPropertyResolver, это правило не затрагивает: для них resolved по-прежнему заполняется только с .VALUE.
Встроенные USER_TYPE:
| USER_TYPE | Результат |
|---|---|
Date |
DateTimeImmutable, время всегда 00:00:00 |
DateTime |
DateTimeImmutable |
Даты
DateTimeValueParser принимает:
| Значение | Пример |
|---|---|
| формат БД | 2024-03-15 14:30:00, 2024-03-15 14:30, 2024-03-15, 2024-03-15T14:30:00 |
| формат сайта | 15.03.2024 14:30:00, 15.03.2024 14:30, 15.03.2024 |
| объект | Bitrix\Main\Type\Date, Bitrix\Main\Type\DateTime, \DateTimeInterface |
- Строки разбираются в часовом поясе по умолчанию — в Bitrix это пояс сайта; время не сдвигается. Объекты
DateTimeсохраняют своё время и пояс. - Пустое или некорректное значение →
null, без исключений и предупреждений. Некорректное — это и несуществующие даты (31.02.2024,2024-13-01,25:00), и двузначный год, и относительные выражения вродеtomorrow.
Даты из обычных строковых свойств (S без USER_TYPE) можно разобрать тем же кодом:
use IblockElementReader\Resolve\BuiltIn\DateTimeValueParser; $raw = $element->value('DELIVERY_DATE'); // "15.03.2024" DateTimeValueParser::parseValue($raw); // DateTimeImmutable 2024-03-15 00:00:00 или null DateTimeValueParser::parseValue($raw, dateOnly: true); // время обнуляется, как у USER_TYPE Date
withResolvers — кастомные резолверы
Для свойств с нестандартным b_iblock_property.USER_TYPE (например DependentSelect, booleanValue) передайте объекты, реализующие интерфейс UserTypePropertyResolver. Если нужно выбирать свойства не только по USER_TYPE (по коду, типу, LINK_IBLOCK_ID) или резолвить свойства без USER_TYPE — реализуйте SupportsPropertyResolver (см. ниже).
use IblockElementReader\Resolve\Contract\UserTypePropertyResolver; use IblockElementReader\Resolve\PropertyResolveContext; final class BooleanValuePropertyResolver implements UserTypePropertyResolver { public function userType(): string { return 'booleanValue'; } public function resolve(PropertyResolveContext $context): array { return [ '0' => false, '1' => true, '' => false, 'null' => false, ]; } }
Подключение:
$elements = (new IblockElementReader()) ->filter(id: [...], iblock: [5]) ->withProps(['IS_ACTIVE.VALUE', 'MAKE_ID.VALUE']) ->withResolvers([ new BooleanValuePropertyResolver(), new DependentSelectPropertyResolver($makeRepo, $modelRepo), ]) ->get();
Контракт резолвера
userType(): string — точное значение b_iblock_property.USER_TYPE. Регистр и строка должны совпадать с БД.
resolve(PropertyResolveContext $context): array — один batch-вызов на тип за весь get().
PropertyResolveContext:
| Поле | Описание |
|---|---|
$definition |
метаданные свойства (code, type, userType, linkIblockId, …) |
$uniqueValues |
уникальные сырые значения по всем элементам запроса |
$uniqueDescriptions |
уникальные description (для multiple) |
$byElement |
elementId => value или elementId => list<value> |
Возвращаемый map: rawValue => resolved. Ключи должны совпадать с тем, что лежит в VALUE свойства (строка "123", не int 123, если так хранит Bitrix).
Пустые single-свойства попадают в batch как null (в $uniqueValues и $byElement). В PHP ключи null и '' в map — одно и то же; достаточно '' => false.
Резолвер срабатывает только если свойство запрошено с .VALUE.
Кастомный резолвер переопределяет встроенный для того же USER_TYPE (например свой Date вместо стандартного парсера).
Два резолвера UserTypePropertyResolver с одинаковым userType() в одном withResolvers() — ошибка: get() бросит LogicException с именами обоих классов (раньше второй молча заменял первый). Чтобы обрабатывать свойства одного USER_TYPE разными резолверами, используйте SupportsPropertyResolver.
SupportsPropertyResolver — выбор свойств по определению
SupportsPropertyResolver расширяет UserTypePropertyResolver методом supports(). Резолвер сам решает, какие свойства обрабатывает, по любым полям IblockPropertyDefinitionRecord: code, type, userType, linkIblockId, multiple, withDescription.
Например, свойства MAKE_ID и MODEL_ID с одним USER_TYPE DependentSelect ссылаются на разные справочники — каждому свой резолвер:
use IblockElementReader\Model\Record\IblockPropertyDefinitionRecord; use IblockElementReader\Resolve\Contract\SupportsPropertyResolver; use IblockElementReader\Resolve\PropertyResolveContext; final class MakePropertyResolver implements SupportsPropertyResolver { public function __construct(private readonly MakeRepository $makes) { } public function supports(IblockPropertyDefinitionRecord $definition): bool { return $definition->code === 'MAKE_ID'; } public function userType(): string { return 'DependentSelect'; // только подсказка } public function resolve(PropertyResolveContext $context): array { return $this->makes->namesByIds($context->uniqueValues); // ['10' => 'Volkswagen', ...] } } $elements = (new IblockElementReader()) ->filter(iblock: [5]) ->withProps(['MAKE_ID.VALUE', 'MODEL_ID.VALUE']) ->withResolvers([ new MakePropertyResolver($makeRepo), new ModelPropertyResolver($modelRepo), // supports(): code === 'MODEL_ID' ]) ->get();
Свойство без USER_TYPE тоже можно забрать своим резолвером — например, привязку E к справочнику, который не нужно загружать как IblockElement:
public function supports(IblockPropertyDefinitionRecord $definition): bool { return $definition->type === PropertyTypeEnum::ELEMENT && $definition->linkIblockId === 12; } public function userType(): string { return ''; // свойство без USER_TYPE }
Порядок выбора резолвера для свойства:
- первый
SupportsPropertyResolverв порядке регистрации, у которогоsupports()вернулtrue; UserTypePropertyResolverпоuserType();- встроенные (
Date,DateTime); - ничего не подошло — авто-резолв по типу свойства (
E,F,L,N, …).
supports()вызывается один раз на свойство заget(), а не на каждое значение.userType()уSupportsPropertyResolver— только подсказка; пустая строка допустима. Проверка на дубликатыuserType()к таким резолверам не применяется.- Резолвер, как и раньше, срабатывает только для свойств с
.VALUE. Если он забрал простоеS/NбезUSER_TYPE, правило «resolvedвсегда» для этого свойства не действует: без.VALUEresolved === null. - Если резолвер забрал свойство
E, связанный элемент не загружается, и вложенные пути (STORE.VALUE.ADDRESS.VALUE) для этого свойства не применяются.
Вложенные свойства (E → другой инфоблок)
Путь через точку после .VALUE родительского свойства типа E:
->withProps([ 'STORE.VALUE.ADDRESS.VALUE', 'STORE.VALUE.PHONES.VALUE', 'STORE.VALUE.NAME', // поле связанного элемента без резолва ])
STORE.VALUE— связанный элемент какIblockElement(поля изwithFieldsродительского запроса)STORE.VALUE.ADDRESS.VALUE— свойствоADDRESSвнутри связанного инфоблока, с резолвом- Связанный элемент (E) наследует
withFieldsверхнего запроса (['*'],['NAME'], …); nested props добавляются отдельно (LABEL.VALUE.ICON) - Максимальная глубина пути — 8 сегментов
Повторные ссылки на один и тот же элемент кешируются (LinkedElementCache).
Описания значений (WITH_DESCRIPTION = Y) у вложенных свойств работают так же, как у свойств самого элемента:
->withProps(['STORE.VALUE.GALLERY.VALUE']) $store = $element->property('STORE')?->first()?->resolved; // ?IblockElement $gallery = $store?->property('GALLERY'); foreach ($gallery?->values() ?? [] as $photo) { $photo->raw; // значение $photo->description; // DESCRIPTION; null, если у свойства нет WITH_DESCRIPTION } $gallery?->descriptions(); // ['Вид сзади', 'Вид спереди', …] — в порядке значений
Значения множественного свойства идут в том порядке, в каком хранятся в БД (по ID значения) — и на верхнем уровне, и во вложенных.
Результат
get() возвращает IblockElementCollection — типизированную domain-модель. Pipeline резолвит значения и сразу собирает IblockElement.
Scope проверяется в IblockElementReader: пустой filter — exception; несовпадение iblock у загруженных элементов — exception после fetch.
IblockElement
$element->id; $element->iblockId; $element->name; $element->code; $element->sort; $element->previewPicture; // null|int|File — int без .VALUE, File с PREVIEW_PICTURE.VALUE $element->detailPicture; // null|int|File $element->isActive; $element->createdAt; // ?DateTimeInterface $element->iblockSectionId; // ?int — основной раздел (IBLOCK_SECTION_ID), null — без раздела $element->properties; // array<string, IblockProperty> ключ = CODE свойства // все запрошенные свойства, в т.ч. с пустым value => null $element->property('MAKE_ID'); // ?IblockProperty — null, если свойство не запрашивали $element->value('MAKE_ID'); // = property('MAKE_ID')?->resolved()
Раздел элемента
iblockSectionId заполняется всегда — IBLOCK_SECTION_ID выбирается тем же запросом, что и остальные поля, даже без withFields. Это основной раздел: если элемент привязан к нескольким разделам, здесь будет тот, что указан основным. У элементов, загруженных через свойство E, поле тоже заполнено:
$store = $element->property('STORE')?->first()?->resolved; // ?IblockElement $store?->iblockSectionId; // раздел связанного элемента
Загрузить сам раздел (Section) путём в withProps пока нельзя — это возможное развитие: например, служебный путь вида STORE.VALUE.IBLOCK_SECTION_ID.VALUE, который предзагружал бы разделы связанных элементов тем же batch-запросом, что и для свойств G. Сейчас разделы нескольких элементов можно получить одним запросом так:
use IblockElementReader\Repository\SectionRepository; $sectionIds = array_map(static fn($e) => $e->iblockSectionId, $elements->elements); $sections = (new SectionRepository())->findByIds($sectionIds); // array<int, Section>, один запрос $sections[$element->iblockSectionId ?? 0] ?? null;
IblockProperty / IblockPropertyValue
$property->definition; // IblockPropertyDefinitionRecord $property->value; // IblockPropertyValue | IblockPropertyValue[] | null $value->raw; // сырое значение из БД $value->resolved; // после .VALUE или null, если .VALUE не запрашивали; у S/N без USER_TYPE — всегда $value->description;
$property->value у одиночного свойства — объект, у множественного — массив. Чтобы не проверять is_array(), читайте через методы — они одинаково работают для обоих случаев:
| Метод | Результат | Одиночное '7' → Enumerate |
Множественное ['a', '', 'b'] |
Пустое |
|---|---|---|---|---|
values() |
list<IblockPropertyValue> |
[value] |
[v1, v2, v3] |
[] |
first() |
?IblockPropertyValue |
value |
v1 |
null |
resolved() |
resolved первого значения, при null — raw |
Enumerate |
'a' |
null |
resolvedList() |
resolved (при null — raw) всех значений без null и '' |
[Enumerate] |
['a', 'b'] |
[] |
descriptions() |
list<?string> в порядке values() |
[null] |
[d1, d2, d3] |
[] |
$element->value('PRICE'); // 1500 $element->property('TAGS')?->resolvedList() ?? []; $element->property('PHOTOS')?->resolvedList(); // [File, File, …]
resolved() и value() подставляют raw, если resolved === null. Для L, F, E, G это значит: если enum, файл, связанный элемент или раздел не найден, вернётся его ID, а не null. Когда нужен строго объект, читайте resolved напрямую:
$element->property('STATUS')?->first()?->resolved?->value; // Enumerate|null → 'Новый' $element->property('STORE')?->first()?->resolved; // IblockElement|null
Методы только читают данные. resolvedList() отбрасывает пустые значения, а descriptions() — нет, поэтому длины списков могут различаться; для пар «значение + описание» используйте values().
Для свойства типа E с .VALUE в resolved — вложенный IblockElement.
Section — привязка к разделу (G)
Для свойства типа G с .VALUE в resolved — объект Section:
$elements = (new IblockElementReader()) ->filter(iblock: [5]) ->withProps(['SECTION_LINK.VALUE']) ->get(); foreach ($elements->elements as $element) { $section = $element->property('SECTION_LINK')?->first()?->resolved; // Section|null $section?->name; // ?string $section?->code; // ?string $section?->id; // int $section?->iblockId; // int $section?->sort; // ?int $section?->parentId; // ?int — IBLOCK_SECTION_ID, null у корневого раздела $section?->isActive; // ?bool }
- Множественное свойство — массив
IblockPropertyValue, у каждого свойSection. - Несуществующий или удалённый раздел —
resolved === null,raw(ID) сохраняется. - Разделы всех элементов и всех свойств
Gзагружаются одним запросом кSectionTableнаget(); один и тот же раздел у разных элементов — один и тот же объект. - Свойства
GсUSER_TYPE, для которых зарегистрирован кастомный резолвер, резолвит он.
IblockElementCollection
$elements->count(); $elements->elements; // IblockElement[]
Используйте first() на reader или $collection->elements[0].
Структура модуля
return null из резолвера — нужен массив. Пустой результат: return [].
Неверные ключи map — ключ должен быть raw value из свойства. Осторожно с array_column($rows, 'name', 'id'): в VALUE может лежать ID из другой таблицы, не id строки.
Забыли .VALUE — резолвер не вызовется, вернётся сырое значение.
withProps(['*.VALUE']) на большом инфоблоке — загружаются и резолвятся все свойства. Для API лучше явный список кодов.
USER_TYPE не совпадает — проверьте b_iblock_property.USER_TYPE в БД (DependentSelect, не dependentSelect).
Multiple-свойство — $byElement[$id] может быть массивом значений; map всё равно строится по uniqueValues.
Полный пример
Каталог автомобилей: марка и модель — свойства одного USER_TYPE DependentSelect со ссылками на разные справочники, категория — привязка к разделу (G), склад — привязка к элементу (E) с галереей, у фото есть описания.
use IblockElementReader\IblockElementReader; use IblockElementReader\Model\IblockElement; use IblockElementReader\Model\Record\IblockPropertyDefinitionRecord; use IblockElementReader\Model\Section; use IblockElementReader\Resolve\Contract\SupportsPropertyResolver; use IblockElementReader\Resolve\PropertyResolveContext; /** Один класс — два резолвера: каждый выбирает своё свойство по коду. */ final class DictionaryResolver implements SupportsPropertyResolver { /** * @param \Closure(list<mixed>): array<string, string> $load ID → название, одним запросом */ public function __construct( private readonly string $code, private readonly \Closure $load, ) { } public function supports(IblockPropertyDefinitionRecord $definition): bool { return $definition->code === $this->code; } public function userType(): string { return 'DependentSelect'; // подсказка } public function resolve(PropertyResolveContext $context): array { return ($this->load)($context->uniqueValues); } } final class CarCatalogReader { public function __construct( private readonly int $carsIblockId, private readonly MakeRepository $makes, private readonly ModelRepository $models, ) { } /** * @param int[] $ids может быть пустым — тогда пустой результат без запросов * @return list<array<string, mixed>> */ public function load(array $ids): array { $elements = (new IblockElementReader()) ->filter(id: $ids, iblock: $this->carsIblockId) ->withFields(['NAME', 'CODE', 'PREVIEW_PICTURE.VALUE']) ->withProps([ 'MAKE_ID.VALUE', 'MODEL_ID.VALUE', 'CATEGORY.VALUE', // G → Section 'PRICE', // N → int|float, и без .VALUE 'SOLD_AT.VALUE', // USER_TYPE Date → DateTimeImmutable 'TAGS', // множественное S 'STORE.VALUE', // E → IblockElement 'STORE.VALUE.GALLERY.VALUE', // множественное S с описаниями ]) ->withResolvers([ new DictionaryResolver('MAKE_ID', $this->makes->namesByIds(...)), new DictionaryResolver('MODEL_ID', $this->models->namesByIds(...)), ]) ->get(); return array_map($this->toArray(...), $elements->elements); } /** * @return array<string, mixed> */ private function toArray(IblockElement $car): array { $category = $car->property('CATEGORY')?->first()?->resolved; // Section|null $store = $car->property('STORE')?->first()?->resolved; // IblockElement|null $gallery = $store?->property('GALLERY'); return [ 'id' => $car->id, 'name' => $car->name, 'section' => $car->iblockSectionId, // основной раздел элемента 'make' => $car->value('MAKE_ID'), // 'Volkswagen' 'model' => $car->value('MODEL_ID'), // 'Golf' 'category' => $category instanceof Section ? $category->name : null, 'price' => $car->value('PRICE'), // 1500000 'soldAt' => $car->property('SOLD_AT')?->first()?->resolved?->format('d.m.Y'), 'tags' => $car->property('TAGS')?->resolvedList() ?? [], 'store' => $store?->name, 'photos' => array_map( static fn($photo) => ['src' => $photo->raw, 'title' => $photo->description], $gallery?->values() ?? [], ), ]; } }
Число запросов не зависит от числа элементов: на каждом уровне (сами автомобили, затем связанные склады) — по одному запросу на тип данных: элементы, значения свойств, разделы, файлы, плюс по одному вызову resolve() на каждый справочник.
value() подставляет raw, если resolved === null, поэтому для объектов (Section, IblockElement, DateTimeImmutable) в примере используется first()?->resolved — так не найденный раздел или некорректная дата дают null, а не исходную строку.
Структура пакета
src/
├── IblockElementReader.php fluent API (filter, withFields, withProps), scope validation
├── IblockElementFetcher.php оркестрация load → resolve
├── Model/ domain-модель
│ ├── IblockElement.php
│ ├── IblockElementCollection.php
│ ├── IblockProperty.php
│ ├── IblockPropertyValue.php
│ ├── Enumerate.php resolved L (список)
│ ├── Section.php resolved G (раздел)
│ ├── File.php resolved F (файл)
│ └── Record/ сырые данные из БД (load → resolve)
│ ├── IblockElementRecord.php
│ ├── IblockElementRecordCollection.php
│ ├── IblockPropertyDefinitionRecord.php
│ ├── IblockPropertyValueRecord.php
│ └── IblockPropertyValueRecordCollection.php
├── Load/ orchestration (specs → records)
│ └── ElementLoader.php
├── Repository/ обращения к БД
│ ├── IblockElementRepository.php
│ ├── IblockPropertyDefinitionRepository.php
│ ├── IblockPropertyValueRepository.php
│ ├── FileRepository.php
│ ├── EnumerateRepository.php
│ └── SectionRepository.php
└── Resolve/
├── Contract/
│ ├── UserTypePropertyResolver.php
│ └── SupportsPropertyResolver.php
├── LinkedElementCache.php flyweight linked E на один get()
├── PropertyResolverRegistry.php
├── ResolvePipeline.php resolve + сборка IblockElement
└── BuiltIn/ Date, DateTime, File, Enum, Number, …
tests/ unit-тесты (без Bitrix runtime)
Install
composer require jiscariot/iblock-element-reader:^0.2