0

I tried some validation rule like: Boolean and Required file with nested array field, but always failing

for example, I tried creating form request like this:

<?php

namespace App\Http\Requests\Test;

use Illuminate\Foundation\Http\FormRequest;

class Test extends FormRequest
{
    public function validationData()
    {
        return [
            'booleanField' => $this->boolean("booleanField"),
            'fileField' => $this->file("fileField"),
            'arrayField' => $this->input("arrayField"),
            'arrayField.*.booleanField' => $this->boolean("arrayField.*.booleanField"),
            'arrayField.*.fileField' => $this->file("arrayField.*.fileField"),
        ];
    }

    public function rules(): array
    {
        return [
            "booleanField" => ["required", "boolean"], // <= works as expected
            "fileField" => ["required", "file", "mimes:jpg,png,jpeg,docx,xlsx,zip", "max:5120"], // <= works as expected
            "arrayField" => ["required", "array"],
            "arrayField.*.booleanField" => ["required", "boolean"], // <= not working, always returning error "The arrayField.0.booleanField field must be true or false."
            "arrayField.*.fileField" => ["required", "file", "mimes:jpg,png,jpeg,docx,xlsx,zip", "max:5120"], // <= not working, always returning error "The arrayField.0.fileField is required."
        ];
    }
}

that's what I found. I don't know if any other rules also not working.

Laravel version 11.31.0. Thank you.

duplicated question from #53489

2 Answers 2

1

The base problem is from client request to my API that using Content-Type: multipart/form-data header

After many hours workaround and based on explanation given by @IGP. This is the solution (probably).

reworked my FormRequest class:

<?php

namespace App\Http\Requests\Test;

use Illuminate\Foundation\Http\FormRequest;

class Test extends FormRequest
{
    public function castData() // reworked from my previous validationData()
    {
        return [
            'booleanField' => [
                "type" => "boolean",
                "default" => false,
            ],
            'nullableBooleanField' => [
                "nullable" => true,
                "type" => "boolean",
            ],
            'fileField' => [
                "type" => "file",
            ],
            'arrayField' => [
                "type" => "input",
            ],
            'arrayField.*.booleanField' => [
                "type" => "boolean",
                "default" => false,
            ],
            'arrayField.*.fileField' => [
                "type" => "file",
            ],
        ];
    }

    public function rules(): array
    {
        return [
            "booleanField" => ["required", "boolean"],
            "nullableBooleanField" => ["nullable", "boolean"],
            "fileField" => ["required", "file", "mimes:jpg,png,jpeg,docx,xlsx,zip", "max:5120"],
            "arrayField" => ["required", "array"],
            "arrayField.*.booleanField" => ["required", "boolean"],
            "arrayField.*.fileField" => ["required", "file", "mimes:jpg,png,jpeg,docx,xlsx,zip", "max:5120"],
        ];
    }

    // I created this custom function below to handle prepareForValidation
    protected function prepareForValidation(): void
    {
        if(method_exists($this, "castData")){
            $this->merge(
                $this->setDefaultToMissingData(
                    $this->resolveCasts(
                        $this->all(),
                        $this->castData()
                    ),
                    Arr::where(Arr::map($this->castData(), function($value,$key){
                        return Arr::get($value, 'default');
                    }), function($value){
                        return !is_null($value);
                    })
                )
            );
        }
    }

    private function resolveCasts(array $data, array $castData, &$discoveredDataKey = null)
    {
        return Arr::map($data, function($value, $key) use ($castData, $discoveredDataKey){
            $discoveredDataKey = ($discoveredDataKey !== null ? $discoveredDataKey.'.' : null).$key;
            if(Arr::accessible($value)){
                return $this->resolveCasts($value, $castData, $discoveredDataKey);
            }else{
                $getCast = Arr::first(Arr::where($castData, function($castValue, $castKey) use ($discoveredDataKey) {
                    return Str::replaceMatches('/\.\d+/', '.*', $discoveredDataKey) === $castKey;
                }));

                $getValue = $this->{Arr::get($getCast, "type", "input")}($discoveredDataKey, Arr::get($getCast, "default"));
                if(Arr::get($getCast, "nullable", false)){
                    $nullableValue = $this->input($discoveredDataKey);
                }
                $value = isset($nullableValue) ? (
                    is_null($nullableValue) ? null : $getValue
                ) : $getValue;

                return $value;
            }
        });
    }

    private function setDefaultToMissingData($data, $casts) {
        foreach ($casts as $cast => $value) {
            $data = $this->setDataValueToDefaultIfNotExists($data, $cast, $value);
        }
        return $data;
    }
    private function setDataValueToDefaultIfNotExists($data, $cast, $value) {
        $keys = explode('.', $cast);
        $current = &$data;

        foreach ($keys as $index => $key) {
            if ($key === '*') {
                foreach ($current as &$subData) {
                    $subData = $this->setDataValueToDefaultIfNotExists($subData, implode('.', Arr::take($keys, $index + 1)), $value);
                }
                return $data;
            }

            if ($index === count($keys) - 1) {
                if (!Arr::exists($current, $key)) {
                    $current[$key] = $value;
                }
                return $data;
            }

            if (!Arr::exists($current, $key) || !Arr::accessible($current[$key])) {
                $current[$key] = [];
            }
            $current = &$current[$key];
        }

        return $data;
    }
}

Now all working as expected.

Maybe not the best for performance. You can always improve that.

Thank you... I hope this helps someone with similar case

Sign up to request clarification or add additional context in comments.

Comments

0

The method validationData is supposed to be a way for you to access the data that is going to be validated. It's not meant to override data.

When a FormRequest is resolved, the application (or container if you prefer) calls its validateResolved method.

public function validateResolved()
{
    $this->prepareForValidation();

    if (! $this->passesAuthorization()) {
        $this->failedAuthorization();
    }

    $instance = $this->getValidatorInstance();

    if ($this->isPrecognitive()) {
        $instance->after(Precognition::afterValidationHook($this));
    }

    if ($instance->fails()) {
        $this->failedValidation($instance);
    }

    $this->passedValidation();
}

If you want to modify the data that is about to get validated, the method you're looking for is prepareForValidation. And to my knowledge you can't use * like a wildcard there.

class Test extends FormRequest
{

    /**
     * Prepare the data for validation.
     *
     * @return void
     */
    public function prepareForValidation(): void
    {
        // please check this and make sure the data you're sending looks like the data you're trying to validate.
        dd($this->validationData());

        $this->merge([
           'key' => 'value,
           ...
        ]);
    }

    ...
}

2 Comments

Thank you for your explanation. It really give me an idea! I will accepted my own answer for the solution
No problem, glad it helped.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.