Please, stop talking about Repository pattern with Eloquent

March 20, 2019

I regularly see articles like "How to use Repository pattern with Eloquent". The usual contents of them: let's create PostRepositoryInterface interface, EloquentPostRepository class, bind them nicely in DI container and use them instead of the standard Eloquent save and find methods.

Why this pattern is needed is not written at all ("This is a pattern! Isn’t it enough?"). Sometimes they write something about possible database engine change (which happens very often in every project), as well as testing and mock+stubs. The benefits of introducing this pattern into a regular Laravel project are difficult to grasp.

Let's try to analyze it and find some benefits. Repository pattern allows to abstract from a specific storage (which is usually a database), providing an abstract concept of collection of entities.

Examples with Eloquent Repository are divided into two types:

  1. Dual Eloquent-array variation
  2. Pure Eloquent Repository

Dual Eloquent-array variation

An example (I found it in one article):

<?php
interface FaqRepository
{
  public function all($columns = array('*'));

  public function newInstance(array $attributes = array());

  public function paginate($perPage = 15, $columns = array('*'));

  public function create(array $attributes);

  public function find($id, $columns = array('*'));

  public function updateWithIdAndInput($id, array $input);

  public function destroy($id);
}

class FaqRepositoryEloquent implements FaqRepository
{
  protected $faqModel;

  public function __construct(Faq $faqModel)
  {
      $this->faqModel = $faqModel;
  }

  public function newInstance(array $attributes = array())
  {
      if (!isset($attributes['rank'])) {
          $attributes['rank'] = 0;
      }
      return $this->faqModel->newInstance($attributes);
  }

  public function paginate($perPage = 0, $columns = array('*'))
  {
      $perPage = $perPage ?: Config::get('pagination.length');

      return $this->faqModel
          ->rankedWhere('answered', 1)
          ->paginate($perPage, $columns);
  }

  public function all($columns = array('*'))
  {
      return $this->faqModel->rankedAll($columns);
  }

  public function create(array $attributes)
  {
      return $this->faqModel->create($attributes);
  }

  public function find($id, $columns = array('*'))
  {
      return $this->faqModel->findOrFail($id, $columns);
  }

  public function updateWithIdAndInput($id, array $input)
  {
      $faq = $this->faqModel->find($id);
      return $faq->update($input);
  }

  public function destroy($id)
  {
      return $this->faqModel->destroy($id);
  }
}

all, find, paginate methods return Eloquent-objects, however create, updateWithIdAndInput takes an arrays. The name updateWithIdAndInput says that this "repository" will be used only for CRUD operations. No normal business logic is assumed, but I'll try to implement the simplest:

<?php
class FaqController extends Controller
{
    public function publish($id, FaqRepository $repository)
    {
        $faq = $repository->find($id);

        //...Some check with $faq->... 
        $faq->published = true;

        $repository->updateWithIdAndInput($id, $faq->toArray());
    }
}

Without repository:

<?php
class FaqController extends Controller
{
    public function publish($id)
    {
        $faq = Faq::findOrFail($id);

        //...Some check with $faq->... 
        $faq->published = true;

        $faq->save();
    }
}

Much easier! Why this abstraction should be introduced to project? It only makes it more complicated.

  • Unit testing? Everyone knows that usual CRUD project at Laravel is 100%-covered by unit tests. I'll talk about unit testing a bit later.
  • For the opportunity to change the database engine? But Eloquent already supports several database engines. Using the Eloquent-entity for unsupported database for application that contains only CRUD-logic will be a torment and a waste of time. In this case, the repository that returns a clean PHP array and also accepts only arrays looks much more natural. By removing Eloquent from repository contract, we got a real abstraction from the storage.

Pure Eloquent Repository

An example of pure Eloquent repository (also found in one article):

<?php

interface PostRepositoryInterface
{
    public function get($id);

    public function all();

    public function delete($id);

    public function save(Post $post);
}

class PostRepository implements PostRepositoryInterface
{
    public function get($id)
    {
        return Post::find($id);
    }

    public function all()
    {
        return Post::all();
    }

    public function delete($id)
    {
        Post::destroy($id);
    }

    public function save(Post $post)
    {
        $post->save();
    }
}

This implementation is more like what the Repository pattern description says. The implementation of simple logic looks a bit more natural:

<?php
class FaqController extends Controller
{
    public function publish($id, PostRepositoryInterface $repository)
    {
        $post = $repository->find($id);

        //... some checks with $post->... 
        $post->published = true;

        $repository->save($post);
    }
}

However, the implementation of Repository pattern for blog posts is just a toy for kids. Let's try something more complicated. Simple entity with sub-entities. For example, a poll with possible options. The poll creation case. Two ways:

  • Create a PollRepository and PollOptionRepository. The problem with this option is that the abstraction failed. A poll with options is one entity and its storing should have been implemented by a single PollRepository class. PollOptionRepository::delete will be complex, because it will need a Poll object to check if it is possible to delete this option (poll should have at least two options). Also, Repository pattern does not allow the business logic inside repositories.
  • Add saveOption and deleteOption methods to PollRepository. The problems are almost the same. The abstraction from storage is not very precise... developer has to take care of the poll options separately. And what if the entity is even more complex? With a bunch of other relations?

The same question: why?

It's not possible to get an abstraction from database more than Eloquent gives.

Unit testing? Here is an example of a possible unit test from my book - https://gist.github.com/adelf/a53ce49b22b32914879801113cf79043

Very few people will enjoy making such a huge unit tests for simple actions like poll creation. I am pretty sure that these tests will be abandoned. No one wants to support them (I was on a project with such tests, I know what I'm talking about). It is much easier and optimal to focus on functional testing, especially if it's an API project.

If the business logic is so complex that developers really want to cover it with unit tests, then it is better to take a data mapper library like Doctrine and completely extract the business logic from the rest of the application. Unit testing will be 10 times easier.

If you are using an Eloquent and want to play with design patterns, in the next article I will show how you can partially use the Repository pattern and get some benefit from it.