Introduction to Eloquent ORM
Eloquent is Laravel's implementation of the Active Record pattern, providing an elegant and intuitive way to interact with your database. Each database table has a corresponding "Model" that is used to interact with that table.
Think of Eloquent models as intelligent representatives of your data tables - they not only know how to retrieve and store data but also understand the relationships between different data entities and enforce business rules.
The power of Eloquent comes from its ability to abstract away the complexity of SQL queries while still offering the flexibility to write custom queries when needed. It's like having both an automatic transmission (for everyday driving) and a manual mode (for when you need precise control) in a high-performance vehicle.
Creating Your First Model
Laravel provides an Artisan command to generate models:
# Generate a basic model
php artisan make:model Product
# Generate a model with a migration
php artisan make:model Product -m
# Generate a model with multiple resources
php artisan make:model Product -mfsc
# -m = migration, -f = factory, -s = seeder, -c = controller
# Generate a model in a specific directory
php artisan make:model Models/Product
The generated model will be placed in the app/Models directory (or app/ directory in older Laravel versions).
Basic Model Structure
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
// Model properties and methods go here
}
This simple class definition gives you access to a powerful set of features for database interaction. It's like having a specialized assistant who knows exactly how to manage a specific type of data.
Table Names and Primary Keys
By default, Eloquent uses certain conventions to determine the database table associated with your model:
Table Name Convention
Eloquent will use the "snake_case", plural name of the class as the table name. For example:
Usermodel →userstableProductmodel →productstableOrderItemmodel →order_itemstable
Customizing the Table Name
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'store_products';
}
Primary Key Convention
By default, Eloquent expects an auto-incrementing integer column named id as the primary key.
Customizing the Primary Key
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
/**
* The primary key for the model.
*
* @var string
*/
protected $primaryKey = 'product_id';
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = false;
/**
* The data type of the auto-incrementing ID.
*
* @var string
*/
protected $keyType = 'string';
}
These conventions follow the "convention over configuration" principle - sensible defaults that can be overridden when needed. It's like having presets on your camera that work well for most situations but can be adjusted for specific needs.
Timestamps and Model Properties
Timestamps
By default, Eloquent expects created_at and updated_at columns to exist on your tables. These will be automatically maintained when models are created or updated.
// Disable timestamps
protected $timestamps = false;
// Customize timestamp column names
const CREATED_AT = 'creation_date';
const UPDATED_AT = 'last_update';
// Customize timestamp format
protected $dateFormat = 'U'; // Store as UNIX timestamps
Default Attribute Values
You can define default values for model attributes:
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'status' => 'pending',
'is_featured' => false,
'discount' => 0,
];
This ensures that newly created models have these default values if not explicitly set. It's like having pre-filled form fields that you can override if needed.
Mass Assignment Protection
Mass assignment is a convenient way to set multiple model attributes at once:
// Create a new model with multiple attributes
$product = new Product([
'name' => 'Laptop',
'price' => 999.99,
'description' => 'Powerful laptop with the latest specs'
]);
$product->save();
// Or use the create method
$product = Product::create([
'name' => 'Smartphone',
'price' => 599.99,
'description' => 'Latest smartphone model'
]);
However, this can be a security risk if not properly handled. For instance, a malicious user might try to set attributes that they shouldn't have access to (like is_admin or approved).
Eloquent provides two approaches to protect against this:
Fillable Attributes
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'price',
'description',
'category_id',
'stock_quantity'
];
Guarded Attributes
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [
'id',
'is_admin',
'approved'
];
// Or to make all attributes mass assignable:
protected $guarded = [];
Think of $fillable as a whitelist (only these fields can be mass-assigned) and $guarded as a blacklist (all fields except these can be mass-assigned). It's analogous to a security checkpoint where you either have a list of approved visitors or a list of people to watch out for.
As a best practice, using $fillable is generally safer as it's explicit about what can be mass-assigned.
Basic CRUD Operations
Creating Records
// Method 1: Create and save
$product = new Product;
$product->name = 'Wireless Headphones';
$product->price = 149.99;
$product->description = 'Noise-cancelling wireless headphones';
$product->save();
// Method 2: Mass assignment with create
$product = Product::create([
'name' => 'Smart Watch',
'price' => 249.99,
'description' => 'Fitness tracking smart watch'
]);
// Method 3: firstOrCreate - find if exists, create if doesn't
$product = Product::firstOrCreate(
['sku' => 'HEAD-001'], // Find by these attributes
[
'name' => 'Wireless Headphones',
'price' => 149.99,
'description' => 'Noise-cancelling wireless headphones'
] // Set these attributes if creating
);
// Method 4: updateOrCreate - update if exists, create if doesn't
$product = Product::updateOrCreate(
['sku' => 'HEAD-001'], // Find by these attributes
[
'name' => 'Wireless Headphones',
'price' => 129.99 // Updated price
] // Update or set these attributes
);
Reading Records
// Retrieving all records
$products = Product::all();
// Retrieving a single record by primary key
$product = Product::find(1);
// Retrieving the first record matching conditions
$product = Product::where('sku', 'HEAD-001')->first();
// With a fallback for not found
$product = Product::findOrFail(1); // Throws ModelNotFoundException if not found
$product = Product::firstOrFail(['sku' => 'HEAD-001']); // Throws exception if not found
Updating Records
// Method 1: Find and update
$product = Product::find(1);
$product->price = 139.99;
$product->save();
// Method 2: Update directly
Product::where('id', 1)->update(['price' => 139.99]);
// Method 3: Mass update
$product = Product::find(1);
$product->update([
'price' => 139.99,
'stock_quantity' => 50
]);
Deleting Records
// 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]); // Multiple IDs
// Method 3: Delete by conditions
Product::where('stock_quantity', 0)->delete();
These CRUD operations abstract away the SQL queries, making your code more expressive and focused on the business logic rather than data access details. It's like having a concierge service for your data - you declare what you want, and Eloquent handles the details of how to get it.
Model Methods and Accessors
Custom Methods
You can add custom methods to your models to encapsulate business logic:
class Product extends Model
{
// ... other properties ...
/**
* Determine if the product is in stock.
*
* @return bool
*/
public function isInStock()
{
return $this->stock_quantity > 0;
}
/**
* Apply a discount to the product.
*
* @param float $percentage
* @return void
*/
public function applyDiscount($percentage)
{
$this->sale_price = $this->price * (1 - ($percentage / 100));
$this->discount_percentage = $percentage;
$this->save();
}
/**
* Get products in the same category.
*
* @param int $limit
* @return \Illuminate\Database\Eloquent\Collection
*/
public function similarProducts($limit = 4)
{
return static::where('category_id', $this->category_id)
->where('id', '!=', $this->id)
->limit($limit)
->get();
}
}
These methods encapsulate related functionality directly in the model, making your code more maintainable and expressive. It's like adding specialized tools to the model's toolkit that are relevant to that specific type of data.
Accessors
Accessors let you transform model attributes when accessing them:
Using the get{Attribute}Attribute Pattern (Laravel 8 and earlier)
/**
* Get the formatted price.
*
* @return string
*/
public function getFormattedPriceAttribute()
{
return '$' . number_format($this->price, 2);
}
// Usage
echo $product->formatted_price; // Outputs: $149.99
Using Accessor Methods (Laravel 9+)
use Illuminate\Database\Eloquent\Casts\Attribute;
/**
* Get the formatted price.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function formattedPrice(): Attribute
{
return Attribute::make(
get: fn () => '$' . number_format($this->price, 2),
);
}
// Usage
echo $product->formatted_price; // Outputs: $149.99
Accessors are like having automated format converters - they take the raw data and present it in a more user-friendly format without changing the underlying stored value.
Mutators and Attribute Casting
Mutators
Mutators let you transform attributes before saving them to the database:
Using the set{Attribute}Attribute Pattern (Laravel 8 and earlier)
/**
* Set the product name with automatic slug generation.
*
* @param string $value
* @return void
*/
public function setNameAttribute($value)
{
$this->attributes['name'] = $value;
$this->attributes['slug'] = Str::slug($value);
}
// Usage
$product->name = 'Wireless Headphones'; // Also sets slug to "wireless-headphones"
Using Mutator Methods (Laravel 9+)
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Str;
/**
* Handle name with automatic slug generation.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function name(): Attribute
{
return Attribute::make(
get: fn ($value) => $value,
set: function ($value) {
return [
'name' => $value,
'slug' => Str::slug($value),
];
},
);
}
// Usage
$product->name = 'Wireless Headphones'; // Also sets slug to "wireless-headphones"
Mutators act like preprocessing filters - they catch data before it's stored and transform it according to your requirements. This ensures consistency and can automate related field updates.
Attribute Casting
Eloquent allows you to automatically cast attributes to common data types:
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'price' => 'float',
'is_featured' => 'boolean',
'options' => 'array',
'published_at' => 'datetime',
'metadata' => 'json',
'dimensions' => 'object',
'sale_ends_at' => 'datetime:Y-m-d',
];
Available cast types include: integer, real, float, double, decimal:<digits>, string, boolean, object, array, collection, date, datetime, timestamp, json, and more.
Casting is like having automatic data converters - the database might store everything as strings, but casting ensures you're working with the appropriate PHP data types in your application code.
Example with JSON Casting
// Database field 'options' stores: {"color":"red","size":"large"}
// With casting
$product->options['color'] = 'blue';
$product->save();
// Without casting, you'd need to do:
$options = json_decode($product->options, true);
$options['color'] = 'blue';
$product->options = json_encode($options);
$product->save();
Custom Casting
For more complex casting needs, you can create custom cast classes:
Creating a Custom Cast
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use InvalidArgumentException;
class Money implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return \App\ValueObjects\Money
*/
public function get($model, $key, $value, $attributes)
{
// Convert from cents to dollars when retrieving
return number_format($value / 100, 2);
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return array
*/
public function set($model, $key, $value, $attributes)
{
// Convert from dollars to cents for storage
return [$key => (int) ($value * 100)];
}
}
Using the Custom Cast
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'price' => \App\Casts\Money::class,
];
Custom casts are extremely powerful for handling complex data types or transformations. They're like having specialized translators for unique data formats or business objects.
Model Events
Eloquent models fire events during various lifecycle stages, allowing you to hook into these moments:
Listening to Events Using Model Hooks
class Product extends Model
{
// ... other properties and methods ...
/**
* The "booted" method of the model.
*
* @return void
*/
protected static function booted()
{
// Before a model is created
static::creating(function ($product) {
$product->slug = Str::slug($product->name);
});
// After a model is created
static::created(function ($product) {
// Notify inventory system
InventorySystem::notifyNewProduct($product);
});
// Before a model is updated
static::updating(function ($product) {
// Record the original price if it's changing
if ($product->isDirty('price')) {
PriceHistory::create([
'product_id' => $product->id,
'old_price' => $product->getOriginal('price'),
'new_price' => $product->price
]);
}
});
// Before a model is deleted
static::deleting(function ($product) {
// Remove related images
$product->images()->delete();
});
}
}
Available Events
retrieved: After a model is retrieved from the databasecreating/created: Before/after a model is createdupdating/updated: Before/after a model is updatedsaving/saved: Before/after a model is saved (created or updated)deleting/deleted: Before/after a model is deletedrestoring/restored: Before/after a soft-deleted model is restored
Model events are like having automated triggers or hooks throughout a model's lifecycle. They're similar to home automation that performs specific actions when certain events occur (like lights turning on when motion is detected).
Model Observers
For more complex event handling, you can create dedicated observer classes:
Creating an Observer
# Generate an observer
php artisan make:observer ProductObserver --model=Product
<?php
namespace App\Observers;
use App\Models\Product;
use Illuminate\Support\Str;
class ProductObserver
{
/**
* Handle the Product "creating" event.
*
* @param \App\Models\Product $product
* @return void
*/
public function creating(Product $product)
{
$product->slug = Str::slug($product->name);
}
/**
* Handle the Product "created" event.
*
* @param \App\Models\Product $product
* @return void
*/
public function created(Product $product)
{
// Notify inventory system
InventorySystem::notifyNewProduct($product);
}
/**
* Handle the Product "updating" event.
*
* @param \App\Models\Product $product
* @return void
*/
public function updating(Product $product)
{
if ($product->isDirty('price')) {
PriceHistory::create([
'product_id' => $product->id,
'old_price' => $product->getOriginal('price'),
'new_price' => $product->price
]);
}
}
/**
* Handle the Product "deleted" event.
*
* @param \App\Models\Product $product
* @return void
*/
public function deleted(Product $product)
{
// Clean up related resources
$product->images()->delete();
}
}
Registering the Observer
// In App\Providers\EventServiceProvider
use App\Models\Product;
use App\Observers\ProductObserver;
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
Product::observe(ProductObserver::class);
}
Observers provide a cleaner way to organize complex event handling logic, especially when you have multiple events to handle for a model. They're like having dedicated event managers that monitor and respond to specific model activities.
Real-World Example: E-commerce Product Model
Let's create a comprehensive Product model for an e-commerce application:
<?php
namespace App\Models;
use App\Casts\Money;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class Product extends Model
{
use HasFactory, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'slug',
'description',
'price',
'sale_price',
'sku',
'stock_quantity',
'category_id',
'brand_id',
'is_featured',
'is_active',
'metadata'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'price' => Money::class,
'sale_price' => Money::class,
'is_featured' => 'boolean',
'is_active' => 'boolean',
'metadata' => 'array',
'published_at' => 'datetime',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'cost_price',
'profit_margin'
];
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'is_featured' => false,
'is_active' => true,
'stock_quantity' => 0,
];
/**
* The "booted" method of the model.
*
* @return void
*/
protected static function booted()
{
static::creating(function ($product) {
if (empty($product->slug)) {
$product->slug = Str::slug($product->name);
}
if (empty($product->sku)) {
$product->sku = static::generateSku($product);
}
});
static::updating(function ($product) {
// If name is changed but slug isn't explicitly set, update the slug
if ($product->isDirty('name') && !$product->isDirty('slug')) {
$product->slug = Str::slug($product->name);
}
// Track price changes
if ($product->isDirty('price')) {
PriceHistory::create([
'product_id' => $product->id,
'old_price' => $product->getOriginal('price'),
'new_price' => $product->price,
'changed_by' => auth()->id() ?? 1
]);
}
});
}
/**
* Generate a unique SKU for a product.
*
* @param \App\Models\Product $product
* @return string
*/
protected static function generateSku($product)
{
$prefix = substr(strtoupper(Str::slug($product->name)), 0, 3);
$suffix = strtoupper(Str::random(5));
return "{$prefix}-{$suffix}";
}
/**
* Determine if the product is on sale.
*
* @return bool
*/
public function isOnSale()
{
return $this->sale_price && $this->sale_price < $this->price;
}
/**
* Get the discount percentage if the product is on sale.
*
* @return float|null
*/
public function getDiscountPercentageAttribute()
{
if ($this->isOnSale()) {
return round((($this->price - $this->sale_price) / $this->price) * 100);
}
return null;
}
/**
* Get the current price (sale price if on sale, regular price otherwise).
*
* @return float
*/
public function getCurrentPriceAttribute()
{
return $this->isOnSale() ? $this->sale_price : $this->price;
}
/**
* Determine if the product is in stock.
*
* @return bool
*/
public function isInStock()
{
return $this->stock_quantity > 0;
}
/**
* Get the formatted current price.
*
* @return string
*/
public function getFormattedPriceAttribute()
{
return '$' . $this->current_price;
}
/**
* Apply a discount to the product.
*
* @param float $percentage
* @return $this
*/
public function applyDiscount($percentage)
{
$this->sale_price = $this->price * (1 - ($percentage / 100));
$this->save();
return $this;
}
/**
* Remove the sale price.
*
* @return $this
*/
public function removeDiscount()
{
$this->sale_price = null;
$this->save();
return $this;
}
/**
* Update the stock quantity.
*
* @param int $quantity
* @return $this
*/
public function updateStock($quantity)
{
$this->stock_quantity = $quantity;
$this->save();
return $this;
}
/**
* Decrease the stock quantity.
*
* @param int $quantity
* @return $this
*/
public function decreaseStock($quantity = 1)
{
$this->stock_quantity = max(0, $this->stock_quantity - $quantity);
$this->save();
return $this;
}
/**
* Increase the stock quantity.
*
* @param int $quantity
* @return $this
*/
public function increaseStock($quantity = 1)
{
$this->stock_quantity += $quantity;
$this->save();
return $this;
}
/**
* Scope a query to only include active products.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('is_active', true);
}
/**
* Scope a query to only include featured products.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeFeatured($query)
{
return $query->where('is_featured', true);
}
/**
* Scope a query to only include products on sale.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeOnSale($query)
{
return $query->whereNotNull('sale_price')
->whereColumn('sale_price', '<', 'price');
}
/**
* Scope a query to only include products in stock.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeInStock($query)
{
return $query->where('stock_quantity', '>', 0);
}
/**
* Scope a query to filter products by price range.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param float $min
* @param float $max
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopePriceRange($query, $min, $max)
{
return $query->where(function ($query) use ($min, $max) {
$query->whereBetween('price', [$min, $max])
->orWhereBetween('sale_price', [$min, $max]);
});
}
/**
* Get the category that owns the product.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function category()
{
return $this->belongsTo(Category::class);
}
/**
* Get the brand that owns the product.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function brand()
{
return $this->belongsTo(Brand::class);
}
/**
* Get the images for the product.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function images()
{
return $this->hasMany(ProductImage::class);
}
/**
* Get the variants for the product.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function variants()
{
return $this->hasMany(ProductVariant::class);
}
/**
* Get the reviews for the product.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function reviews()
{
return $this->hasMany(Review::class);
}
/**
* Get the tags for the product.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function tags()
{
return $this->belongsToMany(Tag::class);
}
/**
* Get the average rating for the product.
*
* @return float
*/
public function getAverageRatingAttribute()
{
return $this->reviews()->avg('rating') ?: 0;
}
/**
* Get similar products based on category.
*
* @param int $limit
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getSimilarProducts($limit = 4)
{
return static::active()
->where('category_id', $this->category_id)
->where('id', '!=', $this->id)
->inStock()
->limit($limit)
->get();
}
/**
* Generate the full product URL.
*
* @return string
*/
public function getUrlAttribute()
{
return route('products.show', $this->slug);
}
}
This comprehensive model demonstrates many of the concepts we've covered:
- Mass assignment protection with
$fillable - Attribute casting for various data types
- Hidden attributes for sensitive data
- Default attribute values
- Event listeners for automatic slug and SKU generation
- Custom accessors for formatted prices and calculated fields
- Helper methods for common operations (apply discount, update stock)
- Query scopes for common filtering conditions
- Relationship definitions for connected models
The model serves as a central hub for product-related functionality, combining data access with business logic relevant to products.
Practical Usage Patterns
Repository Pattern
For complex applications, you might want to abstract model interactions further using the repository pattern:
<?php
namespace App\Repositories;
use App\Models\Product;
class ProductRepository
{
/**
* Get all active products.
*
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getAllActive()
{
return Product::active()->orderBy('name')->get();
}
/**
* Get featured products.
*
* @param int $limit
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getFeatured($limit = 8)
{
return Product::active()->featured()->inStock()->limit($limit)->get();
}
/**
* Get products on sale.
*
* @param int $limit
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getOnSale($limit = 8)
{
return Product::active()->onSale()->inStock()->limit($limit)->get();
}
/**
* Find a product by its slug.
*
* @param string $slug
* @return \App\Models\Product|null
*/
public function findBySlug($slug)
{
return Product::where('slug', $slug)->first();
}
/**
* Search for products.
*
* @param string $term
* @return \Illuminate\Database\Eloquent\Collection
*/
public function search($term)
{
return Product::active()
->where(function ($query) use ($term) {
$query->where('name', 'like', "%{$term}%")
->orWhere('description', 'like', "%{$term}%")
->orWhere('sku', 'like', "%{$term}%");
})
->orderBy('name')
->get();
}
/**
* Create a new product.
*
* @param array $data
* @return \App\Models\Product
*/
public function create(array $data)
{
return Product::create($data);
}
/**
* Update a product.
*
* @param \App\Models\Product $product
* @param array $data
* @return \App\Models\Product
*/
public function update(Product $product, array $data)
{
$product->update($data);
return $product;
}
/**
* Delete a product.
*
* @param \App\Models\Product $product
* @return bool|null
*/
public function delete(Product $product)
{
return $product->delete();
}
}
This pattern adds a layer of abstraction between your controllers and models, allowing for more complex business logic and making your code more testable.
Service Pattern
For operations that span multiple models or involve complex logic, a service class can be useful:
<?php
namespace App\Services;
use App\Models\Product;
use App\Models\Order;
use App\Models\OrderItem;
use App\Exceptions\InsufficientStockException;
class OrderService
{
/**
* Create a new order.
*
* @param array $orderData
* @param array $items
* @return \App\Models\Order
* @throws \App\Exceptions\InsufficientStockException
*/
public function createOrder(array $orderData, array $items)
{
// Verify stock for all products
foreach ($items as $item) {
$product = Product::findOrFail($item['product_id']);
if ($product->stock_quantity < $item['quantity']) {
throw new InsufficientStockException(
"Insufficient stock for product: {$product->name}"
);
}
}
// Start DB transaction
return \DB::transaction(function () use ($orderData, $items) {
// Create the order
$order = Order::create($orderData);
$total = 0;
// Add order items and update stock
foreach ($items as $item) {
$product = Product::findOrFail($item['product_id']);
// Create order item
OrderItem::create([
'order_id' => $order->id,
'product_id' => $product->id,
'quantity' => $item['quantity'],
'unit_price' => $product->current_price,
'total' => $product->current_price * $item['quantity']
]);
// Update product stock
$product->decreaseStock($item['quantity']);
// Add to order total
$total += $product->current_price * $item['quantity'];
}
// Update order total
$order->update(['total' => $total]);
return $order;
});
}
}
This pattern is ideal for complex operations that involve multiple models or need transaction support.
Practice Activity
Basic Model Creation
Create a Category model for an e-commerce system that:
- Has fillable fields for name, slug, description, and parent_id
- Uses an event listener to automatically generate a slug if one isn't provided
- Includes at least two accessors for formatted data
- Has a method to get all parent categories in a hierarchy
- Includes at least two query scopes for common filtering operations
Model Relationships
Extend the Category model to include:
- A self-referential parent/child relationship (categories can have parent categories)
- A relationship to the Product model
- A method to get all products in this category and its subcategories
Advanced Model Features
Create a Coupon model with:
- Fields for code, discount_type (percentage/fixed), discount_amount, starts_at, expires_at, and usage_limit
- A custom cast for the discount_amount field that handles percentage vs. fixed amount formatting
- Methods to validate if a coupon is valid, has expired, or exceeded usage limits
- A method to apply the coupon to a given order total
- Event listeners to log coupon creation and usage
Summary
- Eloquent models provide an elegant way to interact with database tables
- Models follow conventions but can be customized for table names, primary keys, and timestamps
- Mass assignment protection using
$fillableor$guardedproperties - Models support basic CRUD operations with expressive, fluent interfaces
- Accessors and mutators allow transforming attributes when retrieving or setting them
- Attribute casting converts database values to appropriate PHP types
- Model events provide hooks into the model lifecycle for automated actions
- Observers organize event handling logic for complex models
- Custom methods, scopes, and relationships enhance models with domain-specific functionality
In the next lecture, we'll explore model relationships in depth, examining how Eloquent models connect to each other to model complex data structures.