Why Use The Repository Pattern in Laravel?

Why Use The Repository Pattern in Laravel?

ยท

7 min read

In the my last blog post I explained what the Repository pattern is, how it is different from the Active Record pattern and how you can implement it in Laravel. Now I want to go into the reasons why you should use the Repository pattern.

I noticed in the comments of the last post, that the Repository pattern is a controversial topic in the Laravel community. Some people see no reason to use it and stick with the built-in Active Record pattern. Others prefer alternative approaches to separate the data access from domain logic. Please note, that I respect these opinions and I'll dedicate the next blog post to this topic.

With this disclaimer out of the way, let's go through the advantages of using the Repository pattern.

Single Responsibility Principle

The Single Responsibility Principle is the main differentiator which sets apart the Repository pattern from Active Record. The model class already holds data and provides methods on domain objects. Data access is an additional responsibility which gets mixed in, when using the Active Record pattern. This is something I want to examine in the following example:

/**
 * @property string $first_name
 * @property int    $company_id
 */
class Employee extends Model {}

$jack = new Employee();
$jack->first_name = 'Jack';
$jack->company_id = $twitterId;
$jack->save();

Although the responsibilities of the domain model and the data access technology are mixed, it intuitively makes sense. In our application employees have to be stored in the database somehow, so why not call save() on the object. The single object is translated into a single database row and stored.

But let's take it a step further and see what more we can do with our employee:

$jack->where('first_name', 'John')->firstOrFail()->delete();
$competition = $jack->where('company_id', $facebookId)->get();

Now, it gets unintuitive and even against our domain model. Why can Jack suddenly delete another employee, which might even work at a different company? Or why can he fetch the employees of Facebook?

Of course this example is contrived, but it still shows how the Active Record pattern does not allow for an intentional domain model. The line between the employee and the list of all employees is blurred. You always have to think about whether the employee is used as an actual employee or as a mechanism to access other employees.

The Repository pattern addresses this problem by enforcing this basic distinction. Its single purpose is to represents a collection of domain objects, not the domain object itself.

Takeaway:

  • ๐Ÿ‘‰ The Repository pattern honors the Single Responsibility Principle by separating the the collection of all domain objects from a single domain object.

Don't Repeat Yourself (DRY)

Some projects have database queries sprinkled all over the project. Here is an example where we fetch invoices from the database to show them in a Blade view:

class InvoiceController {

    public function index(): View {
        return view('invoices.index', [
            'invoices' => Invoice::where('overdue_since', '>=', Carbon::now())
                ->orderBy('overdue_since')
                ->paginate()
        ]);
    }
}

When such a query gets more complicated and is used in multiple places, consider extracting it into a Repository method.

The Repository pattern helps to reduce duplicate queries by packaging them in expressive methods. If you have to adjust the query, you only need to change it once.

class InvoiceController {

    public __construct(private InvoiceRepository $repo) {}

    public function index(): View {
        return view('invoices.index', [
            'invoices' => $repo->paginateOverdueInvoices()
        ]);
    }
}

Now the query is implemented once, can be tested in isolation and used in other places. Also the Single Responsibility Principle comes into play again, because the controller is not responsible for fetching data, but only to handle the HTTP request and returning the response.

Takeaway:

  • ๐Ÿ‘‰ The Repository pattern helps to reduce duplicate queries.

Dependency Inversion Principle

Explaining the Dependency Inversion Principle is worth its own blog post. I just want to show that a Repository can enable dependency inversion.

When layering components, usually the higher-level components depend on the lower-level components. For example a controller would depend on a model class to fetch data from a database:

class InvoiceController {
    public function index(int $companyId): View {
        return view(
            'invoices.index',
            ['invoices' => Invoice::where('company_id', $companyId)->get()]
        );
    }
}

The dependency relationship is top-down and tightly coupled. The InvoiceController depends on the concrete Invoice class. It's hard to decouple the two classes for example to test them in isolation or substitute the storage mechanism. With the introduction of a Repository interface we can achieve Dependency Inversion:

interface InvoiceRepository {
    public function findByCompanyId($companyId): Collection;
}

class InvoiceController {
    public function __construct(private InvoiceRepository $repo) {}

