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

前言

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

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

官方文檔

從 Laravel 官方文檔的中文翻譯中,咱們能夠找到關於多態關聯的內容。git

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

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

image.png

單從文檔來講,若是你的設計或者你以前的設計符合官方的要求以及要求。 *_type 的值必須爲被關聯的模型的類名數據庫

開始操做

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

  • 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 中打開並斷點調試。oop

image-20191027150847436.png

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

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 字段值變成了字符串,而不是原來的數字 一、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 後綴。

後期補充

2019-10-29

發現通過上面一番操做後,使用 whereHasMorph 方法進行篩選時,type 的值變成了 getAttribute 的值。這時候只須要在被關聯的模型 、「Food」和「Tool」 中重寫 getMorphClass ,返回值分別爲其 type 映射前的值 一、2 便可。

// Tool
public function getMorphClass()
{
    return 1;
}

結束

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

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

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

參考資料

相關文章
相關標籤/搜索