How to implement a simple like system with Laravel
Benjamin Delespierre

Benjamin Delespierre @bdelespierre

About: I do all sorts of mischiefs with Laravel

Location:
Paris, France
Joined:
Sep 16, 2017

How to implement a simple like system with Laravel

Publish Date: Jun 29 '20
37 40

This tutorial has been updated for Laravel 8 (I also fixed a few issues).

A working implementation of this tutorial is also available on GitHub, enjoy!


In this (short) tutorial, I will show you how to add likes on any Laravel Eloquent Model.

// create something...
$something = new Post(['title' => "Laravel is AWESOME!"]);

// add a like to $something
$user->like($something);

// has $user has liked $something?
$user->hasLiked($something);

// how many likes does $something has?
$something->likes()->count();

// remove a like from $something
$user->unlike($something);
Enter fullscreen mode Exit fullscreen mode

We're going to implement this using traits and interfaces. Models "equipped" with the likeable capabilities will look like this:

class Post extends Model implements Likeable
{
    use Likes;
}
Enter fullscreen mode Exit fullscreen mode

I like to builds things from ground up, so we'll start with the database and we'll climb our way up to the browser. En route, we'll use many components provided by Laravel 8, which makes this tutorial a great exercise for beginners.

This tutorial covers:

  • Migrations
  • Models
  • Contracts (interfaces)
  • Concerns (traits)
  • Controllers
  • Requests
  • Gates
  • Routes
  • Views

I won't bother you with details on how everything works, who got time for that? I believe experience is the only teacher. If you need me to explain something specific, let me know in the comments or come chat with me. That being said, let's dive into it!

1. Migrations

Run

php artisan make:migration create_likes_table --create=likes
Enter fullscreen mode Exit fullscreen mode

In database/migrations/(date)_create_likes_table.php

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

