Laravel 如何在坑中掌握模型屬性 $casts 和 $appends 的正確使用姿勢

關(guān)于標(biāo)題產(chǎn)生的兩個原因:一定來源于工作真實案列

原文鏈接

  1. 第一種情況是有一個 mobile 新增入庫成功,編輯時獲取到的mobile 為空斋陪,編輯時數(shù)據(jù)修改了,吧之前的數(shù)據(jù)給覆蓋了舔箭,這種問題已經(jīng)相當(dāng)嚴重了 菱阵?:rage: 【這種是 appends 影響】
  2. 第二種當(dāng)我們編輯一條數(shù)據(jù)踢俄,發(fā)現(xiàn)傳值了,save() 之后卻發(fā)現(xiàn) 字段還是初始值 未更新晴及, 這種一般會發(fā)生在 json都办、arrayobject 這兩種數(shù)據(jù)類型上 【這種是casts 影響】

以下均是測試案例虑稼,模擬工作中使用場景

  • 下面將從上面兩種情況介紹一下這個位置 我們要如何正確使用 $casts$appends| setAppends() 琳钉,使得我們能夠正確的拿到使用的姿勢。

  • 數(shù)據(jù)庫字段(測試表[wecaht_users])

CREATE TABLE `wechat_users` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `nickname` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `mobile` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `avatar` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `custom` json DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
  • 本次測試所用的模型 [WechatUser]
<?php

namespace App\Model;

use Illuminate\Database\Eloquent\Model;

class WechatUser extends Model
{
    use CommonTrait;
    //
    protected $fillable = [
        'nickname',
        'mobile',
        'avatar'
    ];

    public function getTestAttribute($value)
    {
        return $value;
    }
}
  • 模型引用的 Trait
<?php

namespace App\Model;

trait CommonTrait
{
    public function getMobileAttribute($value)
    {
        return $value;
    }

    public function setMobileAttribute($value)
    {
        $this->attributes['mobile'] = $value;
    }
}

產(chǎn)生問題的姿勢:(錯誤姿勢蛛倦,禁止這樣子使用)

一歌懒、setAppends 觸發(fā)的系統(tǒng)bug和注意事項

  • 工作中的用法(模擬):我們在 公用 CommonTrait 內(nèi)重寫了mobile 字段的 gettersetter 方法, 實際工作中不一定是這個字段,這個是舉例使用溯壶,為什么會這么做及皂,因為項目中這個trait 是只要你引入,只需要在主表 加上對應(yīng)的字段且改, 字段內(nèi)的邏輯用的是 trait 控制的验烧,因為工作中沒有注意到 trait 中的操作,在用戶編輯數(shù)據(jù)時 有一段代碼如下:
$user = WechatUser::query()->find(1);
    $user->setAppends([
        'mobile',
        'test'
    ]);

如上操作導(dǎo)致詳情獲取到 mobile 字段為空又跛,用戶編輯打開什么也沒操作碍拆,直接點擊表單提交入庫, 這個時候數(shù)據(jù)庫發(fā)現(xiàn) mobile 空了慨蓝,產(chǎn)生這么大的問題倔监,開發(fā)能不慌嗎,就趕緊查看這個問題菌仁,那么你說為什么會有人 做這個操作浩习, 其實也是沒完全理解 setAppend() 這個函數(shù)做了什么操作

  • 我的排查問題思路:
    • 因為實際數(shù)據(jù)庫在查詢位置我調(diào)試還有數(shù)據(jù)
    • 一開始我以為是字段額外操作了,看了下查詢的邏輯并沒有對字段做處理济丘,但是在最后看到一個操作
$user->setAppends([
        'mobile',
        'test'
    ]);
  • 我就直接定位這個地方的數(shù)據(jù)處理了谱秽,導(dǎo)致后續(xù)的問題
  • 下面是為什么執(zhí)行了 setAppend 之后空了
/**
     * 將模型的屬性轉(zhuǎn)成數(shù)組結(jié)構(gòu)(我們在查詢到結(jié)果時,這個地方都會執(zhí)行一步操作)
     *
     * [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) array
     */
    public function attributesToArray()
    {
        // 處理需要轉(zhuǎn)換成時間格式的屬性
        $attributes = $this->addDateAttributesToArray(
            $attributes = $this->getArrayableAttributes()
        );
        // 這一步就是將變異屬性轉(zhuǎn)成數(shù)組
        $attributes = $this->addMutatedAttributesToArray(
            $attributes, $mutatedAttributes = $this->getMutatedAttributes()
        );

        // 將模型的屬性和變異屬性(重寫了get和set 操作)進行參數(shù)類型處理
        $attributes = $this->addCastAttributesToArray(
            $attributes, $mutatedAttributes
        );

       // 關(guān)鍵的一步摹迷,也正是我們出問題的地方疟赊,獲取到所有的appends 追加的字段 這個地方包含 模型默認設(shè)置的 $appends 屬性的擴充字段,這個位置是 key 是字段 可以看到value 都是 null , 因為 我們所用的 mobile 是系統(tǒng)字段峡碉, 所以這一步銷毀了我們的value ,導(dǎo)致了我們的后續(xù)問題近哟,那么這個地方應(yīng)該怎么用, 咱們?nèi)シ治鲆幌逻@個地方的調(diào)用
    foreach ($this->getArrayableAppends() as $key) {
            $attributes[$key] = $this->mutateAttributeForArray($key, null);
        }

        return $attributes;
    }
  • 下面具體分析一下 append 字段該怎么去用鲫寄,以及下面這段實行了什么

  •      foreach ($this->getArrayableAppends() as $key) {
             $attributes[$key] = $this->mutateAttributeForArray($key, null);
         }
         ````
    
  • $this->mutateAttributeForArray($key, null) 這個其實將我們append 字段的修改器返回的內(nèi)容給轉(zhuǎn)成array 形式

        /**
     * 使用其突變體進行陣列轉(zhuǎn)換吉执,獲取屬性值疯淫。
     *
     * @param  string  $key
     * @param  mixed  $value
     * [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) mixed
     */
    protected function mutateAttributeForArray($key, $value)
    {
        $value = $this->mutateAttribute($key, $value);

        return $value instanceof Arrayable ? $value->toArray() : $value;
    }
    // 這個是獲取我們自定義的變異屬性 默認是我們模型定義了這個 `getMobileAttribute($value)` 的修改器
    protected function mutateAttribute($key, $value)
{
    return $this->{'get'.Str::studly($key).'Attribute'}($value);
}
  • 相比到這里都明白了,為什么這個位置 mobile 會返回空了吧
  • laravel 其實這個位置是讓我們在模型上追加以外的字段的戳玫,所以給我們默認傳的 null 這個熙掺,所以我們不能修改模型已有的屬性,這樣子會打亂我們的正常數(shù)據(jù)咕宿,也不能這么使用币绩,騷操作雖然好用,但是要慎用府阀,使用不好就是坑
    • 模型屬性定義的 $append 原理一樣缆镣,我們一定不要再 appends 里面寫數(shù)據(jù)庫字段,一定不要寫试浙,這個是給別人找麻煩

二董瞻、$casts 類型轉(zhuǎn)換引起的bug,常見問題出在 json 等字段類型映射上

  • 這個問題引起也是因為 我們的 $casts 屬性轉(zhuǎn)換的類型和我們重寫的修改器之后返回的類型不一致導(dǎo)致的,如下我們模型內(nèi)定義為 custom 入庫或者輸出時候 轉(zhuǎn)換成 json 類型:
protected $casts = [
        'custom' => 'json'
    ];

這樣子寫本身也沒問題川队,只要數(shù)據(jù)是數(shù)組格式,自動轉(zhuǎn)成json 格式入庫睬澡,這個要個前端約定好固额,否則可能出現(xiàn)想不到的數(shù)據(jù)異常,假設(shè)我們現(xiàn)在沒有在模型重寫 customgetCustomAttributesetCustomAttribute 這兩個修改器方法煞聪, 這個位置在laravel 中默認處理的方式如下:

有數(shù)據(jù)入庫時會觸發(fā)模型的 save 方法 【laravel 源碼如下】:

/**
     * Save the model to the database.
     *
     * @param  array  $options
     * [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) bool
     */
    public function save(array $options = [])
    {
        $query = $this->newModelQuery();

        // If the "saving" event returns false we'll bail out of the save and return
        // false, indicating that the save failed. This provides a chance for any
        // listeners to cancel save operations if validations fail or whatever.
        if ($this->fireModelEvent('saving') === false) {
            return false;
        }

        // If the model already exists in the database we can just update our record
        // that is already in this database using the current IDs in this "where"
        // clause to only update this model. Otherwise, we'll just insert them.
        if ($this->exists) {
            $saved = $this->isDirty() ?
                        $this->performUpdate($query) : true;
        }

        // If the model is brand new, we'll insert it into our database and set the
        // ID attribute on the model to the value of the newly inserted row's ID
        // which is typically an auto-increment value managed by the database.
        else {
            $saved = $this->performInsert($query);

            if (! $this->getConnectionName() &&
                $connection = $query->getConnection()) {
                $this->setConnection($connection->getName());
            }
        }

        // If the model is successfully saved, we need to do a few more things once
        // that is done. We will call the "saved" method here to run any actions
        // we need to happen after a model gets successfully saved right here.
        if ($saved) {
            $this->finishSave($options);
        }

        return $saved;
    }

我們這里只看 更新操作 有個核心函數(shù): $this->isDirty() 檢測是否有需要更新的字段斗躏,這個函數(shù)又處理了什么操作呢:

/**
     * Determine if the model or any of the given attribute(s) have been modified.
     *
     * @param  array|string|null  $attributes
     * [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) bool
     */
    public function isDirty($attributes = null)
    {
        return $this->hasChanges(
            $this->getDirty(), is_array($attributes) ? $attributes : func_get_args()
        );
    }

hasChanges 這個主要是判斷一下是否有變更,我們主要看 $this->getDirty() 這個里面的操作昔脯,為什么我們會深入到這里去查這個問題啄糙,因為數(shù)據(jù)庫記錄能否更新和這個息息相關(guān), getDirty() 方法內(nèi)又是怎么操作呢

/**
     * Get the attributes that have been changed since last sync.
     *
     * [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) array
     */
    public function getDirty()
    {
        $dirty = [];

        foreach ($this->getAttributes() as $key => $value) {
            if (! $this->originalIsEquivalent($key, $value)) {
                $dirty[$key] = $value;
            }
        }

        return $dirty;
    }

// 接下來的處理是調(diào)用 $this->originalIsEquivalent($key, $value)
/**
     * Determine if the new and old values for a given key are equivalent.
     *
     * @param  string  $key
     * @param  mixed  $current
     * [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) bool
     */
    public function originalIsEquivalent($key, $current)
    {
        if (! array_key_exists($key, $this->original)) {
            return false;
        }

        $original = $this->getOriginal($key);

        if ($current === $original) {
            return true;
        } elseif (is_null($current)) {
            return false;
        } elseif ($this->isDateAttribute($key)) {
            return $this->fromDateTime($current) ===
                   $this->fromDateTime($original);
        } elseif ($this->hasCast($key, ['object', 'collection'])) {
            return $this->castAttribute($key, $current) ==
                $this->castAttribute($key, $original);
        } elseif ($this->hasCast($key, ['real', 'float', 'double'])) {
            if (($current === null && $original !== null) || ($current !== null && $original === null)) {
                return false;
            }

            return abs($this->castAttribute($key, $current) - $this->castAttribute($key, $original)) < PHP_FLOAT_EPSILON * 4;
        } elseif ($this->hasCast($key)) {
            return $this->castAttribute($key, $current) ===
                   $this->castAttribute($key, $original);
        }

        return is_numeric($current) && is_numeric($original)
                && strcmp((string) $current, (string) $original) === 0;
    }

這個時候我們要排查我們 模型內(nèi)定義的 casts 轉(zhuǎn)換的字段默認會執(zhí)行如下代碼:

elseif ($this->hasCast($key)) {
            return $this->castAttribute($key, $current) ===
                   $this->castAttribute($key, $original);
        }

