Maison >développement back-end >tutoriel php >Apprenez à Master Query Scopes à Laravel

Apprenez à Master Query Scopes à Laravel

Emily Anne Brown
Emily Anne Brownoriginal
2025-03-06 02:28:09506parcourir
<p> <img src="/static/imghwm/default1.png" data-src="https://img.php.cn/upload/article/000/000/000/174119929536701.jpg" class="lazy" alt="Learn to master Query Scopes in Laravel"> </p> <p> Lors de la création d'une application Laravel, vous devrez peut-être rédiger des requêtes avec des contraintes utilisées à plusieurs endroits tout au long de l'application. Peut-être que vous créez une application multi-locataire et que vous devez constamment ajouter des contraintes <code>where</code> à la requête à filtrer par l'équipe de l'utilisateur. Ou peut-être que vous construisez un blog et vous devez constamment ajouter des contraintes <code>where</code> à la requête pour filtrer si le billet de blog a été publié. </p> <p> Dans Laravel, nous pouvons utiliser des lunettes de requête pour nous aider à garder ces contraintes proprement au même endroit et à les réutiliser. </p> <p> Dans cet article, nous étudierons la portée de la requête locale et la portée de la requête mondiale. Nous apprendrons la différence entre les deux, comment créer votre propre portée de requête et comment rédiger des tests pour eux. </p> <p> Après avoir lu cet article, vous devriez être en mesure d'utiliser les portes de requête en toute confiance dans votre application Laravel. </p> <h1> Qu'est-ce que la portée de la requête? </h1> <hr> <p> La portée de la requête vous permet de définir des contraintes dans les requêtes éloquentes de manière réutilisable. Ils sont généralement définis comme des méthodes sur le modèle Laravel, ou comme des classes qui implémentent les interfaces <code>IlluminateDatabaseEloquentScope</code>. </p> <p> Non seulement ils sont idéaux pour définir la logique réutilisable en un seul endroit, mais ils peuvent également rendre votre code plus lisible en cachant des contraintes de requête complexes après des appels de fonction simples. </p> <p> Les plages de requête sont divisées en deux types: </p> <ul> <li> GAMMES DE RESQUE LOCALES - Vous devez appliquer manuellement ces gammes à votre requête. </li> <li> Global Query Scopes - Par défaut, ces gammes sont appliquées à toutes les requêtes du modèle, à condition que la requête soit enregistrée. </li> </ul> <p> Si vous avez déjà utilisé la fonction "Soft Delete" intégrée à Laravel, vous avez peut-être utilisé la portée de la requête sans le savoir. Laravel utilise la portée de la requête locale pour vous fournir des méthodes telles que <code>withTrashed</code> et <code>onlyTrashed</code> sur le modèle. Il utilise également la portée de la requête globale pour ajouter automatiquement les contraintes <code>whereNull('deleted_at')</code> à toutes les requêtes du modèle afin que les enregistrements supprimés en douceur ne soient pas renvoyés par défaut dans la requête. </p> <p> Voyons comment créer et utiliser des lunettes de requête locales et des étendues de requête globales dans une application Laravel. </p> <h1> Portée de requête locale </h1> <hr> <p> La portée de la requête locale est définie comme une méthode sur le modèle éloquent, vous permettant de définir des contraintes qui peuvent être appliquées manuellement pour modéliser les requêtes. </p> <p> Supposons que nous créons une application de blog avec un panneau d'administration. Dans le panneau d'administration, nous avons deux pages: l'une pour la liste des articles de blog publiés et l'autre pour la liste des articles de blog non publiés. </p> <p> Nous supposons que le billet de blog est accessible à l'aide du modèle <code>AppModelsArticle</code> et que la table de base de données a une colonne vide <code>published_at</code> pour stocker l'heure de publication du blog. Si <code>published_at</code> est répertorié dans le passé, le billet de blog est considéré comme publié. Si <code>published_at</code> est répertorié à l'avenir ou est <code>null</code>, le billet de blog est jugé non publié. </p> <p> Pour obtenir des articles de blog publiés, nous pouvons écrire la requête suivante: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->where('published_at', '<', now()) ->get();</code></pre> <p> Pour obtenir des articles de blog non publiés, nous pouvons écrire la requête suivante: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; use Illuminate\Contracts\Database\Eloquent\Builder; $unpublishedPosts = Article::query() ->where(function (Builder $query): void { $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }) ->get();</code></pre> <p> La requête ci-dessus n'est pas particulièrement compliquée. Mais supposons que nous les utilisons à plusieurs endroits tout au long de l'application. À mesure que le nombre d'occurrences augmente, la possibilité de faire des erreurs ou d'oublier de mettre à jour la requête en un seul endroit augmente. Par exemple, les développeurs peuvent utiliser accidentellement <code>>=</code> au lieu de <code><</code> pour interroger les articles de blog publiés. Alternativement, la logique pour déterminer si un article de blog a été publié peut changer et nous devons mettre à jour toutes les requêtes. </p> <p> C'est là que les lunettes de requête sont très utiles. Organisons donc nos requêtes en créant une portée de requête locale sur le modèle <code>AppModelsArticle</code>. </p> <p> Les lunettes de requête locales sont définies par la création d'une méthode qui commence par <code>scope</code> et se termine par le nom attendu de la portée. Par exemple, une méthode nommée <code>scopePublished</code> créera une plage <code>published</code> sur le modèle. Cette méthode doit accepter une instance <code>IlluminateContractsDatabaseEloquentBuilder</code> et renvoyer une instance <code>IlluminateContractsDatabaseEloquentBuilder</code>. </p> <p> Nous ajoutons les deux plages au modèle <code>AppModelsArticle</code>: </p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; final class Article extends Model { public function scopePublished(Builder $query): Builder { return $query->where('published_at', '<', now()); } public function scopeNotPublished(Builder $query): Builder { return $query->where(function (Builder $query): Builder { return $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }); } // ... }</code> </p> <p> Comme nous l'avons vu dans l'exemple ci-dessus, nous avons déplacé la contrainte <code>where</code> de la requête précédente en deux méthodes distinctes: <code>scopePublished</code> et <code>scopeNotPublished</code>. Nous pouvons maintenant utiliser ces gammes dans notre requête comme celle-ci: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->published() ->get(); $unpublishedPosts = Article::query() ->notPublished() ->get();</code></pre> <p> À mon avis personnel, je trouve ces requêtes plus faciles à lire et à comprendre. Cela signifie également que si nous devons écrire une requête avec les mêmes contraintes à l'avenir, nous pouvons réutiliser ces lunettes. </p> <h1> Portée de requête globale </h1> <hr> <p> La portée de la requête globale remplit des fonctions similaires à la portée de la requête locale. Cependant, il n'est pas appliqué manuellement sur une requête par requête, mais s'applique automatiquement à toutes les requêtes sur le modèle. </p> <p> Comme nous l'avons mentionné plus tôt, la fonction "Soft Delete" intégrée de Laravel utilise la portée de la requête globale de Laravel. Cette plage ajoute automatiquement des contraintes <code>IlluminateDatabaseEloquentSoftDeletingScope</code> à toutes les requêtes du modèle. Si vous souhaitez comprendre comment cela fonctionne, vous pouvez consulter le code source sur GitHub ici. <code>whereNull('deleted_at')</code> </p> Par exemple, supposons que vous créez une application de blog multi-locataire avec un panneau d'administration. Vous voudrez peut-être simplement permettre aux utilisateurs de voir des articles qui appartiennent à leur équipe. Vous pouvez donc écrire une requête comme ceci: <p></p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->where('published_at', '<', now()) ->get();</code></pre> <p> Cette requête est bonne, mais il est facile d'oublier d'ajouter des contraintes <code>where</code>. Si vous écrivez une autre requête et oubliez d'ajouter des contraintes, vous vous retrouverez avec une erreur dans votre application qui permettra à l'utilisateur d'interagir avec des articles qui ne font pas partie de leur équipe. Bien sûr, nous ne voulons pas que cela se produise! </p> <p> Pour éviter cela, nous pouvons créer une portée globale que nous pouvons appliquer automatiquement à toutes nos requêtes de modèle <code>AppModelArticle</code>. </p> <h3> #Wow pour créer une portée de requête globale </h3> <p> Créons une portée de requête globale qui filtre toutes les requêtes par la colonne <code>team_id</code>. </p> <p> Veuillez noter que aux fins de cet article, nous gardons l'exemple simple. Dans une application réelle, vous souhaiterez peut-être utiliser une approche plus puissante pour gérer des situations comme l'utilisateur n'est pas authentifié ou l'utilisateur appartient à plusieurs équipes. Mais pour l'instant, restons simples afin que nous puissions nous concentrer sur le concept de portée de la requête mondiale. </p> <p> Nous allons d'abord exécuter la commande artisan suivante dans le terminal: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; use Illuminate\Contracts\Database\Eloquent\Builder; $unpublishedPosts = Article::query() ->where(function (Builder $query): void { $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }) ->get();</code></pre> <p> Cela aurait dû créer un nouveau fichier <code>app/Models/Scopes/TeamScope.php</code>. Nous ferons quelques mises à jour de ce fichier, puis examinerons le code fini: </p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; final class Article extends Model { public function scopePublished(Builder $query): Builder { return $query->where('published_at', '<', now()); } public function scopeNotPublished(Builder $query): Builder { return $query->where(function (Builder $query): Builder { return $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }); } // ... }</code></pre> <p> Dans l'exemple de code ci-dessus, nous pouvons voir que nous avons une nouvelle classe qui implémente l'interface <code>IlluminateDatabaseEloquentScope</code> et a une seule méthode appelée <code>apply</code>. C'est ainsi que nous définissons les contraintes à appliquer aux requêtes du modèle. </p> <p> Notre portée mondiale est maintenant disponible. Nous pouvons l'ajouter à n'importe quel modèle que nous voulons affiner la requête à l'équipe utilisateur. </p> <p> appliquons-le au modèle <code>AppModelsArticle</code>. </p> <h3> #Apply Global Query Scope </h3> <p> Il existe plusieurs façons d'appliquer la portée globale au modèle. La première méthode consiste à utiliser l'attribut <code>IlluminateDatabaseEloquentAttributesScopedBy</code> sur le modèle: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->published() ->get(); $unpublishedPosts = Article::query() ->notPublished() ->get();</code></pre> <p> Une autre méthode consiste à utiliser la méthode <code>booted</code> dans la méthode <code>addGlobalScope</code> du modèle: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $articles = Article::query() ->where('team_id', Auth::user()->team_id) ->get();</code></pre> <p> Les deux méthodes appliquent les contraintes <code>where('team_id', Auth::user()->team_id)</code> à toutes les requêtes sur le modèle <code>AppModelsArticle</code>. </p> <p> Cela signifie que vous pouvez maintenant écrire des requêtes sans vous soucier de filtrer par <code>team_id</code> colonne: </p> <pre class="brush:php;toolbar:false"><code>php artisan make:scope TeamScope</code></pre> <p> Si nous supposons que l'utilisateur appartient à une équipe avec <code>team_id</code> étant <code>1</code>, le SQL suivant sera généré pour la requête ci-dessus: </p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models\Scopes; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; use Illuminate\Support\Facades\Auth; final readonly class TeamScope implements Scope { /** * Apply the scope to a given Eloquent query builder. */ public function apply(Builder $builder, Model $model): void { $builder->where('team_id', Auth::user()->team_id); } }</code></pre> <p> C'est cool, non? ! </p> <h3> # Scope de requête mondiale anonyme </h3> <p> Une autre façon de définir et d'appliquer une portée de requête globale est d'utiliser une portée mondiale anonyme. </p> <p> mettons à jour notre modèle <code>AppModelsArticle</code> pour utiliser la portée globale anonyme: </p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use App\Models\Scopes\TeamScope; use Illuminate\Database\Eloquent\Attributes\ScopedBy; use Illuminate\Database\Eloquent\Model; #[ScopedBy(TeamScope::class)] final class Article extends Model { // ... }</code></pre> <p> Dans l'exemple de code ci-dessus, nous avons utilisé la méthode <code>addGlobalScope</code> pour définir la portée globale anonyme dans la méthode <code>booted</code> du modèle. <code>addGlobalScope</code> La méthode accepte deux paramètres: </p> <ul> <li> Le nom de la portée - Si vous devez l'ignorer dans votre requête, vous pouvez utiliser ce nom pour vous référer à la portée </li> <li> Contraintes de portée - Définissez la fermeture pour s'appliquer aux contraintes </li> </ul> <p> Comme avec d'autres méthodes, cela applique la contrainte <code>where('team_id', Auth::user()->team_id)</code> à toutes les requêtes sur le modèle <code>AppModelsArticle</code>. </p> <p> D'après mon expérience, la portée mondiale anonyme n'est pas aussi courante que la définition de la portée globale dans une classe distincte. Mais il est avantageux de savoir qu'ils sont disponibles en cas d'urgence. </p> <h3> #ignore Global Query Scope </h3> <p> Parfois, vous voudrez peut-être écrire une requête qui n'utilise pas la portée de la requête globale qui a été appliquée au modèle. Par exemple, vous pourriez construire une requête de rapport ou d'analyse qui doit contenir tous les enregistrements, quelle que soit la portée de la requête mondiale. </p> <p> Si tel est le cas, vous pouvez utiliser l'une des deux méthodes pour ignorer la portée globale. </p> <p> La première méthode est <code>withoutGlobalScopes</code>. Cette méthode vous permet d'ignorer toutes les lunettes globales sur le modèle si aucun paramètre ne lui est transmis: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->where('published_at', '<', now()) ->get();</code></pre> <p> ou, si vous voulez ignorer seulement un ensemble donné de lunettes globales, vous pouvez passer le nom de la portée à la méthode <code>withoutGlobalScopes</code>: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; use Illuminate\Contracts\Database\Eloquent\Builder; $unpublishedPosts = Article::query() ->where(function (Builder $query): void { $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }) ->get();</code></pre> <p> Dans l'exemple ci-dessus, nous ignorons <code>AppModelsScopesTeamScope</code> et une autre portée globale anonyme fictive appelée <code>another_scope</code>. </p> <p> ou, si vous souhaitez ignorer une seule portée globale, vous pouvez utiliser la méthode <code>withoutGlobalScope</code>: </p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; final class Article extends Model { public function scopePublished(Builder $query): Builder { return $query->where('published_at', '<', now()); } public function scopeNotPublished(Builder $query): Builder { return $query->where(function (Builder $query): Builder { return $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }); } // ... }</code></pre> <h3> #precautions pour la portée de la requête mondiale </h3> <p> Il est important de se rappeler que la portée de la requête globale n'est appliquée qu'aux requêtes faites via le modèle. Si vous écrivez des requêtes de base de données en utilisant l'apparence <code>IlluminateSupportFacadesDB</code>, la portée de la requête globale n'est pas appliquée. </p> <p> Par exemple, supposons que vous ayez écrit cette requête et que vous voulez qu'il craigne uniquement les articles appartenant à l'équipe des utilisateurs connectés: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->published() ->get(); $unpublishedPosts = Article::query() ->notPublished() ->get();</code></pre> <p> Dans la requête ci-dessus, même si la portée de la requête globale de <code>AppModelsArticle</code> est définie sur le modèle <code>AppModelsScopesTeamScope</code>, la portée ne sera pas appliquée. Par conséquent, vous devez vous assurer que les contraintes sont appliquées manuellement dans la requête de la base de données. </p> <h1> Tester la portée de la requête locale </h1> <hr> <p> Maintenant que nous avons appris à créer et à utiliser des lunettes de requête, nous examinerons comment écrire des tests pour eux. </p> <p> Il existe plusieurs façons de tester la portée d'une requête, et la méthode que vous choisissez peut dépendre de votre préférence personnelle ou du contenu de la portée que vous écrivez. Par exemple, vous voudrez peut-être écrire plus de tests de style unité pour la portée. Alternativement, vous voudrez peut-être rédiger plus de tests de style d'intégration qui testaient la portée dans des contextes tels que les contrôleurs. </p> <p> Personnellement, j'aime mélanger les deux afin que je puisse être sûr que la portée ajoute les contraintes correctes et que la portée est réellement utilisée dans la requête. </p> <p> Commençons par les exemples précédents <code>published</code> et <code>notPublished</code> Ranges et écrivez quelques tests pour eux. Nous aurons besoin d'écrire deux tests différents (un pour chaque plage): </p> <ul> <li> Une vérification de test <code>published</code> La plage ne renvoie que des articles publiés. </li> <li> Une vérification de test <code>notPublished</code> La plage ne renvoie que des articles non publiés. </li> </ul> <p> Regardons ces tests, puis discutons de ce qui est fait: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; $publishedPosts = Article::query() ->where('published_at', '<', now()) ->get();</code></pre> <p> Nous pouvons voir dans le fichier de test ci-dessus, nous créons d'abord des données dans la méthode <code>setUp</code>. Nous avons créé deux articles publiés, un article imprévu et un article arrangé. </p> <p> Ensuite, il y a un test (<code>only_published_articles_are_returned</code>) qui vérifie la gamme <code>published</code> pour retourner uniquement les articles publiés. Il existe également un test (<code>only_not_published_articles_are_returned</code>) qui vérifie la plage <code>notPublished</code> pour ne retourner que des articles non publiés. </p> <p> En faisant cela, nous pouvons maintenant être sûrs que notre portée de requête applique des contraintes comme prévu. </p> <h1> Plage de test dans le contrôleur </h1> <hr> <p> Comme nous l'avons mentionné, une autre façon de tester la portée d'une requête est de les tester dans le contexte utilisé dans le contrôleur. Bien que les tests d'isolement des portées peuvent aider à affirmer que la portée ajoute les contraintes correctes à la requête, elle ne teste pas réellement si la portée est utilisée dans l'application comme prévu. Par exemple, vous pouvez oublier d'ajouter une plage <code>published</code> à une requête dans la méthode du contrôleur. </p> <p> Ces types d'erreurs peuvent être capturés en écrivant des tests qui affirment qui renvoient les données correctes lors de l'utilisation de Scopes dans les méthodes de contrôleur. </p> <p> Prenons l'exemple d'avoir une application de blog multi-locataire et rédigez un test pour la méthode du contrôleur qui répertorie les articles. Supposons que nous ayons une méthode de contrôleur très simple, comme suit: </p> <pre class="brush:php;toolbar:false"><code>use App\Models\Article; use Illuminate\Contracts\Database\Eloquent\Builder; $unpublishedPosts = Article::query() ->where(function (Builder $query): void { $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }) ->get();</code></pre> <p> Nous supposons que le modèle <code>AppModelsArticle</code> a appliqué notre <code>AppModelsScopesTeamScope</code>. </p> <p> Nous affirmons que nous ne renvoyons que des articles qui appartiennent à l'équipe utilisateur. Le cas de test peut ressembler à ceci: </p> <pre class="brush:php;toolbar:false"><code>declare(strict_types=1); namespace App\Models; use Illuminate\Contracts\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; final class Article extends Model { public function scopePublished(Builder $query): Builder { return $query->where('published_at', '<', now()); } public function scopeNotPublished(Builder $query): Builder { return $query->where(function (Builder $query): Builder { return $query->whereNull('published_at') ->orWhere('published_at', '>', now()); }); } // ... }</code></pre> <p> Dans le test ci-dessus, nous créons deux équipes. Ensuite, nous créons un utilisateur appartenant à l'équipe un. Nous avons créé 3 articles pour les articles des équipes 1 et 2 pour l'équipe 2. Nous agissons ensuite en tant qu'utilisateurs et faisons une demande à la méthode du contrôleur qui répertorie les articles. La méthode du contrôleur ne doit retourner que 3 articles appartenant à l'équipe un, nous affirmons donc que seuls ces articles sont retournés en comparant les ID de l'article. </p> <p> Cela signifie que nous pouvons être sûrs que la portée globale de la requête est utilisée comme prévu dans la méthode du contrôleur. </p> <h1> Conclusion </h1> <hr> <p> Dans cet article, nous avons appris la portée de la requête locale et la portée mondiale des requêtes. Nous avons appris les différences entre eux, comment les créer et les utiliser, et comment rédiger des tests pour eux. </p> <p> J'espère que vous devriez maintenant pouvoir utiliser des portes de requête en toute confiance dans votre application Laravel. </p>

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn