利用继承和查询作用域来重构 Laravel 的 Eloquent 模型
我们在应用程序中通过手写SQL查询,已经持续了很长一段时间。我们通过像Laravel 的Eloquent ORM 之类的工具使我们能够使用更高级别方式来与数据库进行交互,从而使我们摆脱了查询语法和安全性等较低级别的细节。
从使用Eloquent开始,很自然就可以进行熟悉的操作,例如when
和join
。对于更高级别的使用者,诸如作用域,访问器,修改器之类的功能为旧的查询模式提供了更具表现力的替代方案。
让我们探索另一种替代方法,该方法可以替代用作重复where
语句和本地范围。该技术涉及创建新的Eloquent模型并扩展其他模型。通过扩展一个模型,您可以继承父模型的全部功能,同时保留添加自定义方法,作用域,事件监听等功能。这通常称为“单继承”,但我更喜欢将其称为“模型继承”。
举个栗子
大多数web应用程序都有“管理员”的概念,管理员通常是具有提升的权限和对应用程序的限制区域的访问权限的用户。为了区分普通用户和管理员用户,下面的这样的语句是个常用模式:
$admins = User::where('is_admin', true)->get();
当一个特定的where
语句成为整个应用程序的模式时,用一个局部作用域(local scope)代替它是非常有益的。通过在User
模型中实现isAdmin
作用域,我们可以编写一个更加易读并且可复用的Eloquent
语句:
$admins = User::isAdmin()->get();
// 实现
class User extends Model
{
public function scopeIsAdmin($query)
{
$query->where('is_admin', true);
}
}
让我们使用模型继承进一步抽象化,通过继承User
模型添加一个全局的作用域,我们能获得与之前完全相同的结果,但在我们的程序中有了一个新的实体。这个实体(Admin
)现在可以自定义方法、作用域和其他有有用的功能。
$admins = Admin::all();
// 实现
class Admin extends User
{
protected $table = 'users';
public static function boot()
{
parent::boot();
static::addGlobalScope(function ($query) {
$query->where('is_admin', true);
});
}
}
注意:
protected $table = ‘users’
是保证模型正常使用所必须的。Eloquent
使用模型类名来确定表名。因此,它认为表示是“admins”而不是“users”,导致Base table or view not found
的错误。
一旦有了“Admin”模型,就可以更容易、更清晰地将仅用于管理员的功能与“User”类分开。例如:
通知
使用新的Admin
模型,向所有管理员发送通知等操作将变得更加简单。
Notification::send(Admin::all(), NewSignUp($user));
鉴权语句
每当对 User
模型的操作仅限于管理员时,通常需要一个鉴权语句确保授权。
// 鉴权语句
if ($admin = User::find($id)->is_admin !== true) {
throw new Exception;
}
$admin->impersonate($user);
由于 Admin
的全局作用域, 当对 Admin
类调用 impersonate
方法时,鉴权语句就不必要了。
Admin::findOrFail($id)->impersonate($user);
模型工厂
在测试上下文中,你可能需要使用模型工厂模型工厂 创建具有管理员权限的 User
模型,如下所示:
$admin = factory(User::class)->create(['is_admin' => true]);
// 用户模型实现
$factory->define(User::class, function () {
return [
...
'is_admin' => false,
];
});
我们能通过引入模型工厂状态来封装将用户定义为管理员的内容,从而改进此语句。
$admin = factory(User::class)->states('admin')->create();
// 管理状态实现
$factory->state(User::class, 'admin', function () {
return ['is_admin' => true];
});
这当然是个改进,然而,工厂仍然返回一个User
模型实例。通过为Admin
定义一个全新的工厂,我们可以在返回 Admin
实例时获得相同的权限。
$admin = factory(Admin::class)->create();
// 管理工厂实现
$factory->define(Admin::class, function () {
return ['is_admin' => true]
+ factory(User::class)->raw();
});
有个问题:关联关系不能使用
与Eloquent
计算表名类型,这个模型的类名用于确定外键和关系表。因此Admin
模型的关系是有问题的。
Admin::first()->posts;
// 抛出异常: 未知字段 'posts.admin_id'
// 错误姿势
class Admin extends User {
//
}
class User extends Model {
public function posts() {
return $this->hasMany(Post::class);
}
}
Eloquent
不能处理此关系,因为它认为每个Post
都有admin_id
字段,而不是user_id
字段。我们可以通过在 User
模型里显式传递user_id
外键来解决这个问题:
// 正确实现
class Admin extends User {
//
}
class User extends Model {
public function posts() {
return $this->hasMany(Post::class, 'user_id');
}
}
同样的问题也存在于多对多的关系中。Eloquent
认为中间表名称与当前模型的类名匹配:
Admin::first()->tags;
// Throws: Table 'admin_tag' doesn't exist
// 错误示范
class Admin extends User {
//
}
class User extends Model {
public function tags() {
return $this->belongsToMany(Tag::class);
}
...
同样,我们可以通过显式传递中间表名称和外键来解决这个问题:
// 正确示范
class Admin extends User {
//
}
class User extends Model {
public function tags() {
return $this->belongsToMany(Tag::class, 'user_tag', 'user_id');
}
...
尽管显式的定义外键和关联表名称,能够允许我们在Admin
模型访问定义在User
模型中的关联关系,但这并不太理想。这些看似不必要的代码定义在任何地方都不太易懂。
不过,您可以创建一个HasParentModel trait
来自动处理这个问题。从Eloquent
的角度来看,它用父模型的类名代替当前模型的类名。查看示例GitHub。
为了使Laravel能够很好地处理单表继承,还可以做很多事情。我们已经创建了一个软件包,可以方便地添加到您的Laravel应用程序中,并准备随时发布。一定要关注我们的推特以获取公告!
让我们来看看利用这个trait
的新Admin
模型:
use App\Abilities\HasParentModel;
class Admin extends User
{
use HasParentModel;
// 注意我们不再需要:protected $table = 'users'
public static function boot()
{
parent::boot();
static::addGlobalScope(function ($query) {
$query->where('is_admin', true);
});
}
}
现在我们的User
模型的关系能恢复依赖到Eloquent的合理默认。
// 正在实现:
class User extends Model
{
public function posts() {
return $this->hasMany(Post::class);
}
public function tags() {
return $this->belongsToMany(Tag::class);
}
}
HasParentModel
trait 清理了我们的模型,让我们的开发者知道这儿有些特殊东西正在底层继续。
封装起来
我们已经确定了常见的口才模式,并使用模型继承对其进行了清理。此技术可帮助我们在应用程序中创建名称更好的封装实体。请记住,模型继承可以应用于任何Eloquent模型,而不仅仅是Users
和Admins
。可能性是无止境!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
刚好是我要了解的 :+1:
这是我要的 :heart_eyes: