ITSkillsCenter
Blog

Laravel Eloquent ORM en pratique : guide complet

12 min de lecture

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

  1. Modèle Eloquent : conventions et configuration
  2. CRUD de base et requêtes courantes
  3. Relations : hasMany, belongsTo, manyToMany
  4. Eager loading et N+1
  5. Scopes pour requêtes réutilisables
  6. Accessors, mutators, casts
  7. Events et observers
  8. Transactions
  9. Pagination et chunking
  10. Performance : indexes, requêtes raw
  11. 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 id par défaut, surchargeable
  • Timestamps created_at et updated_at automatiques
  • $fillable ou $guarded obligatoire pour mass assignment
  • $casts convertit 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/larastan dé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)


Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité