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

Пользовательское поле изнутри - описание, значение, вывод

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

Механика

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

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

Код сущности ENTITY_ID - обычная строка, а не ссылка на объект платформы. У пользователя это USER, у разделов инфоблока IBLOCK_N_SECTION, у highload-блока HLBLOCK_N. Строку можно придумать и свою: ядро не проверяет, существует ли за ней настоящий объект.

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

Описаниями полей управляет класс CUserTypeEntity, а значениями - менеджер полей CUserTypeManager. Глобальный объект $USER_FIELD_MANAGER и есть этот менеджер, доступный из любого места кода. D7-таблица UserFieldTable читает описания полей, но менять их через неё нельзя.

Тип поля USER_TYPE_ID задаёт формат хранения значения и разметку поля в форме. Штатных типов около десятка: строка, число, дробное число, флаг, дата, дата со временем, список, файл, ссылка, деньги, привязка к элементу или разделу инфоблока. Когда штатных типов не хватает, свой регистрируют обработчиком события OnUserTypeBuildList.

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

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

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

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

Шаги

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

Код

Смотрим описания полей сущности:

$res = \Bitrix\Main\UserFieldTable::getList([
'select' => ['ID', 'ENTITY_ID', 'FIELD_NAME', 'USER_TYPE_ID', 'MULTIPLE', 'MANDATORY'],
'filter' => ['=ENTITY_ID' => 'IBLOCK_12_SECTION'], // код сущности, а не её объект
'order' => ['SORT' => 'ASC'],
]);
while ($row = $res->fetch()) {
printf("%-4d %-20s тип=%-12s множественное=%s обязательное=%s\n",
$row['ID'], $row['FIELD_NAME'], $row['USER_TYPE_ID'],
$row['MULTIPLE'], $row['MANDATORY']); // здесь только описание, значений нет
}

Справочник описаний отвечает ровно на один вопрос: какое поле заведено у сущности и в каком виде. Значений в нём нет, и править описание через него тоже нельзя - для изменений существует отдельный класс.

Читаем значение вместе с описанием:

global $USER_FIELD_MANAGER;
// третий аргумент - язык подписей: без него в выводе окажется системное имя
$fields = $USER_FIELD_MANAGER->GetUserFields('IBLOCK_12_SECTION', $sectionId, LANGUAGE_ID);
foreach ($fields as $code => $field) {
printf("%-20s %-12s множ=%s подпись=%s\n", $code, $field['USER_TYPE_ID'],
$field['MULTIPLE'], $field['EDIT_FORM_LABEL']);
var_export($field['VALUE']); // значение приходит той же структурой, что и описание
}

Так поле показывают, когда его тип заранее неизвестен коду вывода. По ключу типа выбирают способ печати: подпись варианта для списка, путь для файла, форматирование по настройкам сайта для даты.

Берём одно значение без описания:

$value = $USER_FIELD_MANAGER->GetUserFieldValue('IBLOCK_12_SECTION', 'UF_COLOR', $sectionId);
// одиночное поле возвращает скаляр, множественное - массив значений
$list = $fields['UF_COLOR']['MULTIPLE'] === 'Y' ? (array)$value : [$value];
foreach ($list as $one) {
echo $one, "\n"; // приведение к массиву снимает разницу между двумя видами хранения
}

Разница в форме возврата и есть та самая множественность на уровне кода. Шаблон, ожидающий строку, ломается на первом же множественном поле, даже когда значение в нём ровно одно.

Разворачиваем идентификатор варианта в подпись:

$titles = [];
$res = \CUserFieldEnum::GetList([], ['USER_FIELD_NAME' => 'UF_COLOR']);
while ($v = $res->Fetch()) {
$titles[$v['ID']] = $v['VALUE']; // ключ - идентификатор варианта, значение - подпись
}
foreach ($list as $one) {
echo $titles[$one] ?? 'вариант удалён', "\n"; // в поле лежал именно идентификатор
}

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

Пускаем поле в выборку через ORM:

class ContractTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getTableName() { return 'vendor_contract'; }
public static function getUfId() { return 'VENDOR_CONTRACT'; } // код сущности полей
}
$rows = ContractTable::query()
->addSelect('UF_MANAGER')
->where('UF_MANAGER', '>', 0)
->addOrder('UF_MANAGER', 'DESC')
->fetchCollection();
foreach ($rows as $row) {
echo $row->getUfManager(), "\n"; // геттер генерируется по коду поля
}

Объявленный код сущности - единственное, что связывает таблицу с её пользовательскими полями. Дальше платформа подставляет соединение сама, и поля UF_* работают в выборке наравне со штатными.

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

// значения пишет менеджер полей, а не класс описания поля
$USER_FIELD_MANAGER->Update('IBLOCK_12_SECTION', $sectionId, [
'UF_COLOR' => 6, // у списочного поля передают идентификатор варианта
'UF_GALLERY' => [101, 102, 103], // у множественного поля - массив значений
]);

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

Меняем описание уже созданного поля:

$entity = new \CUserTypeEntity();
$entity->Update($fieldId, [
'MANDATORY' => 'Y',
'SORT' => 100,
'EDIT_FORM_LABEL' => ['ru' => 'Цвет раздела'], // подписи задаются на каждый язык
]);
// тип, код сущности, имя поля и множественность через обновление не меняются

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

Удаляем описание поля целиком:

$entity->Delete($fieldId);
// вместе с описанием исчезают значения у всех записей этой сущности
// выгружать значения надо до вызова: возврата к ним после удаления нет
// у самой записи сущности всё иначе - её значения ядро отдельно не чистит

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

Ограничения

Код поля, его тип, код сущности и множественность задаются один раз при создании. Смена любого из них означает новое поле, перенос значений скриптом и удаление старого поля вручную.

Выборка списком работает только там, где есть таблица для соединения значений. У поля на произвольном коде сущности такой таблицы нет, и отобрать записи по значению одним запросом не получится.

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

Каждое пользовательское поле в выборке - это отдельное соединение таблиц. Десяток полей в списке из тысячи записей заметен в замерах скорости, и лечится это сокращением списка выбираемых полей.

Штатная отрисовка поля рассчитана на административные формы, а не на публичную часть. Модули поддерживают типы неодинаково: в некоторых списочное поле показывает в фильтре и списке идентификатор вместо подписи.

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

В списке вместо значения показывается число.

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

Поле заведено, а в выборке его нет.

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

Значение записано, а в форме сущности пусто.

Запись ушла на другой код сущности: у элементов, разделов и пользователей коды разные. Значение при этом сохранилось честно, просто у совсем другого набора полей.

Обновление описания прошло, а поле работает по-старому.

Через обновление меняются подписи, сортировка и обязательность, но не тип и не множественность. Эти свойства задаются при создании, и попытка их сменить проходит вхолостую.

Дата в поле приходит со сдвигом на несколько часов.

Значение приводится к часовому поясу портала, а не хранится буквально как передано. Дату конвертируют в пояс портала до записи, иначе сдвиг всплывает при выводе.

После удаления записи её значения остались в базе.

У своей сущности ядро не чистит значения: оно просто не знает, что запись исчезла. Значения удаляют своим кодом, иначе они копятся в таблице годами.

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

Где физически лежит значение пользовательского поля?

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

Почему в списке выводится идентификатор, а не значение?

Списочное поле хранит идентификатор варианта, подпись лежит в справочнике вариантов. Вывод разворачивает идентификатор сам, а часть модулей этого не делает.

Чем чтение менеджером отличается от чтения выборкой?

Менеджер отдаёт значение вместе с описанием поля: типом, подписью и настройками. Выборка отдаёт голое значение, и тип для вывода приходится знать заранее.

Можно ли поменять тип уже заполненного поля?

Нет, тип задаётся при создании и дальше неизменен: под разными типами разный формат хранения. Заводят новое поле, переносят значения скриптом и удаляют старое.

Что происходит с полем при удалении сущности?

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

Смежное

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