> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Scopes Eloquent

> Découvrez le fonctionnement des scopes locaux et globaux. Décryptez l'implémentation interne (SoftDeletingScope, etc.) et explorez des cas d'usage pratiques comme le multi-tenant et les filtres publié/privé.

## 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.

| Type             | Moment d'application                    | Usage                                                               |
| ---------------- | --------------------------------------- | ------------------------------------------------------------------- |
| **Scope global** | Toujours appliqué automatiquement       | Soft delete, multi-tenant, filtre de publication                    |
| **Scope local**  | Uniquement 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 theme={null}
<?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);
    }
}
```

<Info>
  L'attribut `#[Scope]` se trouve dans le namespace `Illuminate\Database\Eloquent\Attributes`. C'est une syntaxe native PHP 8.0+.
</Info>

### Utilisation

Les scopes définis s'invoquent comme des méthodes. Le chaînage est possible.

```php theme={null}
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.

```php theme={null}
#[Scope]
protected function ofStatus(Builder $query, string $status): void
{
    $query->where('status', $status);
}
```

Passez l'argument tel quel lors de l'appel.

```php theme={null}
$posts = Post::ofStatus('draft')->get();
$posts = Post::ofStatus('published')->get();
```

### Combinaison avec `orWhere`

Combiner des scopes avec `orWhere` peut nécessiter un groupe logique.

```php theme={null}
// 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`.

```php theme={null}
// 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`.

```bash theme={null}
php artisan make:scope ActiveScope
```

`app/Models/Scopes/ActiveScope.php` est généré.

```php theme={null}
<?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

<Steps>
  <Step title="Enregistrement via l'attribut #[ScopedBy] (recommandé)">
    Sous Laravel 13, l'attribut `#[ScopedBy]` est la manière la plus simple.

    ```php theme={null}
    <?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.

    ```php theme={null}
    #[ScopedBy([ActiveScope::class, TenantScope::class])]
    class User extends Model
    {
        //
    }
    ```
  </Step>

  <Step title="Enregistrement manuel via la méthode booted()">
    Une autre méthode consiste à surcharger la méthode `booted` et à y appeler `addGlobalScope`.

    ```php theme={null}
    <?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);
        }
    }
    ```
  </Step>
</Steps>

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.

```php theme={null}
protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}
```

<Warning>
  Pour exclure ultérieurement un scope défini via closure, il faut utiliser son nom (chaîne) plutôt qu'un nom de classe.
</Warning>

### Exclure un scope global

Il y a des situations où vous voulez désactiver un scope pour certaines requêtes.

```php theme={null}
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`.

```php theme={null}
// 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(),
            ]);
        });
    }
}
```

<Accordion title="Voir l'implémentation de withTrashed()">
  `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.

  ```php theme={null}
  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')`.

  ```php theme={null}
  protected function addOnlyTrashed(Builder $builder)
  {
      $builder->macro('onlyTrashed', function (Builder $builder) {
          $model = $builder->getModel();

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

          return $builder;
      });
  }
  ```
</Accordion>

<Tip>
  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.
</Tip>

## 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 theme={null}
<?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);
        }
    }
}
```

```php theme={null}
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 theme={null}
<?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`.

```php theme={null}
// 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`

<Warning>
  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`.

  ```php theme={null}
  // 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');
  }
  ```
</Warning>

## Étapes suivantes

<Card title="Casts personnalisés Eloquent" icon="wand-magic-sparkles" href="/fr/advanced/eloquent-casts">
  Apprenez à implémenter votre logique de transformation d'attributs sous forme de cast personnalisé et à tirer parti du pattern Value Object.
</Card>


## Related topics

- [Eloquent Bootable Traits](/fr/advanced/eloquent-bootable-traits.md)
- [Attributs PHP](/fr/advanced/php-attributes.md)
- [Service providers différés](/fr/advanced/deferred-provider.md)
- [Socialite for Discord](/fr/packages/socialite-discord.md)
- [Socialite (LINE Login) - LINE SDK for Laravel](/fr/packages/laravel-line-sdk/socialite.md)
