Laravel'de Polymorphic İlişkilerle Like Sistemi Yapmak
Bir post'a like atabiliyorsunuz. Güzel. Peki ya aynı like sistemini yorumlara da, fotoğraflara da, videolara da eklemek isterseniz? Her biri için ayrı post_likes, comment_likes, photo_likes tablosu mu oluşturacaksınız?
İşte polymorphic ilişkiler tam bu sorunu çözüyor. Tek bir likes tablosu, istediğiniz kadar model'e bağlanabiliyor. Nasıl yapıldığına bakalım.
Polymorphic Ne Demek?
Polymorphic ilişkide bir kayıt birden fazla model'e ait olabilir — ama tek bir tablo üzerinden. Laravel bunu iki sütunla çözer:
likeable_type→ Hangi model? (ör:App\Models\Post)likeable_id→ O model'in hangi kaydı? (ör:42)
Yani likes tablosundaki bir satır "Post #42'yi beğendi" veya "Comment #17'yi beğendi" diyebilir. Tablo aynı, hedef farklı.
Migration
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('likes', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->nullableMorphs('likeable');
$table->timestamps();
$table->unique(['user_id', 'likeable_type', 'likeable_id']);
});
}
public function down(): void
{
Schema::dropIfExists('likes');
}
};
nullableMorphs('likeable') iki sütun oluşturur: likeable_type ve likeable_id. İkisine index de ekler.
Son satırdaki unique constraint önemli — aynı kullanıcı aynı içeriği iki kez beğenemesin. Bunu veritabanı seviyesinde garanti altına alıyoruz, sadece PHP tarafında kontrol etmek yetmez (race condition olabilir).
Like Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Like extends Model
{
protected $fillable = ['user_id'];
public function likeable()
{
return $this->morphTo();
}
public function user()
{
return $this->belongsTo(User::class);
}
}
Likeable Trait — İşin Güzel Kısmı
Her model'e tek tek like metodları yazmak yerine bir Trait yazıyoruz. Hangi model use Likeable derse, like özelliği kazanıyor:
namespace App\Concerns;
use App\Models\Like;
use Illuminate\Database\Eloquent\Relations\MorphMany;
trait Likeable
{
protected static function bootLikeable(): void
{
// Model silinince like'ları da temizle
static::deleting(fn ($model) => $model->likes()->delete());
}
public function likes(): MorphMany
{
return $this->morphMany(Like::class, 'likeable');
}
public function like(): void
{
if (!$this->isLiked()) {
$this->likes()->create(['user_id' => auth()->id()]);
}
}
public function unlike(): void
{
$this->likes()->where('user_id', auth()->id())->delete();
}
public function toggleLike(): void
{
$this->isLiked() ? $this->unlike() : $this->like();
}
public function isLiked(): bool
{
return $this->likes()->where('user_id', auth()->id())->exists();
}
public function likesCount(): int
{
return $this->likes()->count();
}
}
Birkaç tercih yaptım burada:
dislikeyerineunlikededim — daha doğru anlam taşıyor. Dislike ayrı bir kavram (YouTube gibi).toggleLike()ekledim — frontend'de tek butonla beğen/beğenmekten vazgeç yapmak istediğinizde kullanışlı.bootLikeableile model silindiğinde orphan like kayıtları kalmasın diye otomatik temizlik yapıyoruz.
Model'lere Eklemek
Artık istediğiniz model'e tek satırla like özelliği ekleyebilirsiniz:
namespace App\Models;
use App\Concerns\Likeable;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use Likeable;
// ... diğer her şey
}
Yarın Comment model'ine de eklemek isterseniz:
class Comment extends Model
{
use Likeable;
}
Başka bir migration'a, controller'a, route'a gerek yok. Aynı likes tablosu, likeable_type sayesinde hangi model'e ait olduğunu biliyor.
Controller
namespace App\Http\Controllers;
use App\Models\Post;
class PostLikeController extends Controller
{
public function toggle(Post $post)
{
$post->toggleLike();
return back()->with('success',
$post->isLiked() ? 'Beğenildi!' : 'Beğeni kaldırıldı.'
);
}
}
Route:
Route::post('/posts/{post}/like', [PostLikeController::class, 'toggle'])
->middleware('auth')
->name('posts.like.toggle');
Tek endpoint, tek buton. Beğenmişse kaldırır, beğenmemişse ekler.
Blade Template
<form action="{{ route('posts.like.toggle', $post) }}" method="POST" class="inline">
@csrf
<button type="submit" class="flex items-center gap-1 text-sm {{ $post->isLiked() ? 'text-red-500' : 'text-gray-400' }}">
@if($post->isLiked())
<svg class="w-5 h-5 fill-current" viewBox="0 0 20 20"><path d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"/></svg>
@else
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 20 20"><path stroke-width="1.5" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"/></svg>
@endif
{{ $post->likesCount() }}
</button>
</form>
N+1 Sorununa Dikkat
Post listesinde her post için isLiked() ve likesCount() çağırıyorsanız N+1 query problemi yaşarsınız. Çözüm: eager loading ve count caching.
// Controller'da
$posts = Post::withCount('likes')
->with(['likes' => fn ($q) => $q->where('user_id', auth()->id())])
->latest()
->paginate(10);
Blade'de ise:
{{-- likes_count otomatik olarak gelir (withCount sayesinde) --}}
{{ $post->likes_count }}
{{-- isLiked kontrolü eager loaded koleksiyondan --}}
{{ $post->likes->isNotEmpty() ? 'Beğendin' : 'Beğen' }}
Bu şekilde 10 post için 2 query çalışır, 20 değil.
API İçin JSON Response
Eğer like sistemini SPA veya mobil uygulamadan kullanacaksanız:
public function toggle(Post $post)
{
$post->toggleLike();
return response()->json([
'liked' => $post->isLiked(),
'count' => $post->likesCount(),
]);
}
Frontend'de fetch ile çağırıp butonu JavaScript ile güncellersiniz. Sayfa yenilemeye gerek yok.
Başka Nerelerde Kullanılır?
Aynı Trait pattern'ini şunlar için de kullanabilirsiniz:
- Bookmark/Favoriler —
Bookmarkabletrait,bookmarkstablosu - Oylama —
Voteabletrait,votestablosu (up/down ek sütunu ile) - Takip etme —
Followabletrait,followstablosu
Polymorphic ilişki + Trait kombinasyonu, Laravel'de tekrar eden cross-cutting concern'leri çözmenin en temiz yolu.