Laravel 5.4 - 如何为同一个自定义验证规则使用多个错误消息

Posted

技术标签:

【中文标题】Laravel 5.4 - 如何为同一个自定义验证规则使用多个错误消息【英文标题】:Laravel 5.4 - How to use multiple error messages for the same custom validation rule 【发布时间】:2017-11-25 12:54:32 【问题描述】:

为了重用代码,我在一个名为 ValidatorServiceProvider 的文件中创建了自己的验证器规则:

class ValidatorServiceProvider extends ServiceProvider

    public function boot()
    
        Validator::extend('checkEmailPresenceAndValidity', function ($attribute, $value, $parameters, $validator) 
            $user = User::where('email', $value)->first();
            
            // Email has not been found
            if (! $user) 
                return false;
            
            
            // Email has not been validated
            if (! $user->valid_email) 
                return false;
            
            
            return true;
        );
    

    public function register()
    
        //
    

我是这样使用这条规则的:

public function rules()

    return [
        'email' => 'bail|required|checkEmailPresenceAndValidity'
    ];

但是,我想为每种情况设置不同的错误消息,如下所示:

if (! $user) 
    $WHATEVER_INST->error_message = 'email not found';
    return false;

        
if (! $user->valid_email) 
    $WHATEVER_INST->error_message = 'invalid email';
    return false;

但我不知道如何在不执行 2 条不同规则的情况下实现这一目标... 当然,它可以使用多个规则,但它也会执行多个 SQL 查询,我真的想避免这种情况。 另外,请记住,在实际情况下,我可以在单个规则中进行 2 个以上的验证,例如论文。

有人有想法吗?

===== 编辑 1:

实际上,我认为我想要一些与betweensize 规则类似的东西。 它们代表一个规则,但提供多个错误消息:

'size'                 => [
    'numeric' => 'The :attribute must be :size.',
    'file'    => 'The :attribute must be :size kilobytes.',
    'string'  => 'The :attribute must be :size characters.',
    'array'   => 'The :attribute must contain :size items.',
],

Laravel 检查该值是否代表数字、文件、字符串或数组;并获得要使用的正确错误消息。 我们如何通过自定义规则来实现这种事情?

【问题讨论】:

【参考方案1】:

您在哪里找到尺寸验证的错误消息?

我在 Illuminate\Validation\ConcernsValidatesAttributes trait 和所有函数都返回一个 bool 值(也是大小验证)。

protected function validateSize($attribute, $value, $parameters)

    $this->requireParameterCount(1, $parameters, 'size');

    return $this->getSize($attribute, $value) == $parameters[0];

你找到的属于这部分:

$keys = ["$attribute.$lowerRule", $lowerRule];

在这种情况下,它仅用于通过设置 lowerRule 值来格式化输出,laravel 在特殊情况下会处理,例如大小验证:

    // If the rule being validated is a "size" rule, we will need to gather the
    // specific error message for the type of attribute being validated such
    // as a number, file or string which all have different message types.
    elseif (in_array($rule, $this->sizeRules)) 
        return $this->getSizeMessage($attribute, $rule);
    

只要验证规则必须返回一个布尔值,就没有办法返回多个错误消息。否则你必须重写验证规则的一部分。

您可以使用存在验证的验证问题的方法:

public function rules()

    return [
        'email' => ['bail', 'required',  Rule::exists('users')->where(function($query) 
        return $query->where('valid_email', 1);
    )]
    ];
   

所以你需要 2 存在验证规则。我建议使用来自 laravel 的现有邮件来检查是否设置了电子邮件,并使用自定义邮件来检查帐户是否经过验证。

【讨论】:

【参考方案2】:

自定义验证规则处理不当是我放弃 laravel 的原因(嗯,这是众多原因之一,但可以说是压垮骆驼的最后一根稻草)。但无论如何,我有一个三部分的答案给你:在这种特定情况下你不想这样做的原因,你必须处理的混乱的快速概述,然后回答你的问题以防万一你还是想做。

重要的安全问题

