hirohito
hirohito

Reputation: 21

weird laravel validation in nested array

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

Upvotes: 0

Views: 84

Answers (2)

hirohito
hirohito

Reputation: 21

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

Upvotes: 1

IGP
IGP

Reputation: 15869

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

    ...
}

Upvotes: 0

Related Questions