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

# Scope di Eloquent

> Meccanismo di scope locali e globali. Analizziamo implementazioni interne del framework come SoftDeletingScope e mostriamo casi d'uso pratici come multi-tenant e filtri pubblicato/non pubblicato.

## Cosa sono gli scope

Gli scope di Eloquent sono un meccanismo che permette di raggruppare e riutilizzare vincoli sulle query. Esistono due tipi di scope.

| Tipo             | Momento di applicazione                       | Uso                                                  |
| ---------------- | --------------------------------------------- | ---------------------------------------------------- |
| **Global scope** | Sempre applicato automaticamente              | Soft delete, multi-tenant, filtro pubblicazione      |
| **Local scope**  | Applicato solo quando invocato esplicitamente | Filtri comuni come "post popolari" o "utenti attivi" |

## Local scope

### Definizione

I local scope si definiscono applicando l'attributo `#[Scope]` a un metodo del modello.

```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
{
    /**
     * Filtra i post pubblicati
     */
    #[Scope]
    protected function published(Builder $query): void
    {
        $query->where('status', 'published');
    }

    /**
     * Filtra i post popolari
     */
    #[Scope]
    protected function popular(Builder $query): void
    {
        $query->where('views', '>', 1000);
    }
}
```

<Info>
  L'attributo `#[Scope]` si trova nel namespace `Illuminate\Database\Eloquent\Attributes`. È sintassi nativa PHP dalla versione 8.0.
</Info>

### Utilizzo

Gli scope definiti si invocano come metodi. Puoi anche concatenarli.

```php theme={null}
use App\Models\Post;

// solo i post pubblicati
$posts = Post::published()->get();

// post pubblicati e popolari in ordine cronologico inverso
$posts = Post::published()->popular()->latest()->get();
```

### Passaggio di parametri

Nei metodi scope puoi definire parametri aggiuntivi dopo il secondo argomento.

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

Alla chiamata, gli argomenti si passano direttamente.

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

### Combinazione con `orWhere`

Concatenando uno scope con `orWhere`, può essere necessario un raggruppamento logico.

```php theme={null}
// con una closure (sicuro ma verboso)
$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

// usando gli higher-order si scrive in modo più conciso
$users = User::popular()->orWhere->active()->get();
```

## Global scope

### Meccanismo

Un global scope è una classe che implementa l'interfaccia `Illuminate\Database\Eloquent\Scope`. L'interfaccia richiede un unico metodo `apply`.

```php theme={null}
// definizione dell'interfaccia nel framework
// src/Illuminate/Database/Eloquent/Scope.php

interface Scope
{
    public function apply(Builder $builder, Model $model);
}
```

All'interno del metodo `apply` aggiungi il vincolo al query builder.

### Creare una classe di global scope

Genera lo scaffold con il comando `make:scope`.

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

Viene generato `app/Models/Scopes/ActiveScope.php`.

```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
{
    /**
     * Applica lo scope al query builder
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('is_active', true);
    }
}
```

### Applicazione al modello

<Steps>
  <Step title="Registrare con l'attributo #[ScopedBy] (consigliato)">
    In Laravel 13 il modo più semplice è usare l'attributo `#[ScopedBy]`.

    ```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
    {
        //
    }
    ```

    Puoi indicare più scope in un array.

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

  <Step title="Registrazione manuale nel metodo booted()">
    Puoi anche fare override di `booted` e chiamare `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>

Aggiungendo il global scope, tutte le query come `User::all()` ricevono automaticamente `WHERE is_active = 1`.

### Global scope con closure anonime

Per scope semplici, per cui non vale la pena creare una classe a parte, si può usare una closure.

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

<Warning>
  Per escludere in seguito uno scope definito come closure devi usare il nome dello scope (stringa), non il nome della classe.
</Warning>

### Esclusione di un global scope

A volte vuoi disattivare uno scope per una query specifica.

```php theme={null}
use App\Models\Scopes\ActiveScope;

// esclude uno scope specifico
User::withoutGlobalScope(ActiveScope::class)->get();

