Passer au contenu principal

Qu’est-ce qu’un scope

Les scopes Eloquent permettent de regrouper des contraintes réutilisables sur une requête. Il en existe deux types.
TypeMoment d’applicationUsage
Scope globalToujours appliqué automatiquementSoft delete, multi-tenant, filtre de publication
Scope localUniquement lorsqu’explicitement invoquéFiltres communs comme « posts populaires », « utilisateurs actifs »

Scopes locaux

Définition

Les scopes locaux sont définis en appliquant l’attribut #[Scope] à une méthode du modèle.
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Filtre sur les posts publiés
     */
    #[Scope]
    protected function published(Builder $query): void
    {
        $query->where('status', 'published');
    }

    /**
     * Filtre sur les posts populaires
     */
    #[Scope]
    protected function popular(Builder $query): void
    {
        $query->where('views', '>', 1000);
    }
}
L’attribut #[Scope] se trouve dans le namespace Illuminate\Database\Eloquent\Attributes. C’est une syntaxe native PHP 8.0+.

Utilisation

Les scopes définis s’invoquent comme des méthodes. Le chaînage est possible.
use App\Models\Post;

// Récupérer uniquement les posts publiés
$posts = Post::published()->get();

// Récupérer les posts publiés et populaires, triés par date décroissante
$posts = Post::published()->popular()->latest()->get();

Passage de paramètres

À partir du second argument, la méthode de scope peut recevoir des paramètres supplémentaires.
#[Scope]
protected function ofStatus(Builder $query, string $status): void
{
    $query->where('status', $status);
}
Passez l’argument tel quel lors de l’appel.
$posts = Post::ofStatus('draft')->get();
$posts = Post::ofStatus('published')->get();

Combinaison avec orWhere

Combiner des scopes avec orWhere peut nécessiter un groupe logique.
// Méthode avec closure (fiable mais verbeuse)
$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

// L'utilisation d'une méthode d'ordre supérieur permet d'écrire plus simplement
$users = User::popular()->orWhere->active()->get();

Scopes globaux

Mécanisme

Un scope global est une classe qui implémente l’interface Illuminate\Database\Eloquent\Scope. Cette interface ne demande qu’une seule méthode apply.
// Définition de l'interface dans le framework
// src/Illuminate/Database/Eloquent/Scope.php

interface Scope
{
    public function apply(Builder $builder, Model $model);
}
Ajoutez les contraintes au query builder dans la méthode apply.

Création d’une classe de scope global

