面向对象的一小步:添加ActiveRecord的Scope功能

面向对象的一小步:添加ActiveRecord的Scope功能 问题场景

我们用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; use yii; use yii\db\ActiveRecord; use yii\db\Expression; use yii\db\Exception; use yii\helpers\ArrayHelper; use common\behavior\TimeBehavior; 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); } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyfysx.html