SC header logo

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

Client CTA background image

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