404 Not Found

404 Not Found


nginx

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


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.

PHP
// 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

BASH
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

PHP
// 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

100%
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

PHP
// 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:

TEXT
// Execution Successful

4. CRUD Operations

(1) Create

PHP
// 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

PHP
// 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

PHP
// 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

PHP
// 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

PHP
// 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:

TEXT
// Execution Successful

5. Query Builder

(1) Conditional Query

PHP
$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

PHP
// 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

PHP
// 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

PHP
// 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:

TEXT
// 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

PHP
// 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:

TEXT
// Execution Successful

7. Soft Delete

(1) Enable soft deletion

PHP
// Model
class Shop extends Model
{
    use SoftDeletes;

    protected $casts = [
        'deleted_at' => 'datetime',
    ];
}

// Migration
$table->softDeletes(); // adds deleted_at TIMESTAMP NULL

(2) Soft Delete Operation

PHP
// 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

PHP
// 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:

TEXT
// Execution Successful

8. Comprehensive Example: ShopMetrics Order Analysis

PHP
// ============================================
// 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

Q What is the difference between $fillable and $guarded?
A $fillable is a whitelist (only these fields are allowed to be assigned in bulk), while $guarded is a blacklist (these fields are prohibited). We recommend using the $fillable whitelist, as it is safer—you must explicitly declare the fields that are allowed to be assigned values.
Q When should you use findOrFail?
A Use 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.
Q What is the difference between Collection methods and query builder methods?
A Query builders operate at the database level (e.g., filtering with 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.
Q What happens to model relationships after a soft delete?
A A soft delete only sets the 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.
Q How do I choose between Eloquent and Query Builder?
A Use Eloquent when you need model instances (to access associations or use traits); use Query Builder (DB::table()) when you just need simple query results and don't need models. Eloquent is essentially a wrapper around the Query Builder.
Q What is the difference between batch updates and individual updates?
A 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


📝 Exercises

  1. Basic Exercise (⭐): Create a Product model for ShopMetrics, define $fillable and $casts, implement complete CRUD operations (create, read, update, delete), and use Tinker to validate each operation.

  2. 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 selectSub subquery and the map method of the Collection to format the output.

  3. 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.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