首页 > 解决方案 > 验证数组 - 获取当前迭代

问题描述

我正在尝试使用 Laravel 的FormRequest.

客户正在提交一个订单,其中包含一系列商品。我们要求用户仅在和special_delivery时才表明该项目是否需要。asking_price > 500quantity > 10

以下是我的预期规则:

public function rules() {
    'customer_id' => 'required|integer|exists:customers,id',
    'items' => 'required|array',
    'items.*.name' => 'required|string',
    'items.*.asking_price' => 'required|numeric',
    'items.*.quantity' => 'required|numeric',
    'items.*.special_delivery' // required if price > 500 && quantity > 10
}

我试图按照这些思路做一些事情:

Rule::requiredIf($this->input('item.*.asking_price') > 500 && $this->input('item.*.quantity' > 10));

这样做的问题是我找不到访问当前items迭代索引以指示要验证哪个项目的方法。

我还尝试了以下自定义验证:

function ($attribute, $value, $fail) {

    preg_match('/\d+/', $attribute, $m);

    $askingPrice = $this->input('items')[$m[0]]['asking_price'];
    $quantity= $this->input('items')[$m[0]]['quantity'];

    if ($askingPrice > 500 && $quantity > 10) {
        $fail("$attribute is required");
    }
}

虽然这个函数让我可以访问当前$attribute的,但问题是它只有在存在时才会运行special_delivery。这违背了整个目的!

任何帮助都感激不尽!谢谢!

标签: laravellaravel-6laravel-validation

解决方案


我可能已经想出了一个解决你的问题的方法,sometimes如果你愿意的话,一个索引知道。

由于很遗憾无法将宏添加到验证器,您要么必须覆盖验证工厂(这就是我的建议)并使用您自己的自定义验证类,要么根据该方法创建一个辅助函数,将验证器实例作为附加参数并使用 this 而不是$this.

酱汁第一:indexAwareSometimes验证函数

function indexAwareSometimes(
    \Illuminate\Contracts\Validation\Validator $validator,
    string $parent,
    $attribute,
    $rules,
    \Closure $callback
) {
    foreach (Arr::get($validator->getData(), $parent) as $index => $item) {
        if ($callback($validator->getData(), $index)) {
            foreach ((array) $attribute as $key) {
                $path = $parent.'.'.$index.'.'.$key;
                $validator->addRules([$path => $rules]);
            }
        }
    }
}

很多灵感显然来自sometimes方法,并没有太大变化。我们基本上是遍历包含所有其他数组 ( ) 的数组($parent在您的情况下为数组),其中包含要验证的实际数据,如果评估为真,则将( ) 添加到当前索引中的( )。itemsitems.*$rulesrequired$attributespecial_delivery$callback

回调闭包需要两个参数,第一个是$data父验证实例的形式,由 检索Validator::getData(),第二个$index是外部foreach在它调用回调时的形式。

在您的情况下,该函数的用法看起来有点像这样:

use Illuminate\Support\Arr;

class YourFormRequest extends FormRequest
{
    public function rules()
    {
        return [
            'customer_id'          => 'required|integer|exists:customers,id',
            'items'                => 'required|array',
            'items.*.name'         => 'required|string',
            'items.*.asking_price' => 'required|numeric',
            'items.*.quantity'     => 'required|numeric',
        ];
    }

    public function getValidatorInstance()
    {
        $validator = parent::getValidatorInstance();

        indexAwareSometimes(
            $validator, 
            'items',
            'special_delivery',
            'required',
            fn ($data, $index) => Arr::get($data, 'items.'.$index.'.asking_price') > 500 &&
                Arr::get($data, 'items.'.$index.'.quantity') > 10
        );
    }
}

扩展原生Validator

扩展 Laravel 的原生Validator 类并不像听起来那么难。我们正在创建一个自定义的 ValidationServiceProvider 并继承 LaravelIlluminate\Validation\ValidationServiceProvider作为父级。只有registerValidationFactory方法需要被它的副本替换,我们指定了工厂应该使用的自定义验证器解析器:

<?php

namespace App\Providers;

use App\Validation\CustomValidator;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Validation\Factory;
use Illuminate\Validation\ValidationServiceProvider as ParentValidationServiceProvider;

class ValidationServiceProvider extends ParentValidationServiceProvider
{
    protected function registerValidationFactory(): void
    {
        $this->app->singleton('validator', function ($app) {
            $validator = new Factory($app['translator'], $app);

            $resolver = function (
                Translator $translator,
                array $data,
                array $rules,
                array $messages = [],
                array $customAttributes = []
            ) {
                return new CustomValidator($translator, $data, $rules, $messages, $customAttributes);
            };

            $validator->resolver($resolver);

            if (isset($app['db'], $app['validation.presence'])) {
                $validator->setPresenceVerifier($app['validation.presence']);
            }

            return $validator;
        });
    }
}

自定义验证器继承了 Laravel 的Illuminate\Validation\Validator并添加了indexAwareSometimes方法:

<?php

namespace App\Validation;

use Closure;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator;

class CustomValidator extends Validator
{
    /**
     * @param  string  $parent
     * @param string|array $attribute
     * @param string|array $rules
     * @param Closure $callback
     */
    public function indexAwareSometimes(string $parent, $attribute, $rules, Closure $callback)
    {
        foreach (Arr::get($this->data, $parent) as $index => $item) {
            if ($callback($this->data, $index)) {
                foreach ((array) $attribute as $key) {
                    $path = $parent.'.'.$index.'.'.$key;
                    $this->addRules([$path => $rules]);
                }
            }
        }
    }
}

然后我们只需要用Illuminate\Validation\ValidationServiceProvider你自己的自定义服务提供商替换 Laravelconfig/app.php就可以了。

它甚至适用于Barry vd。Heuvel 的laravel-ide-helper包裹

return [
    'providers' => [
        //Illuminate\Validation\ValidationServiceProvider::class,
        App\Providers\ValidationServiceProvider::class,
    ]
]

回到上面的例子,你只需要改变getValidatorInstance()你的表单请求的方法:

public function getValidatorInstance()
{
    $validator = parent::getValidatorInstance();

    $validator->indexAwareSometimes(
        'items',
        'special_delivery',
        'required',
        fn ($data, $index) => Arr::get($data, 'items.'.$index.'.asking_price') > 500 &&
            Arr::get($data, 'items.'.$index.'.quantity') > 10
    );
}

推荐阅读