Eloquent ORM: Laravel için desenler ve optimizasyonlar

Eloquent ORM'i ileri düzey desenler ve optimizasyon teknikleriyle ustalaşın. Laravel uygulamaları için eager loading, query scope, accessor, mutator ve performans.

Laravel için Eloquent ORM desenleri ve optimizasyonlar

Eloquent ORM, veritabanı etkileşimlerini akıcı ve ifade gücü yüksek işlemlere dönüştürür. Şık söz diziminin ötesinde, ileri düzey desenleri ve optimizasyon tekniklerini ustalaşmak Laravel uygulamalarının üretimdeki performansını belirler.

Önce performans

N+1 problemi Eloquent uygulamalarındaki yavaşlığın başlıca nedenidir. Optimize edilmemiş her ilişki, kayıt başına ek bir SQL sorgusu üretir.

N+1 problemini eager loading ile çözmek

N+1 problemi, bir koleksiyon üzerinde her yineleme ilişkileri yüklemek için ek bir sorgu tetiklediğinde ortaya çıkar. 100 makale ve yazarlarıyla bu, tek optimize edilmiş sorgu yerine 101 sorgu demektir.

Eager loading, tüm ilişkileri en fazla bir veya iki sorguda alır ve yanıt süresini önemli ölçüde azaltır.

app/Http/Controllers/ArticleController.phpphp
// Demonstration of N+1 problem and its solution

namespace App\Http\Controllers;

use App\Models\Article;
use Illuminate\Http\Request;

class ArticleController extends Controller
{
    // ❌ N+1 problem: 1 articles query + N author queries
    public function indexWithProblem()
    {
        $articles = Article::all(); // 1 query

        foreach ($articles as $article) {
            echo $article->author->name; // N additional queries
        }
    }

    // ✅ Eager loading: 2 queries maximum
    public function indexOptimized()
    {
        $articles = Article::with('author')->get(); // 2 queries total

        foreach ($articles as $article) {
            echo $article->author->name; // No additional queries
        }
    }

    // ✅ Nested eager loading for multiple relationships
    public function indexWithNestedRelations()
    {
        // Loads articles → authors → profiles + articles → comments → users
        $articles = Article::with([
            'author.profile',
            'comments.user'
        ])->get();

        return view('articles.index', compact('articles'));
    }
}

with() ile eager loading, ihtiyaçları öngörerek verileri önceden yükler. Performans farkı büyük koleksiyonlarda olağanüstü hale gelir.

Koşullu ve kısıtlı eager loading

Geniş ilişkiler bazen kısmi yükleme gerektirir. Eager loading üzerindeki kısıtlar, alınan verileri sınırlayarak N+1'den kaçınır.

app/Http/Controllers/UserController.phpphp
// Eager loading with constraints to optimize queries

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;

class UserController extends Controller
{
    public function showWithRecentOrders(int $id)
    {
        // Load only the 5 most recent paid orders
        $user = User::with(['orders' => function (Builder $query) {
            $query->where('status', 'paid')
                  ->orderByDesc('created_at')
                  ->limit(5);
        }])->findOrFail($id);

        return view('users.show', compact('user'));
    }

    public function indexActiveWithStats()
    {
        // Conditional eager loading with withCount
        $users = User::query()
            ->where('active', true)
            ->with(['profile', 'subscription'])
            ->withCount(['orders', 'reviews']) // Adds orders_count and reviews_count
            ->withSum('orders', 'total')       // Adds orders_sum_total
            ->get();

        return view('users.index', compact('users'));
    }

    public function showWithConditionalRelation(int $id)
    {
        // Load relationship only if user is premium
        $user = User::findOrFail($id);

        $user->loadMissing(
            $user->isPremium() ? ['premiumFeatures', 'analytics'] : []
        );

        return view('users.show', compact('user'));
    }
}

withCount() ve withSum() yöntemleri, tam koleksiyonları yüklemeden toplama ekler ve dashboard istatistikleri için idealdir.

Tekrar kullanılabilir sorgular için query scope

Query scope'lar filtreleme mantığını model içinde kapsüller. Yerel scope'lar esneklik sağlarken global scope'lar otomatik olarak tüm sorgulara uygulanır.

Adlandırma kuralı

Yerel scope'lar modelde scope önekini kullanır ancak onsuz çağrılır: scopeActive() çağırırken User::active() olur.

app/Models/Article.phpphp
// Local and global scopes to encapsulate business logic

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([PublishedScope::class])] // Global scope via PHP 8 attribute
class Article extends Model
{
    // Simple local scope: Article::published()
    public function scopePublished(Builder $query): Builder
    {
        return $query->whereNotNull('published_at')
                     ->where('published_at', '<=', now());
    }

