netsells / laravel-resourceful
Laravel API Resources supercharged to get rid of N+1 problems.
Installs: 33 995
Dependents: 0
Suggesters: 0
Security: 0
Stars: 3
Watchers: 1
Forks: 1
Open Issues: 1
Requires
- php: ^8.1 || ^8.2
- ext-json: *
- laravel/framework: ^9.0 || ^10.0
Requires (Dev)
- netsells/code-standards-laravel: ^1.1
- orchestra/testbench: ^7.0 || ^8.0
- tmarsteel/mockery-callable-mock: ~2.1
This package is auto-updated.
Last update: 2024-11-03 10:06:56 UTC
README
Package that helps you use Laravel API Resources without killing your database! Say goodbye to N+1 👋
The Problem
Picture a User model that has an "avatar" relation to a File model:
namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar_url' => $this->avatar->url, ]; } }
To return a list of users from our API, we could have such a controller action:
namespace App\Http\Controllers; use App\Models\User; class UserController extends \Illuminate\Routing\Controller { public function index() { return UserResource::collection(User::paginate()); } }
In the example above:
- if there is 1 user in our database, 2 queries will be executed
- if there are 100 users in our database, 101 queries will be executed - 1 to get the user, and then 1 query for each avatar for each user
We have a classic N+1 problem. The usual solution in Laravel is to perform eager loading within the controller:
namespace App\Http\Controllers; use App\Models\User; use Illuminate\Routing\Controller; class UserController extends Controller { public function index() { return UserResource::collection(User::query()->with('avatar')->paginate()); } }
This works, but has few problems:
- requires the controller to be aware of inner structure of each API resource
- in the example above, our controller had to know that we need access to avatar relation in the User resource
- this is hard to maintain, as we need to keep multiple places in sync
- it's hard to make the eager loading conditional
- in the example above, we may want to return the avatar_url only for users on a certain membership plan
- and if we won't be returning the avatar_url, then why load the Avatar model in the first place?
- it's hard to keep the eager loading DRY, since it is tied to the controller and not the API resource
- imagine an ArticleResource, that returns a list of related reviewers, each reviewer being a UserResource
- fetching the Articles in controller would need to eager load not just the related users, but also the avatars for each user
- since the eager loading is spread in multiple places, any N+1 optimisation is done for each place separately and benefits only that place
- other endpoints relying on affected API resource will still have N+1 problem
This package fixes all those problems and makes the N+1 problem within your API a thing of the past!
Installation
using composer:
composer require netsells/laravel-resourceful
Usage
This package is a drop in replacement for the official API Resources. Instead of extending the builtin Illuminate\Http\Resources\Json\JsonResource
we extend Netsells\Http\Resources\Json\JsonResource
.
Continuing with the example above, we get:
namespace App\Http\Resources; use Netsells\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar_url' => $this->avatar->url, ]; } }
Of course, this doesn't change anything and things still work as they did with N+1 problem, however this first step allows you to easily switch over to this package.
There are few ways now to supercharge your API resource.
Preloads method
First way is to select the relations for eager loading in a dedicated preloads
method.
You should return one or more "deferred resources", which can be easily created by calling preload
helper with a list of relations you want loaded.
The preloads
method can optionally accept a Laravel Request as its first argument. Within the preloads
method we have access to the Eloquent model being resolved.
This allows us to introduce conditional logic for preloading specific relations based on request parameters / model attributes.
namespace App\Http\Resources; use Illuminate\Http\Request; use Netsells\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function preloads() { return $this->preload('avatar'); } public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar_url' => $this->avatar->url, ]; } }
Now before the toArray
is called, the avatar
relation will be loaded for all the users in collection, so we've got full access to $this->avatar
without issuing further queries.
More examples can be seen in the tests.
Use callback method
Second way is to provide a callback which will be called with the resolved relations.
namespace App\Http\Resources; use Illuminate\Http\Request; use Netsells\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar_url' => $this->use('avatar', fn ($avatar) => $avatar->url), ]; } }
Now when toArray
is called, the avatar
relation hasn't been loaded yet. Our code in the toArray
method will execute for all the users in collection, and each call to the use
method will queue a relation to be loaded. We'll then resolve all the queued relations for all users in the most optimal way, and invoke the callback function with resolved relations.
More examples can be seen in the tests.
Inline method
Third way is to use an inline shortcut which is very convenient for loading one or many nested API Resources.
namespace App\Http\Resources; use Illuminate\Http\Request; use Netsells\Http\Resources\Json\JsonResource; class UserResource extends JsonResource { public function toArray(): array { return [ 'id' => $this->id, 'name' => $this->name, 'avatar' => $this->one('avatar', AvatarResource::class), ]; } } class AvatarResource extends JsonResource { public function preloads() { return $this->preload('file'); } public function toArray(): array { return [ 'id' => $this->id, 'width' => $this->width, 'height' => $this->height, 'url' => $this->file->s3Url(), ]; } }
In the example above, we have introduced a nested resource, where a UserResource embeds a separate AvatarResource. Each resource is responsible for eager loading its own relations, and together all relations are loaded in most optimal way.