### 2018 年 11 月 2 日 发布 模型三大利器我们已经了解了[修改器](https://blog.thinkphp.cn/817548)和[搜索器](https://blog.thinkphp.cn/783775),如果还不是特别理解的可以参考之前的博客。 今天来总结下获取器的用法,其实获取器是最容易理解的,但却又是最容易困惑和出问题的。 ## 定义获取器 获取器的作用是对模型对象的(原始)数据做出自动处理。一个获取器对应模型的一个特殊方法(该方法必须为`public`类型),方法命名规范为: >[info]### get**FieldName**Attr `FieldName`为数据表字段的驼峰转换或者你数据表不存在的字段(注意理解后面这句话),下面是一个典型的获取器定义: ``` <?php namespace app\index\model; use think\Model; class User extends Model { public function getUserTypeAttr($value, $data) { $type = [0 => '普通', 1 => 'VIP', 2 => '黄金', 3 => '白金', 4 => '钻石']; return $type[$value]; } } ``` 你需要给每一个需要输出转换处理的数据字段定义一个对应的获取器,但获取器的字段名不一定要和数据表的字段名一致,例如我希望给`user_type`字段定义一个名为`getTypeAttr`的获取器也是允许的,但要注意这个时候传入获取器的第一个参数肯定是没有值(因为没有对应的数据表字段数据),只能通过第二个参数获取你需要的数据。 ``` <?php namespace app\index\model; use think\Model; class User extends Model { public function getTypeAttr($value, $data) { $type = [0 => '普通', 1 => 'VIP', 2 => '黄金', 3 => '白金', 4 => '钻石']; return $type[$data['user_type']]; } } ``` 当然更为严谨的情况下,你还需要判断下是否存在`$data['user_type']`,这个暂且略过。 >[danger] 注意第二个参数的`data`数据,可能本身已经经过了获取器的处理(如果你定义了相关的获取器的话)。 为什么要定义一个和数据报字段不一致的获取器呢?最明显的好处可以区分不同的字段获取原始数据和处理过的数据。事实上,有很多理由可以让你定义一些数据表不存在的字段获取器,这恰恰是获取器的魅力所在。 看的出来获取器定义本身没什么难度,关键在于方法里面的获取逻辑,这是实际应用中最需要关注的。 ## 调用获取器 定义获取器之后会在下列情况自动触发: * 模型的数据对象取值操作(例如`$model->field_name`); * 模型的序列化输出操作(例如`$model->toArray()`或`toJson()`); * 显式调用`getAttr`方法(例如`$model->getAttr('field_name')`); 前面两种其实最终都是调用最后一种来实现的,最关键的是要理解第一种。模型对象取值的时候一般都是通过下面的方式: ``` $user = User::get(1); echo $user->name; echo $user->user_type; ``` 当我们使用上面的方式进行模型对象数据获取或者在模板输出的时候事实上都会按照下面的顺序来检测和获取数据。 * 第`1`步——如果查询结果包含该字段数据,取回原始数据,否则并进入第2步; * 第`2`步——检查是否定义该字段的获取器(包括动态获取器),如果有,则调用获取器返回结果,没有则进入第3步; * 第`3`步——检查是否定义了字段的类型转换,有则进行转换处理并返回结果,没有则进入第4步; * 第`4`步——如果是系统的时间字段,则自动进行时间格式化处理并返回结果,否则进入第5步; * 第`5`步——如果第1步检查的时候不包含该字段数据,则检查是否存在关联属性定义,有则通过关联关系获取数据并返回结果,否则抛出属性未定义的异常。 >[info] 上面的这五个步骤的详细代码,如果你有兴趣的可以直接参考`think\model\concern\Attribute`的`getAttr`方法代码。 简单来说,当你获取`$user->user_type`的时候都会去检查是否定义了相关的获取器,而不管`user_type`字段是否是一个真实的数据表字段。 但很多情况下,你不会一个个去获取模型数据,而是把整个模型数据返回给客户端或者模板。 ``` public function index() { $user = User::get(1); return json($user); } ``` 在这种情况下,其实就是在响应输出的时候进行了模型的`toJson`处理。 >[danger] 有一点至关重要,如果你的获取器定义了非数据表的字段,是不会自动输出的,必须通过`append`方法追加额外属性(并且支持追加关联模型属性)。 如果我们定义了一个`type`属性的获取器(假设这并不是一个真实的数据表字段),那么需要使用下的方式才能正常输出(否则你可能只有`user_type`数据): ``` public function index() { $user = User::get(1); return json($user->append(['type'])); } ``` 如果你是使用`toArray`的话,处理方式相同。 如果是数据集查询的话,一样可以使用`append`方法统一追加额外字段。 ``` public function index() { $users = User::all(); return json($users->append(['type'])); } ``` >[info] 除了`append`方法之外,我们还支持用`hidden`方法临时隐藏一些数据。 ## 获取原始数据 有些情况下,除了要获取处理过的数据外,还需要获取原始数据以便应对不同的需求。 如果你的获取器都是用的区分于实际数据表字段的额外属性字段,那么这个问题本身已经解决了。所以我们主要讨论的是当你的获取器属性和数据表字段一致的情况下,该如何获取原始数据。 一个最简单的办法是使用`getData`方法: ``` $user = User::get(1); // 获取user_type获取器数据 echo $user->user_type; // 获取原始的user_type数据 echo $user->getData('user_type'); // 获取全部原始数据 dump($user->getData()); ``` ## 动态获取器 前面我们提到过动态获取器的概念,动态获取器就是不需要在模型类里面定义获取器方法,而是在查询的时候使用闭包来定义一个字段的获取器对数据进行统一的处理。 ``` User::withAttr('name', function($value, $data) { return strtolower($value); })->select(); ``` 如果你需要定义多个动态获取器,多次调用`withAttr`方法就行。 动态获取器的意义除了可以不用在模型里面定义获取器方法之外,还可以起到覆盖已经定义的获取器的作用,并且动态获取器可以支持`Db`类操作,弥补了`Db`操作不能使用获取器的缺憾,具体就看自己的需求来选择了。 ``` Db::name('user')->withAttr('name', function($value, $data) { return strtolower($value); })->select(); ``` ## 总结 无论是获取器,还是之前提的修改器、搜索器,其作用无非是把你的模型工作细化和拆分,这样代码和逻辑也会更清晰,可维护性也大大增强,至于性能,从来不是模型首先考虑的。