Le Laravel repository pattern est un patron de conception classique qui sort la logique d'accès aux données de vos contrôleurs et services pour la placer dans une couche dédiée. L'idée est simple : votre code ne doit pas savoir d'où viennent les données (Eloquent, une requête brute, une API externe) ; il demande simplement à une interface UserRepository un find() ou un all() et récupère un résultat. Cela paraît propre, mais c'est l'un des sujets les plus débattus dans la communauté Laravel — car Eloquent est déjà une couche d'abstraction, et en empiler une seconde par-dessus apporte plus de poids que de bénéfice dans la plupart des projets. Dans cet article, je clarifie comment mettre en place le patron correctement, ses véritables atouts, ses coûts cachés et le moment où il bascule dans le superflu.
Que résout réellement le repository pattern ?
Le patron trouve son origine dans le Domain-Driven Design. Son but est d'isoler la logique métier (le domaine) des détails de persistance. En théorie, un repository se comporte comme une collection d'objets en mémoire : vous lui demandez des objets, il vous en donne, et savoir si du SQL, un fichier ou une API REST se cache derrière ne vous concerne pas. En pratique, dans Laravel, cela se résume à trois objectifs principaux :
- Abstraction : la logique métier dépend d'une interface, pas d'un ORM concret.
- Testabilité : vous pouvez remplacer l'interface par un mock et écrire des tests sans toucher la base de données.
- Réutilisation : les requêtes complexes vivent à un seul endroit au lieu d'être dispersées dans les contrôleurs.
La question cruciale ici est : Eloquent, en tant qu'implémentation Active Record, ne fournit-il pas déjà une partie de cette abstraction ? Si — et ce chevauchement est précisément l'aspect le plus critiqué du repository pattern.
La mise en place correcte dans Laravel
Si vous appliquez le patron, faites-le proprement. La structure de base est une interface et une classe concrète qui l'implémente. D'abord l'interface :
namespace App\Repositories\Contracts;
interface UserRepositoryInterface
{
public function all();
public function find(int $id);
public function create(array $data);
}
Puis l'implémentation concrète basée sur Eloquent :
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);
}
}
La dernière étape consiste à lier l'interface à la classe concrète. Vous le faites dans un service provider :
// dans AppServiceProvider ou un RepositoryServiceProvider dédié
public function register(): void
{
$this->app->bind(
\App\Repositories\Contracts\UserRepositoryInterface::class,
\App\Repositories\Eloquent\EloquentUserRepository::class
);
}
Désormais le contrôleur demande l'interface et le conteneur de services de Laravel injecte la bonne implémentation :
public function show(UserRepositoryInterface $users, int $id)
{
return view('users.show', [
'user' => $users->find($id),
]);
}
Notez que, comme la liaison cible l'interface, le jour où vous voudrez remplacer Eloquent par une autre implémentation, vous ne changerez que la ligne bind. Sur le papier, c'est la promesse du patron.
Les véritables atouts
Le patron est réellement payant dans le bon contexte. Ses bénéfices les plus concrets sont :
- Centraliser les requêtes complexes. Si la même requête de reporting en plusieurs étapes est dupliquée dans cinq contrôleurs, la déplacer dans une méthode parlante comme
topSpendingCustomers()au sein d'unReportRepositoryaméliore considérablement la lisibilité. - Sources de données multiples. Quand les données proviennent en partie de MySQL et en partie d'une API externe, un repository est un endroit naturel pour fusionner les deux derrière une seule interface.
- Protéger la logique métier de l'ORM. Si votre couche domaine est vaste et durable, ne pas la coupler étroitement aux fonctionnalités d'Eloquent facilite théoriquement une future migration d'ORM.
Remarquez que ces points partagent un trait commun : le projet a franchi un certain seuil de complexité. Dans les petits et moyens projets, ces conditions se réalisent rarement.
Coûts cachés et critiques
La communauté critique souvent ce patron pour de bonnes raisons, et les ignorer serait une erreur :
- Eloquent est déjà une abstraction.
User::find($id)est lui-même une abstraction ; il peut changer de pilote de base de données et génère la requête sans que vous la voyiez. En ajouter une autre par-dessus revient souvent à faire deux fois la même chose. - Vous perdez la puissance d'Eloquent. Quand votre repository renvoie une collection, le code appelant ne peut plus utiliser la chaîne fluide comme
where(),with()oupaginate(). Pour les prendre en charge, vous ajoutez des dizaines de méthodes au repository et finissez par réinventer Eloquent. - Le scénario « changer de pilote » n'arrive presque jamais. La justification phare du patron est l'hypothèse « et si on quittait Eloquent demain ». En pratique, l'écrasante majorité des projets restent sur Eloquent toute leur vie ; vous concevez pour un problème inexistant.
- Plus de fichiers, plus de surcharge. Une interface, une classe concrète et une ligne de liaison par modèle — un labyrinthe qu'un nouveau membre de l'équipe doit parcourir pour peu de gain.
La testabilité est-elle vraiment une justification ?
L'atout le plus fort des défenseurs du repository est le test : vous mockez l'interface et testez sans toucher la base. C'est vrai, mais l'alternative dans Laravel est gratuite. Utiliser une base SQLite rapide en mémoire avec le trait RefreshDatabase est assez rapide pour la plupart des tests et exerce vos vraies requêtes — une interface mockée reste verte même si votre logique de requête est cassée.
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_un_utilisateur_est_cree(): void
{
$user = User::factory()->create(['email' => 'a@b.com']);
$this->assertDatabaseHas('users', ['email' => 'a@b.com']);
}
}
Ainsi, l'affirmation « il faut un repository pour tester » s'effondre dans la plupart des cas face aux outils de Laravel. La testabilité est le résultat d'un code bien structuré, pas du repository en particulier.
Une alternative pratique : une fine couche de service ou des classes Action
Dans la plupart des cas, ce que vous voulez vraiment, c'est « garder les contrôleurs légers » et « rassembler la logique métier à un seul endroit » — ce qui ne nécessite pas une abstraction repository complète. Deux approches légères suffisent. Les classes de service regroupent la logique métier liée tout en utilisant tranquillement Eloquent à l'intérieur. Les classes Action sont de petites classes à responsabilité unique avec une seule méthode handle() :
class CreateUser
{
public function handle(array $data): User
{
$user = User::create($data);
$user->sendWelcomeNotification();
return $user;
}
}
Cette approche organise le code, préserve toute la puissance d'Eloquent et évite la couche d'interface superflue. C'est la voie que de nombreux développeurs Laravel expérimentés préfèrent à un repository complet.
Questions fréquentes
Dois-je utiliser le repository pattern dans chaque projet Laravel ?
Non. Dans la grande majorité des petits et moyens projets, c'est une abstraction superflue ; utiliser Eloquent directement, avec une fine couche de service ou Action si nécessaire, est plus simple et plus facile à maintenir. Ne considérez le patron que si vous avez réellement plusieurs sources de données, des requêtes lourdes et répétées, ou une vaste couche domaine indépendante de l'ORM.
Le repository pattern affecte-t-il les performances ?
Il n'y a aucun impact mesurable directement sur les performances ; le coût d'un appel de classe supplémentaire est négligeable. Le vrai risque est indirect : un repository naïf qui renvoie des collections peut provoquer le problème de requêtes N+1 s'il empêche le code appelant de faire de l'eager loading avec with(). C'est le résultat d'une mauvaise implémentation, pas du patron lui-même.
Le repository pattern remplace-t-il Eloquent ?
Non, il se place au-dessus plutôt que de le remplacer. À l'intérieur du repository, vous utilisez généralement encore Eloquent ; le patron ne fait que cacher cette utilisation derrière une interface. Abandonner Eloquent entièrement n'est pas une condition pour utiliser le patron.
Votre projet Laravel a-t-il vraiment besoin du repository pattern, ou une architecture plus simple suffit-elle ? Nous pouvons examiner l'échelle de votre projet et déterminer ensemble le bon niveau d'abstraction — contactez-moi.