Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти
Інше
Події Про нас

Eloquent: API-ресурси

Вступ

Коли ви будуєте API, вам може знадобитися шар перетворення, який стоїть між вашими моделями Eloquent і JSON-відповідями, що насправді повертаються користувачам застосунку. Наприклад, ви можете захотіти показувати певні атрибути лише частині користувачів, або завжди включати певні зв'язки до JSON-представлення ваших моделей. Класи ресурсів Eloquent дозволяють виразно й легко перетворювати моделі та колекції моделей на JSON.

Звісно, ви завжди можете перетворити моделі чи колекції Eloquent на JSON за допомогою методу toJson; проте ресурси Eloquent дають більш гнучкий і надійний контроль над JSON-серіалізацією ваших моделей та їхніх зв'язків.

Генерування ресурсів

Щоб згенерувати клас ресурсу, скористайтеся артизан-командою make:resource. За замовчуванням ресурси потрапляють до каталогу app/Http/Resources вашого застосунку. Ресурси успадковують клас Illuminate\Http\Resources\Json\JsonResource:

php artisan make:resource UserResource

Колекції ресурсів

Окрім ресурсів, які перетворюють окремі моделі, ви можете генерувати ресурси, відповідальні за перетворення колекцій моделей. Це дозволяє вашим JSON-відповідям містити посилання та іншу метаінформацію, що стосується всієї колекції певного ресурсу.

Щоб створити колекцію ресурсів, скористайтеся прапорцем --collection під час створення ресурсу. Або ж слово Collection в імені ресурсу підкаже Laravel, що потрібно створити саме колекцію. Колекції ресурсів успадковують клас Illuminate\Http\Resources\Json\ResourceCollection:

php artisan make:resource User --collection

php artisan make:resource UserCollection

Огляд концепції

Це високорівневий огляд ресурсів і колекцій ресурсів. Наполегливо радимо прочитати інші розділи цієї документації, щоб глибше зрозуміти можливості налаштування та всю силу, яку дають ресурси.

Перш ніж заглиблюватися в усі доступні опції написання ресурсів, погляньмо згори на те, як ресурси використовуються в Laravel. Клас ресурсу представляє одну модель, яку потрібно перетворити на JSON-структуру. Ось, наприклад, простий клас ресурсу UserResource:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Кожен клас ресурсу визначає метод toArray, який повертає масив атрибутів, що мають бути перетворені на JSON, коли ресурс повертається як відповідь із маршруту чи методу контролера.

Зверніть увагу: ми звертаємося до властивостей моделі напряму через змінну $this. Так відбувається тому, що клас ресурсу автоматично проксіює звернення до властивостей і методів до моделі, яка лежить в основі, - для зручності. Коли ресурс визначено, його можна повернути з маршруту чи контролера. Ресурс приймає екземпляр моделі через свій конструктор:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/user/{id}', function (string $id) {
    return new UserResource(User::findOrFail($id));
});

Для зручності можна скористатися методом моделі toResource, який за угодами фреймворку автоматично знайде відповідний ресурс моделі:

return User::findOrFail($id)->toResource();

Під час виклику методу toResource Laravel спробує знайти ресурс, ім'я якого збігається з іменем моделі та, можливо, має суфікс Resource, у просторі імен Http\Resources, найближчому до простору імен моделі.

Якщо ваш клас ресурсу не дотримується цієї угоди про іменування або розташований в іншому просторі імен, ви можете вказати ресурс за замовчуванням для моделі за допомогою атрибута UseResource:

<?php

namespace App\Models;

use App\Http\Resources\CustomUserResource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResource;

#[UseResource(CustomUserResource::class)]
class User extends Model
{
    // ...
}

Або ж ви можете вказати клас ресурсу, передавши його до методу toResource:

return User::findOrFail($id)->toResource(CustomUserResource::class);

Колекції ресурсів

Якщо ви повертаєте колекцію ресурсів або відповідь із пагінацією, під час створення екземпляра ресурсу в маршруті чи контролері скористайтеся методом collection, який надає ваш клас ресурсу:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users', function () {
    return UserResource::collection(User::all());
});

Або, для зручності, скористайтеся методом колекції Eloquent toResourceCollection, який за угодами фреймворку автоматично знайде відповідну колекцію ресурсів моделі:

return User::all()->toResourceCollection();

Під час виклику методу toResourceCollection Laravel спробує знайти колекцію ресурсів, ім'я якої збігається з іменем моделі та має суфікс Collection, у просторі імен Http\Resources, найближчому до простору імен моделі.

Якщо ваш клас колекції ресурсів не дотримується цієї угоди про іменування або розташований в іншому просторі імен, ви можете вказати колекцію ресурсів за замовчуванням для моделі за допомогою атрибута UseResourceCollection:

<?php

namespace App\Models;

use App\Http\Resources\CustomUserCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResourceCollection;

#[UseResourceCollection(CustomUserCollection::class)]
class User extends Model
{
    // ...
}

Або ж ви можете вказати клас колекції ресурсів, передавши його до методу toResourceCollection:

return User::all()->toResourceCollection(CustomUserCollection::class);

Власні колекції ресурсів

За замовчуванням колекції ресурсів не дозволяють додавати власні метадані, які може знадобитися повернути разом із колекцією. Якщо ви хочете налаштувати відповідь колекції ресурсів, створіть окремий ресурс, що представлятиме колекцію:

php artisan make:resource UserCollection

Коли клас колекції ресурсів згенеровано, ви можете легко визначити будь-які метадані, що мають потрапити до відповіді:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<int|string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => 'link-value',
            ],
        ];
    }
}

Після визначення колекції ресурсів її можна повернути з маршруту чи контролера:

use App\Http\Resources\UserCollection;
use App\Models\User;

Route::get('/users', function () {
    return new UserCollection(User::all());
});

Або, для зручності, скористайтеся методом колекції Eloquent toResourceCollection, який за угодами фреймворку автоматично знайде відповідну колекцію ресурсів моделі:

return User::all()->toResourceCollection();

Під час виклику методу toResourceCollection Laravel спробує знайти колекцію ресурсів, ім'я якої збігається з іменем моделі та має суфікс Collection, у просторі імен Http\Resources, найближчому до простору імен моделі.

Збереження ключів колекції

Коли ви повертаєте колекцію ресурсів із маршруту, Laravel скидає ключі колекції так, щоб вони йшли в числовому порядку. Проте ви можете застосувати до класу ресурсу атрибут PreserveKeys, що вказує, чи слід зберігати початкові ключі колекції:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Attributes\PreserveKeys;
use Illuminate\Http\Resources\Json\JsonResource;

#[PreserveKeys]
class UserResource extends JsonResource
{
    // ...
}

Коли властивість preserveKeys має значення true, ключі колекції зберігатимуться при поверненні колекції з маршруту чи контролера:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/users', function () {
    return UserResource::collection(User::all()->keyBy->id);
});

Налаштування базового класу ресурсу

Зазвичай властивість $this->collection колекції ресурсів автоматично заповнюється результатом перетворення кожного елемента колекції на його одиничний клас ресурсу. Вважається, що одиничний клас ресурсу - це ім'я класу колекції без кінцевої частини Collection. До того ж, залежно від ваших уподобань, одиничний клас ресурсу може мати або не мати суфікс Resource.

Наприклад, UserCollection спробує перетворити передані екземпляри користувачів на ресурс UserResource. Щоб змінити цю поведінку, застосуйте до колекції ресурсів атрибут Collects:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Attributes\Collects;
use Illuminate\Http\Resources\Json\ResourceCollection;

#[Collects(Member::class)]
class UserCollection extends ResourceCollection
{
    // ...
}

