
本文介绍如何通过 laravel 的本地作用域(local scopes)将重复的 `withcount` 统计逻辑封装到模型中,实现如 `group::where(...)->withrequestscount()->find()` 这样的流畅链式调用,提升代码复用性与控制器可读性。
在 Laravel 开发中,当多个控制器或服务频繁使用相同的数据聚合逻辑(例如对关联模型按状态分组计数),直接在控制器中重复编写 withCount([...]) 不仅冗余,还违背单一职责原则。理想的解决方案是将其内聚地封装进 Eloquent 模型,并支持与其他查询构造器(如 where、orderBy)无缝链式调用。
Laravel 提供了 本地作用域(Local Scopes) 机制,专为此类场景设计:它是一个以 scope 开头的公共方法,接收 $query(即当前 Builder 实例)作为第一个参数,并必须返回该 $query 对象,从而维持链式调用能力。
✅ 正确实现:定义本地作用域
在 Group 模型中添加如下方法:
// app/Models/Group.php
class Group extends Model
{
use HasFactory;
public function requests()
{
return $this->hasMany(Request::class);
}
/**
* 本地作用域:预加载请求统计(总数 + 各状态分计数)
* 支持链式调用,如 Group::where(...)->withRequestsCount()->get()
*/
public function scopeWithRequestsCount($query)
{
return $query->withCount([
'requests',
'requests as requests_count_pending' => fn ($q) => $q->where('state', 'pending'),
'requests as requests_count_accepted' => fn ($q) => $q->where('state', 'accepted'),
'requests as requests_count_refused' => fn ($q) => $q->where('state', 'refused'),
]);
}
}⚠️ 注意:方法名必须为 scopeXxx(首字母大写),且必须声明为实例方法(非 static),Laravel 会自动注入当前查询构造器并确保链式兼容性。
✅ 在控制器中流畅使用
重构后的控制器代码简洁清晰,完全符合预期语法:
// app/Http/Controllers/GroupController.php
class GroupController extends Controller
{
public function show(int $group = 1)
{
return Group::where('id', '>', 0)
->withRequestsCount() // ← 链式调用本地作用域
->find($group);
}
}此时生成的 SQL 将一次性完成所有计数(利用 COUNT() 和 CASE WHEN 优化),返回的 Group 实例将包含以下属性:
- requests_count(总请求数)
- requests_count_pending
- requests_count_accepted
- requests_count_refused
? 补充说明与最佳实践
- 命名规范:作用域方法名推荐使用 camelCase(如 withRequestsCount),调用时不带 scope 前缀,Laravel 自动识别。
- 可组合性:本地作用域可与其他作用域、where、orderBy 等任意组合,无顺序限制(只要在 find/get 前调用即可)。
- 避免静态方法陷阱:你之前尝试的 static function withRequestsCount() 返回的是新查询实例,会中断原始构造器链——这是无法链式调用的根本原因;而本地作用域始终操作并返回同一 Builder 实例。
-
扩展建议:如需动态传参(例如指定状态白名单),可定义带参数的作用域:
public function scopeWithRequestsCountByStates($query, array $states = ['pending','accepted','refused']) { /* ... */ }
通过本地作用域,你不仅实现了逻辑复用和链式调用,更让模型真正成为“数据行为的中心”,使业务查询意图一目了然,大幅提升代码可维护性与团队协作效率。










