This package supports the inclusion of related resources. This allows a client to specify resources related to the primary data that should be included in the response. The purpose is to allow the client to reduce the number of HTTP requests it needs to make to obtain all the data it requires.
This package supports:
- Client-specified include paths, via the
include
query parameter. - Validation of include paths provided by a client.
- Default include paths to use if the client does not specify any paths.
- Automatic translation of include paths to Eloquent eager load paths.
For this feature to work, you will need to:
- Define the allowed include paths for a resource on its Validators class. By default validators do not allow include paths.
- Add relationships to the resource Schema and ensure they return data.
- For Eloquent models, define the translation of any JSON API include paths to eager load paths on the resource Adapter.
These are all described in this chapter.
Related resources are specified by the client using the include
query parameter. This parameter
contains a comma separated list of relationship paths that should be included. The response will be a
compound document where the primary data of the
request is in the JSON's data
member, and the related resources are in the included
member.
For example, if a client is requesting posts
resources, it can obtain the related author
and tags
resources in the same request:
GET /api/posts?include=author,tags HTTP/1.1
Accept: application/vnd.api+json
If these include paths are valid, then the client will receive the following response:
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"type": "posts",
"id": "123",
"attributes": {
"title": "Hello World",
"content": "..."
},
"relationships": {
"author": {
"data": {
"type": "users",
"id": "45"
},
"links": {
"self": "/api/posts/123/relationships/author",
"related": "/api/posts/123/author"
}
},
"tags": {
"data": [
{
"type": "tags",
"id": "1"
},
{
"type": "tags",
"id": "3"
}
],
"links": {
"self": "/api/posts/123/relationships/tags",
"related": "/api/posts/123/tags"
}
}
},
"links": {
"self": "/api/posts/123"
}
}
],
"included": [
{
"type": "users",
"id": "45",
"attributes": {
"name": "John Smith"
},
"links": {
"self": "/api/users/45"
}
},
{
"type": "tags",
"id": "1",
"attributes": {
"title": "news"
},
"links": {
"self": "/api/tags/1"
}
},
{
"type": "tags",
"id": "3",
"attributes": {
"title": "world"
},
"links": {
"self": "/api/tags/3"
}
}
]
}
Include paths use dot notation to specify nested paths. For example, if our users
resource had an address
relationship, the client could request the following:
GET /api/posts?include=author.address,tags HTTP/1.1
Accept: application/vnd.api+json
For this request, both the author users
resource and the user's addresses
resource would be present in
the included
member of the JSON document.
You can use include paths when creating resources, for example:
POST /api/posts?include=author HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{...}
The same also applies for updating resources, for example:
PATCH /api/posts/123?include=author HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{...}
For both requests, the include paths refer to what the client wants included in the response.
Include paths for a resource also work on relationship endpoints where that resource is the primary data.
For example, if a comments
resource had a post
relationship, we can use include paths for a posts
resource when retrieving the related post:
GET /api/comments/456/post?include=author,tags HTTP/1.1
Accept: application/vnd.api+json
This request will contain the related post as the primary data, and include the post's author and tags
in the included
member of the compound document.
When making this request, the include paths are validated by the posts
validators, not the validators
for the comments
. This is because the include paths refer to the post's relationships.
By default validators generated by this package do not allow any include paths. This is because the Eloquent adapter automatically converts include paths to eager load paths. We therefore expect include paths to be whitelisted otherwise the client could provide a path that is not a valid model relationship path.
To allow include paths, list them on your resource's validators class using the $allowedIncludePaths
property:
namespace App\JsonApi\Posts;
use CloudCreativity\LaravelJsonApi\Validation\AbstractValidators;
class Validators extends AbstractValidators
{
protected $allowedIncludePaths = [
'author',
'author.address',
'tags'
];
// ...
}
If the client provides an invalid include path, it will receive the following response:
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
"errors": [
{
"title": "Invalid Query Parameter",
"status": "400",
"detail": "Include path foo is not allowed.",
"source": {
"parameter": "include"
}
}
]
}
For include paths to work, you must return relationship data in your resource's schema. This
is done in the getRelationships
method of your schema. For the above example to work,
the posts schema would have to return both the related author and the related tags:
namespace App\JsonApi\Posts;
use CloudCreativity\LaravelJsonApi\Schema\SchemaProvider;
class Schema extends SchemaProvider
{
// ...
public function getRelationships($post, $isPrimary, array $includeRelationships)
{
return [
'author' => [
self::SHOW_SELF => true,
self::SHOW_RELATED => true,
self::SHOW_DATA => isset($includeRelationships['author']),
self::DATA => function () use ($post) {
return $post->createdBy;
},
],
'tags' => [
self::SHOW_SELF => true,
self::SHOW_RELATED => true,
self::SHOW_DATA => isset($includeRelationships['tags']),
self::DATA => function () use ($post) {
return $post->tags;
},
],
];
}
}
In the above, the data is only shown if the relationship is included. Returning the related record is wrapped in a closure so that we only incur the cost of querying the database if the relationship is included.
If you are allowing nested paths, e.g. author.address
the posts
schema only needs
to return the related author. The author's address is returned in the getRelationships()
method on the users
schema.
The Eloquent adapter automatically converts JSON API include paths to Eloquent model
eager loading paths.
The JSON API path is converted to a camel-case path. For example, the JSON API path
author.current-address
is converted to the author.currentAddress
Eloquent path.
If this automatic conversion is not correct for a particular path, you can define the mapping
on your Eloquent adapter using the $includePaths
property. For example, if a posts
resource's author
relationship was actually the createdBy
relationship on a post model, we
would need to add the following mapping to the adapter:
namespace App\JsonApi\Posts;
use CloudCreativity\LaravelJsonApi\Eloquent\AbstractAdapter;
class Adapter extends AbstractAdapter
{
protected $includePaths = [
'author' => 'createdBy',
'author.current-address' => 'createdBy.currentAddress',
];
// ...
}
If you do not want a JSON API path to be translated to an Eloquent path, map it to null
.
For example:
protected $includePaths = [
'author' => null,
];
It is possible to define default include paths to use if the client does not specify any paths.
Default include paths are defined on a resource's schema via the getIncludePaths()
method.
For example, if we always wanted to include the author
and tags
when posts
resources are
requested as primary data:
namespace App\JsonApi\Posts;
use CloudCreativity\LaravelJsonApi\Schema\SchemaProvider;
class Schema extends SchemaProvider
{
// ...
public function getIncludePaths()
{
return ['author', 'tags'];
}
}
This would mean that the following request receive a response with users
and tags
resources in the
included
member of the JSON document:
GET /api/posts HTTP/1.1
Accept: application/vnd.api+json
Default paths are not automatically translated to Eloquent eager load paths. If you are using them, it is recommended that you also add default eager load paths to your resource adapter.
namespace App\JsonApi\Posts;
use CloudCreativity\LaravelJsonApi\Eloquent\AbstractAdapter;
class Adapter extends AbstractAdapter
{
protected $defaultWith = ['author', 'tags'];
// ...
}
Note that the default paths here are your model's relationship paths, not the JSON API include paths.