Unetway

Laravel - API Resources

Вступление

При создании API вам может понадобиться слой преобразования, который находится между вашими моделями Eloquent и ответами JSON, которые фактически возвращаются пользователям вашего приложения. Классы ресурсов Laravel позволяют вам выразительно и легко преобразовать ваши модели и коллекции моделей в JSON.

Создание ресурсов

Для создания класса ресурса вы можете использовать команду Artisan. По умолчанию ресурсы будут размещаться в каталоге вашего приложения. Ресурсы расширяют класс:make:resourceapp/Http/ResourcesIlluminate\Http\Resources\Json\JsonResource

php artisan make:resource User

Коллекции Ресурсов

Помимо создания ресурсов, преобразующих отдельные модели, вы можете создавать ресурсы, отвечающие за преобразование коллекций моделей. Это позволяет вашему ответу включать ссылки и другую мета-информацию, относящуюся ко всей коллекции данного ресурса.

Чтобы создать коллекцию ресурсов, вы должны использовать --collectionфлаг при создании ресурса. Или включение слова Collectionв имя ресурса будет указывать Laravel, что он должен создать ресурс коллекции. Ресурсы коллекции расширяют класс:Illuminate\Http\Resources\Json\ResourceCollection

php artisan make:resource Users --collection

php artisan make:resource UserCollection

 

Обзор концепции

Это общий обзор ресурсов и коллекций ресурсов. Вам настоятельно рекомендуется прочитать другие разделы этой документации, чтобы глубже понять настройки и возможности, предлагаемые вам ресурсами.

Прежде чем углубляться во все опции, доступные вам при написании ресурсов, давайте сначала рассмотрим, как ресурсы используются в Laravel. Класс ресурса представляет собой единую модель, которую необходимо преобразовать в структуру JSON. Например, вот простой Userкласс ресурсов:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class User extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        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\User;
use App\Http\Resources\User as UserResource;

Route::get('/user', function () {
    return new UserResource(User::find(1));
});

 

Коллекции Ресурсов

Если вы возвращаете коллекцию ресурсов или разбитый на страницы ответ, вы можете использовать этот collectionметод при создании экземпляра ресурса в вашем маршруте или контроллере:

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

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

Обратите внимание, что это не позволяет добавлять метаданные, которые, возможно, потребуется вернуть вместе с коллекцией. Если вы хотите настроить ответ коллекции ресурсов, вы можете создать выделенный ресурс для представления коллекции:

php artisan make:resource UserCollection

После того как класс сбора ресурсов сгенерирован, вы можете легко определить любые метаданные, которые должны быть включены в ответ:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

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

После определения вашей коллекции ресурсов она может быть возвращена с маршрута или контроллера:

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

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

Сохранение ключей коллекции

При возврате коллекции ресурсов из маршрута Laravel сбрасывает ключи коллекции, чтобы они находились в простом числовом порядке. Однако вы можете добавить preserveKeysсвойство в свой класс ресурсов, указывающее, должны ли ключи коллекции быть сохранены:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class User extends JsonResource
{
    /**
     * Indicates if the resource's collection keys should be preserved.
     *
     * @var bool
     */
    public $preserveKeys = true;
}

Когда для preserveKeysсвойства установлено значение true, ключи коллекции будут сохранены:

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

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

Настройка базового класса ресурсов

Как правило, свойство коллекции ресурсов автоматически заполняется результатом сопоставления каждого элемента коллекции с его единственным классом ресурсов. Предполагается, что единственный класс ресурсов является именем класса коллекции без завершающей строки.$this->collectionCollection

Например, UserCollectionпопытается отобразить указанные пользовательские экземпляры в Userресурс. Чтобы настроить это поведение, вы можете переопределить $collectsсвойство вашей коллекции ресурсов:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * The resource that this resource collects.
     *
     * @var string
     */
    public $collects = 'App\Http\Resources\Member';
}

 

Письменные Ресурсы

Если вы еще не ознакомились с обзором концепции , настоятельно рекомендуем сделать это, прежде чем приступить к этой документации.

По сути, ресурсы просты. Им нужно только преобразовать данную модель в массив. Итак, каждый ресурс содержит toArrayметод, который переводит атрибуты вашей модели в дружественный к API массив, который может быть возвращен вашим пользователям:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class User extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'created_at' => $this->created_at,
            'updated_at' => $this->updated_at,
        ];
    }
}

Как только ресурс определен, он может быть возвращен непосредственно с маршрута или контроллера:

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

Route::get('/user', function () {
    return new UserResource(User::find(1));
});

Отношения

Если вы хотите включить связанные ресурсы в свой ответ, вы можете добавить их в массив, возвращаемый вашим toArrayметодом. В этом примере мы будем использовать метод Postресурса, collectionчтобы добавить сообщения блога пользователя в ответ ресурса:

