Eloquent ORM
๐๏ธ Eloquent ORM
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.
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.
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.
$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.