Decorating Laravel repositories

Published by at 1st March 2017 11:16 pm

As mentioned previously, when building any nontrivial Laravel application, it's prudent to decouple our controllers from the Eloquent ORM (or any other ORM or data source we may be using) by creating an interface, and then writing a repository that implements that interface. We can then resolve the interface to our repository, and use the repository to interact with our data source. Should we need to switch to a different implementation, we then need only create the new repository and amend how Laravel resolves that interface.

The same principle applies when it comes to caching. Database queries are typically a major bottleneck in a web application, and so it's prudent to implement some form of caching for your queries. However, it's a bad idea to do so in your controllers, because just as with Eloquent models, you're tying yourself to one particular implementation and won't be able to switch without rewriting a good chunk of your controllers, as well as possibly having to maintain large amounts of duplicate code for when a query is made in several places.

Alternatively, you could implement caching within the methods of your repository, which might make sense for smaller projects. However, it means that your repository is now dependent on both the ORM and cache you chose. If you decide you want to change your ORM but retain the same caching system, or vice versa, you're stuck with writing a new repository to handle both, duplicating work you've already done.

Fortunately, there's a more elegant solution. Using the decorator pattern, we can create a second repository that implements the same interface and "wraps" the original repository. Each of its methods will call its counterpart in the original, and if appropriate cache the response. That way, our caching is implemented separately from our database interactions, and we can easily create a repository for a new data source without affecting the caching in the slightest.

Say we have the following interface for our User model:

1<?php
2
3namespace App\Repositories\Interfaces;
4
5interface UserRepositoryInterface {
6 public function all();
7
8 public function findOrFail($id);
9
10 public function create($input);
11}

And the following repository implements that interface:

1<?php
2
3namespace App\Repositories;
4
5use App\User;
6use App\Repositories\Interfaces\UserRepositoryInterface;
7use Hash;
8
9class EloquentUserRepository implements UserRepositoryInterface {
10
11 private $model;
12
13 public function __construct(User $model)
14 {
15 $this->model = $model;
16 }
17
18 public function all()
19 {
20 return $this->model->all();
21 }
22
23 public function findOrFail($id)
24 {
25 return $this->model->findOrFail($id);
26 }
27
28 public function create($input)
29 {
30 $user = new $this->model;
31 $user->email = $input['email'];
32 $user->name = $input['name'];
33 $user->password = Hash::make($input['password']);
34 $user->save();
35 return $user;
36 }
37}

We might implement the following repository class to handle caching:

1<?php
2
3namespace App\Repositories\Decorators;
4
5use App\Repositories\Interfaces\UserRepositoryInterface;
6use Illuminate\Contracts\Cache\Repository as Cache;
7
8class CachingUserRepository implements UserRepositoryInterface {
9
10 protected $repository;
11
12 protected $cache;
13
14 public function __construct(UserRepositoryInterface $repository, Cache $cache)
15 {
16 $this->repository = $repository;
17 $this->cache = $cache;
18 }
19
20 public function all()
21 {
22 return $this->cache->tags('users')->remember('all', 60, function () {
23 return $this->repository->all();
24 });
25 }
26
27 public function findOrFail($id)
28 {
29 return $this->cache->tags('users')->remember($id, 60, function () use ($id) {
30 return $this->repository->findOrFail($id);
31 });
32 }
33
34 public function create($input)
35 {
36 $this->cache->tags('users')->flush();
37 return $this->repository->create($input);
38 }
39}

Note how each method doesn't actually do any querying. Instead, the constructor accepts an implementation of the same interface and the cache, and we defer all interactions with the database to that implementation. Each call that queries the database is wrapped in a callback so that it's stored in Laravel's cache when it's returned, without touching the original implementation. When a user is created, the users tag is flushed from the cache so that stale results don't get served.

To actually use this implementation, we need to update our service provider so that it resolves the interface to an implementation of our decorator:

1<?php
2
3namespace App\Providers;
4
5use Illuminate\Support\ServiceProvider;
6
7class AppServiceProvider extends ServiceProvider
8{
9 /**
10 * Bootstrap any application services.
11 *
12 * @return void
13 */
14 public function boot()
15 {
16 //
17 }
18
19 /**
20 * Register any application services.
21 *
22 * @return void
23 */
24 public function register()
25 {
26 $this->app->singleton('App\Repositories\Interfaces\UserRepositoryInterface', function () {
27 $baseRepo = new \App\Repositories\EloquentUserRepository(new \App\User);
28 $cachingRepo = new \App\Repositories\Decorators\CachingUserRepository($baseRepo, $this->app['cache.store']);
29 return $cachingRepo;
30 });
31 }
32}

