最近公司專案,加了強型別,踩了不少坑,這次分享一下關於強制轉換請求引數格式的問題。
背景
我們有很多介面,需要前端傳$limit這個引數來限制請求資料條數,在Controller中獲取引數
$params = $request->validate([
'limit' => 'required|integer'
]);
$this->service->foo($params['limit']);
在Service中定義具體實現方法
public function foo(int $limit): array
{
return [];
}
這樣子看起來沒啥問題,但實際執行,就會報錯,Argument 1 passed to foo() must be of the type integer, string given。雖然我們在validate中限定了integer,但是實際上傳過來的引數還是字串,這樣一旦加了強型別,方法就會報錯了,那遇到這種情況該如何處理呢,總不能每次呼叫方法的時候都加一個(int)強制型別轉換吧,下面我會介紹一下處理的幾種方式。
中介軟體
我們介面有自定義了一些中介軟體,最開始的想法就只直接在中介軟體中強制轉換一下$limit的型別
$params = $request->all();
foreach ($params as $key => $param) {
if ('limit' != $key) {
continue;
}
$request->request->set($key, intval($param));
}
這種做法,雖然解決了$limit的型別問題,但是有很多侷限性,比如$quantity等其他引數也需要轉換呢?而且這種寫法也損耗效能。
重寫ValidatesRequests
我們在基類Controller當中是有引用Illuminate\Foundation\Validation\ValidatesRequests這個trait的,既然我們的引數都要經過validate,那麼我們就直接重寫這個檔案,在驗證的時候,就進行型別轉換
<?php
declare(strict_types=1);
namespace App\Http\Validation;
use Illuminate\Foundation\Validation\ValidatesRequests as BaseValidatesRequests;
use Illuminate\Http\Request;
trait ValidatesRequests
{
use BaseValidatesRequests;
/**
* {@inheritdoc}
*/
public function validate(Request $request, array $rules, array $messages = [], array $customAttributes = []): array
{
if (array_get($rules, 'limit')) {
$request->request->set('limit', intval($request->limit));
}
return $this->getValidationFactory()->make(
$request->all(), $rules, $messages, $customAttributes
)->validate();
}
}
這種做法也侷限了引數,不同的引數需要寫成一個陣列,使用in_array()去判斷,並不是一種好的實現方式,不過不需要每個請求都判斷一次了,有validate才判斷,效能損耗比上一種方式好一些。
為了更好的匹配不同引數,又進行了最佳化下,將驗證後的資料取出,根據判斷驗證規則中是否包含integer規則來處理型別轉換
$validated = $this->getValidationFactory()->make(
$request->all(), $rules, $messages, $customAttributes
)->validate();
foreach ($rules as $key => $rule) {
if (str_contains($rule, 'integer')) {
$validated[$key] = intval($validated[$key]);
}
}
return $validated
這樣子,就不需要特殊定義需要轉換的引數了。
重寫ValidatorFactory
第三種實現方式其實與第二種類似,區別就是第一種方式是引用的trait,所以我們控制器中驗證引數的時候就必須使用,$this->validate($request, []),而我們的程式碼中很多是使用$request->validate([]),這種方式的,為了不大量修改程式碼,所以換了一種實現方式。我們重寫了ValidationServiceProvider,這個檔案只是複製繼承一下原檔案,然後在配置檔案app.php中使用我們自己定義的ValidationServiceProvider,最後我們需要重寫Illuminate\Validation\Factory這個檔案,去實現我們的需求。
public function make(array $data, array $rules, array $messages = [], array $customAttributes = []): \Illuminate\Validation\Validator
{
$validator = parent::make($data, $rules, $messages, $customAttributes);
$validator->after(function ($v): void {
if (! $v->getMessageBag()->isEmpty()) {
return;
}
$data = $v->getData();
foreach (array_keys($v->getRules()) as $attribute) {
if (! $v->hasRule($attribute, 'Integer')) {
continue;
}
$value = array_get($data, $attribute, null);
//只有當使用者有傳值進來時, 才轉換
if (null === $value) {
continue;
}
$data[$attribute] = $value;
}
$v->setData($data);
});
return $validator;
}
可以看出,三種不同的方式,一種種在改善,正如我們老大所說的,沒有最完美的實現方式,我們能做的,就是不斷的去完善自己的程式碼,不斷的去code review。
本作品採用《CC 協議》,轉載必須註明作者和本文連結