Laravel 模型操作中一次奇妙踩坑經歷

finecho發表於2019-03-29

@這是小豪的第十三篇文章

最近被 Laravel 模型中的一些小問題折騰的死去活來的,明明看著很清晰很明瞭的程式碼,卻偏偏不能實現功能,現在帶大家來切身經歷一下這次奇妙的踩坑經歷,程式碼看似很多,實則不多,大家別急著跑,哈哈。

準備

需求: 獲取專案下的所有任務,且需要合併公共任務

邏輯關係:

  • 一個專案有很多工
  • 一個專案有很多專案成員
  • 一個任務有一個執行人 (當任務型別為:1 的時候為公共事務)
  • 一個人有多個專案
  • 一個人有多個任務

前端所需資料格式如下:

{
    "user1": {
        "id": 1,
        "name": "Lhao",
        "email": "lhao@qq.com",
        "email_verified_at": null,
        "created_at": null,
        "updated_at": null,
        "pivot": {
            "project_id": 1,
            "user_id": 1
        },
        "tasks": [
            {
                "id": 1,
                "project_id": 1,
                "user_id": 1,
                "type": 0,
                "name": "task 1",
                "created_at": null,
                "updated_at": null
            }
            ...
        ]
    },
    "user2": {
            ...
    }
}

那我們現在來看看需要用到的各個模型,其中的各種對應關係我就不做講解了哈,上面也有介紹,不太清楚的建議把模型關聯再去細讀一遍:

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Project extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'name',
    ];

    public function users()
    {  
        return $this->belongsToMany(User::class);
    }

    public function tasks()
    {  
        return $this->hasMany(Task::class);
    }
}
namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Task extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'user_id',
        'name',
    ];

    const COMMON_TASK_TYPE = 1;

    public function scopeOfCommonTask(Builder $query)
    {
        return $query->where('type', self::COMMON_TASK_TYPE);
    }

    public function users()
    {  
        return $this->belongsTo(User::class);
    }

    public function project()
    {
        return $this->belongsTo(Project::class);
    }
}
...

class User extends Model
{
    ...

    public function projects()
    {  
        return $this->belongsToMany(Project::class);
    }

    public function tasks()
    {  
        return $this->hasMany(Task::class);
    }

    ...
}

開始

從上面的需求中大家可能會說,獲取專案下的所有任務和公共事務直接通過:

$projectTasks = $project->tasks->merge(Task::ofCommonTask()->get())->groupBy('user_id');

這樣不就可以了嗎,但是這樣有個問題就是資料格式不是前端所需要的,如果我們要轉化成上面的格式的話,還需要獲取使用者資料然後將上面查詢出來的資料塞進去,不太想這麼幹,不夠優雅,哈哈,我打算通過專案獲取到專案成員然後再載入任務資料,最後整合進公共任務,話不多說上程式碼:

public static function getProjectUserTasks(Project $project)
{
    $userTasks = $project->users->load(['tasks' => function ($query) use ($project) {
        $query->where('project_id', $project->id);
    }])->keyBy('name');

    // 不太清楚的 請看 scope 相關知識
    $commonTasks = Task::ofCommonTask()->get();

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->tasks = $userTask->tasks->merge($commonTasks);

        return $userTask;
    });

    return $userTasks;
}

看上面的程式碼是不是感覺很清爽,很直接,但是... 返回的資料是沒有整合進 commonTask 的,這是為什麼呢,明明 $userTask->tasks->merge($tasks) 也賦值了呀,問題出在哪裡呢,我們測試一下:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->tasks = $userTask->tasks->merge($commonTasks);

        dd($userTask->tasks->toArray(), $userTask->toArray());

        return $userTask;
    });

    ....

具體的資料列印結果我就不貼出來了哈,佔地方,哈哈,我直接說結果。

