timacdonald / json-api
A Lightweight JSON:API Resource for Laravel
Installs: 366 116
Dependents: 3
Suggesters: 0
Security: 0
Stars: 581
Watchers: 12
Forks: 38
Open Issues: 8
Requires
- php: ~8.1.0 || ~8.2.0 || ~8.3.0
- illuminate/collections: ^9.0 || ^10.0 || ^11.0
- illuminate/database: ^9.0 || ^10.0 || ^11.0
- illuminate/http: ^9.0 || ^10.0 || ^11.0
- illuminate/support: ^9.0 || ^10.0 || ^11.0
- symfony/http-kernel: ^6.0 || ^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.13
- laravel/framework: ^9.0 || ^10.0 || ^11.0
- opis/json-schema: ^2.3
- orchestra/testbench: ^7.0 || ^8.0 || ^9.0
- phpunit/phpunit: ^9.0 || ^10.5
This package is auto-updated.
Last update: 2024-11-07 20:14:38 UTC
README
JSON:API
Resource for Laravel
A lightweight API resource for Laravel that helps you adhere to the JSON:API
standard with support for sparse fieldsets, compound documents, and more baked in.
Note These docs are not designed to introduce you to the
JSON:API
specification and the associated concepts, instead you should head over and read the specification if you are not yet familiar with it. The documentation that follows only covers how to implement the specification via the package.
Table of contents
Version support
- PHP:
8.1
,8.2
,8.3
- Laravel:
9.0
,10.0
,11.0
Installation
You can install using composer from Packagist.
composer require timacdonald/json-api
Getting started
The JsonApiResource
class provided by this package is a specialisation of Laravel's Eloquent API resource. All public facing APIs are still accessible; in a controller, for example, you interact with a JsonApiResource
as you would with Laravel's JsonResource
class.
<?php namespace App\Http\Controllers; use App\Http\Resources\UserResource; use App\Models\User; class UserController { public function index() { $users = User::with([/* ... */])->paginate(); return UserResource::collection($users); } public function show(User $user) { $user->load([/* ... */]); return UserResource::make($user); } }
As we make our way through the examples you will see that new APIs are introduced when interacting with the class internally, for example, the toArray()
method is no longer used.
Creating your first JSON:API
resource
To get started, let's create a UserResource
for our User
model. In our user resource will expose the user's name
, website
, and twitter_handle
in the response.
First we will create a new API resource that extends TiMacDonald\JsonApi\JsonApiResource
.
<?php namespace App\Http\Resources; use TiMacDonald\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { // }
Adding attributes
We will now create an $attributes
property and list the model's attributes we want to expose.
<?php namespace App\Http\Resources; use TiMacDonald\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { public $attributes = [ 'name', 'website', 'twitter_handle', ]; }
When making a request to an endpoint that returns the UserResource
, for example:
Route::get('users/{user}', fn (User $user) => UserResource::make($user));
The following JSON:API
formatted data would be returned:
{ "data": { "type": "users", "id": "74812", "attributes": { "name": "Tim", "website": "https://timacdonald.me", "twitter_handle": "@timacdonald87" } } }
🎉 You have just created your first JSON:API
resource 🎉
Congratulations...and what. a. rush!
We will now dive into adding relationships to your resources, but if you would like to explore more complex attribute features you may like to jump ahead:
Adding relationships
Available relationships may be specified in a $relationships
property, similar to the $attributes
property, however you may use a key / value pair to provide the resource class that should be used for the given relationship.
We will make two relationships available on the resource:
$user->team
: a "toOne" /HasOne
relationship.$user->posts
: a "toMany" /HasMany
relationship.
<?php namespace App\Http\Resources; use TiMacDonald\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { public $attributes = [ 'name', 'website', 'twitter_handle', ]; public $relationships = [ 'team' => TeamResource::class, 'posts' => PostResource::class, ]; }
Assuming the key / value pair follows the convention '{myKey}' => {MyKey}Resource::class
, the class may be omitted to streamline things further.
<?php namespace App\Http\Resources; use TiMacDonald\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { public $attributes = [ 'name', 'website', 'twitter_handle', ]; public $relationships = [ 'team', 'posts', ]; }
Example request and response
The client may now request these relationships via the include
query parameter.
GET /users/74812?include=posts,team
Note Relationships are not exposed in the response unless they are requested by the calling client via the
include
query parameter. This is intended and is part of theJSON:API
specification.
{ "data": { "id": "74812", "type": "users", "attributes": { "name": "Tim", "website": "https://timacdonald.me", "twitter_handle": "@timacdonald87" }, "relationships": { "posts": { "data": [ { "type": "posts", "id": "25240" }, { "type": "posts", "id": "39974" } ] }, "team": { "data": { "type": "teams", "id": "18986" } } } }, "included": [ { "id": "25240", "type": "posts", "attributes": { "title": "So what is `JSON:API` all about anyway?", "content": "...", "excerpt": "..." } }, { "id": "39974", "type": "posts", "attributes": { "title": "Building an API with Laravel, using the `JSON:API` specification.", "content": "...", "excerpt": "..." } }, { "id": "18986", "type": "teams", "attributes": { "name": "Laravel" } } ] }
To learn about more complex relationship features you may like to jump ahead:
A note on eager loading
This package does not eager load Eloquent relationships. If a relationship is not eagerly loaded, the package will lazy load the relationship on the fly. I highly recommend using Spatie's query builder package which will eager load your models against the JSON:API
query parameter standards.
Spatie provide comprehensive documentation on how to use the package, but I will briefly give an example of how you might use this in a controller.
<?php namespace App\Http\Controllers; use App\Http\Resources\UserResource; use App\Models\User; use Spatie\QueryBuilder\QueryBuilder; class UserController { public function index() { $users = QueryBuilder::for(User::class) ->allowedIncludes(['team', 'posts']) ->paginate(); return UserResource::collection($users); } public function show($id) { $user = QueryBuilder::for(User::class) ->allowedIncludes(['team', 'posts']) ->findOrFail($id); return UserResource::make($user); } }
Digging deeper
We have now covered the basics of exposing attributes and relationships on your resources. We will now cover more advanced topics to give you even greater control.
Attributes
toAttributes()
As we saw in the adding attributes section, the $attributes
property is the fastest way to expose attributes for a resource. In some scenarios you may need greater control over the attributes you are exposing. If that is the case, you may implement the toAttributes()
method. This will grant you access to the current request and allow for conditional logic.
<?php namespace App\Http\Resources; use TiMacDonald\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { /** * @param \Illuminate\Http\Request $request * @return array<string, mixed> */ public function toAttributes($request) { return [ 'name' => $this->name, 'website' => $this->website, 'twitter_handle' => $this->twitter_handle, 'email' => $this->when($this->email_is_public, $this->email, '<private>'), 'address' => [ 'city' => $this->address('city'), 'country' => $this->address('country'), ], ]; } }
Example response
{ "data": { "id": "74812", "type": "users", "attributes": { "name": "Tim", "website": "https://timacdonald.me", "twitter_handle": "@timacdonald87", "email": "<private>", "address": { "city": "Melbourne", "country": "Australia" } } } }
Sparse fieldsets
Sparse fieldsets are a feature of the JSON:API
specification that allows clients to specify which attributes, for any given resource type, they would like to receive. This allows for more deterministic responses, while also improving server-side performance and reducing payload sizes. Sparse fieldsets work out of the box for your resources.
We will cover them briefly here, but we recommend reading the specification to learn more.
As an example, say we are building out an index page for a blog. The page will show each post's title
and excerpt
, and also the name
of the post's author. If the client wishes, they may limit the response to only include the required attributes for each resource type, and exclude the other attributes, such as the post's content
and the authors twitter_handle
.
To achieve this we will send the following request.
GET /posts?include=author&fields[posts]=title,excerpt&fields[users]=name
Note The
include
query parameter key isauthor
, while the sparse fieldset parameter key isusers
. This is because authors are users, e.g. the Eloquentauthor()
relationship returns aUser
model.
Example response
{ "data": [ { "id": "25240", "type": "posts", "attributes": { "title": "So what is `JSON:API` all about anyway?", "excerpt": "..." }, "relationships": { "author": { "data": { "type": "users", "id": "74812" } } } }, { "id": "39974", "type": "posts", "attributes": { "title": "Building an API with Laravel, using the `JSON:API` specification.", "excerpt": "..." }, "relationships": { "author": { "data": { "type": "users", "id": "74812" } } } } ], "included": [ { "type": "users", "id": "74812", "attributes": { "name": "Tim" } } ] }
Minimal attributes
Resources return a maximal attribute payload when sparse fieldsets are not in use i.e. all declared attributes on the resource are returned. If you prefer you can make the use of sparse fieldsets required in order to retrieve any attributes.
You may call the useMinimalAttributes()
method in an application service provider.
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use TiMacDonald\JsonApi\JsonApiResource; class AppServiceProvider extends ServiceProvider { public function boot() { JsonApiResource::useMinimalAttributes(); // ... } }
Lazy attribute evaluation
For attributes that are expensive to calculate, it is possible to have them evaluated only when they are to be included in the response, i.e. they have not been excluded via sparse fieldsets or minimal attributes. This may be useful if you are interacting with a database or making HTTP requests in a resource.
As an example, let's imagine that we expose a base64 encoded avatar for each user. Our implementation downloads the avatar from our in-house avatar microservice.
<?php namespace App\Http\Resources; use Illuminate\Support\Facades\Http; use TiMacDonald\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { /** * @param \Illuminate\Http\Request $request * @return array<string, mixed> */ public function toAttributes($request) { return [ // ... 'avatar' => Http::get("https://avatar.example.com/{$this->id}")->body(), ]; } }
The above implementation would make a HTTP request to our microservice even when the client is excluding the avatar
attribute via sparse fieldsets or minimal attributes. To improve performance when this attribute is not being returned we can wrap the value in a Closure
. The Closure
will only be evaluated when the avatar
is to be returned.
<?php namespace App\Http\Resources; use Illuminate\Support\Facades\Http; use TiMacDonald\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { /** * @param \Illuminate\Http\Request $request * @return array<string, mixed> */ public function toAttributes($request) { return [ // ... 'avatar' => fn () => Http::get("https://avatar.example.com/{$this->id}")->body(), ]; } }
Relationships
toRelationships()
As we saw in the adding relationships section, the $relationships
property is the fastest way to specify the available relationships for a resource. In some scenarios you may need greater control over the relationships you are making available. If that is the case, you may implement the toRelationships()
method. This will grant you access to the current request and allow for conditional logic.
The value must always be wrapped in a Closure
, which will only be called if the relationships is requested by the client.
<?php namespace App\Http\Resources; use TiMacDonald\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { /** * @param \Illuminate\Http\Request $request * @return array<string, (callable(): \TiMacDonald\JsonApi\JsonApiResource|\TiMacDonald\JsonApi\JsonApiResourceCollection|\Illuminate\Http\Resources\PotentiallyMissing)> */ public function toRelationships($request) { return [ 'team' => fn () => TeamResource::make($this->team), 'posts' => fn () => $request->user()->is($this->resource) ? PostResource::collection($this->posts) : PostResource::collection($this->posts->where('published', true)), ]; } }
Customising the relationship resource class guessing
//----- Everything that follows is WIP and should be ignored ------- //
Resource Identification
[JSON:API
docs: Identification](https://jsonapi.org/format/#document-resource-object-identification)
We have defined a sensible default for you so you can hit the ground running without having to fiddle with the small stuff.
The "id"
and "type"
of a resource is automatically resolved for you under-the-hood if you are using resources solely with Eloquent models.
"id"
is resolved by calling the $model->getKey()
method and the "type"
is resolved by using a camel case of the model's table name, e.g. blog_posts
becomes blogPosts
.
You can customise how this works to support other types of objects and behaviours, but that will follow in the advanced usage section.
Nice. Well that was easy, so let's move onto...
Resource Links
[JSON:API
docs: Links](https://jsonapi.org/format/#document-resource-object-links)
To provide links for a resource, you can implement the toLinks($request)
method...
<?php use TiMacDonald\JsonApi\Link; class UserResource extends JsonApiResource { public function toLinks($request): array { return [ Link::self(route('users.show', $this->resource)), 'related' => 'https://example.com/related' ]; } }
Resource Meta
[JSON:API
docs: Meta](https://jsonapi.org/format/#document-meta)
To provide meta information for a resource, you can implement the toMeta($request)
method...
<?php class UserResource extends JsonApiResource { public function toMeta($request): array { return [ 'resourceDeprecated' => true, ]; } }
Advanced usage
Resource Identification
Customising the resource "id"
You can customise the resolution of the id
by specifying an id resolver in your service provider.
<?php class AppServiceProvider extends ServiceProvider { public function boot() { JsonApiResource::resolveIdUsing(function (mixed $resource, Request $request): string { // your custom resolution logic... }); } }
Although it is not recommended, you can also override the toId(Request $request): string
method on a resource by resource basis.
Customising the resource "type"
You can customise the resolution of the type
by specifying a type resolver in your service provider.
<?php class AppServiceProvider extends ServiceProvider { public function boot() { JsonApiResource::resolveTypeUsing(function (mixed $resource, Request $request): string { // your custom resolution logic... }); } }
Although it is not recommended, you can also override the toType(Request $request): string
method on a resource by resource basis.
Resource Relationships
[JSON:API
docs: Inclusion of Related Resources](https://jsonapi.org/format/#fetching-includes)
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
- Using "whenLoaded is an anti-pattern"
Credits
- Tim MacDonald
- Jess Archer for co-creating our initial in-house version and the brainstorming
- All Contributors
And a special (vegi) thanks to Caneco for the logo ✨
v1 todo
- Server implementation rethink.
- Rethink naming of objects and properties
- Camel case everything
- Allow resources to specify their JsonResource class.
- Make all caches WeakMaps.
- Things that "must" need to be first in the __consstructor. See Links:href
- Should it be withResourceIdentifier or mapResourceIdentifier. Feel like we are mapping. or pipeResourceIdentifier
- Should all caches use weakmap with request key?