這個地方有個類型處理器 【castAttribute】:

/**
     * Cast an attribute to a native PHP type.
     *
     * @param  string  $key
     * @param  mixed  $value
     * [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) mixed
     */
    protected function castAttribute($key, $value)
    {
        if (is_null($value)) {
            return $value;
        }

        switch ($this->getCastType($key)) {
            case 'int':
            case 'integer':
                return (int) $value;
            case 'real':
            case 'float':
            case 'double':
                return $this->fromFloat($value);
            case 'decimal':
                return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
            case 'string':
                return (string) $value;
            case 'bool':
            case 'boolean':
                return (bool) $value;
            case 'object':
                return $this->fromJson($value, true);
            case 'array':
            case 'json':
                return $this->fromJson($value);
            case 'collection':
                return new BaseCollection($this->fromJson($value));
            case 'date':
                return $this->asDate($value);
            case 'datetime':
            case 'custom_datetime':
                return $this->asDateTime($value);
            case 'timestamp':
                return $this->asTimestamp($value);
            default:
                return $value;
        }
    }

到這個位置我們大概就知道我們所定義的 casts 類型到底在什么時候幫我們執(zhí)行數(shù)據(jù)轉(zhuǎn)換了云稚, 入庫的前一步操作隧饼,而我們往往不注意開發(fā)的時候,問題也就出在這個地方

出問題原因:

  1. 我們定義了 custom => json 類型 静陈,本身我們要求前端傳過來的是一個數(shù)組ID燕雁,后端轉(zhuǎn)成 逗號拼接入庫,這個時候由于開發(fā)沒有前后端統(tǒng)一鲸拥,出現(xiàn)了更新不上的問題 拐格,但是這個時候因為我們這個模型繼承的父類模型 又是有個修改器,如 getCustomAttribute 返回是一個字符串刑赶, 但是 我們最終在 $this->fromJson($value); 時候因為value 的非法捏浊,導(dǎo)致json_encode 失敗,返回了 false
/**
     * Decode the given JSON back into an array or object.
     *
     * @param  string  $value
     * @param  bool  $asObject
     * [[@return](https://learnku.com/users/31554)](https://learnku.com/users/31554) mixed
     */
    public function fromJson($value, $asObject = false)
    {
        return json_decode($value, ! $asObject);
    }

而模型內(nèi)的 getCustomAttribute 里面代碼是如下格式:

public function setCustomAttribute($value)
    {
        if ($value) {
            $value = implode(',', $value);
        }

        $this->attributes['custom'] = $value;
    }

這個是否修改器內(nèi)的值已經(jīng)不是數(shù)組了撞叨, 是一個字符串金踪,這個是否 執(zhí)行 fromJson 就會返回 false
下面這個條件就會一直返回 true , 默認相等了 浊洞,然后上面! $this->originalIsEquivalent($key, $value)的就會認為 這個字段 新值和舊數(shù)據(jù) 相等,不需要更新

$this->castAttribute($key, $current) ===
                   $this->castAttribute($key, $original)

因為 save 這個位置是只更新變更的數(shù)據(jù)字段热康,沒有變更的默認舍棄沛申,所以就出現(xiàn)我們項目中遇到的一個問題,一直不被更新姐军,排查到這個問題铁材,就趕緊更新了代碼

  • 這個位置的注意事項咱們要記一下 【最好是根據(jù)自己的需要寫】
      1. 如果前端提交的參數(shù) 正好是我們想要的,我們直接定義 $casts 字段類型奕锌,就不用后續(xù)處理轉(zhuǎn)換了著觉。這個時候正常寫 custom => json 就行 【推薦】
      1. 如果針對前端傳過來的參數(shù)不滿意,需要特殊處理成我們想要的惊暴, 也就是我們現(xiàn)在所做的操作 重寫了 setCustomAttribute 修改器饼丘, 在這個位置直接處理成我們要入庫的數(shù)據(jù)類型和類型就行 【推薦】
      1. 模型已經(jīng)定義了 $casts 針對 custom => json 類型的轉(zhuǎn)換 ,這個時候又在模型 重新定義了setCustomAttribute 修改器辽话,也是當(dāng)前我們項目中這么做出現(xiàn)bug 的一個原因肄鸽,不是不能這么寫,而是 這個修改器的值類型必須和我們定義的 casts 需要轉(zhuǎn)換的類型保持一致油啤,json 一定要求是對象或者數(shù)組才能序列化典徘,string 不能執(zhí)行這個操作,出現(xiàn)前后不一致的類型益咬,導(dǎo)致數(shù)據(jù)寫入失敗逮诲,這種方式我們需要盡量避免,要么直接用 casts 類型轉(zhuǎn)換幽告, 要么直接定義 修改器修改格式梅鹦, 兩者確實需要用了 一定要保持格式正確

正確姿勢:

  1. 如何正確掌握 $appendssetAppends($appends) 的使用姿勢

    • 如何正確使用
      • 非模型字段
      • 一定要在模型內(nèi)實現(xiàn)變異屬性修改器 如: getTestAttribute($value) , 這樣子我們就能在模型里面動態(tài)追加了
      • 模型的$appends 會在全局追加該屬性,只要有查詢模型的地方冗锁,返回之后都會帶上
      • setAppends 只會在調(diào)用的地方返回追加字段齐唆,其他地方觸發(fā)不會主動返回該字段
  2. 如何正確掌握 $casts 的使用姿勢

    • 如何正確使用
      • 非模型字段, 這個處理只是展示數(shù)據(jù)有影響,不影響我們?nèi)霂鞌?shù)據(jù)
      • 如果合理冻河,盡量不要重寫修改器蝶念, 前端傳入的參數(shù)直接就是我們所要的數(shù)據(jù),限制嚴格一點沒有壞處芋绸, 這個時候我們 直接使用系統(tǒng)的類型轉(zhuǎn)換 媒殉,節(jié)約開發(fā)時間
      • 第三種是我們?nèi)绻惺褂?修改器調(diào)整數(shù)據(jù)格式,那么 $casts 位置就請刪除掉字段類型轉(zhuǎn)換摔敛,因為多人合作廷蓉,避免不掉類型會對不上,針對這種,建議自己寫修改器桃犬,不要添加字段對應(yīng)的轉(zhuǎn)換器刹悴,也是比較推薦的一種

如果哪位在開發(fā)中也有類似的騷操作, 歡迎評論學(xué)習(xí)攒暇。
文中如果錯誤地方土匀,還望各位大佬指正!:stuck_out_tongue:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末形用,一起剝皮案震驚了整個濱河市就轧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌田度,老刑警劉巖妒御,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異镇饺,居然都是意外死亡乎莉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門奸笤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惋啃,“玉大人,你說我怎么就攤上這事监右”呙穑” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵秸侣,是天一觀的道長存筏。 經(jīng)常有香客問我宠互,道長味榛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任予跌,我火速辦了婚禮搏色,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘券册。我一直安慰自己频轿,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布烁焙。 她就那樣靜靜地躺著航邢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪骄蝇。 梳的紋絲不亂的頭發(fā)上膳殷,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音九火,去河邊找鬼赚窃。 笑死册招,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的勒极。 我是一名探鬼主播是掰,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辱匿!你這毒婦竟也來了键痛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤掀鹅,失蹤者是張志新(化名)和其女友劉穎散休,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乐尊,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡戚丸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了扔嵌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片限府。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖痢缎,靈堂內(nèi)的尸體忽然破棺而出胁勺,到底是詐尸還是另有隱情,我是刑警寧澤独旷,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布署穗,位于F島的核電站,受9級特大地震影響嵌洼,放射性物質(zhì)發(fā)生泄漏案疲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一麻养、第九天 我趴在偏房一處隱蔽的房頂上張望褐啡。 院中可真熱鬧,春花似錦鳖昌、人聲如沸备畦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽懂盐。三九已至,卻和暖如春糕档,著一層夾襖步出監(jiān)牢的瞬間莉恼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留类垫,地道東北人司光。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像悉患,于是被迫代替她去往敵國和親残家。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345