Het Laravel repository pattern is een klassiek ontwerppatroon dat de logica voor gegevenstoegang uit je controllers en services haalt en in een aparte laag plaatst. Het idee is simpel: je code hoort niet te weten waar de data vandaan komt (Eloquent, een ruwe query, een externe API); hij vraagt gewoon aan een UserRepository-interface om find() of all() en krijgt een resultaat terug. Het klinkt netjes, maar het is een van de meest besproken onderwerpen in de Laravel-gemeenschap — want Eloquent is al een abstractielaag, en er een tweede bovenop stapelen levert in de meeste projecten meer last dan voordeel op. In dit artikel verduidelijk ik hoe je het patroon correct opzet, wat de echte voordelen zijn, wat de verborgen kosten zijn en wanneer het doorslaat naar overdreven.
Wat lost het repository pattern eigenlijk op?
Het patroon komt voort uit Domain-Driven Design. Het doel is om de bedrijfslogica (het domein) te isoleren van de details van persistentie. In theorie gedraagt een repository zich als een verzameling objecten in het geheugen: je vraagt om objecten, hij geeft objecten terug, en of er SQL, een bestand of een REST API achter zit gaat je niet aan. In de praktijk komt het in Laravel neer op drie hoofddoelen:
- Abstractie: de bedrijfslogica hangt af van een interface, niet van een concrete ORM.
- Testbaarheid: je kunt de interface vervangen door een mock en tests schrijven zonder de database aan te raken.
- Hergebruik: complexe queries staan op één plek in plaats van verspreid over controllers.
De cruciale vraag is hier: biedt Eloquent, als Active Record-implementatie, niet al een deel van deze abstractie? Jawel — en die overlap is precies het meest bekritiseerde aspect van het repository pattern.
De juiste opzet in Laravel
Als je het patroon toepast, doe het dan goed. De basisstructuur is een interface en een concrete klasse die hem implementeert. Eerst de interface:
namespace App\Repositories\Contracts;
interface UserRepositoryInterface
{
public function all();
public function find(int $id);
public function create(array $data);
}
Daarna de op Eloquent gebaseerde concrete implementatie:
namespace App\Repositories\Eloquent;
use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;
class EloquentUserRepository implements UserRepositoryInterface
{
public function __construct(private User $model) {}
public function all()
{
return $this->model->all();
}
public function find(int $id)
{
return $this->model->findOrFail($id);
}
public function create(array $data)
{
return $this->model->create($data);
}
}
De laatste stap is het binden van de interface aan de concrete klasse. Dat doe je in een service provider:
// in AppServiceProvider of een aparte RepositoryServiceProvider
public function register(): void
{
$this->app->bind(
\App\Repositories\Contracts\UserRepositoryInterface::class,
\App\Repositories\Eloquent\EloquentUserRepository::class
);
}
Nu vraagt de controller om de interface en injecteert de servicecontainer van Laravel de juiste implementatie:
public function show(UserRepositoryInterface $users, int $id)
{
return view('users.show', [
'user' => $users->find($id),
]);
}
Merk op dat omdat de binding op de interface gericht is, je op de dag dat je Eloquent door een andere implementatie wilt vervangen alleen de bind-regel wijzigt. Op papier is dat de belofte van het patroon.
De echte voordelen
Het patroon loont werkelijk in de juiste context. De meest concrete voordelen zijn:
- Complexe queries centraliseren. Als dezelfde rapportagequery met meerdere stappen in vijf controllers gedupliceerd staat, verbetert het verplaatsen ervan naar een sprekende methode als
topSpendingCustomers()in eenReportRepositoryde leesbaarheid aanzienlijk. - Meerdere gegevensbronnen. Wanneer data deels uit MySQL en deels uit een externe API komt, is een repository een natuurlijke plek om die twee achter één interface samen te voegen.
- Bedrijfslogica afschermen van de ORM. Als je domeinlaag groot en langlevend is, maakt het niet strak koppelen aan de functies van Eloquent een toekomstige ORM-migratie theoretisch makkelijker.
Merk op dat deze punten één eigenschap delen: het project heeft een bepaalde complexiteitsdrempel overschreden. In kleine en middelgrote projecten doen die voorwaarden zich zelden voor.
Verborgen kosten en kritiek
De gemeenschap bekritiseert het patroon vaak om goede redenen, en die negeren is een fout:
- Eloquent is al een abstractie.
User::find($id)is zelf een abstractie; het kan van databasedriver wisselen en genereert de query zonder dat je hem ziet. Er nog een laag bovenop leggen is vaak hetzelfde tweemaal doen. - Je verliest de kracht van Eloquent. Wanneer je repository een collectie teruggeeft, kan de aanroepende code de vloeiende keten als
where(),with()ofpaginate()niet meer gebruiken. Om die te ondersteunen voeg je tientallen methodes aan de repository toe en vind je uiteindelijk Eloquent opnieuw uit. - Het scenario "driver verwisselen" gebeurt bijna nooit. De voornaamste rechtvaardiging van het patroon is de aanname "wat als we morgen van Eloquent afstappen". In de praktijk blijft de overgrote meerderheid van projecten hun hele leven op Eloquent; je ontwerpt voor een probleem dat niet bestaat.
- Meer bestanden, meer overhead. Een interface, een concrete klasse en een bindregel per model — een doolhof dat een nieuw teamlid moet doorlopen voor weinig winst.
Is testbaarheid echt een rechtvaardiging?
De sterkste troef van de repository-voorstanders is testen: je mockt de interface en test zonder de database te raken. Dat klopt, maar het alternatief in Laravel is gratis. Een snelle in-memory SQLite-database met de RefreshDatabase-trait is snel genoeg voor de meeste tests en oefent je echte queries — een gemockte interface blijft groen, zelfs als je querylogica kapot is.
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_gebruiker_wordt_aangemaakt(): void
{
$user = User::factory()->create(['email' => 'a@b.com']);
$this->assertDatabaseHas('users', ['email' => 'a@b.com']);
}
}
Zo stort de bewering "je hebt een repository nodig om te testen" in de meeste gevallen in tegen Laravels eigen gereedschap. Testbaarheid is het gevolg van goed gestructureerde code, niet van de repository in het bijzonder.
Een praktisch alternatief: een dunne servicelaag of Action-klassen
In de meeste gevallen wil je eigenlijk "controllers dun houden" en "bedrijfslogica op één plek verzamelen" — en dat vereist geen volledige repository-abstractie. Twee lichte benaderingen volstaan. Serviceklassen groeperen verwante bedrijfslogica terwijl ze binnenin rustig Eloquent gebruiken. Action-klassen zijn kleine klassen met één verantwoordelijkheid en één handle()-methode:
class CreateUser
{
public function handle(array $data): User
{
$user = User::create($data);
$user->sendWelcomeNotification();
return $user;
}
}
Deze aanpak ordent de code, behoudt alle kracht van Eloquent en vermijdt de overbodige interfacelaag. Het is de weg die veel ervaren Laravel-ontwikkelaars verkiezen boven een volledige repository.
Veelgestelde vragen
Moet ik het repository pattern in elk Laravel-project gebruiken?
Nee. In de overgrote meerderheid van kleine en middelgrote projecten is het een overbodige abstractie; Eloquent direct gebruiken, met indien nodig een dunne service- of Action-laag, is eenvoudiger en makkelijker te onderhouden. Overweeg het patroon alleen als je echt meerdere gegevensbronnen, zware en herhaalde queries of een grote ORM-onafhankelijke domeinlaag hebt.
Beïnvloedt het repository pattern de prestaties?
Er is geen direct meetbaar effect op de prestaties; de kost van een extra klasse-aanroep is verwaarloosbaar. Het echte risico is indirect: een naïeve repository die collecties teruggeeft kan het N+1-queryprobleem veroorzaken als hij de aanroepende code belet om met with() aan eager loading te doen. Dat is het gevolg van een slechte implementatie, niet van het patroon zelf.
Vervangt het repository pattern Eloquent?
Nee, het komt erbovenop in plaats van het te vervangen. Binnen de repository gebruik je meestal nog steeds Eloquent; het patroon verbergt dat gebruik enkel achter een interface. Eloquent volledig opgeven is geen voorwaarde om het patroon te gebruiken.
Heeft je Laravel-project echt het repository pattern nodig, of volstaat een eenvoudiger architectuur? We kunnen samen naar de schaal van je project kijken en het juiste abstractieniveau bepalen — neem contact met me op.