Relaciones polimórficas en Laravel

- Andrés Cruz

In english

Este material forma parte de mi curso y libro completo; puedes adquirirlos desde el apartado de libros y/o cursos Curso y libro Laravel 11 con Tailwind Vue 3, introducción a Jetstream Livewire e Inerta desde cero - 2024.

Anteriormente vimos cómo manejar una relación de tipo muchos a muchos entre las etiquetas y las publicaciones, las etiquetas son un tipo de relación que los podemos emplear en otros tipos de relaciones y nos solo con los posts, por ejemplo, si tuviéramos una web de alquileres de habitaciones u hoteles, podemos emplear un sistema de etiquetas, o de ventas de casas, automóviles, una web de videos como YouTube entre otros, las etiquetas son una estructura muy común para agregar dados a las entidades principales, por lo tanto, resulta muy común querer emplear este tipo de relaciones con otros datos y en Laravel lo tenemos muy fácil, en vez de crear una tabla pivot para cada relación como mostramos anteriormente, podemos crear las relaciones polimórficas que en otras palabras, permite emplear la misma tabla pivote para cualquier relación que queramos relacionar con las etiquetas y Laravel de manera interna sabe a que le pertenece cada relación mediante una etiqueta:

 

Campo de tipo para identificar la relación

 

De esta forma, con esta columna que es gestionada internamente por Laravel, podemos emplear una misma tabla pivote para mapear distintos tipos como usuarios, videos o publicaciones en nuestros modelos en Laravel de manera transparente para nosotros.

Otro ejemplo de relación que puede ser de tipo polimórfica es el de documentos/comentarios, que pueden ser de una persona, publicación, usuario, entre otros este tipo de relación sería de uno a muchos de tipo polimórfica.

Anteriormente empleamos una relación polimórfica entre los usuarios y los tokens de autenticación mediante Sanctum.

Las relaciones polimórficas permiten que un registro en una tabla esté relacionado con múltiples modelos diferentes.

Las relaciones polimórficas en Laravel Eloquent son una herramienta poderosa para manejar situaciones en las que un registro puede estar relacionado con diferentes entidades. En lugar de crear tablas separadas para cada tipo de relación, las relaciones polimórficas nos permiten establecer conexiones flexibles entre modelos. Aquí tienes una introducción con ejemplos:

A diferencia de las relaciones tradicionales (como 1 a n o n a n), donde la relación es siempre fija, en las relaciones polimórficas, la relación puede variar según el registro.

Aunque comenzamos introduciendo el uso de las relaciones polimorfismo para las relaciones de tipo muchos a muchos, también las podemos emplear en el resto de las relaciones pero, es en el uso de las relaciones de tipo muchos a muchos que tiene principal importancia (y también las de uno a muchos).

Es importante notar que para la relación principal, la de etiquetas se emplea el método de morphToMany() para definir la relación, y para la "etiquetable" se emplea morphedByMany(), es decir, esta última sería la que tiene la relación polimórfica. 

Ahora, para todos los tipos de relaciones polimorfismo, se emplea el prefijo de morph para definir las mismas:

  • morphOne(MODEL, PIVOTTABLE): Define una relación uno a uno polimórfica. Por ejemplo, una relación para el perfil puede emplearse para distintos tipos como usuarios, personas o empresas, su equivalente a las relaciones clásicas viene siendo el de tipo hasOne().
  • morphMany(MODEL, PIVOTTABLE): Se utiliza para definir una relación uno a muchos polimórfica. Por ejemplo, una relación de categorías puede estar relacionada con otros modelos como posts o vídeos, su equivalente a las relaciones clásicas vienen siendo hasMany().
  • ManyToMany:
    • morphToMany(MODEL, PIVOTTABLE): Este método se utiliza para definir una relación muchos a muchos polimórfica. Por ejemplo, la tabla tags que puede estar relacionada con diferentes tipos de modelos como posts o vídeos.
    • morphedByMany(MODEL, PIVOTTABLE): Este método se utiliza en un modelo para establecer una relación de muchos a muchos con otros modelos utilizando una relación polimórfica, esta relación se coloca al "etiquetable".