Написання ресурсів

Якщо ви ще не прочитали огляд концепції, наполегливо радимо зробити це, перш ніж рухатися далі.

Ресурсам потрібно лише перетворити задану модель на масив. Тож кожен ресурс містить метод toArray, який перекладає атрибути вашої моделі на зручний для API масив, що може бути повернений із маршрутів чи контролерів вашого застосунку:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Коли ресурс визначено, його можна повернути прямо з маршруту чи контролера:

use App\Models\User;

Route::get('/user/{id}', function (string $id) {
    return User::findOrFail($id)->toUserResource();
});

Зв'язки

Якщо ви хочете включити до відповіді пов'язані ресурси, додайте їх до масиву, який повертає метод toArray вашого ресурсу. У цьому прикладі ми скористаємося методом collection ресурсу PostResource, щоб додати до відповіді ресурсу дописи користувача:

use App\Http\Resources\PostResource;
use Illuminate\Http\Request;

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts' => PostResource::collection($this->posts),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

Якщо ви хочете включати зв'язки лише тоді, коли їх уже завантажено, перегляньте документацію про умовні зв'язки.

Колекції ресурсів

Якщо ресурси перетворюють одну модель на масив, то колекції ресурсів перетворюють на масив колекцію моделей. Проте визначати клас колекції ресурсів для кожної моделі зовсім не обов'язково, адже всі колекції моделей Eloquent мають метод toResourceCollection, який на льоту створює «ad-hoc» колекцію ресурсів:

use App\Models\User;

Route::get('/users', function () {
    return User::all()->toResourceCollection();
});

Однак якщо вам потрібно налаштувати метадані, що повертаються разом із колекцією, доведеться визначити власну колекцію ресурсів:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'data' => $this->collection,
            'links' => [
                'self' => 'link-value',
            ],
        ];
    }
}

Як і одиничні ресурси, колекції ресурсів можна повертати прямо з маршрутів чи контролерів:

use App\Http\Resources\UserCollection;
use App\Models\User;

Route::get('/users', function () {
    return new UserCollection(User::all());
});

Або, для зручності, скористайтеся методом колекції Eloquent toResourceCollection, який за угодами фреймворку автоматично знайде відповідну колекцію ресурсів моделі:

return User::all()->toResourceCollection();

Під час виклику методу toResourceCollection Laravel спробує знайти колекцію ресурсів, ім'я якої збігається з іменем моделі та має суфікс Collection, у просторі імен Http\Resources, найближчому до простору імен моделі.

Обгортання даних

За замовчуванням найзовнішніший ресурс обгортається в ключ data, коли відповідь ресурсу перетворюється на JSON. Тож типова відповідь колекції ресурсів має такий вигляд:

{
    "data": [
        {
            "id": 1,
            "name": "Eladio Schroeder Sr.",
            "email": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ]
}

Якщо ви хочете вимкнути обгортання найзовнішнішого ресурсу, викличте метод withoutWrapping на базовому класі Illuminate\Http\Resources\Json\JsonResource. Зазвичай цей метод викликають із AppServiceProvider або іншого сервіс-провайдера, який завантажується на кожному запиті до застосунку:

<?php

namespace App\Providers;

use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        // ...
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        JsonResource::withoutWrapping();
    }
}

Метод withoutWrapping впливає лише на найзовнішнішу відповідь і не прибирає ключі data, які ви вручну додали до власних колекцій ресурсів.

Обгортання вкладених ресурсів

Ви маєте цілковиту свободу визначати, як обгортаються зв'язки вашого ресурсу. Якщо ви хочете, щоб усі колекції ресурсів обгорталися в ключ data незалежно від рівня вкладеності, визначте клас колекції ресурсів для кожного ресурсу й повертайте колекцію всередині ключа data.

