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);
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;
}
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
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');
}
}
2. Model
Run
php artisan make:model Like
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();
}
}
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;
}
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');
}
}
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;
}
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();
}
}
7. Controller
Run
php artisan make:controller LikeController
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();
}
}
8. Requests
Run
php artisan make:request LikeRequest
php artisan make:request UnlikeRequest
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'));
}
}
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());
}
}
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();
});
}
}
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');
});
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
In resources/views/post.blade.php
:
<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:
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