问题场景

我们用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. 将DataRow拷贝到另一个DataRow

    DataRow dr = dtPadFluid.Rows[gvPadFluid.FocusedRowHandle]; foreach (DataColumn dc in _dr.Table.Colum ...

  2. centos7下搭建高匿HTTP代理

    一.一般适用情况1.两台都有外网IP,一台服务器请求资源通过另外一个服务器,本文重点讲第一种.2.两台服务器,其中一台服务器只有内网IP,另外一台服务器有公网和内网IP. 二.前提 # 确认服务器端i ...

  3. 20190108C++MFC error 2065 未定义XX原因以及解决方式

    今天写界面的时候,明明直接在rc和reourse.h里面加了控件下面是rc和reourse.h照片 编辑的时候一直报错,找了很久发现是新定义的控件有两处定义,定义到其他工程里了所以才会这样,把其他工程 ...

  4. UML建模工具

    UML:Unified Modeling Language (UML)又称统一建模语言或标准建模语言,是始于1997年一个OMG标准,它是一个支持模型化和软件系统开发的图形化语言,为软件开发的所有阶段 ...

  5. ndk编译libx264生成库

    编译脚本如下: TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.6/prebuilt/linux-x86_64 function build_x26 ...

  6. kali linux 网络渗透测试学习笔记(二)OWASP ZAP工具扫描SQL injection漏洞失败

    按照惯例,利用OWASP ZAP工具扫描SQL injection漏洞时,应该很快就可以扫描出来,但是在笔者进行扫描的时候,却遇到了以下状况: 这说明了该工具根本就没能够扫描出SQL注入的漏洞,不知道 ...

  7. JAVA四类八种基本数据类型

    boolean类型 Boolean在内存中占用一个字节. 当java编译器把java源代码编译为字节码时,会用int或byte来表示boolean.在java虚拟机中,用整数零来表示false,用任意 ...

  8. [Swift]LeetCode150. 逆波兰表达式求值 | Evaluate Reverse Polish Notation

    Evaluate the value of an arithmetic expression in Reverse Polish Notation. Valid operators are +, -, ...

  9. [Swift]LeetCode670. 最大交换 | Maximum Swap

    Given a non-negative integer, you could swap two digits at most once to get the maximum valued numbe ...

  10. [Swift]LeetCode797. 所有可能的路径 | All Paths From Source to Target

    Given a directed, acyclic graph of N nodes.  Find all possible paths from node 0 to node N-1, and re ...