Концепция :meta эндпоинтов
Задача: В Административной панели требуется, чтобы администратор мог табличные списки гибко настраивать, выводить только интересующие столбцы, фильтры, менять сортировку. При этом добавление новых полей в сущность не должно требовать доработок табличного списка на фронте.
Для решения этой задачи реализуется следующее:
- На каждый табличный список бэк предоставляет эндпоинт
:meta
, который отдаёт информацию о каждом поле сущности, с детализацией как по этому полю фильтровать, доступна ли сортировка и т.д. - Фронт использует эту мета-информацию о сущности, чтобы построить страницу. Для получения самих данных необходимо использовать эндпоинт
: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)
Реализация на бэке
Со стороны бэкенда подготовлены хелперы для формирования списка полей:
new \App\Http\ApiV1\Support\Resources\ModelMetaResource([...])
- ресурс для формирования ответа\App\Domain\Common\Data\Meta\Field::{type}()
- получение объекта для поля нужного типа\App\Domain\Common\Data\Meta\Fields\AbstractField
- базовый класс для работы описанием поля, содержит методы для конфигурации описания\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(),
]);
}