Eloquent ORM Basics
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.
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 = [];
}
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();
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'
Create a Post model with the following features:
- Fillable attributes: title, slug, content, excerpt, is_published, published_at
- Soft deletes enabled
- Cast
is_publishedto boolean - Cast
published_atto datetime - Create a scope
published()that filters published posts - Create a scope
recent()that orders by published_at descending - Create an accessor for
excerptthat limits it to 150 characters
Add the following scopes to your Product model:
scopeLowStock($query, $threshold = 10)- products with quantity below thresholdscopeDiscount($query, $percentage)- products with discount >= percentagescopeCategory($query, $categoryId)- products in a specific categoryscopeSearch($query, $term)- search in name and description
Then write queries that combine multiple scopes.
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
$fillableor$guardedto 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!