在模型基礎定義與CURD操作之上,ThinkPHP 模型還提供了多種高級特性,用於精細化控制數據流程與業務邏輯。這些特性通過優雅的封裝,進一步簡化開發複雜度,提升數據操作的安全性與可維護性。本文作為模型系列文章的第二篇,將系統學習獲取器、修改器、搜索器的數據轉換機制,數據集與只讀字段的數據保護策略,軟刪除的場景應用,以及字段映射、類型轉換、模型輸出和模型事件等綜合功能。本篇文章將記錄這些高級特性使用的學習過程。

一、獲取器

1、獲取原始數據

2、動態獲取器

3、查詢結果處理

二、修改器

1、批量修改

三、搜索器

四、數據集

1、批量刪除和更新數據

五、只讀字段

六、軟刪除

七、字段映射

八、類型轉換

九、模型輸出

1、數組轉換

十、模型事件

1、模型事件定義

2、寫入事件


一、獲取器

獲取器的作用是對模型實例的原始數據做出自動處理。一個獲取器對應模型的一個特殊方法(該方法必須為 public 類型),方法命名規範為:getFieldNameAttr。

FieldName

  • 模型的數據對象取值操作($model->field_name)
  • 模型的序列化輸出操作($model->toArray() 及 toJson())
  • 顯式調用 getAttr

獲取器的場景包括:

  • 時間日期字段的格式化輸出
  • 集合或枚舉類型的輸出
  • 數字狀態字段的輸出
  • 組合字段的輸出

例如,需要對狀態值進行轉換,可以這樣做:

<?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

$user = User::find(1);
echo $user->status; // 例如輸出“正常”

獲取器還可以定義數據表中不存在的字段,例如:

<?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 字段的值了,例如:

$user = User::find(1);
echo $user->status_text; // 例如輸出“正常”

1、獲取原始數據

如果在定義了獲取器的情況下,你希望獲取數據表中的原始數據,可以使用 getData([字段])

$user = User::find(1);
// 通過獲取器獲取字段,傳入需要獲取的字段名稱
echo $user->status;
// 獲取原始字段數據
echo $user->getData('status');
// 獲取全部原始數據
dump($user->getData());

2、動態獲取器

可以支持對模型使用動態獲取器,無需在模型類中定義獲取器方法。動態獲取器需要使用 withAttr()

User::withAttr('status', function($value) {
    $status = [-1 => '刪除', 0=> '禁用', 1=> '正常', 2=> '待審核'];
    return $status[$value];
})->select();

withAttr 方法支持多次調用,定義多個字段的獲取器。另外注意,withAttr 方法之後不能再使用模型的查詢方法,必須使用 Db 類的查詢方法。並且動態獲取器定義後會在模型輸出的時候自動追加,無需手動調用 append

注意:如果同時在模型裏面定義了相同字段的獲取器,則動態獲取器優先,也就是可以臨時覆蓋定義某個字段的獲取器。

3、查詢結果處理

如果需要處理多個字段的數據或者增加虛擬字段的話,可以使用 filter

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
namespace app\model;

use think\model;

class User extends Model {
    // 對 name 字段的值進行小寫處理
    public function setNameAttr($value)
    {
        return strtolower($value);
    }
}

如下代碼實際保存到數據庫中的時候會轉為小寫:

$user = new User();
$user->name = 'THINKPHP';
$user->save();
echo $user->name; // 輸出:thinkphp

修改器方法的第二個參數會自動傳入當前的所有數據數組。

1、批量修改

除了賦值的方式可以觸發修改器外,還可以用下面的方法批量觸發修改器:

$user = new User();
$data['name'] = 'THINKPHP';
$user->data($data, true);
$user->save();
echo $user->name; // 輸出:thinkphp

或者直接使用 save

$user = new User();
$data['name'] = 'THINKPHP';
$user->save($data);
echo $user->name; // 輸出:thinkphp

注意:修改器方法僅對模型的寫入方法有效,調用數據庫的寫入方法寫入無效。

三、搜索器

搜索器的作用是用於封裝字段(或者搜索標識)的查詢條件表達式,一個搜索器對應一個特殊的方法(該方法必須是 public 類型),方法命名規範為:searchFieldNameAttr。

FieldName 為數據表字段的駝峯轉換,搜索器僅在調用 withSearch

搜索器的場景包括:

  • 限制和規範表單的搜索條件
  • 預定義查詢條件簡化查詢

