Could we help you? Please click the banners. We are young and desperately need the money
Laravel has always made database interactions and model management accessible, but Laravel 12 introduces refinements to lifecycle hooks and query builder capabilities that can significantly improve how you write code. Whether you're handling complex model logic or optimizing database queries, understanding these features will help you write cleaner, more maintainable applications.
In this post, we'll explore practical use cases for lifecycle hooks and query builder improvements in Laravel 12, focusing on real-world scenarios you'll encounter as a developer.
Lifecycle hooks are methods that execute automatically at specific points in a model's lifecycle. They allow you to trigger custom logic when models are created, updated, deleted, or retrieved from the database. Laravel 12 refines these hooks to make them more intuitive and powerful.
Laravel 12 supports several lifecycle hooks that fire at different stages:
One of the most common use cases for lifecycle hooks is tracking changes to important data. Imagine you're building an e-commerce platform and need to log every time a product's price changes:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['name', 'price', 'description'];
protected static function booted()
{
static::updating(function ($product) {
// Log price changes
if ($product->isDirty('price')) {
$oldPrice = $product->getOriginal('price');
$newPrice = $product->price;
\App\Models\AuditLog::create([
'model' => 'Product',
'model_id' => $product->id,
'action' => 'price_change',
'old_value' => $oldPrice,
'new_value' => $newPrice,
'timestamp' => now(),
]);
}
});
}
}
?>
This approach keeps your audit logic within the model where it belongs, making it automatically applied whenever a product is updated—no need to remember to log changes in your controller.
Another common scenario is automatically generating URL-friendly slugs when a model is created or updated:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class BlogPost extends Model
{
protected $fillable = ['title', 'slug', 'content'];
protected static function booted()
{
static::saving(function ($post) {
if (empty($post->slug) || $post->isDirty('title')) {
$post->slug = Str::slug($post->title);
}
});
}
}
?>
The saving hook triggers before both creation and updates, ensuring your slug is always in sync with the title without requiring manual intervention in your controller.
Lifecycle hooks are also perfect for maintaining relationships when a model changes. For example, updating an organization's status and cascading that change to all its users:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Organization extends Model
{
protected $fillable = ['name', 'status'];
public function users()
{
return $this->hasMany(User::class);
}
protected static function booted()
{
static::updating(function ($org) {
if ($org->isDirty('status') && $org->status === 'inactive') {
// Deactivate all users in this organization
$org->users()->update(['active' => false]);
}
});
}
}
?>
This ensures data consistency across your database without needing to handle this logic separately in your application code.
Laravel 12's query builder improvements make it easier to write complex queries in a readable, maintainable way. These enhancements focus on reducing boilerplate and improving code clarity.
Building flexible queries that filter based on user input is a daily task. Laravel 12 makes this cleaner with improved conditional methods:
<?php
// Old approach - lots of if statements
$query = Product::query();
if ($request->has('category')) {
$query->where('category', $request->category);
}
if ($request->has('min_price')) {
$query->where('price', '>=', $request->min_price);
}
if ($request->has('in_stock')) {
$query->where('stock', '>', 0);
}
$products = $query->get();
// Laravel 12 approach - cleaner and more chainable
$products = Product::query()
->when($request->has('category'), function ($query) use ($request) {
$query->where('category', $request->category);
})
->when($request->has('min_price'), function ($query) use ($request) {
$query->where('price', '>=', $request->min_price);
})
->when($request->has('in_stock'), function ($query) use ($request) {
$query->where('stock', '>', 0);
})
->get();
?>
The when() method accepts a condition and only applies the query modification if that condition is true. This pattern is far more readable than nested if statements.
Search queries often need to filter across multiple columns. Laravel 12's query builder makes this straightforward:
<?php
namespace App\Http\Controllers;
use App\Models\User;
class UserController extends Controller
{
public function search(Request $request)
{
$query = $request->input('q');
$users = User::query()
->when($query, function ($builder) use ($query) {
$builder
->where('name', 'like', "%{$query}%")
->orWhere('email', 'like', "%{$query}%")
->orWhere('phone', 'like', "%{$query}%");
})
->orderByDesc('created_at')
->paginate(15);
return view('users.search', ['users' => $users]);
}
}
?>
This approach keeps your search logic clean and only applies filtering when a search query is actually provided.
Laravel 12 improves handling of eager loading by making it easier to load related data with specific constraints, helping you avoid N+1 query problems:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Author extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
public function publishedPosts()
{
return $this->hasMany(Post::class)
->where('published', true)
->orderByDesc('published_at');
}
}
// In your controller
$authors = Author::query()
->with('publishedPosts') // Only load published posts
->get();
// Or with additional constraints
$authors = Author::query()
->with(['publishedPosts' => function ($query) {
$query->limit(5); // Only 5 most recent posts per author
}])
->get();
?>
Eager loading with constraints is crucial for performance. Without it, you'd either load all posts (wasting memory) or end up with N+1 queries (wasting database calls).
Sometimes you need to combine multiple conditions with proper grouping. Laravel 12's query builder handles this elegantly:
<?php
$orders = Order::query()
->where('status', 'completed')
->where(function ($query) {
// Group these conditions with OR
$query
->where('payment_method', 'credit_card')
->orWhere('payment_method', 'paypal');
})
->whereIn('currency', ['USD', 'EUR'])
->orderByDesc('created_at')
->get();
?>
This creates a query like: WHERE status = 'completed' AND (payment_method = 'credit_card' OR payment_method = 'paypal') AND currency IN ('USD', 'EUR')—all without writing raw SQL.
Let's see how lifecycle hooks and query builder improvements work together in a practical scenario:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $fillable = ['name', 'email', 'status'];
protected static function booted()
{
// When a user is created, send them a welcome email
static::created(function ($user) {
\App\Jobs\SendWelcomeEmail::dispatch($user);
});
// When a user is deleted, clean up their data
static::deleting(function ($user) {
$user->posts()->delete();
$user->comments()->delete();
});
}
public function posts()
{
return $this->hasMany(Post::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
}
// In your controller - using the improved query builder
class UserController extends Controller
{
public function index(Request $request)
{
$users = User::query()
->when($request->input('status'), function ($query, $status) {
$query->where('status', $status);
})
->when($request->input('search'), function ($query, $search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
})
->with(['posts' => function ($query) {
$query->where('published', true)->latest();
}])
->paginate(20);
return view('users.index', ['users' => $users]);
}
}
?>
Notice how the lifecycle hooks handle automatic tasks (welcome emails, cleanup) while the query builder handles efficient data retrieval with proper eager loading and filtering.
Lifecycle hooks should be quick. Avoid heavy operations like sending emails or making external API calls directly in hooks. Use Laravel's job queue system instead:
<?php
// Good - dispatch to a queue
static::created(function ($user) {
\App\Jobs\SendWelcomeEmail::dispatch($user);
});
// Avoid - blocking operation in hook
static::created(function ($user) {
Mail::send(...); // This blocks the request!
});
?>
When updating models, use isDirty() to only run logic when specific fields change:
<?php
static::updating(function ($model) {
if ($model->isDirty('email')) {
// Only run this if email actually changed
$model->email_verified_at = null;
}
});
?>
The when() method is more elegant than conditional logic outside your query:
<?php
// Good
User::query()
->when($sortBy, fn($q) => $q->orderBy($sortBy))
->get();
// Less elegant
$query = User::query();
if ($sortBy) {
$query->orderBy($sortBy);
}
$query->get();
?>
Laravel 12's lifecycle hooks and query builder improvements are powerful tools that help you write cleaner, more maintainable code. Lifecycle hooks automatically handle repetitive tasks like slug generation, audit logging, and cascading updates. The improved query builder—especially methods like when() and eager loading with constraints—makes it easier to write complex queries without sacrificing readability.
The combination of these features allows you to build robust applications where data consistency is automatic and queries are optimized. Start incorporating these patterns into your projects, and you'll quickly see how they improve your code quality and reduce bugs.