Polymorphic relationships in Laravel

- Andrés Cruz

En español

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.

Previously we saw how to manage a many-to-many relationship between tags and posts, tags are a type of relationship that we can use in other types of relationships and not only with posts, for example, if we had a website for room rentals or hotels, we can use a tag system, or for sales of houses, cars, a video website like YouTube among others, tags are a very common structure to add data to the main entities, therefore, it is very common to want to use this type of relationships with other data and in Laravel we have it very easy, instead of creating a pivot table for each relationship as we showed previously, we can create polymorphic relationships that in other words, allow us to use the same table pivot for any relationship that we want to relate to the labels and Laravel internally knows what each relationship belongs to by means of a label:

 

Type field to identify the relationship

 

In this way, with this column that is managed internally by Laravel, we can use the same pivot table to map different types such as users, videos or publications in our models in Laravel in a transparent way for us.

Another example of a relationship that can be polymorphic is that of documents/comments, which can be from a person, publication, user, among others, this type of relationship would be one to many of a polymorphic type.

We previously used a polymorphic relationship between users and authentication tokens using Sanctum.

Polymorphic relationships allow a record in a table to be related to multiple different models.

Polymorphic relationships in Laravel Eloquent are a powerful tool for handling situations where a record may be related to different entities. Instead of creating separate tables for each type of relationship, polymorphic relationships allow us to establish flexible connections between models. Here is an introduction with examples:

Unlike traditional relationships (such as 1 to n or n to n), where the relationship is always fixed, in polymorphic relationships, the relationship can vary depending on the record.

Although we begin by introducing the use of polymorphism relationships for many-to-many relationships, we can also use them in the rest of the relationships, but it is in the use of many-to-many relationships that is of main importance (and also the from one to many).

It is important to note that for the main relationship, the one with labels, the morphToMany() method is used to define the relationship, and for the "taggable" one, morphedByMany() is used, that is, the latter would be the one that has the polymorphic relationship.

Now, for all types of polymorphism relationships, the morph prefix is used to define them:

  • morphOne(MODEL, PIVOTTABLE): Defines a polymorphic one-to-one relationship. For example, a relationship for the profile can be used for different types such as users, people or companies, its equivalent to classic relationships is the hasOne() type.
  • morphMany(MODEL, PIVOTTABLE): Used to define a polymorphic one-to-many relationship. For example, a category relationship can be related to other models such as posts or videos, its equivalent to classic relationships is hasMany().
  • ManyToMany:
    • morphToMany(MODEL, PIVOTTABLE): This method is used to define a polymorphic many-to-many relationship. For example, the tags table that can be related to different types of models such as posts or videos.
    • morphedByMany(MODEL, PIVOTTABLE): This method is used on a model to establish a many-to-many relationship with other models using a polymorphic relationship, this relationship is set to "taggable".

The definition of these methods can be a bit abstract, so let's look at some examples.

Practical case: Many-to-many relationship

Let's discuss creating the migrations:

$ 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');
    }
}

Another migration for the pivot table:

$ 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');
    }
} 

Which is usually given the suffix of "able" as in the previous case.

And in the models:

// 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');
    }
}

In the post controller, we can make some modifications to assign tags to posts:

app\Http\Controllers\Dashboard\PostController.php

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);
        ***
    }
}

As for the view, it looks like:

resources\views\dashboard\post\_form.blade.php

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

With the previous code, we create a multiple selection list of all the tags, the tags that are assigned to the post are selected by default, also using the old() function as we did with the rest of the fields, the priority is taken by the user's selection and not by what we have in the array of post tags in the database.

We also create the CRUD process for the tags:

<?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');
    }
}

In the previous code, only the controller is shown, you must implement the rest of the code such as the requests, views, routes and associated permissions classes. If you have any questions, you can consult the source code at the end of the character.

Now, we will see some other examples that were taken from the official documentation and that you can take as a reference to know how to use the rest of the types of relationships available, if you were able to understand the many-to-many polymorphic relationship that we presented before, these will be many easier to understand.

Practical case: One-to-many relationship

In this example we have the comments model as the main model, as the "able" type models we have the videos and posts models, that is, the comments can be used by the posts and videos entity:

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

As for the models, they look like:

<?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');
    }
}

Practical case: One-to-one relationship

In this example we have images as the main model, as "able/selectable" type models we have users and posts, that is, these entities have an associated image

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

As for the models, they look like:

<?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');
    }
}

Conclusion

In summary, in the last two examples we see that we have to use the morphTo() method for the main relationship and for the "able" type the morphMany() and morphOne() methods are used respectively.

Finally, the morph() type methods to specify the relationships receive several parameters that can be useful if Laravel cannot correctly interpret the name of the table and relationship, for example:

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

As an additional consideration, as we mentioned previously, many-to-many and one-to-many relationships of the polymorphism type are the most interesting in this type of relationship since, by wanting to make a many-to-many relationship 'taggable/able' without being polymorphic, we would have to duplicate the pivot table for this purpose (and in one-to-many we could not create it to simulate a polymorphic table of the same type), but, with the use of polymorphic relationships we can use the same table; on the other hand, the use of polymorphic relationships for one-to-one relationships can be easily handled by a traditional relationship of the same type, that is, they are not polymorphic, for example, remember that for one-to-one relationships have:

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

We could have a relationship similar to the previous one where posts and users can have an image through:

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

More information about relationships at:

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.