    public function index(int $companyId): View {
        return view(
            'invoices.index',
            ['invoices' => $this->repo->findByCompanyId($companyId)]
        );
    }
}

class EloquentInvoiceRepository implements InvoiceRepository {
    public function findByCompanyId($companyId): Collection {
        // implement the method using the Eloquent query builder
    }
}

The Controller now only depends on the Repository interface, as does the Repository implementation. Both classes now only depend on an abstraction, which reduces coupling. As I'll explain in the next sections, this opens up even more advantages.

Takeaway:

  • ๐Ÿ‘‰ The Repository pattern serves as an abstraction which enables the Dependency Inversion.

Abstraction

The Repository increases readability because complex operations are hidden by high-level methods with expressive names.

The code accessing the Repository gets decoupled from the underlying data access technology. If necessary, you can switch out implementations and you can even leave out the implementation and only provide a Repository interface. This can be handy for libraries, which aim to be framework-agnostic.

This approach is used in the framework-independent OAuth2 server league/oauth2-server. Laravel Passport integrates this library by implementing the Repositories from league/oauth2-server with the Eloquent query builder.

Not only can you switch out Repository implementations, you can also combine them, as Benjamin Delespierre pointed out to me in a comment in response to my previous blog post. Roughly building on his example, you can see how a Repository can wrap another Repository to provide additional functionality:

interface InvoiceRepository {
    public function findById(int $id): Invoice;
}

class InvoiceCacheRepository implements InvoiceRepository {

    public function __construct(
        private InvoiceRepository $repo,
        private int $ttlSeconds
    ) {}

    public function findById(int $id): Invoice {
        return Cache::remember(
            "invoice.$id",
            $this->ttlSeconds,
            fn(): Invoice => $this->repo->findById($id)
        );
    }
}

class EloquentInvoiceRepository implements InvoiceRepository {

    public function findById(int $id): Invoice { /* retrieves $id from the DB */ }
}

// --- Usage:

$repo = new InvoiceCacheRepository(
    new EloquentInvoiceRepository();
);

Takeaway:

  • ๐Ÿ‘‰ The Repository pattern abstracts away details about the data access.
  • ๐Ÿ‘‰ The Repository decouples the clients from the data access technology.
  • ๐Ÿ‘‰ This allows to switch out implementations, increases readability and enables composability.

Testability

The abstraction provided by the Repository pattern also helps with testing.

If you have a Repository interface in place, you can provide an alternative implementation for testing. Instead of accessing a database, you can back the Repository with an array, holding all the domain objects in an array:

class InMemoryInvoiceRepository implements InvoiceRepositoryInterface {

    private array $invoices;

    // implement the methods by accessing $this->invoices...
}

// --- Test Case:

$repo = new InMemoryInvoiceRepository();
$service = new InvoiceService($repo);

With this approach you get a realistic implementation which is fast and runs in-memory. But you have to provide a correct Repository implementation just for the test, which can be a lot of work itself. In my opinion, this can be justified in two cases:

  1. You are developing a (framework-agnostic) library which does not provide a Repository implementation itself.
  2. The test cases are complex and the state of the Repository is important.

Another approach is "mocking". To use this technique, you don't need an interface in place. You can mock any non-final class. With the PHPUnit API you can formulate how you expect the repository to be called and what should be returned.

$companyId = 42;

/** @var InvoiceRepository&MockObject */
$repo = $this->createMock(InvoiceRepository::class);

$repo->expects($this->once())
    ->method('findInvoicedToCompany')
    ->with($companyId)
    ->willReturn(collect([ /* invoices to return in the test case */ ]));

$service = new InvoiceService($repo);

$result = $service->calculateAvgInvoiceAmount($companyId);

$this->assertEquals(1337.42, $result);

With the mock in place, the test case is a proper unit test. The only code tested in the example above is the service. There is no database access, which makes the test case fast to setup and run.

Takeaway:

  • ๐Ÿ‘‰ The Repository pattern allows for proper unit tests, which run fast and isolate the unit under test.

  • ๐Ÿ‘‰ Let me know which advantages you see in using the Repository pattern.
  • ๐Ÿ‘‰ In the next post, I'll explore the disadvantages of using the Repository pattern.
ย