В Laravel есть удобные API ресурсы, с которыми легко и приятно работать в области трансформации данных для ответа на запрос. Но что делать когда возникает необходимость изменить их структуру в соответствии с бизнес-потребностями? Разберёмся вместе!
Вводная
Laravel предоставляет возможность транформации данных для ответа на запрос при помощи API ресурсов. По-умолчанию ответ будет иметь примерно следующий вид для возврата одного инстанса:
{
"data": {
"id": 123,
"title": "Some title"
}
}Для коллекций:
{
"data": [
{
"id": 123,
"title": "Some title"
}
]
}И для пагинатора:
{
"data": [
{
"id": 123,
"title": "Some title"
}
],
"links": {
"first": "http://localhost:8000/?page=1",
"last": "http://localhost:8000/?page=2",
"prev": null,
"next": "http://localhost:8000/?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 2,
"links": [
{
"url": null,
"label": "« Previous",
"active": false
},
{
"url": "http://localhost:8000/?page=1",
"label": "1",
"active": true
},
{
"url": "http://localhost:8000/?page=2",
"label": 2,
"active": false
},
{
"url": "http://localhost:8000/?page=2",
"label": "Next »",
"active": false
}
],
"path": "http://localhost:8000/",
"per_page": 2,
"to": 2,
"total": 3
}
}И данный ответ выглядит уже устрашающе в случая, когда Laravel работает в режиме API и фронтенд к нему разрабатывается снаружи другой командой.
Преобразование snake_case в camelCase
В последнее время всё больше приложений переходят на именование ключей в формате camelCase. Не будем вдаваться в разговоры насколько это оправдано, просто примем факт.
В случае с данными всё просто - при создании ресурсов пишем имена ключей в нужном стиле и радуемся. Но как быть с пагинатором?
Мне известно три пути решения проблемы: очень костыльный, условно неудобный и мудрёный.
Костыльный способ
Пример костыльного метода, который я видел, некоторых людей приводит в ужас. Просто посмотрите на это:
<?php
public function index(Request $request)
{
$items = Some::paginate();
$resource = SomeResource::collection($items);
$result = $this->convertToArray($resource->toArray($request));
return response()->json($result);
}
protected function convertToArray(array $items): array
{
$result = [];
foreach ($items as $key => $value) {
$newKey = Str::camel($key);
$result[$newKey] = is_array($value) ? $this->convertToArray($value) : $value;
}
return $result;
}Здесь даже говорить ничего не надо - все проблемы на лицо. Поэтому перейдём к неудобному способу.
Условно неудобный
Laravel позволяет легко изменять ключи пагинатора путём объявления метода paginationInformation в классе ресурса. Для удобства можно вынести его в общий абстрактный класс и наслаждаться.
<?php
public function paginationInformation($request, $paginated, $default)
{
$default['links']['custom'] = 'https://example.com';
return $default;
}Такой подход довольно простой в реализации, полностью соответствует логике фреймворка и не требует дополнительных затрат по сопровождению. Но почему он неудобный?
Всё дело в том, что если придёт задача на изменение структуры ресурсов, этот метод придётся обрабатывать вручную и переписывать. Именно в этом случае в одном из проектов появился третий способ - мудрёный.
Мудрёный способ
Мудрёный он потому, что помимо превращения имён ключей в camelCase возникла бизнес-потребность дополнительно оборачивать ключи массивов в дополнительный ключ.
Так, например, если обычная коллекция имеет следующую структуру:
{
"data": [
{
"id": 123,
"title": "Some title"
}
]
}То по новым условиям нужно возвращать в такой:
{
"data": {
"somes": [
{
"id": 123,
"title": "Some title"
}
]
}
}Где somes - это множественная форма имени ключа и у каждого ресурса она своя.
Спросите зачем? Ответ прост - мета-данные. Периодически возникают потребности отдать какие-либо жёстко связанные именно с этим ответом данные, когда проще объединить их в один ответ нежели запрашивать из двух мест. В данном случае имена ключей не будут конфликтовать, т.к. внутри data у каждого будет, скажем так, своя область видимости.
Вдобавок, можно расширять ответы, чем мы ниже и займёмся.
Бизнес-потребность
Итак, нам прилетела необходимость выполнения следующих преобразований в респонсах:
В случае возврата одного инстанса тело размещать внутри объекта
data;В случае возврата коллекции, в том числе пагинации:
тело размещать во вложенном в объект
dataключе, имя которого имеет множественное значение (например,users,posts);выводить объект пагинатора в определённом формате;
Приводить все даны на выходе к формату
Y-m-d\TH:i.
Конечный вид объектов должен соответствовать следующему виду:
{
"data": {
"id": 123,
"title": "Some title",
"createdAt": "2024-03-16T18:54"
},
"status": "OK"
}{
"data": {
"somes": [
{
"id": 123,
"title": "Some title",
"createdAt": "2024-03-16T18:54",
"category": {
"id": 1,
"title": "Category name"
}
},
{
"id": 456,
"title": "Another title",
"createdAt": "2024-03-16T18:55",
"category": null
}
]
},
"pagination": {
"total": 2,
"perPage": 2,
"currentPage": 1,
"lastPage": 2
},
"status": "OK"
}Выглядит просто? Это просто так выглядит
