Cómo usar relaciones polimórficas en Laravel

Clase 9 de 33Curso Avanzado de Laravel

Contenido del curso

Laravel y Base de Datos

    Resumen

    Con Eloquent puedes construir un sistema de calificaciones flexible y escalable sin duplicar lógica. Aquí verás cómo las relaciones polimórficas permiten que una sola tabla pivot conecte múltiples modelos, cómo estructurar la migración, el modelo Rating y dos traits reutilizables: CanRate y CanBeRated. El resultado: un único flujo para puntuar productos, páginas o categorías con pruebas que pasan.

    ¿Qué resuelven las relaciones polimórficas en Eloquent?

    Las relaciones polimórficas permiten que un modelo pertenezca a más de un tipo de modelo. Con un ejemplo de rating, un usuario o invitado puede puntuar un producto, una página o una categoría con una sola relación. Así evitas múltiples tablas intermedias y repeticiones.

    ¿Cómo funciona el concepto de morph en Eloquent?

    • Un par de columnas define la relación: <nombre>_id y <nombre>_type.
    • Con morphs('ratable') y morphs('qualifier') identificas quién es calificado y quién califica.
    • Se usa una única tabla pivot para todos los tipos relacionados.

    ¿Qué roles intervienen en el rating?

    • Ratable: la entidad que recibe la calificación, por ejemplo, un producto.
    • Qualifier: la entidad que califica, por ejemplo, un usuario.
    • Score: el valor de la calificación almacenado en la tabla pivot.

    ¿Cómo se implementa el sistema de rating con morphs?

    Primero se crea la migración para la tabla intermedia y luego el modelo Rating que extiende Pivot. Esto habilita timestamps, score y los tipos polimórficos.

    ¿Cómo crear la migración de ratings con morphs?

    • Ejecuta el comando: php artisan make:migration para crear la tabla.
    • Define id, timestamps, score y los morphs para las dos entidades.
    Schema::create('ratings', function (Blueprint $table) { $table->id(); $table->morphs('ratable'); // ratable_id, ratable_type $table->morphs('qualifier'); // qualifier_id, qualifier_type $table->unsignedTinyInteger('score'); $table->timestamps(); });

    ¿Cómo definir el modelo Rating y morphTo?

    • Extiende de Pivot en lugar de Model porque es una tabla intermedia.
    • Habilita incrementing y referencia la tabla ratings.
    • Agrega dos relaciones morphTo: ratable() y qualifier().
    use Illuminate\Database\Eloquent\Relations\Pivot; class Rating extends Pivot { public $incrementing = true; protected $table = 'ratings'; public function ratable() { return $this->morphTo(); } public function qualifier() { return $this->morphTo(); } }

    ¿Qué comandos de artisan intervienen?

    • php artisan migrate: crea la tabla ratings.
    • php artisan make:model Rating: genera el modelo para la tabla intermedia.

    ¿Cómo usar traits para calificar y ser calificado?

    Para mantener el diseño flexible, se usan traits: uno para quienes califican (CanRate) y otro para quienes son calificados (CanBeRated). Allí se centraliza la relación morphToMany, alias, timestamps, columnas pivot y filtros por tipo.

    ¿Cómo implementar el trait CanRate con morphToMany?

    • La relación ratings($model = null) arma dinámicamente la clase objetivo con get_class o getMorphClass.
    • Usa morphToMany con: clase relacionada, nombre de la relación, tabla ratings, qualifier_id y ratable_id.
    • Define alias con as('rating'), agrega withTimestamps() y columnas pivot: score y ratable_type.
    • Filtra por ratable_type y qualifier_type para asegurar consistencia.
    • Expone métodos rate($model, $score) y hasRated($model) para operar y validar no calificar dos veces.
    trait CanRate { public function ratings($model = null) { $modelClass = $model ? get_class($model) : $this->getMorphClass(); return $this->morphToMany( $modelClass, 'qualifiers', 'ratings', 'qualifier_id', 'ratable_id' )->as('rating') ->withTimestamps() ->withPivot('score', 'ratable_type') ->wherePivot('ratable_type', $modelClass) ->wherePivot('qualifier_type', $this->getMorphClass()); } public function hasRated($model) { return $this->ratings($model)->whereKey($model->getKey())->exists(); } public function rate($model, $score) { if ($this->hasRated($model)) { return false; } $this->ratings($model)->attach($model->getKey(), [ 'score' => $score, 'ratable_type' => get_class($model), ]); return true; } }

    ¿Cómo implementar el trait CanBeRated y calcular promedio?

    • La relación se llama qualifiers() e invierte las llaves pivot.
    • Permite calcular el promedio con avg('score'). Si no hay datos, devuelve 0.
    trait CanBeRated { public function qualifiers($model = null) { $modelClass = $model ? get_class($model) : $this->getMorphClass(); return $this->morphToMany( $modelClass, 'ratables', 'ratings', 'ratable_id', 'qualifier_id' )->withPivot('score') ->wherePivot('ratable_type', $this->getMorphClass()); } public function averageRating($modelClass = null) { return (float) ($this->qualifiers($modelClass)->avg('score') ?? 0); } }

    ¿Cómo integrarlo en modelos y pruebas?

    • En User usa el trait CanRate para que el usuario pueda calificar.
    • En Product usa CanBeRated para que el producto sea calificado.
    • En pruebas, si aparece “espera un objeto y le pasamos un string”, pasa el modelo completo y guarda ratable_type con get_class.
    • Ejecuta tests y valida que el attach incluya score y type.

    Palabras clave y habilidades trabajadas:

    • Relaciones polimórficas: morphs, morphTo, morphToMany.
    • Tabla pivot: ratings, score, ratable_type, qualifier_type, timestamps.
    • Traits reutilizables: CanRate, CanBeRated con alias as('rating').
    • Prácticas de calidad: evitar calificar dos veces con hasRated y cubrir con tests.

    ¿Te gustaría que también los usuarios se califiquen entre sí? Comenta cómo lo implementarías y qué validaciones agregarías.