Eloquent ORM: patronen en optimalisaties voor Laravel
Beheers Eloquent ORM met geavanceerde patronen en optimalisatietechnieken. Eager loading, query scopes, accessors, mutators en performance voor Laravel-applicaties.

Eloquent ORM zet database-interacties om in vloeiende en expressieve operaties. Naast de elegante syntax bepaalt de beheersing van geavanceerde patronen en optimalisatietechnieken de prestaties van Laravel-applicaties in productie.
Het N+1-probleem is de belangrijkste oorzaak van traagheid in Eloquent-applicaties. Elke niet-geoptimaliseerde relatie genereert een extra SQL-query per record.
Het N+1-probleem oplossen met eager loading
Het N+1-probleem doet zich voor wanneer elke iteratie over een collectie een extra query activeert om relaties te laden. Met 100 artikelen en hun auteurs zijn dat 101 queries in plaats van één geoptimaliseerde.
Eager loading haalt alle relaties op in maximaal één of twee queries en verkort zo de responstijd drastisch.
// 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 met with() anticipeert op de behoeften en laadt de gegevens vooraf. Het verschil in prestaties wordt spectaculair bij grote collecties.
Voorwaardelijke en beperkte eager loading
Grote relaties vereisen soms gedeeltelijk laden. Beperkingen op de eager loading begrenzen de opgehaalde data en voorkomen tegelijkertijd N+1.
// 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'));
}
}De methoden withCount() en withSum() voegen aggregaties toe zonder volledige collecties te laden, ideaal voor dashboardstatistieken.
Query scopes voor herbruikbare queries
Query scopes kapselen de filterlogica binnen het model. Lokale scopes bieden flexibiliteit terwijl globale scopes automatisch op alle queries worden toegepast.
Lokale scopes gebruiken het voorvoegsel scope in het model maar worden zonder dat aangeroepen: scopeActive() wordt User::active().
// 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}%"));
});
}
}// 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());
}
}// 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();Het aaneenketenen van scopes levert leesbare en onderhoudbare queries op terwijl de bedrijfslogica in het model wordt gecentraliseerd.
Klaar om je Laravel gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Accessors en mutators met Attribute
Laravel 9+ introduceert een uniforme syntax voor accessors en mutators via de klasse Attribute. Deze moderne aanpak vervangt de get*Attribute- en set*Attribute-methoden.
// 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'),
);
}
}// 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"De methode shouldCache() optimaliseert dure accessors door meerdere herberekeningen op hetzelfde model te vermijden.
Eigen casts voor complexe types
Casts transformeren waarden automatisch tussen PHP en de database. Eigen casts kapselen de serialisatielogica voor bedrijfstypes.
// 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');
}
}// 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,
];
}
}// 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',
];
}
}// 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 DBValue Objects gecombineerd met eigen casts garanderen de integriteit van bedrijfsdata terwijl een elegante API behouden blijft.
Grote queries optimaliseren
Operaties op miljoenen records vereisen specifieke technieken om geheugenuitputting te vermijden. Chunking en cursors verwerken de gegevens in batches.
Model::all() laadt alle records in het geheugen. Op een tabel van 100.000 rijen kan dit meerdere gigabytes RAM gebruiken en de applicatie laten crashen.
// 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;
}
}De keuze tussen chunk(), lazy() en cursor() hangt af van de use case: chunk() voor wijzigingen, lazy() voor tussenliggende operaties en cursor() voor enkel lezen met minimaal geheugenverbruik.
Geavanceerde polymorfe relaties
Polymorfe relaties laten een model toe om via één relatie tot meerdere verschillende modeltypes te behoren. Deze flexibiliteit is ideaal voor reacties, tags of bijgevoegde bestanden.
// 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);
}
}// 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');
}
}// 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');
}
}// 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']);
});
}
};// 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\VideoPolymorfe relaties vermijden tabelduplicatie en centraliseren de logica voor transversale functionaliteiten.
Klaar om je Laravel gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Traits en observers voor herbruikbare logica
Traits kapselen herbruikbare gedragingen over modellen heen. Observers centraliseren hooks op levenscyclusgebeurtenissen.
// 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;
}
}// 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';
}
}// 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;
}
}// 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'];
}De boot*-methoden van traits worden automatisch uitgevoerd tijdens de modelinitialisatie en zorgen voor een transparante integratie.
Conclusie
Eloquent ORM beheersen vereist begrip van de onderliggende mechanismen en het toepassen van de juiste patronen. De gepresenteerde technieken transformeren naïeve queries in performante en onderhoudbare code.
Eloquent-optimalisatiechecklist:
✅ Systematisch with() gebruiken voor weergegeven relaties
✅ withCount() toepassen in plaats van collecties laden om te tellen
✅ Filterlogica in query scopes kapselen
✅ Accessors verkiezen boven herhaalde berekeningen in views
✅ Eigen casts implementeren voor zakelijke Value Objects
✅ chunk() of lazy() gebruiken voor operaties op grote tabellen
✅ Neveneffecten centraliseren in observers
✅ Gemeenschappelijk gedrag extraheren naar traits
De tools php artisan telescope of laravel-debugbar maken de gegenereerde SQL-queries zichtbaar en helpen ontbrekende optimalisaties te identificeren.
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Tags
Delen
Gerelateerde artikelen

Laravel en PHP sollicitatievragen: de Top 25 in 2026
De 25 meest gestelde Laravel- en PHP-sollicitatievragen. Eloquent ORM, middleware, Artisan, queues, tests en architectuur met uitgebreide antwoorden en codevoorbeelden.

Laravel 11: Een complete applicatie bouwen vanaf nul
Uitgebreide handleiding voor Laravel 11: installatie, Eloquent-modellen, authenticatie met Breeze, controllers, routes, autorisatie met policies, REST API met Sanctum, testen met Pest en productie-deployment.

Laravel Middleware Uitgelicht: Authenticatie, Rate Limiting en Eigen Middleware
Uitgebreide handleiding over Laravel middleware met praktische voorbeelden voor authenticatie, rate limiting met throttle, eigen middleware bouwen en geavanceerde patronen voor productieomgevingen.