/**
 * Transform the resource into an array.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
public function toArray($request)
{
    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,
    ];
}

Если вы хотите включить отношения только тогда, когда они уже загружены, ознакомьтесь с документацией по условным отношениям .

Коллекции Ресурсов

В то время как ресурсы переводят одну модель в массив, коллекции ресурсов переводят коллекцию моделей в массив. Нет необходимости определять класс коллекции ресурсов для каждого из типов вашей модели, поскольку все ресурсы предоставляют collectionметод для создания «специальной» коллекции ресурсов на лету:

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

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

Однако, если вам нужно настроить метаданные, возвращаемые вместе с коллекцией, необходимо определить коллекцию ресурсов:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

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

Как и отдельные ресурсы, коллекции ресурсов могут быть возвращены непосредственно с маршрутов или контроллеров:

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

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

 

Перенос данных

По умолчанию ваш самый внешний ресурс оборачивается dataключом, когда ответ ресурса преобразуется в JSON. Так, например, типичный ответ сбора ресурсов выглядит следующим образом:

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

Если вы хотите отключить перенос самого внешнего ресурса, вы можете использовать withoutWrappingметод базового класса ресурсов. Как правило, вам следует вызывать этот метод у вашего AppServiceProviderили другого поставщика услуг, который загружается при каждом запросе к вашему приложению:

<?php

namespace App\Providers;

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

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register bindings in the container.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Resource::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.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        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/pagination?page=1",
        "last": "http://example.com/pagination?page=1",
        "prev": null,
        "next": null
    },
    "meta":{
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "path": "http://example.com/pagination",
        "per_page": 15,
        "to": 10,
        "total": 10
    }
}

 

Пагинация

Вы всегда можете передать экземпляр paginator collectionметоду ресурса или пользовательской коллекции ресурсов:

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

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

Разбитые на страницы ответы всегда содержат 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/pagination?page=1",
        "last": "http://example.com/pagination?page=1",
        "prev": null,
        "next": null
    },
    "meta":{
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "path": "http://example.com/pagination",
        "per_page": 15,
        "to": 10,
        "total": 10
    }
}

 

Условные атрибуты

Иногда вы можете захотеть включить атрибут в ответ ресурса, только если данное условие выполнено. Например, вы можете включить значение, только если текущий пользователь является «администратором». Laravel предоставляет различные вспомогательные методы, чтобы помочь вам в этой ситуации. whenМетод может быть использован , чтобы условно добавить атрибут в ответ ресурса:

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

В этом примере secretключ будет возвращен только в окончательном ответе ресурса, если метод аутентифицированного пользователя isAdminвернется true. Если метод вернется falsesecretключ будет полностью удален из ответа ресурса перед его отправкой обратно клиенту. whenМетод позволяет определить экспрессивно свои ресурсы , не прибегая к условным операторам при построении массива.

whenМетод также принимает Замыкание в качестве второго аргумента, что позволяет рассчитать итоговое значение , только если данное условие true:

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

Слияние условных атрибутов

Иногда у вас может быть несколько атрибутов, которые должны быть включены только в ответ ресурса на основе одного и того же условия. В этом случае вы можете использовать mergeWhenметод для включения атрибутов в ответ, только если данное условие true:

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

Опять же, если данное условие выполнено false, эти атрибуты будут полностью удалены из ответа ресурса перед его отправкой клиенту.

Этот mergeWhenметод не должен использоваться в массивах, которые смешивают строковые и числовые ключи. Кроме того, он не должен использоваться в массивах с цифровыми клавишами, которые не упорядочены последовательно.

 

Условные Отношения

В дополнение к условной загрузке атрибутов, вы можете условно включать отношения в ваши ответы ресурса на основе того, были ли отношения уже загружены в модель. Это позволяет вашему контроллеру решать, какие отношения должны быть загружены в модель, и ваш ресурс может легко включать их только тогда, когда они действительно были загружены.

В конечном итоге это позволяет избежать проблем с запросами "N + 1" в ваших ресурсах. whenLoadedМетод может быть использован для загрузки условно отношений. Чтобы избежать ненужной загрузки отношений, этот метод принимает имя отношения вместо самого отношения:

/**
 * Transform the resource into an array.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return array
 */
public function toArray($request)
{
    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ключ будет полностью удален из ответа ресурса перед его отправкой клиенту.

Условная сводная информация

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

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

Если ваша промежуточная таблица использует другой метод доступа, чем pivot, вы можете использовать whenPivotLoadedAsметод:

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

 

Добавление метаданных

Некоторые стандарты JSON API требуют добавления метаданных к вашим ресурсам и ответам коллекций ресурсов. Это часто включает в себя такие вещи, как linksресурс или связанные ресурсы, или метаданные о самом ресурсе. Если вам необходимо вернуть дополнительные метаданные о ресурсе, включите их в свой toArrayметод. Например, вы можете включить linkинформацию при преобразовании коллекции ресурсов:

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

При возврате дополнительных метаданных из ваших ресурсов вам никогда не придется беспокоиться о случайном переопределении клавиш linksили meta, которые автоматически добавляются Laravel при возврате разбитых на страницы ответов. Любые дополнительные, которые linksвы определяете, будут объединены со ссылками, предоставленными paginator.

Метаданные верхнего уровня

Иногда вы можете захотеть включить определенные метаданные в ответ ресурса, если ресурс является самым внешним возвращаемым ресурсом. Как правило, это включает метаинформацию об ответе в целом. Чтобы определить эти метаданные, добавьте withметод в свой класс ресурсов. Этот метод должен возвращать массив метаданных, которые будут включены в ответ ресурса, только когда ресурс является самым внешним ресурсом, который отображается:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transform the resource collection into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return parent::toArray($request);
    }

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

Добавление метаданных при построении ресурсов

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

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

 

Ответы ресурса

Как вы уже прочитали, ресурсы могут быть возвращены непосредственно с маршрутов и контроллеров:

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

Route::get('/user', function () {
    return new UserResource(User::find(1));
});

Однако иногда вам может потребоваться настроить исходящий HTTP-ответ перед его отправкой клиенту. Есть два способа сделать это. Во-первых, вы можете связать responseметод с ресурсом. Этот метод вернет экземпляр, что позволит вам полностью контролировать заголовки ответа:Illuminate\Http\Response

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

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

Кроме того, вы можете определить withResponseметод в самом ресурсе. Этот метод будет вызван, когда ресурс возвращается как самый внешний ресурс в ответе:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class User extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
        ];
    }

    /**
     * Customize the outgoing response for the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Http\Response  $response
     * @return void
     */
    public function withResponse($request, $response)
    {
        $response->header('X-Value', 'True');
    }
}