Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Illuminate/Translation/lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'alpha_num' => 'The :attribute field must only contain letters and numbers.',
'any_of' => 'The :attribute field is invalid.',
'array' => 'The :attribute field must be an array.',
'array_keys' => 'The :attribute field must only contain the following keys: :values. Unexpected: :unexpected.',
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
'base64' => 'The :attribute field must be a valid Base64 string.',
'before' => 'The :attribute field must be a date before :date.',
Expand Down
25 changes: 25 additions & 0 deletions src/Illuminate/Validation/Concerns/ReplacesAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,31 @@ protected function replaceInArrayKeys($message, $attribute, $rule, $parameters)
return $this->replaceIn($message, $attribute, $rule, $parameters);
}

/**
* Replace all place-holders for the array rule when accepted keys were given.
*
* @param string $message
* @param string $attribute
* @param string $rule
* @param array<int,string> $parameters
* @return string
*/
protected function replaceArrayKeys($message, $attribute, $rule, $parameters)
{
$value = $this->getValue($attribute);

$unexpected = is_array($value)
? array_keys(array_diff_key($value, array_fill_keys($parameters, '')))
: [];

$displayable = fn ($key) => $this->getDisplayableValue($attribute, $key);

return $this->replaceWhileKeepingCase($message, [
'values' => implode(', ', array_map($displayable, $parameters)),
'unexpected' => implode(', ', array_map($displayable, $unexpected)),
]);
}

/**
* Replace all place-holders for the required_array_keys rule.
*
Expand Down
19 changes: 18 additions & 1 deletion src/Illuminate/Validation/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -1016,13 +1016,30 @@ public function addFailure($attribute, $rule, $parameters = [])
$parameters = $this->replaceDotPlaceholderInParameters($parameters);
}

$messageRule = $this->failedOnUnexpectedArrayKeys($rule, $attribute, $parameters) ? 'ArrayKeys' : $rule;

$this->messages->add($attribute, $this->makeReplacements(
$this->getMessage($attributeWithPlaceholders, $rule), $attribute, $rule, $parameters
$this->getMessage($attributeWithPlaceholders, $messageRule), $attribute, $messageRule, $parameters
));

$this->failedRules[$attribute][$rule] = $parameters;
}

/**
* Determine if the "array" rule failed because the value contained unexpected keys.
*
* @param string $rule
* @param string $attribute
* @param array $parameters
* @return bool
*/
protected function failedOnUnexpectedArrayKeys($rule, $attribute, $parameters)
{
return $rule === 'Array'
&& $parameters !== []
&& is_array($this->getValue($attribute));
}

/**
* Add the given attribute to the list of excluded attributes.
*
Expand Down
103 changes: 103 additions & 0 deletions tests/Validation/ValidationArrayRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public function testArrayValidation()
$v = new Validator($trans, ['foo' => (object) ['key_1' => 'bar']], ['foo' => Rule::array()]);
$this->assertTrue($v->fails());

$v = new Validator($trans, ['foo' => ['key_1' => 'bar', 'key_3' => 'baz']], ['foo' => Rule::array(['key_1', 'key_2'])]);
$this->assertTrue($v->fails());

$v = new Validator($trans, ['foo' => ['bar', 'baz']], ['foo' => Rule::array(['key_1'])]);
$this->assertTrue($v->fails());

$v = new Validator($trans, ['foo' => null], ['foo' => ['nullable', Rule::array()]]);
$this->assertTrue($v->passes());

Expand All @@ -75,5 +81,102 @@ public function testArrayValidation()

$v = new Validator($trans, ['foo' => ['key_1' => 'bar', 'key_2' => '']], ['foo' => ['required', Rule::array(['key_1', 'key_2'])]]);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['foo' => ['key_1' => 'bar']], ['foo' => Rule::array(['key_1', 'key_2'])]);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['foo' => []], ['foo' => Rule::array(['key_1', 'key_2'])]);
$this->assertTrue($v->passes());

$v = new Validator($trans, ['foo' => ['bar', 'baz']], ['foo' => Rule::array([0, 1])]);
$this->assertTrue($v->passes());
}

public function testArrayValidationErrorMessage()
{
$trans = $this->getTranslator();

$v = new Validator($trans, ['foo' => ['key_3' => 'bar', 'key_1' => 'baz', 'key_4' => 'qux']], ['foo' => Rule::array(['key_1', 'key_2'])]);

$this->assertTrue($v->fails());
$this->assertSame(
'The foo field must only contain the following keys: key_1, key_2. Unexpected: key_3, key_4.',
$v->messages()->first('foo')
);
}

public function testArrayValidationErrorMessageFallsBackWhenNotConstrainedByKeys()
{
$trans = $this->getTranslator();

$v = new Validator($trans, ['foo' => 'not an array'], ['foo' => Rule::array(['key_1', 'key_2'])]);
$this->assertTrue($v->fails());
$this->assertSame('The foo field must be an array.', $v->messages()->first('foo'));

$v = new Validator($trans, ['foo' => 'not an array'], ['foo' => Rule::array()]);
$this->assertTrue($v->fails());
$this->assertSame('The foo field must be an array.', $v->messages()->first('foo'));
}

public function testArrayValidationFailureIsRecordedAsTheArrayRule()
{
$trans = $this->getTranslator();

$v = new Validator($trans, ['foo' => ['key_1' => 'bar', 'key_3' => 'baz']], ['foo' => Rule::array(['key_1', 'key_2'])]);

$this->assertTrue($v->fails());
$this->assertSame(['Array' => ['key_1', 'key_2']], $v->failed()['foo']);
}

public function testArrayValidationErrorMessageCanBeCustomised()
{
$trans = $this->getTranslator();

// A custom message may reference just the accepted keys...
$v = new Validator(
$trans,
['foo' => ['key_1' => 'bar', 'key_3' => 'baz']],
['foo' => Rule::array(['key_1', 'key_2'])],
['foo.array_keys' => 'The :attribute field only accepts :values.']
);

$this->assertTrue($v->fails());
$this->assertSame('The foo field only accepts key_1, key_2.', $v->messages()->first('foo'));

// ...or just the unexpected keys.
$v = new Validator(
$trans,
['foo' => ['key_1' => 'bar', 'key_3' => 'baz']],
['foo' => Rule::array(['key_1', 'key_2'])],
['foo.array_keys' => 'The :attribute field may not contain :unexpected.']
);

$this->assertTrue($v->fails());
$this->assertSame('The foo field may not contain key_3.', $v->messages()->first('foo'));
}

public function testArrayValidationErrorMessageDoesNotReExpandUnexpectedKeys()
{
$trans = $this->getTranslator();

$v = new Validator($trans, ['foo' => ['key_1' => 'a', ':values' => 'x', ':attribute' => 'y']], ['foo' => Rule::array(['key_1'])]);

$this->assertTrue($v->fails());
$this->assertSame(
'The foo field must only contain the following keys: key_1. Unexpected: :values, :attribute.',
$v->messages()->first('foo')
);
}

protected function getTranslator()
{
$trans = new Translator(new ArrayLoader, 'en');

$trans->addLines([
'validation.array' => 'The :attribute field must be an array.',
'validation.array_keys' => 'The :attribute field must only contain the following keys: :values. Unexpected: :unexpected.',
], 'en');

return $trans;
}
}
Loading