The Ultimate Like System For Laravel Thanks To Many to Many Polymorphic Relationships

The Ultimate Like System For Laravel Thanks To Many to Many Polymorphic Relationships

Several months ago I wrote an article on How to create a Simple Like System For Laravel. In the comment of this article, a cool guy asked me about using a "Many to Many Polymorphic Relationship" for this "Like" logic.

I found the idea very appealing and I decided to create another article to write about this topic, and I hope to achieve the most perfect and simple Like System for any Laravel Application. The purpose is to make it so easy that you could actually include this system in each of your projects, in only a bunch of minutes.

Quick Intro

In the first article, we created a system that allows the users of your app to "like" and "unlike" a specific entity (or model) of your application (e.g. an article, a comment, a photo, whatever..). But if you want your users to be able to like several things, you have to create other "likes" tables. On Facebook, you can like a post, a comment, a picture, etc. So let's be efficient, and create a single table that will contain every likes of all your likeable entities.

Less database tables, more efficient and more "Laravel oriented".

We will obviously keep the "soft delete" feature from the previous article, so you'll be able to only notify your users once, at the creation of a new Like.

In the example I will setup in this article, I will have products and posts, that users can like.

Step 1: Database

We only need a single table to store de likes. Of course you need one table per likeable entities (posts, products, comments, etc - whatever you actually use in your application). And you also need a "users" table.

Now let's create a "likeables" table!

First, run this command php artisan make:migration create_likeables_table --create=likeables

In your new migration file, set the up() method like this:

 // migration file
public function up()
{
    Schema::create('likeables', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('user_id');
        $table->integer('likeable_id');
        $table->string('likeable_type');
        $table->softDeletes();
        $table->timestamps();
    });
}

You can now update your database by running the command php artisan migrate

Step 2: Models

First, create a Like model: php artisan make:model Like and write this inside your new Like.php file

// app/Like.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Like extends Model
{
    use SoftDeletes;

    protected $table = 'likeables';

    protected $fillable = [
        'user_id',
        'likeable_id',
        'likeable_type',
    ];

    /**
     * Get all of the products that are assigned this like.
     */
    public function products()
    {
        return $this->morphedByMany('App\Product', 'likeable');
    }

    /**
     * Get all of the posts that are assigned this like.
     */
    public function posts()
    {
        return $this->morphedByMany('App\Post', 'likeable');
    }
}

Quick analysis:

  1. We implements the SoftDeletes trait so we don't actually delete a record from the database when a user "unlike" something.
  2. We define the table that Eloquent needs to use. It actually won't work without that...
  3. As we will create new likes manually we need to define the $fillable variable so we don't encounter any MassAssignmentException.
  4. You will probably never use the products() and posts() method (I don't think you'll need to retrieve an object from a specific like). But they are here just in case you need them.

Second, you need to update the model of the entity your users should be able to like. For our example, I have a Post model and a Product model. As I'll write the exact same thing in both of them, I give you the Post.php file only:

// app/Post.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;

class Post extends Model
{
    public function likes()
    {
        return $this->morphToMany('App\User', 'likeable')->whereDeletedAt(null);
    }

    public function getIsLikedAttribute()
    {
        $like = $this->likes()->whereUserId(Auth::id())->first();
        return (!is_null($like)) ? true : false;
    }
}

And that's it! From your views, you can now grab all users that have liked a post. You can also check if the current user has liked the post or not.

We are now adding a nice method in our User.php model so you can get all the posts that a specific user has liked:


// app/User.php
public function likedPosts()
{
    return $this->morphedByMany('App\Post', 'likeable')->whereDeletedAt(null);
}

In order to see it all in action we still need to create a LikeController and define some routes. Here we go.

Step 3: Routes

You need one new route per likeable entity. In the scope of this example, I just need to add these two routes to the routes.php file.

// app/Http/routes.php

Route::get('product/like/{id}', ['as' => 'product.like', 'uses' => 'LikeController@likeProduct']);
Route::get('post/like/{id}', ['as' => 'post.like', 'uses' => 'LikeController@likePost']);

Step 4: LikeController

Go ahead and create the controller with this command php artisan make:controller LikeController

Here's what you need inside it.

// app/Http/Controllers/LikeController.php
<?php

namespace App\Http\Controllers;

use App\Like;
use Illuminate\Support\Facades\Auth;

class LikeController extends Controller
{
    public function likeProduct($id)
    {
        // here you can check if product exists or is valid or whatever

        $this->handleLike('App\Product', $id);
        return redirect()->back();
    }

    public function likePost($id)
    {
        // here you can check if product exists or is valid or whatever

        $this->handleLike('App\Post', $id);
        return redirect()->back();
    }

    public function handleLike($type, $id)
    {
        $existing_like = Like::withTrashed()->whereLikeableType($type)->whereLikeableId($id)->whereUserId(Auth::id())->first();

        if (is_null($existing_like)) {
            Like::create([
                'user_id'       => Auth::id(),
                'likeable_id'   => $id,
                'likeable_type' => $type,
            ]);
        } else {
            if (is_null($existing_like->deleted_at)) {
                $existing_like->delete();
            } else {
                $existing_like->restore();
            }
        }
    }
}

Some notes about this:

I know that the handleLike() method is not perfect. If you have ideas or suggestions, please share in the comments. As a wise man once said : "at least it works".

Important: These routes/controller combination is actually just an example. You can use POST methods if you prefer. You can use Ajax. You can do whatever you want. I just made it very simple to make it work without any JavaScript.

Finally, what can do in your views:

// someview.blade.php

@foreach ($products as $product)

    @foreach ($product->likes as $user)
        {{ $user->name }} likes this !
    @endforeach

...

    @if ($product->isLiked)
        <a href="{{ route('product.like', $product->id) }}">Unlike this shit</a>
    @else
        <a href="{{ route('product.like', $product->id) }}">Like this awesome product!</a>
    @endif
@endforeach

And voila ! I'm done !

I really hope you like all this ! (haha)

For those who might be interested here's a working example repo of what I setup in this article: https://github.com/mydnic/Laravel-Like-System-Example

If you have any questions or suggestions, please post a comment below.