Lecture : 14 minutes · Niveau : intermédiaire · Mise à jour : avril 2026
Eloquent est l’un des arguments les plus forts de Laravel : ORM élégant, lisible, productif. Mais bien l’utiliser demande de connaître ses nombreuses capacités et d’éviter les pièges classiques (N+1, mass assignment, requêtes inutiles). Ce guide rassemble les patterns vraiment utiles pour produire du code Laravel propre et performant en production.
L’erreur classique avec Eloquent est de l’utiliser comme un simple Active Record : créer, lire, modifier, supprimer, sans exploiter les fonctionnalités plus avancées. Un autre piège est l’inverse : utiliser Eloquent pour tout, y compris des cas où une simple requête SQL aurait été plus performante et plus claire. Ce guide vise l’équilibre pragmatique : exploiter Eloquent là où il brille, savoir le contourner là où il pèse.
Voir aussi → Laravel pour PME : guide backend PHP moderne.
Sommaire
- Modèle Eloquent : conventions et configuration
- CRUD de base et requêtes courantes
- Relations : hasMany, belongsTo, manyToMany
- Eager loading et N+1
- Scopes pour requêtes réutilisables
- Accessors, mutators, casts
- Events et observers
- Transactions
- Pagination et chunking
- Performance : indexes, requêtes raw
- FAQ
1. Modèle Eloquent : conventions et configuration
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class Client extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['nom', 'email', 'telephone', 'actif'];
protected $casts = [
'actif' => 'boolean',
'metadata' => 'array',
'created_at' => 'datetime',
];
protected $hidden = ['password', 'remember_token'];
protected $appends = ['url'];
public function getUrlAttribute(): string
{
return route('clients.show', $this);
}
}
Conventions importantes
- Nom du modèle au singulier (
Client), table en pluriel snake_case (clients) — automatique - Clé primaire
idpar défaut, surchargeable - Timestamps
created_atetupdated_atautomatiques $fillableou$guardedobligatoire pour mass assignment$castsconvertit automatiquement entre types (string DB → boolean PHP, JSON → array, etc.)
Mass assignment
// Avec $fillable défini : OK
$client = Client::create($request->validated());
// Sans $fillable : MassAssignmentException
// Toujours définir $fillable explicitement
$fillable liste les champs autorisés en mass assignment. $guarded = [] autorise tout (à éviter sauf modèles internes très contrôlés).
Modèles « non standards »
class CustomTable extends Model
{
protected $table = 'mon_autre_table';
protected $primaryKey = 'uid';
public $incrementing = false;
protected $keyType = 'string';
public $timestamps = false;
}
Eloquent supporte des structures non-conventionnelles, à clarifier explicitement.
2. CRUD de base et requêtes courantes
// Create
$client = Client::create([
'nom' => 'Acme',
'email' => 'contact@acme.test',
]);
// Read
$client = Client::find(1);
$client = Client::findOrFail(1); // 404 si non trouvé
$client = Client::where('email', $email)->first();
$client = Client::firstWhere('email', $email); // raccourci
// Update
$client->update(['actif' => false]);
// ou
$client->actif = false;
$client->save();
// Delete
$client->delete();
Client::destroy([1, 2, 3]); // delete multiple par IDs
// Soft delete (avec trait SoftDeletes)
$client->delete(); // marque deleted_at
$client->restore(); // annule
$client->forceDelete(); // suppression définitive
// Restoring
Client::onlyTrashed()->find(1)->restore();
Requêtes complexes
$clients = Client::query()
->where('actif', true)
->where('created_at', '>=', now()->subMonth())
->whereNotNull('telephone')
->orderBy('nom')
->take(20)
->get();
// Conditional queries
$clients = Client::query()
->when($request->search, fn ($q, $search) =>
$q->where('nom', 'like', "%{$search}%")
)
->when($request->actif === 'oui', fn ($q) => $q->where('actif', true))
->paginate(20);
when() évite les if/else autour de la query, plus lisible.
3. Relations : hasMany, belongsTo, manyToMany
One-to-Many
// Client a plusieurs commandes
class Client extends Model
{
public function orders()
{
return $this->hasMany(Order::class);
}
}
class Order extends Model
{
public function client()
{
return $this->belongsTo(Client::class);
}
}
$client = Client::find(1);
$orders = $client->orders; // Collection
$total = $client->orders()->where('status', 'paid')->sum('total');
$order = Order::find(1);
$client = $order->client;
Many-to-Many
// Un produit a plusieurs catégories
class Product extends Model
{
public function categories()
{
return $this->belongsToMany(Category::class)
->withTimestamps()
->withPivot('order');
}
}
// Migration : table pivot products_categories
Schema::create('category_product', function (Blueprint $table) {
$table->foreignId('category_id')->constrained();
$table->foreignId('product_id')->constrained();
$table->integer('order')->default(0);
$table->timestamps();
$table->primary(['category_id', 'product_id']);
});
$product->categories()->attach($categoryId);
$product->categories()->detach($categoryId);
$product->categories()->sync([1, 2, 3]); // remplace tout
$product->categories()->syncWithoutDetaching([4]); // ajoute sans retirer
One-to-One
class User extends Model
{
public function profile() { return $this->hasOne(Profile::class); }
}
HasManyThrough
// Pays → Villes → Habitants
class Country extends Model
{
public function residents()
{
return $this->hasManyThrough(Resident::class, City::class);
}
}
Polymorphes
// Un commentaire peut être sur un Post, une Photo, etc.
class Comment extends Model
{
public function commentable()
{
return $this->morphTo();
}
}
class Post extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
4. Eager loading et N+1
Le piège de performance le plus classique d’Eloquent.
Le problème N+1
// Mauvais : 1 + N requêtes
$clients = Client::all();
foreach ($clients as $client) {
echo $client->orders->count(); // 1 requête PAR client
}
Avec 100 clients : 101 requêtes pour la même information.
Solution : eager loading
// 2 requêtes : une pour clients, une pour orders
$clients = Client::with('orders')->get();
// Imbrication
$clients = Client::with('orders.items')->get();
// Plusieurs relations
$clients = Client::with(['orders', 'invoices', 'address'])->get();
// Avec contraintes
$clients = Client::with(['orders' => fn ($q) => $q->where('status', 'paid')])->get();
withCount
Pour ne charger que le nombre :
$clients = Client::withCount('orders')->get();
foreach ($clients as $client) {
echo $client->orders_count; // disponible directement
}
withSum, withAvg, withMin, withMax existent aussi.
Détecter les N+1
- Laravel Debugbar affiche les requêtes en dev
- Telescope montre tout
- Larastan rule
larastan/larastandétecte certains cas - En prod : monitoring des slow queries via le logger
Configurer en local :
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
Cette config fait planter en dev tout accès à une relation non eager-loadée. Force la rigueur pendant le développement.
5. Scopes pour requêtes réutilisables
class Client extends Model
{
public function scopeActif($query)
{
return $query->where('actif', true);
}
public function scopeRecent($query, int $days = 30)
{
return $query->where('created_at', '>=', now()->subDays($days));
}
public function scopeFromCity($query, string $city)
{
return $query->whereHas('address', fn ($q) => $q->where('city', $city));
}
}
Usage :
Client::actif()->recent(7)->fromCity('Dakar')->get();
Lisible, composable, réutilisable. Pattern central pour des modèles riches.
Global scopes
S’appliquent automatiquement à toutes les requêtes.
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('tenant_id', auth()->user()?->tenant_id);
}
}
// Dans le modèle
protected static function booted(): void
{
static::addGlobalScope(new TenantScope);
}
// Pour outrepasser
Client::withoutGlobalScope(TenantScope::class)->get();
Utilisé pour des cas comme multi-tenant, soft deletes (déjà natif), filtres organisationnels.
6. Accessors, mutators, casts
Casts
protected $casts = [
'actif' => 'boolean',
'metadata' => 'array',
'options' => AsArrayObject::class,
'birthdate' => 'date',
'preferences' => 'collection',
'amount' => 'decimal:2',
'role' => RoleEnum::class,
'encrypted_field' => 'encrypted',
];
Eloquent convertit automatiquement entre la base et l’application.
Accessors / Mutators (Laravel 9+)
use Illuminate\Database\Eloquent\Casts\Attribute;
class User extends Model
{
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}",
);
}
protected function password(): Attribute
{
return Attribute::make(
set: fn ($value) => bcrypt($value),
);
}
}
echo $user->full_name; // accessor
$user->password = 'secret'; // mutator hash automatiquement
Custom casts
Pour des cas non standards :
class MoneyCast implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return Money::fromCents((int) $value);
}
public function set($model, $key, $value, $attributes)
{
return $value->cents();
}
}
// Usage
protected $casts = [
'price' => MoneyCast::class,
];
7. Events et observers
Eloquent émet des événements à différents points du cycle de vie : creating, created, updating, updated, deleting, deleted, saving, saved, restoring, restored.
Observers
class ClientObserver
{
public function creating(Client $client): void
{
$client->reference = 'CLI-' . str_pad(Client::max('id') + 1, 6, '0', STR_PAD_LEFT);
}
public function created(Client $client): void
{
AuditLog::create([
'model' => Client::class,
'model_id' => $client->id,
'action' => 'created',
'user_id' => auth()->id(),
]);
}
public function deleting(Client $client): void
{
if ($client->orders()->exists()) {
throw new BusinessException("Client avec commandes ne peut être supprimé");
}
}
}
// Enregistrer
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Client::observe(ClientObserver::class);
}
}
Très utile pour audit, génération de références, validation métier, side effects.
Attention : observers sont synchrones
Pour des opérations lourdes (envoi mail, appel API), dispatcher un job depuis l’observer plutôt qu’exécuter directement :
public function created(Client $client): void
{
EnvoyerEmailBienvenue::dispatch($client);
}
8. Transactions
DB::transaction(function () {
$client = Client::create([...]);
$order = $client->orders()->create([...]);
Stock::decrementFor($order);
Notification::send($client->email, $order);
});
Si une exception est levée dans la closure, tout est rollback.
Avec retry sur deadlock
DB::transaction(function () {
// ...
}, attempts: 3);
Réessaie en cas de deadlock SQL (jusqu’à 3 fois).
Niveau d’isolation
DB::statement('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
DB::transaction(function () {
// ...
});
Pour des cas critiques (transferts financiers, compteurs concurrents).
9. Pagination et chunking
Pagination
$clients = Client::paginate(20);
$clients = Client::simplePaginate(20); // pas de count total
$clients = Client::cursorPaginate(20); // cursor-based, plus performant sur gros datasets
Dans la vue Blade :
{{ $clients->links() }}
Chunking pour très gros datasets
Client::chunk(500, function ($clients) {
foreach ($clients as $client) {
// traiter
}
});
Client::chunkById(1000, function ($clients) {
// ...
});
Pour traiter des millions de lignes sans charger toute la mémoire.
Lazy collections
Client::lazy()->each(function ($client) {
// traite un client à la fois, mémoire constante
});
10. Performance : indexes, requêtes raw
Indexes
Les index sont la première optimisation. Ajouter dans les migrations :
$table->index('email');
$table->index(['client_id', 'created_at']);
$table->unique('email');
Pour analyser les requêtes lentes : EXPLAIN dans MySQL/PostgreSQL.
Requêtes raw quand nécessaire
// Aggregations complexes
$stats = DB::table('orders')
->select(
DB::raw('DATE(created_at) as jour'),
DB::raw('COUNT(*) as nombre'),
DB::raw('SUM(total) as ca'),
)
->where('status', 'paid')
->groupBy('jour')
->orderByDesc('jour')
->take(30)
->get();
// Avec bindings (sécurité)
$users = DB::select(
'SELECT * FROM users WHERE role = ? AND created_at >= ?',
['admin', $date]
);
DB::raw ou DB::select pour des cas où Eloquent serait verbeux ou limité.
Bulk operations
// Insert massif
Client::insert($arrayDe1000Clients); // 1 requête
// Update massif
Client::where('actif', false)
->where('created_at', '<', now()->subYear())
->update(['archived' => true]); // 1 requête
// Delete massif
Client::where('archived', true)->delete();
Pour des batch jobs, ces opérations bulk sont indispensables.
Cache de modèle
$config = Cache::remember('app.config', 3600, fn () => Config::all());
Eloquent + Redis = cache simple et performant pour des données peu changeantes.
11. FAQ
Eloquent ou Query Builder, quoi choisir ?
Eloquent pour 90% des cas (lisibilité, relations). Query Builder (DB::table()) pour les requêtes complexes ou volumineuses où la magie Eloquent ralentit. SQL brut quand vraiment nécessaire.
Comment éviter les N+1 sans y penser ?
Model::preventLazyLoading() en non-production force à découvrir les N+1 pendant le développement. Combiné à Telescope ou Debugbar, on les attrape avant la prod.
Soft deletes : avantages et risques ?
Avantages : récupération facile, audit. Risques : tables qui grossissent indéfiniment, oublier les ->withTrashed() quand nécessaire. À utiliser avec discipline et politique de purge périodique des soft-deleted anciens.
Comment versionner les modèles (history) ?
Plusieurs packages : spatie/laravel-activitylog ou tightenco/laravel-quicker. Loggent automatiquement les changements de chaque modèle. Précieux pour audit et traçabilité.
Performance Eloquent vs SQL brut ?
Eloquent ajoute un overhead (hydration des modèles, événements). Pour 1000 lignes : négligeable. Pour 1M lignes : significatif, préférer Query Builder ou raw. Toujours mesurer avant d’optimiser.
Validation au niveau Eloquent ?
Pas vraiment l’idiome Laravel. La validation se fait avant Eloquent (Form Requests). Mais des saving events peuvent valider des invariants métier complexes en dernière ligne de défense.
Migrations en production : précautions ?
Migrations risquées (ALTER sur grosse table, ajout NOT NULL, drop de colonnes utilisées) doivent être décomposées en étapes compatibles. Tester sur staging avec données de prod. Backup avant chaque migration significative.
Articles liés (cluster Laravel)
- 👉 Laravel pour PME : guide backend PHP moderne (pillar)
- 👉 Laravel Filament : back-office rapide
- 👉 Laravel déploiement production
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.