We instantiate the base repository, passing it the appropriate model. Then we instantiate the decorator, passing it the base repository and the cache, and return it. Now our controllers will start using the new decorator.

Testing the decorator

Now that we have a working decorator, how do we test it? Just as with the decorator itself, we want our tests to be completely decoupled from any particular implementation of the dependencies. If in future we're asked to migrate the database to MongoDB, say, we'll have plenty of work writing our new database repositories, so we don't want to have to rewrite the tests for our decorator as well. Fortunately, using Mockery we can just mock the interface for the repository, and pass that mock into the constructor of the decorator in our test. That way we can have the mock return a known response and not involve either the database repository or the underlying models in any way.

We will also want to mock the cache itself, as this is a unit test and so as far as possible it should not be testing anything outside of the repository class. Here's an example of how we might test the above decorator.

1<?php
2
3namespace Tests\Repositories\Decorators;
4
5use Tests\TestCase;
6use App\Repositories\Decorators\CachingUserRepository;
7use Mockery as m;
8
9class UserTest extends TestCase
10{
11 /**
12 * Test fetching all items
13 *
14 * @return void
15 */
16 public function testFetchingAll()
17 {
18 // Create mock of decorated repository
19 $repo = m::mock('App\Repositories\Interfaces\UserRepositoryInterface');
20 $repo->shouldReceive('all')->andReturn([]);
21
22 // Create mock of cache
23 $cache = m::mock('Illuminate\Contracts\Cache\Repository');
24 $cache->shouldReceive('tags')->with('users')->andReturn($cache);
25 $cache->shouldReceive('remember')->andReturn([]);
26
27 // Instantiate the repository
28 $repository = new CachingUserRepository($repo, $cache);
29
30 // Get all
31 $items = $repository->all();
32 $this->assertCount(0, $items);
33 }
34
35 /**
36 * Test fetching a single item
37 *
38 * @return void
39 */
40 public function testFindOrFail()
41 {
42 // Create mock of decorated repository
43 $repo = m::mock('App\Repositories\Interfaces\UserRepositoryInterface');
44 $repo->shouldReceive('findOrFail')->with(1)->andReturn(null);
45
46 // Create mock of cache
47 $cache = m::mock('Illuminate\Contracts\Cache\Repository');
48 $cache->shouldReceive('tags')->with('users')->andReturn($cache);
49 $cache->shouldReceive('remember')->andReturn(null);
50
51 // Instantiate the repository
52 $repository = new CachingUserRepository($repo, $cache);
53
54 // Get all
55 $item = $repository->findOrFail(1);
56 $this->assertNull($item);
57 }
58
59 /**
60 * Test creating a single item
61 *
62 * @return void
63 */
64 public function testCreate()
65 {
66 // Create mock of decorated repository
67 $repo = m::mock('App\Repositories\Interfaces\UserRepositoryInterface');
68 $repo->shouldReceive('create')->with(['email' => 'bob@example.com'])->andReturn(true);
69
70 // Create mock of cache
71 $cache = m::mock('Illuminate\Contracts\Cache\Repository');
72 $cache->shouldReceive('tags')->with('usersUser')->andReturn($cache);
73 $cache->shouldReceive('flush')->andReturn(true);
74
75 // Instantiate the repository
76 $repository = new CachingUserRepository($repo, $cache);
77
78 // Get all
79 $item = $repository->create(['email' => 'bob@example.com']);
80 $this->assertTrue($item);
81 }
82
83 public function tearDown()
84 {
85 m::close();
86 parent::tearDown();
87 }
88}

As you can see, all we care about is that the underlying repository interface receives the correct method calls and arguments, nothing more. That way our test is fast and repository-agnostic.

Other applications

Here I've used this technique to cache the queries, but that's not the only use case for decorating a repository. For instance, you could decorate a repository to fire events when certain methods are called, and write different decorators when reusing these repositories for different applications. You could create one to log interactions with the repository, or you could use an external library to cache your queries, all without touching your existing repository. Should we need to switch back to our base repository, it's just a matter of amending the service provider accordingly as both the decorator and the repository implement the same interface.

Creating decorators does mean you have to implement all of the interface's methods again, but if you have a base repository that your other ones inherit from, you can easily create a base decorator in a similar fashion that wraps methods common to all the repositories, and then just implement the additional methods for each decorator as required. Also, each method is likely to be fairly limited in scope so it's not generally too onerous.