diff --git a/src/translation/lang/en/validation.php b/src/translation/lang/en/validation.php index b535c494d..d5ae0ea35 100644 --- a/src/translation/lang/en/validation.php +++ b/src/translation/lang/en/validation.php @@ -33,7 +33,9 @@ 'array' => 'The :attribute must have between :min and :max items.', ], 'boolean' => 'The :attribute field must be true or false.', + 'can' => 'The :attribute field contains an unauthorized value.', 'confirmed' => 'The :attribute confirmation does not match.', + 'contains' => 'The :attribute field is missing a required value.', 'date' => 'The :attribute is not a valid date.', 'date_equals' => 'The :attribute must be a date equal to :date.', 'date_format' => 'The :attribute does not match the format :format.', @@ -45,6 +47,7 @@ 'digits_between' => 'The :attribute must be between :min and :max digits.', 'dimensions' => 'The :attribute has invalid image dimensions.', 'distinct' => 'The :attribute field has a duplicate value.', + 'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.', 'doesnt_end_with' => 'The :attribute must not end with one of the following: :values.', 'doesnt_start_with' => 'The :attribute must not start with one of the following: :values.', 'email' => 'The :attribute must be a valid email address.', diff --git a/src/validation/src/Concerns/ValidatesAttributes.php b/src/validation/src/Concerns/ValidatesAttributes.php index 10ba647d3..49a3b3641 100644 --- a/src/validation/src/Concerns/ValidatesAttributes.php +++ b/src/validation/src/Concerns/ValidatesAttributes.php @@ -432,6 +432,26 @@ public function validateContains(string $attribute, mixed $value, mixed $paramet return true; } + /** + * Validate an attribute does not contain a list of values. + * + * @param array $parameters + */ + public function validateDoesntContain(string $attribute, mixed $value, mixed $parameters): bool + { + if (! is_array($value)) { + return false; + } + + foreach ($parameters as $parameter) { + if (in_array($parameter, $value)) { + return false; + } + } + + return true; + } + /** * Validate that the password of the currently authenticated user matches the given value. * diff --git a/src/validation/src/Rule.php b/src/validation/src/Rule.php index 626cfa1aa..de57cca43 100644 --- a/src/validation/src/Rule.php +++ b/src/validation/src/Rule.php @@ -14,8 +14,10 @@ use Hypervel\Validation\Rules\AnyOf; use Hypervel\Validation\Rules\ArrayRule; use Hypervel\Validation\Rules\Can; +use Hypervel\Validation\Rules\Contains; use Hypervel\Validation\Rules\Date; use Hypervel\Validation\Rules\Dimensions; +use Hypervel\Validation\Rules\DoesntContain; use Hypervel\Validation\Rules\Email; use Hypervel\Validation\Rules\Enum; use Hypervel\Validation\Rules\ExcludeIf; @@ -121,6 +123,30 @@ public static function notIn(array|Arrayable|BackedEnum|string|UnitEnum $values) return new NotIn(is_array($values) ? $values : func_get_args()); } + /** + * Get a contains rule builder instance. + */ + public static function contains(array|Arrayable|BackedEnum|string|UnitEnum $values): Contains + { + if ($values instanceof Arrayable) { + $values = $values->toArray(); + } + + return new Contains(is_array($values) ? $values : func_get_args()); + } + + /** + * Get a doesnt_contain rule builder instance. + */ + public static function doesntContain(array|Arrayable|BackedEnum|string|UnitEnum $values): DoesntContain + { + if ($values instanceof Arrayable) { + $values = $values->toArray(); + } + + return new DoesntContain(is_array($values) ? $values : func_get_args()); + } + /** * Get a required_if rule builder instance. */ @@ -153,6 +179,14 @@ public static function date(): Date return new Date(); } + /** + * Get a datetime rule builder instance. + */ + public static function dateTime(): Date + { + return (new Date())->format('Y-m-d H:i:s'); + } + /** * Get an email rule builder instance. */ diff --git a/src/validation/src/Rules/Contains.php b/src/validation/src/Rules/Contains.php new file mode 100644 index 000000000..f736aa794 --- /dev/null +++ b/src/validation/src/Rules/Contains.php @@ -0,0 +1,46 @@ +toArray(); + } + + $this->values = is_array($values) ? $values : func_get_args(); + } + + /** + * Convert the rule to a validation string. + */ + public function __toString(): string + { + $values = array_map(function ($value) { + $value = enum_value($value); + + return '"' . str_replace('"', '""', (string) $value) . '"'; + }, $this->values); + + return 'contains:' . implode(',', $values); + } +} diff --git a/src/validation/src/Rules/DoesntContain.php b/src/validation/src/Rules/DoesntContain.php new file mode 100644 index 000000000..3a9357751 --- /dev/null +++ b/src/validation/src/Rules/DoesntContain.php @@ -0,0 +1,46 @@ +toArray(); + } + + $this->values = is_array($values) ? $values : func_get_args(); + } + + /** + * Convert the rule to a validation string. + */ + public function __toString(): string + { + $values = array_map(function ($value) { + $value = enum_value($value); + + return '"' . str_replace('"', '""', (string) $value) . '"'; + }, $this->values); + + return 'doesnt_contain:' . implode(',', $values); + } +} diff --git a/tests/Validation/ValidationContainsRuleTest.php b/tests/Validation/ValidationContainsRuleTest.php new file mode 100644 index 000000000..4a21e3ae5 --- /dev/null +++ b/tests/Validation/ValidationContainsRuleTest.php @@ -0,0 +1,98 @@ +assertSame('contains:"foo","bar"', (string) $rule); + + $rule = new Contains(collect(['foo', 'bar'])); + + $this->assertSame('contains:"foo","bar"', (string) $rule); + + $rule = new Contains(['value with "quotes"']); + + $this->assertSame('contains:"value with ""quotes"""', (string) $rule); + + $rule = Rule::contains(['foo', 'bar']); + + $this->assertSame('contains:"foo","bar"', (string) $rule); + + $rule = Rule::contains(collect([1, 2, 3])); + + $this->assertSame('contains:"1","2","3"', (string) $rule); + + $rule = Rule::contains(new Values()); + + $this->assertSame('contains:"1","2","3","4"', (string) $rule); + + $rule = Rule::contains('foo', 'bar', 'baz'); + + $this->assertSame('contains:"foo","bar","baz"', (string) $rule); + + $rule = new Contains('foo', 'bar', 'baz'); + + $this->assertSame('contains:"foo","bar","baz"', (string) $rule); + + $rule = Rule::contains([StringStatus::done]); + + $this->assertSame('contains:"done"', (string) $rule); + + $rule = Rule::contains([IntegerStatus::done]); + + $this->assertSame('contains:"2"', (string) $rule); + + $rule = Rule::contains([PureEnum::one]); + + $this->assertSame('contains:"one"', (string) $rule); + } + + public function testContainsRuleValidation() + { + $trans = new Translator(new ArrayLoader(), 'en'); + + // Array contains the required value + $v = new Validator($trans, ['x' => ['foo', 'bar', 'baz']], ['x' => Rule::contains('foo')]); + $this->assertTrue($v->passes()); + + // Array contains multiple required values + $v = new Validator($trans, ['x' => ['foo', 'bar', 'baz']], ['x' => Rule::contains('foo', 'bar')]); + $this->assertTrue($v->passes()); + + // Array missing a required value + $v = new Validator($trans, ['x' => ['foo', 'bar']], ['x' => Rule::contains('baz')]); + $this->assertFalse($v->passes()); + + // Array missing one of multiple required values + $v = new Validator($trans, ['x' => ['foo', 'bar']], ['x' => Rule::contains('foo', 'qux')]); + $this->assertFalse($v->passes()); + + // Non-array value fails + $v = new Validator($trans, ['x' => 'foo'], ['x' => Rule::contains('foo')]); + $this->assertFalse($v->passes()); + + // Combined with other rules + $v = new Validator($trans, ['x' => ['foo', 'bar']], ['x' => ['required', 'array', Rule::contains('foo')]]); + $this->assertTrue($v->passes()); + } +} diff --git a/tests/Validation/ValidationDoesntContainRuleTest.php b/tests/Validation/ValidationDoesntContainRuleTest.php new file mode 100644 index 000000000..830f2d0a6 --- /dev/null +++ b/tests/Validation/ValidationDoesntContainRuleTest.php @@ -0,0 +1,98 @@ +assertSame('doesnt_contain:"foo","bar"', (string) $rule); + + $rule = new DoesntContain(collect(['foo', 'bar'])); + + $this->assertSame('doesnt_contain:"foo","bar"', (string) $rule); + + $rule = new DoesntContain(['value with "quotes"']); + + $this->assertSame('doesnt_contain:"value with ""quotes"""', (string) $rule); + + $rule = Rule::doesntContain(['foo', 'bar']); + + $this->assertSame('doesnt_contain:"foo","bar"', (string) $rule); + + $rule = Rule::doesntContain(collect([1, 2, 3])); + + $this->assertSame('doesnt_contain:"1","2","3"', (string) $rule); + + $rule = Rule::doesntContain(new Values()); + + $this->assertSame('doesnt_contain:"1","2","3","4"', (string) $rule); + + $rule = Rule::doesntContain('foo', 'bar', 'baz'); + + $this->assertSame('doesnt_contain:"foo","bar","baz"', (string) $rule); + + $rule = new DoesntContain('foo', 'bar', 'baz'); + + $this->assertSame('doesnt_contain:"foo","bar","baz"', (string) $rule); + + $rule = Rule::doesntContain([StringStatus::done]); + + $this->assertSame('doesnt_contain:"done"', (string) $rule); + + $rule = Rule::doesntContain([IntegerStatus::done]); + + $this->assertSame('doesnt_contain:"2"', (string) $rule); + + $rule = Rule::doesntContain([PureEnum::one]); + + $this->assertSame('doesnt_contain:"one"', (string) $rule); + } + + public function testDoesntContainRuleValidation() + { + $trans = new Translator(new ArrayLoader(), 'en'); + + // Array doesn't contain the forbidden value + $v = new Validator($trans, ['x' => ['foo', 'bar', 'baz']], ['x' => Rule::doesntContain('qux')]); + $this->assertTrue($v->passes()); + + // Array doesn't contain any of the forbidden values + $v = new Validator($trans, ['x' => ['foo', 'bar', 'baz']], ['x' => Rule::doesntContain('qux', 'quux')]); + $this->assertTrue($v->passes()); + + // Array contains a forbidden value + $v = new Validator($trans, ['x' => ['foo', 'bar', 'baz']], ['x' => Rule::doesntContain('foo')]); + $this->assertFalse($v->passes()); + + // Array contains one of the forbidden values + $v = new Validator($trans, ['x' => ['foo', 'bar', 'baz']], ['x' => Rule::doesntContain('qux', 'bar')]); + $this->assertFalse($v->passes()); + + // Non-array value fails + $v = new Validator($trans, ['x' => 'foo'], ['x' => Rule::doesntContain('foo')]); + $this->assertFalse($v->passes()); + + // Combined with other rules + $v = new Validator($trans, ['x' => ['foo', 'bar']], ['x' => ['required', 'array', Rule::doesntContain('baz')]]); + $this->assertTrue($v->passes()); + } +}