class CreateLikesTable extends Migration
{
    public function up()
    {
        Schema::create('likes', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->morphs('likeable');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('likes');
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Model

Run

php artisan make:model Like
Enter fullscreen mode Exit fullscreen mode

In app/Models/Like.php

namespace App\Models;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Like extends Model
{
    use HasFactory;

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

    public function likeable()
    {
        return $this->morphTo();
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Contract (interface)

In app/Contrats/Likeable.php (you need to create the app/Contracts folder)

namespace App\Contracts;

use Illuminate\Database\Eloquent\Relations\MorphMany;

interface Likeable
{
    public function likes(): MorphMany;
}
Enter fullscreen mode Exit fullscreen mode

4. Concern (trait)

In app/Models/Concerns/Likes.php (you need to create the app/Models/Concerns folder)

namespace App\Models\Concerns;

use App\Models\Like;
use Illuminate\Database\Eloquent\Relations\MorphMany;

trait Likes
{
    public function likes(): MorphMany
    {
        return $this->morphMany(Like::class, 'likeable');
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Likeable models

For the sake of the example, I'm going to use a post model generated using php artisan make:model Post. The logic is exactly the same regardless of the models you're using.

In app/Models/Post.php

namespace App\Models;

use App\Contracts\Likeable;
use App\Models\Concerns\Likes;
use Illuminate\Database\Eloquent\Model;

class Post extends Model implements Likeable
{
    use Likes;
}
Enter fullscreen mode Exit fullscreen mode

6. User

In app/Models/User.php add those methods

namespace App\Models;

use App\Contracts\Likeable;
use App\Models\Like;
// other namespaces...

class User extends Authenticatable
{
    // other user stuff...

    public function likes()
    {
        return $this->hasMany(Like::class);
    }

    public function like(Likeable $likeable): self
    {
        if ($this->hasLiked($likeable)) {
            return $this;
        }

        (new Like())
            ->user()->associate($this)
            ->likeable()->associate($likeable)
            ->save();

        return $this;
    }

    public function unlike(Likeable $likeable): self
    {
        if (! $this->hasLiked($likeable)) {
            return $this;
        }

        $likeable->likes()
            ->whereHas('user', fn($q) => $q->whereId($this->id))
            ->delete();

        return $this;
    }

    public function hasLiked(Likeable $likeable): bool
    {
        if (! $likeable->exists) {
            return false;
        }

        return $likeable->likes()
            ->whereHas('user', fn($q) =>  $q->whereId($this->id))
            ->exists();
    }
}
Enter fullscreen mode Exit fullscreen mode

7. Controller

Run

php artisan make:controller LikeController
Enter fullscreen mode Exit fullscreen mode

In app/Http/Controllers/LikeController.php

namespace App\Http\Controllers;

use App\Http\Requests\LikeRequest;
use App\Http\Requests\UnlikeRequest;

class LikeController extends Controller
{
    public function like(LikeRequest $request)
    {
        $request->user()->like($request->likeable());

        if ($request->ajax()) {
            return response()->json([
                'likes' => $request->likeable()->likes()->count(),
            ]);
        }

        return redirect()->back();
    }

    public function unlike(UnlikeRequest $like)
    {
        $request->user()->unlike($request->likeable());

        if ($request->ajax()) {
            return response()->json([
                'likes' => $request->likeable()->likes()->count(),
            ]);
        }

        return redirect()->back();
    }
}
Enter fullscreen mode Exit fullscreen mode

8. Requests

Run

php artisan make:request LikeRequest
php artisan make:request UnlikeRequest
Enter fullscreen mode Exit fullscreen mode

In app/Http/Requests/LikeRequest.php

namespace App\Http\Requests;

use App\Contracts\Likeable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Http\FormRequest;

class LikeRequest extends FormRequest
{
    public function authorize()
    {
        return $this->user()->can('like', $this->likeable());
    }

    public function rules()
    {
        return [
            // the class of the liked object
            'likeable_type' => [
                "bail",
                "required",
                "string",
                function ($attribute, $value, $fail) {
                    if (! class_exists($value, true)) {
                        $fail($value . " is not an existing class");
                    }

                    if (! in_array(Model::class, class_parents($value))) {
                        $fail($value . " is not Illuminate\Database\Eloquent\Model");
                    }

                    if (! in_array(Likeable::class, class_implements($value))) {
                        $fail($value . " is not App\Contracts\Likeable");
                    }
                },
            ],

            // the id of the liked object
            'id' => [
                "required",
                function ($attribute, $value, $fail) {
                    $class = $this->input('likeable_type');

                    if (! $class::where('id', $value)->exists()) {
                        $fail($value . " does not exists in database");
                    }
                },
            ],
        ];
    }

    public function likeable(): Likeable
    {
        $class = $this->input('likeable_type');

        return $class::findOrFail($this->input('id'));
    }
}
Enter fullscreen mode Exit fullscreen mode

Fortunately for us, the unlike request is very easy since it's basically the same request with a minor difference in the authorize method.

In app/Http/Requests/UnlikeRequest.php

namespace App\Http\Requests;

use App\Http\Requests\LikeRequest;

class UnlikeRequest extends LikeRequest
{
    public function authorize()
    {
        return $this->user()->can('unlike', $this->likeable());
    }
}
Enter fullscreen mode Exit fullscreen mode

9. Gates

In app/Providers/AuthServiceProvider.php

namespace App\Providers;

use App\Contracts\Likeable;
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        // your policies...
    ];

    public function boot()
    {
        $this->registerPolicies();

        // $user->can('like', $post)
        Gate::define('like', function (User $user, Likeable $likeable) {
            if (! $likeable->exists) {
                return Response::deny("Cannot like an object that doesn't exists");
            }

            if ($user->hasLiked($likeable)) {
                return Response::deny("Cannot like the same thing twice");
            }

            return Response::allow();
        });

        // $user->can('unlike', $post)
        Gate::define('unlike', function (User $user, Likeable $likeable) {
            if (! $likeable->exists) {
                return Response::deny("Cannot unlike an object that doesn't exists");
            }

            if (! $user->hasLiked($likeable)) {
                return Response::deny("Cannot unlike without liking first");
            }

            return Response::allow();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

10. Routes

In routes/web.php

Route::middleware('auth')->group(function () {
    Route::post('like', 'LikeController@like')->name('like');
    Route::delete('like', 'LikeController@unlike')->name('unlike');
});
Enter fullscreen mode Exit fullscreen mode

11. Views (Blade)

For the sake of the example, I'm going to use a $post variable representing an App\Models\Post model (as described above). You're obviously free to use whatever model you like, as long as they implement the App\Contracts\Likeable interface and possess the App\Models\Concerns\Likes trait.

In resources/views/like.blade.php:

{{ trans_choice('{0} no like|{1} :count like|[2,*] :count likes', count($model->likes), ['count' => count($model->likes)]) }}

@can('like', $model)
    <form action="{{ route('like') }}" method="POST">
        @csrf
        <input type="hidden" name="likeable_type" value="{{ get_class($model) }}"/>
        <input type="hidden" name="id" value="{{ $model->id }}"/>
        <button>@lang('Like')</button>
    </form>
@endcan

@can('unlike', $model)
    <form action="{{ route('unlike') }}" method="POST">
        @csrf
        @method('DELETE')
        <input type="hidden" name="likeable_type" value="{{ get_class($model) }}"/>
        <input type="hidden" name="id" value="{{ $model->id }}"/>
        <button>@lang('Unlike')</button>
    </form>
@endcan
Enter fullscreen mode Exit fullscreen mode

In resources/views/post.blade.php:

<div class="post">
    <h3>{{ $post->title }}</h3>
    <p>{{ $post->body }}</p>
    @include('like', ['model' => $post])
</div>
Enter fullscreen mode Exit fullscreen mode

Congratulations! You have now finished this tutorial and have implemented a basic, yet robust like system!

Tell me what you think! Especially if you faced difficulties understanding and/or implementing. I always do my best to respond to comments and help my readers.


From the same author:

Comments 40 total

  • Winner Onaolapo
    Winner OnaolapoJul 20, 2020

    Thanks for this tutorial sir. Just curious is there a way to do this without having the user to sign in before liking a post? Thank you, sir

    • Benjamin Delespierre
      Benjamin DelespierreJul 20, 2020

      Well, you could tweak the design a bit by making the user_id nullable in order to create userless likes.

  • Theo Harkenbusch
    Theo HarkenbuschAug 1, 2020

    Hey, thanks for the tutorial! However, I think there is a slight mistake in the Likes.php class.

    It should be
    return $likeable->likes()->whereHas('user', function(Builder $query) {
    return $query->whereId($this->id);
    })->exists();

    rather than
    return $likeable->likes()->whereHas('user', function($query) {
    return $query->whereId($this->id)->exists();
    });

    in Laravel 7.

    • mateator
      mateatorAug 2, 2020

      Ty man, i have been stuck for weeks because of that, it wasnt working until you wrote to took out the ->exists();

      same thing to delete, the result is:
      $likeable->likes()->whereHas('user', function(Builder $query) {
      $query->whereId($this->id);
      })->delete();

      Also, if someone implements this, dont forget to add this in Likes.php
      use Illuminate\Database\Eloquent\Builder;

    • Benjamin Delespierre
      Benjamin DelespierreAug 2, 2020

      My mistake. I'll change it, thanks for pointing it out

    • stleroux
      stlerouxJul 10, 2021

      For some reason I get an error message when I try to post my own comment

  • Deyvidbh
    DeyvidbhAug 6, 2020

    Thank you very much for the tutorial! I have some doubts regarding how to implement this on my blade, I am a beginner. could you give me a tip? what do I put on my like button?

    • Benjamin Delespierre
      Benjamin DelespierreAug 6, 2020

      assuming you've done everything in the tutorial right you should have a route "/like"

      You can check it with php artisan route:list --name like

      The button should look like this:

      @if(!auth()->user()->hasLiked($post))
          <form action="/like" method="post">
              @csrf
              <input type="hidden" name="likeable" value="{{ get_class($post) }}">
              <input type="hidden" name="id" value="{{ $post->id }}">
              <button type="submit" class="btn btn-primary">
                  Like
              </button>
          </form>
      @else
          <button class="btn btn-secondary" disabled>
              {{ $post->likes()->count() }} likes
          </button>
      @endif
      
      Enter fullscreen mode Exit fullscreen mode

      I believe you can figure out how to make the unlike button by yourself with the resources provided in the tutorial ;-)

      • MD3108
        MD3108May 25, 2021

        Could u show how unlike works because I'm just managing to like but not dislike... #alsoBeginner

        • MD3108
          MD3108May 26, 2021

          I get this error when I try my dislikeSQLSTATE[22007]: Invalid datetime format: 1292 Truncated incorrect INTEGER value: 'like' (SQL: delete fromnoteswhereid= like

          • Benjamin Delespierre
            Benjamin DelespierreMay 27, 2021

            Sure, I think I'll update the article to add the views, stay tuned!

            • Benjamin Delespierre
              Benjamin DelespierreMay 27, 2021

              Aaaaand done. check it out!

              • MD3108
                MD3108May 27, 2021

                ohh nice, couldn't have hoped for better. I'll leave a comment on how it went once I get to it again!

                • Benjamin Delespierre
                  Benjamin DelespierreMay 27, 2021

                  most welcome my friend. You can also check this out, it's a working implementation of the like system github.com/bdelespierre/laravel-li...

                  • MD3108
                    MD3108May 29, 2021

                    I managed to get as far as last time same error presists...
                    SQLSTATE[22007]: Invalid datetime format: 1292 Truncated incorrect INTEGER value: 'like' (SQL: delete fromnoteswhereid= like)`

                    however I think in 'app/Models/Concerns/Likes.php' the 'use App\Like' should be 'use App\Models\Like'. For the rest I got a flaw less exp. just don't know how to solve that issue there.

  • devkitshow
    devkitshowOct 23, 2020

    Hello! There's also a problem in LikesRequest

    'id' => [
                    "required",
                    function ($attribute, $value, $fail) {
                        $class = $this->getClass($value);
    
                        if (! $class::where('id', $value)->exists()) {
                            $fail($value." is not present in database");
                        }
                    },
                ],
    
    Enter fullscreen mode Exit fullscreen mode

    this would give you an error due to getClass($value) is trying to get class of numeric id.
    So it should probably be like

    $class = $this->getClass($this->input('likeable'));
    
    Enter fullscreen mode Exit fullscreen mode
    • Benjamin Delespierre
      Benjamin DelespierreMay 27, 2021

      Thanks for pointing this out. I'm fixing the article as we speak to take your feedback into account. Stay tuned!

  • webfuelcode
    webfuelcodeMar 2, 2021

    I followed the steps, running the localhost says "trait app\concerns\likeable" not found... Tried a lot to figure out the reason but failed. It may be a typo or the new version is the reason. Can you tell me...

    • Benjamin Delespierre
      Benjamin DelespierreMar 10, 2021

      It's very unlikely a version issue (if you're running PHP7.2+ of course).

      If you see the 'not found' error it means a namespace import is missing.

  • Silvere
    SilvereMay 28, 2021

    Nice work that will serve as a reference for the design of features on Laravel 8
    Thank you 👍

  • stleroux
    stlerouxJun 12, 2021

    Hi,
    Love the code. Straight forward and works like a charm.
    Is there a way to eager load the likes data on index pages?
    I tried adding "likes" to the with query but it doesn't seem to reduce the amount of queries being performed.

    Thanks

    • Benjamin Delespierre
      Benjamin DelespierreJun 15, 2021

      Can you show us what you tried?

    • Benjamin Delespierre
      Benjamin DelespierreJun 15, 2021

      I have just tried

      $posts = Post::latest()->with('likes')->take(5)->get();
      
      Enter fullscreen mode Exit fullscreen mode

      Using the demo project and Laravel Debug bar.

      From 8 queries (with the N+1) problem you mention, I now have only 4 👍

      See before and after

      Hint: be sure to place the ->with('...') part BEFORE ->get() or ->all().

      • stleroux
        stlerouxJun 16, 2021

        Actually, I was using the code in the index page and doing a @can ('like') and @can ('unlike') which was querying the DB twice per record. I changed the @can ('unlike') for @else and now the duplicate queries are gone.
        Thanks

        • Benjamin Delespierre
          Benjamin DelespierreJun 16, 2021

          Yes, that's because the gates in AuthServiceProvider are using User::hasLike :

              public function hasLiked(Likeable $likeable): bool
              {
                  if (! $likeable->exists) {
                      return false;
                  }
          
                  return $likeable->likes()
                      ->whereHas('user', fn($q) =>  $q->whereId($this->id))
                      ->exists();
              }
          
          Enter fullscreen mode Exit fullscreen mode

          You may change it to use a cache, like this (actual working solution) :

              public function hasLiked(Likeable $likeable): bool
              {
                  if (! $likeable->exists) {
                      return false;
                  }
          
                  return $this->likes->contains(
                      fn($like) => $like->likeable_type == get_class($likeable) && $like->likeable_id == $likeable->id
                  );
              }
          
          Enter fullscreen mode Exit fullscreen mode

          It should minimize the number of queries. It's ugly but it works!

          Result before with 15 queries (one per @can('like')) and after only 6!

  • stleroux
    stlerouxJul 9, 2021

    Hello,
    I have run into a weird issue.
    Wen I run the code on the localhost where I do my development, everything works fine.
    This is a Windows 10 machine running PHP 7.4.15

    However, when I transfer my files to my Ubuntu 18.04 server, I get the following error message on all pages :
    ->> syntax error, unexpected '=>' (T_DOUBLE_ARROW), expecting '{' <<-
    And it is pointing to

    
          $likeable->likes()
            ->whereHas('user', function($q) => $q->whereId($this->id))
            ->delete();
    
    
    Enter fullscreen mode Exit fullscreen mode

    in my User model.
    I am running PHP 7.4.21 on the server.
    If I comment out the lines, the pages load properly.
    Hopefully, this is enough info to TS, if not, please let me know what else is needed.
    Thanks

    • Benjamin Delespierre
      Benjamin DelespierreJul 10, 2021

      In the code snippet you provided, there's a typo on the arrow function, it should be:

            $likeable->likes()
              ->whereHas('user', fn($q) => $q->whereId($this->id))
              ->delete();
      
      Enter fullscreen mode Exit fullscreen mode
  • FatahIdzhar
    FatahIdzharSep 10, 2021

    i get proble on
    Undefined property: stdClass::$likes

    flareapp.io/share/q5YLj4j7#F62

    • Benjamin Delespierre
      Benjamin DelespierreSep 10, 2021

      Your $model variable shouldn't be an stdClass instance. It should be a Post instance. (or anything with the Likes trait).

      I suggest you dump this variable with dd($model) in the controller before returning the view and try to understand what's going on.

      Good luck!

      • FatahIdzhar
        FatahIdzharSep 10, 2021

        I've tried it, it's still an error in the
        Undefined property: stdClass::$likes

        and already using dd()

  • FatahIdzhar
    FatahIdzharSep 10, 2021

    i get proble on stdClass::$Likes

    More details are here :
    flareapp.io/share/q5YLj4j7#F62

    • zhandos998
      zhandos998Nov 11, 2021

      using Post::all();
      no DB::table('post')->get();

  • Benjamin
    BenjaminMar 15, 2022

    Hello @bdelespierre

    That's really brillant, thanks a lot for sharing!

    I noticed an error in app/Http/Controllers/LikeController.php

    Change
    public function unlike(UnlikeRequest $like)
    with
    public function unlike(UnlikeRequest $request) ;-)

    Thank you again,

  • Jayrajsinh Solanki
    Jayrajsinh SolankiMar 21, 2022

    Thanks for the tutorial...

  • atr0x23
    atr0x23Nov 12, 2022

    Hi, I am new to Laravel. I followed your orders here but when i am trying to run and see a view (blade) with the posts from db I am getting this error:

    Trait 'App\Concerns\Likes' not found

    What am I missing?
    Thanks in advance.

  • Ozai.hft
    Ozai.hftNov 19, 2022

    How the user look what was his liked ? @bdelespierre

Add comment