Thinkphp 模型和数据库:模型数据处理
模型提供了比数据库类更为强大的数据处理功能,本章讲解了如何使用模型的各种数据自动处理机制来简化开发,包括模型数据的转换和输出,是模型的核心和必须掌握的部分,学习内容主要包含:
获取器和修改器
获取器和修改器是5.0
模型的核心功能之一,也是数据处理的关键,它们的配合完成了模型数据的输入和输出(自动)处理,但获取器和修改器并非一一对应。
获取器
获取器的作用是对模型的数据对象的(原始)数据做出自动处理。一个获取器对应模型的一个特殊方法,方法格式为:
getFieldNameAttr
FieldName
为数据表字段的驼峰转换,定义了获取器之后会在下列情况自动触发:
- 模型的数据对象取值操作(
$model->field_name
); - 模型的序列化输出操作(
$model->toArray()
); - 显式调用
getAttr
方法($this->getAttr('field_name')
);
获取器的场景包括:
- 时间日期字段的格式化输出;
- 集合或枚举类型的输出;
- 数字状态字段的输出;
- 组合字段的输出;
例如,User模型有一个时间戳类型的birthday
属性,那么如果使用
$user = User::get(1);
// 输出 1234567890
echo $user->birthday;
输出的是一个数字时间戳,并不是标准日期字符串格式,一般的处理是,我们手动处理,例如:
$user = User::get(1);
// 输出 2009-02-14
echo date('Y-m-d', $user->birthday);
这只是举个例子,因为数据的输出处理可能非常的复杂,涉及到多个字段的不同处理方式。
定义模型的读取器可以简化此类操作,例如:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected function getBirthdayAttr($value)
{
return date('Y-m-d', $value);
}
}
getBirthdayAttr
就是一个获取器方法,定义后再次测试下面的示例,结果就不同了。
$user = User::get(1);
// 输出 2009-02-14
echo $user->birthday;
定义了获取器之后,如果要获取原始数据的值怎么办呢?框架提供了getData
方法,用法如下:
$user = User::get(1);
// 输出 2009-02-14
echo $user->birthday;
// 输出 1234567890
echo $user->getData('birthday');
如果要获取数据表的create_time
字段,获取器方法定义方式为:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected function getBirthdayAttr($value)
{
return date('Y-m-d', $value);
}
protected function getCreateTimeAttr($value)
{
return date('Y-m-d H:i:s', $value);
}
}
获取create_time
字段值的时候,使用:
$user = User::get(1);
// 输出 2009-02-14
echo $user->create_time;
// 或者
echo $user->createTime;
获取模型的对象属性的时候驼峰法和小写命名方式都可以取到值。
如果你的获取器方法需要根据其它字段的值来组合,可以给获取器方法添加第二个参数,如下:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected function getBirthdayAttr($value)
{
return date('Y-m-d', $value);
}
protected function getCreateTimeAttr($value)
{
return date('Y-m-d H:i:s', $value);
}
protected function getUserTitleAttr($value,$data)
{
return $data['name'] . ':' . $data['nickname'];
}
}
获取器方法的第二个参数表示当前数据对象的所有数据,数据表并不存在user_title
字段,其实获取器读取的数据对象属性和字段是无关的。
$user = User::get(1);
// 输出 thinkphp:流年
echo $user->user_title;
修改器
和获取器相反,修改器的主要作用是对模型设置的数据对象值进行处理。修改器方法的命名规范为:
setFieldNameAttr
修改器的使用场景和读取器类似:
- 时间日期字段的转换写入;
- 集合或枚举类型的写入;
- 数字状态字段的写入;
- 某个字段涉及其它字段的条件或者组合写入;
定义了修改器之后会在下列情况下触发:
- 模型对象赋值;
- 调用模型的
data
方法,并且第二个参数传入true
; - 调用模型的
save
方法,并且传入数据; - 显式调用模型的
setAttr
方法; - 定义了该字段的自动完成;
还是用之前的例子,比如说你对User模型的birthday
属性设置了一个2017-1-1
的日期字符串,但是数据表的字段类型是时间戳类型的,普通的方式是:
$user = User::get(1);
$user->birthday = strtotime('2017-1-1');
$user->save();
如果使用修改器定义,首先定义
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected function getBirthdayAttr($value)
{
return date('Y-m-d', $value);
}
protected function setBirthdayAttr($value)
{
return strtotime($value);
}
}
通常读取器和修改器都是配套定义的,现在我们不需要对日期数据进行处理了,直接使用:
$user = User::get(1);
$user->birthday = '2017-1-1';
// 实际写入数据表的值是
$user->save();
同样,如果你需要在修改器中使用其它属性的值,可以添加第二个参数,例如:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected function setUserTokenAttr($value, $data)
{
return md5($data['name'] . $data['birthday']);
}
}
这里传入的
data
可能已经经过了其它的修改器操作,并非原始的数据。
定义后的设置代码如下:
$user = User::get(1);
$user->birthday = '2017-1-1';
$user->user_token = null;
$user->save();
由于已经定义了user_token
字段的修改器,而改修改器并没有根据当前字段的值来处理,因此设置任何值都不影响,这里设置为null
。和读取器不同,修改器的属性必须是数据表中存在的字段,否则修改器的值仅仅能作为数据辅助作用。
下面的写法是错误的(会抛出字段不存在的异常)
$user = User::get(1);
$user->birthday = '2017-1-1';
$user->userToken = null;
$user->save();
读取器是可以使用$user->userToken
来获取,为了避免类似问题的困惑,我们的建议是:
- 数据表字段统一使用小写+下划线命名;
- 方法中统一使用驼峰法命名;
- 模型的对象属性统一使用小写+下划线命名;
但实际应用开发过程中,修改器的定义往往少于读取器的定义数量。同时也要避免多次修改的问题。
自动时间字段
由于数据表的时间字段是一个非常普遍的需求,因此框架做了一些强化支持,无需定义获取器和修改器就能完成时间日期类型字段的自动处理。
默认情况下自动写入时间戳字段功能是关闭的,可以在模型里面定义
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
// 开启时间字段自动写入
protected $autoWriteTimestamp = true;
}
开启时间字段(这里的时间字段支持整型、时间戳和日期类型)自动写入后,会默认自动写入两个时间字段:create_time
(创建时间,新增数据的时候自动写入)和update_time
(更新时间,新增和更新的时候都会自动写入),并且以整型类型写入数据库。
如果你的时间字段名不是默认字段,则需要添加属性设置。
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
// 开启时间字段自动写入
protected $autoWriteTimestamp = true;
// 定义时间字段名
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
}
如果你的时间字段类型不是整型,也可以单独设置如下:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
// 开启时间字段自动写入 并设置字段类型为datetime
protected $autoWriteTimestamp = 'datetime';
}
autoWriteTimestamp
属性支持设置的时间字段类型包括:整型(设置为true
的时候使用该类型)、时间(datetime
)和时间戳(timestamp
)。
现在来看一个例子说明自动时间字段的写入
// 新增用户数据
$user = new User;
$user->name = 'thinkphp';
// 会自动写入create_time和update_time字段
$user->save();
echo $user->create_time;
echo $user->update_time;
// 更新用户数据
$user->name = 'topthink';
// 会自动更新update_time字段
$user->save();
echo $user->create_time;
echo $user->update_time;
create_time
和update_time
字段的值不需要进行设置,系统会自动写入。如果你手动进行设置的话,则不会触发自动写入机制(也就是说不会进行时间字段的格式转换),你需要按照实际的字段类型设置。
如果你的时间字段类型为整型,自动写入的时间字段会在获取的时候自动转换为
dateFormat
属性设置的时间格式,所以不需要再次对时间字段进行格式化输出,以免出错。如果不希望自动格式化,可以设置数据库配置参数datetime_format
的值为false
。(时间类型字段则无需更改设置)
我们来改变下时间字段的输出格式,
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
// 开启时间字段自动写入 并设置字段类型为datetime
protected $autoWriteTimestamp = 'datetime';
protected $dateFormat = 'Y/m/d H:i:s';
}
再来看输出的值是否有变化
// 新增用户数据
$user = new User;
$user->name = 'thinkphp';
// 会自动写入create_time和update_time字段
$user->save();
echo $user->create_time;
echo $user->update_time;
// 更新用户数据
$user->name = 'topthink';
// 会自动更新update_time字段
$user->save();
echo $user->create_time;
echo $user->update_time;
上面的设置都是针对单个模型的,如果需要设置全局使用,可以在数据库配置文件中设置下面的参数:
// 开启自动写入时间字段 支持设置字段类型(同前)
'auto_timestamp' => true,
// 时间字段取出后的时间格式
'datetime_format' => 'Y-m-d H:i:s',
如果全局设置开启时间字段自动写入后,部分模型可以单独关闭,例如:
<?php
namespace app\index\model;
use think\Model;
class Data extends Model
{
// 关闭时间字段自动写入
protected $autoWriteTimestamp = false;
}
甚至说部分模型的时间字段名和类型可以单独设置
<?php
namespace app\index\model;
use think\Model;
class Data extends Model
{
// 关闭时间字段自动写入
protected $autoWriteTimestamp = 'datetime';
// 定义时间字段名
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
}
在系统自动时间字段之外的其它时间字段,如果需要自动格式输出,可以设置类型转换,这个下一节就会讲到。
数据类型转换
自动时间字段写入只支持创建时间和更新时间的自动写入和格式化读取,如果你的模型有其它时间字段的话,则可以通过设置类型转换来完成,例如User
模型的birthday
字段也使用了时间类型。
前面我们已经了解了定义修改器和读取器的方式来处理birthday
字段,更简单的办法就是上面这种设置类型转换,免去定义修改器和读取器的麻烦。
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected $type = [
'birthday' => 'datetime:Y/m/d',
];
}
type
属性用于定义类型转换(支持相当多的类型,后面会提及),'datetime:Y/m/d'
表示使用datetime
类型,输出格式为Y/m/d
,下面是一段代码示例。
$user = User::get(1);
// 输出 2009/02/14
echo $user->birthday;
$user->birthday = '2017-1-1';
$user->save();
// 输出 2017/1/1
echo $user->birthday;
类型转换支持的类型设置包括:
integer
设置为integer(整型)后,该字段写入和输出的时候都会自动转换为整型。
float
该字段的值写入和输出的时候自动转换为浮点型。
boolean
该字段的值写入和输出的时候自动转换为布尔型。
array
如果设置为强制转换为array
类型,系统会自动把数组编码为json格式字符串写入数据库,取出来的时候会自动解码。
object
该字段的值在写入的时候会自动编码为json字符串,输出的时候会自动转换为stdclass
对象。
serialize
指定为序列化类型的话,数据会自动序列化写入,并且在读取的时候自动反序列化。
json
指定为json
类型的话,数据会自动json_encode
写入,并且在读取的时候自动json_decode
处理。
timestamp
指定为时间戳字段类型(注意并不是数据库的timestamp
类型,事实上是int
类型)的话,该字段的值在写入时候会自动使用strtotime
生成对应的时间戳,输出的时候会自动转换为dateFormat
属性定义的时间字符串格式,默认的格式为Y-m-d H:i:s
,如果希望改变其他格式,可以定义如下:
class User extends Model
{
protected $dateFormat = 'Y/m/d';
protected $type = [
'status' => 'integer',
'score' => 'float',
'birthday' => 'timestamp',
];
}
或者在类型转换定义的时候使用:
class User extends Model
{
protected $type = [
'status' => 'integer',
'score' => 'float',
'birthday' => 'timestamp:Y/m/d',
];
}
然后就可以
$user = User::find(1);
echo $user->birthday; // 2015/5/1
datetime
和timestamp
类似,区别在于写入和读取数据的时候都会自动处理成时间字符串Y-m-d H:i:s
的格式。
PHP5.6版本以下,数据库查询的字段返回数据类型都是字符串的,在做API开发的时候最好是使用类型转换强制处理下,PHP5.6版本开始,PDO查询的返回数据的字段类型都是实际的字段类型格式。
数据自动完成
前面提到的修改器是在设置数据的时候自动触发,有时候我们希望数据修改是自动触发的,类似于自动时间字段一样(只能自动完成固定时间字段),这就是本节要讲解的数据自动完成的概念。
数据自动完成是依赖修改器的(和3.2版本区别很大),不支持使用函数或者其它回调来自动完成(但可以支持固定值),足见5.0版本是推崇使用修改器。
系统支持auto
、insert
和update
三个属性,可以分别设置写入、新增和更新的时候需要进行自动完成的字段列表(auto
属性包含新增和更新操作),下面是一个示例:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected $auto = ['active_time'];
protected $insert = ['reg_ip', 'status' => 1];
protected $update = [];
}
定义了数据自动完成后不需要手动设置属性,一旦使用手动设置的话,自动完成就会忽略,以避免产生多次处理的数据混乱。
数据转换和输出
数组转换
模型的查询返回数据都是模型的对象实例,其实模型对象在数据存取方面和数组几乎没什么差异,不信的话,你可以测试下下面的一段代码:
$user = User::get(1);
echo $user->name;
echo $user['name'];
$user->name = 'test';
$user->save();
echo $user->name;
$user['name'] = 'test2';
$user->save();
echo $user['name'];
即使在模板中输出,也可以正常使用:
{$user.name}
{$name.email}
答案是由于模型类think\Model
实现了ArrayAccess
接口,所以对象可以采用数组的方式访问。
如果是数据集查询,也是和数组一样foreach
遍历后使用
$list = User::all();
foreach ($list as $user) {
echo $user->name . ':' . $user['name'];
}
// 当然如果你需要也可以直接
echo $list[0]->name . ':' . $list[0]['name'];
所以,看起来模型查询返回的对象对使用上并没有任何的影响。
如果因为某种原因,必须要转换为数组的话,可以使用模型类提供的toArray
方法,用法如下:
$user = User::get(1);
if($user) {
$data = $user->toArray();
}
如果是数据集查询的话有两种情况,由于默认的数据集返回结果的类型是一个数组,因此无法调用toArray
方法,必须先转成数据集对象然后再使用toArray
方法,系统提供了一个collection
助手函数实现数据集对象的转换,代码如下:
$list = User::all();
if($list) {
$list = collection($list)->toArray();
}
如果设置了模型的数据集返回类型的话,则可以简化使用
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected $resultSetType = 'collection';
}
关键代码是在模型中添加下面一行定义
protected $resultSetType = 'collection';
设置后,模型所有的数据集查询返回结果的类型都是think\model\Collection
对象实例。
下面的事情就简单了,直接使用:
$list = User::all();
$list = $list->toArray();
当然toArray
方法不仅仅只是转换一个数组这么简单,我们可以在转换数据的时候进行个别字段的隐藏和追加,涉及到四个方法:
方法 | 说明 |
---|---|
hidden |
设置隐藏的属性 |
visible |
设置输出的属性 |
append |
追加额外的(获取器)属性 |
appendRelationAttr |
追加额外的关联属性 |
这几个方法都是针对模型对象示例的(数据集对象无法使用),前三个方法的参数都是数组,而且默认会和模型类的hidden
、visible
以及append
属性合并。下面是一个代码示例:
$user = User::get(1);
$data = $user->hidden(['id'])->toArray();
dump($data);
$data = $user->visible(['name', 'email'])->toArray();
dump($data);
$data = $user->append(['status_text'])->toArray();
dump($data);
hidden
和visible
方法设置的是当前模型对应的数据表中的字段列表中需要隐藏或者显示的属性,如果不在数据表字段列表中,但设置过获取器的话,可以通过append
方法追加。如果不在字段列表中,也没有设置过任何的获取器,则只能手动赋值,例如:
$user = User::get(1);
$user->hello = 'thinkphp';
$data = $user->toArray();
dump($data);
如果已经在模型中设置了hidden
、visible
或者append
属性,但临时不需要输出,可以在方法中传入第二个参数true
设置为覆盖而不是合并。
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected $hidden = ['id'];
protected $visible = ['name', 'email'];
protected $append = ['level'];
protected function getLevelAttr($value, $data)
{
$score = $data['score'];
if ($score < 100) {
$level = 1;
} elseif ($score < 500) {
$level = 2;
} elseif ($score < 2000) {
$level = 3;
} elseif ($score < 5000) {
$level = 4;
} else {
$level = 5;
}
return $level;
}
}
下面是测试代码:
$user = User::get(1);
$data = $user->hidden(['email'], true)->toArray();
dump($data);
$data = $user->visible(['name'], true)->toArray();
dump($data);
$data = $user->append(['level'], true)->toArray();
dump($data);
appendRelationAttr
方法是用于追加关联对象中的某个属性到当前模型对象属性中,暂且不表,会在关联和聚合章节中给你详细讲解用法。
如果是数据集查询的话,只能通过在模型类中设置属性的方式来设置需要输出的属性,同样是上面的User模型类,用下面的代码测试:
$list = User::all();
$data = $list->toArray();
dump($data);
JSON序列化
除了转换为数组数据外,还支持对模型对象进行JSON
序列化,序列化方法为toJson
,使用方法和toArray
类似,并且调用toJson
序列化之前同样支持hidden
、visible
、append
和appendRelationAttr
方法。
模型事件
虽然说模型的数据自动完成和修改器可以很方便的进行数据处理,但也有自身的不足,而模型事件功能则提供了更灵活和强大的模型数据处理机制。
模型事件可以看成是模型层的钩子和行为,只不过钩子的位置主要针对模型数据的写入操作,包含下面这些:
钩子 | 对应操作 | 快捷注册方法 |
---|---|---|
before_insert | 新增前 | beforeInsert |
after_insert | 新增后 | afterInsert |
before_update | 更新前 | beforeUpdate |
after_update | 更新后 | afterUpdate |
before_write | 写入前 | beforeWrite |
after_write | 写入后 | afterWrite |
before_delete | 删除前 | beforeDelete |
after_delete | 删除后 | afterDelete |
before_write
和after_write
表示无论是新增还是更新都会执行的钩子。
要使用模型事件功能,就必须先给模型注册事件,我们建议在模型类的init
方法中统一注册模型事件(init
静态方法会在实例化模型的时候调用,并且仅会执行一次),下面是一个例子,在写入用户数据的时候。
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected static function init()
{
User::event('before_insert', function ($user) {
$user->reg_ip = request()->ip();
});
User::event('before_write', function ($user) {
$user->name = strtolower($user->name);
});
}
}
为了更简单的定义,系统提供了快捷方法,上面的模型事件注册可以改为:
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
protected static function init()
{
User::beforeInsert(function ($user) {
$user->reg_ip = request()->ip();
});
User::beforeWrite(function ($user) {
$user->name = strtolower($user->name);
});
}
}
在事件方法中可以对当前模型实例做任何的操作,所以理论上来说,修改器和自动完成可以做的事情,模型的事件方法都可以完成,而且具备修改器和自动完成没有的优势,包括:
- 批量完成(修改器只能针对某个字段进行修改);
- 支持判断并自动终止模型数据写入操作;
- 便于统一管理模型数据操作;
不要对一个模型数据同时使用修改器和模型事件
注册的回调方法的第一个参数是当前的模型对象实例,并且所有before
开头的事件方法(包括before_write
、before_insert
、 before_update
、before_delete
)如果返回false
,则写入操作不会执行,并且也不再执行该钩子位置的后续事件操作。
<?php
namespace app\index\model;
use think\Model;
class User extends Model
{
public function isValid()
{
}
protected static function init()
{
User::event('before_insert', function ($user) {
$user->reg_ip = request()->ip();
});
User::event('before_write', function ($user) {
return $user->isValid();
});
}
}
支持给一个位置注册多个回调方法,例如:
User::event('before_insert', function ($user) {
if ($user->status != 1) {
return false;
}
});
// 注册回调到beforeInsert函数
User::event('before_insert', 'beforeInsert');
当同一个钩子注册了多个模型事件的话,在特殊的情况下,你可以传入第二个参数为true
覆盖之前注册的其它模型事件。
User::event('before_insert', function ($user) {
if ($user->status != 1) {
return false;
}
});
// 注册回调到beforeInsert函数 并覆盖前面的
User::event('before_insert', 'beforeInsert',true);
数据分批处理
模型也可以支持对返回的数据分批处理,并且由于返回的是模型对象,更方便进行业务操作,例如:
$count = 0;
User::chunk(100, function ($users) use ($event,$count) {
foreach ($users as $user) {
// 用户活动报名
if ($user->age > 18) {
$user->sign($event);
$count++;
if ($count >= 300) {
// 超过300则不再接受报名
return false;
}
}
}
});
总结
数据写入的自动处理规则及优先顺序如下:
- 时间字段自动格式化写入
- 修改器
- 类型转换
- 模型事件
数据读取的自动处理规则及优先顺序如下:
- 获取器
- 类型转换
- 时间字段自动格式化输出
通过本章的学习,我们基本掌握了模型数据的处理和转换、输出,下一章我们来学习模型的一些高级用法。
上一篇:第五章:模型和对象
下一篇:第七章:模型高级用法