Passer au contenu principal
Lorsque vous voulez accéder aux modèles récupérés par leur ID, ou regrouper le traitement des résultats dans un scope local, vous pouvez utiliser afterQuery(). En plus d’ajouter des conditions SQL, il permet d’enregistrer dans le query builder une Closure exécutée sur les résultats récupérés. Cependant, ce n’est pas « un hook qui reçoit un résultat du même type à chaque exécution SQL ». Le contenu de la Collection transmise et le nombre d’appels varient selon la méthode de récupération.
Cette page concerne Laravel 13. En partant de After Query Callbacks dans la documentation officielle, elle vérifie les limites d’utilisation à partir du builder Eloquent et des tests officiels.

Regrouper le traitement des résultats dans un scope local

Si vous accédez souvent aux modèles par leur ID, vous pouvez préparer un scope qui uniformise les clés des résultats. Définissez l’exemple suivant dans app/Models/Post.php.
L’appelant peut l’enchaîner comme n’importe quel scope.
$posts reste une Eloquent Collection, et ses éléments restent des Post. get(42) renvoie le modèle dont la clé est 42, ou null s’il n’existe pas. Si vous limitez les colonnes récupérées avec select(), incluez aussi l’id utilisé comme clé. Si vous ne traitez les résultats qu’à un seul endroit, get()->keyBy('id') est plus simple. Choisissez afterQuery() lorsque vous voulez inclure le traitement post-récupération dans un scope ou une requête réutilisée. Pour la définition des scopes eux-mêmes, consultez Les scopes Eloquent.

Comprendre l’ordre d’exécution de get

Eloquent\Builder::get() applique les scopes globaux, crée les modèles, charge en eager loading les relations demandées, puis exécute en dernier les callbacks afterQuery. Avec un get() classique, les callbacks peuvent utiliser les relations chargées en eager loading. L’accès à des relations non demandées peut déclencher du SQL supplémentaire, concevez donc le scope en tenant compte de l’eager loading. Le moment diffère de celui de l’événement de modèle retrieved. retrieved est déclenché à la création de chaque modèle dans Model::newFromBuilder(), avant l’eager loading de get(). afterQuery(), qui traite l’ensemble des modèles récupérés, et les événements de chaque modèle n’ont pas le même rôle.
Il s’agit du flux de Eloquent\Builder::get(). cursor() ne charge pas les relations en eager loading et exécute les callbacks modèle par modèle. N’attendez pas le même traitement par lot qu’avec get().

La valeur de retour est transmise au callback suivant

afterQuery() ajoute les Closures dans l’ordre d’enregistrement. La partie qui les exécute, applyAfterQueryCallbacks(), est implémentée ainsi.
Ce n’est pas ?? qui est utilisé ici, mais ?:, qui repose sur l’évaluation truthy de PHP. Vous pouvez aussi utiliser un callback void qui modifie directement la Collection sans rien renvoyer. Dans ce cas, le même objet modifié est conservé. En revanche, si vous créez une nouvelle Collection, vous devez la renvoyer. En interne, l’argument et la valeur de retour sont mixed, mais évitez de faire de conversions de type arbitraires une API publique. Par exemple, remplacer le résultat de get() par une chaîne ou un tableau casse l’appel ->first() que first() effectue ensuite. Dans les scopes, limitez-vous à des traitements qui conservent les modèles, le type de Collection, le nombre d’éléments et l’ordre.

Différences selon la méthode de récupération

Le tableau suivant montre comment est traité un afterQuery() enregistré sur le builder Eloquent. first() appelle en interne limit(1)->get()->first(). Utiliser first() ne signifie pas qu’il faut changer le paramètre de la Closure en Post.
Le scope keyedById() de cette page sert à récupérer des modèles. Vous ne pouvez pas l’utiliser avec Post::keyedById()->pluck('id'). pluck() transmet une Illuminate\Support\Collection, qui ne correspond pas au type Illuminate\Database\Eloquent\Collection de l’exemple. Assouplir la déclaration de type ne suffit pas à rendre sûr un code qui lit des attributs de modèle à partir de valeurs.
Si vous n’avez besoin que de valeurs, n’utilisez pas le scope et écrivez Post::query()->pluck('id'). Pour extraire des valeurs après avoir traité les modèles, vous pouvez écrire Post::keyedById()->get()->pluck('id'). Le pluck() du second cas est une opération sur la Collection récupérée, pas sur le query builder.