Ви можете замислитися, чи не призведе це до подвійного обгортання найзовнішнішого ресурсу в два ключі data. Не хвилюйтеся: Laravel ніколи не дозволить випадково обгорнути ваші ресурси двічі, тож про рівень вкладеності колекції ресурсів, яку ви перетворюєте, можна не турбуватися:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class CommentsCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return ['data' => $this->collection];
    }
}

Обгортання даних і пагінація

Коли ви повертаєте колекції з пагінацією через відповідь ресурсу, Laravel обгортає дані ресурсу в ключ data, навіть якщо було викликано метод withoutWrapping. Так відбувається тому, що відповіді з пагінацією завжди містять ключі meta та links з інформацією про стан пагінатора:

{
    "data": [
        {
            "id": 1,
            "name": "Eladio Schroeder Sr.",
            "email": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ],
    "links":{
        "first": "http://example.com/users?page=1",
        "last": "http://example.com/users?page=1",
        "prev": null,
        "next": null
    },
    "meta":{
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "path": "http://example.com/users",
        "per_page": 15,
        "to": 10,
        "total": 10
    }
}

Пагінація

Ви можете передати екземпляр пагінатора Laravel до методу collection ресурсу або до власної колекції ресурсів:

use App\Http\Resources\UserCollection;
use App\Models\User;

Route::get('/users', function () {
    return new UserCollection(User::paginate());
});

Або, для зручності, скористайтеся методом пагінатора toResourceCollection, який за угодами фреймворку автоматично знайде колекцію ресурсів для моделі з пагінацією:

return User::paginate()->toResourceCollection();

Відповіді з пагінацією завжди містять ключі meta та links з інформацією про стан пагінатора:

{
    "data": [
        {
            "id": 1,
            "name": "Eladio Schroeder Sr.",
            "email": "therese28@example.com"
        },
        {
            "id": 2,
            "name": "Liliana Mayert",
            "email": "evandervort@example.com"
        }
    ],
    "links":{
        "first": "http://example.com/users?page=1",
        "last": "http://example.com/users?page=1",
        "prev": null,
        "next": null
    },
    "meta":{
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "path": "http://example.com/users",
        "per_page": 15,
        "to": 10,
        "total": 10
    }
}

Налаштування інформації про пагінацію

Якщо ви хочете налаштувати інформацію, що потрапляє до ключів links чи meta відповіді з пагінацією, визначте на ресурсі метод paginationInformation. Цей метод отримає дані $paginated і масив стандартної інформації $default, який містить ключі links та meta:

/**
 * Customize the pagination information for the resource.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  array  $paginated
 * @param  array  $default
 * @return array
 */
public function paginationInformation($request, $paginated, $default)
{
    $default['links']['custom'] = 'https://example.com';

    return $default;
}

Умовні атрибути

Іноді потрібно включати атрибут до відповіді ресурсу лише за певної умови. Наприклад, ви можете захотіти показувати значення лише тоді, коли поточний користувач - «адміністратор». Laravel надає для таких випадків набір допоміжних методів. Метод when дозволяє додавати атрибут до відповіді ресурсу за умовою:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'secret' => $this->when($request->user()->isAdmin(), 'secret-value'),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

У цьому прикладі ключ secret потрапить до підсумкової відповіді ресурсу лише тоді, коли метод isAdmin автентифікованого користувача поверне true. Якщо метод поверне false, ключ secret буде видалено з відповіді ресурсу ще до того, як її буде надіслано клієнту. Метод when дозволяє виразно описувати ресурси, не вдаючись до умовних конструкцій під час побудови масиву.

Метод when також приймає замикання другим аргументом, тож ви можете обчислювати підсумкове значення лише тоді, коли задана умова істинна (true):

'secret' => $this->when($request->user()->isAdmin(), function () {
    return 'secret-value';
}),

Метод whenHas дозволяє включити атрибут, якщо він справді присутній на моделі:

'name' => $this->whenHas('name'),

Крім того, метод whenNotNull дозволяє включити атрибут до відповіді ресурсу, якщо цей атрибут не є null:

'name' => $this->whenNotNull($this->name),

Об'єднання умовних атрибутів

Іноді у вас є кілька атрибутів, які мають потрапляти до відповіді ресурсу за однією й тією ж умовою. У такому разі скористайтеся методом mergeWhen, щоб включити атрибути до відповіді лише тоді, коли задана умова істинна (true):

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        $this->mergeWhen($request->user()->isAdmin(), [
            'first-secret' => 'value',
            'second-secret' => 'value',
        ]),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

І знову: якщо задана умова хибна (false), ці атрибути буде видалено з відповіді ресурсу ще до надсилання клієнту.

Метод mergeWhen не слід використовувати всередині масивів, де змішані рядкові та числові ключі. Також його не варто застосовувати в масивах із числовими ключами, що йдуть не послідовно.

Умовні зв'язки

Окрім умовного завантаження атрибутів, ви можете умовно включати до відповідей ресурсу зв'язки - залежно від того, чи вже завантажено зв'язок на моделі. Це дозволяє контролеру вирішувати, які зв'язки слід завантажити на моделі, а ресурс легко включить їх лише тоді, коли їх справді завантажено. Зрештою, так простіше уникати проблем із запитами «N+1» усередині ресурсів.

Метод whenLoaded дозволяє завантажувати зв'язок за умовою. Щоб не завантажувати зв'язки без потреби, цей метод приймає ім'я зв'язку, а не сам зв'язок:

use App\Http\Resources\PostResource;

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts' => PostResource::collection($this->whenLoaded('posts')),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

У цьому прикладі, якщо зв'язок не було завантажено, ключ posts буде видалено з відповіді ресурсу ще до надсилання клієнту.

Умовна кількість пов'язаних записів

Окрім умовного включення зв'язків, ви можете умовно включати до відповідей ресурсу «кількість» пов'язаних записів - залежно від того, чи завантажено цю кількість на моделі:

new UserResource($user->loadCount('posts'));

Метод whenCounted дозволяє умовно включити кількість пов'язаних записів до відповіді ресурсу. Цей метод уникає зайвого додавання атрибута, якщо кількості для зв'язку немає:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'email' => $this->email,
        'posts_count' => $this->whenCounted('posts'),
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

У цьому прикладі, якщо кількість для зв'язку posts не було завантажено, ключ posts_count буде видалено з відповіді ресурсу ще до надсилання клієнту.

Інші види агрегатів, як-от avg, sum, min і max, також можна завантажувати за умовою за допомогою методу whenAggregated:

'words_avg' => $this->whenAggregated('posts', 'words', 'avg'),
'words_sum' => $this->whenAggregated('posts', 'words', 'sum'),
'words_min' => $this->whenAggregated('posts', 'words', 'min'),
'words_max' => $this->whenAggregated('posts', 'words', 'max'),

Умовна інформація з проміжної таблиці

Окрім умовного включення інформації про зв'язки до відповідей ресурсу, ви можете умовно включати дані з проміжних таблиць зв'язків «багато до багатьох» за допомогою методу whenPivotLoaded. Метод whenPivotLoaded приймає першим аргументом ім'я проміжної таблиці. Другим аргументом має бути замикання, що повертає значення, яке слід повернути, якщо інформація з проміжної таблиці доступна на моделі:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'expires_at' => $this->whenPivotLoaded('role_user', function () {
            return $this->pivot->expires_at;
        }),
    ];
}

