Repository Pattern : quand et comment l'utiliser avec Laravel

Le Repository Pattern est l'un des patterns les plus discutés dans l'écosystème Laravel. Souvent mal compris, parfois sur-utilisé, il peut pourtant apporter une vraie valeur dans certains contextes. Démystifions-le ensemble.

Le Repository Pattern, c'est quoi exactement ?

L'idée est simple : abstraire la couche de persistance derrière une interface. Au lieu d'appeler directement Eloquent dans vos controllers ou services, vous passez par un "repository" qui encapsule les requêtes.

L'objectif théorique

Pouvoir changer de source de données (MySQL vers MongoDB, API externe, fichiers...) sans toucher au code métier. En pratique, ce cas arrive rarement, mais le pattern offre d'autres avantages.

Voici la structure classique d'un repository :

                
app/Contracts/UserRepositoryInterface.php
namespace App\Contracts; interface UserRepositoryInterface { public function find(int $id): ?User; public function findByEmail(string $email): ?User; public function getActiveUsers(): Collection; public function save(User $user): User; }
                
app/Repositories/EloquentUserRepository.php
namespace App\Repositories; use App\Contracts\UserRepositoryInterface; use App\Models\User; use Illuminate\Support\Collection; class EloquentUserRepository implements UserRepositoryInterface { public function find(int $id): ?User { return User::find($id); } public function findByEmail(string $email): ?User { return User::where('email', $email)->first(); } public function getActiveUsers(): Collection { return User::where('active', true) ->orderBy('name') ->get(); } public function save(User $user): User { $user->save(); return $user; } }

Quand le Repository Pattern a du sens

Utiliser un repository "parce que c'est une bonne pratique" est une erreur. Voici les cas concrets où il apporte une vraie valeur :

1. Logique de requête complexe et réutilisée

Si vous avez des requêtes complexes utilisées à plusieurs endroits, un repository centralise cette logique et évite la duplication.

                
app/Repositories/OrderRepository.php
class OrderRepository { // Logique complexe centralisée public function getPendingOrdersForShipping(): Collection { return Order::where('status', 'paid') ->where('shipping_status', 'pending') ->whereHas('items', fn($q) => $q->where('requires_shipping', true)) ->with(['customer', 'items.product', 'shippingAddress']) ->orderBy('created_at') ->get(); } // Cette méthode est appelée depuis plusieurs endroits : // - ShippingController // - DailyShippingReportJob // - ShippingDashboard }

2. Tests avec sources de données alternatives

Quand vous devez tester du code métier sans toucher à la base de données, un repository permet d'injecter une implémentation "in-memory".

                
tests/Unit/InMemoryUserRepository.php
class InMemoryUserRepository implements UserRepositoryInterface { private array $users = []; public function find(int $id): ?User { return $this->users[$id] ?? null; } public function save(User $user): User { $this->users[$user->id] = $user; return $user; } // Tests ultra-rapides, sans DB }

3. Agrégation de sources de données

Quand vos données viennent de plusieurs sources (base locale + API externe), un repository peut unifier l'accès.

                
app/Repositories/ProductRepository.php
class ProductRepository { public function __construct( private InventoryApiClient $inventoryApi, ) {} public function getWithStock(int $productId): ProductWithStock { $product = Product::findOrFail($productId); $stock = $this->inventoryApi->getStock($productId); return new ProductWithStock($product, $stock); } }

Quand c'est de l'over-engineering

Dans la majorité des projets Laravel, le Repository Pattern est superflu. Voici les signaux d'alerte :

Red flags

Votre repository ne fait que proxifier Eloquent. Si find($id) appelle juste Model::find($id), vous n'ajoutez aucune valeur, juste de l'indirection.

L'anti-pattern classique

                
Le repository inutile
// Repository qui n'apporte RIEN class UserRepository { public function all() { return User::all(); } public function find($id) { return User::find($id); } public function create($data) { return User::create($data); } public function update($id, $data) { return User::find($id)->update($data); } public function delete($id) { return User::destroy($id); } } // Autant utiliser Eloquent directement !

Pourquoi c'est un problème

Les alternatives Laravel-native

Avant d'implémenter un repository, considérez ces alternatives intégrées à Laravel :

Query Scopes pour la logique de requête

Les scopes locaux (avec l'attribut #[Scope] en Laravel 12) permettent de centraliser la logique de requête directement sur le modèle.

                
app/Models/Order.php
use Illuminate\Database\Eloquent\Attributes\Scope; class Order extends Model { #[Scope] protected function pendingShipping(Builder $query): void { $query->where('status', 'paid') ->where('shipping_status', 'pending'); } #[Scope] protected function withShippingDetails(Builder $query): void { $query->with(['customer', 'items.product', 'shippingAddress']); } } // Utilisation fluide et expressive $orders = Order::pendingShipping() ->withShippingDetails() ->get();

Services pour la logique métier

Un service est souvent plus approprié qu'un repository pour encapsuler la logique métier complexe.

                
app/Services/OrderService.php
class OrderService { public function processRefund(Order $order): RefundResult { // Logique métier : validation, appel API paiement, mise à jour if (!$order->canBeRefunded()) { throw new RefundNotAllowedException(); } $refund = $this->paymentGateway->refund($order->payment_id); $order->update(['status' => 'refunded']); return new RefundResult($refund); } }

Repository vs Service

Repository = accès aux données (lecture/écriture)
Service = orchestration de logique métier
Ne mélangez pas les deux. Si votre "repository" contient de la logique métier, c'est un service.

Implémenter correctement avec Laravel 12

Si après analyse vous décidez qu'un repository est justifié, voici comment l'implémenter proprement avec le service container de Laravel 12.

Binding dans un Service Provider

                
app/Providers/RepositoryServiceProvider.php
namespace App\Providers; use App\Contracts\OrderRepositoryInterface; use App\Repositories\EloquentOrderRepository; use Illuminate\Support\ServiceProvider; class RepositoryServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind( OrderRepositoryInterface::class, EloquentOrderRepository::class ); } }

Injection dans un Controller

                
app/Http/Controllers/ShippingController.php
class ShippingController extends Controller { public function __construct( private OrderRepositoryInterface $orders, ) {} public function pending(): View { return view('shipping.pending', [ 'orders' => $this->orders->getPendingForShipping(), ]); } }

Test avec implémentation alternative

                
tests/Feature/ShippingTest.php
class ShippingTest extends TestCase { public function test_pending_page_shows_orders(): void { // Remplacer par une implémentation de test $this->app->bind( OrderRepositoryInterface::class, FakeOrderRepository::class ); $response = $this->get('/shipping/pending'); $response->assertOk(); } }

Mon avis pragmatique

Pour 90% des projets Laravel, le Repository Pattern est inutile. Eloquent avec ses scopes, les services pour la logique métier, et les outils de test de Laravel couvrent la majorité des besoins.

Réservez le Repository Pattern pour les cas où il apporte une vraie valeur :

La règle d'or : n'ajoutez pas d'abstraction tant que vous n'en avez pas besoin. Le code le plus simple à maintenir est celui qui n'existe pas.

Checklist avant d'implémenter

Besoin d'aide sur votre projet ?

Je peux vous accompagner dans le développement ou l'optimisation de votre application.

Me contacter