For even faster gains on high-traffic Laravel applications, you can cache authenticated users to avoid a trip to the database.
In this article, I'll show you how — but this isn't a quick fix; we'll have to consider what happens when the user gets updated or deleted.
Let's cache.
For every authenticated page or API request in your application, Laravel will fetch the fresh user from the database with a query similar to this (depending on the ID, of course):
select * from `users` where `id` = 1 limit 1
Currently, there's no way to automatically cache this user object. So, as long as your user is authenticated, this query will always be run for every request.
Since it's unlikely for your user to change that frequently between requests, it makes sense to cache this until something does change, particularly for high-traffic applications.
The first step is to understand Laravel's Auth
provider mechanism.
By default, Laravel uses the EloquentUserProvider
to manage authenticated users. This class contains many helpful methods like retrieveById
, rehashPasswordIfRequired
and validateCredentials
. Basically, everything required to fetch and update the user in relation to authentication.
You can add a new provider using the Auth::provider
method like this:
Auth::provider('someCustomProvider', function (Application $app, array $config) {
// return a custom provider here
});
Once you've added this provider at runtime, you can swap it over in the config/auth.php
config file:
'providers' => [
'users' => [
'driver' => 'someCustomProvider',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
Now we understand auth providers a little better, let's create our own!
Start by creating a provider class, CachedEloquentUserProvider
. You can name this anything you like or put it anywhere in your application:
namespace App\Auth\Providers;
use Illuminate\Auth\EloquentUserProvider;
class CachedEloquentUserProvider extends EloquentUserProvider
{
public function retrieveById($identifier)
{
//
}
}
This extends the base EloquentUserProvider
to provide all the additional functionality we want to keep. We're only concerned with overriding the retrieveById
to choose how we fetch the user.
We're not doing anything to fetch the user right now (we'll cover this next), but let's hook it up to our custom provider in the AppServerProvider
's boot
method':
public function boot(): void
{
Auth::provider('cachedEloquent', function (Application $app, array $config) {
return new CachedEloquentUserProvider(
$app['hash'],
$config['model']
);
});
}
In the constructor for the original EloquentUserProvider
, we need to pass in the current hasher (responsible for hashing passwords, etc.), as well as the model namespace from config that represents the User (typically App\Models\User
).
So that's why we've passed these two things above.
Now switch over the driver in config/auth.php
to cachedEloquent
(or whatever you named it).
'providers' => [
'users' => [
'driver' => 'cachedEloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
Your application won't be able to fetch authenticated users at this point since we've left the new retrieveById
method empty.
It's pretty much time to fill in the retrieveById
method now with a cached version of the user.
The options here are endless, but here's a good start:
public function retrieveById($identifier)
{
return cache()->remember('user_' . $identifier, now()->addHours(2), function () use ($identifier) {
return parent::retrieveById($identifier);
});
}
The $identifier
here is just the user's ID, so we pass this to the parent retrieveById
method to do its thing. But of course, we using cache()->remember
to cache and return the result.
Here's a slightly shorter way to get the same result with a PHP arrow function:
public function retrieveById($identifier)
{
return cache()->remember(
'user_' . $identifier,
now()->addHours(2),
fn () => parent::retrieveById($identifier)
);
}
Either method works fine; it depends on whether you're doing much else within the closure, in which case you'd choose a standard callback function.
By default, Laravel uses the database as the cache driver. You'll want to change this otherwise we're back to fetching the cached version of the user from the database... again.
CACHE_STORE=redis
Once that's changed, you should be able to sign into your application and see an initial database request for the user. On refresh, though, we're pulling the user's details from our (in this case, Redis) cache!
Caching is great, but we need to take care of busting (invalidating) the cache when things change.
If your user were to update their details within your application right now, they won't see these changes reflected immediately, and they'd have to wait for the cache to expire. Not ideal.
To get around this, we can simply observe the User
for changes and invalidate the cache manually.
Start by creating a UserObserver
:
php artisan make:observer UserObserver
Open it up and register events for updated
and deleted
.
class UserObserver
{
public function updated(User $user)
{
cache()->forget('user_' . $user->id);
}
public function deleted(User $user)
{
cache()->forget('user_' . $user->id);
}
}
Our cache key was set to user_[id]
, so we simply invalidate that key.
Register the observer on the User
model, and you're done:
use App\Observers\UserObserver;
#[ObservedBy(UserObserver::class)]
class User extends Authenticatable
{
//...
}
When users change or get deleted, the cache will invalidate, and we'll end up caching the fresh data (or just removing it entirely if the user is deleted).
It's crucial to note that this is a very simplistic view of invalidating the user's cached details with two Eloquent events. In reality, as your application becomes more complex, there may be edge cases where you either don't want to invalidate the cache or the cache doesn't get invalidated when you want.
For example:
DB
facade anywhere in your application to update a user, the cache won't be invalidated because an Eloquent event will not be firedAs I said at the start of the article, this isn't a quick and easy fix that doesn't require thought down the line.
Yes, it'll speed up your application. Still, it requires a little more effort on your part to ensure the cache isn't returning stale data or you're not invalidating the cache too often and rendering authenticated caching useless.