Laravel 引數驗證的疑與惑

寫PHP的老王發表於2019-11-19

驗證器怎麼建立的,誰建立的

Laravel 文件呼叫驗證器,除了透過控制器,還有就是透過Facades的方式建立驗證器物件。
Validator::make($data,$rule,$message)

config/app.php 中註冊了'Validator' => Illuminate\Support\Facades\Validator::class

<?php

namespace Illuminate\Support\Facades;

/**
 * @see \Illuminate\Validation\Factory
 */
class Validator extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'validator';
    }
}

從上面可以看出,Validator的實際實現類是容器中的validator物件,那這個validator物件是哪個?

<?php

namespace Illuminate\Foundation;
...
class Application extends Container implements ApplicationContract, HttpKernelInterface
{
    ...
    public function registerCoreContainerAliases()
    {
        foreach ([
            ...
            'validator'=> [
                \Illuminate\Validation\Factory::class, 
                \Illuminate\Contracts\Validation\Factory::class
            ],
        ])
        ...
    }
    ...
}

可以看出,最終建立驗證器是透過實現\Illuminate\Contracts\Validation\Factory介面的\Illuminate\Validation\Factory類建立的。再來看看,這個工廠類怎麼建立實際的驗證器的。

//\Illuminate\Contracts\Validation\Factory 原始碼

protected function resolve(array $data, array $rules, array $messages, array $customAttributes)
{
    if (is_null($this->resolver)) {
        return new Validator(
            $this->translator, 
            $data,
            $rules, 
            $messages, 
            $customAttributes
        );
    }

    return call_user_func(
        $this->resolver, 
        $this->translator, 
        $data, 
        $rules, 
        $messages, 
        $customAttributes
    );
}

到這裡,可以看出Laravel的驗證器的建立都是透過特定的工廠類建立。

如果需要自定義驗證器類(比如我需要把5.8的一些新功能遷移到5.5的版本上),有兩種方式:

一,建立一個自定義的工廠類。然後在AppServiceProvider中重新繫結新的驗證器工廠建立類;

二,AppServiceProvider中透過resolver方法設定工廠類的resolver屬性,接管驗證器的例項化,例如:

Validator::resolver(function($translator, $data, $rules, $messages, $customAttributes){
    return new ExtendValidator($translator, $data, $rules, $messages, $customAttributes);
});

如何自定義驗證規則

Laravel本身提供了很多通用的引數驗證規則,但是對於一些特定的場景,還是需要提供驗證規則的擴充套件。

Laravel驗證規則的擴充套件有兩種方式。

  • 1 透過extend方法擴充套件
//這是一個簡單的引數比較的驗證規則,Laravel5.8中提供,Laravel5.5中未提供
//驗證規則如下: 'max_num'=>'gte:min',
Validator::extend('gte',function($attribute, $value, $parameters, $validator){
    if($value>=data_get($validator->getData(),$parameters[0]))
    {
        return true;
    }
    return false;
});
//\Illuminate\Contracts\Validation\Factory  原始碼
public function extend($rule, $extension, $message = null)
{
    $this->extensions[$rule] = $extension;

    if ($message) {
        $this->fallbackMessages[Str::snake($rule)] = $message;
    }
}
//\Illuminate\Validation\Validator 原始碼
protected function callExtension($rule, $parameters)
{
    $callback = $this->extensions[$rule];

    if (is_callable($callback)) {
        return call_user_func_array($callback, $parameters);
    } elseif (is_string($callback)) {
        return $this->callClassBasedExtension($callback, $parameters);
    }
}

protected function validateAttribute($attribute, $rule)
{
    ...
    $method = "validate{$rule}";
    if ($validatable && ! $this->$method($attribute, $value, $parameters, $this)) {
        $this->addFailure($attribute, $rule, $parameters);
    }
}

