From 965a45dfc6f1a0b0bde9c131caccabc0a7c39fc1 Mon Sep 17 00:00:00 2001 From: "cuong.tt" Date: Thu, 6 Feb 2025 10:51:00 +0700 Subject: [PATCH 1/6] Fluent string validation --- src/Illuminate/Validation/Rule.php | 11 + .../Validation/Rules/StringRule.php | 310 ++++++++++++++++++ .../Validation/ValidationRuleParser.php | 3 +- tests/Validation/ValidationStringRuleTest.php | 283 ++++++++++++++++ 4 files changed, 606 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Validation/Rules/StringRule.php create mode 100644 tests/Validation/ValidationStringRuleTest.php diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index c15772bba7d5..fc5001d567fb 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -19,6 +19,7 @@ use Illuminate\Validation\Rules\Numeric; use Illuminate\Validation\Rules\ProhibitedIf; use Illuminate\Validation\Rules\RequiredIf; +use Illuminate\Validation\Rules\StringRule; use Illuminate\Validation\Rules\Unique; class Rule @@ -243,4 +244,14 @@ public static function numeric() { return new Numeric; } + + /** + * Get a string rule builder instance. + * + * @return \Illuminate\Validation\Rules\StringRule + */ + public static function string() + { + return new StringRule; + } } diff --git a/src/Illuminate/Validation/Rules/StringRule.php b/src/Illuminate/Validation/Rules/StringRule.php new file mode 100644 index 000000000000..cd9b8ae5702d --- /dev/null +++ b/src/Illuminate/Validation/Rules/StringRule.php @@ -0,0 +1,310 @@ +addRule('alpha'.($onlyAscii ? ':ascii' : '')); + } + + /** + * The field under validation must be entirely Unicode alpha-numeric characters. + * + * @param bool $onlyAscii + * @return $this + */ + public function alphaNumeric(bool $onlyAscii = false): static + { + return $this->addRule('alpha_num'.($onlyAscii ? ':ascii' : '')); + } + + /** + * The field under validation must be entirely Unicode alpha-numeric characters and dash, underscore. + * + * @param bool $onlyAscii + * @return $this + */ + public function alphaDash(bool $onlyAscii = false): static + { + return $this->addRule('alpha_dash'.($onlyAscii ? ':ascii' : '')); + } + + /** + * The field under validation must be entirely 7-bit ASCII characters. + * + * @return $this + */ + public function ascii(): static + { + return $this->addRule('ascii'); + } + + /** + * The field under validation must contain a valid color value in hexadecimal format. + * + * @return $this + */ + public function hexColor(): static + { + return $this->addRule('hex_color'); + } + + /** + * The field under validation must be an IP address. + * + * @param int|null $ver + * @return $this + */ + public function ipAddress(?int $ver = null): static + { + $rule = match ($ver) { + 4 => 'ipv4', + 6 => 'ipv6', + default => 'ip', + }; + + return $this->addRule($rule); + } + + /** + * The field under validation must be an IPv4 address. + * + * @return $this + */ + public function ipv4(): static + { + return $this->ipAddress(4); + } + + /** + * The field under validation must be an IPv6 address. + * + * @return $this + */ + public function ipv6(): static + { + return $this->ipAddress(6); + } + + /** + * The field under validation must be a MAC address. + * + * @return $this + */ + public function macAddress(): static + { + return $this->addRule('mac_address'); + } + + /** + * The field under validation must be a valid JSON string. + * + * @return $this + */ + public function json(): static + { + return $this->addRule('json'); + } + + /** + * The field under validation must not start with one of the given values. + * + * @param array|mixed $values + * @return $this + */ + public function doesntStartWith($values): static + { + $values = is_array($values) ? $values : func_get_args(); + + return $this->addRule('doesnt_start_with:'.Arr::join($values, ',')); + } + + /** + * The field under validation must not end with one of the given values. + * + * @param array|mixed $values + * @return $this + */ + public function doesntEndWith($values): static + { + $values = is_array($values) ? $values : func_get_args(); + + return $this->addRule('doesnt_end_with:'.Arr::join($values, ',')); + } + + /** + * The field under validation must start with one of the given values. + * + * @param array|mixed $values + * @return $this + */ + public function startsWith($values): static + { + $values = is_array($values) ? $values : func_get_args(); + + return $this->addRule('starts_with:'.Arr::join($values, ',')); + } + + /** + * The field under validation must end with one of the given values. + * + * @param array|mixed $values + * @return $this + */ + public function endsWith($values): static + { + $values = is_array($values) ? $values : func_get_args(); + + return $this->addRule('ends_with:'.Arr::join($values, ',')); + } + + /** + * The field under validation must be lowercase. + * + * @return $this + */ + public function lowercase(): static + { + return $this->addRule('lowercase'); + } + + /** + * The field under validation must be uppercase. + * + * @return $this + */ + public function uppercase(): static + { + return $this->addRule('uppercase'); + } + + /** + * The given field must have length equal to the given value. + * + * @param int $length + * @return $this + */ + public function length(int $length): static + { + return $this->addRule('size:'.$length); + } + + /** + * The field under validation must have length less than or equal to the given value. + * + * @param int $max + * @return $this + */ + public function maxLength(int $max): static + { + return $this->addRule('max:'.$max); + } + + /** + * The field under validation must have length greater than or equal to the given value. + * + * @param int $min + * @return $this + */ + public function minLength(int $min): static + { + return $this->addRule('min:'.$min); + } + + /** + * The field under validation must have a different value than field. + * + * @param string $field + * @return $this + */ + public function different(string $field): static + { + return $this->addRule('different:'.$field); + } + + /** + * The given field must match the field under validation. + * + * @param string $field + * @return $this + */ + public function same(string $field): static + { + return $this->addRule('same:'.$field); + } + + /** + * The field under validation must be a valid URL. + * + * @param array|mixed $protocols + * @return $this + */ + public function url($protocols = []): static + { + $protocols = is_array($protocols) ? $protocols : func_get_args(); + + return $this->addRule('url'.($protocols ? ':'.Arr::join($protocols, ',') : '')); + } + + /** + * The field under validation must be a valid ULID. + * + * @return $this + */ + public function ulid(): static + { + return $this->addRule('ulid'); + } + + /** + * The field under validation must be a valid UUID. + * + * @return $this + */ + public function uuid(): static + { + return $this->addRule('uuid'); + } + + /** + * Convert the rule to a validation string. + * + * @return string + */ + public function __toString(): string + { + return implode('|', array_unique($this->constraints)); + } + + /** + * Add custom rules to the validation rules array. + * + * @param array|string $rules + * @return $this + */ + protected function addRule(array|string $rules): static + { + $this->constraints = array_merge($this->constraints, Arr::wrap($rules)); + + return $this; + } +} diff --git a/src/Illuminate/Validation/ValidationRuleParser.php b/src/Illuminate/Validation/ValidationRuleParser.php index eccd65102ef7..68c4149252fb 100644 --- a/src/Illuminate/Validation/ValidationRuleParser.php +++ b/src/Illuminate/Validation/ValidationRuleParser.php @@ -12,6 +12,7 @@ use Illuminate\Validation\Rules\Date; use Illuminate\Validation\Rules\Exists; use Illuminate\Validation\Rules\Numeric; +use Illuminate\Validation\Rules\StringRule; use Illuminate\Validation\Rules\Unique; class ValidationRuleParser @@ -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 StringRule) { $rules = array_merge($rules, explode('|', (string) $value)); } else { $rules[] = $this->prepareRule($value, $attribute); diff --git a/tests/Validation/ValidationStringRuleTest.php b/tests/Validation/ValidationStringRuleTest.php new file mode 100644 index 000000000000..ff687c95336d --- /dev/null +++ b/tests/Validation/ValidationStringRuleTest.php @@ -0,0 +1,283 @@ +assertEquals('string', (string) $rule); + + $rule = new StringRule(); + $this->assertSame('string', (string) $rule); + } + + public function testAlphaRule() + { + $rule = Rule::string()->alpha(); + $this->assertEquals('string|alpha', (string) $rule); + + $rule = Rule::string()->alpha(true); + $this->assertEquals('string|alpha:ascii', (string) $rule); + } + + public function testAlphaNumericRule() + { + $rule = Rule::string()->alphaNumeric(); + $this->assertEquals('string|alpha_num', (string) $rule); + + $rule = Rule::string()->alphaNumeric(true); + $this->assertEquals('string|alpha_num:ascii', (string) $rule); + } + + public function testAlphaDashRule() + { + $rule = Rule::string()->alphaDash(); + $this->assertEquals('string|alpha_dash', (string) $rule); + + $rule = Rule::string()->alphaDash(true); + $this->assertEquals('string|alpha_dash:ascii', (string) $rule); + } + + public function testAsciiRule() + { + $rule = Rule::string()->ascii(); + $this->assertEquals('string|ascii', (string) $rule); + } + + public function testHexColorRule() + { + $rule = Rule::string()->hexColor(); + $this->assertEquals('string|hex_color', (string) $rule); + } + + public function testIpAddressRule() + { + $rule = Rule::string()->ipAddress(); + $this->assertEquals('string|ip', (string) $rule); + + $rule = Rule::string()->ipAddress(4); + $this->assertEquals('string|ipv4', (string) $rule); + + $rule = Rule::string()->ipAddress(6); + $this->assertEquals('string|ipv6', (string) $rule); + + $rule = Rule::string()->ipv4(); + $this->assertEquals('string|ipv4', (string) $rule); + + $rule = Rule::string()->ipv6(); + $this->assertEquals('string|ipv6', (string) $rule); + } + + public function testMacAddressRule() + { + $rule = Rule::string()->macAddress(); + $this->assertEquals('string|mac_address', (string) $rule); + } + + public function testJsonRule() + { + $rule = Rule::string()->json(); + $this->assertEquals('string|json', (string) $rule); + } + + public function testDoesntStartWithRule() + { + $rule = Rule::string()->doesntStartWith('foo'); + $this->assertEquals('string|doesnt_start_with:foo', (string) $rule); + + $rule = Rule::string()->doesntStartWith(['foo', 'bar']); + $this->assertEquals('string|doesnt_start_with:foo,bar', (string) $rule); + + $rule = Rule::string()->doesntStartWith('foo', 'bar'); + $this->assertEquals('string|doesnt_start_with:foo,bar', (string) $rule); + } + + public function testDoesntEndWithRule() + { + $rule = Rule::string()->doesntEndWith('foo'); + $this->assertEquals('string|doesnt_end_with:foo', (string) $rule); + + $rule = Rule::string()->doesntEndWith(['foo', 'bar']); + $this->assertEquals('string|doesnt_end_with:foo,bar', (string) $rule); + + $rule = Rule::string()->doesntEndWith('foo', 'bar'); + $this->assertEquals('string|doesnt_end_with:foo,bar', (string) $rule); + } + + public function testStartsWithRule() + { + $rule = Rule::string()->startsWith('foo'); + $this->assertEquals('string|starts_with:foo', (string) $rule); + + $rule = Rule::string()->startsWith(['foo', 'bar']); + $this->assertEquals('string|starts_with:foo,bar', (string) $rule); + + $rule = Rule::string()->startsWith('foo', 'bar'); + $this->assertEquals('string|starts_with:foo,bar', (string) $rule); + } + + public function testEndsWithRule() + { + $rule = Rule::string()->endsWith('foo'); + $this->assertEquals('string|ends_with:foo', (string) $rule); + + $rule = Rule::string()->endsWith(['foo', 'bar']); + $this->assertEquals('string|ends_with:foo,bar', (string) $rule); + + $rule = Rule::string()->endsWith('foo', 'bar'); + $this->assertEquals('string|ends_with:foo,bar', (string) $rule); + } + + public function testLowercaseRule() + { + $rule = Rule::string()->lowercase(); + $this->assertEquals('string|lowercase', (string) $rule); + } + + public function testUppercaseRule() + { + $rule = Rule::string()->uppercase(); + $this->assertEquals('string|uppercase', (string) $rule); + } + + public function testLengthRule() + { + $rule = Rule::string()->length(10); + $this->assertEquals('string|size:10', (string) $rule); + } + + public function testMaxLengthRule() + { + $rule = Rule::string()->maxLength(10); + $this->assertEquals('string|max:10', (string) $rule); + } + + public function testMinLengthRule() + { + $rule = Rule::string()->minLength(3); + $this->assertEquals('string|min:3', (string) $rule); + } + + public function testDifferentRule() + { + $rule = Rule::string()->different('foo'); + $this->assertEquals('string|different:foo', (string) $rule); + } + + public function testSameRule() + { + $rule = Rule::string()->same('foo'); + $this->assertEquals('string|same:foo', (string) $rule); + } + + public function testUrlRule() + { + $rule = Rule::string()->url(); + $this->assertEquals('string|url', (string) $rule); + + $rule = Rule::string()->url('http'); + $this->assertEquals('string|url:http', (string) $rule); + + $rule = Rule::string()->url(['http', 'https']); + $this->assertEquals('string|url:http,https', (string) $rule); + + $rule = Rule::string()->url('http', 'https'); + $this->assertEquals('string|url:http,https', (string) $rule); + } + + public function testChainedRules() + { + $rule = Rule::string() + ->minLength(3) + ->maxLength(10) + ->alpha(true) + ->different('foo'); + + $this->assertEquals('string|min:3|max:10|alpha:ascii|different:foo', (string) $rule); + + $rule = Rule::string() + ->hexColor() + ->when(true, function ($rule) { + $rule->same('foo'); + }) + ->unless(true, function ($rule) { + $rule->different('bar'); + }); + $this->assertSame('string|hex_color|same:foo', (string) $rule); + } + + public function testStringRuleValidation() + { + $trans = new Translator(new ArrayLoader, 'en'); + + $rule = Rule::string(); + + $validator = new Validator( + $trans, + ['foo' => [1,2,3]], + ['foo' => $rule] + ); + + $this->assertSame( + $trans->get('validation.foo'), + $validator->errors()->first('foo') + ); + + $validator = new Validator( + $trans, + ['foo' => 'bar'], + ['foo' => $rule] + ); + + $this->assertEmpty($validator->errors()->first('foo')); + + $rule = Rule::string()->alpha(true)->minLength(3)->maxLength(10); + + $validator = new Validator( + $trans, + ['foo' => 'bar'], + ['foo' => (string) $rule] + ); + + $this->assertEmpty($validator->errors()->first('foo')); + + $rule = Rule::string()->different('bar'); + + $validator = new Validator( + $trans, + ['foo' => 'new', 'bar' => 'old'], + ['foo' => (string) $rule] + ); + + $this->assertEmpty($validator->errors()->first('foo')); + + $rule = Rule::string()->length(5); + + $validator = new Validator( + $trans, + ['foo' => 'field'], + ['foo' => (string) $rule] + ); + + $this->assertEmpty($validator->errors()->first('foo')); + + $rule = Rule::string()->length(5)->ascii()->lowercase(); + + $validator = new Validator( + $trans, + ['foo' => 'field'], + ['foo' => [$rule]] + ); + + $this->assertEmpty($validator->errors()->first('foo')); + } +} From 5f2e63edd49b5d6c5924162ef85a51e81a232239 Mon Sep 17 00:00:00 2001 From: "cuong.tt" Date: Thu, 6 Feb 2025 11:06:18 +0700 Subject: [PATCH 2/6] styleci --- src/Illuminate/Validation/Rules/StringRule.php | 2 +- tests/Validation/ValidationStringRuleTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Validation/Rules/StringRule.php b/src/Illuminate/Validation/Rules/StringRule.php index cd9b8ae5702d..49e4a8a079d4 100644 --- a/src/Illuminate/Validation/Rules/StringRule.php +++ b/src/Illuminate/Validation/Rules/StringRule.php @@ -16,7 +16,7 @@ class StringRule implements Stringable protected array $constraints = ['string']; /** - * The field under validation must be entirely Unicode alphabetic characters + * The field under validation must be entirely Unicode alphabetic characters. * * @param bool $onlyAscii * @return $this diff --git a/tests/Validation/ValidationStringRuleTest.php b/tests/Validation/ValidationStringRuleTest.php index ff687c95336d..ee380cf0795a 100644 --- a/tests/Validation/ValidationStringRuleTest.php +++ b/tests/Validation/ValidationStringRuleTest.php @@ -223,7 +223,7 @@ public function testStringRuleValidation() $validator = new Validator( $trans, - ['foo' => [1,2,3]], + ['foo' => [1, 2, 3]], ['foo' => $rule] ); From 0b612d7763d5403d49ef21541fc0b38dc5a2da07 Mon Sep 17 00:00:00 2001 From: "cuong.tt" Date: Thu, 6 Feb 2025 11:10:52 +0700 Subject: [PATCH 3/6] fix testcase --- tests/Validation/ValidationStringRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Validation/ValidationStringRuleTest.php b/tests/Validation/ValidationStringRuleTest.php index ee380cf0795a..3f0c98e1c96e 100644 --- a/tests/Validation/ValidationStringRuleTest.php +++ b/tests/Validation/ValidationStringRuleTest.php @@ -228,7 +228,7 @@ public function testStringRuleValidation() ); $this->assertSame( - $trans->get('validation.foo'), + $trans->get('validation.string'), $validator->errors()->first('foo') ); From 3a57ccf8f60b8ff7554fe719dcae3c3bd6b9ff9c Mon Sep 17 00:00:00 2001 From: "cuong.tt" Date: Thu, 6 Feb 2025 15:40:40 +0700 Subject: [PATCH 4/6] add active url for string rule validation --- src/Illuminate/Validation/Rules/StringRule.php | 10 ++++++++++ tests/Validation/ValidationStringRuleTest.php | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/src/Illuminate/Validation/Rules/StringRule.php b/src/Illuminate/Validation/Rules/StringRule.php index 49e4a8a079d4..fc917033e7f5 100644 --- a/src/Illuminate/Validation/Rules/StringRule.php +++ b/src/Illuminate/Validation/Rules/StringRule.php @@ -252,6 +252,16 @@ public function same(string $field): static return $this->addRule('same:'.$field); } + /** + * The field under validation must be a valid A or AAAA record. + * + * @return $this + */ + public function activeUrl(): static + { + return $this->addRule('active_url'); + } + /** * The field under validation must be a valid URL. * diff --git a/tests/Validation/ValidationStringRuleTest.php b/tests/Validation/ValidationStringRuleTest.php index 3f0c98e1c96e..0bfcc02f8ac6 100644 --- a/tests/Validation/ValidationStringRuleTest.php +++ b/tests/Validation/ValidationStringRuleTest.php @@ -179,6 +179,12 @@ public function testSameRule() $this->assertEquals('string|same:foo', (string) $rule); } + public function testActiveUrlRule() + { + $rule = Rule::string()->activeUrl(); + $this->assertEquals('string|active_url', (string) $rule); + } + public function testUrlRule() { $rule = Rule::string()->url(); From 730853b573ce106c98d7ebe8559e880dc240dcc6 Mon Sep 17 00:00:00 2001 From: "cuong.tt" Date: Sat, 8 Feb 2025 00:59:35 +0700 Subject: [PATCH 5/6] add more rules for string validation --- .../Validation/Rules/StringRule.php | 44 +++++++++++++++++++ tests/Validation/ValidationStringRuleTest.php | 34 ++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/Illuminate/Validation/Rules/StringRule.php b/src/Illuminate/Validation/Rules/StringRule.php index fc917033e7f5..d66ae0ec136c 100644 --- a/src/Illuminate/Validation/Rules/StringRule.php +++ b/src/Illuminate/Validation/Rules/StringRule.php @@ -230,6 +230,50 @@ public function minLength(int $min): static return $this->addRule('min:'.$min); } + /** + * The field under validation must be shorter than the given field. + * + * @param string $field + * @return $this + */ + public function shorterThan(string $field): static + { + return $this->addRule('lt:'.$field); + } + + /** + * The field under validation must be shorter than or equal to the given field. + * + * @param string $field + * @return $this + */ + public function shorterThanOrEqualTo(string $field): static + { + return $this->addRule('lte:'.$field); + } + + /** + * The field under validation must be longer than the given field. + * + * @param string $field + * @return $this + */ + public function longerThan(string $field): static + { + return $this->addRule('gt:'.$field); + } + + /** + * The field under validation must be longer than or equal to the given field. + * + * @param string $field + * @return $this + */ + public function longerThanOrEqualTo(string $field): static + { + return $this->addRule('gte:'.$field); + } + /** * The field under validation must have a different value than field. * diff --git a/tests/Validation/ValidationStringRuleTest.php b/tests/Validation/ValidationStringRuleTest.php index 0bfcc02f8ac6..8a47bc780a02 100644 --- a/tests/Validation/ValidationStringRuleTest.php +++ b/tests/Validation/ValidationStringRuleTest.php @@ -167,6 +167,30 @@ public function testMinLengthRule() $this->assertEquals('string|min:3', (string) $rule); } + public function testShorterThanRule() + { + $rule = Rule::string()->shorterThan('foo'); + $this->assertEquals('string|lt:foo', (string) $rule); + } + + public function testShorterThanOrEqualToRule() + { + $rule = Rule::string()->shorterThanOrEqualTo('foo'); + $this->assertEquals('string|lte:foo', (string) $rule); + } + + public function testLongerThanRule() + { + $rule = Rule::string()->longerThan('foo'); + $this->assertEquals('string|gt:foo', (string) $rule); + } + + public function testLongerThanOrEqualToRule() + { + $rule = Rule::string()->shorterThanOrEqualTo('foo'); + $this->assertEquals('string|gte:foo', (string) $rule); + } + public function testDifferentRule() { $rule = Rule::string()->different('foo'); @@ -276,6 +300,16 @@ public function testStringRuleValidation() $this->assertEmpty($validator->errors()->first('foo')); + $rule = Rule::string()->longerThan('foo')->shorterThan('bar'); + + $validator = new Validator( + $trans, + ['foo' => 'bb', 'bar' => 'aaaaa', 'baz' => 'ccc'], + ['baz' => (string) $rule] + ); + + $this->assertEmpty($validator->errors()->first('baz')); + $rule = Rule::string()->length(5)->ascii()->lowercase(); $validator = new Validator( From a7a4b80bae5160f0b9de18987784aa26afa0bd7c Mon Sep 17 00:00:00 2001 From: "cuong.tt" Date: Sat, 8 Feb 2025 01:11:05 +0700 Subject: [PATCH 6/6] fix tests --- tests/Validation/ValidationStringRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Validation/ValidationStringRuleTest.php b/tests/Validation/ValidationStringRuleTest.php index 8a47bc780a02..5316813800c5 100644 --- a/tests/Validation/ValidationStringRuleTest.php +++ b/tests/Validation/ValidationStringRuleTest.php @@ -187,7 +187,7 @@ public function testLongerThanRule() public function testLongerThanOrEqualToRule() { - $rule = Rule::string()->shorterThanOrEqualTo('foo'); + $rule = Rule::string()->longerThanOrEqualTo('foo'); $this->assertEquals('string|gte:foo', (string) $rule); }