Advanced Eloquent Scopes and Accessors: Cleaner Laravel Models
Master Eloquent query scopes and accessors to write reusable, expressive database logic. Learn to clean up your controllers by shifting data handling to models.
Previously in this course, we covered Project Board Domain Modeling, where we established the relationships between users, projects, and tasks. Now that our schema is in place, we need to ensure our interaction with this data remains maintainable as the project grows.
In this lesson, we’re moving beyond basic CRUD. We’ll focus on how to use eloquent features to keep our domain logic expressive and our controllers thin. By leveraging query scopes and accessors, we stop repeating filtering logic and data formatting across our services and controllers.
Encapsulating Logic with Query Scopes
A common pain point in growing applications is the repetition of database constraints. If you find yourself writing ->where('status', 'completed') in five different controllers, you’ve created a maintenance burden. If the definition of "completed" changes, you have to hunt down every instance.
Query scopes allow you to define these constraints as reusable methods on your model.
Implementing Local Scopes
A local scope is defined by prefixing a method name with scope. Laravel automatically handles the translation, so calling Task::completed() triggers scopeCompleted().
PHPnamespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; class Task extends Model { #6A9955">/** * Scope a query to only include completed tasks. */ public function scopeCompleted(Builder $query): Builder { return $query->where('status', 'completed'); } #6A9955">/** * Scope a query to only include tasks due soon. */ public function scopeDueSoon(Builder $query): Builder { return $query->where('due_date', '<=', now()->addDays(3)) ->where('status', '!=', 'completed'); } }
Now, instead of writing raw where clauses, your service layer reads like English:
PHP#6A9955">// Clean, expressive, and easily testable $upcomingTasks = Task::dueSoon()->get();
As discussed in Mastering Laravel Local Scopes for Cleaner Database Filtering, these methods are chainable, allowing you to build complex queries dynamically without bloating your controller.
Transforming Data with Accessors and Mutators
While scopes handle retrieval, accessors and mutators handle data transformation. An accessor transforms a value when it is retrieved from the database, while a mutator modifies it before it is saved.
Custom Accessors
Let's say our Task model has a title column, but we want to ensure it’s always returned in title case when accessed via our API.
PHPuse Illuminate\Database\Eloquent\Casts\Attribute; protected function title(): Attribute { return Attribute::make( get: fn(string $value) => ucfirst($value), ); }
This is particularly useful for formatting dates, concatenating name fields, or calculating values on the fly. If you want to dive deeper into these patterns, Laravel Eloquent Accessors and Mutators: A Practical Guide provides a comprehensive look at the modern Attribute syntax introduced in recent Laravel versions.
Mutators for Data Integrity
Mutators ensure data is sanitized before it hits the database. If we want to ensure all task titles are stored in lowercase to allow for case-insensitive searching, we add a set method to our attribute definition:
PHPprotected function title(): Attribute { return Attribute::make( get: fn(string $value) => ucfirst($value), set: fn(string $value) => strtolower($value), ); }
Hands-on Exercise
- Open your
Taskmodel. - Create a
scopeOverduemethod that filters tasks where thedue_dateis in the past and the status is not 'completed'. - Implement an accessor for a
is_overdueattribute that returns a boolean based on thedue_date. - Use these in a temporary route to verify that
Task::overdue()->get()returns the correct items and$task->is_overdueworks as expected.
Common Pitfalls
- Over-engineering: Don't turn every simple
whereclause into a scope. Only create scopes for logic you actually reuse or that significantly improves readability. - Performance: Remember that scopes are just query builder modifications. They don't execute the query themselves. Always ensure you are mindful of indexes on the columns you are filtering by.
- Accessor Side Effects: Never perform heavy operations (like external API calls) inside an accessor. Accessors run every time you touch the property; you don't want to trigger a network request every time you access a field in a loop.
Recap
By moving database constraints into query scopes and data formatting into accessors, we keep our business logic encapsulated within the model. This makes our controllers cleaner and ensures that our data handling rules are applied consistently across the entire application.
Up next: We will implement the TaskService to handle complex task creation logic and manage user-task assignments, tying these model improvements into a cohesive service layer.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.