管理登录的最佳安全实践要求您始终针对登录问题返回一条通用错误消息。典型的反例是,如果您针对未找到电子邮件返回“该电子邮件未在我们的系统中注册”,而针对密码错误的正确电子邮件返回“错误密码”。在您提供单独的验证消息的情况下,您为潜在的攻击者提供了有关如何更有效地指导他们的攻击的额外信息。因此,所有与登录相关的问题都应返回通用验证消息,无论其根本原因如何,都会产生“无效的电子邮件/密码组合”的效果。密码恢复表单也是如此,通常会说“密码恢复说​​明已发送到该电子邮件,如果它存在于我们的系统中”。否则,您为攻击者(和其他人)提供了一种方法来了解在您的系统中注册了哪些电子邮件地址,这可能会暴露额外的攻击媒介。因此,在这种特定情况下,您需要一条验证消息。

laravel 的麻烦

您遇到的问题是 laravel 验证器仅返回 true 或 false 来表示是否满足规则。错误消息单独处理。您特别不能从验证器逻辑内部指定验证器错误消息。我知道。这很荒谬,而且计划不周。你所能做的就是返回真或假。您无权获得任何其他帮助,因此您的伪代码不会这样做。

(丑陋的)答案

创建您自己的验证消息的最简单方法是发送至create your own validator。看起来像这样(在您的控制器内部):

$validator = Validator::make($input, $rules, $messages);

您仍然需要在启动时创建您的验证器(您的Valiator::Extend 调用。然后您可以通过将$rules 传递给您的自定义验证器来正常指定它们。最后,您可以指定您的消息。像这样,整体(在您的控制器内):

public function login( Request $request )

    $rules = [
        'email' => 'bail|required|checkEmailPresenceAndValidity'
    ]

    $messages = [
        'checkEmailPresenceAndValidity' => 'Invalid email.',
    ];

    $validator = Validator::make($request->all(), $rules, $messages);

(我不记得您是否必须在 $messages 数组中指定每个规则。我不这么认为)。当然,即使这样也不是很好,因为你传递给 $messages 的只是一个字符串数组(这就是它所允许的全部)。结果,您仍然无法根据用户输入轻松更改此错误消息。这一切都发生在您的验证器运行之前。您的目标是根据验证结果更改验证消息,但是 laravel 会强制您首先构建消息。结果,要真正做自己想做的事,就得调整系统的实际流量,这不是很牛逼。

解决方案是在您的控制器中有一个方法来计算您的自定义验证规则是否得到满足。它会在您创建验证器之前执行此操作,以便您可以向您构建的验证器发送适当的消息。然后,当您创建验证规则 时,您还可以将其绑定到验证计算器的结果,只要您将规则定义移动到控制器中即可。你只需要确保而不是意外地把事情搞砸了。您还必须记住,这需要将您的验证逻辑移到验证器之外,这是相当麻烦的。不幸的是,我有 95% 的把握确实没有其他方法可以做到这一点。

我在下面有一些示例代码。它肯定有一些缺点:你的规则不再是全局的(它在控制器中定义),验证逻辑移出验证器(这违反了最小惊讶原则),你将不得不想出一个 in -object 缓存方案(这并不难)以确保您不会执行两次查询,因为验证逻辑被调用了两次。重申一下,这绝对是 hacky,但我相当肯定这是用 laravel 做你想做的事情的唯一方法。可能有更好的方法来组织它,但这至少应该让您了解需要做什么。

<?php
namespace App\Http\Controllers;

use User;
use Validator;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class LoginController extends Controller

    public function __construct() 
        Validator::extend('checkEmailPresenceAndValidity', function ($attribute, $value, $parameters, $validator) 

            return $this->checkLogin( $value ) === true ? true : false;

        );
    

    public function checkLogin( $email ) 
        $user = User::where('email', $email)->first();

        // Email has not been found
        if (! $user) 
            return 'not found';
        

        // Email has not been validated
        if (! $user->valid_email) 
            return 'invalid';
        

        return true;
    

    public function login( Request $request ) 

        $rules = [
            'email' => 'bail|required|checkEmailPresenceAndValidity'
        ]

        $hasError = $this->checkLogin( $request->email );
        if ( $hasError === 'not found' )
            $message = "That email wasn't found";
        elseif ( $hasError === 'invalid' )
            $message = "That is an invalid email";
        else
            $message = "Something was wrong with your request";


        $messages = [
            'checkEmailPresenceAndValidity' => $message,
        ];

        $validator = Validator::make($request->all(), $rules, $messages);

        if ($validator->fails()) 
            // do something and redirect/exit
        

        // process successful form here
    

另外,值得一提的是,这个实现依赖于$this 对闭包的支持,(我相信)它是在 PHP 5.4 中添加的。如果您使用的是旧版本的 PHP,则必须使用 use$this 提供给闭包。

编辑以咆哮

真正归结为 laravel 验证系统设计得非常精细。每个验证规则都应该只验证一件事。因此,给定验证器的验证消息永远不必更改,因此$messages(当您构建自己的验证器时)只接受纯字符串。

一般来说,粒度在应用程序设计中是一件好事,也是正确实施 SOLID 原则所追求的。然而,这种特殊的实现让我抓狂。我的一般编程理念是,一个好的实现应该使最常见的用例变得非常容易,然后为不太常见的用例摆脱困境。在这种情况下,laravel 的架构使最常见的用例变得容易,但不太常见的用例几乎不可能。我不同意这种权衡。我对 Laravel 的总体印象是,只要您需要以 laravel 的方式做事,它就可以很好地工作,但是如果您出于任何原因必须跳出框框,它将会使您的生活变得更加困难。在您的情况下,最好的答案可能是直接回到该框内,即制作两个验证器,即使这意味着进行冗余查询。对您的应用程序性能的实际影响可能根本不重要,但是您对长期可维护性的影响会很大,以使 laravel 以您希望的方式运行。

【讨论】:

【参考方案3】:

不幸的是,Laravel 目前没有提供直接从属性参数数组添加和调用验证规则的具体方法。但这并不排除基于TraitRequest 使用的潜在且友好的解决方案。

例如,请在下面找到我的解决方案。

第一件事是等待表单被处理,然后用抽象类自己处理表单请求。您需要做的是获取当前的Validator 实例,并在出现任何相关错误时阻止它进行进一步验证。否则,您将存储验证器实例并调用您稍后将创建的自定义用户验证规则函数:

<?php

namespace App\Custom\Validation;

use \Illuminate\Foundation\Http\FormRequest;

abstract class MyCustomFormRequest extends FormRequest

    /** @var \Illuminate\Support\Facades\Validator */
    protected $v = null;

    protected function getValidatorInstance()
    
        return parent::getValidatorInstance()->after(function ($validator) 
            if ($validator->errors()->all()) 
                // Stop doing further validations
                return;
            
            $this->v = $validator;
            $this->next();
        );
    

    /**
     * Add custom post-validation rules
     */
    protected function next()
    
        
    

下一步是创建您的Trait,这将提供通过当前验证器实例验证您的输入并处理您想要显示的正确错误消息的方法:

<?php

namespace App\Custom\Validation;

trait CustomUserValidations

    protected function validateUserEmailValidity($emailField)
    
        $email = $this->input($emailField);

        $user = \App\User::where('email', $email)->first();
        
        if (! $user) 
            return $this->v->errors()->add($emailField, 'Email not found');
        
        if (! $user->valid_email) 
            return $this->v->errors()->add($emailField, 'Email not valid');
        

        // MORE VALIDATION POSSIBLE HERE
        // YOU CAN ADD AS MORE AS YOU WANT
        // ...
    

最后,不要忘记扩展您的MyCustomFormRequest。例如,在您的php artisan make:request CreateUserRequest 之后,这是一个简单的方法:

<?php

namespace App\Http\Requests;

use App\Custom\Validation\MyCustomFormRequest;
use App\Custom\Validation\CustomUserValidations;

class CreateUserRequest extends MyCustomFormRequest

    use CustomUserValidations;

    /**
     * Add custom post-validation rules
     */
    public function next()
    
        $this->validateUserEmailValidity('email');
    

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    
        return true;
    

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    
        return [
            'email' => 'bail|required|email|max:255|unique:users',
            'password' => 'bail|required',
            'name' => 'bail|required|max:255',
            'first_name' => 'bail|required|max:255',
        ];
    

我希望你能按照我的建议找到自己的方式。

【讨论】:

有趣。我认为这个答案只是支持我的总体总结:做这个相当简单的事情(根据输入自定义错误消息)在 laravel 中几乎是不可能的!你的答案和我的答案都不是特别简单。 作为一个快速说明,你的 trait 依赖于它用于工作的类的实现细节的行为,这通常会导致一个非常脆弱的应用程序。详细地说,您的 trait 依赖于 $this-&gt;v 中的验证器,但它之所以存在,是因为 MyCustomFormRequest 填充了它。这意味着这个 trait 根本不能重用,这违背了 trait 的目的。我认为你最好放弃 trait 并将 validateUserEmailAndValidity 移动到 CreateUserRequest 或将验证器作为你的 trait 中的必需参数,而不是从 $this-&gt;v 获取它。【参考方案4】:

除了其他建议之外,我认为除了Validator::extend('yourRule', function(...)) 之外,您还可以调用Validator::replacer('yourRule', function()) 并跟踪导致您从中扩展验证器的服务提供者类中验证失败的原因。这样,您就可以将默认错误消息完全替换为另一个错误消息。

根据docs,replacer() 用于在错误消息返回之前在错误消息中进行占位符替换,因此虽然这不是严格意义上的这种情况,但它已经足够接近了。当然,这是一种丑陋(ish)的解决方法,但它可能会起作用(至少乍一看它似乎对我有用)。

但要记住的一件事是,如果您想避免为所有未通过自定义的字段自动返回相同的消息,您可能必须在数组中跟踪这些失败原因验证规则。

【讨论】:

【参考方案5】:

如果您使用 Laravel 8 并希望针对特定验证显示不同的错误消息,请按照以下步骤操作。

我将创建一个验证规则来检查字段是有效的电子邮件还是有效的电话号码。它还会返回不同的错误消息。

制定自定义验证规则,例如

php artisan make:rule EmailOrPhone

导航到Rules文件夹中创建的文件,即Root->App->Rules->EmailOrPhone.php

粘贴以下代码

<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;

class EmailOrPhone implements Rule

    public $error_message;
    public function __construct()
    
    

    public function passes($attribute, $value)
    

        $value = trim($value);
        if (is_numeric($value))
            if (strlen($value) != 10)
                $this->error_message = "Phone number must contain 10 digits";
                return false;
            else if (!Str::startsWith($value, '0'))
                $this->error_message = "Phone number must start with 0";
                return false;
            else
                return true;
            
        else
            $validator = Validator::make(['email' => $value],[
                'email' => 'required|email'
            ]);

            if($validator->passes())
               return true;
            else
                $this->error_message = "Please provide a valid email address";
                return false;
            
        
    

    public function message()
    
         return $this->error_message;
    

您现在可以在验证器中使用自定义验证,例如

 return Validator::make($data, [
            'firstname' => ['required', 'string', 'max:255'],
            'lastname' => ['required', 'string', 'max:255'],
            'email_phone' => ['required', 'string', 'max:255', new EmailOrPhone()],
            'password' => ['required', 'string',  'confirmed'],
        ]);

【讨论】:

简洁大方,我喜欢。其他! 像维京人一样在地上砸老鼠(并立即后悔)

以上是关于Laravel 5.4 - 如何为同一个自定义验证规则使用多个错误消息的主要内容,如果未能解决你的问题,请参考以下文章

Laravel 5.4 Auth Guardian 驱动程序 [customers] 未定义

如何在 Laravel 5.4 中添加自定义用户提供程序

如何为 Laravel 发布和自定义 .htaccess?

Laravel 5.4 验证请求,如何处理更新时的唯一验证?

Laravel 5.4 登录后重定向到自定义 url

如何为 Laravel 工厂使用自定义值列表,维护订单并仍在使用 Faker 外观?