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 dansapp/Models/Post.php.
$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.
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.
?? 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é unafterQuery() 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.
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 avecfirst(), 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.
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 unpaginate()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()etBuildsQueries.
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.