Laravel Framework

Eloquent ORM Basics

20 min Lesson 7 of 45

Eloquent ORM Basics

Eloquent is Laravel's powerful and elegant Object-Relational Mapping (ORM) system. It provides a beautiful, simple ActiveRecord implementation for working with your database. Each database table has a corresponding "Model" that is used to interact with that table.

What is an ORM?

An ORM (Object-Relational Mapping) allows you to interact with your database using objects and methods instead of writing raw SQL queries. Eloquent makes database interactions feel natural and intuitive.

Tip: With Eloquent, you write less code and achieve more. Instead of writing SQL queries manually, you use expressive PHP methods that are easier to read and maintain.

Creating Your First Model

Use the Artisan command to generate a new model:

# Create a model
php artisan make:model Product

# Create a model with migration
php artisan make:model Product -m

# Create a model with migration, factory, and seeder
php artisan make:model Product -mfs

# Create a model with all options
php artisan make:model Product --all

This creates a model file in app/Models/Product.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;
}

Eloquent Naming Conventions

Eloquent follows naming conventions that reduce configuration. Understanding these conventions helps you write cleaner code:

// Table Name Convention
// Model: Product → Table: products (plural, lowercase)
// Model: Category → Table: categories
// Model: OrderItem → Table: order_items (snake_case)

// Primary Key Convention
// Default: 'id' column (auto-increment integer)

// Timestamp Convention
// Default: created_at and updated_at columns

Customizing Conventions

You can override these conventions when needed:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    // Custom table name
    protected $table = 'my_products';

    // Custom primary key
    protected $primaryKey = 'product_id';

    // Non-incrementing primary key
    public $incrementing = false;

    // Non-integer primary key (like UUID)
    protected $keyType = 'string';

    // Disable timestamps
    public $timestamps = false;

    // Custom timestamp column names
    const CREATED_AT = 'creation_date';
    const UPDATED_AT = 'updated_date';

    // Custom database connection
    protected $connection = 'mysql2';
}

Mass Assignment Protection

Mass assignment is a security feature that protects against vulnerabilities. You must specify which attributes can be mass assigned:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    // Fillable: whitelist approach (recommended)
    protected $fillable = [
        'name',
        'description',
        'price',
        'quantity',
        'category_id',
    ];

    // Guarded: blacklist approach
    // protected $guarded = ['id', 'created_at'];

    // Allow all attributes (NOT RECOMMENDED)
    // protected $guarded = [];
}
Warning: Never set protected $guarded = []; without understanding the security implications. Always explicitly define fillable attributes or carefully guard sensitive fields.

CRUD Operations with Eloquent

Creating Records

<?php

use App\Models\Product;

// Method 1: Create and save
$product = new Product;
$product->name = 'Laptop';
$product->description = 'High-performance laptop';
$product->price = 999.99;
$product->quantity = 50;
$product->save();

// Method 2: Mass assignment with create()
$product = Product::create([
    'name' => 'Mouse',
    'description' => 'Wireless mouse',
    'price' => 29.99,
    'quantity' => 100,
]);

// Method 3: firstOrCreate() - find or create
$product = Product::firstOrCreate(
    ['name' => 'Keyboard'],
    ['price' => 79.99, 'quantity' => 30]
);

// Method 4: updateOrCreate() - find and update or create
$product = Product::updateOrCreate(
    ['name' => 'Monitor'],
    ['price' => 299.99, 'quantity' => 20]
);

Reading Records

<?php

use App\Models\Product;

// Retrieve all records
$products = Product::all();

// Find by primary key
$product = Product::find(1);

// Find or fail (throws 404 if not found)
$product = Product::findOrFail(1);

// Find by other column
$product = Product::where('name', 'Laptop')->first();

// Get multiple records with conditions
$products = Product::where('price', '<', 100)->get();

// Count records
$count = Product::where('quantity', '>', 0)->count();

// Check if record exists
$exists = Product::where('name', 'Laptop')->exists();

// Get maximum/minimum values
$maxPrice = Product::max('price');
$minPrice = Product::min('price');

// Get sum/average
$totalValue = Product::sum('price');
$avgPrice = Product::avg('price');

Updating Records

<?php

use App\Models\Product;

// Method 1: Find and update
$product = Product::find(1);
$product->price = 899.99;
$product->save();

// Method 2: Mass update
Product::where('quantity', '<', 10)
    ->update(['status' => 'low_stock']);

// Method 3: Update or create
$product = Product::updateOrCreate(
    ['name' => 'Tablet'],
    ['price' => 499.99, 'quantity' => 25]
);

// Increment/decrement
$product = Product::find(1);
$product->increment('quantity');        // Add 1
$product->increment('quantity', 5);     // Add 5
$product->decrement('quantity', 3);     // Subtract 3

// Increment with additional updates
$product->increment('views', 1, [
    'last_viewed_at' => now()
]);

Deleting Records

<?php

use App\Models\Product;

// Method 1: Find and delete
$product = Product::find(1);
$product->delete();

// Method 2: Delete by primary key
Product::destroy(1);
Product::destroy([1, 2, 3]);

// Method 3: Delete with conditions
Product::where('quantity', 0)->delete();

// Force delete (bypass soft deletes)
$product->forceDelete();

Query Scopes

Scopes allow you to define common query constraints that you can easily reuse throughout your application.

Local Scopes

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = ['name', 'price', 'quantity', 'is_active'];

    // Scope: active products
    public function scopeActive($query)
    {
        return $query->where('is_active', true);
    }

    // Scope: products in stock
    public function scopeInStock($query)
    {
        return $query->where('quantity', '>', 0);
    }

    // Scope: expensive products (with parameter)
    public function scopeExpensive($query, $minPrice = 100)
    {
        return $query->where('price', '>=', $minPrice);
    }

    // Scope: price range
    public function scopePriceBetween($query, $min, $max)
    {
        return $query->whereBetween('price', [$min, $max]);
    }
}

