Back to Course |
Design Patterns in Laravel 11

Repository Pattern: Why I Don't Recommend It

If you have worked with Laravel long enough, you probably saw the Repository pattern used quite a lot 5-10 years ago. But it's not so popular now. Let me demonstrate.

This lesson will be a bit more opinionated but based on the strong opinions of a few more influential Laravel community members.


Typical Not-So-Practical Repository Example

In some Laravel tutorials, especially older ones, you may find example similar to this:

namespace App\Http\Controllers;
 
use App\Repositories\UserRepository;
use Illuminate\View\View;
 
class UserController extends Controller
{
public function __construct(
protected UserRepository $users,
) {}
 
public function show(string $id): View
{
$user = $this->users->find($id);
 
return view('user.profile', ['user' => $user]);
}
}

Then, the Repository class may look something like this:

app/Repositories/UserRepository.php:

class UserRepository
{
public function find($id)
{
return User::find($id);
}
}

But this doesn't seem very useful. Why a Repository with $this->users->find() instead of just User::find()? A separate class for doing the same thing?

Typically, the authors of those tutorials emphasize mocking or swapping the repository class with some other class in the future, for better testability and isolation.

But this typical over-simplified example above doesn't do the justice to the idea of Repository pattern in general.

Let me show you better examples, and then I will still debate that I do NOT approve of them in Laravel.


More Practical Repository Example

To actually show the "swapping" and "mocking" idea from the same example above, it should be this:

  • Create a Repository interface
  • Create a Repository class implementing that interface
  • In Controller, type-hint the interface
  • In Service Provider, bind the interface to this class, with the future possibility to bind it with another different Repository class

Here's an example.

app/Interfaces/UserRepositoryInterface.php:

namespace App\Interfaces;
 
interface UserRepositoryInterface
{
public function all();
public function find(int $id);
public function store(array $userData);
public function update(int $id, array $userData);
public function delete(int $id);
}

So, we define the same typical Eloquent methods.

Then, the implementation is in the repository.

app/Repositories/UserRepository.php:

namespace App\Repositories;
 
use App\Interfaces\UserRepositoryInterface;
 
class UserRepository implements UserRepositoryInterface
{
public function all()
{
return User::all();
}
 
public function find($id)
{
return User::find($id);
}
 
// ...
}

Then, in the Controller, we type-hint the interface:

namespace App\Http\Controllers;
 
use App\Interfaces\UserRepositoryInterface;
use Illuminate\View\View;
 
class UserController extends Controller
{
public function __construct(
protected UserRepositoryInterface $users,
) {}
 
public function show(int $id): View
{
$user = $this->users->find($id);
 
return view('user.profile', ['user' => $user]);
}
}

Finally, we specify which Repository class to use (yes, we have only one, for now) in the AppServiceProvider:

app/Providers/AppServiceProvider.php:

use App\Interfaces\UserRepositoryInterface;
use App\Repositories\UserRepository;
 
// ...
 
public function register()
{
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
}

Then, our goal is achieved: we can later swap that UserRepository with whatever other Repository class that would implement the same interface but have different codes to work with users.


Small Improvement: Model as a Parameter?

Another example is having the Eloquent Model as a parameter inside the Repository class.

namespace App\Repositories;
 
use App\Interfaces\UserRepositoryInterface;
 
class UserRepository implements UserRepositoryInterface
{
public function __construct(
protected User $model
) {}
 
public function all()
{
return $this->model->all();
}
 
public function find($id)
{
return $this->model->find($id);
}
 
// ...
}

Then, potentially, you would swap that Model for something else?


Going Even More "Meta": BaseRepository

Another example I've seen is the so-called BaseRepository or BaseEloquentRepository class, which contains all the default implementations of the Eloquent methods. Then, separate UserRepository or OrderRepository classes extend that Base Class.

app/Interfaces/EloquentRepositoryInterface.php:

namespace App\Interfaces;
 
use Illuminate\Database\Eloquent\Model;
 
interface EloquentRepositoryInterface
{
public function create(array $attributes): Model;
 
public function find($id): ?Model;
 
// ...
}

app/Repositories/BaseRepository.php:

namespace App\Repositories;
 
use App\Interfaces\EloquentRepositoryInterface;
use Illuminate\Database\Eloquent\Model;
 