Якщо ваш зв'язок використовує власну модель проміжної таблиці, ви можете передати екземпляр цієї моделі першим аргументом до методу whenPivotLoaded:

'expires_at' => $this->whenPivotLoaded(new Membership, function () {
    return $this->pivot->expires_at;
}),

Якщо ваша проміжна таблиця використовує аксесор, відмінний від pivot, скористайтеся методом whenPivotLoadedAs:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'id' => $this->id,
        'name' => $this->name,
        'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () {
            return $this->subscription->expires_at;
        }),
    ];
}

Додавання метаданих

Деякі стандарти JSON API вимагають додавати метадані до відповідей ресурсів і колекцій ресурсів. Найчастіше це links на сам ресурс чи пов'язані ресурси або метадані про сам ресурс. Якщо вам потрібно повернути додаткові метадані про ресурс, включіть їх до методу toArray. Наприклад, ви можете додати інформацію links під час перетворення колекції ресурсів:

/**
 * Transform the resource into an array.
 *
 * @return array<string, mixed>
 */
public function toArray(Request $request): array
{
    return [
        'data' => $this->collection,
        'links' => [
            'self' => 'link-value',
        ],
    ];
}

Повертаючи додаткові метадані з ресурсів, ви можете не хвилюватися про випадкове перезаписування ключів links чи meta, які Laravel автоматично додає до відповідей із пагінацією. Будь-які визначені вами додаткові links буде об'єднано з посиланнями, що їх надає пагінатор.

