Eloquent ORM

Laravel Fundamentals
Course 1 ยท Chapter 5 ยท Eloquent ORM

๐Ÿ—ƒ๏ธ Eloquent ORM

Eloquent is Laravel's ORM, included the same "batteries first" way Django's ORM is โ€” but one real difference stands out immediately: Django's migrations are auto-generated by diffing your models, while Laravel's are hand-written PHP files, both the "up" and the "down" direction, from the start.

Defining a Model

php artisan make:model Book -m
# Model created successfully.
# Created Migration: 2026_07_05_120000_create_books_table.php

The -m flag generates a matching migration in the same step โ€” Django keeps model definition and makemigrations as two separate commands; Laravel bundles them:

// app/Models/Book.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    protected $fillable = ['title', 'description', 'price'];
}

Convention Over Configuration

Eloquent infers the table name (books, pluralized snake_case of Book) and primary key (id) automatically โ€” no explicit declaration needed unless you deviate from convention.

-m: One Command, Two Files

A convenience Django doesn't offer directly โ€” creating a model and its first migration together, rather than running make:model then a separate migration step.

๐Ÿ”€ Migrations: Hand-Written, Not Auto-Generated

This is the single biggest ORM difference from Django worth calling out explicitly. Django's Course 1 makemigrations diffs your models against migration history and writes the file for you. Laravel migrations are plain PHP files you write, defining both directions yourself:

// database/migrations/2026_07_05_120000_create_books_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('books', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('description')->nullable();
            $table->decimal('price', 6, 2);
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('books');
    }
};

Django's Auto-Generated Migration vs Laravel's Hand-Written One

Django
python manage.py makemigrations
# diffs models.py against migration history,
# WRITES the migration file for you โ€”
# including the reverse operation automatically.
Laravel
// you write up() (create the table)
// AND down() (drop it) yourself โ€”
// nothing is inferred from a model diff.

up()/down() mirror Django's own up/down migration concept exactly โ€” the difference is authorship, not the underlying idea.

php artisan migrate
# Migrating: 2026_07_05_120000_create_books_table
# Migrated:  2026_07_05_120000_create_books_table (12.34ms)

The Eloquent Query Builder

Once past the migration difference, Eloquent's query API reads remarkably close to Django's QuerySet:

Book::all();                                    // all rows
Book::where('price', '<', 15)->get();          // filtered
Book::where('in_stock', true)->orderBy('title')->get();  // chained
Book::find(1);                                   // by primary key โ€” returns null if missing
Book::findOrFail(1);                             // throws ModelNotFoundException if missing
Book::where('price', 0)->count();

๐Ÿ›ก๏ธ Mass Assignment Protection

This exact concept has now appeared in every backend framework covered so far โ€” Rails' strong parameters, Django's ModelForm.Meta.fields, DRF's serializer fields โ€” and Eloquent's version is $fillable:

class Book extends Model
{
    protected $fillable = ['title', 'description', 'price'];
    // Only these fields can be set via mass-assignment methods like create()/update()
}

// Safe โ€” only fillable fields are actually written
Book::create($request->only(['title', 'description', 'price']));

Every ORM/framework combination this project has covered enforces the same rule with different syntax: explicitly state which fields are writable, never assume "all of them" is safe.

Automatic SQL Injection Protection

The same callback as Django's ORM chapter โ€” Eloquent parameterizes every value passed through the query builder automatically:

// Completely safe โ€” Eloquent parameterizes $userSuppliedTitle regardless of content
Book::where('title', $userSuppliedTitle)->get();

// โŒ Still dangerous โ€” raw string interpolation into a raw query bypasses protection,
// the exact same SQLi course warning, just in PHP:
// DB::select("SELECT * FROM books WHERE title = '$userSuppliedTitle'")

๐Ÿ’ป Coding Challenges

Challenge 1: Create a Model and Migration

Run php artisan make:model Author -m, write the migration's up()/down() methods for a table with name (string) and bio (nullable text), and add a proper $fillable array to the model.

Goal: Practice writing both migration directions by hand and setting up mass-assignment protection from the start.

โ†’ Solution

Challenge 2: Write Five Query Builder Queries

Against the Book model, write Eloquent queries for: all in-stock books ordered by price ascending, books under $15, the single book with id=3 (throwing if missing), all books excluding out-of-stock ones, and the count of all books.

Goal: Build fluency with where, orderBy, findOrFail, and count.

โ†’ Solution

Challenge 3: Fix an Open $fillable

Given a User-like model with protected $guarded = []; (meaning nothing is protected) and a controller calling User::create($request->all()), rewrite both to safely allow only name and email, and explain what a malicious request could have done to the original version.

Goal: Practice recognizing and fixing the sharpest version of the mass-assignment vulnerability seen yet.

โ†’ Solution

โš ๏ธ Gotcha: An Open Model + $request->all()

This is the sharpest version of the mass-assignment warning this project has covered. If a model sets $guarded = [] (meaning "nothing is protected") or omits $fillable entirely on an older Laravel setup, calling User::create($request->all()) blindly writes every field present in the request โ€” including one an attacker added that was never part of the intended form, like is_admin or role. Always define $fillable explicitly, and never pass $request->all() directly into a mass-assignment method without validating and allowlisting the fields first (Course 2's Validation-adjacent chapters build on this further). Separately: writing a migration file doesn't apply it โ€” php artisan migrate still has to be run, the same "code exists but the framework doesn't know about it yet" gap seen in earlier chapters' controller/route wiring.

๐ŸŽฏ What's Next

A single model can now be created, queried, and migrated โ€” the next chapter covers what happens when models need to reference each other: Eloquent Relationships, hasMany/belongsTo/belongsToMany, and eager loading with with() to solve the N+1 problem this project keeps running into.