例如,我們需要給 User 模型定義 name 字段的搜索器,可以使用:

<?php
namespace app\model;

use think\model;

class User extends Model {
    public function searchNameAttr($query, $value, $data)
    {
        $query->where('name', 'like', '%' . $value . '%');
    }
}

然後,我們可以使用下面的查詢方法:

User::withSearch(['name'], [
    'name' => 'zhangsan',
    'status' => 1
])->select();

默認情況下,搜索器會首先檢查數據是否存在(如果不存在該項數據則跳過),最終生成的 SQL 語句為:

SELECT * FROM `user` WHERE  `name` LIKE '%zhangsan%'

可以看到查詢條件中並沒有 status 字段的數據,因此可以很好的避免表單的非法查詢條件傳入,在這個示例中僅能使用 name 條件進行查詢。

可以看到查詢條件中並沒有 status 字段的數據,因此可以很好的避免表單的非法查詢條件傳入,在這個示例中僅能使用 name 條件進行查詢。

搜索器通常會和查詢範圍進行比較,搜索器無論定義了多少,只需要一次調用,查詢範圍如果需要組合查詢的時候就需要多次調用。

四、數據集

模型的 select 查詢方法返回數據集對象 think\model\Collection,該對象繼承自 think\Collection,因此具有數據庫的數據集類的所有方法,而且還提供了額外的模型操作方法。

數據集基本用法和數組一樣,例如可以遍歷和直接獲取某個元素。

// 模型查詢返回數據集對象
$list = User::where('id', '>', 0)->select();
// 獲取數據集的數量
echo count($list);
// 直接獲取其中的某個元素
dump($list[0]);
// 遍歷數據集對象
foreach ($list as $user) {
    dump($user);
}
// 刪除某個元素
unset($list[0]);

需要注意的是,如果要判斷數據集是否為空,不能直接使用 empty 方法判斷,而必須使用數據集對象的 isEmpty

$users = User::select();
if($users->isEmpty()){
    echo '數據集為空';
}

可以使用模型的 hidden / visible / append / withAttr

// 模型查詢返回數據集對象
$list = User::where('id', '>', 0)->select();
// 對輸出字段進行處理
$list->hidden(['password']) 
    	->append(['status_text'])
        ->withAttr('name', function($value, $data) {
            return strtolower($value);
        });
dump($list);

如果需要對數據集的結果進行篩選,可以使用 where

// 模型查詢返回數據集對象
$list = User::where('id', '>', 0)->select()
    ->where('name', 'think')
    ->where('score', '>', 80);
dump($list);

支持 whereLike / whereIn / whereBetween 等快捷方法,也支持數據集的 order

支持數據集的 diff(差集) / intersect(交集)

// 模型查詢返回數據集對象
$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、批量刪除和更新數據

支持對數據集的數據進行批量刪除和更新操作,例如:

$list = User::where('status', 1)->select();
$list->update(['name' => 'php']);

$list = User::where('status', 1)->select();
$list->delete();

五、只讀字段

只讀字段用來保護某些特殊的字段值不被更改,這個字段的值一旦寫入,就無法更改。 要使用只讀字段的功能,我們只需要在模型中定義 readonly

<?php
namespace app\model;

use think\model;

class User extends Model {
    protected $readonly = ['name'];
}

例如,上面定義了當前模型的 name

下面通過舉例説明:

$user = User::find(1);
// 更改某些字段的值
$user->name = 'thinkphp';
$user->age = 22;
// 保存更改後的用户數據
$user->save();

由於我們對 name 字段設置了只讀,因此只有 age 字段的值被更新了,而 name

也支持動態設置只讀字段,動態設置只讀字段需要使用 readonly

$user = User::find(1);
// 更改某些字段的值
$user->name = 'thinkphp';
$user->age = 25;
// 保存更改後的用户數據
$user->readonly(['name'])->save();

只讀字段僅針對模型的更新方法,如果使用數據庫的更新方法則無效。

六、軟刪除

在實際項目中,對數據頻繁使用刪除操作會導致性能問題,軟刪除的作用就是把數據加上刪除標記,而不是真正的刪除,同時也便於需要的時候進行數據恢復。

要使用軟刪除功能,需要引入 SoftDelete

<?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
namespace app\model;

use think\model;
use think\model\concern\SoftDelete;

class User extends Model {
    use SoftDelete;
    protected $deleteTime = 'delete_time';
    protected $defaultSoftDelete = 0;
}

定義好模型後,我們就可以對數據使用軟刪除了。例如:

// 軟刪除
User::destroy(1);
// 真實刪除
User::destroy(1, true);

$user = User::find(1);
// 軟刪除
$user->delete();
// 真實刪除
$user->force()->delete();

默認情況下查詢的數據不包含軟刪除數據,如果需要包含軟刪除的數據,可以使用 withTrashed

User::withTrashed()->find();
User::withTrashed()->select();

如果只需要查詢軟刪除的數據,可以使用 onlyTrashed

User::onlyTrashed()->find();
User::onlyTrashed()->select();

restore

$user = User::onlyTrashed()->find(1);
$user->restore();

軟刪除的刪除操作僅對模型的刪除方法有效,如果直接使用數據庫的刪除方法則無效。

七、字段映射

可以統一定義模型屬性的字段映射。例如下面的定義把數據表 name 字段映射為模型的 nickname 屬性。

<?php
namespace app\model;

use think\model;

class User extends Model {
    protected $mapping = [
        'name' => 'nickname' // 數據表 name 字段映射為模型的 nickname 屬性
    ];
}

查詢 User 模型數據後(包括數據集)獲取該屬性或模型輸出的時候,會自動處理映射字段。例如:

$user = User::find(1);
echo $user->nickname; 
dump($user->toArray());

寫入或更新數據的時候,也會自動處理映射字段。例如:

$user = User::find(1);
$user->nickname = 'new nickname';
$user->save();

注意:字段映射後獲取和設置映射字段的值的時候,字段名必須和映射名保持一致。

也可以在查詢的時候動態設置字段映射

User::where('status', 1)->select()->mapping(['name' => 'nickname']);

八、類型轉換

模型支持給字段設置類型自動轉換,會在寫入和讀取的時候自動進行類型轉換處理。例如:

<?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
namespace app\model;

use think\Model;

class User extends Model 
{
    protected $dateFormat = 'Y/m/d'; // 定義時間字符串格式
    protected $type = [
        'status'    =>  'integer'
    ];
}

或者在類型轉換定義的時候這樣做:

<?php
namespace app\model;

use think\Model;

class User extends Model 
{
    protected $type = [
        'status'    =>  'integer',
        'birthday'  =>  'timestamp:Y/m/d'
    ];
}

九、模型輸出

模型數據的模板輸出可以直接把模型對象實例賦值給模板變量,在模板中可以直接輸出。例如:

<?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();
    }
}

在模板文件中可以獲取該模型數據。例如:

<!-- 模板文件為 index.html -->
{$user.name}
{$user.email}

模板中的模型數據輸出一樣會調用獲取器。

1、數組轉換

可以使用 toArray

$user = User::find(1);
dump($user->toArray());

支持使用 hidden

$user = User::find(1);
dump($user->hidden(['create_time', 'update_time'])->toArray());

數組輸出的字段值會經過獲取器的處理,也可以支持追加其它獲取器定義(不在數據表字段列表中)的字段。例如:

$user = User::find(1);
dump($user->append(['status_text'])->toArray());

支持使用 visible

$user = User::find(1);
dump($user->visible(['id', 'name'])->toArray());

對於數據集結果一樣可以直接使用 append、visible 和 hidden 方法。可以在查詢之前定義 hidden / visible / append

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

註冊的回調方法支持傳入一個參數(當前的模型對象實例),但支持依賴注入的方式增加額外參數。可以支持直接通過事件監聽和訂閲。

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
namespace app\model;

use think\Model;

class User extends Model
{
    public static function onBeforeUpdate($user)
    {
    	if ('thinkphp' == $user->name) {
        	return false;
        }
    }
}

參數是當前的模型對象實例,支持使用依賴注入傳入更多的參數。

如果當前操作無需響應事件,可以使用 withEvent

示例

$user = User::find(1);
$user->name = 'thinkphp';
$user->withEvent(false)->save();

2、寫入事件

onBeforeWrite 和 onAfterWrite

// 執行 onBeforeWrite
// 如果事件沒有返回`false`,那麼繼續執行
// 執行新增或更新操作(onBeforeInsert/onAfterInsert或onBeforeUpdate/onAfterUpdate)
// 新增或更新執行成功
// 執行 onAfterWrite

注意:模型的新增或更新是自動判斷的。