Пользовательское поле изнутри - описание, значение, вывод
Разбираем пользовательское поле по слоям: где живёт его описание, где лежит значение и что связывает их с конкретной записью. Дальше смотрим, как значение попадает в форму и на витрину и почему оно там иногда выглядит числом.
Механика
Пользовательское поле не добавляет колонку в таблицу сущности и не меняет схему базы. Механизм состоит из двух половин: описания поля и значений этого поля у конкретных записей. Половины живут в разных таблицах и правятся разными классами ядра.
Описание - это одна запись в служебном справочнике полей платформы. В ней лежат код сущности, имя поля, тип, признак множественности, подписи и настройки под тип. Значения хранятся отдельно, в своей таблице на каждую сущность, и связаны с записью её идентификатором.
Код сущности 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. Отсюда самая частая жалоба по теме: в списке показывается
число, потому что вывод не развернул идентификатор в подпись.
Чтение менеджером и чтение выборкой сущности дают разный результат по составу. Менеджер отдаёт значение вместе с описанием поля: типом, подписью и настройками. Выборка отдаёт голое значение, и разбирать его тип приходится уже самому коду шаблона.
В форму редактирования поле попадает само: ядро рисует его по типу и подписям из описания. На витрине штатного вывода нет вообще, значение читают кодом и печатают своим шаблоном. Поэтому список и файл на публичной странице всегда требуют дополнительного разворачивания.
Шаги
- Найти описание поля в справочнике определений по коду сущности и проверить обязательный префикс имени.
- Выписать из описания тип поля и признак множественности: эта пара определяет и хранение, и способ чтения.
- Для списочного типа собрать варианты с их идентификаторами, потому что в значении лежит именно идентификатор.
- Прочитать значение менеджером полей, чтобы увидеть его вместе с типом и подписью, а не голым.
- Решить, откуда поле возьмёт витрина: выборкой сущности, менеджером полей или запросом с объявленным кодом сущности.
Код
Смотрим описания полей сущности:
$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);// вместе с описанием исчезают значения у всех записей этой сущности// выгружать значения надо до вызова: возврата к ним после удаления нет// у самой записи сущности всё иначе - её значения ядро отдельно не чиститПроверять, где поле используется, платформа не станет: подтверждения у вызова нет вообще. Поэтому удаление поля держат в установщике модуля, а не в разовом скрипте на рабочем сайте.
Ограничения
Код поля, его тип, код сущности и множественность задаются один раз при создании. Смена любого из них означает новое поле, перенос значений скриптом и удаление старого поля вручную.
Выборка списком работает только там, где есть таблица для соединения значений. У поля на произвольном коде сущности такой таблицы нет, и отобрать записи по значению одним запросом не получится.
Множественное поле не сортируется в принципе, потому что у одной записи значений несколько. Фильтр по такому полю даёт повторы строк, и убирают их группировкой уже в собственном коде выборки.
Каждое пользовательское поле в выборке - это отдельное соединение таблиц. Десяток полей в списке из тысячи записей заметен в замерах скорости, и лечится это сокращением списка выбираемых полей.
Штатная отрисовка поля рассчитана на административные формы, а не на публичную часть. Модули поддерживают типы неодинаково: в некоторых списочное поле показывает в фильтре и списке идентификатор вместо подписи.
Типичные проблемы
В списке вместо значения показывается число.
Списочное поле хранит идентификатор варианта, а подпись лежит в отдельной таблице вариантов. Вывод обязан развернуть идентификатор сам, иначе на странице остаётся голое число.
Поле заведено, а в выборке его нет.
Оно либо не перечислено в списке выбираемых полей, либо у таблицы не объявлен код сущности. Без этого объявления платформе нечего присоединять к запросу.
Значение записано, а в форме сущности пусто.
Запись ушла на другой код сущности: у элементов, разделов и пользователей коды разные. Значение при этом сохранилось честно, просто у совсем другого набора полей.
Обновление описания прошло, а поле работает по-старому.
Через обновление меняются подписи, сортировка и обязательность, но не тип и не множественность. Эти свойства задаются при создании, и попытка их сменить проходит вхолостую.
Дата в поле приходит со сдвигом на несколько часов.
Значение приводится к часовому поясу портала, а не хранится буквально как передано. Дату конвертируют в пояс портала до записи, иначе сдвиг всплывает при выводе.
После удаления записи её значения остались в базе.
У своей сущности ядро не чистит значения: оно просто не знает, что запись исчезла. Значения удаляют своим кодом, иначе они копятся в таблице годами.
Частые вопросы
Где физически лежит значение пользовательского поля?
В отдельной таблице значений, своей на каждую сущность, а не в таблице самой записи. Поэтому добавление поля не меняет схему базы, но каждая выборка со значением означает соединение.
Почему в списке выводится идентификатор, а не значение?
Списочное поле хранит идентификатор варианта, подпись лежит в справочнике вариантов. Вывод разворачивает идентификатор сам, а часть модулей этого не делает.
Чем чтение менеджером отличается от чтения выборкой?
Менеджер отдаёт значение вместе с описанием поля: типом, подписью и настройками. Выборка отдаёт голое значение, и тип для вывода приходится знать заранее.
Можно ли поменять тип уже заполненного поля?
Нет, тип задаётся при создании и дальше неизменен: под разными типами разный формат хранения. Заводят новое поле, переносят значения скриптом и удаляют старое.
Что происходит с полем при удалении сущности?
Удаление описания поля уносит все его значения сразу и без подтверждения. При удалении отдельной записи своей сущности значения остаются, и чистят их своим кодом.
Смежное
- Пользовательские поля на практике - оглавление подтемы
- Пользовательские поля (UF) - концепт механизма целиком
- Пользовательское поле: заведение, вывод значения, типы - как завести поле и прочитать значение
- Пользовательское поле не работает: разбор причин - когда поле есть, а значения нет
- Поля разделов и пользователей: выборка, множественные значения, файлы - как забрать поля вместе с сущностью
- Свой тип пользовательского поля: регистрация, формы, значение - когда штатных типов не хватает
- Пользовательские поля у своей сущности: регистрация, запись, чтение - те же поля у своего объекта
- Хранение свойств инфоблока: две версии, скорость, выбор режима - как устроен соседний механизм
- Свойства-списки: значения, сортировка, значение по умолчанию - список на свойствах элемента
- Где хранить данные: инфоблок, highload-блок или своя таблица - выбор хранилища под задачу