QUERY SCOPES EN LARAVEL: LA FORMA PROFESIONAL DE ESCRIBIR CONSULTAS REUTILIZABLES

Un Query Scope es un método que define una consulta común que se puede reutilizar en cualquier parte de la aplicación. En lugar de repetir condiciones como "where('active', true)" o "where('published_at', '<', now())" en varios sitios de la aplicación, se define esa lógica una sola vez. Existen dos tipos principales de Query Scopes en Laravel:
  • Local Scopes
  • Global Scopes
Los Query Scope
  • Mejoran la legibilidad del código
  • Facilitar el mantenimiento del código
  • Hacen tu código más expresivo
Si hay una condición "where()" que es usado en más de un lugar, es mejor convertirlo en un Query Scope. Los Query Scopes son una de las funcionalidades más útiles y elegantes de Eloquent. Permiten encapsular lógica de consultas complejas en métodos reutilizables, manteniendo el código limpio, legible y fácil de mantener.

Local Scopes

Se definen como métodos en el modelo comenzando con el prefijo "scope".

// app/Models/Post.php
namespace App\Models;

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

class Post extends Model
{
    public function scopeActive(Builder $query): void
    {
        $query->where('active', true);
    }

    public function scopePublished(Builder $query): void
    {
        $query->where('published_at', '<=', now());
    }

    public function scopePopular(Builder $query): void
    {
        $query->where('views', '>=', 1000);
    }
}
Como usarlo:

// En controladores, jobs, etc.
$posts = Post::active()->published()->popular()->get();

Post::active()->latest()->paginate(15);

Post::published()->where('category_id', 5)->get();

Scopes Dinámicos (con parámetros)

Se puedes pasar parámetros a los scopes".

public function scopeOfCategory(Builder $query, $categoryId): void
{
    $query->where('category_id', $categoryId);
}

public function scopeRecent(Builder $query, $days = 7): void
{
    $query->where('created_at', '>=', now()->subDays($days));
}
Como usarlo:

Post::ofCategory(3)->recent(30)->get();
Post::recent()->get(); // usa el valor por defecto (7 días)

Global Scope

Un Global Scope se aplica automáticamente a todas las consultas del modelo. Ejemplo

//Solo mostrar registros activos

// app/Models/User.php
namespace App\Models;

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

class User extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope('active', function (Builder $builder) {
            $builder->where('active', true);
        });
    }
}
Ahora todas las consultas de User solo devolverán usuarios activos:

User::all();           // Solo usuarios activos
User::find(1);         // Solo si está activo
Eliminar un Global Scope:

User::withoutGlobalScope('active')->get();

User::withoutGlobalScopes()->get(); // Elimina todos

Mejores Prácticas

  • Nombrar bien tus scopes. Usar nombres descriptivos dependiendo de su funcionalidad.
  • Mantener los scopes simples: Un scope debería tener una sola funcionalidad.
  • Combínalos: Los scopes están diseñados para encadenarse.
  • Usar traits para scopes reutilizables entre modelos.

Usar scopes en varios modelos

// app/Models/Scopes/HasActiveScope.php
trait HasActiveScope
{
    public function scopeActive(Builder $query): void
    {
        $query->where('active', true);
    }
}
Y en el modelo:

use HasActiveScope;

public function scopeActive(Builder $query): void
{
    $this->applyActiveScope($query); // o simplemente usa el trait
}

Ejemplo Real Completo

// app/Models/Order.php
class Order extends Model
{
    public function scopePending(Builder $query): void
    {
        $query->where('status', 'pending');
    }

    public function scopeCompleted(Builder $query): void
    {
        $query->where('status', 'completed');
    }

    public function scopeFromLastMonth(Builder $query): void
    {
        $query->where('created_at', '>=', now()->subMonth());
    }

    public function scopeByCustomer(Builder $query, $customerId): void
    {
        $query->where('customer_id', $customerId);
    }
}
Uso en un controlador:
public function index()
{
    $orders = Order::pending()
                   ->fromLastMonth()
                   ->byCustomer(request('customer_id'))
                   ->with(['customer', 'items'])
                   ->latest()
                   ->paginate(25);

    return view('orders.index', compact('orders'));
}