问题场景

我们用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; 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进一步向面向对象化又迈出了坚实的一小步!

面向对象的一小步:添加ActiveRecord的Scope功能的更多相关文章

  1. Scope 功能的改进

    前段时间发表了一篇文章 面向对象的一小步:添加 ActiveRecord 的 Scope 功能 提到一种更加友好的方式做数据库查询.经小伙伴的建议,在满足同样条件下,可以有更为简洁的封装方法. 这需要 ...

  2. 为现有图像处理程序添加读写exif的功能

    为现有图像处理程序添加读取exif的功能 exif是图片的重要参数,在使用过程中很关键的一点是exif的数据能够和图片一起存在.exif的相关功能在操作系统中就集成了,在csharp中也似乎有了实现. ...

  3. FastReport 中添加二维码功能.(Delphi)

    http://www.cnblogs.com/fancycloud/archive/2011/07/24/2115240.html FastReport 中添加二维码功能.(Delphi)   在实际 ...

  4. phpcms 移植【添加相关文章】功能

    添加相关文章功能相当有用,移植一个过来基本上可以实现比较复杂的页面内包含分类功能,做二次开发时可以省下不少力气. 用例:如果一个产品,属于一个厂家,而这个厂家是动态添加的,既不是一个分类,而是一个厂家 ...

  5. TogetherJS – 酷!在网站中添加在线实时协作功能

    TogetherJS是一个免费.开源的 JavaScript 库,来自 Mozilla 实验室,可以实现基于 Web 的在线协作功能.把 TogetherJS 添加到您的网站中,您的用户可以在实时的互 ...

  6. Swift - 给表格添加移动单元格功能(拖动行)

    1,下面的样例是给表格UITableView添加单元格移动功能: (1)给表格添加长按功能,长按后表格进入编辑状态  (2)在编辑状态下,可以看到单元格后面出现拖动按钮  (3)鼠标按住拖动按钮,可以 ...

  7. Webstorm添加新建.vue文件功能并支持高亮vue语法和es6语法

    转载:https://blog.csdn.net/qq_33008701/article/details/56486893 Webstorm 添加新建.vue文件功能并支持高亮vue语法和es6语法 ...

  8. 3D打印机如何添加自动调平功能

    原理说明 Kossel/Rostock等Delta(并联/三角洲)类型的机器,可以参考:http://learn.makerlab.me/guides/11 3d打印打印时最重要的是第一层的效果,如果 ...

  9. Tengine是由淘宝网发起的Web服务器项目。它在Nginx的基础上,针对大访问量网站的需求,添加了很多高级功能和特性

    简介 Tengine是由淘宝网发起的Web服务器项目.它在Nginx的基础上,针对大访问量网站的需求,添加了很多高级功能和特性.Tengine的性能和稳定性已经在大型的网站如淘宝网,天猫商城等得到了很 ...

随机推荐

  1. 实验三:分别用for,while;do-while循坏语句以及递归的方法计算n!,并输出算式。

    源代码: package jiecheng;import java.util.Scanner;public class JieCheng {public static void main(String ...

  2. unittest各个组件之间的关系

    各个组件的含义: TestCase:测试用例,测试用例里面会有很多测试方法,是单元测试中最小维度的测试行为. TestSuite:测试套件,是测试用例的集合. TestFixure:测试固件,测试准备 ...

  3. SQLite异常 qAdmin: Cannot perform this operation on a closed dataset.【申明:来源于网络】

    SQLite异常 qAdmin: Cannot perform this operation on a closed dataset. 当使用 SQLite administrator,打开SQLit ...

  4. 【安富莱专题教程第4期】SEGGER的J-Scope波形上位机软件,HSS模式简单易用,无需额外资源,也不需要写目标板代码

    说明:1.在实际项目中,很多时候,我们需要将传感器或者ADC的数值以波形的形式显示.通常的解决办法是用串口上位机,USB接口上位机或者MDK的逻辑分析仪功能,使用这三种方式都比较繁琐.本期专题为大家讲 ...

  5. FFmpeg 结构体学习(四): AVFrame 分析

    在上文FFmpeg 结构体学习(三): AVPacket 分析我们学习了AVPacket结构体的相关内容.本文,我们将讲述一下AVFrame. AVFrame是包含码流参数较多的结构体.下面我们来分析 ...

  6. 吴恩达机器学习笔记57-基于内容的推荐系统(Content Based Recommendations)

    假使我们是一个电影供应商,我们有 5 部电影和 4 个用户,我们要求用户为电影打分. 前三部电影是爱情片,后两部则是动作片,我们可以看出Alice 和Bob 似乎更倾向与爱情片, 而 Carol 和 ...

  7. [Swift]LeetCode435. 无重叠区间 | Non-overlapping Intervals

    Given a collection of intervals, find the minimum number of intervals you need to remove to make the ...

  8. [Swift]LeetCode679. 24点游戏 | 24 Game

    You have 4 cards each containing a number from 1 to 9. You need to judge whether they could operated ...

  9. IDEA 配置远程debug

    1. 启动远程服务配置debug参数 远程服务启动时配置如下debug相关参数 java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address= ...

  10. java集合框架整理

    一.总体框架 Java集合是java提供的工具包,包含了常用的数据结构:集合.链表.队列.栈.数组.映射等.Java集合工具包位置是java.util.* .Java集合主要可以划分为4个部分:Lis ...