Stories

Detail Return Return

Laravel獨立驗證器(表單請求)用例大全 - Stories Detail

表單請求(FormRequest)獨立驗證類完整例子

<?php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class TestRequest extends FormRequest
{

    /**
     * 表示驗證器是否應在第一個規則失敗時停止
     * 注意此處是停止所有屬性與規則,與停止單個屬性的bail規則不同
     * 
     * @var bool
     */
    protected $stopOnFirstFailure = false;


    /**
     * 確定用户是否有權提出此請求
     * 可以在該處做權限判斷,比如用户發表帖子,可以在此處判斷用户是否有權發帖
     *
     * @return bool
     */
    public function authorize(): bool
    {
        // 例子
        $registerTimestamp = strtotime(Auth::user()->created_at);
        if( (time() - $registerTimestamp) < 86400 ){
            //註冊時間不足24小時,禁止發表新帖子
            return false;
        }

        return true;
    }


    /**
     * 要驗證的數據,可省略,默認值 request()->all()
     */
    public function validationData()
    {
        return [
            'title'    => '不像我只會心疼哥哥',
            'content'  => '哥哥,你女朋友要是知道我倆吃同一個棒棒糖,你女朋友不會吃醋吧!',
            'password' => '88888888',  
        ];
    }

    /**
     * 驗證規則
     */
    public function rules(): array
    {
        return [
            'title'    => 'required|between:8,50',
            'content'  => 'required|min:100',

            // rule可以是|分割的字符串,也可以是一維數組
            'website'  => ['required', 'url'],

            // 某些非內置的非通用特殊驗證需求,可以在閉包裏完成
            'test' => ['size:5', function($attribute, $value, $fail){
                if( $value !== '12345' ){
                    $fail("參數 {$attribute} 不符合要求.");
                }
            }]

            // 假設我們為了用户安全,發帖時必須提交密碼二次驗證
            // 可以使用閉包完成數據庫對比密碼,但是我們希望其他參數都驗證通過後再驗證,節省數據庫查詢,所以我們在after中進行數據庫密碼比對
            'password' => 'required|between:6,20',
        ];
    }

    /**
     * 自定義屬性別名,可省略
     */
    public function attributes()
    {
        return [
            'title'   => '帖子標題',
            'content' => '帖子內容',
        ];
    }

    /**
     * 自定義錯誤消息,可省略
     */
    public function messages()
    {
        return [
            'required'         => ':attribute不能為空, 請填寫後再提交',
            'title.between'    => '帖子標題長度限制在:min至:max個字之間',
            'content.min'      => '帖子內容至少需要100個字',
            'password.between' => '密碼錯誤',
        ];
    }


    // 授權失敗處理, 當前類authorize()方法存在且返回 false 時調用此處 可省略
    protected function failedAuthorization()
    {
        throw new \Illuminate\Auth\Access\AuthorizationException;
    }
 
    // 授權失敗處理, 當前類authorize()方法存在且返回 false 時調用此處 可省略
    protected function failedAuthorization()
    {
        throw new \Illuminate\Auth\Access\AuthorizationException;
    }
 
    // 驗證器預處理 
    protected function prepareForValidation(): void
    {
        // 避免大量的自定義驗證規則通過 Validator::extend 註冊在boot中,每個請求都加載到
        // 這樣寫可以僅本驗證器運行時才加載
        \Illuminate\Support\Facades\Validator::extend('title', [$this, 'validateTitle']);

        $this->merge([
            /**
             * rules驗證的是這裏你修改過後的數據
             * 'www.domain.com' 變成 'http://www.domain.com' 進行驗證,且影響 $this->all() 與 $this->validated() 結果
             */
            'website' => "http://".$this->website,
        ]);
    }

    // 驗證完成後對任何請求數據進行規範化 可省略
    protected function passedValidation(): void
    {
        /**
         * 'http://www.domain.com' 變成 'http://www.domain.com?code=123456'
         * 影響 $this->all()、 $this->input('website') 等
         * 不影響 $this->validated() 結果,裏面還是 'http://www.domain.com'
         */
        $this->replace([
            'website' => $this->website.'?code=123456'
        ]);
    }

    /**
     * 行內驗證器
     * 也許你在yii2(行內驗證器)和thinkphp(自定義驗證規則)中經常這樣做, 但是laravel中默認無法實現
     * 請在 prepareForValidation 中通過 \Illuminate\Support\Facades\Validator::extend 註冊本方法後使用
     *
     * @return bool
     */
    public function validateTitle($attribute, $value, $parameters, $validator)
    {
        return true;
    }


    /**
     *  配置驗證器實例。
     *
     * @param  \Illuminate\Validation\Validator  $validator
     * @return void
     */
    public function withValidator(\Illuminate\Validation\Validator $validator)
    {
        // 這裏是驗證器的後置操作
        $validator->after(function (\Illuminate\Validation\Validator $validator) {

            /**
             * errors 具體內容可以參考 Illuminate\Support\MessageBag
             */
            if( $validator->errors()->isEmpty() ){

                /**
                 * 運行到 isEmpty() 時,表示rules裏面的驗證規則都通過了
                 * 此時密碼只進行了rules裏的規則驗證,我們需要繼續驗證密碼是否與持久儲存相同
                 */
                $password = $this->input('password');
                if( $password !== '123456' ){
                    $validator->errors()->add('password', '數據庫比對密碼錯誤');
                } 

            }

        });
    }

    /**
     *
     * 自定義驗證失敗後的錯誤處理
     *
     * 必需拋出一個Exception,否則業務代碼會繼續執行,拋出了Exception則會被框架捕捉處理
     * 直接return返回一個response對象是無效的
     * 
     * 父類 FormRequest 裏面默認拋出的是 Illuminate\Validation\ValidationException
     * 如果是表單請求(application/x-www-form-urlencoded)它會重定向回來源頁面,並且把錯誤信息寫入flash session,
     * 非表單請求否則返回一個固定格式的json響應
     * 
     * 父類默認的failedValidation不能滿足以下需求
     * 1、在實際使用中,有的時候我們想驗證失敗時始終返回json而不跳轉,甚至是xml
     * 2、默認返回的json格式是固定,而我們可能需要自定義一些json字段
     * 
     */
    protected function failedValidation(Validator $validator)
    {
        // 具體內容可以參考 Illuminate\Support\MessageBag 
        $error = $validator->errors();

        // 自定義response對象 Illuminate\Http\Response
        $response = response()->json([
            //...自定義的錯誤響應內容
            'code'    => 10001,
            'message' => $error->first(),
            'errors'  => $error->errors(),
        ]);

        /**
         * 拋出一個HttpResponseException異常類,
         * 這將阻止驗證器調用後的後續代碼,直接發送response到瀏覽器
         */
        throw new HttpResponseException($response);
    }
}

