diff --git a/packages/validation/src/Rules/ValidateWith.php b/packages/validation/src/Rules/ValidateWith.php new file mode 100644 index 000000000..28a1c9ff4 --- /dev/null +++ b/packages/validation/src/Rules/ValidateWith.php @@ -0,0 +1,45 @@ +callback = $callback; + + $reflection = new ReflectionFunction($callback); + + // Must be static + if (! $reflection->isStatic()) { + throw new InvalidArgumentException('Validation closures must be static'); + } + + // Must not capture variables + if ($reflection->getStaticVariables() !== []) { + throw new InvalidArgumentException('Validation closures may not capture variables.'); + } + } + + public function isValid(mixed $value): bool + { + return ($this->callback)($value); + } +} diff --git a/packages/validation/tests/Fixtures/ValidateWithObject.php b/packages/validation/tests/Fixtures/ValidateWithObject.php new file mode 100644 index 000000000..3b21125d9 --- /dev/null +++ b/packages/validation/tests/Fixtures/ValidateWithObject.php @@ -0,0 +1,15 @@ +getAttributes(ValidateWith::class); + + $this->assertCount(1, $attributes); + + $rule = $attributes[0]->newInstance(); + $this->assertTrue($rule->isValid('user@example')); + $this->assertFalse($rule->isValid('invalid-prop')); + } + + public function test_closure_validation_passes(): void + { + $rule = new ValidateWith(static fn (mixed $value): bool => str_contains((string) $value, '@')); + $this->assertTrue($rule->isValid('user@example.com')); + $this->assertTrue($rule->isValid('test@domain.org')); + } + + public function test_closure_validation_fails(): void + { + $rule = new ValidateWith(static fn (mixed $value): bool => str_contains((string) $value, '@')); + + $this->assertFalse($rule->isValid('username')); + $this->assertFalse($rule->isValid('example.com')); + } + + public function test_non_string_value_fails(): void + { + $rule = new ValidateWith(static fn (mixed $value): bool => str_contains((string) $value, '@')); + + $this->assertFalse($rule->isValid(12345)); + $this->assertFalse($rule->isValid(null)); + $this->assertFalse($rule->isValid(false)); + } + + public function test_static_closure_required(): void + { + $this->expectException(\InvalidArgumentException::class); + + new ValidateWith(fn (mixed $value): bool => str_contains((string) $value, '@')); + } +}