From 9e86414170b5399bd47c3e2c111e722bbbb568ed Mon Sep 17 00:00:00 2001 From: Ahmad Mohammad Kouja Date: Sat, 8 Feb 2025 10:52:23 +0300 Subject: [PATCH 1/4] [11.x] Fluent Array validation --- src/Illuminate/Validation/Rules/ArrayRule.php | 138 ++++++++++++++-- tests/Validation/ValidationArrayRuleTest.php | 151 ++++++++++++++++++ 2 files changed, 276 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Validation/Rules/ArrayRule.php b/src/Illuminate/Validation/Rules/ArrayRule.php index 8914f77a449c..854961fc209c 100644 --- a/src/Illuminate/Validation/Rules/ArrayRule.php +++ b/src/Illuminate/Validation/Rules/ArrayRule.php @@ -3,18 +3,20 @@ namespace Illuminate\Validation\Rules; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Support\Arr; +use Illuminate\Support\Traits\Conditionable; use Stringable; use function Illuminate\Support\enum_value; class ArrayRule implements Stringable { + use Conditionable; + /** - * The accepted keys. - * - * @var array + * The constraints for the number rule. */ - protected $keys; + protected array $constraints = []; /** * Create a new array rule instance. @@ -28,25 +30,135 @@ public function __construct($keys = null) $keys = $keys->toArray(); } - $this->keys = is_array($keys) ? $keys : func_get_args(); + $keys = is_array($keys) ? $keys : func_get_args(); + + if (empty($keys)) { + return $this->addRule('array'); + } + + $keys = array_map( + static fn($key) => enum_value($key), + $keys, + ); + + return $this->addRule('array:' . implode(',', $keys)); } /** - * Convert the rule to a validation string. + * The field under validation must have a distinct values. + * + * @param bool $strict + * @return $this + */ + public function distinct(bool $strict = false) + { + return $this->addRule($strict ? 'distinct:strict' : 'distinct'); + } + + /** + * The field under validation must have size less than or equal to a maximum value + * + * @param int $max + * @return $this + */ + public function max(int $max) + { + return $this->addRule("max:$max"); + } + + /** + * The field under validation must have size greater than or equal to a minimum value + * + * @param int $min + * @return $this + */ + public function min(int $min) + { + return $this->addRule("min:$min"); + } + + /** + * The field under validation must have a size matching the given value + * + * @param int $size + * @return $this + */ + public function size(int $size) + { + return $this->addRule("size:$size"); + } + + /** + * The field under validation must have a size between the given min and max * - * @return string + * @param int $min + * @param int $max + * @return $this */ - public function __toString() + public function between(int $min, int $max) { - if (empty($this->keys)) { - return 'array'; + return $this->addRule("between:$min,$max"); + } + + /** + * The field under validation must be an array that is a list. + * An array is considered a list if its keys consist of consecutive numbers from 0 to count($array) - 1. + * + * @return $this + */ + public function list() + { + return $this->addRule('list'); + } + + /** + * The field under validation must be an array that contains all of the given parameter values. + * + * @param array|string $keys + * @return $this + */ + public function contains($keys) + { + if ($keys instanceof Arrayable) { + $keys = $keys->toArray(); } + $keys = is_array($keys) ? $keys : func_get_args(); + $keys = array_map( - static fn ($key) => enum_value($key), - $this->keys, + static fn($key) => enum_value($key), + $keys, ); - return 'array:'.implode(',', $keys); + return $this->addRule('contains:' . implode(',', $keys)); + } + + /** + * The field under validation must exist in anotherfield's values. + * + * string $anotherField + * @return $this + */ + public function inArray(string $anotherField) + { + return $this->addRule("in_array:$anotherField"); + } + + /** + * Convert the rule to a validation string. + */ + public function __toString(): string + { + return implode('|', array_unique($this->constraints)); + } + + /** + * Add custom rules to the validation rules array. + */ + protected function addRule(array|string $rules): ArrayRule + { + $this->constraints = array_merge($this->constraints, Arr::wrap($rules)); + + return $this; } } diff --git a/tests/Validation/ValidationArrayRuleTest.php b/tests/Validation/ValidationArrayRuleTest.php index 9df05e81430a..b2c4dde4827d 100644 --- a/tests/Validation/ValidationArrayRuleTest.php +++ b/tests/Validation/ValidationArrayRuleTest.php @@ -5,6 +5,7 @@ use Illuminate\Translation\ArrayLoader; use Illuminate\Translation\Translator; use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\ArrayRule; use Illuminate\Validation\Validator; use PHPUnit\Framework\TestCase; @@ -39,6 +40,107 @@ public function testItCorrectlyFormatsAStringVersionOfTheRule() $this->assertSame('array:key_1,key_2,key_3', (string) $rule); } + public function testDefaultArrayRule() + { + $rule = Rule::array(); + $this->assertEquals('array', (string) $rule); + + $rule = new ArrayRule(); + $this->assertSame('array', (string) $rule); + } + + public function testDefaultArrayRuleWithKeys() + { + $rule = Rule::array(['a', 'b']); + $this->assertEquals('array:a,b', (string) $rule); + + $rule = new ArrayRule([1, 2]); + $this->assertSame('array:1,2', (string) $rule); + } + + public function testDistinctValidation() + { + $rule = Rule::array()->distinct(); + $this->assertEquals('array|distinct', (string) $rule); + + $rule = Rule::array()->distinct(strict: true); + $this->assertEquals('array|distinct:strict', (string) $rule); + } + + public function testMaxRule() + { + $rule = Rule::array()->max(10); + $this->assertEquals('array|max:10', (string) $rule); + } + + public function testMinRule() + { + $rule = Rule::array()->min(10); + $this->assertEquals('array|min:10', (string) $rule); + } + + public function testSizeRule() + { + $rule = Rule::array()->size(10); + $this->assertEquals('array|size:10', (string) $rule); + } + + public function testBetweenRule() + { + $rule = Rule::array()->between(1, 5); + $this->assertEquals('array|between:1,5', (string) $rule); + } + + public function testListRule() + { + $rule = Rule::array()->list(); + $this->assertEquals('array|list', (string) $rule); + } + + public function testContainsRule() + { + $rule = Rule::array()->contains(['a', 'b']); + $this->assertEquals('array|contains:a,b', (string) $rule); + + $rule = Rule::array()->contains('a,b'); + $this->assertEquals('array|contains:a,b', (string) $rule); + + $rule = Rule::array()->contains(collect(['key_1', 'key_2', 'key_3'])); + $this->assertSame('array|contains:key_1,key_2,key_3', (string) $rule); + + $rule = Rule::array()->contains([ArrayKeys::key_1, ArrayKeys::key_2, ArrayKeys::key_3]); + $this->assertSame('array|contains:key_1,key_2,key_3', (string) $rule); + + $rule = Rule::array()->contains([ArrayKeysBacked::key_1, ArrayKeysBacked::key_2, ArrayKeysBacked::key_3]); + $this->assertSame('array|contains:key_1,key_2,key_3', (string) $rule); + } + + public function testInArrayRule() + { + $rule = Rule::array()->inArray('another_filed'); + $this->assertEquals('array|in_array:another_filed', (string) $rule); + } + + public function testChainedRules() + { + $rule = Rule::array() + ->min(5) + ->max(10) + ->distinct() + ->list(); + $this->assertEquals('array|min:5|max:10|distinct|list', (string) $rule); + + $rule = Rule::array() + ->size(5) + ->when(true, function ($rule) { + $rule->distinct(); + }) + ->unless(false, function ($rule) { + $rule->list(); + }); + $this->assertSame('array|size:5|distinct|list', (string) $rule); + } + public function testArrayValidation() { $trans = new Translator(new ArrayLoader, 'en'); @@ -54,5 +156,54 @@ 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' => [1, 2, 3]], + ['foo' => (string) Rule::array()->list()] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => (string) Rule::array()->max(3)] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => (string) Rule::array()->min(3)] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => (string) Rule::array()->between(3, 5)] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => (string) Rule::array()->size(3)] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => (string) Rule::array()->distinct()] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => (string) Rule::array()->contains(1)] + ); + $this->assertTrue($v->passes()); } } From a80ce3f4e3e9c829e8bddd5e8426658eeac35a13 Mon Sep 17 00:00:00 2001 From: Ahmad Mohammad Kouja Date: Sat, 8 Feb 2025 11:02:30 +0300 Subject: [PATCH 2/4] fix styles --- src/Illuminate/Validation/Rules/ArrayRule.php | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Illuminate/Validation/Rules/ArrayRule.php b/src/Illuminate/Validation/Rules/ArrayRule.php index 854961fc209c..a79fff4400ce 100644 --- a/src/Illuminate/Validation/Rules/ArrayRule.php +++ b/src/Illuminate/Validation/Rules/ArrayRule.php @@ -37,17 +37,17 @@ public function __construct($keys = null) } $keys = array_map( - static fn($key) => enum_value($key), + static fn ($key) => enum_value($key), $keys, ); - return $this->addRule('array:' . implode(',', $keys)); + return $this->addRule('array:'.implode(',', $keys)); } /** * The field under validation must have a distinct values. * - * @param bool $strict + * @param bool $strict * @return $this */ public function distinct(bool $strict = false) @@ -56,9 +56,9 @@ public function distinct(bool $strict = false) } /** - * The field under validation must have size less than or equal to a maximum value + * The field under validation must have size less than or equal to a maximum value. * - * @param int $max + * @param int $max * @return $this */ public function max(int $max) @@ -67,9 +67,9 @@ public function max(int $max) } /** - * The field under validation must have size greater than or equal to a minimum value + * The field under validation must have size greater than or equal to a minimum value. * - * @param int $min + * @param int $min * @return $this */ public function min(int $min) @@ -78,9 +78,9 @@ public function min(int $min) } /** - * The field under validation must have a size matching the given value + * The field under validation must have a size matching the given value. * - * @param int $size + * @param int $size * @return $this */ public function size(int $size) @@ -89,10 +89,10 @@ public function size(int $size) } /** - * The field under validation must have a size between the given min and max + * The field under validation must have a size between the given min and max. * - * @param int $min - * @param int $max + * @param int $min + * @param int $max * @return $this */ public function between(int $min, int $max) @@ -114,7 +114,7 @@ public function list() /** * The field under validation must be an array that contains all of the given parameter values. * - * @param array|string $keys + * @param array|string $keys * @return $this */ public function contains($keys) @@ -126,17 +126,18 @@ public function contains($keys) $keys = is_array($keys) ? $keys : func_get_args(); $keys = array_map( - static fn($key) => enum_value($key), + static fn ($key) => enum_value($key), $keys, ); - return $this->addRule('contains:' . implode(',', $keys)); + return $this->addRule('contains:'.implode(',', $keys)); } /** * The field under validation must exist in anotherfield's values. * * string $anotherField + * * @return $this */ public function inArray(string $anotherField) From 2820da4aded4b6bf85e9465718c5b999eb736315 Mon Sep 17 00:00:00 2001 From: Ahmad Mohammad Kouja Date: Sat, 8 Feb 2025 11:32:59 +0300 Subject: [PATCH 3/4] check with === [] instead of empty --- src/Illuminate/Validation/Rules/ArrayRule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Rules/ArrayRule.php b/src/Illuminate/Validation/Rules/ArrayRule.php index a79fff4400ce..f8db3d3d7c11 100644 --- a/src/Illuminate/Validation/Rules/ArrayRule.php +++ b/src/Illuminate/Validation/Rules/ArrayRule.php @@ -32,7 +32,7 @@ public function __construct($keys = null) $keys = is_array($keys) ? $keys : func_get_args(); - if (empty($keys)) { + if ($keys === []) { return $this->addRule('array'); } From f07fd46ea0941370fb7cf5be3e552c748f006cf0 Mon Sep 17 00:00:00 2001 From: Ahmad Mohammad Kouja Date: Sun, 9 Feb 2025 17:03:30 +0300 Subject: [PATCH 4/4] update the method explodeExplicitRule to support Customizable Array Validation --- .../Validation/ValidationRuleParser.php | 3 +- tests/Validation/ValidationArrayRuleTest.php | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/ValidationRuleParser.php b/src/Illuminate/Validation/ValidationRuleParser.php index eccd65102ef7..190cfde31a64 100644 --- a/src/Illuminate/Validation/ValidationRuleParser.php +++ b/src/Illuminate/Validation/ValidationRuleParser.php @@ -9,6 +9,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use Illuminate\Validation\Rules\ArrayRule; use Illuminate\Validation\Rules\Date; use Illuminate\Validation\Rules\Exists; use Illuminate\Validation\Rules\Numeric; @@ -100,7 +101,7 @@ protected function explodeExplicitRule($rule, $attribute) $rules = []; foreach ($rule as $value) { - if ($value instanceof Date || $value instanceof Numeric) { + if ($value instanceof Date || $value instanceof Numeric || $value instanceof ArrayRule) { $rules = array_merge($rules, explode('|', (string) $value)); } else { $rules[] = $this->prepareRule($value, $attribute); diff --git a/tests/Validation/ValidationArrayRuleTest.php b/tests/Validation/ValidationArrayRuleTest.php index b2c4dde4827d..fd1dde7232a4 100644 --- a/tests/Validation/ValidationArrayRuleTest.php +++ b/tests/Validation/ValidationArrayRuleTest.php @@ -205,5 +205,54 @@ public function testArrayValidation() ['foo' => (string) Rule::array()->contains(1)] ); $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => ['required', Rule::array()->list()]] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => ['required', Rule::array()->max(3)]] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => ['required', Rule::array()->min(3)]] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => ['required', Rule::array()->between(3, 5)]] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => ['required', Rule::array()->size(3)]] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => ['required', Rule::array()->distinct()]] + ); + $this->assertTrue($v->passes()); + + $v = new Validator( + $trans, + ['foo' => [1, 2, 3]], + ['foo' => ['required', Rule::array()->contains(1)]] + ); + $this->assertTrue($v->passes()); } }