Laravel -Eloquent API Resources

Introduction

When we build an API, we may need a transformation layer that will sit between our Eloquent models and the JSON responses that are actually returned to users of our application. Laravel’s resource classes allow us to expressively and easily transform our models and collections of our into JSON.

Generating Resources

For us to generate a resource class, we can use the make:resource Artisan command. By default, all resources will be placed in the app/Http/Resources directory of our application. Resources will extend the Illuminate\Http\Resources\Json\JsonResource class:

php artisan make:resource User

Resource Collections

In addition to generating resources that will transform individual models, you can generate resources that are responsible for transforming collections of models. This will allow your response to include links and other meta information that will be relevant to an entire collection of a given resource.

If you want to create a resource collection, then use the –collection flag when creating the resource. Or, if you include the word Collection in the resource name, it will indicate to Laravel that it should create a collection resource. The Collection resources extend the Illuminate\Http\Resources\Json\ResourceCollection class:

php artisan make:resource Users -collection
php artisan make:resource UserCollection

Concept Overview

Before we dive into all of the options available to us when writing resources, let us first take a high-level look at how resources are used within Laravel. Every resource class represents a single model that needs to be transformed into a JSON structure. For instance, consider is a simple User resource class:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class User extends JsonResource
{
    /**
     * this will 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,
        ];
    }
}

All resource class defines a toArray method which will return the array of attributes that should be converted to JSON when sending the response. Note that we can access model properties directly from the $this variable. This is because a resource class will automatically proxy method and property access down to the underlying model for convenient access. Immediately the resource is defined, it can be returned from a route or controller:

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

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

Resource Collections

In the case where you are returning a collection of resources or you are returning a paginated response, you can use the collection method when creating the resource instance in your route or controller:

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

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

Note that this doesn’t allow any addition of meta data that may need to be returned with the collection. If you want to customize the resource collection response, you can create a dedicated resource to represent the collection:

php artisan make:resource UserCollection

Once you generate the resource collection class, you can easily define any meta data that should be included with the response:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * this transforms 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',
            ],
        ];
    }
}

After you have defined your resource collection, it can be returned from a route or controller:

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

Route::get('/users', function () {
    return new UserCollection(User::all());
});
Customizing The Underlying Resource Class

Typically, the $this->collection property of a resource collection will be automatically populated with the result of mapping each item of the collection to its singular resource class. The singular resource class will be assumed to be the collection’s class name without the trailing Collection string.

For instance, the UserCollection will attempt to map the given user instances into the User resource. If you want to customize this behavior, you can override the $collects property of your resource collection:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

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

Writing Resources

Resources are very simple and this is a fact. They will only need to transform a given model into an array. So, every resource contains a toArray method which will translate your model’s attributes into an API friendly array that can be returned to your users:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class User extends JsonResource
{
    /**
     * Transforms 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,
        ];
    }
}

Once a resource is defined, it can be returned directly from a route or controller:

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

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

Relationships

If you want to include related resources in your response, you can add them to the array returned by your toArray method. In example below, we will use the Post resource’s collection method to add the blog posts of a user to the resource response:

/**
 * Transforms 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,
    ];
}

Resource Collections

A resource will translate a single model into an array, while a resource collection translates a collection of models into an array. It is not absolutely necessary to define a resource collection class for all of your model types since all resources will provide a collection method to generate an “ad-hoc” resource collection on the fly:

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

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

However, if you have to customize the meta data returned with the collection, it is necessary to define a resource collection:

<?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',
            ],
        ];
    }
}

Just like singular resources, resource collections can be returned directly from routes or controllers:

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

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

Data Wrapping

By default, your outer-most resource will be wrapped in a data key when the resource response is converted to JSON. A typical resource collection response will look like the following:

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

If you want to disable the wrapping of the outer-most resource, you can use the withoutWrapping method on the base resource class. In typical cases, you need to call this method from your AppServiceProvider or any other service provider that is loaded on every request to your application:

<?php

namespace App\Providers;

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

class AppServiceProvider extends ServiceProvider
{
    /**
     * Perform post-registration booting of services.
     *
     * @return void
     */
    public function boot()
    {
        Resource::withoutWrapping();
    }

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

Wrapping Nested Resources

You have total freedom to determine how the relationships of your resource is wrapped. If you want all resource collections to be wrapped in a data key, regardless of their nesting, you have to define a resource collection class for each resource and return the collection within a data key.

By now, you must be wondering if this will cause your outer-most resource to be wrapped in two data keys. Do not worry, Laravel will never allow your resources to be accidentally double-wrapped, so you do not have to be concerned about the nesting level of the resource collection you are transforming:

<?php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

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

Data Wrapping And Pagination

When you return paginated collections in a resource response, Laravel will wrap your resource data in a data key even if you have called the withoutWrapping method. This is because paginated responses will always contain links and meta keys with information about the paginator’s state:

{
    "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
    }
}

Pagination

You can always pass a paginator instance to the collection method of a resource or to a custom resource collection:

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

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

Paginated responses will always contain links and meta keys with information about the paginator’s state:

{
    "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
    }
}

Conditional Attributes

Sometimes you only want to include an attribute in a resource response if a given condition is met. For instance, you may want to include a value only if the current user is an “administrator”. Laravel provides a variety of helper methods that assists you in this situation. The when method can be used to conditionally add an attribute to a resource response:

/**
 * Transforms 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,
    ];
}

In above example, the secret key is only returned in the final resource response if the authenticated user’s isAdmin method returns true. In the case where the method returns false, the secret key is removed from the resource response entirely before it is sent back to the client. The when method will allow you to expressively define your resources without resorting to conditional statements when building the array.

The when method will also accept a Closure as its second argument, this allows you to calculate the resulting value only if the given condition is true:

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

Merging Conditional Attributes

There are times when you have several attributes that should only be included in the resource response based on the same condition. In such cases, you can use the mergeWhen method to include the attributes in the response only when the given condition is true:

/**
 * Transforms 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,
    ];
}

Again, when the given condition is false, these attributes are removed from the resource response entirely before they are sent to the client.

Conditional Relationships

Aside loading attributes conditionally, you can conditionally include relationships on your resource responses based on if the relationship has already been loaded on the model. This will allow your controller to decide which relationships should be loaded on the model and your resource will easily include them only when they have actually been loaded.

Ultimately, this will make it easier to avoid “N+1” query problems within your resources. The whenLoaded method can be used to conditionally load a relationship. In order for us to avoid unnecessarily loading relationships, this method will accept the name of the relationship instead of the relationship itself:

/**
 * The resource is transformed 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,
    ];
}

In the above example, if the relationship hasn’t been loaded, the posts key is removed from the resource response entirely before it is sent to the client.

Conditional Pivot Information

Aside conditionally including relationship information in your resource responses, you can conditionally include data from the intermediate tables of many-to-many relationships using the whenPivotLoaded method. The whenPivotLoaded method will accept the name of the pivot table as its first argument. The second argument has to be a Closure that defines the value to be returned if the pivot information is available on the model:

/**
 * The resource will be transformed 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;
        }),
    ];
}

If your intermediate table is using an accessor rather than a pivot, you can use the whenPivotLoadedAs method:

/**
 * 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;
        }),
    ];
}

Adding Meta Data

Some JSON API standards will require the addition of meta data to your resource and resource collections responses. This often includes things such as links to the resource or related resources, or the meta data about the resource itself. If you have to return additional meta data about a resource, you should include it in your toArray method. For instance, you might include link information when you are transforming a resource collection:

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

When you return additional meta data from your resources, you don’t have to worry about accidentally overriding the meta or links keys that are automatically added by Laravel when returning paginated responses. All additional links you define will be merged with the links provided by the paginator.

Top Level Meta Data

There are times when you may wish to only include certain meta data with a resource response if the resource is the outer-most resource being returned. Typically, this will include meta information about the response as a whole. If you want to define this meta data, you should add a with method to your resource class. This method has to return an array of meta data to be included with the resource response only when the resource is the outer-most resource being rendered:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\ResourceCollection;

class UserCollection extends ResourceCollection
{
    /**
     * Transforms 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 has to be returned with the resource array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function with($request)
    {
        return [
            'meta' => [
                'key' => 'value',
            ],
        ];
    }
}

Adding Meta Data When Constructing Resources

You can also add top-level data when constructing resource instances in your route or controller. This additional method, that is available on all resources, will accept an array of data that should be added to the resource response:

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

Resource Responses

As you have already read, resources may be returned directly from routes and controllers:

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

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

However, sometimes you may want to customize the outgoing HTTP response before it is sent to the client. There are two ways to achieve this. First, you can chain the response method onto the resource. This method returns an Illuminate\Http\Response instance, allowing you a full control of the response’s headers:

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

Alternatively, you can define a withResponse method within the resource itself. This method is called when the resource is returned as the outer-most resource in a response:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

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

    /**
     * Customizing 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');
    }
}

 

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *