Проверка массива - получить текущую итерацию

Я пытаюсь подтвердить запрос POST с помощью Laravel FormRequest.

Клиент отправляет заказ, в котором есть массив товаров. Мы требуем от пользователя указать, требуется ли элементspecial_delivery только если asking_price > 500 и quantity > 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существует. Что побеждает всю цель!

Любая помощь будет высоко ценится! Спасибо!

2 ответа

Решение

Возможно, я придумал решение вашей проблемы с индексом sometimes если так будет.

Поскольку, к сожалению, невозможно добавить макросы в Validator, вам придется либо переопределить фабрику проверки (это то, что я предлагаю) и использовать свой собственный класс проверки, либо создать вспомогательную функцию на основе метода, передать экземпляр Validator как дополнительный параметр и используйте его вместо $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 массив, в вашем случае items), содержащий все наши другие массивы (items.*) с фактическими данными для проверки и добавлением $rules (required) к $attribute (special_delivery) в текущем индексе, если $callback оценивается как истина.

Для закрытия обратного вызова требуются два параметра, первым из которых является форма $data вашего родительского экземпляра проверки, полученного Validator::getData(), второй $index внешний foreach was at the time it called the callback.

In your case the usage of the function would look a little like this:

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
        );
    }
}

Extending the native Validator class

Extending Laravel's native Validator class isn't as hard as it sounds. We're creating a custom ValidationServiceProvider and inherit Laravel's Illuminate\Validation\ValidationServiceProvider as a parent. Only the registerValidationFactory method needs to be replaced by a copy of it where we specify our custom Validator resolver that should be used by the factory instead:

<?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;
        });
    }
}

The custom validator inherits Laravel's Illuminate\Validation\Validator and adds the indexAwareSometimes method:

<?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]);
                }
            }
        }
    }
}

Then we just need to replace Laravel's Illuminate\Validation\ValidationServiceProvider with your own custom service provider in config/app.php and you're good to go.

It even works with Barry vd. Heuvel's laravel-ide-helper package.

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

Going back to the example above, you only need to change the getValidatorInstance() method of your form request:

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
    );
}

Проблема в том, что я не могу найти способ получить доступ к индексу итерации текущего элемента, чтобы указать, какой элемент проверять.

Почему нет? в вашем запросе... вы можете получить доступ к текущим элементам

$items=$this->items;

foreach($items as $item)

{
 if($item['asking_price']>500 && $item['quantity'] > 10)
{
   if(!isset($item['special_delivery'])
   {
    abort(422,'special_delivery is required');
   }
}
Другие вопросы по тегам