更加順手的用好 Laravel 的多態關聯

前言

在業務中,關聯是我們最常用到的場景。在開發時我們始終都在強調對數據庫設計選擇可解耦,簡潔化,最小化。在這種開發環境下,往往都會將傳統的一個大表拆分成多個小表,這時候關聯就顯得很重要。

MySQL 爲我們提供了像 inner joinleft joinright join 這些關聯方式,滿足了絕大部分需求。但是在實際開發中,我們還是會去選擇一些程序上的關聯關係,讓代碼去處理關聯,這些關聯從簡單的一對一,一對多,再到複雜的多態關聯、中間表關聯,等等,下面主要從源碼的角度去講解一下 Laravel 中的多態關聯

官方文檔

從 Laravel 官方文檔的中文翻譯中,我們可以找到關於多態關聯的內容。

一對一多態關聯與簡單的一對一關聯類似;不過,目標模型能夠在一個關聯上從屬於多個模型。例如,博客 Post 和 User 可能共享一個關聯到 Image 模型的關係。使用一對一多態關聯允許使用一個唯一圖片列表同時用於博客文章和用戶賬戶。

官網的文檔可能不是那麼的直觀,這裏推薦一個 文章 可以幫助你加深理解,這裏就不展開了。

image.png

單從文檔來說,如果你的設計或者你之前的設計符合官方的要求以及要求。 *_type 的值必須爲被關聯的模型的類名

很多時候,我們的設計中 type 都不一定會那樣設計,基本都是以數字爲主,雖然 Laravel 爲我們提供了自定義 type 的解決辦法 ,但是也不能很好的解決關於數字作爲 type 的問題,我還搜索到了一個一樣的問題。那麼我們就來解決一下,現在有三張表。

  • shopping_cart (購物車表)
字段 類型 介紹
id int 主鍵
product_type tinyint(1) 關聯的產品類型 1 表示 Tool、2 表示 Food
product_id int 關聯的產品的ID
  • tool (工具表)
字段 類型 介紹
id int 主鍵ID
name varchar(20) 名字
  • food (食品表)
字段 類型 介紹
id int 主鍵ID
name varchar(20) 名字

現在我們有了這三張表,購物車表中根據 product_type 的不同值去關聯不同的模型,這裏就要用到 多態關聯,現在如果我們直接按照官方的文檔來編寫我們的 Model ,那麼,應該是下面這樣的。

class ShoppingCart extends Model
{
    const TABLE = 'shopping_cart';
    protected $table = self::TABLE;

    public function product()
    {
        return $this->morphTo();
    }
    
}
class Tool extends Model
{
    const TABLE = 'tool';
    protected $table = self::TABLE;

    public function product()
    {
        return $this->morphOne(ShoppingCart::class, 'product');
    }

}
class Food extends Model
{
    const TABLE = 'food';
    protected $table = self::TABLE;

    public function product()
    {
        return $this->morphOne(ShoppingCart::class, 'product');
    }
    
}

根據官方的文檔:模型關聯 |《Laravel 5.8 中文文檔》| Laravel China 社區。我們的代碼應該可以運行,但是可能不符合預期。

image-20191027150453093.png

不出意外的看到了錯誤信息 “類名必須是有效的對象或者字符”,源碼中也是一個 new $class,到 IDE 中打開並斷點調試。

image-20191027150847436.png

此時 $class 爲 1 ,根據調用棧一路往上找,發現了一個有價值的方法。

image-20191027151420811.png

可以看到,這個 $type 是從這裏 $this->dictionary取出來的,按住 Ctrl + 點擊 後到了屬性定義的位置,然後再按住 Ctrl + 點擊 ,選擇上面的篩選賦值操作,可以看到只有一處有賦值的操作,點擊轉到。

image-20191027151751484.png

轉到賦值的位置後,打一個斷點。

image-20191027151949597.png

看到這裏調用棧,源碼 過多,就不展開講解。下面講重點。
image-20191027152116492.png

看到這個屬性,$model->{$this->morphType},先打印它的值$this->morphType,結果是 product_type,然後外層還有 點擊進入按鈕,我們進入到了模型實例中的 __get 魔術方法。

image-20191027152533661.png

image-20191027152622940.png

在官方手冊中,關於 __get 的定義爲:

讀取不可訪問屬性的值時,__get() 會被調用。

