Advanced Eloquent - Scopes, Accessors, Observers
Master query scopes, attribute accessors/mutators, and model observers for cleaner code
Advanced Eloquent features help you write DRY (Don't Repeat Yourself) code by encapsulating common queries and business logic directly in your models. These features make your code more maintainable and expressive.
Query Scopes:
Reusable query constraints that you can chain like any Eloquent method. Instead of repeating ->where('published', true) everywhere, create a scope: ->published()
Accessors & Mutators:
Automatically transform attributes when getting or setting them. Perfect for formatting names, encrypting data, or combining fields.
Observers:
Listen to model events (creating, created, updating, updated, deleting, deleted) without cluttering your controllers. Ideal for sending emails, clearing caches, or logging changes.
Real-World Impact:
Instead of scattered business logic in controllers, you centralize it in models. This makes testing easier and ensures consistency across your application.
Query Scopes:
Reusable query constraints that you can chain like any Eloquent method. Instead of repeating ->where('published', true) everywhere, create a scope: ->published()
Accessors & Mutators:
Automatically transform attributes when getting or setting them. Perfect for formatting names, encrypting data, or combining fields.
Observers:
Listen to model events (creating, created, updating, updated, deleting, deleted) without cluttering your controllers. Ideal for sending emails, clearing caches, or logging changes.
Real-World Impact:
Instead of scattered business logic in controllers, you centralize it in models. This makes testing easier and ensures consistency across your application.
Code Examples
Query Scopes - Reusable Filters
💡 Solution
php
<?php
class Post extends Model
{
// Local scope: prefix with scope
public function scopePublished($query)
{
return $query->where('published', true);
}
public function scopeFeatured($query)
{
return $query->where('is_featured', true);
}
public function scopeRecent($query)
{
return $query->orderBy('created_at', 'desc');
}
// Scope with parameters
public function scopeByAuthor($query, $authorId)
{
return $query->where('user_id', $authorId);
}
public function scopePublishedInYear($query, $year)
{
return $query->whereYear('published_at', $year);
}
}
// Usage: Chain scopes like any Eloquent method
$posts = Post::published()->featured()->recent()->get();
// Without scopes (repetitive):
$posts = Post::where('published', true)
->where('is_featured', true)
->orderBy('created_at', 'desc')
->get();
// With parameters:
$posts = Post::byAuthor(5)->published()->get();
$posts = Post::publishedInYear(2024)->recent()->take(10)->get();
// Real-world example: E-commerce
class Product extends Model
{
public function scopeInStock($query)
{
return $query->where('stock', '>', 0);
}
public function scopeOnSale($query)
{
return $query->whereNotNull('sale_price');
}
public function scopePriceBetween($query, $min, $max)
{
return $query->whereBetween('price', [$min, $max]);
}
public function scopeByCategory($query, $categoryId)
{
return $query->where('category_id', $categoryId);
}
}
// Clean, expressive queries:
$products = Product::inStock()
->onSale()
->priceBetween(10, 100)
->byCategory(5)
->get();
💡 Query scopes encapsulate common queries into reusable, chainable methods. They make your code more readable and DRY.
Accessors & Mutators - Attribute Transformation
💡 Solution
php
<?php
class User extends Model
{
// MODERN WAY (Laravel 9+): Using Attribute class
use Illuminate\Database\Eloquent\Casts\Attribute;
// Accessor: Transform when retrieving
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}"
);
}
// Mutator: Transform when setting
protected function password(): Attribute
{
return Attribute::make(
get: fn ($value) => $value, // Return as-is
set: fn ($value) => bcrypt($value) // Hash when setting
);
}
// Both accessor and mutator
protected function email(): Attribute
{
return Attribute::make(
get: fn ($value) => strtolower($value),
set: fn ($value) => strtolower(trim($value))
);
}
}
// Usage:
$user = User::find(1);
echo $user->full_name; // "John Doe" (accessor)
$user->password = 'secret123'; // Automatically hashed (mutator)
$user->save();
$user->email = ' TEST@EXAMPLE.COM '; // Stored as "test@example.com"
// OLD WAY (still works):
class User extends Model
{
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
public function setPasswordAttribute($value)
{
$this->attributes['password'] = bcrypt($value);
}
}
// Real-world examples:
class Product extends Model
{
// Format price with currency
protected function formattedPrice(): Attribute
{
return Attribute::make(
get: fn () => '$' . number_format($this->price, 2)
);
}
// Calculate discount percentage
protected function discountPercent(): Attribute
{
return Attribute::make(
get: fn () => $this->sale_price
? round((($this->price - $this->sale_price) / $this->price) * 100)
: 0
);
}
// Always uppercase SKU
protected function sku(): Attribute
{
return Attribute::make(
get: fn ($value) => strtoupper($value),
set: fn ($value) => strtoupper($value)
);
}
}
$product = Product::find(1);
echo $product->formatted_price; // "$99.99"
echo $product->discount_percent; // 25
💡 Accessors transform attributes when reading, mutators transform when writing. Perfect for formatting, calculations, or data sanitization.
Model Observers - Centralized Event Handling
💡 Solution
php
<?php
// Create an observer: php artisan make:observer UserObserver --model=User
namespace App\Observers;
use App\Models\User;
use Illuminate\Support\Str;
class UserObserver
{
// Called before creating
public function creating(User $user): void
{
// Generate UUID if not set
if (empty($user->uuid)) {
$user->uuid = Str::uuid();
}
}
// Called after created
public function created(User $user): void
{
// Send welcome email
Mail::to($user->email)->send(new WelcomeEmail($user));
// Create default settings
$user->settings()->create([
'theme' => 'light',
'notifications_enabled' => true,
]);
// Log the event
Log::info("New user registered: {$user->email}");
}
// Called before updating
public function updating(User $user): void
{
// If email changed, mark as unverified
if ($user->isDirty('email')) {
$user->email_verified_at = null;
}
}
// Called after updated
public function updated(User $user): void
{
// Clear cache
Cache::forget("user.{$user->id}");
// If role changed, update permissions cache
if ($user->wasChanged('role_id')) {
Cache::forget("user.{$user->id}.permissions");
}
}
// Called before deleting
public function deleting(User $user): void
{
// Delete related data
$user->posts()->delete();
$user->comments()->delete();
}
// Called after deleted
public function deleted(User $user): void
{
Log::info("User deleted: {$user->email}");
}
}
// Register observer in AppServiceProvider:
use App\Models\User;
use App\Observers\UserObserver;
public function boot(): void
{
User::observe(UserObserver::class);
}
// Now all these events fire automatically:
$user = User::create([...]); // Fires: creating → created
$user->update([...]); // Fires: updating → updated
$user->delete(); // Fires: deleting → deleted
// Real-world example: Product observer
class ProductObserver
{
public function creating(Product $product): void
{
// Auto-generate slug from title
$product->slug = Str::slug($product->title);
}
public function updated(Product $product): void
{
// If price changed, notify subscribers
if ($product->wasChanged('price')) {
event(new ProductPriceChanged($product));
}
// Clear product cache
Cache::tags(['products'])->flush();
}
public function deleted(Product $product): void
{
// Delete product images from storage
Storage::delete($product->images->pluck('path')->toArray());
}
}
💡 Observers listen to model events and perform actions automatically. They keep your controllers clean and ensure consistent behavior.