Laravel: как фильтровать поля в API Resources

Laravel: как фильтровать поля в API Resources

Предположим, надо отдать данные в 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']);

 

Успехов!