Decorating Laravel repositories
Published by Matthew Daly 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<?php23namespace App\Repositories\Interfaces;45interface UserRepositoryInterface {6 public function all();78 public function findOrFail($id);910 public function create($input);11}
And the following repository implements that interface:
1<?php23namespace App\Repositories;45use App\User;6use App\Repositories\Interfaces\UserRepositoryInterface;7use Hash;89class EloquentUserRepository implements UserRepositoryInterface {1011 private $model;1213 public function __construct(User $model)14 {15 $this->model = $model;16 }1718 public function all()19 {20 return $this->model->all();21 }2223 public function findOrFail($id)24 {25 return $this->model->findOrFail($id);26 }2728 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<?php23namespace App\Repositories\Decorators;45use App\Repositories\Interfaces\UserRepositoryInterface;6use Illuminate\Contracts\Cache\Repository as Cache;78class CachingUserRepository implements UserRepositoryInterface {910 protected $repository;1112 protected $cache;1314 public function __construct(UserRepositoryInterface $repository, Cache $cache)15 {16 $this->repository = $repository;17 $this->cache = $cache;18 }1920 public function all()21 {22 return $this->cache->tags('users')->remember('all', 60, function () {23 return $this->repository->all();24 });25 }2627 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 }3334 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<?php23namespace App\Providers;45use Illuminate\Support\ServiceProvider;67class AppServiceProvider extends ServiceProvider8{9 /**10 * Bootstrap any application services.11 *12 * @return void13 */14 public function boot()15 {16 //17 }1819 /**20 * Register any application services.21 *22 * @return void23 */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<?php23namespace Tests\Repositories\Decorators;45use Tests\TestCase;6use App\Repositories\Decorators\CachingUserRepository;7use Mockery as m;89class UserTest extends TestCase10{11 /**12 * Test fetching all items13 *14 * @return void15 */16 public function testFetchingAll()17 {18 // Create mock of decorated repository19 $repo = m::mock('App\Repositories\Interfaces\UserRepositoryInterface');20 $repo->shouldReceive('all')->andReturn([]);2122 // Create mock of cache23 $cache = m::mock('Illuminate\Contracts\Cache\Repository');24 $cache->shouldReceive('tags')->with('users')->andReturn($cache);25 $cache->shouldReceive('remember')->andReturn([]);2627 // Instantiate the repository28 $repository = new CachingUserRepository($repo, $cache);2930 // Get all31 $items = $repository->all();32 $this->assertCount(0, $items);33 }3435 /**36 * Test fetching a single item37 *38 * @return void39 */40 public function testFindOrFail()41 {42 // Create mock of decorated repository43 $repo = m::mock('App\Repositories\Interfaces\UserRepositoryInterface');44 $repo->shouldReceive('findOrFail')->with(1)->andReturn(null);4546 // Create mock of cache47 $cache = m::mock('Illuminate\Contracts\Cache\Repository');48 $cache->shouldReceive('tags')->with('users')->andReturn($cache);49 $cache->shouldReceive('remember')->andReturn(null);5051 // Instantiate the repository52 $repository = new CachingUserRepository($repo, $cache);5354 // Get all55 $item = $repository->findOrFail(1);56 $this->assertNull($item);57 }5859 /**60 * Test creating a single item61 *62 * @return void63 */64 public function testCreate()65 {66 // Create mock of decorated repository67 $repo = m::mock('App\Repositories\Interfaces\UserRepositoryInterface');68 $repo->shouldReceive('create')->with(['email' => 'bob@example.com'])->andReturn(true);6970 // Create mock of cache71 $cache = m::mock('Illuminate\Contracts\Cache\Repository');72 $cache->shouldReceive('tags')->with('usersUser')->andReturn($cache);73 $cache->shouldReceive('flush')->andReturn(true);7475 // Instantiate the repository76 $repository = new CachingUserRepository($repo, $cache);7778 // Get all79 $item = $repository->create(['email' => 'bob@example.com']);80 $this->assertTrue($item);81 }8283 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.