Ne pas modifier le nombre d’éléments par page ni la position de récupération suivante

paginate() calcule le total en SQL, puis récupère les modèles de la page. Filtrer la Collection après la récupération ne recalcule pas le total. De plus, simplePaginate() et cursorPaginate() utilisent le résultat traité pour déterminer s’il existe une page suivante. chunkById() utilise l’ID en fin de chunk comme position de récupération suivante. Si vous modifiez le nombre d’éléments ou l’ordre dans le callback, la détection de page ou la position de reprise peuvent ne plus correspondre à la requête d’origine. Spécifiez le filtrage, le tri et les colonnes dans le SQL, et limitez afterQuery() à des traitements qui ne changent pas le sens des résultats récupérés. Si vous voulez seulement modifier le format de sortie d’une API, envisagez aussi une transformation avec les ressources API. La documentation officielle contient un exemple qui ajoute aux modèles un attribut d’extrait de résultat de recherche. Assigner des attributs de modèle peut aussi modifier ce qui sera enregistré. Évitez les conceptions qui appellent directement save() sur des modèles portant des attributs d’affichage inexistants en base de données.

L’enregistrement reste sur le query builder

Chaque appel à afterQuery() ajoute une Closure, et son exécution ne supprime pas l’enregistrement. Une nouvelle récupération avec le même builder la réexécute, et appeler deux fois le même scope enregistre aussi deux fois le callback. Cloner le builder transmet également les callbacks enregistrés. Dans le code qui effectue plusieurs récupérations ou dans les workers de longue durée, créez une nouvelle requête pour chaque usage et limitez les callbacks à un traitement des résultats sans effet de bord. Si vous y placez l’envoi d’e-mails ou la persistance, on confond facilement les multiples exécutions dues aux chunks ou aux curseurs avec une seule opération métier. Si vous fournissez des scopes réutilisables dans un package, indiquez aussi les méthodes de récupération prises en charge comme API publique et testez le contrat sur les types et le nombre d’appels.

Tester les différences entre les chemins de récupération

Dans l’application Laravel 13 utilisatrice, vérifiez non seulement le traitement des clés, mais aussi les appels avec first(), cursor() et les pages vides. L’exemple suivant suppose le modèle Post précédent et une migration de la table posts avec id, title, body, created_at et updated_at. RefreshDatabase isole les enregistrements entre les tests. Créez le test dans tests/Feature/AfterQueryTest.php.
Vous pouvez exécuter uniquement ce test.

Liste de vérification pour la maintenance

  • Indiquez les méthodes de récupération prises en charge par le scope, ainsi que les types du paramètre et de la valeur de retour de la Closure.
  • Vérifiez séparément le résultat vide de get() et un paginate() dont le total est 0.
  • Pour les chunks et les curseurs, confirmez que le nombre d’appels et l’ordre de récupération ne changent pas.
  • Concevez le code pour qu’un double enregistrement de callback ou la réutilisation d’un builder ne provoque pas d’effet de bord métier.
  • Lors des mises à jour de Laravel, revérifiez les chemins d’appel de get(), pluck(), cursor() et BuildsQueries.

Pages associées

Les scopes Eloquent

Revoyez les bases pour regrouper des contraintes SQL dans des scopes locaux et globaux.

Collections Eloquent

Revoyez la différence entre les Collections de modèles et les Support Collections.

Observers Eloquent et événements de modèle

Revoyez les événements par modèle, distincts du traitement des résultats récupérés.

Ressources API

Séparez la transformation des données renvoyées par l’API de l’enregistrement des modèles.

Sources primaires consultées

Dernière modification le 6 novembre 2026