From 1a563c43b7f17198e79177ad5dc986cc8f532e76 Mon Sep 17 00:00:00 2001 From: Grant Bennett Date: Fri, 24 Jul 2026 21:50:15 +0100 Subject: [PATCH] Add a dedicated validation message for array key failures When the "array" rule is given a set of accepted keys and rejects a genuine array because of an unexpected key, report it with a specific "array_keys" message that lists the accepted (:values) and unexpected (:unexpected) keys, instead of the misleading "must be an array" message. The failure is still recorded as the "array" rule, so failed() is unchanged. --- .../Translation/lang/en/validation.php | 1 + .../Concerns/ReplacesAttributes.php | 25 +++++ src/Illuminate/Validation/Validator.php | 19 +++- tests/Validation/ValidationArrayRuleTest.php | 103 ++++++++++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Translation/lang/en/validation.php b/src/Illuminate/Translation/lang/en/validation.php index 185720dc6d1b..112895d2c35a 100644 --- a/src/Illuminate/Translation/lang/en/validation.php +++ b/src/Illuminate/Translation/lang/en/validation.php @@ -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.', diff --git a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php index 62ebbfbb4b95..1e003e51831e 100644 --- a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php @@ -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 $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. * diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 0880ddf061b6..5022ab940261 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -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. * diff --git a/tests/Validation/ValidationArrayRuleTest.php b/tests/Validation/ValidationArrayRuleTest.php index 1057faa7adf1..9a476d9fc092 100644 --- a/tests/Validation/ValidationArrayRuleTest.php +++ b/tests/Validation/ValidationArrayRuleTest.php @@ -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()); @@ -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; } }