Laravel Eloquent ORM Basics
Eloquent is Laravel's "database translator"—you interact with it using PHP objects, and it translates them into SQL queries for execution.
1. What You'll Learn
- Model Creation and Attribute Definition: $fillable/$guarded/$casts/$attributes
- The Complete CRUD Workflow: create/all/find/update/delete and Batch Assignment
- Query builders: where/orderBy/groupBy/subqueries
- Collection operations: filter/map/reduce/each chained processing
- Soft Deletion and Recovery: SoftDeletes trait
2. A True Story of a Full-Stack Developer
(1) Pain Point: SQL string concatenation leads to injection and data loss
In the early days, Bob wrote ShopMetrics in native PHP—all SQL queries were built using string concatenation: "SELECT * FROM shops WHERE id = " . $_GET['id']. A hacker injected 1 OR 1=1 into Alice's store ID, causing a site-wide data breach. A more common issue was when Bob forgot to include a WHERE clause in an UPDATE query; a single command reset the revenue for all stores to zero, and it took a full day to restore the data.
(2) The Eloquent ORM Solution
Eloquent uses PHP objects to interact with databases, provides automatic parameter binding to prevent injection attacks, protects sensitive fields with bulk assignment, and uses soft deletion to prevent accidental deletion.
// Safe, readable, no SQL injection possible
$shop = Shop::create([
'name' => 'Alice Store',
'tenant_id' => 1,
]);
// Mass assignment protection — only $fillable fields allowed
protected $fillable = ['name', 'slug', 'tenant_id'];
// revenue is NOT in $fillable — can't be set via create()
(3) Revenue
After Bob started using Eloquent, SQL injection risk was eliminated, and accidentally deleted data could be restored with a single click using soft delete. The amount of code was reduced from 200 lines of SQL to 30 lines of PHP.
3. Model Definition
(1) Create a Model
php artisan make:model Shop
# Creates: app/Models/Shop.php
# With migration
php artisan make:model Shop -m
# Creates: app/Models/Shop.php + database/migrations/create_shops_table.php
(2) Model Property Configuration
// app/Models/Shop.php
class Shop extends Model
{
protected $fillable = [
'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
];
protected $guarded = ['id']; // Alternative: block specific fields
protected $attributes = [
'status' => 'active',
'revenue' => 0,
];
protected $casts = [
'revenue' => 'decimal:2',
'is_active' => 'boolean',
'metadata' => 'json',
'launched_at' => 'datetime',
];
}
| Property | Function | Recommended Method |
|---|---|---|
$fillable |
Fields that allow batch assignment | ✅ Whitelist |
$guarded |
Fields where batch assignment is prohibited | ❌ Blacklist |
$casts |
Automatic Type Conversion | Required |
$attributes |
Field Default Value | Replace DB Default Value |
(3) Eloquent Model Class Relationship Diagram
classDiagram
class Model {
+save()
+delete()
+update(array data)
+fresh()
+refresh()
+toArray()
+toJson()
}
class Shop {
+array fillable
+array casts
+tenant()
+orders()
+products()
}
class SoftDeletes {
+forceDelete()
+restore()
+trashed()
+withTrashed()
+onlyTrashed()
}
Model <|-- Shop
Shop ..|> SoftDeletes : uses trait
(1) ▶ Example: ShopMetrics Shop Model
// app/Models/Shop.php
class Shop extends Model
{
use SoftDeletes;
protected $fillable = [
'tenant_id', 'name', 'slug', 'description', 'status', 'revenue',
];
protected $casts = [
'revenue' => 'decimal:2',
'metadata' => 'array',
];
protected $attributes = [
'status' => 'active',
'revenue' => 0,
];
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function scopeActive(Builder $query): Builder
{
return $query->where('status', 'active');
}
}
Output:
// Execution Successful
4. CRUD Operations
(1) Create
// Method 1: create() with mass assignment
$shop = Shop::create([
'tenant_id' => 1,
'name' => 'Alice Store',
'slug' => 'alice-store',
]);
// Method 2: new + save
$shop = new Shop();
$shop->tenant_id = 1;
$shop->name = 'Alice Store';
$shop->slug = 'alice-store';
$shop->save();
// Method 3: firstOrCreate — find or create
$shop = Shop::firstOrCreate(
['slug' => 'alice-store'], // search criteria
['name' => 'Alice Store', 'tenant_id' => 1], // values if creating
);
// Method 4: updateOrCreate — update or create
$shop = Shop::updateOrCreate(
['slug' => 'alice-store'],
['name' => 'Alice Store Updated', 'revenue' => 5000],
);
(2) Read
// Find by primary key
$shop = Shop::find(1);
$shop = Shop::findOrFail(1); // throws 404 if not found
// Find by column
$shop = Shop::where('slug', 'alice-store')->first();
$shop = Shop::whereSlug('alice-store')->firstOrFail();
// Get all
$shops = Shop::all();
$shops = Shop::active()->get(); // using scope
// Chunk for large datasets
Shop::chunk(200, function ($shops) {
foreach ($shops as $shop) {
// Process 200 shops at a time
}
});
(3) Update
// Update single model
$shop->update(['name' => 'New Name']);
// Update via query
Shop::where('status', 'suspended')->update(['status' => 'active']);
// Increment/Decrement
$shop->increment('revenue', 1500);
Shop::whereId(1)->decrement('stock', 5);
(4) Delete
// Soft delete (sets deleted_at)
$shop->delete();
// Force delete (permanent)
$shop->forceDelete();
// Restore soft-deleted
$shop->restore();
// Query with trashed
Shop::withTrashed()->where('id', 1)->first();
Shop::onlyTrashed()->get();
(1) ▶ Example: ShopMetrics Complete CRUD Workflow
// Create a shop with products
$shop = Shop::create([
'tenant_id' => 1,
'name' => 'Bob Electronics',
'slug' => 'bob-electronics',
]);
$shop->products()->createMany([
['name' => 'Widget A', 'sku' => 'W-001', 'price' => 29.99],
['name' => 'Widget B', 'sku' => 'W-002', 'price' => 49.99],
]);
// Read with eager loading
$shop = Shop::with('products')->whereSlug('bob-electronics')->firstOrFail();
// Update shop and product
$shop->update(['revenue' => 15000]);
$shop->products()->whereSku('W-001')->update(['price' => 34.99]);
// Soft delete and restore
$shop->delete();
Shop::withTrashed()->whereSlug('bob-electronics')->first()->restore();
Output:
// Execution Successful
5. Query Builder
(1) Conditional Query
$shops = Shop::where('status', 'active')
->where('revenue', '>', 1000)
->orWhere(function ($query) {
$query->where('status', 'new')
->where('created_at', '>', now()->subDays(7));
})
->get();
// Dynamic where
$shops = Shop::whereStatus('active')
->whereRevenueGreaterThan(1000)
->get();
(2) Sorting, Grouping, and Pagination
// OrderBy
$shops = Shop::orderBy('revenue', 'desc')->get();
// GroupBy with having
$revenueByStatus = Shop::select('status', DB::raw('SUM(revenue) as total'))
->groupBy('status')
->having('total', '>', 1000)
->get();
// Pagination
$shops = Shop::where('tenant_id', 1)->paginate(15);
$shops = Shop::where('tenant_id', 1)->simplePaginate(15);
$shops = Shop::where('tenant_id', 1)->cursorPaginate(15);
| Pagination Methods | Execute Query | Use Cases |
|---|---|---|
paginate() |
COUNT + SELECT | Total Number of Pages Required |
simplePaginate() |
SELECT only | No total number of pages required |
cursorPaginate() |
SELECT with WHERE Only | Most Efficient for Large Datasets |
(3) Subqueries
// Subquery in select
$shops = Shop::select('shops.*')
->selectSub(
Order::selectRaw('SUM(total)')
->whereColumn('shop_id', 'shops.id'),
'orders_total'
)
->get();
// Subquery in where
$latestOrders = Shop::where('created_at', function ($query) {
$query->selectRaw('MAX(created_at)')
->from('orders')
->whereColumn('shop_id', 'shops.id');
})->get();
(1) ▶ Example: ShopMetrics Complex Query
// Top 10 shops by revenue in current tenant, with order count
$topShops = Shop::select('shops.*')
->selectSub(
Order::selectRaw('COUNT(*)')
->whereColumn('shop_id', 'shops.id')
->where('created_at', '>=', now()->subDays(30)),
'recent_orders_count'
)
->where('tenant_id', tenant()->id)
->where('status', 'active')
->orderBy('revenue', 'desc')
->take(10)
->get();
Output:
// Execution Successful
6. Set Operations
Eloquent get() returns a Collection object, which provides more powerful chained methods than arrays.
| Method | Function | SQL Equivalent |
|---|---|---|
filter() |
Filter | WHERE |
map() |
Mapping Conversion | SELECT Conversion |
sortBy() |
Sort | ORDER BY |
groupBy() |
Group | GROUP BY |
sum() |
Sum | SUM() |
count() |
Count | COUNT() |
pluck() |
Extract Column | SELECT one column |
unique() |
Remove duplicates | DISTINCT |
each() |
Iterate and execute | — |
reduce() |
Cumulative Calculation | — |
(1) ▶ Example: ShopMetrics Collection Chain Operations
// Get all shops for a tenant, filter and transform
$topShops = Shop::where('tenant_id', 1)
->with('products')
->get()
->filter(fn ($shop) => $shop->revenue > 1000)
->sortByDesc('revenue')
->map(fn ($shop) => [
'name' => $shop->name,
'revenue' => $shop->revenue,
'product_count' => $shop->products->count(),
])
->take(10);
// Group shops by status and count
$shopsByStatus = Shop::where('tenant_id', 1)
->get()
->groupBy('status')
->map(fn ($group) => $group->count());
// ['active' => 15, 'suspended' => 2, 'closed' => 1]
// Pluck IDs for bulk operation
$shopIds = Shop::where('status', 'active')->pluck('id');
// [1, 2, 5, 8, 12]
Output:
// Execution Successful
7. Soft Delete
(1) Enable soft deletion
// Model
class Shop extends Model
{
use SoftDeletes;
protected $casts = [
'deleted_at' => 'datetime',
];
}
// Migration
$table->softDeletes(); // adds deleted_at TIMESTAMP NULL
(2) Soft Delete Operation
// Delete (soft — sets deleted_at)
$shop->delete();
// Check if trashed
$shop->trashed(); // true if soft-deleted
// Include trashed records
Shop::withTrashed()->get();
// Only trashed records
Shop::onlyTrashed()->get();
// Restore
$shop->restore();
// Permanent delete
$shop->forceDelete();
(1) ▶ Example: ShopMetrics Soft Deletion Recovery Scenario
// Alice accidentally deleted a shop
$shop = Shop::whereSlug('alice-store')->first();
$shop->delete();
// Bob can still find it in trashed records
$trashed = Shop::onlyTrashed()->whereSlug('alice-store')->first();
// Restore the shop with all relationships intact
if ($trashed) {
$trashed->restore();
// $trashed->products still exist — they weren't deleted
}
Output:
// Execution Successful
8. Comprehensive Example: ShopMetrics Order Analysis
// ============================================
// Comprehensive: ShopMetrics Order Analytics
// Covers: CRUD, queries, collections, soft delete, scopes
// ============================================
// app/Models/Order.php
class Order extends Model
{
use SoftDeletes;
protected $fillable = [
'tenant_id', 'shop_id', 'user_id', 'order_number',
'subtotal', 'discount', 'total', 'status', 'metadata',
];
protected $casts = [
'total' => 'decimal:2',
'metadata' => 'array',
'deleted_at' => 'datetime',
];
public function shop(): BelongsTo
{
return $this->belongsTo(Shop::class);
}
public function scopeCompleted(Builder $query): Builder
{
return $query->where('status', 'completed');
}
public function scopeThisMonth(Builder $query): Builder
{
return $query->whereBetween('created_at', [
now()->startOfMonth(), now()->endOfMonth(),
]);
}
}
// Analytics query — monthly revenue report
$monthlyReport = Order::where('tenant_id', tenant()->id)
->completed()
->thisMonth()
->with('shop')
->get()
->groupBy('shop.name')
->map(fn ($orders) => [
'shop' => $orders->first()->shop->name,
'order_count' => $orders->count(),
'revenue' => $orders->sum('total'),
'avg_order' => $orders->avg('total'),
])
->sortByDesc('revenue')
->values();
❓ FAQ
findOrFail?findOrFail when you need to return a 404 page if no records are found. If not finding any records is part of normal business logic (such as a search with no results), use find and check for null.WHERE in SQL), while Collections operate in memory (e.g., filtering with filter in PHP). Large datasets should be filtered at the query level, while small result sets can use Collection methods.deleted_at field; the associated data remains in the database. Once the parent model is restored, the relationship becomes available immediately. If you need to perform a cascading soft delete, you can listen for the deleting event in the model's boot() method.DB::table()) when you just need simple query results and don't need models. Eloquent is essentially a wrapper around the Query Builder.Shop::where(...)->update([...]) Executes a single SQL statement to update all matching records; this is highly efficient. $shops->each->update([...]) Executes SQL statements one by one, triggering model events. Use individual updates when you need to trigger events.📖 Summary
- Eloquent uses PHP objects to interact with databases, with automatic parameter binding to prevent SQL injection
- $fillable whitelists protect against bulk assignments; $casts performs automatic type conversion
- The four steps of CRUD: create/read/update/delete;
findOrFailreturns a 404 - The query builder supports WHERE, ORDER BY, GROUP BY, and subqueries
- Collections offer more powerful chained operations (filter/map/sortBy) than arrays.
- Soft delete marked with
deleted_atrather than a permanent delete; supports restoration
📝 Exercises
-
Basic Exercise (⭐): Create a
Productmodel for ShopMetrics, define$fillableand$casts, implement complete CRUD operations (create, read, update, delete), and use Tinker to validate each operation. -
Advanced Problem (⭐⭐): Write a query to retrieve the top 5 stores with the highest revenue under the current tenant, along with their order counts. Use the
selectSubsubquery and themapmethod of theCollectionto format the output. -
Challenge (⭐⭐⭐): Implement soft deletion and cascading restoration for the Order model: When an Order is deleted, its OrderItems are also soft-deleted; when the Order is restored, the OrderItems are restored along with it. Implement this using model event listeners.



