VS Vikesh Singh

Courses PHP & Laravel Lesson 9

PHP & LARAVEL · LESSON 9 OF 18

Eloquent ORM: Working With the Database Like PHP Objects

· 5 min read

Lesson 7 showed a controller calling `Trade::where(...)->get()` without explaining what was actually happening underneath.

Lesson 7 showed a controller calling Trade::where(...)->get() without explaining what was actually happening underneath. That is Eloquent, Laravel's ORM (Object-Relational Mapper), and it is the single feature most developers mean when they say they "like working with Laravel." This lesson explains what an ORM actually does and why it matters.

What An ORM Does

An ORM's job is to let you work with database rows as PHP objects instead of as raw arrays pulled from hand-written SQL. Every database table gets a matching PHP class, called a model, and every row in that table becomes an instance of that class when you fetch it:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Trade extends Model
{
    protected $fillable = ['symbol', 'quantity', 'entry_price', 'exit_price', 'user_id'];
}

Common Eloquent Methods

By Laravel convention (the same "convention over configuration" idea from lesson 6), a model named Trade maps automatically to a database table named trades (plural, snake_case), with no configuration needed, unless you deliberately override it. That one small class, with almost no code in it, already gives you a full set of database operations:

$trade = Trade::find(42);
echo $trade->symbol;

$allTrades = Trade::all();

$niftyTrades = Trade::where('symbol', 'NIFTY')
    ->where('quantity', '>', 10)
    ->orderBy('entry_price', 'desc')
    ->get();

$newTrade = Trade::create([
    'symbol' => 'NIFTY',
    'quantity' => 50,
    'entry_price' => 19500.0,
    'user_id' => 1,
]);

$trade->exit_price = 19650.0;
$trade->save();

Compare Trade::where('symbol', 'NIFTY')->where('quantity', '>', 10)->orderBy('entry_price', 'desc')->get(); to the raw SQL it generates underneath: SELECT * FROM trades WHERE symbol = 'NIFTY' AND quantity > 10 ORDER BY entry_price DESC. This chained, method-based style is called the query builder, and Eloquent models inherit it directly. Each method (where, orderBy, and so on) returns the query builder itself, which is why you can keep chaining more methods onto it; only get() (or first(), or find()) actually executes the query against the database and returns results. Everything before that point is just building up the SQL to be run, which is a useful thing to know when debugging: you can call ->toSql() instead of ->get() on a query to see the exact SQL Eloquent would run, without actually running it.

Mass Assignment And Fillable

The $fillable property on the model deserves specific attention, because skipping it is a real security mistake. Trade::create([...]) and the related fill() method accept an array of column names and values, often built directly from user-submitted form input. Without $fillable restricting which columns are allowed to be set this way, a malicious user could submit an extra field in their form data, like is_admin => true or user_id set to someone else's ID, and have it silently written straight into the database, a vulnerability called mass assignment. $fillable is an explicit allow-list: only the columns named there can ever be set through create() or fill(), no matter what extra fields show up in the input array. Always set $fillable (or its inverse, $guarded) deliberately; never leave a model wide open.

Accessors Mutators And Casts

Eloquent also gives you accessors and mutators, which let a model compute or transform a value whenever it is read or written, without the rest of your application needing to know that any transformation is happening at all:

class Trade extends Model
{
    protected $fillable = ['symbol', 'quantity', 'entry_price', 'exit_price', 'user_id'];

    public function profit(): float
    {
        return ($this->exit_price - $this->entry_price) * $this->quantity;
    }

    protected function casts(): array
    {
        return [
            'entry_price' => 'decimal:2',
            'exit_price' => 'decimal:2',
        ];
    }
}

$trade = Trade::find(42);
echo $trade->profit();

casts() tells Eloquent to automatically convert raw database values into a specific PHP representation every time the model is loaded, and back again every time it is saved; here, raw database columns get treated as decimals with two-digit precision rather than plain strings or floats, which matters a lot for money values where floating-point rounding errors are a real, recurring source of bugs (famously, 0.1 + 0.2 does not equal exactly 0.3 in floating point arithmetic; using decimal casting, or an integer number of paise/cents, avoids the whole category of problem).

The N Plus One Problem

One performance trap every Eloquent beginner eventually hits, worth naming directly here rather than discovering it in production: the "N+1 query problem." Imagine looping over trades and, for each one, accessing a related $trade->user->name (relationships are covered fully in lesson 11). Written naively, that is one query to fetch all the trades, and then one additional query per trade to fetch its user, meaning 100 trades becomes 101 total queries instead of 2. Eloquent gives you Trade::with('user')->get() specifically to solve this: it fetches all the trades in one query and all their related users in a second query, then matches them up in PHP memory, for exactly 2 queries total regardless of how many trades there are. If a page that lists records suddenly gets slow as the dataset grows, N+1 queries are the first thing to check, and Laravel's debug tooling (like Laravel Debugbar, a commonly installed package) will show you the query count directly.

It is worth also knowing that Eloquent is not the only way to talk to the database inside a Laravel application; the underlying query builder (DB::table('trades')->where(...)->get()) is available directly, without a model at all, and raw SQL via DB::select() remains an option for genuinely complex reporting queries that are awkward to express through method chaining. Reach for those only when Eloquent's expressiveness runs out, not as a default; a model gives you relationships, casts, and mass-assignment protection for free, and bypassing it means re-earning all of that by hand.

Try it yourself: create a Trade model backed by a trades table with the columns shown above, insert a few rows through Trade::create(), then write a query chaining where, orderBy, and get() to find every losing trade (where exit_price is less than entry_price), and call ->toSql() on the same query (before calling get()) to see the raw SQL Eloquent builds for you.