zoukankan      html  css  js  c++  java
  • 面向对象的一小步:添加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,..., yiidbActiveQuery $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 commonase;
    
    use yii;
    use yiidbActiveRecord;
    
    
    class BaseModel extends ActiveRecord
    {    
    
        /**
         * {@inheritdoc}
         *
         * @return yiidbActiveQuery 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 yiiaseInvalidCallException("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 yiidbActiveQuery
    {
        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进一步向面向对象化又迈出了坚实的一小步!

  • 相关阅读:
    Luogu 三国游戏
    牛客练习赛60E
    cf1321E
    cf988D
    cf1089F
    cf1055C
    cf997B
    cf1033D
    cf1062D
    cf1081E
  • 原文地址:https://www.cnblogs.com/minirice/p/9364730.html
Copyright © 2011-2022 走看看