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

Концепция :meta эндпоинтов

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

Для решения этой задачи реализуется следующее:

  1. На каждый табличный список бэк предоставляет эндпоинт :meta, который отдаёт информацию о каждом поле сущности, с детализацией как по этому полю фильтровать, доступна ли сортировка и т.д.
  2. Фронт использует эту мета-информацию о сущности, чтобы построить страницу. Для получения самих данных необходимо использовать эндпоинт :search

Таким образом можно настроить редактирование пользователем выводимых полей, а при добавлении нового поля, оно просто дополнительно регистрируется в :meta

Структура ответа эндпоинта :meta

{
"data": {
"fields": [ // Массив с описанием полей сущности
{
"code": "id", // Символьный код поля
"name": "ID", // Название поля
"list": true, // Доступно ли поле для вывода в таблицу
"type": "string", // Тип поля. Список актуальных доступных значений смотрите в документации админки, FieldTypeEnum
"value_types" : [ // Значения типов, для отображения поля с несколькими типами
{
"type": "price", // Тип поля. Используется для форматирования при отображении
"field": "1", // Значение поля которому соответствует данный тип,
"field_value": "value_type" // Имя поля из ответа :search по которому определяется тип
}
],
"enum_info": { // Заполняется, если поле типа `enum`. Доступно 2 варианта, либо статический список перечислений, либо догрузка списка через отдельный эндпоинт
"endpoint": "string", // Заполняется для динамических перечислений, тут будет относительный путь до эндпоинта от корня сервиса
"values": [ // Заполняется для статических перечислений
{
"id": "int/string/...",
"title": "string"
}
]
},
"is_object": true, // Поле вложено в связанный объект
"is_array": true, // Поле является массивом. Перед отображением для каждого элемента массива необходимо применить правило преобразования в соответствии с type
"include": "brand", // Ключ по которому можно загрузить связанную сущность
"sort": true, // Доступно ли поле для сортировки
"sort_key": "id", // Заполняется, если доступна сортировка. Этот ключ необходимо передавать в :search метод для сортировки по данному полю
"filter": "default", // Тип фильтра по данному полю. Список актуальных доступных значений смотрите в документации админки, FieldFilterTypeEnum
"filter_range_key_from": "id_from", // Если тип фильтра по диапазону - ключ, под которым необходимо передавать в :search значение ОТ
"filter_range_key_to": "id_to", // Если тип фильтра по диапазону - ключ, под которым необходимо передавать в :search значение ДО
"filter_key": "id" // Если фильтр обычный - ключ, под которым необходимо передавать в :search значение
}
],
"detail_link": "number", // Код поля, которое нужно использовать для вывода ссылки на детальную страницу. Такое поле пользователь не может скрыть
"default_sort": "id", // Код поля, по которому необходимо проводить сортировку по-умолчанию
"default_list": [ // Коды полей, которые по-умолчанию выводятся в таблицу
"id",
"created_at"
],
"default_filter": [ // Коды полей, которые по-умолчанию выводятся в фильтр
"id",
"created_at"
]
}
}

Столбец с множественным типом

Если тип одного поля зависит от значения в другом поле сущности, то необходимо передавать объект value_types. Поле type - тип из перечисления FieldTypeEnum. Для определения типа отображения нужно передать название поля с типом сущности из метода {entities}:search в field_value. Значение типа сущности нужно передать в поле field.

Например: отображение значения скидки (рубли или проценты) зависит от типа скидок.

"value_types" : [
{
"type": "price",
"field": "RUB",
"field_value": "value_type"
},
{
"type": "int",
"field": "PERCENT",
"field_value": "value_type"
}
],

Значение поля для строки с типом RUB в поле value_type будет отображаться как price, для PERCENT как int.

Отображение полей связанных сущностей

Для отображения полей из связанных сущностей необходимо при описании свойства использовать метод ->object(). В ответе у данного поля будет установлен признак "is_object": true.

Например для отображения имени бренда:

Field::string('brand.name', 'Название бренда')->object('brand'),

В качестве кода передается иерархия поля в ответе метода search, в качестве разделителя используется .. Данное поле не поддерживает фильтрацию и сортировку. Если вложенная сущность не подгружается по умолчанию, то для ее загрузки необходимо передать код include во сходной параметр метода object.

Эндпоинт для перечислений

Обычно структура этого эндпоинта одинаковая:

  • Фильтрация по 2 полям: id (для получения значений по id, например для вывода значения в таблицу) или query (для автодополнения, при введении в фильтр).
  • Массив объектов в ответе, каждый объект содержит id (int/string/...) + title (string)

Реализация на бэке

Со стороны бэкенда подготовлены хелперы для формирования списка полей:

  1. new \App\Http\ApiV1\Support\Resources\ModelMetaResource([...]) - ресурс для формирования ответа
  2. \App\Domain\Common\Data\Meta\Field::{type}() - получение объекта для поля нужного типа
  3. \App\Domain\Common\Data\Meta\Fields\AbstractField - базовый класс для работы описанием поля, содержит методы для конфигурации описания
  4. \App\Domain\Common\Data\Meta\Enum\AbstractEnumInfo - базовый класс для заполнения информации о Enum. Либо указываем name эндпоинта, либо загружаем статическое перечисление

В итоге стандартный :meta эндпоинт выглядит примерно так:

public function propertiesMeta(AsyncLoadAction $action, PropertyTypeEnumInfo $types): ModelMetaResource
{
$action->execute([$types]);

return new ModelMetaResource([
Field::id()->listDefault()->filterDefault()->detailLink(),
Field::text('name', 'Рабочее название')->listDefault()->filterDefault()->resetSort(),
Field::text('display_name', 'Публичное название')->listDefault()->filterDefault()->resetSort(),
Field::text('code', 'Код')->listDefault()->filterDefault()->resetSort(),
Field::enum('type', 'Тип данных', $types)->listDefault()->filterDefault()->resetSort(),
Field::boolean('is_active', 'Активный')->listDefault()->filterDefault()->resetSort(),
Field::boolean('is_public', 'Выводить на витрине')->listDefault()->filterDefault()->resetSort(),
Field::boolean('is_required', 'Обязательность')->listDefault()->filterDefault()->resetSort(),
Field::boolean('is_gluing', 'Параметр склеивания')->listDefault()->filterDefault()->resetSort(),
Field::datetime('created_at', 'Дата создания')->listDefault()->filterDefault(),
Field::datetime('updated_at', 'Дата обновления')->listDefault()->filterDefault(),
Field::boolean('is_filterable', 'Фильтр на витрине')->resetSort(),
Field::boolean('is_multiple', 'Множественный')->resetSort(),
Field::boolean('has_directory', 'Справочник')->resetSort(),
Field::string('brand.name', 'Название бренда')->object(),
]);
}