SC header logo
Switch to Explorer mode

Simplifying Many-to-Many Relationships with Laravel Polymorphic Relations

Author

Bruno

CategoryDevelopment

In database architecture, many-to-many relationships often add complexity to our system. One way to tackle this is by using polymorphic relations, which Laravel gracefully handles. This blog post will delve into how you can use Laravel’s polymorphic relations to simplify many-to-many relationships.

We will use the scenario where a user can "like" an item in an online store. To achieve this, we typically need user, item, and pivot tables. This structure can prove challenging, especially when adding more likeable types to the system. Fortunately, with polymorphic relations, we can consolidate all these relationships into one table, simplifying the process of adding new likeable types to the system.

The following illustrations compare a non-polymorphic table structure with a polymorphic table structure:

Non-polymorphic table structure

many-to-many-polymorphic-example---many-to-many

Polymorphic table structure

many-to-many-polymorphic-example---polymorphic

The latter structure is simpler and more reusable. When we want to introduce a new likeable type to the system, we only need to create a table for the model and add internal relationships within Laravel. We can forgo the extra steps of creating multiple pivot tables.

In this blog post, we won't delve into the entire process of creating tables, models, migrations, etc. Instead, we'll focus on the key steps to make the transition to polymorphic relationships.

You can find a full working example on our GitHub repository.

Code Examples & Definitions

Before we proceed, it's worth mentioning that we have a working example of the Laravel setup on our GitHub. This provides a practical context to understand the code examples and definitions we'll discuss.

It's good practice to rename the morph relationship to something other than the model namespace - a simple string would suffice. In the boot method of the AppServiceProvider, add a morph map that specifies the class used. By doing this, we decouple the names from the application's internal structure.

Relation::enforceMorphMap([
    'book' => Book::class,
    'item' => Item::class,
    'sweet' => Sweet::class,
]);

You can read more about this in the Laravel documentation here.

Models

Sweet/Book/Item Model relationships

After creating the necessary models, we must define the polymorphic relationship within them. In this case, we're focusing on the likeables table. This definition must be added to all models that will be "likeable" by the user.

public function likes()
{
	// omit Model name with the one you are using
    return $this->morphToMany(User::class, 'likeable');
}

In our case, all models share the same relationship.

You can find the final Sweet, Book, Item models here:

User Model relationships

We also need to define additional relationships for all models that the user can like inside the User model.

// Sweets relation
public function likedSweets(): MorphToMany
{
    return $this->morphedByMany(Sweet::class, 'likeable');
}

// Items relation
public function likedItems(): MorphToMany
{
    return $this->morphedByMany(Item::class, 'likeable');
}

// Books relation
public function likedBooks(): MorphToMany
{
    return $this->morphedByMany(Book::class, 'likeable');
}

To fetch all items liked by the user, we add a hasMany relationship.

// All likes of this user
public function likes(): HasMany
{
    return $this->hasMany(Likeable::class);
}

The final User Model can be found at this link.

Likeable Model

The Likeable model should be created with a belongsTo relation to the user. This way, we can retrieve all items liked by a user when needed.

public function user(): BelongsTo
{
  return $this->belongsTo(User::class);
}

Controllers

Sweet/Book/ItemController Controllers

The like controllers will be identical, as their goal is to fetch the template we're using and count the items liked by the user.

Take the BookController as an example, and add the index method inside.

// BookController.php
public function index()
{
  // Just count number of likes here
  return view('books', [
      'books' => Book::withCount('likes')->get()
  ]);
}

You can apply the same logic to all other controllers for likeables - just replace the model name used.

You can find them here:

LikeController

The Like Controller is responsible for adding or removing likes from items. Create a store method that is triggered via a POST request.

Below is an example of a method responsible for adding the necessary relationships to a pivot table. This method finds the model passed from the request as the model type and tries to find that model_id. If we have defined all relationships between the models correctly, we can use the attach & detach method to add or remove relationships from the pivot table.

// LikeController.php
public function store(LikeableRequest $request): RedirectResponse
{
  $validated = $request->validated();

  $likeable = $validated['model_type']::findOrFail($validated['model_id']);

  $likeable->likes()->where('user_id', auth()->id())->exists() ?
      $likeable->likes()->detach(auth()->id()):
      $likeable->likes()->attach(auth()->id());

  return redirect()->back();
}

Views

Let's take a look at a short example of a form-action view. It passes the model ID and modelType to the controller.

// books.blade.php
<form action="{{ route('like', ['model_type' => \App\Models\Book::class, 'model_id' => $book->id]) }}" method="POST">
  @csrf
  <button type="submit" class="bg-gray-200">
      @if(auth()->user()->likes()->where('likeable_id', $book->id)->where('likeable_type', 'book')->first())
          liked
      @else
          like
      @endif
  </button>
</form>

You can find the full code at this link.

Seeders

Lastly, we have a simple seeder for creating random users and adding polymorphic relationships.

Check the Database\Seeders\UserSeeder here.

public function likeRandomly()
{
    $users = User::all();
    $sweets = Sweet::factory()->count(10)->create();
    $items = Item::factory()->count(10)->create();
    $books = Book::factory()->count(10)->create();

    foreach ($users as $user) {
        for ($i = 0; $i < random_int(1, 10); $i++) {
            $user->likedSweets()->attach($sweets->random());
            $user->likedItems()->attach($items->random());
            $user->likedBooks()->attach($books->random());
        }
    }
}

Conclusion

By leveraging Laravel's polymorphic relationships, you can simplify your many-to-many relationships. The likable example we explored today shows how polymorphic relations can streamline your code, making it cleaner and more manageable. Enjoy building and keep coding!

Articles You Might Like

https://cdn.simple-code.agency/case-studies/evolution/evolution-mockup.jpg
Bespoke software development: a practical guide for business owners
Development
April 23, 2025

Frustrated by generic tools? Learn how bespoke software solves real business problems and supports growth....

robert

Robert,

CEO

https://cdn.simple-code.agency/articles/storyblok-vs-wordpress.jpg
Storyblok vs WordPress: Which CMS is Best for Your Website?
Development
December 20, 2024

Discover the key differences between Storyblok and WordPress to choose the perfect CMS for your project....

matej

Matej,

Software Developer

https://cdn.simple-code.agency/articles/desktop-mobile-person.jpg
How Progressive Web Apps (PWAs) Are Transforming the Digital Experience
Development
October 02, 2024

Discover how Progressive Web Apps (PWAs) can improve user engagement and boost business growth....

robert

Robert,

CEO

https://cdn.simple-code.agency/articles/svelte-5.jpg
Svelte 5 - A magical revolution
Development
August 26, 2024

Explore how Svelte 5 revolutionizes web development with runes and enhanced reactivity for faster apps....

renato

Renato,

JavaScript Lead

https://cdn.simple-code.agency/articles/website-hacker.jpg
Why Websites Are Hacked and How to Protect Yours
Development
August 07, 2024

Learn why websites get hacked and how to protect yours with practical security measures and best practices....

robert

Robert,

CEO

https://cdn.simple-code.agency/articles/computer-code-editor.jpg
Popular Node.js Backend Frameworks in 2024
Development
July 24, 2024

Wondering what the top Node.js frameworks in 2024 are? Read on to discover the best options for your project!...

robert

Robert,

CEO

https://cdn.simple-code.agency/articles/frustrated-person.jpg
Speed Up Your Website in 10 Easy Steps
Development
July 16, 2024

Slow website costing you customers? Discover 10 practical ways to enhance your site's performance and keep visitors engaged....

robert

Robert,

CEO

Client CTA background image

A new project on the way? We’ve got you covered.