Menü schliessen
Created: December 19th 2025
Last updated: December 19th 2025
Categories: IT Development,  Laravel
Author: Ian Walser

Laravel 12 Lifecycle Hooks & Query Builder: Features That Level Up Your Code

Introduction

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.

Understanding Model Lifecycle Hooks in Laravel 12

What Are Lifecycle Hooks?

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.

The Core Lifecycle Hooks

Laravel 12 supports several lifecycle hooks that fire at different stages:

  • retrieved - After a model is fetched from the database
  • creating - Before a model is inserted into the database
  • created - After a model is successfully inserted
  • updating - Before a model is updated
  • updated - After a model is successfully updated
  • deleting - Before a model is deleted
  • deleted - After a model is deleted
  • saving - Before a model is saved (create or update)
  • saved - After a model is saved

Practical Use Case: Maintaining an Audit Trail

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.

Practical Use Case: Auto-Generating Slugs

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.

Practical Use Case: Syncing Related Data

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.

Query Builder Improvements in Laravel 12

Enhanced Query Chaining and Readability

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.

Practical Use Case: Conditional Filtering

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.

Practical Use Case: Building Search Functionality

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.

Practical Use Case: Eager Loading with Constraints

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).

Practical Use Case: Advanced Filtering with Multiple Conditions

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.

Combining Lifecycle Hooks and Query Builder

A Real-World Example: User Account Management

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.

Best Practices for Lifecycle Hooks and Query Builder

Keep Hooks Focused and Lightweight

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!
});
?>

Use isDirty() to Check for Changes

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;
    }
});
?>

Prefer Query Builder's when() Over if Statements

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();
?>

Key Takeaways

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.