### 2018 年 10 月 27 日 发布 修改器是模型的三大利「器」之一,本篇我们来总结下修改器的用法,以及一些注意事项。 ## 定义修改器 修改器的作用是在模型对象数据写入数据库之前进行一些必要的数据处理,修改器的标准定义如下: ``` public function setFieldNameAttr($value, $data) { // 对value值进行处理 data参数是当前全部数据 // 返回值就是实际要写入数据库的值 return $value; } ``` 其中`FieldName`对应数据表的`field_name`字段(注意数据表字段的规范和修改器方法定义规范,否则会导致错误)。 原则上,每个修改器应当仅处理对应字段的数据,但在必要的情况下允许同时处理多个字段。 下面是一个例子 ``` public function setBirthdayAttr($value, $data) { // 格式化生日数据 $birthday = strtotime($value); // 根据生日判断年龄 $age = getAgeByBirthday($birthday); // 赋值年龄数据 $this->setAttr('age', $age); return $birthday; } public function setAgeAttr($value,$data) { return floor($value); } ``` 之所以使用`setAttr`方法是确保年龄赋值操作仍然可以走单独的修改器。如果你没有额外的修改器,那么也可以写成 ``` public function setBirthdayAttr($value, $data) { // 格式化生日数据 $birthday = strtotime($value); // 根据生日判断年龄 $age = getAgeByBirthday($birthday); // 赋值年龄数据 $this->data['age'] = $age; return $birthday; } ``` 注意一定不能写成 ``` $this->age = $age; ``` 因为在模型内部进行数据对象的赋值,会因为和模型内部属性混淆而导致不可预知的后果。 >[danger] 如果你在某个修改器中可能会对其它字段进行修改,务必记得你需要额外修改的字段修改器必须已经经过赋值操作(或者已经触发过修改器)。 ## 如何调用 修改器方法不需要手动调用,按照定义规范定义好后,系统会在下面的情况下自动调用: * 模型对象赋值; * 调用模型的`data`方法,并且第二个参数传入true; * 调用模型的`save`方法,并且传入数组数据; * 显式调用模型的`setAttr`方法; * 定义了该字段的自动完成; 例如`User`模型定义了`setPasswordAttr`修改器方法。 ``` public function setPasswordAttr($value, $data) { return md5($value); } ``` 当下面这样使用的时候,保存到数据库的`password`字段的值就会变成`md5('think')`后的值。 ``` $user = User::get(1); $user->password = 'think'; $user->save(); ``` 如果你在一些情况下,不希望使用修改器而是想要手动控制数据,可以尝试使用下面的方法。 ``` $user = User::get(1); $user->data('password', md5('think')); $user->save(); ``` 这个时候就不会经过修改器处理。 ## 避免冲突 很多开发者喜欢给修改器定义自动完成`auto`(包括`insert`和`update`)。 ``` protected $auto = ['password']; ``` 这在`V5.1.27`版本之前是一个看似聪明却非常致命的错误,要尽量避免,因为根据我们之前给出的修改器触发条件,会导致该修改器被执行两次。这会是一个灾难性的错误,将导致所有的用户注册后都无法正常登录。 解决办法取消`password`字段的自动完成设置,因为修改器会在每次赋值的时候自动触发,如果没有赋值说明密码没有被修改,也谈不上自动完成。 >[danger] 自动完成的字段通常是不在表单里面的字段,一般是由系统自动处理的字段。 `V5.1.27`版本改进了这个问题,所有的修改器只允许执行一次,上面的问题就不复存在了。但好像又带来了一个新的问题,很多时候,你也许想在模型的事件中对数据进行修改。 ``` User::beforeUpdate(function($user) { $user->password = md5('think'); }); ``` 会发现,在模型`beforeUpdate`事件中,数据的值怎么都修改不了,原因是模型的修改器之前在第一次赋值的时候已经执行了,第二次再赋值的时候已经无效了(不会再执行)。 解决办法就是我前面提过的使用`data`方法不调用修改器进行数据赋值操作。 ``` User::beforeUpdate(function($user) { $user->data('password', md5('think')); }); ``` 当然,更好的建议是规划好修改器、自动完成和模型事件的数据处理机制,不要对一个字段同时使用多重机制修改数据,并且写入数据库的数据**应该并且只有**修改器这一个途径进行数据**修改**操作。 ## 类型自动转化 如果你的修改器仅仅是对数据做类型转换处理的话,可以无需定义修改器,而是直接定义字段类型就可以了。 ``` public function setScoreAttr($value, $data) { return (float) $score; } ``` 上面的修改器方法可以直接改成 ``` protected $type = [ 'score' => 'float', ]; ``` 如果你同时对一个字段定义了修改器和类型的话,修改器是优先的。 >[info] 类型定义不仅能定义简单的数据类型,还有一些额外的用途,例如:`json` 类型、`array`类型和`object` 类型会进行`JSON`序列化,`serialize`类型则会把数据进行`serialize`序列化。