Cómo usar relaciones polimórficas en Laravel

Clase 9 de 33Curso Avanzado de Laravel

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.

      Cómo usar relaciones polimórficas en Laravel