La definición de estos métodos pueden ser un poco abstractos, así que veamos algunos ejemplos.

Caso práctico: Relación muchos a muchos

Comentemos creando las migraciones:

$ php artisan make:migration create_tags_table

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTagsTable extends Migration
{
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('tags');
    }
}

Otra migración para la tabla pivote:

$ php artisan make:migration create_taggables_table

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTaggablesTable extends Migration
{
    public function up()
    {
        Schema::create('taggables', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('tag_id');             $table->unsignedBigInteger('taggable_id');
            $table->string('taggable_type'); // 'App\Models\Post'
            $table->timestamps();
            
            $table->unique(['tag_id', 'taggable_id', 'taggable_type']);
            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('taggables');
    }
} 

Que usualmente se le coloca el sufijo de able como en el caso anterior.

Y en los modelos:

// app/Models/Tag.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }
}

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }
}

En el controlador de posts, podemos realizar algunas modificaciones para la asignación de etiquetas a los posts:

class PostController extends Controller
{
    public function create()
    {
        $tags = Tag::pluck('id', 'title');
        $categories = Category::pluck('id', 'title');
        $post = new Post();
        return view('dashboard.post.create', compact('post', 'categories', 'tags'));
    }

    public function store(StorePostPost $request)
    {
        $post = Post::create($requestData);
        $post->tags()->sync($request->tags_id);
        ***
    }
    
    public function edit(Post $post)
    {
        $tags = Tag::pluck('id', 'title');
        ***
        return view('dashboard.post.edit', compact('post', 'categories', 'tags'));
    }

    public function update(UpdatePostPut $request, Post $post)
    {
        //$post->tags()->attach(1);
        $post->tags()->sync($request->tags_id);
        ***
    }
}

En cuanto a la vista, queda como:

<div class="mt-3">
    <label for="tag_id">Tags</label>
    <select multiple class="form-control" name="tags_id[]" id="tags_id">
        @foreach ($tags as $title => $id)
    <option {{ in_array($id, old('tags_id') ?: $post->tags->pluck("id")->toArray()) ? "selected": "" }} value="{{ $id }}">{{ $title }}</option>
        @endforeach
    </select>
</div>

Con el código anterior, creamos un listado de selección múltiple de todas las etiquetas, las etiquetas que se encuentren asignadas al post, se encuentran seleccionadas por defecto.

También creamos el proceso CRUD para las etiquetas:

<?php

namespace App\Http\Controllers\Dashboard;

use App\Models\Tag;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tag\PutRequest;
use App\Http\Requests\Tag\StoreRequest;

class TagController extends Controller
{

    public function index()
    {

        if (!auth()->user()->hasPermissionTo('editor.tag.index')) {
            return abort(403);
        }

        $tags = Tag::paginate(2);
        return view('dashboard/tag/index', compact('tags'));
    }

    public function create()
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.create')) {
            return abort(403);
        }
        $tag = new Tag();
        return view('dashboard.tag.create', compact('tag'));
    }

    public function store(StoreRequest $request)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.create')) {
            return abort(403);
        }
        Tag::create($request->validated());
        return to_route('tag.index')->with('status', 'Tag created');
    }

    public function show(Tag $tag)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.index')) {
            return abort(403);
        }
        return view('dashboard/tag/show', ['tag' => $tag]);
    }

    public function edit(Tag $tag)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.update')) {
            return abort(403);
        }
        return view('dashboard.tag.edit', compact('tag'));
    }

    public function update(PutRequest $request, Tag $tag)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.update')) {
            return abort(403);
        }
        $tag->update($request->validated());
        return to_route('tag.index')->with('status', 'Tag updated');
    }

    public function destroy(Tag $tag)
    {
        if (!auth()->user()->hasPermissionTo('editor.tag.destroy')) {
            return abort(403);
        }
        $tag->delete();
        return to_route('tag.index')->with('status', 'Tag delete');
    }
}