從列印的結果中可以看到 $userTask->tasks 中是有合併之後的資料的,但是 $userTask 還是原先的資料。這是為啥,我有點懵了,難道說 $userTask->tasks 操作是關聯查詢操作了?($userTask 是一個 User 物件集合,$userTask->tasks 會不會再次查詢資料了?而不是直接獲取的原有屬性?),疑問出現了,我們就來測試看看:

    ...

    $userTasks = $project->users->load(['tasks' => function ($query) use ($project) {
        // $query->where('project_id', $project->id);
    }])->keyBy('name');

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        dd($userTask->tasks);
    });

    ....

通過對上面的測試發現,$userTask->tasks 是有攜帶上面查詢條件的,所以說這個疑問排除了!

難道是集合屬性不能這樣賦值?我們再來測試一下:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->name = 111;

        return $userTasks;
    });

    ....

返回的結果是修改了的....

這就尷尬了,難道是物件集合中的非物件屬性不能這樣賦值?也不對呀,思來想去決定對物件本身做一個探索,直接在 map 中列印 $userTask :

Laravel 集合操作中一次奇妙踩坑經歷

大家可以看到兩個關鍵的屬性:attributes、relations ,在實踐中可以發現不管是 $userTask->name = "user**" 還是 $user->tasks = *** 的賦值操作都有對 attributes 做更改,這一點也可以從 Model 中的 __set 魔術方法中看到,其中是有呼叫一個 setAttribute 方法的,我們來看一下:

Laravel 集合操作中一次奇妙踩坑經歷

Laravel 集合操作中一次奇妙踩坑經歷

既然 attributes 被修改了,那究竟為啥在輸出的時候只有他本身的屬性有變更但是關聯屬性沒有呢?

還記得我們剛才測試列印時候的 toArray 嗎,就是他把物件集合轉變成了一個陣列,我們來看一下:

Laravel 集合操作中一次奇妙踩坑經歷

明顯看到 toArray 方法將 attributes 和 relations 轉化成陣列了,而且用的 array_merge 方法,大家知道相同 key 的時候,後面陣列會覆蓋前面陣列,從前面的測試中可以看到 $userTask 中 attributes 是有變更,但是 relations 中的資料是沒有發生任何變化的,這就可以解釋為什麼賦值 tasks 沒有任何效果了,原有的資料覆蓋掉了變更的資料。

所以我們現在要做的就是,對 relations 處理,那我們現在來看一下直接對 relations 處理是否有用:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        $userTask->relations['tasks'] = $userTask->tasks->merge($commonTasks);

        return $userTasks;
    });

    ....

測試結果很顯然是成功的,但是大家可能會發現直接操作 relations 或許有些不妥,別急,Laravel 也給我們提供了這樣一個方法:

Laravel 集合操作中一次奇妙踩坑經歷

現在我們把程式碼優化一下:

    ...

    $userTasks = $userTasks->map(function ($userTask) use ($commonTasks) {
        return $user->setRelation('tasks', $user->tasks->merge($commonTasks));;
    });

    ....

大公告成,可以說很優雅,哈哈,大家可能會問,你這直接返回了沒有呼叫 toArray 啊,資料是怎麼合併的怎麼轉換的?大家知道在控制器中直接 return 的時候,是會直接轉化為 Json 資料格式的,模型中也相對應的有這麼一個方法:

Laravel 模型操作中一次奇妙踩坑經歷

Laravel 模型操作中一次奇妙踩坑經歷

一步步走下來發現,最終還是呼叫了 toArray 。所以嘛,這次踩坑算是跨過去了,哈哈。不知道大家有沒有理解,有需要改進的地方大家在評論區留言噢。

特別鳴謝: zIym 同學 (我們倆一起跨的坑,哈哈)

結束語

其實吧最初我也沒有想這麼多,想了很多其它的解決辦法,但是都是治根不治本,到頭來發現自己對 Laravel 模型的工作原理還是不熟悉,只存在簡單的應用上面,所以呀還是得追根溯源,並不是把時間都浪費在嘗試上面,多看看原始碼,會有想不到的收穫,哈哈。

finecho # Lhao

相關文章