    // Local scope with parameter: Article::byCategory('tech')
    public function scopeByCategory(Builder $query, string $category): Builder
    {
        return $query->where('category', $category);
    }

    // Local scope with optional parameter
    public function scopePopular(Builder $query, int $minViews = 1000): Builder
    {
        return $query->where('views_count', '>=', $minViews)
                     ->orderByDesc('views_count');
    }

    // Dynamic scope for flexible search
    public function scopeSearch(Builder $query, ?string $term): Builder
    {
        if (empty($term)) {
            return $query;
        }

        return $query->where(function (Builder $q) use ($term) {
            $q->where('title', 'like', "%{$term}%")
              ->orWhere('content', 'like', "%{$term}%")
              ->orWhereHas('tags', fn($t) => $t->where('name', 'like', "%{$term}%"));
        });
    }
}
app/Models/Scopes/PublishedScope.phpphp
// Reusable global scope across models

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class PublishedScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        // Automatically applied to all Article queries
        $builder->whereNotNull('published_at')
                ->where('published_at', '<=', now());
    }
}
php
// Using scopes in a controller
$articles = Article::query()
    ->byCategory('technology')
    ->popular(500)
    ->search($request->input('q'))
    ->with('author')
    ->paginate(20);

// Disable a global scope temporarily
$allArticles = Article::withoutGlobalScope(PublishedScope::class)->get();

Scope'ların zincirlenmesi, iş mantığını modelde merkezileştirerek okunabilir ve sürdürülebilir sorgular üretir.

Laravel mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Attribute ile accessor ve mutator

Laravel 9+, accessor ve mutator için Attribute sınıfı aracılığıyla birleşik bir söz dizimi sunar. Bu modern yaklaşım get*Attribute ve set*Attribute yöntemlerinin yerini alır.

app/Models/User.phpphp
// Modern accessors and mutators with the Attribute class

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class User extends Model
{
    protected $fillable = ['first_name', 'last_name', 'email', 'password'];

    // Accessor: generates a virtual attribute from other fields
    protected function fullName(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->first_name} {$this->last_name}",
        );
    }

    // Mutator: transforms value before saving
    protected function password(): Attribute
    {
        return Attribute::make(
            set: fn (string $value) => Hash::make($value),
        );
    }

    // Combined accessor + mutator
    protected function email(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => Str::lower($value),
            set: fn (string $value) => Str::lower(trim($value)),
        );
    }

    // Cached accessor to avoid recalculations
    protected function initials(): Attribute
    {
        return Attribute::make(
            get: fn () => Str::upper(
                Str::substr($this->first_name, 0, 1) .
                Str::substr($this->last_name, 0, 1)
            ),
        )->shouldCache(); // Caches result during request
    }

    // Computed attribute based on a relationship
    protected function ordersTotal(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->orders->sum('total'),
        );
    }
}
php
// Transparent usage of accessors and mutators
$user = new User();
$user->first_name = 'John';
$user->last_name = 'Doe';
$user->email = '  JOHN@EXAMPLE.COM  '; // Automatically normalized
$user->password = 'secret123';          // Automatically hashed
$user->save();

echo $user->full_name;  // "John Doe"
echo $user->initials;   // "JD"
echo $user->email;      // "john@example.com"

shouldCache() yöntemi, aynı modeldeki birden fazla yeniden hesaplamayı önleyerek pahalı accessor'ları optimize eder.

Karmaşık tipler için özel cast'ler

Cast'ler, değerleri PHP ile veritabanı arasında otomatik olarak dönüştürür. Özel cast'ler, iş tipleri için serileştirme mantığını kapsüller.

app/Casts/MoneyCast.phpphp
// Custom cast for handling monetary amounts

namespace App\Casts;

use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;

class MoneyCast implements CastsAttributes
{
    public function __construct(
        protected string $currency = 'USD'
    ) {}

    // DB → PHP conversion: cents to Money object
    public function get(Model $model, string $key, mixed $value, array $attributes): ?Money
    {
        if ($value === null) {
            return null;
        }

        return new Money(
            amount: (int) $value,
            currency: $this->currency
        );
    }

    // PHP → DB conversion: Money object to cents
    public function set(Model $model, string $key, mixed $value, array $attributes): ?int
    {
        if ($value === null) {
            return null;
        }

        if ($value instanceof Money) {
            return $value->getAmountInCents();
        }

        if (is_numeric($value)) {
            return (int) ($value * 100);
        }

        throw new InvalidArgumentException('Value must be Money instance or numeric');
    }
}
app/ValueObjects/Money.phpphp
// Immutable Value Object representing amounts

namespace App\ValueObjects;

use JsonSerializable;

final readonly class Money implements JsonSerializable
{
    public function __construct(
        private int $amount,     // Stored in cents
        private string $currency
    ) {}

    public function getAmountInCents(): int
    {
        return $this->amount;
    }

    public function getAmountInUnits(): float
    {
        return $this->amount / 100;
    }

    public function format(): string
    {
        return number_format($this->getAmountInUnits(), 2) . ' ' . $this->currency;
    }

    public function add(Money $other): self
    {
        return new self($this->amount + $other->amount, $this->currency);
    }

    public function jsonSerialize(): array
    {
        return [
            'amount' => $this->getAmountInUnits(),
            'currency' => $this->currency,
        ];
    }
}
app/Models/Order.phpphp
// Using the custom cast

namespace App\Models;

use App\Casts\MoneyCast;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected function casts(): array
    {
        return [
            'total' => MoneyCast::class,           // USD by default
            'shipping_cost' => MoneyCast::class . ':USD',
            'tax_amount' => MoneyCast::class . ':USD',
            'paid_at' => 'datetime',
            'metadata' => 'array',
        ];
    }
}
php
// Natural usage with the cast
$order = Order::find(1);
echo $order->total->format();              // "149.99 USD"
echo $order->total->getAmountInCents();    // 14999

$order->total = 199.99;  // Automatically converted
$order->save();          // Stored as 19999 in DB

Value Object'ler özel cast'lerle birleştirildiğinde, zarif bir API'yi koruyarak iş verilerinin bütünlüğünü garanti altına alır.

Büyük ölçekli sorguları optimize etmek

Milyonlarca kayıt üzerindeki işlemler bellek tükenmesini önlemek için belirli teknikler gerektirir. Chunking ve cursor'lar verileri partiler halinde işler.

Bellek dikkati

Model::all() tüm kayıtları belleğe yükler. 100.000 satırlık bir tabloda bu birkaç gigabayt RAM tüketebilir ve uygulamayı çökertebilir.

app/Console/Commands/ProcessUsersCommand.phpphp
// Batch processing techniques for large tables

namespace App\Console\Commands;

use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class ProcessUsersCommand extends Command
{
    protected $signature = 'users:process';

    public function handle(): int
    {
        // ✅ Chunk: processes in batches of 1000, reloads from DB
        User::query()
            ->where('needs_processing', true)
            ->chunk(1000, function ($users) {
                foreach ($users as $user) {
                    $user->processAccount();
                }
            });

        // ✅ Chunk with updates: avoids infinite loop during modifications
        User::query()
            ->where('status', 'pending')
            ->chunkById(1000, function ($users) {
                foreach ($users as $user) {
                    $user->update(['status' => 'processed']);
                }
            });

        // ✅ Lazy collection: single record in memory at a time
        foreach (User::lazy(1000) as $user) {
            $user->sendNewsletter();
        }

        // ✅ Cursor: for read-only operations, minimal memory
        foreach (User::cursor() as $user) {
            $this->info("Processing: {$user->email}");
        }

        // ✅ Mass update without Eloquent: maximum performance
        User::query()
            ->where('last_login_at', '<', now()->subYear())
            ->update(['status' => 'inactive']);

        // ✅ Optimized mass deletion
        User::query()
            ->where('deleted_at', '<', now()->subMonths(6))
            ->forceDelete();

        return self::SUCCESS;
    }
}

chunk(), lazy() ve cursor() arasındaki seçim kullanım senaryosuna bağlıdır: değişiklikler için chunk(), ara işlemler için lazy() ve minimum bellek kullanımıyla basit okumalar için cursor().

İleri düzey polimorfik ilişkiler

Polimorfik ilişkiler bir modelin tek bir ilişki üzerinden birden fazla farklı model türüne ait olmasına olanak tanır. Bu esneklik yorumlar, etiketler veya eklenmiş dosyalar için idealdir.

app/Models/Comment.phpphp
// Model with inverse polymorphic relationship

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class Comment extends Model
{
    protected $fillable = ['body', 'user_id'];

    // A comment can belong to Article, Video, or any other model
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }
}
app/Models/Article.phpphp
// Parent model with polymorphic relationship

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Article extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
app/Models/Video.phpphp
// Another parent model using the same relationship

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;

class Video extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}
php
// Migration for polymorphic comments table
// database/migrations/2026_01_15_create_comments_table.php

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('comments', function (Blueprint $table) {
            $table->id();
            $table->text('body');
            $table->foreignId('user_id')->constrained();
            $table->morphs('commentable'); // Creates commentable_type and commentable_id
            $table->timestamps();

            // Composite index for polymorphic queries
            $table->index(['commentable_type', 'commentable_id']);
        });
    }
};
php
// Using polymorphic relationships
$article = Article::find(1);
$article->comments()->create([
    'body' => 'Excellent article!',
    'user_id' => auth()->id(),
]);

$video = Video::find(1);
$video->comments()->create([
    'body' => 'Very instructive video',
    'user_id' => auth()->id(),
]);

// Retrieve parent from comment
$comment = Comment::with('commentable')->find(1);
echo get_class($comment->commentable); // App\Models\Article or App\Models\Video

Polimorfik ilişkiler tablo tekrarlarını önler ve yatay özellikler için mantığı merkezileştirir.

Laravel mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Tekrar kullanılabilir mantık için trait ve observer

Trait'ler modeller arasında tekrar kullanılabilir davranışları kapsüller. Observer'lar yaşam döngüsü olayları üzerindeki hook'ları merkezileştirir.

app/Models/Concerns/HasSlug.phpphp
// Trait for automatic slug generation

namespace App\Models\Concerns;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

trait HasSlug
{
    public static function bootHasSlug(): void
    {
        static::creating(function (Model $model) {
            if (empty($model->slug)) {
                $model->slug = $model->generateUniqueSlug();
            }
        });
    }

    protected function generateUniqueSlug(): string
    {
        $slug = Str::slug($this->getSlugSource());
        $originalSlug = $slug;
        $counter = 1;

        // Check uniqueness and add suffix if needed
        while (static::where('slug', $slug)->exists()) {
            $slug = "{$originalSlug}-{$counter}";
            $counter++;
        }

        return $slug;
    }

    // Can be overridden in the model
    protected function getSlugSource(): string
    {
        return $this->title ?? $this->name;
    }
}
app/Models/Concerns/HasUuid.phpphp
// Trait for using UUIDs as primary key

namespace App\Models\Concerns;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

trait HasUuid
{
    public static function bootHasUuid(): void
    {
        static::creating(function (Model $model) {
            if (empty($model->{$model->getKeyName()})) {
                $model->{$model->getKeyName()} = (string) Str::uuid();
            }
        });
    }

    public function getIncrementing(): bool
    {
        return false;
    }

    public function getKeyType(): string
    {
        return 'string';
    }
}
app/Observers/ArticleObserver.phpphp
// Observer to centralize hooks on Article model

namespace App\Observers;

use App\Models\Article;
use App\Jobs\NotifySubscribersJob;
use App\Services\SearchIndexService;
use Illuminate\Support\Facades\Cache;

class ArticleObserver
{
    public function __construct(
        private SearchIndexService $searchIndex
    ) {}

    public function created(Article $article): void
    {
        // Invalidate recent articles cache
        Cache::tags(['articles', 'recent'])->flush();

        // Index for search
        $this->searchIndex->index($article);
    }

    public function updated(Article $article): void
    {
        // Update search index
        $this->searchIndex->update($article);

        // Notify subscribers if article was just published
        if ($article->wasChanged('published_at') && $article->published_at !== null) {
            NotifySubscribersJob::dispatch($article);
        }

        Cache::tags(['articles'])->flush();
    }

    public function deleted(Article $article): void
    {
        $this->searchIndex->remove($article);
        Cache::tags(['articles'])->flush();
    }

    // Prevent deletion if article has comments
    public function deleting(Article $article): bool
    {
        if ($article->comments()->exists()) {
            return false; // Cancel deletion
        }
        return true;
    }
}
app/Models/Article.phpphp
// Model using traits and observer

namespace App\Models;

use App\Models\Concerns\HasSlug;
use App\Models\Concerns\HasUuid;
use App\Observers\ArticleObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;

#[ObservedBy(ArticleObserver::class)]
class Article extends Model
{
    use HasSlug, HasUuid;

    protected $fillable = ['title', 'content', 'published_at'];
}

Trait'lerin boot* yöntemleri model başlatma sırasında otomatik olarak çalıştırılır ve şeffaf bir entegrasyon sağlar.

Sonuç

Eloquent ORM'i ustalaşmak, alttaki mekanizmaları anlamaya ve uygun desenleri uygulamaya dayanır. Sunulan teknikler saf sorguları performanslı ve sürdürülebilir koda dönüştürür.

Eloquent optimizasyon kontrol listesi:

✅ Görüntülenen ilişkiler için sistematik olarak with() kullanın ✅ Saymak için koleksiyonları yüklemek yerine withCount() uygulayın ✅ Filtreleme mantığını query scope'larda kapsülleyin ✅ View'lerde tekrarlanan hesaplamalar yerine accessor'ları tercih edin ✅ İş Value Object'leri için özel cast'ler uygulayın ✅ Büyük tablolardaki işlemler için chunk() veya lazy() kullanın ✅ Yan etkileri observer'larda merkezileştirin ✅ Ortak davranışları trait'lere çıkarın

php artisan telescope veya laravel-debugbar araçları üretilen SQL sorgularını görselleştirir ve eksik optimizasyonları belirler.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

#laravel
#eloquent
#php
#orm
#database

Paylaş

İlgili makaleler