// esclude uno scope definito con closure
User::withoutGlobalScope('active')->get();

// esclude tutti i global scope
User::withoutGlobalScopes()->get();

// esclude più scope
User::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();

// esclude tutti tranne quelli indicati
User::withoutGlobalScopesExcept([TenantScope::class])->get();
```

## Interno del framework: SoftDeletingScope

Osservare come il trait standard `SoftDeletes` usa i global scope fa capire i pattern implementativi.

`SoftDeletingScope` implementa l'interfaccia `Scope`.

```php theme={null}
// src/Illuminate/Database/Eloquent/SoftDeletingScope.php

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

    /**
     * Applica lo scope al query builder
     * aggiunge il vincolo per recuperare solo i record con deleted_at NULL
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->whereNull($model->getQualifiedDeletedAtColumn());
    }

    /**
     * Estende il query builder con macro
     * abilita withTrashed() / onlyTrashed() ecc.
     */
    public function extend(Builder $builder)
    {
        foreach ($this->extensions as $extension) {
            $this->{"add{$extension}"}($builder);
        }

        // sovrascrive l'operazione delete() per aggiornare deleted_at
        $builder->onDelete(function (Builder $builder) {
            $column = $this->getDeletedAtColumn($builder);
            return $builder->update([
                $column => $builder->getModel()->freshTimestampString(),
            ]);
        });
    }
}
```

<Accordion title="L'implementazione di withTrashed()">
  In pratica `withTrashed()` chiama `withoutGlobalScope($this)`. Escludendo lo stesso `SoftDeletingScope`, i record cancellati diventano di nuovo recuperabili.

  ```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);
      });
  }
  ```

  Anche `onlyTrashed()` esclude lo scope e aggiunge `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>
  L'interfaccia `Scope` non definisce il metodo `extend`, ma il builder di Eloquent, se lo scope lo espone, lo chiama automaticamente. Puoi sfruttarlo per aggiungere macro personalizzate.
</Tip>

## Casi d'uso pratici

### Multi-tenant: filtro automatico per tenant ID

Nelle applicazioni SaaS è essenziale che tutte le query filtrino automaticamente per l'ID del tenant.

```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
{
    //
}
```

Ora basta `Post::all()` per ottenere solo i dati del tenant dell'utente autenticato.

### Filtro pubblicato/non pubblicato

Quando nel back office vuoi vedere anche i post non pubblicati, ma sul frontend solo quelli pubblicati.

```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());
    }
}
```

Nel back office escludi lo scope con `withoutGlobalScope`.

```php theme={null}
// frontend: solo pubblicati (PublishedScope si applica automaticamente)
$posts = Post::latest()->get();

// back office: mostra tutti i post
$posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get();
```

### Usa `addSelect` invece di `select`

<Warning>
  Quando aggiungi colonne all'interno di un global scope, usa `addSelect` invece di `select`. Con `select` sovrascriveresti le colonne che il chiamante ha già indicato.

  ```php theme={null}
  // esempio errato: sovrascrive il select del chiamante
  public function apply(Builder $builder, Model $model): void
  {
      $builder->select('id', 'tenant_id', 'name');
  }

  // esempio corretto: si aggiunge alle select esistenti
  public function apply(Builder $builder, Model $model): void
  {
      $builder->addSelect('tenant_id');
  }
  ```
</Warning>

## Prossimi passi

<Card title="Cast personalizzati di Eloquent" icon="wand-magic-sparkles" href="/it/advanced/eloquent-casts">
  Impara a implementare la logica di conversione degli attributi come cast personalizzato e a utilizzare il pattern Value Object.
</Card>


## Related topics

- [Cast personalizzati di Eloquent](/it/advanced/eloquent-casts.md)
- [Accessor, mutator e cast di Eloquent](/it/eloquent-mutators.md)
- [Eloquent Bootable Traits](/it/advanced/eloquent-bootable-traits.md)
- [Attributi PHP](/it/advanced/php-attributes.md)
- [Service provider differiti](/it/advanced/deferred-provider.md)
