phplaravellaravel-validationlaravel-11

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


Solution

  • 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