Saltar al contenido principal

Qué es un scope

Un scope de Eloquent es un mecanismo que empaqueta y reutiliza restricciones aplicadas a una consulta. Existen dos tipos.
TipoMomento de aplicaciónUso
Scope globalSe aplica siempre de forma automática.Soft delete, multi-tenant, filtros de publicación.
Scope localSe 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

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);
    }
}
El atributo #[Scope] está en el namespace Illuminate\Database\Eloquent\Attributes. Es sintaxis nativa de PHP 8.0 o superior.

Uso

Los scopes se invocan como métodos. Se pueden encadenar.
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.
#[Scope]
protected function ofStatus(Builder $query, string $status): void
{
    $query->where('status', $status);
}
Al invocarlo, pasa los argumentos como es habitual.
$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.
// 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.
// 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.
php artisan make:scope ActiveScope
Se crea app/Models/Scopes/ActiveScope.php.
<?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

1

Registro con el atributo #[ScopedBy] (recomendado)

En Laravel 13, la forma más sencilla es usar el atributo #[ScopedBy].
<?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.
#[ScopedBy([ActiveScope::class, TenantScope::class])]
class User extends Model
{
    //
}
2

Registro manual mediante booted()

También puedes sobrescribir el método booted y llamar a 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);
    }
}
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.
protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}
Para excluir después un scope definido con closure, tienes que usar el nombre del scope (cadena), no un nombre de clase.

Excluir scopes globales

En algunas consultas concretas querrás desactivar el scope.
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.
// 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(),
            ]);
        });
    }
}
withTrashed() en realidad llama a withoutGlobalScope($this). Es decir, se excluye a sí mismo (SoftDeletingScope) para que los registros eliminados se puedan recuperar.
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').
protected function addOnlyTrashed(Builder $builder)
{
    $builder->macro('onlyTrashed', function (Builder $builder) {
        $model = $builder->getModel();

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

        return $builder;
    });
}
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.

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

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

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

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.
// 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');
}

Próximos pasos

Casts personalizados de Eloquent

Aprende a implementar la lógica de conversión de atributos como casts personalizados y a aprovechar el patrón Value Object.
Última modificación el 13 de julio de 2026