首先,對於 Model 而言,是沒有 product_type 屬性的,所以觸發了它,方法內部調用了 getAttribute

image-20191027152924822.png

看到 getAttribute 方法內部,第 321 行,使用了一個屬性 $this->attribute ,執行表達式可以看到,這就是我們的數據結果。而根據 array_key_exists 的判斷可以確定這個 if 是成立的,因爲 後面的是 || 運算,即使後面是 false ,這個表達式也是成立,但是我們這裏還是希望來看一下這個方法。

image-20191027153530439.png

這個方法只做了一件事,就是判斷一個 getter 方法是否存在,這裏的 Str::studly() 的作用是把 字符串從下劃線命名規則轉爲大駝峯。也就是說,在這裏會檢查訪問器 ,當然,現在我們是沒有這個方法的,繼續往下。
image-20191027153637551.png

果然,在 349 ~ 351 行,有着這樣的一個邏輯,那麼我們回過來看一下 Laravel 文檔中關於 修改器 & 訪問器 的介紹。
image-20191027153847195.png

簡而言之就是,當在訪問這個字段的值時,我們可以自己根據獲取器的規則定一個名爲 getProductTypeAttribute 的訪問器方法,在這個方法中,我們可以修改其返回值,作爲最終的結果返回給訪問者。這樣看來,我們就可以在訪問器中修改我們原本的 product_type1 爲對應的需要實例化的類名稱,即可,現在開始定義一下。

public function getProductTypeAttribute($val)
{
    $map = [
        1 => Tool::class,
        2 => Food::class,
    ];
    return $map[$val] ?? Tool::class;
}

根據文檔我們可以得知,在對一個已存在的字段添加訪問器時,訪問器方法可以接受一個參數,其值爲原本值,在這個方法中,我們編寫了一個 $map ,其 key 爲 product_type 字段的原值$val,如果這個字段原值 ($val) ,對應的 key 不存在,就返回默認爲 App\Models\Tool模型類,現在這樣就夠了嗎?我們可以來試試。

image-20191027155028728.png

果然,代碼可以工作了 ,不再報錯,而且,在 relations 屬性中我們還可以看到 product 分別是兩個不同的模型,接下來我們 toArray 看一下結果。

image-20191027155216334.png

果然,結果已經達到了我們的預期,但是我們卻發現 product_type 字段值變成了字符串,而不是原來的數字 1、2,該怎麼辦?兩個辦法。

  • 利用獲取器添加一個輔助字段,來存儲原來的 product_type 。
  • 遍歷重新賦值。

下面來展示一下第二種方法,從上面的截圖中可以瞭解到,查詢結果給我們返回的是一個Eloquent 集合,現在我們使用其中的 transform,方法來轉換原集合。

$list = $cart->with(['product'])->get();
$list->transform(function (ShoppingCart $item) {
    $item->product_type_origin = $item->getOriginal('product_type');
    return $item;
});
dump($list->toArray());

通過模型的 getOriginal 方法拿到了原有的值。
image-20191027160107439.png

到這裏,問題已經解決了,那麼我們可以自定義 productproduct_typeproduct_id 這三個的名字嗎?這一點在 Laravel 文檔中鮮有提到,在這裏答案是可以的。

我們通過 ShoppingCart 模型的 product 方法,這裏我們調用 morphTo 方法沒有傳遞 任何的值。

public function product()
{
    return $this->morphTo();
}

接下來我們進入進入 morphTo 方法,一探究竟。

image-20191027161339955.png

首先映入眼簾的是一段註釋,這段註釋的 大概意思就是,如果沒有指定 $name 那麼就從調用棧中取第一條的 function名字作爲 $name 也就是最終掛載的模型上的字段名字 方法實現如下

protected function guessBelongsToRelation()
{
    [$one, $two, $caller] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);

    return $caller['function'];
}

接着往下看$type$id

protected function getMorphs($name, $type, $id)
{
    return [$type ?: $name.'_type', $id ?: $name.'_id'];
}

可以看到,當我們沒有自己給定 $type$id 時,那麼默認值即爲 $name 分別加上 _type_id 後綴。

結束

至此,文章內容結束了。本文主要涉及 Laravel 中關於 多態關聯獲取器 兩個知識點的瞭解。

文中所使用的調試工具爲 PHPStorm 和 Xdebug 。

文中如有紕漏,請不吝賜教,如文中內容涉及到你的利益,請與我聯繫。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章