14

How can we validate form fields that are arrays? Take a look at the following code

UserPhone Model:

 public static $rules= array(
    'phonenumber'=>'required|numeric',
    'isPrimary'=>'in:0,1'
)
...........

UserController:

$validation = UserPhone::validate(Input::only('phonenumber')));


    if($validation->passes())
      {
         $allInputs = Input::only('phonenumber','tid');
         $loopSize = sizeOf($allInputs);

         for($i=0;$i<$loopSize;$i++)
         {

         $phone = UserPhone::find($allInputs['tid'][$i]);
         $phone->phonenumber = $allInputs['phonenumber'][$i];
         $phone->save();

        }

     return Redirect::to('myprofile')->with('message','Update OK');

  }
  else
  {
     return Redirect::to('editPhone')->withErrors($validation);

  } 

}

the $validation comes from a BaseModel which extends Eloquent.

In my view:

 <?php $counter=1; ?>
          @foreach($phones as $thephone)

           <section class="col col-12">
              <label class="label">Phone Number {{$counter++}}</label>
              <label class="input">
              <i class="icon-append icon-phone"></i>
                 {{Form::text('phonenumber[]',$thephone->phonenumber)}}
                 {{Form::hidden('tid[]',$thephone->id)}}
              </label>
            </section>
          @endforeach

Everything is working fine and I get all the phone numbers I want in the Update Form, but I cannot update the model because the validation fails with the message "Phonenumber must be a number".

I know that there is not a simple solution for validating array form fields and I tried to extend the validator class but with no success.

How can I validate this kind of fields?

Luís Cruz
  • 14,780
  • 16
  • 68
  • 100
Archimidis M
  • 151
  • 1
  • 2
  • 9

5 Answers5

25

Here's the solution I use:

Usage

Simply transform your usual rules by prefixing each. For example:

'names' => 'required|array|each:exists,users,name'

Note that the each rule assumes your field is an array, so don't forget to use the array rule before as shown here.

Error Messages

Error messages will be automatically calculated by the singular form (using Laravel's str_singular() helper) of your field. In the previous example, the attribute is name.

Nested Arrays

This method works out of the box with nested arrays of any depth in dot notation. For example, this works:

'members.names' => 'required|array|each:exists,users,name'

Again, the attribute used for error messages here will be name.

Custom Rules

This method supports any of your custom rules out of the box.

Implementation

1. Extend the validator class

class ExtendedValidator extends Illuminate\Validation\Validator {

    public function validateEach($attribute, $value, $parameters)
    {
        // Transform the each rule
        // For example, `each:exists,users,name` becomes `exists:users,name`
        $ruleName = array_shift($parameters);
        $rule = $ruleName.(count($parameters) > 0 ? ':'.implode(',', $parameters) : '');

        foreach ($value as $arrayKey => $arrayValue)
        {
            $this->validate($attribute.'.'.$arrayKey, $rule);
        }

        // Always return true, since the errors occur for individual elements.
        return true;
    }

    protected function getAttribute($attribute)
    {
        // Get the second to last segment in singular form for arrays.
        // For example, `group.names.0` becomes `name`.
        if (str_contains($attribute, '.'))
        {
            $segments = explode('.', $attribute);

            $attribute = str_singular($segments[count($segments) - 2]);
        }

        return parent::getAttribute($attribute);
    }
}

2. Register your validator extension

Anywhere in your usual bootstrap locations, add the following code:

Validator::resolver(function($translator, $data, $rules, $messages)
{
    return new ExtendedValidator($translator, $data, $rules, $messages);
});

And that's it! Enjoy!

Bonus: Size rules with arrays

As a comment pointed out, there's seems to be no easy way to validate array sizes. However, the Laravel documentation is lacking for size rules: it doesn't mention that it can count array elements. This means you're actually allowed to use size, min, max and between rules to count array elements.

Lazlo
  • 8,518
  • 14
  • 77
  • 116
  • Thank you. But when I run `$validator = \Validator::make(Input::all(), ['names' => 'required|array|each:exists,users,username']);` . It always return true even I don't fill anything. – Joshua Hansen Jan 27 '15 at 05:34
  • 1
    The solution is to use `required|array|min:1|each...`. I updated my answer to mention why this works. **Why it happens**: I agree your case might seem odd, but it's normal behaviour. You are passing an array object (PHP type `array()`), not null, so the field isn't empty (`required` passes). Your object is an array, so `array` passes. And finally, no element is in the array, so `each` doesn't perform any validation and passes. – Lazlo Feb 24 '15 at 19:25
  • 1
    In which folder do I create it? – Volatil3 Apr 03 '15 at 06:32
9

It works best to extend the Validator class and re-use the existing Validator functions:

Validator::resolver(function($translator, $data, $rules, $messages)
{

    return new Validation($translator, $data, $rules, $messages);

});

class Validation extends Illuminate\Validation\Validator {

    /**
     * Magically adds validation methods. Normally the Laravel Validation methods
     * only support single values to be validated like 'numeric', 'alpha', etc.
     * Here we copy those methods to work also for arrays, so we can validate
     * if a value is OR an array contains only 'numeric', 'alpha', etc. values.
     *
     * $rules = array(
     *     'row_id' => 'required|integerOrArray', // "row_id" must be an integer OR an array containing only integer values
     *     'type'   => 'inOrArray:foo,bar' // "type" must be 'foo' or 'bar' OR an array containing nothing but those values
     * );
     *
     * @param string $method Name of the validation to perform e.g. 'numeric', 'alpha', etc.
     * @param array $parameters Contains the value to be validated, as well as additional validation information e.g. min:?, max:?, etc.
     */
    public function __call($method, $parameters)
    {

        // Convert method name to its non-array counterpart (e.g. validateNumericArray converts to validateNumeric)
        if (substr($method, -7) === 'OrArray')
            $method = substr($method, 0, -7);

        // Call original method when we are dealing with a single value only, instead of an array
        if (! is_array($parameters[1]))
            return call_user_func_array(array($this, $method), $parameters);

        $success = true;
        foreach ($parameters[1] as $value) {
            $parameters[1] = $value;
            $success &= call_user_func_array(array($this, $method), $parameters);
        }

        return $success;

    }

    /**
     * All ...OrArray validation functions can use their non-array error message counterparts
     *
     * @param mixed $attribute The value under validation
     * @param string $rule Validation rule
     */
    protected function getMessage($attribute, $rule)
    {

        if (substr($rule, -7) === 'OrArray')
            $rule = substr($rule, 0, -7);

        return parent::getMessage($attribute, $rule);

    }
}
Ronald Hulshof
  • 1,986
  • 16
  • 22
6

each()

It's not in the docs, but the 4.2 branch may have a simple solution around line 220.

Just like the sometimes($attribute, $rules, callable $callback) function, there is now an each($attribute, $rules) function.

To use it, the code would be something simpler than a sometimes() call:

$v->each('array_attribute',array('rule','anotherRule')); //$v is your validator

Caveats

  • sometimes() and each() don't seem to be easily chainable with each other so if you want to do specifically conditioned rules on array values, you're better off with the magic solutions in other answers for now.
  • each() only goes one level deep which isn't that different from other solutions. The nice thing about the magic solutions is that they will go 0 or 1 level deep as needed by calling the base rules as appropriate so I suppose if you wanted to go 1 to 2 levels deep, you could simply merge the two approaches by calling each() and passing it a magic rule from the other answers.
  • each() only takes one attribute, not an array of attributes as sometimes() does, but adding this feature to each() wouldn't be a massive change to the each() function - just loop through the $attribute and array_merge() $data and the array_get() result. Someone can make it a pull request on master if they see it as desirable and it hasn't already been done and we can see if it makes it into a future build.
skovacs1
  • 461
  • 1
  • 6
  • 14
  • Also, in the past, to generally deal with this problem in any version of laravel, I often just sanitized my input to not be arrays in the first place by running some loops on the input to pull out the things I want, adding to my validation rules array as I go. One particular advantage is that when the array key is not meaningful output (multi-selects) in the validator's error messages, you can insert something more meaningful based on the value at that key, leading to more comprehensible errors. – skovacs1 Jul 28 '14 at 03:46
  • just a note. At each you should use array for rules parameter otherwise it wont work – GorillaApe Sep 01 '14 at 10:20
  • @Parhs indeed. Corrected. – skovacs1 Sep 02 '14 at 19:47
1

Here's an update to the code of Ronald, because my custom rules wouldn't work with the array extension. Tested with Laravel 4.1, default rules, extended rules, …

public function __call($method, $parameters) {
    $isArrayRule = FALSE;
    if(substr($method, -5) === 'Array') {
        $method = substr($method, 0, -5);
        $isArrayRule = TRUE;
    }

    //
    $rule = snake_case(substr($method, 8));

    // Default or custom rule
    if(!$isArrayRule) {
        // And we have a default value (not an array)
        if(!is_array($parameters[1])) {
            // Try getting the custom validation rule
            if(isset($this->extensions[$rule])) {
                return $this->callExtension($rule, $parameters);
            }

            // None found
            throw new \BadMethodCallException("Method [$method] does not exist.");
        } // Array given for default rule; cannot be!
        else return FALSE;
    }

    // Array rules
    $success = TRUE;
    foreach($parameters[1] as $value) {
        $parameters[1] = $value;

        // Default rule exists, use it
        if(is_callable("parent::$method")) {
            $success &= call_user_func_array(array($this, $method), $parameters);
        } else {
            // Try a custom rule
            if(isset($this->extensions[$rule])) {
                $success &= $this->callExtension($rule, $parameters);
            }

            // No custom rule found
            throw new \BadMethodCallException("Method [$method] does not exist.");
        }
    }

    // Did any of them (array rules) fail?
    return $success;
}
Sebastiaan Luca
  • 486
  • 1
  • 8
  • 9
1

There are now array validation rules in case this helps anybody. It doesn't appear that these have been written up in the docs yet.

https://github.com/laravel/laravel/commit/6a2ad475cfb21d12936cbbb544d8a136fc73be97

Gravy
  • 12,264
  • 26
  • 124
  • 193
  • These only cover very specific cases, but you are correct. The other answers provide a much more comprehensive set of "magic" functions. – ahamilton9 May 06 '14 at 14:18
  • Seems for Laravel 5 only – Volatil3 Apr 02 '15 at 18:37
  • @Volatil3 - Considering L5 only came out this year, and I answered the question almost exactly 1 year ago - how is it for L5 only? See tags in link: v5.0.22 v5.0.16 v5.0.1 v5.0.0 v4.2.11 v4.2.0 v4.1.27 v4.1.18 v4.1.0 v4.0.9 v4.0.8 v4.0.7 v4.0.6 – Gravy Apr 02 '15 at 20:55
  • @Gravy pardon. Yes you are right. So setting rule like `'availability_date[]' => 'array|required',` will work? – Volatil3 Apr 03 '15 at 06:23
  • @Gravy Also, do I need to write custom validator too? – Volatil3 Apr 03 '15 at 06:25