> ## 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 de Eloquent

> Explica cómo funcionan los scopes locales y globales. Analiza la implementación interna del framework, como SoftDeletingScope, y presenta casos de uso prácticos como multi-tenant o filtros de publicación.

## Qué es un scope

Un scope de Eloquent es un mecanismo que empaqueta y reutiliza restricciones aplicadas a una consulta. Existen dos tipos.

| Tipo             | Momento de aplicación                          | Uso                                                          |
| ---------------- | ---------------------------------------------- | ------------------------------------------------------------ |
| **Scope global** | Se aplica siempre de forma automática.         | Soft delete, multi-tenant, filtros de publicación.           |
| **Scope local**  | Se aplica solo cuando se llama explícitamente. | Filtros comunes como "posts populares" o "usuarios activos". |

## Scopes locales

### Definición

Los scopes locales se declaran añadiendo el atributo `#[Scope]` a un método del modelo.

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

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

<Info>
  El atributo `#[Scope]` está en el namespace `Illuminate\Database\Eloquent\Attributes`. Es sintaxis nativa de PHP 8.0 o superior.
</Info>

### Uso

Los scopes se invocan como métodos. Se pueden encadenar.

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

// Solo los posts publicados
$posts = Post::published()->get();

// Publicados y populares, ordenados de más nuevo a más antiguo
$posts = Post::published()->popular()->latest()->get();
```

### Paso de parámetros

Puedes definir parámetros adicionales a partir del segundo argumento del método.

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

Al invocarlo, pasa los argumentos como es habitual.

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

### Combinación con `orWhere`

Al conectar un scope con `orWhere`, a veces necesitas agrupar lógicamente.

```php theme={null}
// Con closure (funciona seguro pero es más verboso)
$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

// Usando el método de orden superior queda más limpio
$users = User::popular()->orWhere->active()->get();
```

## Scopes globales

### Cómo funcionan

Un scope global es una clase que implementa la interfaz `Illuminate\Database\Eloquent\Scope`. Esta interfaz solo requiere un método `apply`.

```php theme={null}
// Definición de la interfaz en el framework
// src/Illuminate/Database/Eloquent/Scope.php

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

Dentro de `apply` añades restricciones al query builder.

### Crear una clase de scope global

Genera el esqueleto con el comando `make:scope`.

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

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

### Aplicación al modelo

<Steps>
  <Step title="Registro con el atributo #[ScopedBy] (recomendado)">
    En Laravel 13, la forma más sencilla es usar el atributo `#[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
    {
        //
    }
    ```

    Puedes indicar varios scopes en el array.

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

  <Step title="Registro manual mediante booted()">
    También puedes sobrescribir el método `booted` y llamar a `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>

Cuando añades un scope global, todas las consultas (como `User::all()`) reciben automáticamente el filtro `WHERE is_active = 1`.

### Scope global mediante closure anónima

Para scopes simples que no merecen una clase propia, puedes definirlos con un closure.

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

<Warning>
  Para excluir después un scope definido con closure, tienes que usar el nombre del scope (cadena), no un nombre de clase.
</Warning>

### Excluir scopes globales

En algunas consultas concretas querrás desactivar el scope.

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

// Excluir un scope concreto
User::withoutGlobalScope(ActiveScope::class)->get();

// Excluir un scope definido con closure
User::withoutGlobalScope('active')->get();

// Excluir todos los scopes globales
User::withoutGlobalScopes()->get();

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

// Excluir todos menos los indicados
User::withoutGlobalScopesExcept([TenantScope::class])->get();
```

## Interior del framework: SoftDeletingScope

Ver cómo el trait estándar `SoftDeletes` aprovecha los scopes globales revela un patrón de implementación útil.

`SoftDeletingScope` implementa la interfaz `Scope`.

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

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

    /**
     * Aplica el scope al query builder
     * Añade la restricción para obtener solo registros con deleted_at NULL
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->whereNull($model->getQualifiedDeletedAtColumn());
    }

    /**
     * Extiende el query builder con macros
     * Deja disponibles métodos como withTrashed() y onlyTrashed()
     */
    public function extend(Builder $builder)
    {
        foreach ($this->extensions as $extension) {
            $this->{"add{$extension}"}($builder);
        }

        // Sobrescribe la operación delete() para que actualice deleted_at en su lugar
        $builder->onDelete(function (Builder $builder) {
            $column = $this->getDeletedAtColumn($builder);
            return $builder->update([
                $column => $builder->getModel()->freshTimestampString(),
            ]);
        });
    }
}
```

<Accordion title="Implementación de withTrashed()">
  `withTrashed()` en realidad llama a `withoutGlobalScope($this)`. Es decir, se excluye a sí mismo (`SoftDeletingScope`) para que los registros eliminados se puedan recuperar.

  ```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 forma parecida, `onlyTrashed()` se excluye a sí mismo y añade `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>
  La interfaz `Scope` no define el método `extend`, pero el builder de Eloquent lo llama automáticamente si el scope lo tiene. Es útil cuando quieres añadir macros propias.
</Tip>

## Casos de uso prácticos

### Multi-tenant: filtrado automático por ID de tenant

En una aplicación SaaS es crítico aplicar el filtro por tenant a todas las consultas de forma automática.

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

Con esto, al llamar a `Post::all()` solo se devuelven los datos del tenant del usuario autenticado.

### Filtros de publicación

En el panel de administración quieres ver también los posts no publicados, pero en el frontend solo los ya publicados.

```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 el panel de administración se excluye el scope con `withoutGlobalScope`.

```php theme={null}
// Frontend: solo publicados (PublishedScope se aplica automáticamente)
$posts = Post::latest()->get();

// Admin: se muestran todos los posts
$posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get();
```

### Usa `addSelect` en lugar de `select`

<Warning>
  Cuando añadas columnas dentro de un scope global, utiliza `addSelect` en lugar de `select`. Si usas `select`, sobrescribirás las columnas que la consulta original haya seleccionado.

  ```php theme={null}
  // Mal ejemplo: sobrescribe el select original
  public function apply(Builder $builder, Model $model): void
  {
      $builder->select('id', 'tenant_id', 'name');
  }

  // Buen ejemplo: añade la columna al select existente
  public function apply(Builder $builder, Model $model): void
  {
      $builder->addSelect('tenant_id');
  }
  ```
</Warning>

## Próximos pasos

<Card title="Casts personalizados de Eloquent" icon="wand-magic-sparkles" href="/es/advanced/eloquent-casts">
  Aprende a implementar la lógica de conversión de atributos como casts personalizados y a aprovechar el patrón Value Object.
</Card>


## Related topics

- [Atributos de PHP](/es/advanced/php-attributes.md)
- [Socialite for Discord](/es/packages/socialite-discord.md)
- [Socialite (LINE Login) - LINE SDK for Laravel](/es/packages/laravel-line-sdk/socialite.md)
- [Factorías de Eloquent](/es/eloquent-factories.md)
- [Autenticación OAuth 2.0 - Google Sheets API for Laravel](/es/packages/laravel-google-sheets/oauth.md)