Using scopes in your queries:

// Use single scope
$products = Product::active()->get();

// Chain multiple scopes
$products = Product::active()
                   ->inStock()
                   ->expensive(200)
                   ->get();

// Combine with other query methods
$products = Product::active()
                   ->priceBetween(50, 200)
                   ->orderBy('name')
                   ->limit(10)
                   ->get();

Soft Deletes

Soft deleting allows you to "delete" records without actually removing them from the database. They are marked with a deleted_at timestamp.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Product extends Model
{
    use SoftDeletes;

    protected $fillable = ['name', 'price', 'quantity'];
}

// Using soft deletes
$product = Product::find(1);
$product->delete();  // Sets deleted_at timestamp

// Queries automatically exclude soft deleted records
$products = Product::all();  // Only non-deleted

// Include soft deleted records
$products = Product::withTrashed()->get();

// Get only soft deleted records
$products = Product::onlyTrashed()->get();

// Restore soft deleted record
$product = Product::withTrashed()->find(1);
$product->restore();

// Force delete (permanent)
$product->forceDelete();
Note: Don't forget to add the deleted_at column to your migration using $table->softDeletes();

Accessors and Mutators

Accessors allow you to format Eloquent attribute values when retrieving them. Mutators allow you to format values when setting them.

Accessors (Getters)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;

class Product extends Model
{
    // Accessor for name (automatically uppercase)
    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => strtoupper($value),
        );
    }

    // Accessor for price (formatted)
    protected function price(): Attribute
    {
        return Attribute::make(
            get: fn (float $value) => number_format($value, 2),
        );
    }

    // Virtual accessor (computed attribute)
    protected function fullDescription(): Attribute
    {
        return Attribute::make(
            get: fn () => "{$this->name} - \${$this->price}",
        );
    }
}

// Usage
$product = Product::find(1);
echo $product->name;              // LAPTOP
echo $product->price;             // 999.99
echo $product->full_description;  // LAPTOP - $999.99

Mutators (Setters)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;

class Product extends Model
{
    // Mutator for name (store as lowercase)
    protected function name(): Attribute
    {
        return Attribute::make(
            get: fn (string $value) => ucfirst($value),
            set: fn (string $value) => strtolower($value),
        );
    }

    // Mutator for price (ensure positive value)
    protected function price(): Attribute
    {
        return Attribute::make(
            set: fn (float $value) => max(0, $value),
        );
    }
}

// Usage
$product = new Product;
$product->name = 'GAMING MOUSE';  // Stored as 'gaming mouse'
$product->price = -50;             // Stored as 0

Attribute Casting

Cast attributes to common data types automatically:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $casts = [
        'price' => 'decimal:2',
        'quantity' => 'integer',
        'is_active' => 'boolean',
        'options' => 'array',
        'specifications' => 'json',
        'published_at' => 'datetime',
        'discount_rate' => 'float',
    ];
}

// Usage
$product = Product::find(1);

// Boolean cast
if ($product->is_active) {
    // Works with 0/1, true/false, 'yes'/'no'
}

// Array cast (stored as JSON)
$product->options = ['color' => 'red', 'size' => 'large'];
$product->save();

// Retrieve array
$color = $product->options['color'];

// DateTime cast
$published = $product->published_at;
echo $published->format('Y-m-d');
echo $published->diffForHumans();  // '2 days ago'
Exercise 1: Create a Post Model

Create a Post model with the following features:

  • Fillable attributes: title, slug, content, excerpt, is_published, published_at
  • Soft deletes enabled
  • Cast is_published to boolean
  • Cast published_at to datetime
  • Create a scope published() that filters published posts
  • Create a scope recent() that orders by published_at descending
  • Create an accessor for excerpt that limits it to 150 characters
Exercise 2: Implement Query Scopes

Add the following scopes to your Product model:

  • scopeLowStock($query, $threshold = 10) - products with quantity below threshold
  • scopeDiscount($query, $percentage) - products with discount >= percentage
  • scopeCategory($query, $categoryId) - products in a specific category
  • scopeSearch($query, $term) - search in name and description

Then write queries that combine multiple scopes.

Exercise 3: CRUD Operations Practice

Create a simple product management system with these operations:

  • Create 5 products with different prices and quantities
  • Find all products with price > 100
  • Update the quantity of a specific product
  • Increment the views count for a product
  • Soft delete a product
  • Restore the soft deleted product
  • Permanently delete (force delete) a product
  • Calculate the total inventory value (sum of price * quantity)

Best Practices

  • Always Use Mass Assignment Protection: Define $fillable or $guarded to prevent security vulnerabilities.
  • Use Query Scopes: Encapsulate common queries in scopes for reusability and readability.
  • Leverage Soft Deletes: Use soft deletes for records that may need to be recovered.
  • Cast Attributes: Use attribute casting to ensure data types are correct and consistent.
  • Use Accessors Wisely: Don't perform heavy computations in accessors; they run every time the attribute is accessed.
  • Follow Naming Conventions: Stick to Laravel's conventions to write less configuration code.
  • Use findOrFail(): In controllers, use findOrFail() to automatically return 404 for missing records.

Summary

In this lesson, you've mastered:

  • Creating and configuring Eloquent models
  • Understanding Eloquent naming conventions
  • Implementing mass assignment protection
  • Performing CRUD operations with elegant syntax
  • Creating and using query scopes
  • Implementing soft deletes for safe data removal
  • Using accessors and mutators to format data
  • Casting attributes to appropriate data types

Eloquent makes database operations intuitive and expressive. With these basics mastered, you're ready to explore relationships in the next lesson!