
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']);
Успіхів!