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']);

 

Успіхів!