面向对象的一小步:添加 ActiveRecord 的 Scope 功能 [ 2.0 版本 ]
问题场景
我们用Yii2的ActiveRecord功能非常的方便,假如我们有个Model叫Student,那么ActiveQuery可以通过这种方式轻便地获得:
$query = Student::find();
然后,我们就可以在$query
上继续使用各种方法添加SQL Clause:
$query->where(['gender' => 'male' ]); //选择男生
$query->where(['>', age' => '18' ]); //选择年龄大于18的
如果这条学生的记录需要审核,还可能有
$query->where([''check_status' => '1' ]); // 1-审核通过
最后,使用all()
或者one()
获取结果。
这是使用Yii2的小伙伴们天天在做的事情。
但是,笔者有时也觉得动不动就写一长串的where条件挺讨厌的,一则是太长,二则是可阅读性也不大好,['check_status' => '1' ]
这类的条件有时并不容易看出是何种意图。所以得找一种更为简便的方法就变得很有必要。
优化
写出面向对象化的代码是笔者的追求,我们希望能这样的去做查询:
Student::male()->all(); //选择男生
Student::checked()->male()->all(); //选择审核通过的男生
看不到那么多长长的where语句,就像伪代码一样良好的可读性,就算没用用过Yii2的小伙伴也能看懂是啥意思。如果能能这样去写代码,你愿不愿意?
实现
要说到如何实现,那就得依赖强大的魔术方法__call()
和__callStatic()
了。我们直接抛出代码,让大家看看是怎么实现的。
定义Scope方法
首先,需要在Model内部,定义一些Scope方法,用以封装特定的where条件集合,表示相对常用筛选条件。
Scope方法的格式为
public function xxxScope($param1, $param2,..., \yii\db\ActiveQuery $query)
{
return $query->where(['attr1' => $param1])->where(['attr2' => $param2]);
}
注意,xxxScope方法的前面的$param1, $param2,...都是可选的,最后一个参数一定是AQ的一个实例$query
,用以接收where条件,最后需要return将其返回。
扩展ActiveRecord
要实现Student::checked(),Student::male()这类方法,使得其也能得到AQ实例,那就得重载ActiveRecord的find()
静态方法;同时,为了使::male()
和::gender()
这类静态方法得以实现,还实现了__callStatic()
方法:
<?php
namespace common\base;
class BaseModel extends ActiveRecord
{
/**
* {@inheritdoc}
*
* @return yii\db\ActiveQuery the newly created [[ActiveQuery]] instance
*/
public static function find()
{
//这里的BaseActiveQuery是扩展ActiveQuery得来的
return Yii::createObject(BaseActiveQuery::className(), [get_called_class()]);
}
/**
* @param $name
* @param $arguments
*
* @return mixed
*/
public static function __callStatic($name, $arguments)
{
$static = new static();
$scopeMethod = $name.'Scope';
//检查Scope方法是否存在
if (method_exists($static, $scopeMethod)) {
//用ReflectionMethod分析Scope方法分析参数列表
$method = new \ReflectionMethod($static, $scopeMethod);
$params = $method->getParameters();
array_pop($params); // 先将$query pop出
$newArgs = [];
foreach ($params as $k => $param) { //对除$query外形参进行遍历
if (isset($arguments[$k])) {
$newArgs[$k] = $arguments[$k];
} else { //实参数小于形参数,传参不够的情况
if ($param->isDefaultValueAvailable()) {//有默认值就取默认值
$newArgs[$k] = $param->getDefaultValue();
} else {
$newArgs[$k] = null; //无默认值设为null
}
}
}
$newArgs[] = $static::find(); //将static::find()作为最后一个参数
return call_user_func_array([$static, $scopeMethod], $newArgs); //调用Scope方法
}
throw new yii\base\InvalidCallException("Method: $name not found!");
}
}
上面使用了ReflectionMethod反射类来分析Scope方法的参数列表,是为了避免在使用Scope方法时因为传参数量不对而导致的错误。例如,我们在Student中定义了一个status的Scope方法,参数为一个$status
public function statusScope($status = 1, ActiveQuery $query)
{
return $query->where(['check_status' => $status]);
}
我们在使用的时候如果不小心这样使用:
Student()::status(1, 2)->all();
那么statusScope方法自动过滤多余传参,保证最后一个参数为一个ActiveQuery。
扩展ActiveQuery
上面提到的BaseActiveQuery是扩展自ActiveQuery,重载了__call()
方法,使得->male()
,->checked()
访问得以实现:
class BaseActiveQuery extends yii\db\ActiveQuery
{
public function __call($name, $arguments)
{
$scopeMethod = $name.'Scope';
//检查Scope方法是否存在
if (method_exists($this->modelClass, $scopeMethod)) {
//用ReflectionMethod分析Scope方法分析参数列表
$method = new \ReflectionMethod($this->modelClass, $scopeMethod);
$params = $method->getParameters();
array_pop($params); // 先将$query pop出
$newArgs = [];
foreach ($params as $k => $param) {
if (isset($arguments[$k])) {
$newArgs[$k] = $arguments[$k];
} else {
if ($param->isDefaultValueAvailable()) {
$newArgs[$k] = $param->getDefaultValue(); /
} else {
$newArgs[$k] = null;
}
}
}
$newArgs[] = $this;//将自身作为形参最后一个参数
return call_user_func_array([$this->modelClass, $scopeMethod], $newArgs);
}
//最后的关键一步:别忘了调用父方法的__call
return parent::__call($name, $arguments);
}
}
另外,如果你喜欢,还可以在BaseActiveQuery加上另外两个常用的方法,用来转化为数组:
public function get()
{
return $this->asArray()->all();
}
public function first()
{
return $this->asArray()->one();
}
BaseActiveRecord和BaseActiveQuery都可以放在自己的一个公共目录下,例如
common/base/BaseActiveRecord.php
common/base/BaseActiveQuery.php
使用
实现了前面几步,我们就可以愉快的玩耍了。
Scope方法可以作为静态方法被AR调用,也可以作为非静态方法被AQ调用,同时支持链式操作,灵活性非常大。
Student::male()->checked()->age(20)->all();
Student::age(20)->checked()->get();
Student::find()->checked()->where(['is_deleted' => '0'])->male()->all();
Student::checked()->where(['like', 'name', 'Jason'])->female()->first();
.....
这样的查询是不是更清晰,更友好?
进一步优化
PHP是世界上最好的语言——这句话一直争议不断,然而PHPStorm是PHP最好的编辑器却似乎越来越没有争议。因此为了PHPStorm能更好的追踪代码,还需要做小小的优化。
在AR(如Student)头部DOC部分添加:
* @method BaseActiveQuery checked()
* @method BaseActiveQuery male()
* @method BaseActiveQuery age()
恭喜Yii2进一步向面向对象化又迈出了坚实的一小步!
米粒人生 苏州
最后登录:2021-04-25
在线时长:47小时54分
- 粉丝111
- 金钱6555
- 威望230
- 积分9325
热门源码
- 基于 Yii 2 + Bootstrap 3 搭建一套后台管理系统 CMF
- 整合完 yii2-rbac+yii2-admin+adminlte 等库的基础开发后台源码
- 适合初学者学习的一款通用的管理后台
- yii-goaop - 将 goaop 集成到 Yii,在 Yii 中优雅的面向切面编程
- yii-log-target - 监控系统异常且多渠道发送异常信息通知
- 店滴云1.3.0
- 面向对象的一小步:添加 ActiveRecord 的 Scope 功能
- Yii2 开源商城 FecShop
- 基于 Yii2 开发的多店铺商城系统,免费开源 + 适合二开
- leadshop - 基于 Yii2 开发的一款免费开源且支持商业使用的商城管理系统
共 8 条评论
没看明白为什么写这么多
例:轮播图模型
Focus.php
Focus 中
type=0
PC端,type=1
手机端FocusQuery.php 中添加方法
//改自andWhere方法 public function pc(){ if ($this->where === null) { $this->where = ['type'=>0]; } elseif (is_array($this->where) && isset($this->where[0]) && strcasecmp($this->where[0], 'and') === 0) { $this->where[] = ['type'=>0]; } else { $this->where = ['and', $this->where,['type'=>0]]; } return $this; }
Focus.php 添加方法
public static function pc(){ return self::find()->pc(); }
同样能实现
你没有重载
__staticCall(),Focus::pc()->all()
这一步会报错。另外,你的那么多if()..esle()...
不大好@米粒人生
实际用过,能正常返回结果,没发现有什么问题
想了想
if可以去掉
FocusQuery.php 中的函数可以改成
public function pc(){ return $this->andWhere(['type'=>0]); }
我看了下,你这样子封装也是可以的,效果一样。而且没有调用魔术方法,性能更高。不过缺点是每次要在xxxQuery和Model中定义两次。为了只定义一次,你可以将pc写在FocusQuery中,而在Focus重载__callStatic方法,这样Focus::pc()找不到时就去对应FocusQuery中去找。
敢不敢不要写的这么优秀 都学不过来了
其实没什么...一是要有这么个需求,二是知道魔术方法。上面那个自己也可实现
问题场景
我们用Yii2的ActiveRecord功能非常的方便,假如我们有个Model叫Student,那么ActiveQuery可以通过这种方式轻便地获得:
$query = Student::find();
然后,我们就可以在$query上继续使用各种方法添加SQL Clause:
$query->where(['gender' => 'male' ]); //选择男生
$query->where(['>', age' => '18' ]); //选择年龄大于18的
如果这条学生的记录需要审核,还可能有
$query->where([''check_status' => '1' ]); // 1-审核通过
最后,使用all()或者one()获取结果。
这是使用Yii2的小伙伴们天天在做的事情。
但是,笔者有时也觉得动不动就写一长串的where条件挺讨厌的,一则是太长,二则是可阅读性也不大好,['check_status' => '1' ]这类的条件有时并不容易看出是何种意图。所以得找一种更为简便的方法就变得很有必要。
优化
写出面向对象化的代码是笔者的追求,我们希望能这样的去做查询:
Student::male()->all(); //选择男生
Student::checked()->male()->all(); //选择审核通过的男生
看不到那么多长长的where语句,就像伪代码一样良好的可读性,就算没用用过Yii2的小伙伴也能看懂是啥意思。如果能能这样去写代码,你愿不愿意?
实现
要说到如何实现,那就得依赖强大的魔术方法call()和callStatic()了。我们直接抛出代码,让大家看看是怎么实现的。
定义Scope方法
首先,需要在Model内部,定义一些Scope方法,用以封装特定的where条件集合,表示相对常用筛选条件。 Scope方法的格式为
public function xxxScope($param1, $param2,..., \yii\db\ActiveQuery $query)
{
return $query->where(['attr1' => $param1])->where(['attr2' => $param2]);
}
注意,xxxScope方法的前面的$param1, $param2,...都是可选的,最后一个参数一定是AQ的一个实例$query,用以接收where条件,最后需要return将其返回。
这个和下面的有何区别?而且可以直接GII生成类似的链式操作
//Student类 public static function find() { return Yii::createObject(StudentQuery::className(), [get_called_class()]); }
//StudentQuery类,继承ActiveQuery类 public function male(){ return $this->andWhere(['gender' => 'male' ]); }
使用:
Student::find()->male()->all()
基本没区别,可以参看:https://www.yiichina.com/code/1780
报错Invalid argument supplied for foreach
model::male()
是不是应该有个find呢
sorry 没有看到你的__call 里面反射了model
model 重写find 比较简单。 链式调用的逻辑放到 BaseActiveQuery 防止model 代码过载