驗證器錯誤揹包用法 Illuminate\Support\MessageBag

    $validator = Validator::make($request->all(), [
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
    ]);

    if ($validator->fails()) {
        $errors = $validator->errors();

        // 最常用
        $errors->first($key = null, $format = null);
        $errors->get($key, $format = null);
        $errors->all($format = null);    
        $errors->isEmpty();                          // bool 是否沒有錯誤
        $errors->isNotEmpty();                       // bool 是否包含錯誤
        $errors->count();                           // int  錯誤總數
        $errors->add($key, $message);              // this 添加錯誤

        // 非常用
        $errors->keys();                          // 返回包含錯誤的keys
        $errors->addIf($boolean, $key, $message); // 如果參數1為“true”,則將消息添加到消息包中。
        $errors->isUnique($key, $message);        // 確定key和message的組合是否已經存在
        $errors->merge($messages);                // 將新的消息數組合併到消息包中
        $errors->has($key);
        $errors->hasAny($keys = []);              // 確定是否存在任何給定key的message,可以傳入非數組
        $errors->missing($key);                      // 確定是否不存在所有給定key的message
        $errors->unique($format = null);          // 確定是否不存在所有給定key的message
        $errors->forget($key);
        $errors->toArray();
        $errors->toJson();
    }

獨立表單驗證類自動調用流程

class HomeController extends Controller{

    // FormRequest代替Illuminate\Http\Request作為控制器操作的request參數注入
    public function myaction(TestRequest $request)
    {
        // 你會發現使用獨立驗證類時,不需要fails()
        // 驗證通過了
        return $request->validated();
    }    
    
}
  1. 實現自定義驗證類 MyRequest
  2. MyRequest 繼承 Illuminate\Foundation\Http\FormRequest
  3. FormRequest 中Trait了 ValidatesWhenResolvedTrait
  4. ValidatesWhenResolvedTraitvalidateResolved 方法調用了 MyRequest->fails(), 失敗時調用 failedValidation方法
  5. Illuminate\Foundation\Providers\FormRequestServiceProvider 中啓用了你的 MyRequest

Precognitive預認知

瀏覽器頭包含Precognition-Validate-Only時,laravel響應header自動加入Precognition-Success=true
可以使用 $this->isPrecognitive() 判斷

    public function rules(): array
    {
        return [
            'password' => ['required',
                $this->isPrecognitive()
                    ? Password::min(8)
                    : Password::min(8)->uncompromised(),
            ],
        ];
    }

Add a new Comments

Some HTML is okay.