在模型基础定义与CURD操作之上,ThinkPHP 模型还提供了多种高级特性,用于精细化控制数据流程与业务逻辑。这些特性通过优雅的封装,进一步简化开发复杂度,提升数据操作的安全性与可维护性。本文作为模型系列文章的第二篇,将系统学习获取器、修改器、搜索器的数据转换机制,数据集与只读字段的数据保护策略,软删除的场景应用,以及字段映射、类型转换、模型输出和模型事件等综合功能。本篇文章将记录这些高级特性使用的学习过程。
一、获取器
获取器的作用是对模型实例的原始数据做出自动处理。一个获取器对应模型的一个特殊方法(该方法必须为 public 类型),方法命名规范为:getFieldName Attr。
FieldName 为数据表字段的驼峰转换,定义了获取器之后会在下列情况自动触发:
- 模型的数据对象取值操作($model->field_name)
- 模型的序列化输出操作($model->toArray() 及 toJson())
- 显式调用 getAttr 方法($this->getAttr('field_name'))
获取器的场景包括:
- 时间日期字段的格式化输出
- 集合或枚举类型的输出
- 数字状态字段的输出
- 组合字段的输出
例如,需要对状态值进行转换,可以这样做:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
public function getStatusAttr($value)
{
$status = [-1 => '删除', 0=> '禁用', 1=> '正常', 2=> '待审核'];
return $status[$value];
}
}
数据表的字段会自动转换为驼峰法,这里 status 字段的值采用数值类型,我们可以通过获取器定义,自动转换为字符串描述。在接下来的使用中,status 字段显示的就是对应的字符串描述,而非数值。
php
$user = User::find(1);
echo $user->status; // 例如输出"正常"
获取器还可以定义数据表中不存在的字段,例如:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
public function getStatusTextAttr($value, $data)
{
$status = [-1 => '删除', 0=> '禁用', 1=> '正常', 2=> '待审核'];
return $status[$data['status']];
}
}
获取器方法的第二个参数传入的是当前所有数据的数组。
此时我们就可以直接使用 status_text 字段的值了,例如:
php
$user = User::find(1);
echo $user->status_text; // 例如输出"正常"
1、获取原始数据
如果在定义了获取器的情况下,你希望获取数据表中的原始数据,可以使用 getData([字段]) 方法:
php
$user = User::find(1);
// 通过获取器获取字段,传入需要获取的字段名称
echo $user->status;
// 获取原始字段数据
echo $user->getData('status');
// 获取全部原始数据
dump($user->getData());
2、动态获取器
可以支持对模型使用动态获取器,无需在模型类中定义获取器方法。动态获取器需要使用 withAttr() 方法,例如:
php
User::withAttr('status', function($value) {
$status = [-1 => '删除', 0=> '禁用', 1=> '正常', 2=> '待审核'];
return $status[$value];
})->select();
withAttr 方法支持多次调用,定义多个字段的获取器。另外注意,withAttr 方法之后不能再使用模型的查询方法,必须使用 Db 类的查询方法。并且动态获取器定义后会在模型输出的时候自动追加,无需手动调用 append 方法。
注意:如果同时在模型里面定义了相同字段的获取器,则动态获取器优先,也就是可以临时覆盖定义某个字段的获取器。
3、查询结果处理
如果需要处理多个字段的数据或者增加虚拟字段的话,可以使用 filter 方法对查询结果原始数据进行统一处理。例如:
php
User::filter(function($user) {
$user->name = 'new value';
$user->test = 'test';
})->select();
注意 :filter 方法的数据处理和获取器并不冲突,filter 处理的数据会改变模型的原始数据,获取器只有在取值或输出的时候才会进行处理。
二、修改器
修改器和获取器相反,修改器的主要作用是对模型设置的数据对象值进行处理,方法的命名规范为:setFieldNameAttr。
修改器的使用场景和读取器类似:
- 时间日期字段的转换写入
- 集合或枚举类型的写入
- 数字状态字段的写入
- 某个字段涉及其它字段的条件或者组合写入
定义了修改器之后会在下列情况下触发:
- 模型对象赋值
- 调用模型的 data 方法,并且第二个参数传入 true
- 调用模型的 appendData 方法,并且第二个参数传入 true
- 调用模型的 save 方法,并且传入数据
- 显式调用模型的 setAttr 方法
- 显式调用模型的 setAttrs 方法,效果与 appendData 并传入 true 的用法相同
示例
php
<?php
namespace app\model;
use think\model;
class User extends Model {
// 对 name 字段的值进行小写处理
public function setNameAttr($value)
{
return strtolower($value);
}
}
如下代码实际保存到数据库中的时候会转为小写:
php
$user = new User();
$user->name = 'THINKPHP';
$user->save();
echo $user->name; // 输出:thinkphp
修改器方法的第二个参数会自动传入当前的所有数据数组。
1、批量修改
除了赋值的方式可以触发修改器外,还可以用下面的方法批量触发修改器:
php
$user = new User();
$data['name'] = 'THINKPHP';
$user->data($data, true);
$user->save();
echo $user->name; // 输出:thinkphp
或者直接使用 save 方法触发,例如:
php
$user = new User();
$data['name'] = 'THINKPHP';
$user->save($data);
echo $user->name; // 输出:thinkphp
注意:修改器方法仅对模型的写入方法有效,调用数据库的写入方法写入无效。
三、搜索器
搜索器的作用是用于封装字段(或者搜索标识)的查询条件表达式,一个搜索器对应一个特殊的方法(该方法必须是 public 类型),方法命名规范为:searchFieldNameAttr。
FieldName 为数据表字段的驼峰转换,搜索器仅在调用 withSearch 方法的时候触发。
搜索器的场景包括:
- 限制和规范表单的搜索条件
- 预定义查询条件简化查询
例如,我们需要给 User 模型定义 name 字段的搜索器,可以使用:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
public function searchNameAttr($query, $value, $data)
{
$query->where('name', 'like', '%' . $value . '%');
}
}
然后,我们可以使用下面的查询方法:
php
User::withSearch(['name'], [
'name' => 'zhangsan',
'status' => 1
])->select();
默认情况下,搜索器会首先检查数据是否存在(如果不存在该项数据则跳过),最终生成的 SQL 语句为:
sql
SELECT * FROM `user` WHERE `name` LIKE '%zhangsan%'
可以看到查询条件中并没有 status 字段的数据,因此可以很好的避免表单的非法查询条件传入,在这个示例中仅能使用 name 条件进行查询。
可以看到查询条件中并没有 status 字段的数据,因此可以很好的避免表单的非法查询条件传入,在这个示例中仅能使用 name 条件进行查询。
搜索器通常会和查询范围进行比较,搜索器无论定义了多少,只需要一次调用,查询范围如果需要组合查询的时候就需要多次调用。
四、数据集
模型的 select 查询方法返回数据集对象 think\model\Collection,该对象继承自 think\Collection,因此具有数据库的数据集类的所有方法,而且还提供了额外的模型操作方法。
数据集基本用法和数组一样,例如可以遍历和直接获取某个元素。
php
// 模型查询返回数据集对象
$list = User::where('id', '>', 0)->select();
// 获取数据集的数量
echo count($list);
// 直接获取其中的某个元素
dump($list[0]);
// 遍历数据集对象
foreach ($list as $user) {
dump($user);
}
// 删除某个元素
unset($list[0]);
需要注意的是,如果要判断数据集是否为空,不能直接使用 empty 方法判断,而必须使用数据集对象的 isEmpty 方法判断,例如:
php
$users = User::select();
if($users->isEmpty()){
echo '数据集为空';
}
可以使用模型的 hidden / visible / append / withAttr 方法进行数据集的输出处理,例如:
php
// 模型查询返回数据集对象
$list = User::where('id', '>', 0)->select();
// 对输出字段进行处理
$list->hidden(['password'])
->append(['status_text'])
->withAttr('name', function($value, $data) {
return strtolower($value);
});
dump($list);
如果需要对数据集的结果进行筛选,可以使用 where 方法:
php
// 模型查询返回数据集对象
$list = User::where('id', '>', 0)->select()
->where('name', 'think')
->where('score', '>', 80);
dump($list);
支持 whereLike / whereIn / whereBetween 等快捷方法,也支持数据集的 order 排序操作。
支持数据集的 diff(差集) / intersect(交集) 操作。
php
// 模型查询返回数据集对象
$list1 = User::where('status', 1)->field('id,name')->select();
$list2 = User::where('name', 'like', 'think')->field('id,name')->select();
// 计算差集
dump($list1->diff($list2));
// 计算交集
dump($list1->intersect($list2));
1、批量删除和更新数据
支持对数据集的数据进行批量删除和更新操作,例如:
php
$list = User::where('status', 1)->select();
$list->update(['name' => 'php']);
$list = User::where('status', 1)->select();
$list->delete();
五、只读字段
只读字段用来保护某些特殊的字段值不被更改,这个字段的值一旦写入,就无法更改。 要使用只读字段的功能,我们只需要在模型中定义 readonly 属性:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
protected $readonly = ['name'];
}
例如,上面定义了当前模型的 name 字段为只读字段,不允许被更改。也就是说当执行更新方法之前会自动过滤掉只读字段的值,避免更新到数据库。
下面通过举例说明:
php
$user = User::find(1);
// 更改某些字段的值
$user->name = 'thinkphp';
$user->age = 22;
// 保存更改后的用户数据
$user->save();
由于我们对 name 字段设置了只读,因此只有 age 字段的值被更新了,而 name 字段的值仍然还是更新之前的值。
也支持动态设置只读字段,动态设置只读字段需要使用 readonly 方法。例如:
php
$user = User::find(1);
// 更改某些字段的值
$user->name = 'thinkphp';
$user->age = 25;
// 保存更改后的用户数据
$user->readonly(['name'])->save();
只读字段仅针对模型的更新方法,如果使用数据库的更新方法则无效。
六、软删除
在实际项目中,对数据频繁使用删除操作会导致性能问题,软删除的作用就是把数据加上删除标记,而不是真正的删除,同时也便于需要的时候进行数据恢复。
要使用软删除功能,需要引入 SoftDelete trait,例如 User 模型按照下面的定义就可以使用软删除功能:
php
<?php
namespace app\model;
use think\model;
use think\model\concern\SoftDelete;
class User extends Model {
use SoftDelete;
protected $deleteTime = 'delete_time';
}
deleteTime 属性用于定义你的软删除标记字段,ThinkPHP 的软删除功能使用时间戳类型(数据表默认值为 Null),用于记录数据的删除时间。还支持 defaultSoftDelete 属性来定义软删除字段的默认值,在此之前的版本,软删除字段的默认值必须为 null。
php
<?php
namespace app\model;
use think\model;
use think\model\concern\SoftDelete;
class User extends Model {
use SoftDelete;
protected $deleteTime = 'delete_time';
protected $defaultSoftDelete = 0;
}
定义好模型后,我们就可以对数据使用软删除了。例如:
php
// 软删除
User::destroy(1);
// 真实删除
User::destroy(1, true);
$user = User::find(1);
// 软删除
$user->delete();
// 真实删除
$user->force()->delete();
默认情况下查询的数据不包含软删除数据,如果需要包含软删除的数据,可以使用 withTrashed 方法进行查询:
php
User::withTrashed()->find();
User::withTrashed()->select();
如果只需要查询软删除的数据,可以使用 onlyTrashed 方法:
php
User::onlyTrashed()->find();
User::onlyTrashed()->select();
restore 方法用于恢复被软删除的数据。例如:
php
$user = User::onlyTrashed()->find(1);
$user->restore();
软删除的删除操作仅对模型的删除方法有效,如果直接使用数据库的删除方法则无效。
七、字段映射
可以统一定义模型属性的字段映射。例如下面的定义把数据表 name 字段映射为模型的 nickname 属性。
php
<?php
namespace app\model;
use think\model;
class User extends Model {
protected $mapping = [
'name' => 'nickname' // 数据表 name 字段映射为模型的 nickname 属性
];
}
查询 User 模型数据后(包括数据集)获取该属性或模型输出的时候,会自动处理映射字段。例如:
php
$user = User::find(1);
echo $user->nickname;
dump($user->toArray());
写入或更新数据的时候,也会自动处理映射字段。例如:
php
$user = User::find(1);
$user->nickname = 'new nickname';
$user->save();
注意:字段映射后获取和设置映射字段的值的时候,字段名必须和映射名保持一致。
也可以在查询的时候动态设置字段映射
php
User::where('status', 1)->select()->mapping(['name' => 'nickname']);
八、类型转换
模型支持给字段设置类型自动转换,会在写入和读取的时候自动进行类型转换处理。例如:
php
<?php
namespace app\model;
use think\model;
class User extends Model {
protected $type = [
'age' => 'integer',
'status' => 'integer'
];
}
数据库查询默认取出来的数据都是字符串类型,如果需要转换为其他的类型,需要设置,支持的类型包括如下类型:
|-----------|---------------------------------------------------------------------------------------------------------|
| 类型 | 描述 |
| integer | 设置为 integer(整型)后,该字段写入和输出的时候都会自动转换为整型。 |
| float | 该字段的值写入和输出的时候自动转换为浮点型。 |
| boolean | 该字段的值写入和输出的时候自动转换为布尔型。 |
| array | 如果设置为强制转换为 array 类型,系统会自动把数组编码为 json 格式字符串写入数据库,取出来的时候会自动解码。 |
| object | 该字段的值在写入的时候会自动编码为 json 字符串,输出的时候会自动转换为 stdclass 对象。 |
| serialize | 指定为序列化类型的话,数据会自动序列化写入,并且在读取的时候自动反序列化。 |
| json | 指定为 json 类型的话,数据会自动 json_encode 写入,并且在读取的时候自动 json_decode 处理。 |
| timestamp | 指定为时间戳字段类型的话,该字段的值在写入时候会自动使用 strtotime 生成对应的时间戳,输出的时候会自动转换为 dateFormat 属性定义的时间字符串格式,默认的格式为 Y-m-d H:i:s。 |
| datetime | 和 timestamp 类似,区别在于写入和读取数据的时候都会自动处理成时间字符串 Y-m-d H:i:s 的格式。 |
如果指定为 timestamp 时间戳字段类型并且希望改变其输出的时间格式字符串,可以按照如下方式进行定义:
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
protected $dateFormat = 'Y/m/d'; // 定义时间字符串格式
protected $type = [
'status' => 'integer'
];
}
或者在类型转换定义的时候这样做:
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
protected $type = [
'status' => 'integer',
'birthday' => 'timestamp:Y/m/d'
];
}
九、模型输出
模型数据的模板输出可以直接把模型对象实例赋值给模板变量,在模板中可以直接输出。例如:
php
<?php
namespace app\controller;
use app\model\User;
use think\facade\View;
class Index
{
public function index()
{
$user = User::find(1);
View::assign('user', $user);
return View::fetch();
}
}
在模板文件中可以获取该模型数据。例如:
html
<!-- 模板文件为 index.html -->
{$user.name}
{$user.email}
模板中的模型数据输出一样会调用获取器。
1、数组转换
可以使用 toArray 方法将当前的模型实例输出为数组。例如:
php
$user = User::find(1);
dump($user->toArray());
支持使用 hidden 方法设置不输出的字段属性:
php
$user = User::find(1);
dump($user->hidden(['create_time', 'update_time'])->toArray());
数组输出的字段值会经过获取器的处理,也可以支持追加其它获取器定义(不在数据表字段列表中)的字段。例如:
php
$user = User::find(1);
dump($user->append(['status_text'])->toArray());
支持使用 visible 方法设置允许输出的字段属性。例如:
php
$user = User::find(1);
dump($user->visible(['id', 'name'])->toArray());
对于数据集结果一样可以直接使用 append、visible 和 hidden 方法。可以在查询之前定义 hidden / visible / append 方法。例如:
php
dump(User::where('id', 10)->hidden(['create_time', 'update_time'])->append(['status_text'])->find()->toArray());
注意 :必须要首先调用一次 Db 类的方法后才能调用 hidden / visible / append 方法。
十、模型事件
模型事件是指在进行模型的查询和写入操作的时候触发的操作行为。模型事件只在调用模型的方法时生效,使用 Db 查询构造器操作是无效的。
模型支持如下事件:
|---------------|--------|-----------------|
| 事件 | 描述 | 事件方法名 |
| AfterRead | 查询后 | onAfterRead |
| BeforeInsert | 新增前 | onBeforeInsert |
| AfterInsert | 新增后 | onAfterInsert |
| BeforeUpdate | 更新前 | onBeforeUpdate |
| AfterUpdate | 更新后 | onAfterUpdate |
| BeforeWrite | 写入前 | onBeforeWrite |
| AfterWrite | 写入后 | onAfterWrite |
| BeforeDelete | 删除前 | onBeforeDelete |
| AfterDelete | 删除后 | onAfterDelete |
| BeforeRestore | 恢复前 | onBeforeRestore |
| AfterRestore | 恢复后 | onAfterRestore |
注册的回调方法支持传入一个参数(当前的模型对象实例),但支持依赖注入的方式增加额外参数。可以支持直接通过事件监听和订阅。
php
Event::listen('app\model\User.BeforeUpdate', function($user) {
//
});
Event::listen('app\model\User.AfterDelete', function($user) {
//
});
如果 before_write、before_insert、 before_update 、before_delete 事件方法中返回 false 或者抛出 think\exception\ModelEventException 异常的话,则不会继续执行后续的操作。
1、模型事件定义
最简单的方式是在模型类里面定义静态方法来定义模型的相关事件响应。
示例
php
<?php
namespace app\model;
use think\Model;
class User extends Model
{
public static function onBeforeUpdate($user)
{
if ('thinkphp' == $user->name) {
return false;
}
}
}
参数是当前的模型对象实例,支持使用依赖注入传入更多的参数。
如果当前操作无需响应事件,可以使用 withEvent 方法关闭。
示例
php
$user = User::find(1);
$user->name = 'thinkphp';
$user->withEvent(false)->save();
2、写入事件
onBeforeWrite 和 onAfterWrite 事件会在新增操作和更新操作时触发,具体的触发顺序为:
php
// 执行 onBeforeWrite
// 如果事件没有返回`false`,那么继续执行
// 执行新增或更新操作(onBeforeInsert/onAfterInsert或onBeforeUpdate/onAfterUpdate)
// 新增或更新执行成功
// 执行 onAfterWrite
注意:模型的新增或更新是自动判断的。