How to implement a simple like system with Laravel
Tags: laravel, tutorial, php
Published: 2020-06-29 07:35:24

---
title: How to implement a simple like system with Laravel
published: true
description: We're going to leverage the power of Eloquent's morphs to make anything in our system likeable.
tags: ["Laravel","tutorial","php"]
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3lup5upld044dutbyt25.png
---

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](https://github.com/bdelespierre/laravel-like-demo), enjoy!

---

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

```PHP
// 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);
```

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

```PHP
class Post extends Model implements Likeable
{
    use Likes;
}
```

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](https://dev.to/connect/@bdelespierre). That being said, let's dive into it!

## 1. Migrations

Run

```bash
php artisan make:migration create_likes_table --create=likes
```

In `database/migrations/(date)_create_likes_table.php`

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

## 2. Model

Run

```bash
php artisan make:model Like
```

In `app/Models/Like.php`

```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();
    }
}
```

## 3. Contract (interface)

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

```PHP
namespace App\Contracts;

use Illuminate\Database\Eloquent\Relations\MorphMany;

interface Likeable
{
    public function likes(): MorphMany;
}
```

## 4. Concern (trait)

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

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

## 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`

```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;
}
```

## 6. User

In `app/Models/User.php` add those methods

```PHP
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();
    }
}
```

## 7. Controller

Run

```bash
php artisan make:controller LikeController
```

In `app/Http/Controllers/LikeController.php`

```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();
    }
}
```

## 8. Requests

Run

```bash
php artisan make:request LikeRequest
php artisan make:request UnlikeRequest
```

In `app/Http/Requests/LikeRequest.php`

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

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`

```php
namespace App\Http\Requests;

use App\Http\Requests\LikeRequest;

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

## 9. Gates

In `app/Providers/AuthServiceProvider.php`

```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();
        });
    }
}
```

## 10. Routes

In `routes/web.php`

```php
Route::middleware('auth')->group(function () {
    Route::post('like', 'LikeController@like')->name('like');
    Route::delete('like', 'LikeController@unlike')->name('unlike');
});
```

## 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`:

```html
{{ 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
```

In `resources/views/post.blade.php`:

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

---

**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:

- [Package Artisan commands in 5 minutes](https://dev.to/bdelespierre/package-artisan-commands-in-5-minutes-457j)
- [PHPUnit from scratch in 5 minutes](https://dev.to/bdelespierre/phpunit-from-scratch-in-5-minutes-48fa)
- [How to add basic localization to your HTML page with just jQuery](https://dev.to/bdelespierre/how-to-add-basic-localization-to-your-html-page-with-just-jquery-ncl)