diff --git a/packages/validation/src/Rules/Closure.php b/packages/validation/src/Rules/Closure.php new file mode 100644 index 000000000..cd6d60fd0 --- /dev/null +++ b/packages/validation/src/Rules/Closure.php @@ -0,0 +1,44 @@ +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/Rules/ClosureTest.php b/packages/validation/tests/Rules/ClosureTest.php new file mode 100644 index 000000000..ea81e9c0e --- /dev/null +++ b/packages/validation/tests/Rules/ClosureTest.php @@ -0,0 +1,45 @@ + 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 Closure(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 Closure(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 Closure(fn (mixed $value): bool => str_contains((string) $value, '@')); + } +}