class BaseRepository implements EloquentRepositoryInterface
{
public function __construct(
protected Model $model
) {}
 
public function create(array $attributes): Model
{
return $this->model->create($attributes);
}
 
public function find($id): ?Model
{
return $this->model->find($id);
}
 
// ...
}

app/Repositories/UserRepository.php:

namespace App\Repositories\Eloquent;
 
use App\Models\User;
use Illuminate\Support\Collection;
 
class UserRepository extends BaseRepository
{
public function __construct(User $model)
{
parent::__construct($model);
}
 
// We don't need to implement `find()` method here
// Because it comes from the BaseRepository
 
// We only implement EXTRA methods in this class
// That are not in the BaseRepository
}

There are also other variations, like the Service-Repository pattern in this article, that I don't want to show here. Instead, I want to get to the main point.

Those examples look so cool: separated, abstracted, flexible, the "design pattern way", right?

But let me ask you this: when was the last time you had to swap Eloquent implementation for something different?


Repository for Eloquent: Useless in Laravel?

The whole Repository pattern idea is good and comes outside Laravel or PHP.

One of the typical non-Laravel examples of Repository pattern is exactly that: separate database layer implementation, so you would be able to swap database/storage engines easily.

But in Laravel, Eloquent ORM is... kinda already doing that for us.

If you want to switch between MySQL, SQLite, PostgreSQL, or SQL Server, you don't change Eloquent to something else. You just configure a different driver in the config/database.php and maybe need a few more changes to specific queries.

So, in a way, Eloquent is already a repository layer. You call User::all() and then, under the hood, Eloquent knows which driver to use: MySQL or SQLite.

If you want to use DB drivers other than those officially supported by Laravel, you would probably need to change your whole Laravel code quite a bit. Just having a Repository pattern for DB operations would not solve that.

My point is this: is there a point in the Repository pattern with this "classic" example of "What if we need to replace Eloquent?"

Another argument is that creating those interfaces/repositories with longer Controller code seems quite a lot of work for no immediate benefit. (or, most likely, ever)


Opinions By Community

You can also read a similar opinion by Adel, the author of Laravel Idea, in his 2019 (!) article Please, stop talking about Repository pattern with Eloquent.

An even stronger opinion comes from Martin Bean in this Reddit thread as a reply to the Service-Repository pattern shown:

I’m absolutely sick of seeing the “repository” pattern advocated for Laravel apps, and then all they do is proxy calls to Eloquent models.

Need to find a model? Use the find() method on this repository class that just calls the find method on a model. Great. You’ve now lost all the functionality and convenience of Eloquent less you write methods that completely re-implement each feature you need.

I get design and architectural patterns are needed. But the repository pattern is never used for what it’s intended for and just means you now need to write more code for negative benefit.


But What if... Repository NOT For Eloquent?

When discussing Repositories, we often assume they have to be connected to Eloquent. But what if they are not?

In this case, we have a simple code example from Gummibeer:

class AuthorRepository
{
public function all(): Collection
{
return collect([
[
'nickname' => 'Gummibeer',
'email' => 'dev@gummibeer.de',
'firstname' => 'Tom',
'lastname' => 'Witkowski',
'payment_pointer' => '$ilp.uphold.com/EagWEdJU64mN',
'twitter' => '@devgummibeer',
],
])
->mapInto(Author::class);
}
 
public function find(string $nickname): Author
{
return $this->all()
->first(fn (Author $author): bool => Str::slug($author->nickname) === Str::slug($nickname));
}
}

As you can see, no Model is used here. In fact, we are working directly with collections/arrays. And that is where Repositories can be helpful. For example, let's switch the Repository from a Collection to a real API call:

class AuthorRepository
{
public function all(): Collection
{
return Http::get('https://api.example.com/authors')
->json()
->mapInto(Author::class);
}
 
public function find(string $nickname): Author
{
return $this->all()
->first(fn (Author $author): bool => Str::slug($author->nickname) === Str::slug($nickname));
}
}

As you can see, we only had to change one method (in this case, the source) to apply a different logic, and Controller would not change at all, it will still call something like $this->authorRepository->all(), without even knowing where that data comes from.

Without Repositories, we might have had to change a lot of files. This is where Repositories can be great, if you are working with different sources of data that may change, not just Eloquent.