Laravel'in Eloquent ORM'u, PHP'deki en etkileyici veritabanı soyutlama katmanlarından biridir. Ham SQL yazmadan karmaşık ilişkileri, eager loading'i ve dinamik kapsamları yönetebilirsin. Bu derste Eloquent'i performanslı ve temiz kullanmanın yollarını öğreneceksin.
Model Oluşturma ve Temel Yapı
// BASH //
# Model + migration + factory + seeder + controller
php artisan make:model Post -mfsc
# Sadece model
php artisan make:model Category// PHP //
// app/Models/Post.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use HasFactory, SoftDeletes;
// Atanabilir alanlar (mass assignment koruması)
protected $fillable = ['title', 'slug', 'body', 'user_id', 'category_id', 'published_at'];
// Otomatik tip dönüşümleri
protected $casts = [
'published_at' => 'datetime',
'is_featured' => 'boolean',
'meta' => 'array', // JSON → PHP array
];
// Her zaman yüklenecek ilişkiler
protected $with = ['author']; // N+1 önlemek için dikkatli kullan
// Özel tablo adı
protected $table = 'blog_posts'; // varsayılan: posts
}Migration: Şema Tasarımı
// PHP //
// database/migrations/xxxx_create_posts_table.php
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title', 200);
$table->string('slug', 220)->unique();
$table->text('body');
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
$table->json('meta')->nullable();
$table->boolean('is_featured')->default(false);
$table->timestamp('published_at')->nullable()->index();
$table->softDeletes(); // deleted_at sütunu
$table->timestamps();
// Composite index — birlikte sık sorgulananlar
$table->index(['user_id', 'published_at']);
});
}İlişki Tipleri
Bire-Çok (hasMany / belongsTo)
// PHP //
// app/Models/User.php
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
// Yazarın en son 5 yazısı
public function latestPosts(): HasMany
{
return $this->hasMany(Post::class)
->orderByDesc('published_at')
->limit(5);
}
// app/Models/Post.php
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}Çoğa-Çok (belongsToMany)
// PHP //
// Pivot tablo: post_tag (post_id, tag_id)
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class)
->withTimestamps() // pivot'ta timestamps
->withPivot('order'); // pivot'taki ekstra sütun
}
// Kullanım
$post->tags()->attach([1, 2, 3]);
$post->tags()->sync([1, 4]); // sadece 1 ve 4 kalsın
$post->tags()->detach(2);Uzaktan Bire-Çok (hasManyThrough)
// PHP //
// Kullanıcının sahip olduğu postların yorumları
// User → Post → Comment
public function allComments(): HasManyThrough
{
return $this->hasManyThrough(Comment::class, Post::class);
}Polimorfik İlişkiler
// PHP //
// Hem Post hem de Video'nun yorumu olabilir
// comments tablosu: commentable_id, commentable_type
// app/Models/Comment.php
public function commentable(): MorphTo
{
return $this->morphTo();
}
// app/Models/Post.php
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
// Kullanım
$post->comments()->create(['body' => 'Harika makale!', 'user_id' => 1]);Sorgu Builder ve Scope
// PHP //
// Global scope — model her sorgulandığında otomatik uygulanır
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->whereNotNull('published_at')
->where('published_at', '<=', now());
}
}
// Post.php — global scope kaydet
protected static function booted(): void
{
static::addGlobalScope(new PublishedScope);
}
// Local scope — isteğe bağlı kullanım
public function scopePopular(Builder $query, int $minViews = 1000): Builder
{
return $query->where('view_count', '>=', $minViews);
}
public function scopeByCategory(Builder $query, int|string $category): Builder
{
return $query->whereHas('category', fn($q) => $q->where('slug', $category));
}
// Kullanım
Post::popular(500)->byCategory('php')->latest()->paginate(10);
Post::withoutGlobalScope(PublishedScope::class)->get(); // draft'lar dahilEager Loading: N+1 Problemi
// PHP //
// N+1 PROBLEM — 1 sorgu + N ayrı sorgu (100 post = 101 sorgu)
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Her döngüde ayrı SQL
}
// ÇÖZÜM — Eager loading (2 sorgu)
$posts = Post::with('author')->get();
// Nested eager loading
$posts = Post::with(['author', 'category', 'tags'])->get();
// Koşullu eager loading
$posts = Post::with(['comments' => function ($q) {
$q->where('approved', true)->latest()->limit(5);
}])->get();
// withCount — ilişki sayısını yükle
$posts = Post::withCount(['comments', 'likes'])->get();
echo $post->comments_count;Eloquent Events ve Observer
// PHP //
// app/Observers/PostObserver.php
class PostObserver
{
public function creating(Post $post): void
{
$post->slug = Str::slug($post->title);
}
public function updated(Post $post): void
{
if ($post->isDirty('body')) {
Cache::forget("post:{$post->id}"); // önbelleği temizle
}
}
public function deleting(Post $post): void
{
$post->tags()->detach(); // hard delete öncesi temizlik
}
}
// AppServiceProvider'da kaydet
Post::observe(PostObserver::class);Factory ve Seeder
// PHP //
// database/factories/PostFactory.php
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence(6),
'slug' => fn(array $a) => Str::slug($a['title']),
'body' => fake()->paragraphs(5, true),
'user_id' => User::factory(), // ilişkili user da oluştur
'published_at' => fake()->dateTimeBetween('-1 year', 'now'),
];
}
// State — taslak post
public function draft(): static
{
return $this->state(['published_at' => null]);
}
// State — öne çıkarılmış
public function featured(): static
{
return $this->state(['is_featured' => true]);
}
}
// Kullanım
Post::factory()->count(50)->create();
Post::factory()->draft()->count(10)->create();
Post::factory()->featured()->for(User::find(1))->create();Performans İpuçları
// PHP //
// 1. Sadece ihtiyaç duyulan sütunları çek
Post::select(['id', 'title', 'slug', 'published_at'])->get();
// 2. chunk — büyük veriyi belleğe sığdır
Post::chunk(200, function ($posts) {
foreach ($posts as $post) {
// ...
}
});
// 3. lazy() — generator, bellek dostu
foreach (Post::lazy() as $post) {
// sadece bir satır bellekte
}
// 4. upsert — toplu güncelleme/ekleme
Post::upsert(
$records,
uniqueBy: ['slug'],
update: ['title', 'body', 'updated_at'],
);
// 5. whereHas vs join — sayı için withCount, filtreleme için whereHas
Post::whereHas('comments', fn($q) => $q->where('approved', true))->get();Özet
Eloquent'in gücü; ilişki tanımları (hasMany, belongsToMany, morphMany), eager loading (with, withCount) ve scope'lardır. N+1 problemi en yaygın performans tuzağıdır — with() ile kaçınılır. Observer'lar model event'larını temiz yakalar, Factory + Seeder test verisini güvenilir oluşturur.