Laravel 11 模型作用域与访问器

Eloquent 模型提供了两个强大的特性来简化代码:作用域(Scopes)用于封装常用的查询约束,访问器(Accessors)和修改器(Mutators)则用于处理属性的格式化和转换。 本章将系统讲解这些功能,让你的模型代码更加简洁、可复用。

🎯 什么是作用域?

作用域允许你定义通用的查询约束,并在模型查询时重复使用。作用域分为两类:

  • 本地作用域:在模型类中定义,调用时需显式使用。
  • 全局作用域:自动应用到所有查询,无需显式调用。

📦 本地作用域

本地作用域是在模型中以 scope 为前缀定义的方法,返回查询构建器实例。

定义本地作用域
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function scopePublished($query)
    {
        return $query->where('is_published', true);
    }

    public function scopeOfUser($query, $userId)
    {
        return $query->where('user_id', $userId);
    }

    public function scopePopular($query, $minComments = 10)
    {
        return $query->where('comments_count', '>=', $minComments);
    }
}
使用本地作用域

调用作用域时,直接使用方法名(去掉 scope 前缀):

$posts = Post::published()->get();
$userPosts = Post::published()->ofUser(1)->get();
$popularPosts = Post::published()->popular(5)->get();

作用域可以链式调用,也可以与其他查询构建器方法结合使用。

动态作用域

作用域可以接受参数,如上例中的 $userId$minComments

💡 最佳实践: 将常用的查询条件提取为本地作用域,如「已发布」、「热门」、「按用户筛选」等,提高代码复用性和可读性。

🌍 全局作用域

全局作用域自动应用于模型的所有查询,无需手动调用。常用于「软删除」、「多租户」等场景。

创建全局作用域

首先创建一个实现 Illuminate\Database\Eloquent\Scope 接口的类:

namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class ActiveUserScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('active', 1);
    }
}
注册全局作用域

在模型的 booted 方法中注册:

namespace App\Models;

use App\Scopes\ActiveUserScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new ActiveUserScope);
    }
}
使用匿名全局作用域

也可以直接在 booted 中使用闭包:

protected static function booted()
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('active', 1);
    });
}
移除全局作用域

在查询时临时移除全局作用域:

// 移除所有全局作用域
$users = User::withoutGlobalScopes()->get();

// 移除指定作用域(使用闭包注册时需指定名称)
$users = User::withoutGlobalScope('active')->get();

// 移除指定作用域类
$users = User::withoutGlobalScope(ActiveUserScope::class)->get();

🔧 访问器(Accessors)

访问器用于在获取模型属性时自动格式化其值。定义方式是在模型中添加 get{Attribute}Attribute 方法。

定义访问器
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function getFullNameAttribute()
    {
        return "{$this->first_name} {$this->last_name}";
    }

    public function getAvatarUrlAttribute()
    {
        return $this->avatar ? asset('storage/' . $this->avatar) : 'https://www.gravatar.com/avatar/' . md5($this->email);
    }
}
使用访问器

访问器通过属性名(蛇形命名)访问:

$user = User::find(1);
echo $user->full_name;      // 输出 "John Doe"
echo $user->avatar_url;     // 输出头像 URL
💡 注意: 访问器方法名使用驼峰,如 getFullNameAttribute,对应的属性名是蛇形 full_name。Laravel 会自动转换。
访问器与原始值

在访问器内部可以通过 $this->attributes 访问原始数据库值:

public function getNameAttribute($value)
{
    return ucfirst($value);
}

访问器可以接收一个参数 $value,即数据库原始值,方便处理。

✍️ 修改器(Mutators)

修改器用于在设置模型属性时自动转换值。定义方式是在模型中添加 set{Attribute}Attribute 方法。

定义修改器
public function setFirstNameAttribute($value)
{
    $this->attributes['first_name'] = strtolower($value);
}

public function setPasswordAttribute($value)
{
    $this->attributes['password'] = bcrypt($value);
}
使用修改器

当赋值时,修改器会自动触发:

$user = new User;
$user->first_name = 'JOHN';  // 自动转为小写 'john'
$user->password = 'secret';   // 自动加密后存储
⚠️ 注意: 修改器只会在通过模型属性赋值时触发,使用 fill()create() 批量赋值同样会触发。

📅 日期转换与自定义属性

Laravel 提供了便捷的日期转换,也可以结合访问器/修改器实现更复杂的逻辑。

日期转换
protected $casts = [
    'created_at' => 'datetime',
    'published_at' => 'datetime:Y-m-d',
];

使用 $castsCarbon 实例或指定格式。

自定义虚拟属性(无数据库字段)

通过访问器可以创建不存在的虚拟属性,例如上面的 full_name

🎨 实战示例:博客模型综合应用

结合作用域和访问器,构建一个功能完善的 Post 模型:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['title', 'content', 'user_id', 'is_published', 'published_at'];

    protected $casts = [
        'is_published' => 'boolean',
        'published_at' => 'datetime',
    ];

    // 本地作用域
    public function scopePublished($query)
    {
        return $query->where('is_published', true);
    }

    public function scopeDraft($query)
    {
        return $query->where('is_published', false);
    }

    public function scopeRecent($query)
    {
        return $query->orderBy('created_at', 'desc');
    }

    // 访问器
    public function getExcerptAttribute()
    {
        return \Str::limit(strip_tags($this->content), 100);
    }

    public function getPublishedAtFormattedAttribute()
    {
        return $this->published_at ? $this->published_at->format('Y年m月d日') : null;
    }

    // 修改器
    public function setTitleAttribute($value)
    {
        $this->attributes['title'] = ucfirst($value);
        $this->attributes['slug'] = \Str::slug($value);
    }
}

使用示例:

// 获取最近发布的10篇文章
$posts = Post::published()->recent()->take(10)->get();

foreach ($posts as $post) {
    echo $post->title;                    // 首字母大写
    echo $post->excerpt;                  // 摘要
    echo $post->published_at_formatted;   // 格式化日期
}

// 创建新文章
$post = Post::create([
    'title' => 'laravel scopes',
    'content' => '...',
    'user_id' => 1,
    'is_published' => false,
]);
// title 自动转为 'Laravel scopes',slug 自动生成

⚠️ 常见问题

本地作用域需要显式调用,适用于需要灵活选择的查询约束;全局作用域自动应用到所有查询,适用于跨模型、始终生效的约束(如多租户隔离、软删除等)。

访问器不会覆盖原始属性,只是为模型增加了一个虚拟属性。原始值仍可通过 $this->getOriginal('attribute') 获取。

是的,无论是通过 createupdate 还是 fill 方法,修改器都会在赋值时触发。

📝 小结

作用域与访问器是 Laravel 模型的两大得力助手,它们让代码更加整洁、可维护。 作用域封装查询逻辑,访问器与修改器统一数据格式,合理运用这些特性可以大幅提升开发效率和代码质量。 下一章我们将学习模型事件与观察者,掌握模型生命周期的精细控制。