public function __call($method, $parameters)
{
    $rule = Str::snake(substr($method, 8));

    if (isset($this->extensions[$rule])) {
        return $this->callExtension($rule, $parameters);
    }

    throw new BadMethodCallException(sprintf(
        'Method %s::%s does not exist.', static::class, $method
    ));
}

Factory提供了extend方法用於擴充套件規則驗證方法。所有的擴充套件規則最終都會被傳到驗證器中。驗證器在驗證引數的過程中,如果找到匹配的驗證規則,則直接進行驗證。否則呼叫魔術方法__call查詢擴充套件驗證函式。擴充套件函式返回布林值,返回true則表示驗證透過,返回false表示驗證失敗。

  • 2 透過自定義規則類擴充套件

Laravel 中提供了Illuminate\Contracts\Validation\Rule介面,只有實現了這個介面的類都認為是符合的自定義驗證規則類。

<?php

namespace Illuminate\Contracts\Validation;

interface Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value);

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message();
}

自定義規則類需要實現的方法有passes方法,用於驗證引數是否合法。message方法,用於提供驗證失敗的錯誤提示資訊。

使用自定義驗證類,相對於extend方法擴充套件有一個很大的bug就是無法在自定義類中獲取到當期的驗證器物件。從而導致在當前擴充套件的驗證規則中,只能過獲取到需要驗證的資料,而獲取不到其他的欄位資料,無法進行聯合欄位的驗證。像上面比較兩個欄位的大小的驗證規則就無法實現。

如果想要透過自定義驗證規則類實現上面兩個欄位大小比較的驗證規則,則需要自定義驗證類,修改validateUsingCustomRule方法,將當期驗證器傳入到自定義驗證規則例項物件中去。

protected function validateUsingCustomRule($attribute, $value, $rule)
{
    if(method_exists($rule, 'setValidator'))
    {
        $rule->setValidator($this);
    }
    return parent::validateUsingCustomRule($attribute,$value,$rule);
}

如何實現用當期類方法作為驗證規則驗證函式

像Yii2中,因為基本上所有的物件都有驗證方法,所以很容易用當期類方法作為驗證規則驗證函式。

例如,一個驗證規則如下,表示用當期類的validateMinNum對引數進行驗證,那麼,這樣的一個功能,如何在Laravel中實現呢。

['min_num'=>'validateMinNum']
  • 方法1 透過自定義類實現
    Laravel提供了ClosureValidationRule自定義驗證類,用來新增回撥函式的驗證。

例如

$rule = [
    'min'=>new ClosureValidationRule([$this,'checkv'])
];
$data = ['min'=>10];
$v = Validator::make($data,$rule);
  • 方法2 透過extend方式實現
$rule = [
    'min'=>'checkv'
];
Validator::extend('checkv',[$this,'checkv']);

但是這種方式對驗證器的影響是全域性的。不建議使用。

總結

透過以上原始碼的學習,可以看出Laravel驗證器的建立都是用過驗證器工廠類建立的。如果需要自定義驗證器,可以透過修改驗證器工廠類,或者設定驗證器工廠類的resolver屬性接管驗證器的例項化。

驗證規則的擴充套件有兩種方式,一種是透過extend方式實現。extend方式對驗證器的影響是全域性的,整個執行程式有效。可以獲取到驗證器本身,因此可以做多個欄位關係的驗證;另一種是透過自定義規則類實現。自定義規則了只對使用自定義規則類的驗證有效。但是自定義規則類本身無法直接獲取到驗證器本身,不能夠做多個欄位關係的驗證。如果需要實現,則需要使用自定義驗證器,將驗證器傳入到驗證規則中去。

Laravel本身提供了ClosureValidationRule的驗證規則用於處理回撥函式驗證規則。同時也可以使用extend方式進行回撥函式的驗證。

更多內容歡迎關注公眾號【寫PHP的老王】

本作品採用《CC 協議》,轉載必須註明作者和本文連結
寫PHP的老王

相關文章