Caching the Laravel user provider with a decorator

Published by at 11th March 2020 9:20 pm

A couple of years ago I posted this article about constructing a caching user provider for Laravel. It worked, but with the benefit of hindsight I can now see that there were a number of issues with this solution:

  • Because it extended the existing Eloquent user provider, it was dependent on the internals of that remaining largely the same - any change in how that worked could potentially break it
  • For the same reason, if you wanted to switch to a different user provider, you'd need to add the same functionality to that provider, either by writing a new provider from scratch or extending an existing one

I've used the decorator pattern a few times in the past, and it's a good fit for situations like this where you want to add functionality to something that implements an interface. It allows you to separate out one part of the functionality (in this case, caching) into its own layer, so it's not dependent on any one implementation and can wrap any other implementation of that same interface you wish. Also, as long as the interface remains the same, there likely won't be any need to change it when the implementation that is wrapped changes. Here I'll demonstrate how to create a decorator to wrap the existing user providers.

If we only want to cache the retrieveById() method, like the previous implementation, the decorator class might look something like this:

1<?php
2
3namespace App\Auth;
4
5use Illuminate\Contracts\Auth\Authenticatable;
6use Illuminate\Contracts\Auth\UserProvider;
7use Illuminate\Contracts\Cache\Repository;
8
9final class UserProviderDecorator implements UserProvider
10{
11 /**
12 * @var UserProvider
13 */
14 private $provider;
15
16 /**
17 * @var Repository
18 */
19 private $cache;
20
21 public function __construct(UserProvider $provider, Repository $cache)
22 {
23 $this->provider = $provider;
24 $this->cache = $cache;
25 }
26
27 /**
28 * {@inheritDoc}
29 */
30 public function retrieveById($identifier)
31 {
32 return $this->cache->remember('id-' . $identifier, 60, function () use ($identifier) {
33 return $this->provider->retrieveById($identifier);
34 });
35 }
36
37 /**
38 * {@inheritDoc}
39 */
40 public function retrieveByToken($identifier, $token)
41 {
42 return $this->provider->retrieveById($identifier, $token);
43 }
44
45 /**
46 * {@inheritDoc}
47 */
48 public function updateRememberToken(Authenticatable $user, $token)
49 {
50 return $this->provider->updateRememberToken($user, $token);
51 }
52
53 /**
54 * {@inheritDoc}
55 */
56 public function retrieveByCredentials(array $credentials)
57 {
58 return $this->provider->retrieveByCredentials($credentials);
59 }
60
61 /**
62 * {@inheritDoc}
63 */
64 public function validateCredentials(Authenticatable $user, array $credentials)
65 {
66 return $this->provider->validateCredentials($user, $credentials);
67 }
68}

It implements the same interface as the user providers, but accepts two arguments in the constructor, which are injected and stored as properties:

  • Another instance of Illuminate\Contracts\Auth\UserProvider
  • An instance of the cache repository Illuminate\Contracts\Cache\Repository

Most of the methods just defer to their counterparts on the wrapped instance - in this example I have cached the response to retrieveById() only, but you can add caching to the other methods easily enough if need be. You do of course still need to flush the cache at appropriate times, which is out of scope for this example, but can be handled by model events as appropriate, as described in the prior article.

Then you add the new decorator as a custom user provider, but crucially, you need to first resolve the provider you're going to use, then wrap it in the decorator:

1<?php
2
3namespace App\Providers;
4
5use Illuminate\Support\Facades\Gate;
6use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
7use Illuminate\Contracts\Auth\UserProvider;
8use Auth;
9use Illuminate\Auth\EloquentUserProvider;
10use Illuminate\Contracts\Cache\Repository;
11use App\Auth\UserProviderDecorator;
12
13class AuthServiceProvider extends ServiceProvider
14{
15 /**
16 * The policy mappings for the application.
17 *
18 * @var array
19 */
20 protected $policies = [
21 'App\Model' => 'App\Policies\ModelPolicy',
22 ];
23
24 /**
25 * Register any authentication / authorization services.
26 *
27 * @return void
28 */
29 public function boot()
30 {
31 $this->registerPolicies();
32
33 Auth::provider('cached', function ($app, array $config) {
34 $provider = new EloquentUserProvider($app['hash'], $config['model']);
35 $cache = $app->make(Repository::class);
36 return new UserProviderDecorator($provider, $cache);
37 });
38 }
39}

Finally, set up the config to use the caching provider:

1 'providers' => [
2 'users' => [
3 'driver' => 'cached',
4 'model' => App\Eloquent\Models\User::class,
5 ],
6 ],

This is pretty rough and ready, and could possibly be improved upon by allowing you to specify a particular provider to wrap in the config, as well as caching more of the methods, but it demonstrates the principle effectively.

By wrapping the existing providers, you can change the behaviour of the user provider without touching the existing implementation, which is in line with the idea of composition over inheritance. Arguably it's more complex, but it's also more flexible - if need be you can swap out the wrapped user provider easily, and still retain the same caching functionality.