
Laravel: как фильтровать поля в API Resources
02.02.2019 09:53 | Laravel
Предположим, надо отдать данные в json формате в js-приложение. Написаны классы ресурсов (и коллекций), но появляется другая проблема - в разных случаях следует передавать разный набор полей. Возьмём UserResource: одно дело данные для профиля пользователя, и другое дело - данные автора для поста. Как поступим?
Начнём с очевидных вещей:
- не стоит в методе toArray класса ресурса прописывать все поля, которые есть в модели. Только те, которые будут нужны
- обратите внимание на условные атрибуты - может есть что-то, что понадобится только в определённых ситуациях? (Посмотрите на пример с полем "админ" в документации)
- об отношениях - если таковые нужны в ресурсе, я бы советовал воспользоваться опцией Conditional Relationships и жадной загрузкой. Это придаст гибкости и потенциально позволит избежать ошибки N+1
Вернёмся к набору полей. На просторах интернета нашёл статью о динамическом скрытии полей. Как раз то, что нужно, да и код неплох. Однако, есть некоторые минусы:
- фактически реализован только метод
hide
. Но что, если из множества полей надо показать только два? Не перечислять же, например, 8 из 10. На такой случай не помешал бы методonly
. - дублирование кода в классах
Resource
иResoruceCollection
Тем не менее - реализация хороша. Поэтому я решил не придумывать новое, а лишь усовершенствовать данное решение:
- добавить возможность показывать только определённые поля. Ну и поскольку буду использовать коллекции, методы назову аналогично
only
иexcept
(вместоhide
) - избежать дублирование кода в классах единичного ресурса и коллекции ресурсов
- добавить возможность передавать название полей не только в массиве, но и просто перечислять их через запятую
Касательно именования. Поскольку JsonResource
уже использует метод filter
(который определён в трейте ConditionallyLoadsAttributes
) и предназначен для фильтрации условных отношений, я буду отталкиваться от термина filtrate
.
И по структуре. Сначала была мысль создать родительские классы для фильтрации полей, но есть момент: если класс коллекции ресурсов наследовать от условного FiltratedResource
, придётся дублировать код из ResourceCollection
. Если наследовать FiltratedResourceCollection
от ResourceCollection
, не избежать повторения кода из FiltratedResource
. Множественое наследование в PHP не поддерживается. Посему будем использовать трейт. Вот его код:
<?php
namespace App\Traits\Resources;
trait Filtratable
{
/**
* Fields to filtrate
*
* @var array
*/
protected $fieldsToFiltrate = [];
/**
* Filtrate method name
*
* @var string
*/
protected $filtrateMethod;
/**
* Set properties to exclude specified fields
*
* @param array|string $fieldsToFiltrate
* @return object $this
*/
public function except(...$fieldsToFiltrate)
{
$this->setFiltrateProps(__FUNCTION__, $fieldsToFiltrate);
return $this;
}
/**
* Set properties to make visible specified fields
*
* @param [type] $fieldsToFiltrate [description]
* @return [type] [description]
*/
public function only(...$fieldsToFiltrate)
{
$this->setFiltrateProps(__FUNCTION__, $fieldsToFiltrate);
return $this;
}
/**
* Filtrate fields
*
* @param array $data
* @return array [filtrated data]
*/
public function filtrateFields($data)
{
if ($this->filtrateMethod) {
$filter = $this->filtrateMethod;
return collect($data)->{$filter}($this->fieldsToFiltrate)->toArray();
}
return $data;
}
/**
* Set properties for filtrating
*
* @param string $method
* @param array|type $fields
*/
protected function setFiltrateProps($method, $fields)
{
$this->filtrateMethod = $method;
$this->fieldsToFiltrate = is_array($fields[0]) ? $fields[0] : $fields;
}
/**
* Send fields to filtrate to Resource while processing the collection
*
* @param $request
* @return array
*/
protected function processCollection($request)
{
if (! $this->filtrateMethod) {
return $this->collection;
}
return $this->collection->map(function ($resource) use ($request) {
$method = $this->filtrateMethod;
return $resource->$method($this->fieldsToFiltrate)->toArray($request);
})->all();
}
}
Примеры использования
В классе ресурса:
<?php
namespace App\Http\Resources;
use App\Traits\Resources\Filtratable;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\JsonResource;
class CategoryResource extends JsonResource
{
use Filtratable;
public function toArray($request)
{
return $this->filtrateFields([
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'created_at' => Carbon::parse($this->created_at)->toDateTimeString(),
]);
}
}
В классе коллекции ресурсов:
<?php
namespace App\Http\Resources;
use App\Traits\Resources\Filtratable;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CategoryResourceCollection extends ResourceCollection
{
use Filtratable;
public function toArray($request)
{
return [
'data' => $this->processCollection($request),
];
}
}
В коде-клиенте (единичный ресурс):
(new CategoryResource($category))->only('id', 'name', 'description');
// или так
(new CategoryResource($category))->only(['id', 'name', 'description']);
// или так
(new CategoryResource($category))->except('created_at');
// или так
(new CategoryResource($category))->except(['created_at']);
В коде-клиенте (коллекция ресурсов):
(new CategoryResourceCollection($category))->only('id', 'name');
// или так
(new CategoryResourceCollection($category))->only(['id', 'name']);
// или так
(new CategoryResourceCollection($category))->except('description', 'created_at');
// или так
(new CategoryResourceCollection($category))->except(['description', 'created_at']);
Успехов!