加了強型別如何將請求引數轉為整型

zdg1992發表於2020-01-16

最近公司專案,加了強型別,踩了不少坑,這次分享一下關於強制轉換請求引數格式的問題。

背景

我們有很多介面,需要前端傳$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 協議》,轉載必須註明作者和本文連結

相關文章