PHP实战

Thinkphp 模型和数据库:模型数据处理

2019-07-10  本文已影响0人  寒冬夜行人_51a4

模型提供了比数据库类更为强大的数据处理功能,本章讲解了如何使用模型的各种数据自动处理机制来简化开发,包括模型数据的转换和输出,是模型的核心和必须掌握的部分,学习内容主要包含:

获取器和修改器

获取器和修改器是5.0模型的核心功能之一,也是数据处理的关键,它们的配合完成了模型数据的输入和输出(自动)处理,但获取器和修改器并非一一对应。

获取器

获取器的作用是对模型的数据对象的(原始)数据做出自动处理。一个获取器对应模型的一个特殊方法,方法格式为:

getFieldNameAttr

FieldName为数据表字段的驼峰转换,定义了获取器之后会在下列情况自动触发:

获取器的场景包括:

例如,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

修改器的使用场景和读取器类似:

定义了修改器之后会在下列情况下触发:

还是用之前的例子,比如说你对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_timeupdate_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版本是推崇使用修改器。

系统支持autoinsertupdate三个属性,可以分别设置写入、新增和更新的时候需要进行自动完成的字段列表(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 追加额外的关联属性

这几个方法都是针对模型对象示例的(数据集对象无法使用),前三个方法的参数都是数组,而且默认会和模型类的hiddenvisible以及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);

hiddenvisible方法设置的是当前模型对应的数据表中的字段列表中需要隐藏或者显示的属性,如果不在数据表字段列表中,但设置过获取器的话,可以通过append方法追加。如果不在字段列表中,也没有设置过任何的获取器,则只能手动赋值,例如:

$user        = User::get(1);
$user->hello = 'thinkphp';
$data        = $user->toArray();
dump($data);

如果已经在模型中设置了hiddenvisible或者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序列化之前同样支持hiddenvisibleappendappendRelationAttr方法。

模型事件

虽然说模型的数据自动完成和修改器可以很方便的进行数据处理,但也有自身的不足,而模型事件功能则提供了更灵活和强大的模型数据处理机制。

模型事件可以看成是模型层的钩子和行为,只不过钩子的位置主要针对模型数据的写入操作,包含下面这些:

钩子 对应操作 快捷注册方法
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_writeafter_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_writebefore_insertbefore_updatebefore_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;
            }
        }
    }
});

总结

数据写入的自动处理规则及优先顺序如下:

  1. 时间字段自动格式化写入
  2. 修改器
  3. 类型转换
  4. 模型事件

数据读取的自动处理规则及优先顺序如下:

  1. 获取器
  2. 类型转换
  3. 时间字段自动格式化输出

通过本章的学习,我们基本掌握了模型数据的处理和转换、输出,下一章我们来学习模型的一些高级用法。

上一篇:第五章:模型和对象
下一篇:第七章:模型高级用法

上一篇下一篇

猜你喜欢

热点阅读