En el código anterior solamente se muestra el controlador, debes de implementar el resto del código como lo son las clases Requests, vistas, rutas y permisos asociados, cualquier duda, puedes consultar el código fuente al final de la sección.

Ahora, veremos algunos otros ejemplos que fueron tomados de la documentación oficial y que puedes tomar de referencia para conocer cómo emplear el resto de los tipos de relaciones disponibles, si pudistes entender la relación polimórfica de tipo muchos a muchos que presentamos antes, estos serán muchos más fáciles de comprender.

Caso práctico: Relación uno a muchos

En este ejemplo tenemos como modelo principal el de comentarios, como modelos de tipo "able/seleccionable" tenemos los de vídeos y posts, es decir, los comentarios pueden ser empleados por la entidad de posts y vídeos:

posts
    id - integer
    title - string
    body - text
 
videos
    id - integer
    title - string
    url - string
 
comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

En cuanto a los modelos, quedan como:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Comment extends Model
{
    /**
     * Get the parent commentable model (post or video).
     */
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
 
class Video extends Model
{
    /**
     * Get all of the video's comments.
     */
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Caso práctico: Relación uno a uno

En este ejemplo tenemos como modelo principal el de imágenes, como modelos de tipo "able/seleccionable" tenemos los de usuarios y posts, es decir, estas entidades tienen una imagen asociada

posts
    id - integer
    name - string
 
users
    id - integer
    name - string
 
images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

En cuanto a los modelos, quedan como:

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
 
class Image extends Model
{
    /**
     * Get the parent imageable model (user or post).
     */
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
 
class Post extends Model
{
    /**
     * Get the post's image.
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
 
class User extends Model
{
    /**
     * Get the user's image.
     */
    public function image(): MorphOne
    {
        return $this->morphOne(Image::class, 'imageable');
    }
}

Conclusión

En resumen, en los dos últimos ejemplos vemos que tenemos que emplear el método morphTo() para la relación principal y para los de tipo "able/seleccionable" se emplean los de tipo morphMany()morphOne() respectivamente.

Finalmente, los métodos tipo morph() para especificar las relaciones reciben varios parámetros que pueden ser útiles si Laravel no logra interpretar de manera correcta el nombre de la tabla y relación, por ejemplo:

return $this->morphToMany(Tag::class, 'taggable', 'taggables', 'taggable_id');

Como consideración adicional, como comentamos anteriormente, las relaciones muchos a muchos y uno a muchos de tipo polimorfismo son las más interesantes en este tipo de relaciones ya que, al querer hacer 'etiquetable/able' una relación de tipo muchos a muchos sin ser polimórfica, tendríamos que duplicar la tabla pivote para tal fin (y en la de uno a muchos no podríamos crearla para que simule una del tipo polimórfica del mismo tipo), pero, con el uso de las relaciones polimórficas podemos emplear la misma tabla; en contraparte, el uso de las relaciones polimórficas para la relación de tipo uno a uno puede ser fácilmente manejados mediante una relación tradicional del mismo tipo, es decir, que no sean polimórficas, por ejemplo, recordemos que para las relaciones de tipo uno a uno tenemos:

posts
    id - integer
    name - string
 
users
    id - integer
    name - string
 
images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

Pudiéramos tener una relación similar a la anterior en donde los posts y usuarios puedan tener una imagen mediante:

posts
    id - integer
    name - string
    image_id - integer
 
users
    id - integer
    name - string
    image_id - integer
 
images
    id - integer
    url - string

Más información sobre las relaciones en:

https://laravel.com/docs/master/eloquent-relationships

Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz en Udemy

Acepto recibir anuncios de interes sobre este Blog.