Метадані верхнього рівня

Іноді потрібно включати певні метадані до відповіді ресурсу лише тоді, коли цей ресурс є найзовнішнішим із тих, що повертаються. Зазвичай це метаінформація про відповідь загалом. Щоб визначити такі метадані, додайте до класу ресурсу метод with. Цей метод має повертати масив метаданих, які потрапляють до відповіді ресурсу лише тоді, коли ресурс є найзовнішнішим із тих, що перетворюються:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }

    /**
     * Get additional data that should be returned with the resource array.
     *
     * @return array<string, mixed>
     */
    public function with(Request $request): array
    {
        return [
            'meta' => [
                'key' => 'value',
            ],
        ];
    }
}

Додавання метаданих під час створення ресурсів

Ви також можете додавати дані верхнього рівня під час створення екземплярів ресурсів у маршруті чи контролері. Метод additional, доступний на всіх ресурсах, приймає масив даних, які слід додати до відповіді ресурсу:

return User::all()
    ->load('roles')
    ->toResourceCollection()
    ->additional(['meta' => [
        'key' => 'value',
    ]]);

Ресурси JSON:API

Laravel постачається з JsonApiResource - класом ресурсу, який формує відповіді, сумісні зі специфікацією JSON:API. Він успадковує стандартний клас JsonResource й автоматично дбає про структуру об'єкта ресурсу, зв'язки, розріджені набори полів, includes, ліниве обчислення атрибутів, а також встановлює заголовок Content-Type у значення application/vnd.api+json.

Ресурси JSON:API в Laravel відповідають за серіалізацію ваших відповідей. Якщо вам також потрібно розбирати вхідні параметри запиту JSON:API, як-от фільтри та сортування, чудовим доповненням стане Laravel Query Builder від Spatie.

Генерування ресурсів JSON:API

Щоб згенерувати ресурс JSON:API, скористайтеся артизан-командою make:resource із прапорцем --json-api:

php artisan make:resource PostResource --json-api

Згенерований клас успадкує Illuminate\Http\Resources\JsonApi\JsonApiResource і міститиме властивості $attributes та $relationships, які вам потрібно заповнити:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;

class PostResource extends JsonApiResource
{
    /**
     * The resource's attributes.
     */
    public $attributes = [
        // ...
    ];

    /**
     * The resource's relationships.
     */
    public $relationships = [
        // ...
    ];
}

Ресурси JSON:API можна повертати з маршрутів і контролерів так само, як і стандартні ресурси:

use App\Http\Resources\PostResource;
use App\Models\Post;

Route::get('/api/posts/{post}', function (Post $post) {
    return new PostResource($post);
});

Або, для зручності, скористайтеся методом моделі toResource:

Route::get('/api/posts/{post}', function (Post $post) {
    return $post->toResource();
});

