Understanding Eloquent ORM in Laravel

Developers coming from plain PHP often find Eloquent a little too magical: one line of code can fire many queries without you noticing. The orders page that was instant in developm...

Understanding Eloquent ORM in Laravel

Developers coming from plain PHP often find Eloquent a little too magical: one line of code can fire many queries without you noticing. The orders page that was instant in development suddenly takes three seconds once the table holds a few thousand rows. The problem is rarely Eloquent itself; it is how it is used.

This guide explains Eloquent ORM in Laravel 11 through a small store example (customers, orders, products): how models work, CRUD, relationships, eager loading, accessors, scopes, soft deletes, and where the query builder or raw SQL is the better tool.

1. What an ORM Does

An ORM maps table rows to PHP objects. Eloquent follows the Active Record pattern: one model instance represents one row and knows how to save itself. Calls such as where() and get() are translated into SQL by the query builder and executed through PDO with bound parameters, so they are safe from SQL injection as long as you never paste raw input into whereRaw().

2. Models and Conventions

php artisan make:model Order -mf

-m adds a migration and -f a factory. By convention, Order maps to the orders table with an id key and automatic timestamps.

class Order extends Model
{
    use SoftDeletes;

    protected $fillable = ['customer_id', 'invoice_no', 'status', 'total', 'paid_at'];

    protected function casts(): array
    {
        return [
            'total'   => 'decimal:2',
            'paid_at' => 'datetime',
            'meta'    => 'array',
        ];
    }
}

Laravel 11 defines casts in a casts() method. A datetime cast gives you a Carbon instance, so $order->paid_at->format('M j, Y') just works. $fillable limits mass assignment, stopping a user from slipping status=paid into a form submission.

3. CRUD

$order  = Order::findOrFail($id);
$recent = Order::where('status', 'paid')->latest()->limit(20)->get();

$order = Order::create([
    'customer_id' => $customer->id,
    'invoice_no'  => 'INV-2026-0001',
    'status'      => 'pending',
    'total'       => 49.90,
]);

$order->update(['status' => 'paid', 'paid_at' => now()]);

Customer::updateOrCreate(['email' => 'sam@example.com'], ['name' => 'Sam']);

$order->delete();   // soft delete

get() returns a Collection, first() returns a model or null, and findOrFail() throws an exception Laravel renders as a 404.

4. Relationships

TypeExampleMethods
One to manyA customer has many ordershasMany / belongsTo
One to oneA user has one profilehasOne / belongsTo
Many to manyOrders contain products; products appear in many ordersbelongsToMany + pivot
Has many throughCustomer to order items via ordershasManyThrough
// Customer.php
public function orders(): HasMany
{
    return $this->hasMany(Order::class);
}

// Order.php
public function customer(): BelongsTo
{
    return $this->belongsTo(Customer::class);
}

public function products(): BelongsToMany
{
    return $this->belongsToMany(Product::class, 'order_items')
        ->withPivot(['qty', 'price'])
        ->withTimestamps();
}

Storing price on the pivot is deliberate: product prices change, but historic orders must keep the price charged at the time.

5. The N+1 Problem

$orders = Order::latest()->take(50)->get();

foreach ($orders as $order) {
    echo $order->customer->name;   // one query per order
}

That is 51 queries. Eager loading brings it down to two or three:

$orders = Order::with(['customer:id,name', 'products'])
    ->withCount('products')
    ->latest()
    ->take(50)
    ->get();

Catch mistakes early by enabling lazy-loading protection outside production:

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

More techniques are in optimizing Eloquent query performance.

6. Accessors, Mutators and Scopes

protected function invoiceNo(): Attribute
{
    return Attribute::make(set: fn (string $v) => strtoupper(trim($v)));
}

protected function totalFormatted(): Attribute
{
    return Attribute::make(get: fn () => '$' . number_format($this->total, 2));
}

public function scopePaid($query)
{
    return $query->where('status', 'paid');
}

public function scopeBetween($query, $from, $to)
{
    return $query->whereBetween('created_at', [$from, $to]);
}

$revenue = Order::paid()->between(now()->startOfMonth(), now())->sum('total');

7. Working With Large Tables

  • chunkById(500, ...) processes rows in safe batches even while updating them.
  • lazy() iterates one model at a time with low memory.
  • select([...]) fetches only the columns you need.
  • paginate(20) on list pages instead of get().

8. When Not to Use Eloquent

Eloquent shines for CRUD and per-record logic. For heavy aggregate reports, such as revenue per category per month across millions of rows, the query builder or DB::select() is often faster and clearer because it skips hydrating a model for every row. Mixing both in one project is perfectly normal.

9. Common Errors

  • MassAssignmentException: the column is missing from $fillable. Add it rather than setting $guarded = [] on user-facing models.
  • "Call to a member function on null": an empty belongsTo. Use $order->customer?->name or withDefault().
  • Records vanish or reappear: SoftDeletes filters automatically; use withTrashed() when needed.
  • Totals off by a cent: money stored as float. Use DECIMAL columns with a decimal:2 cast, or integer cents.

Healthy Model Checklist

  • $fillable lists only form-editable columns.
  • Dates, booleans, JSON and money have proper casts.
  • Relationships declare return types.
  • List pages use with() and paginate().
  • preventLazyLoading is on in development.
  • Frequently filtered columns are indexed in migrations.

Design the tables before the models: designing a simple ERD helps you pick the right relationships, and safe database migrations covers changing them later without losing data.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Eloquent Orm Laravel Programming & Laravel
Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now