# Eloquent ORM: Patterns and Optimizations for Laravel > Master Eloquent ORM with advanced patterns and optimization techniques. Eager loading, query scopes, accessors, mutators and performance for Laravel applications. - Published: 2026-02-10 - Updated: 2026-03-31 - Author: SharpSkill - Tags: laravel, eloquent, php, orm, database - Reading time: 18 min --- Eloquent ORM transforms database interactions into fluid and expressive operations. Beyond the elegant syntax, mastering advanced patterns and optimization techniques determines the performance of Laravel applications in production. > **Performance First** > > The N+1 problem is the primary cause of slowness in Eloquent applications. Each unoptimized relationship generates an additional SQL query per record. ## Solving the N+1 Problem with Eager Loading The N+1 problem occurs when each iteration over a collection triggers an additional query to load relationships. With 100 articles and their authors, this means 101 queries instead of a single optimized one. Eager loading fetches all relationships in one or two queries maximum, drastically reducing response time. ```php // app/Http/Controllers/ArticleController.php // 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')); } } ``` Eager loading with `with()` anticipates needs and loads data upfront. The performance difference becomes spectacular on large collections. ## Conditional and Constrained Eager Loading Large relationships sometimes require partial loading. Constraints on eager loading limit the retrieved data while avoiding N+1. ```php // app/Http/Controllers/UserController.php // 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')); } } ``` The `withCount()` and `withSum()` methods add aggregations without loading complete collections, ideal for dashboard statistics. ## Query Scopes for Reusable Queries Query scopes encapsulate filtering logic within the model. Local scopes offer flexibility while global scopes apply automatically to all queries. > **Naming Convention** > > Local scopes use the `scope` prefix in the model but are called without it: `scopeActive()` becomes `User::active()`. ```php // app/Models/Article.php // 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}%")); }); } } ``` ```php // app/Models/Scopes/PublishedScope.php // 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(); ``` Chained scopes create readable and maintainable queries while centralizing business logic in the model. ## Accessors and Mutators with Attributes Laravel 9+ introduces a unified syntax for accessors and mutators via the `Attribute` class. This modern approach replaces the `get*Attribute` and `set*Attribute` methods. ```php // app/Models/User.php // 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" ``` The `shouldCache()` method optimizes expensive accessors by avoiding multiple recalculations on the same model. ## Custom Casts for Complex Types Casts automatically transform values between PHP and the database. Custom casts encapsulate serialization logic for business types. ```php // app/Casts/MoneyCast.php // 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'); } } ``` ```php // app/ValueObjects/Money.php // 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, ]; } } ``` ```php // app/Models/Order.php // 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 Objects combined with custom casts ensure business data integrity while maintaining an elegant API. ## Optimizing Large-Scale Queries Operations on millions of records require specific techniques to avoid memory exhaustion. Chunking and cursors process data in batches. > **Memory Warning** > > `Model::all()` loads all records into memory. On a 100,000-row table, this can consume several gigabytes of RAM and crash the application. ```php // app/Console/Commands/ProcessUsersCommand.php // 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; } } ``` The choice between `chunk()`, `lazy()`, and `cursor()` depends on the use case: `chunk()` for modifications, `lazy()` for intermediate operations, `cursor()` for simple reading with minimal memory. ## Advanced Polymorphic Relationships Polymorphic relationships allow a model to belong to multiple different model types via a single relationship. This flexibility is ideal for comments, tags, or attached files. ```php // app/Models/Comment.php // 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); } } ``` ```php // app/Models/Article.php // 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'); } } ``` ```php // app/Models/Video.php // 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 ``` Polymorphic relationships avoid table duplication and centralize logic for cross-cutting features. ## Traits and Observers for Reusable Logic Traits encapsulate reusable behaviors across models. Observers centralize hooks on lifecycle events. ```php // app/Models/Concerns/HasSlug.php // 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; } } ``` ```php // app/Models/Concerns/HasUuid.php // 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'; } } ``` ```php // app/Observers/ArticleObserver.php // 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; } } ``` ```php // app/Models/Article.php // 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 `boot*` methods execute automatically during model initialization, enabling seamless integration. ## Conclusion Mastering Eloquent ORM relies on understanding the underlying mechanisms and applying appropriate patterns. The techniques presented transform naive queries into performant and maintainable code. **Eloquent optimization checklist:** ✅ Systematically use `with()` for displayed relationships ✅ Apply `withCount()` rather than loading collections to count ✅ Encapsulate filtering logic in query scopes ✅ Prefer accessors over repeated calculations in views ✅ Implement custom casts for business Value Objects ✅ Use `chunk()` or `lazy()` for operations on large tables ✅ Centralize side effects in observers ✅ Extract common behaviors into traits The `php artisan telescope` or `laravel-debugbar` tool visualizes generated SQL queries and identifies missing optimizations. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/laravel/eloquent-orm-patterns-optimizations