Générez un squelette avec la commande make:scope.
php artisan make:scope ActiveScope
app/Models/Scopes/ActiveScope.php est généré.
<?php

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class ActiveScope implements Scope
{
    /**
     * Applique le scope au query builder
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('is_active', true);
    }
}

Application au modèle

1

Enregistrement via l'attribut #[ScopedBy] (recommandé)

Sous Laravel 13, l’attribut #[ScopedBy] est la manière la plus simple.
<?php

namespace App\Models;

use App\Models\Scopes\ActiveScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Model;

#[ScopedBy([ActiveScope::class])]
class User extends Model
{
    //
}
Vous pouvez spécifier plusieurs scopes dans un tableau.
#[ScopedBy([ActiveScope::class, TenantScope::class])]
class User extends Model
{
    //
}
2

Enregistrement manuel via la méthode booted()

Une autre méthode consiste à surcharger la méthode booted et à y appeler addGlobalScope.
<?php

namespace App\Models;

use App\Models\Scopes\ActiveScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope(new ActiveScope);
    }
}
Une fois le scope global ajouté, chaque requête (comme User::all()) reçoit automatiquement WHERE is_active = 1.

Scope global via closure anonyme

Pour un scope simple qui ne mérite pas un fichier séparé, définissez-le via une closure.
protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}
Pour exclure ultérieurement un scope défini via closure, il faut utiliser son nom (chaîne) plutôt qu’un nom de classe.

Exclure un scope global

Il y a des situations où vous voulez désactiver un scope pour certaines requêtes.
use App\Models\Scopes\ActiveScope;

// Exclure un scope particulier
User::withoutGlobalScope(ActiveScope::class)->get();

// Exclure un scope défini via closure
User::withoutGlobalScope('active')->get();

// Exclure tous les scopes globaux
User::withoutGlobalScopes()->get();

// Exclure plusieurs scopes
User::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();

// Exclure tout sauf les scopes spécifiés
User::withoutGlobalScopesExcept([TenantScope::class])->get();

Interne du framework : SoftDeletingScope

Voir comment le trait standard SoftDeletes de Laravel utilise un scope global vous éclaire sur le pattern d’implémentation. SoftDeletingScope implémente l’interface Scope.
// src/Illuminate/Database/Eloquent/SoftDeletingScope.php

class SoftDeletingScope implements Scope
{
    protected $extensions = [
        'Restore', 'RestoreOrCreate', 'CreateOrRestore',
        'WithTrashed', 'WithoutTrashed', 'OnlyTrashed',
    ];

    /**
     * Applique le scope au query builder
     * Ajoute la contrainte pour ne récupérer que les lignes où deleted_at est NULL
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->whereNull($model->getQualifiedDeletedAtColumn());
    }

    /**
     * Étend le query builder avec des macros
     * Rend disponibles withTrashed() / onlyTrashed(), etc.
     */
    public function extend(Builder $builder)
    {
        foreach ($this->extensions as $extension) {
            $this->{"add{$extension}"}($builder);
        }

        // Surcharger l'opération delete() pour qu'elle mette à jour deleted_at à la place
        $builder->onDelete(function (Builder $builder) {
            $column = $this->getDeletedAtColumn($builder);
            return $builder->update([
                $column => $builder->getModel()->freshTimestampString(),
            ]);
        });
    }
}
withTrashed() appelle en réalité withoutGlobalScope($this). Autrement dit, il exclut SoftDeletingScope lui-même pour permettre la récupération des enregistrements supprimés.
protected function addWithTrashed(Builder $builder)
{
    $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) {
        if (! $withTrashed) {
            return $builder->withoutTrashed();
        }

        return $builder->withoutGlobalScope($this);
    });
}
De même, onlyTrashed() exclut le scope puis ajoute whereNotNull('deleted_at').
protected function addOnlyTrashed(Builder $builder)
{
    $builder->macro('onlyTrashed', function (Builder $builder) {
        $model = $builder->getModel();

        $builder->withoutGlobalScope($this)->whereNotNull(
            $model->getQualifiedDeletedAtColumn()
        );

        return $builder;
    });
}
Bien que la méthode extend ne soit pas définie dans l’interface Scope, le builder Eloquent l’appelle automatiquement si le scope la possède. C’est utile pour ajouter des macros personnalisées.

Cas d’usage pratiques

Multi-tenant : filtrage automatique par tenant ID

Dans une application SaaS, il est essentiel d’appliquer automatiquement un filtre de tenant à toutes les requêtes.
<?php

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if ($tenantId = auth()->user()?->tenant_id) {
            $builder->where('tenant_id', $tenantId);
        }
    }
}
use App\Models\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([TenantScope::class])]
class Post extends Model
{
    //
}
Ainsi, un simple Post::all() ne renvoie que les données du tenant de l’utilisateur authentifié.

Filtre publié/non publié

Cas où l’administration doit voir aussi les brouillons, mais le frontend uniquement les posts publiés.
<?php

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class PublishedScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('status', 'published')
                ->where('published_at', '<=', now());
    }
}
En admin, excluez le scope avec withoutGlobalScope.
// Frontend : uniquement publiés (PublishedScope appliqué automatiquement)
$posts = Post::latest()->get();

// Admin : tous les posts
$posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get();

Utilisez addSelect plutôt que select

Pour ajouter une colonne dans un scope global, utilisez addSelect et non select. Utiliser select écrasera les colonnes que l’appelant a définies dans son select.
// Mauvais exemple : écrase le select de l'appelant
public function apply(Builder $builder, Model $model): void
{
    $builder->select('id', 'tenant_id', 'name');
}

// Bon exemple : ajoute au select existant
public function apply(Builder $builder, Model $model): void
{
    $builder->addSelect('tenant_id');
}

Étapes suivantes

Casts personnalisés Eloquent

Apprenez à implémenter votre logique de transformation d’attributs sous forme de cast personnalisé et à tirer parti du pattern Value Object.
Dernière modification le 13 juillet 2026