Це дасть відповідь, сумісну з JSON:API:

{
    "data": {
        "id": "1",
        "type": "posts",
        "attributes": {
            "title": "Hello World",
            "body": "This is my first post."
        }
    }
}

Щоб повернути колекцію ресурсів JSON:API, скористайтеся методом collection або зручним методом toResourceCollection:

return PostResource::collection(Post::all());

return Post::all()->toResourceCollection();

Визначення атрибутів

Є два способи визначити, які атрибути потраплять до вашого ресурсу JSON:API.

Найпростіший підхід - визначити на ресурсі властивість $attributes. Ви можете перелічити імена атрибутів як значення, і їх буде прочитано напряму з моделі:

public $attributes = [
    'title',
    'body',
    'created_at',
];

Якщо обчислення атрибута дороге, поверніть його з toAttributes як замикання - тоді його буде обчислено лише тоді, коли атрибут справді знадобиться у відповіді.

Або ж, для повного контролю над атрибутами ресурсу, перевизначте на ресурсі метод toAttributes:

/**
 * Get the resource's attributes.
 *
 * @return array<string, mixed>
 */
public function toAttributes(Request $request): array
{
    return [
        'title' => $this->title,
        'body' => $this->body,
        'is_published' => fn () => $this->published_at !== null,
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
    ];
}

Визначення зв'язків

Ресурси JSON:API підтримують визначення зв'язків згідно зі специфікацією JSON:API. Зв'язки серіалізуються лише тоді, коли клієнт запитує їх через параметр запиту include.

Властивість $relationships

Ви можете визначити зв'язки ресурсу, доступні для включення, через властивість $relationships:

public $relationships = [
    'author',
    'comments',
];

Коли ім'я зв'язку вказано як значення, Laravel визначить відповідний зв'язок Eloquent і автоматично знайде потрібний клас ресурсу. Якщо вам потрібно вказати клас ресурсу явно, визначте зв'язок як пару ключ / клас:

use App\Http\Resources\UserResource;

public $relationships = [
    'author' => UserResource::class,
    'comments',
];

Або ж перевизначте на ресурсі метод toRelationships:

/**
 * Get the resource's relationships.
 */
public function toRelationships(Request $request): array
{
    return [
        'author' => UserResource::class,
        'comments' => fn () => CommentResource::collection(
            $request->user()->is($this->resource)
                ? $this->comments
                : $this->comments->where('is_public', true),
        ),
    ];
}

Замикання дають більше контролю над даними зв'язку, водночас зв'язок так само розв'язується лише тоді, коли клієнт його запитує.

Включення зв'язків

Клієнти можуть запитувати пов'язані ресурси через параметр запиту include:

GET /api/posts/1?include=author,comments

Це дасть відповідь з ідентифікаторами ресурсів у ключі relationships і повними об'єктами ресурсів у масиві included верхнього рівня:

{
    "data": {
        "id": "1",
        "type": "posts",
        "attributes": {
            "title": "Hello World"
        },
        "relationships": {
            "author": {
                "data": {
                    "id": "1",
                    "type": "users"
                }
            },
            "comments": {
                "data": [
                    {
                        "id": "1",
                        "type": "comments"
                    }
                ]
            }
        }
    },
    "included": [
        {
            "id": "1",
            "type": "users",
            "attributes": {
                "name": "Taylor Otwell"
            }
        },
        {
            "id": "1",
            "type": "comments",
            "attributes": {
                "body": "Great post!"
            }
        }
    ]
}

Вкладені зв'язки можна включати через крапкову нотацію:

GET /api/posts/1?include=comments.author

Глибина зв'язків

За замовчуванням включення вкладених зв'язків обмежене максимальною глибиною. Ви можете змінити це обмеження методом maxRelationshipDepth, зазвичай в одному із сервіс-провайдерів застосунку:

use Illuminate\Http\Resources\JsonApi\JsonApiResource;

JsonApiResource::maxRelationshipDepth(3);

Тип та ID ресурсу

За замовчуванням type ресурсу виводиться з імені класу ресурсу. Наприклад, PostResource дає тип posts, а BlogPostResource - blog-posts. id ресурсу визначається з первинного ключа моделі.

Якщо вам потрібно змінити ці значення, перевизначте на ресурсі методи toType і toId:

/**
 * Get the resource's type.
 */
public function toType(Request $request): string
{
    return 'articles';
}

/**
 * Get the resource's ID.
 */
public function toId(Request $request): string
{
    return (string) $this->uuid;
}

Це особливо корисно, коли тип ресурсу має відрізнятися від імені класу - наприклад, коли AuthorResource обгортає модель User і має віддавати тип authors.

Розріджені набори полів і includes

Ресурси JSON:API підтримують розріджені набори полів, що дозволяє клієнтам запитувати лише певні атрибути для кожного типу ресурсу через параметр запиту fields:

GET /api/posts?fields[posts]=title,created_at&fields[users]=name

Це включить лише атрибути title та created_at для ресурсів posts і атрибут name для ресурсів users.

Ігнорування рядка запиту

Якщо ви хочете вимкнути фільтрацію за розрідженими наборами полів для певної відповіді ресурсу, викличте метод ignoreFieldsAndIncludesInQueryString:

return $post->toResource()
    ->ignoreFieldsAndIncludesInQueryString();

Включення раніше завантажених зв'язків

За замовчуванням зв'язки потрапляють до відповіді лише тоді, коли їх запитано через параметр запиту include. Якщо ви хочете включити всі раніше жадібно завантажені зв'язки незалежно від рядка запиту, викличте метод includePreviouslyLoadedRelationships:

return $post->load('author', 'comments')
    ->toResource()
    ->includePreviouslyLoadedRelationships();

Ви можете додати посилання й метаінформацію до об'єктів ресурсу JSON:API, перевизначивши на ресурсі методи toLinks і toMeta:

/**
 * Get the resource's links.
 */
public function toLinks(Request $request): array
{
    return [
        'self' => route('api.posts.show', $this->resource),
    ];
}

/**
 * Get the resource's meta information.
 */
public function toMeta(Request $request): array
{
    return [
        'readable_created_at' => $this->created_at->diffForHumans(),
    ];
}

Це додасть до об'єкта ресурсу у відповіді ключі links та meta:

{
    "data": {
        "id": "1",
        "type": "posts",
        "attributes": {
            "title": "Hello World"
        },
        "links": {
            "self": "https://example.com/api/posts/1"
        },
        "meta": {
            "readable_created_at": "2 hours ago"
        }
    }
}

Відповіді ресурсів

Як ви вже прочитали, ресурси можна повертати прямо з маршрутів і контролерів:

use App\Models\User;

Route::get('/user/{id}', function (string $id) {
    return User::findOrFail($id)->toResource();
});

Проте іноді потрібно налаштувати вихідну HTTP-відповідь, перш ніж її буде надіслано клієнту. Зробити це можна двома способами. По-перше, ви можете додати до ресурсу ланцюжком метод response. Цей метод поверне екземпляр Illuminate\Http\JsonResponse, даючи вам повний контроль над заголовками відповіді:

use App\Http\Resources\UserResource;
use App\Models\User;

Route::get('/user', function () {
    return User::find(1)
        ->toResource()
        ->response()
        ->header('X-Value', 'True');
});

Або ж ви можете визначити метод withResponse усередині самого ресурсу. Цей метод буде викликано, коли ресурс повертається як найзовнішніший ресурс у відповіді:

<?php

namespace App\Http\Resources;

use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
        ];
    }

    /**
     * Customize the outgoing response for the resource.
     */
    public function withResponse(Request $request, JsonResponse $response): void
    {
        $response->header('X-Value', 'True');
    }
}