JSON:API Resource for Laravel
A lightweight Laravel implementation of JSON:API.
This is a WIP project currently being built out via livestream on my YouTube channel. Come hang out next stream.
TODO
- Top-level collections
- Nested collections
- Caching for performance (use a weakmap with the request as the key?)
- Document that this is to be used in conjunction with Spatie Query Builder
- document why whenLoaded isn't great
- How to handle single resources loading / allow listing (can we PR Spatie Query Builder for this or does it already support it?).
Basic usage
Identification
The "id"
and "type"
of a resource is automatically resolved for you under-the-hood if you are using resources solely with Eloquent models.
The default bindings resolve the "id"
by calling (string) $model->getKey()
and they resolves the "type"
by using a camel case of the model's table name, e.g. blog_posts
becomes blogPosts
.
Nice. Well that was easy, so let's move onto...
Attributes
To provide a set of attributes for a resource, you can implement the toAttributes(Request $request)
method...
class UserResource extends JsonApiResource
{
/**
* @return array
*/
public function toAttributes(Request $request): array
{
return [
'name' => $this->name,
'email' => $this->email,
];
}
}
Relationships
Just like we saw with attributes above, we can specify relationships that should be available on the resource by using the toRelationships(Request $request)
method, however with relationships you should always wrap the values in a Closure
.
class UserResource extends JsonApiResource
{
/**
* @return array
*/
public function toRelationships(Request $request): array
{
return [
'posts' => fn () => PostResource::collect($this->posts),
'subscription' => fn () => SubscriptionResource::make($this->subscription),
];
}
}
Including relationships
JSON:API docs: Inclusion of Related Resources
These relationships however, are not included in the response unless the calling client requests them. To do this, the calling client needs to "include" them by utilising the include
query parameter.
# Include the posts...
/api/users/8?include=posts
# Include the comments...
/api/users/8?include=comments
# Include both...
/api/users/8?include=posts,comments
Note: In the advanced usage you can learn how to include relationships in the response without them being included by the client.
Advanced usage
Identification
"id"
resolver
Customising the You can change the "id"
resolver via a service provider by binding your own implementation of the ResourceIdResolver
, which can be fulfilled by any callable
. The callable
receives the Resource Object as it's first parameter.
class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(ResourceIdResolver::class, fn () => function (mixed $resourceObject): string {
if ($resourceObject instanceof Model) {
return (string) $resourceObject->getKey();
}
if ($resourceObject instanceof ValueObject) {
return (string) $resourceObject->getValue();
}
if (is_object($resourceObject)) {
throw new RuntimeException('Unable to resolve Resource Object id for class '.$resourceObject::class);
}
throw new RuntimeException('Unable to resolve Resource Object id for type '.gettype($resourceObject));
});
}
}
"type"
resolver
Customising the You can change the "type"
resolver via a service provider by binding your own implementation of the ResourceTypeResolver
, which can be fulfilled by any callable
. The callable
receives the Resource Object as it's first parameter.
class AppServiceProvider
{
public function register()
{
$this->app->singleton(ResourceTypeResolver::class, fn () => function (mixed $resourceObject): string {
if (! is_object($resourceObject)) {
throw new RuntimeException('Unable to resolve Resource Object type for type '.gettype($resourceObject));
}
return match($resourceObject::class) {
User::class => 'users',
Post::class => 'posts',
Comment::class => 'comments',
default => throw new RuntimeException('Unable to resolve Resource Object type for class '.$resourceObject::class),
};
});
}
}
Attributes
Sparse fieldsets
JSON:API docs: Sparse fieldsets
Without any work, your response supports sparse fieldsets. If you are utilising sparse fieldsets and have some attributes that are expensive to create, it is a good idea to wrap them in a Closure
. Under the hood, we only call the Closure
if the attribute is to be included in the response.
class UserResource extends JsonResource
{
public function toAttributes(Request $request): array
{
return [
'name' => $this->name,
'email' => $this->email,
'profile_image' => fn () => base64_encode(
// don't really download a file like this. It's just an example of a slow operation...
file_get_contents('https://www.gravatar.com/avatar/'.md5($this->email)),
),
];
}
}
The Closure
is only called when the attribute is going to be included in the response, which improves performance of requests that don't require the returned value.
# The Closure is not called...
/api/users/8?fields[users]=name,email
# The Closure is called...
/api/users/8?fields[users]=name,profile_image
Relationships
JSON:API docs: Inclusion of Related Resources
Relationships can be resolved deeply and also multiple relationship paths can be included. Of course you should be careful about n+1 issues, which is why we recommend using this package in conjunction with Spatie's Query Builder.
# Including deeply nested relationships
/api/posts/8?include=author.comments
# Including multiple relationship paths
/api/posts/8?include=comments,author.comments