backend / php-laravel
Eloquent ORM : modèles et requêtes
Explication
Ce que vous allez apprendre
- Comprendre ce qu'est un ORM et pourquoi Eloquent évite d'écrire du SQL à la main
- Protéger un modèle contre le mass assignment avec
$fillable - Convertir automatiquement des types entre la base de données et PHP avec
$casts - Créer des scopes locaux réutilisables pour composer des requêtes lisibles
- Traiter de très gros volumes de données sans épuiser la mémoire avec
chunkById
Dans quel contexte ?
Une équipe backend gère un catalogue de produits stocké en base MySQL. Plutôt que d'écrire des requêtes SQL à la main pour chaque opération (créer, filtrer, agréger), Eloquent permet de manipuler ces données comme de simples objets PHP, tout en gardant un contrôle fin sur la sécurité et les performances. Comprendre Eloquent en profondeur, c'est éviter à la fois les failles de sécurité (mass assignment) et les ralentissements silencieux (chargement de tables entières en mémoire).
D'abord, qu'est-ce qu'un ORM apporte concrètement ?
Un ORM (Object-Relational Mapper) fait correspondre une table de base de données à une classe PHP, et chaque ligne à une instance de cette classe. Écrire Produit::find(42) évite d'écrire SELECT * FROM produits WHERE id = 42, tout en restant portable si la base de données change de moteur (MySQL, PostgreSQL...).
Eloquent va plus loin qu'un simple mapping : il propose un query builder fluide, des relations entre modèles (vues en prochaine leçon), et des mécanismes de protection intégrés.
Prérequis
Cette leçon suppose une bonne compréhension des contrôleurs et requêtes HTTP (leçon 9), puisque c'est typiquement depuis un contrôleur que ces requêtes Eloquent sont déclenchées.
Ensuite, une faille de sécurité à connaître avant d'aller plus loin
Sans protection, Produit::create($request->all()) accepterait n'importe quel champ envoyé par le client — y compris, potentiellement, un champ sensible qui ne devrait jamais être modifiable directement. $fillable définit une liste blanche explicite des champs autorisés en création/mise à jour de masse.
Piège dangereux
Un modèle sans $fillable ni $guarded explicite accepte tous les champs par défaut sur les anciennes conventions Laravel. Toujours déclarer $fillable explicitement, même une liste courte, pour ne jamais dépendre d'un comportement implicite sur des données sensibles.
Il reste un problème pratique : les types ne correspondent pas toujours
Une base de données stocke souvent des booléens comme des 0/1, du JSON comme du texte brut. Les $casts d'Eloquent convertissent automatiquement ces valeurs dans les deux sens : un champ metadonnees casté en array se lit et s'écrit comme un vrai tableau PHP, sans json_encode/json_decode manuel à chaque endroit du code.
| Cast | Type en base | Type en PHP |
|---|---|---|
boolean | 0 / 1 | true / false |
array | Texte JSON | Tableau PHP |
datetime | Chaîne de date | Instance Carbon |
decimal:2 | Nombre | Chaîne formatée à 2 décimales |
Maintenant, composer des requêtes sans les répéter partout
Un scope local (scopeDisponibles($query)) encapsule une condition de requête fréquemment utilisée, appelable ensuite simplement via Produit::disponibles(). Ça évite de dupliquer ->where('est_disponible', true)->where('stock', '>', 0) dans dix contrôleurs différents, et centralise la définition métier de "disponible" à un seul endroit.
Astuce
firstOrCreate() et updateOrCreate() évitent une race condition classique : vérifier si une ligne existe puis la créer en deux requêtes séparées laisse une fenêtre où deux requêtes concurrentes pourraient créer un doublon. Ces méthodes gèrent ce cas en une seule opération atomique côté base.
Enfin, ne jamais charger une table entière en mémoire
Produit::all() sur une table de plusieurs millions de lignes ferait exploser la mémoire du processus PHP. chunkById(500, function ($lot) { ... }) traite les données par lots de 500, en gardant une empreinte mémoire constante quelle que soit la taille de la table.
Maintenant que tu sais interroger un modèle isolé, la prochaine leçon aborde ce qui rend Eloquent vraiment puissant : les relations entre plusieurs modèles, et comment éviter le fameux problème de performance N+1.
Commandes & code
Eloquent ORM : modèles et requêtes
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class Produit extends Model {
// Mass assignment : seuls ces champs sont remplissables via create()/update()
protected $fillable = ['nom', 'prix', 'categorie_id', 'description'];
// Casts automatiques : conversion type base <-> type PHP
protected $casts = [
'prix' => 'decimal:2',
'est_disponible' => 'boolean',
'metadonnees' => 'array', // JSON <-> tableau PHP automatique
'publie_le' => 'datetime',
];
// Accessor moderne (Laravel 9+) : calcule une valeur virtuelle
protected function nomAffiche(): Attribute {
return Attribute::make(
get: fn () => strtoupper($this->nom),
);
}
// Scope local réutilisable dans les requêtes : Produit::disponibles()->get()
public function scopeDisponibles($query) {
return $query->where('est_disponible', true)->where('stock', '>', 0);
}
public function scopePrixInferieurA($query, float $montant) {
return $query->where('prix', '<', $montant);
}
}
// Utilisation du query builder Eloquent
$produits = Produit::disponibles()
->prixInferieurA(100)
->orderByDesc('cree_le')
->limit(10)
->get();
// find / findOrFail / firstOrCreate / updateOrCreate : les raccourcis courants
$produit = Produit::findOrFail(42); // 404 automatique si absent (dans un contrôleur)
$produit = Produit::firstOrCreate(['sku' => 'ABC123'], ['nom' => 'Nouveau produit', 'prix' => 0]);
$produit = Produit::updateOrCreate(['sku' => 'ABC123'], ['prix' => 29.90]);
// Agrégations
$total = Produit::disponibles()->sum('prix');
$moyenne = Produit::avg('prix');
$parCategorie = Produit::selectRaw('categorie_id, COUNT(*) as total')
->groupBy('categorie_id')
->having('total', '>', 5)
->get();
// Requêtes brutes quand le query builder ne suffit pas
$resultats = \Illuminate\Support\Facades\DB::select(
'SELECT categorie_id, AVG(prix) as prix_moyen FROM produits WHERE stock > ? GROUP BY categorie_id',
[0]
);
// Chunking : traiter de grandes tables sans tout charger en mémoire
Produit::where('est_disponible', true)->chunkById(500, function ($lot) {
foreach ($lot as $produit) {
// traitement par lots de 500
}
});Résumé
$fillable(whitelist) protège contre le mass assignment non désiré — toujours l'expliciter.- Les scopes locaux (
scopeXxx) rendent les requêtes composables et lisibles. firstOrCreate/updateOrCreateévitent les races de type "vérifier puis créer".chunkByIdtraite de grands volumes sans exploser la mémoire du process.
Exercices pratiques
Mission : colmater une faille de mass assignment et un plantage memoire
Objectif : Proteger un modele Eloquent contre le mass assignment non controle et corriger un traitement qui charge une table entiere en memoire.
Contexte
Le modele Produit ne declare ni $fillable ni $guarded, et le controleur appelle directement Produit::create($request->all()). Un test de securite montre qu'envoyer un champ cout_achat (normalement reserve aux calculs internes de marge) dans le payload de creation suffit a le faire enregistrer tel quel en base. Par ailleurs, une commande de maintenance nocturne fait foreach (Produit::all() as $produit) { ... } sur une table qui vient de depasser 2 millions de lignes, et le processus PHP se fait tuer pour depassement de memoire.