關於LaravelAdmin和Dcat在$form->saving修改了表單提交的欄位值,但儲存時卻不生效的解決方法

DogLoML發表於2021-04-23

前段時間在學laravel admin,做人員外出管理後臺demo的時候遇到一個坑,就是需要在資料儲存之前,對錶單提交的部分資料進行處理。

場景如下:
人員申請休假,建立休假記錄時,用結束休假時間減去開始休假時間,得出花費的時間,然後用這個時間和members表的剩餘休假時間進行比較,如果剩餘假期不足則無法休假,如果足夠,則更新members表的剩餘假期。

起初,我是想直接前端模板裡面加members.remaing_holiday的隱藏域,提交的時候計算賦值,不過這樣可能不安全,我改為了後端處理。

根據文件,使用$form->saving()回撥處理資料,對$form的remaing_holiday巴拉巴拉一頓操作,這時候就出問題了,雖然我已經修改了欄位的值,但是資料庫儲存的卻沒改過來,後來我直接在saving裡面手動儲存,卻儲存成功了。

花了一些時間後,用xdebug找到了框架執行儲存操作的程式碼。
檔案位於vendor/encore/laravel-admin/src/Form.php

public function store()
    {
    // 獲取請求的表單資料
        $data = \request()->all();
        // Handle validation errors.  驗證是否符合rules
        if ($validationMessages = $this->validationMessages($data)) {
            return $this->responseValidationError($validationMessages);
        }
        //  重點就是這裡,進到$this->prepare
        if (($response = $this->prepare($data)) instanceof Response) {
            return $response;
        }
        // 開啟事務,當時還有一個疑惑此時也得到答案:如果儲存失敗會回滾,但是自增id沒有變回去
        DB::transaction(function () {
            // 插入的預處理,傳進去的是$this->updates,也就是說要修改updates,那麼如何改他呢,再繼續找
            $inserts = $this->prepareInsert($this->updates);

            foreach ($inserts as $column => $value) {
                $this->model->setAttribute($column, $value);
            }
            // 儲存
            $this->model->save();
            // 更新關聯模型
            $this->updateRelation($this->relations);
        });

        if (($response = $this->callSaved()) instanceof Response) {
            return $response;
        }

        if ($response = $this->ajaxResponse(trans('admin.save_succeeded'))) {
            return $response;
        }

        return $this->redirectAfterStore();
    }

接下來看$this->prepare()

    protected function prepare($data = [])
    {
    // 觸發submitted hook
        if (($response = $this->callSubmitted()) instanceof Response) {
            return $response;
        }

        // $data移除忽略欄位後和舊的$this->inputs合併,再賦值給$this->inputs
        $this->inputs = array_merge($this->removeIgnoredFields($data), $this->inputs);

        // 觸發saving hook
        if (($response = $this->callSaving()) instanceof Response) {
            return $response;
        }

        // 獲取$this->inputs的關聯關係
        $this->relations = $this->getRelationInputs($this->inputs);

        // 這裡排除掉inputs裡面的relations再賦值給updates(更新欄位的陣列)
        $this->updates = Arr::except($this->inputs, array_keys($this->relations));
    }

看到這裡,也就知道之前改$form->xxx為什麼不生效了,$data是在store()執行開始就獲取了\request()->all(),而saving則是在prepare()裡面,$this->input賦值之後才執行的,此時改$form->xxx沒有用,要改$form->inputs才有用。

那麼換一個方向想,save儲存的是$this->updates處理後的資料,而updates又是Arr::except($this->inputs, array_keys($this->relations));之後的資料,那麼我們要改updates就要改inputs。

可以在saving裡面修改inputs嗎?不行,$inputs是protected。

原先我是換一個辦法,既然儲存前處理不了,我就儲存後再處理。用saved回撥,獲取到儲存後的model然後update或者save。

不過這個辦法很low,最近在學dcat,正好今天在dcat群看到有群友遇到了類似的問題,於是交流了一下,發現一個更好的辦法,既然是繼承關係,其實也可以在子類重寫store方法,這樣就方便修改了。

但是這樣就行了嗎?如果有很多地方都要重寫,感覺很麻煩,有沒有更好的辦法?

我又去社群搜了搜相關的帖子,恰好看到了dcat作者寫的Dcat Admin 教程 - 如何優雅地更改表單值的資料型別?
根據文章,在dcat中可以這樣寫

$form->text('title')->saving(function ($value) {
    // 強制轉化為string型別
    return (string) $value;
});

更加簡潔的寫法是:利用 Laravel 的 macro 功能來擴充套件這個功能。
開啟 app/Admin/bootstrap.php,寫入以下程式碼

// 擴充套件表單欄位方法
// 儲存為字串
Form\Field::macro('saveAsString', function () {
    return $this->saving(function ($v) {
        return (string) $v;
    });
});

然後就可以直接在表單使用$form->text('title')->saveAsString();

非常方便,nice

在dcat嘗試之後,我想laravel admin能不能也這麼用,於是試了一下,可惜沒有這個巨集指令。

BadMethodCallException In Macroable.php line 103 :
  Method Encore\Admin\Form\Field\Number::saving does not exist.

於是我又比較了一下二者的原始碼,dcat的裡面有這個saving方法,並且prepare也多了一些東東
檔案位於:vendor/encore/laravel-admin/src/Form/Field.php

    public function saving(\Closure $closure)
    {
        $this->savingCallbacks[] = $closure;

        return $this;
    }

    final public function prepare($value)
    {
        $value = $this->prepareInputValue($value);

        if ($this->savingCallbacks) {
            foreach ($this->savingCallbacks as $callback) {
                $value = $callback->call($this->data(), $value);
            }
        }

        return $value;
    }

看來就是這裡呼叫了回撥吧,既然二者都差不多的,感覺laravel admin應該也可以做到,可能是因為我比較懶,我就沒繼續深入瞭解了,以後有機會再繼續吧。

最後總結一下:

  • 解決一個問題有多種方法,思路開啟,說不定就會有更好的辦法
  • 框架只是工具,怎麼方便,怎麼合適就怎麼來
  • 多交流想法,同樣的問題,其他人很可能也遇到過,看看別人是如何解決的
  • 社群裡大神很多,要多多滴學習大佬們的經驗
  • 要善用各種除錯工具,比如xdebug,\Log::info等等
  • 遇到網上搜不到的問題時,可以試著看原始碼或者請教大佬,有些坑還是要看原始碼才能解決,文件不可能面面俱到。
  • 舊的疑問,過段時間再看,說不定就有了答案。每一次看看過去遇到的問題,都可能有新的收穫,新的見解。
    我之前在b站學了前端,php,laravel。然後學laravel的框架,由於之前都是看視訊教程在學,突然看文件其實還是遇到了不少困難,好在社群裡面有很多網友的答疑解惑,感謝社群提供了一個這麼好的學習交流的地方。
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章