
Pour tester une valeur nulle, utilisez whereNull() et whereNotNull(). Pour savoir si une ligne existe, exists() plutôt que count() > 0 : la base renvoie un booléen au lieu de compter. Sur une collection déjà chargée, testez isEmpty() et jamais if ($collection), qui est toujours vrai.
Quatre questions reviennent en boucle dès qu’on écrit des requêtes Eloquent : cette colonne est-elle nulle, cet enregistrement existe-t-il, combien y en a-t-il, et comment ne récupérer que les colonnes utiles. Ce guide traite les quatre, avec le SQL réellement émis et les pièges qui coûtent une requête de trop.
Cet article fait partie du parcours Développement web. Tous les exemples ont été exécutés sur Laravel 13.30.1 avec PHP 8.4.25, sur une table tasks contenant les colonnes id, title, assignee (nullable), active, priority et les colonnes de timestamps, dont l’absence provoque une erreur au premier enregistrement, et dont la conversion en objets Carbon est automatique.
Tester si une colonne est nulle : whereNull et whereNotNull
En SQL, on écrit IS NULL et IS NOT NULL parce que = NULL ne renvoie jamais vrai. Eloquent expose les deux conditions sous forme de méthodes :
// les tâches sans responsable
Task::whereNull('assignee')->get();
// les tâches qui en ont un
Task::whereNotNull('assignee')->get();Le SQL produit est exactement celui attendu :
select * from "tasks" where "assignee" is not nullLa même chose fonctionne sur le constructeur de requêtes, sans passer par un modèle :
use Illuminate\Support\Facades\DB;
DB::table('tasks')->whereNull('assignee')->get();Vous pouvez passer un tableau pour tester plusieurs colonnes à la fois, la condition portant alors sur toutes :
Task::whereNull(['assignee', 'closed_at'])->get();On lit souvent que where('assignee', null) ne fonctionne pas. C’est faux depuis longtemps : Laravel détecte la valeur nulle et génère is null. Vérifié sur Laravel 13, Task::where('assignee', null) produit select * from "tasks" where "assignee" is null et renvoie les mêmes lignes que whereNull(). Préférez quand même whereNull() : l’intention est explicite et ne dépend pas de cette normalisation.
Les variantes suivent la logique habituelle du constructeur, avec or et la négation :
Task::whereNull('assignee')->orWhereNotNull('closed_at')->get();Tester si un enregistrement existe
Pour savoir si une ligne correspond, n’allez pas la chercher. exists() demande à la base une réponse booléenne :
if (Task::where('title', 'Publier')->exists()) {
// au moins une ligne correspond
}
if (Task::where('title', 'Publier')->doesntExist()) {
// aucune ligne
}La différence avec count() se voit dans le journal des requêtes. Voici ce que les trois écritures envoient réellement à la base :
-- exists()
select exists(select * from "tasks" where "active" = ?) as "exists"
-- count() > 0
select count(*) as "aggregate" from "tasks" where "active" = ?
-- get()->isNotEmpty()
select * from "tasks" where "active" = ?Les trois répondent à la question, mais la troisième rapatrie et hydrate toutes les lignes en objets pour en jeter le contenu aussitôt. Sur une table volumineuse, c’est la différence entre une requête constante et une requête qui grossit avec les données.
Créer seulement si l’enregistrement n’existe pas
Le couple « je vérifie puis j’insère » a un nom : firstOrCreate(). Le premier tableau sert à chercher, le second fournit les valeurs utilisées uniquement en cas de création.
$post = Post::firstOrCreate(
['slug' => $slug], // critère de recherche
['title' => $title, 'body' => $body], // utilisé seulement à la création
);Si la ligne existe déjà, le second tableau est ignoré : l’enregistrement est renvoyé tel quel, sans écriture. Deux variantes complètent la famille :
// même logique, mais sans enregistrer en base
$post = Post::firstOrNew(['slug' => $slug], ['title' => $title]);
$post->exists; // false tant que vous n’appelez pas save()
// trouve et met à jour, ou crée
$post = Post::updateOrCreate(['slug' => $slug], ['title' => $title]);firstOrCreate() exécute un SELECT puis un INSERT. Deux requêtes simultanées peuvent passer le SELECT en même temps et tenter deux insertions. Posez une contrainte d’unicité en base sur la colonne concernée, par une migration : c’est elle, et non le code PHP, qui garantit l’unicité.
Compter et détecter une collection vide
Il faut distinguer deux count(). Sur le constructeur de requêtes, c’est un COUNT(*) exécuté par la base. Sur une collection déjà chargée, c’est un simple comptage en mémoire.
// COUNT(*) en base, aucune ligne rapatriée
Task::where('active', true)->count(); // 3
// les lignes sont chargées, puis comptées en PHP
$tasks = Task::where('active', true)->get();
$tasks->count(); // 3Si vous avez déjà besoin des lignes, la seconde forme est la bonne : ne relancez pas une requête de comptage. Si vous ne voulez que le nombre, prenez la première.
Pour tester si une collection est vide, deux méthodes explicites :
$tasks = Task::where('title', 'Inexistant')->get();
$tasks->isEmpty(); // true
$tasks->isNotEmpty(); // falseif ($tasks) est toujours vrai, même quand la collection ne contient rien : c’est un objet. Le test doit porter sur isEmpty(), isNotEmpty() ou blank(). C’est l’erreur la plus fréquente de cette section.
Notez au passage que isEmpty() ne prend aucun argument. On croise souvent $posts->isEmpty($posts), qui fonctionne par accident parce que l’argument est ignoré.
Dans une vue Blade, @forelse traite le cas vide sans condition explicite :
@forelse ($tasks as $task)
<li>{{ $task->title }}</li>
@empty
<li>Aucune tâche pour le moment.</li>
@endforelseNe sélectionner que les colonnes utiles
Par défaut, Eloquent fait un select *. Sur une table large, ou quand vous n’affichez que deux champs, c’est du transfert et de l’hydratation pour rien. Trois écritures permettent de restreindre les colonnes :
// 1. select() sur le constructeur
Task::select('title', 'priority')->where('active', true)->get();
// 2. en argument de get() ou de first()
Task::where('active', true)->get(['title']);
Task::where('assignee', 'Damien')->first(['title', 'priority']);
// 3. en ajoutant à une sélection existante
Task::select('id')->addSelect('title')->first();Quand vous ne voulez qu’une valeur, deux méthodes évitent de passer par un modèle complet :
// une seule valeur scalaire
Task::where('assignee', 'Damien')->value('title'); // 'Écrire le guide'
// une liste de valeurs
Task::where('active', true)->pluck('title'); // ['Écrire le guide', 'Relire']
// une liste indexée par une autre colonne
Task::pluck('title', 'id'); // [1 => 'Écrire le guide', …]value() remplace avantageusement le first(['title'])->title qu’on écrit par réflexe : il renvoie directement la chaîne, et null si aucune ligne ne correspond, là où l’autre écriture provoque une erreur sur un objet nul.
Après un first(['title']), l’attribut priority n’existe pas sur le modèle. Y accéder renvoie null sans erreur, ce qui masque le problème jusqu’à l’affichage. Pensez aussi à inclure la clé primaire dans votre sélection : sans id, les relations et save() ne peuvent plus retrouver la ligne.
Erreurs fréquentes
isEmpty(), isNotEmpty() ou blank().first(['title']), lire priority renvoie null sans erreur. Incluez toujours la clé primaire, sinon les relations et save() ne fonctionnent plus.count() sur le constructeur : c’est un COUNT(*) qui ne rapatrie aucune ligne.$posts->isEmpty($posts) se voit souvent et fonctionne par accident, l’argument étant ignoré.

