Gestión de Lógica de Consultas y Relaciones con Scopes y associate() en Laravel Eloquent

El ORM Eloquent de Laravel proporciona diversas herramientas para simplificar la interacción con la base de datos. Entre las más potentes se encuentran los Scopes, que permiten encapsular lógica de consulta, y el método associate(), que facilita la gestión de claves foráneas en las relaciones.

Scopes Locales

Los scopes locales permiten definir restricciones de consulta personalizadas que pueden ser reutilizadas a lo largo de la aplicación. Se definen dentro del modelo anteponiendo el prefijo scope al nombre del método.

Definición de un Scope Local

Supongamos que tenemos un modelo Product y queremos filtrar frecuentemente aquellos que están en stock y tienen un precio competitivo.

namespace App\Models;

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

class Product extends Model
{
    /**
     * Scope para filtrar productos con existencias.
     */
    public function scopeInStock(Builder $query): Builder
    {
        return $query->where('quantity', '>', 0);
    }

    /**
     * Scope para filtrar por categoría específica.
     */
    public function scopeOfCategory(Builder $query, string $category): Builder
    {
        return $query->where('category_name', $category);
    }
}

Implementación en Consultas

Para aplicar estos filtros, simplemente se invoca el método omitiendo el prefijo scope:

use App\Models\Product;

// Obtener productos disponibles de la categoría 'Electrónica'
$gadgets = Product::inStock()->ofCategory('Electronics')->get();

Scopes Globales

A diferencia de los locales, los scopes globales se aplican automáticamente a todas las consultas realizadas sobre un modelo específico. Esto es útil para implementar lógicas como el "Soft Delete" o el filtrado por inquilino en aplicaciones multi-tenant.

Creación de una Clase de Scope Global

Primero, definimos una clase que implemente la interfaz Scope:

namespace App\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)
    {
        $builder->where('is_published', true);
    }
}

Registro del Scope en el Modelo

Para activar el scope, debemos sobrescribir el método booted del modelo:

namespace App\Models;

use App\Scopes\PublishedScope;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new PublishedScope);
    }
}

Si en algún momento necesitamos obtener todos los registros, incluyendo los que el scope global filtra, utilizamos withoutGlobalScope:

$allArticles = Article::withoutGlobalScope(PublishedScope::class)->get();

El Método associate()

El método associate() se utiliza específicamente para actualizar relaciones de tipo belongsTo (pertenece a). En lugar de asignar manualmente el ID de la clave foránea, este método permite pasar directamente una instancia del modelo relacionado.

Ejemplo Práctico de Asociación

Imaginemos que un Comment pertenece a un Post. Para vincular un comentario con su publicación correspondiente, haríamos lo siguiente:

use App\Models\Post;
use App\Models\Comment;

// Cargamos las instancias
$post = Post::find(10);
$comment = new Comment(['content' => 'Excelente artículo.']);

// Establecemos la relación
$comment->post()->associate($post);

// Guardamos el modelo hijo
$comment->save();

Este procedimiento actualiza automáticamente la columna post_id en el objeto $comment basándose en la clave primaria de $post.

Disociación de Modelos

Si necesitamos eliminar la relación (hacer que la clave foránea sea null), podemos emplear el método dissociate():

$comment->post()->dissociate();
$comment->save();

Es importante notar que associate() y dissociate() solo afectan a la instancia en memoria; para persistir el cambio en la base de datos, siempre se debe llamar al método save().

Etiquetas: Laravel Eloquent PHP ORM query-builder

Publicado el 8-8 17:22