From 3dd6b45e891557dfe209a45332da9fd66843de4e Mon Sep 17 00:00:00 2001 From: MohammadAlhallaq Date: Sun, 21 Dec 2025 15:20:08 +0300 Subject: [PATCH 1/5] wip --- packages/validation/src/Rules/Custom.php | 47 +++++++++++++++++++ .../validation/tests/Rules/CustomTest.php | 47 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 packages/validation/src/Rules/Custom.php create mode 100644 packages/validation/tests/Rules/CustomTest.php diff --git a/packages/validation/src/Rules/Custom.php b/packages/validation/src/Rules/Custom.php new file mode 100644 index 000000000..7669dc557 --- /dev/null +++ b/packages/validation/src/Rules/Custom.php @@ -0,0 +1,47 @@ +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/CustomTest.php b/packages/validation/tests/Rules/CustomTest.php new file mode 100644 index 000000000..49352ba25 --- /dev/null +++ b/packages/validation/tests/Rules/CustomTest.php @@ -0,0 +1,47 @@ + 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 Custom(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 Custom(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); + + // Non-static closure should throw exception + new Custom(fn(mixed $value): bool => str_contains((string) $value, '@')); + } +} From 03b89a6efcfbcc1f690420476062badbcfc09832 Mon Sep 17 00:00:00 2001 From: MohammadAlhallaq Date: Sun, 21 Dec 2025 15:24:52 +0300 Subject: [PATCH 2/5] feat(validation): Validation rule based on closure --- packages/validation/src/Rules/Custom.php | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/validation/src/Rules/Custom.php b/packages/validation/src/Rules/Custom.php index 7669dc557..20a8a6549 100644 --- a/packages/validation/src/Rules/Custom.php +++ b/packages/validation/src/Rules/Custom.php @@ -10,7 +10,6 @@ use ReflectionFunction; use Tempest\Validation\Rule; - /** * Custom validation rule defined by a closure. * From a8ef450e9cf85d86a063dc63ad3e80c6abda431a Mon Sep 17 00:00:00 2001 From: MohammadAlhallaq Date: Sun, 21 Dec 2025 15:30:27 +0300 Subject: [PATCH 3/5] feat(validation): validation rules based on closures --- packages/validation/src/Rules/Custom.php | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/validation/src/Rules/Custom.php b/packages/validation/src/Rules/Custom.php index 20a8a6549..7669dc557 100644 --- a/packages/validation/src/Rules/Custom.php +++ b/packages/validation/src/Rules/Custom.php @@ -10,6 +10,7 @@ use ReflectionFunction; use Tempest\Validation\Rule; + /** * Custom validation rule defined by a closure. * From 694e74a5475e826cd019de80de77e0fd68709fb3 Mon Sep 17 00:00:00 2001 From: MohammadAlhallaq Date: Sun, 21 Dec 2025 15:43:40 +0300 Subject: [PATCH 4/5] fix: files formatting --- packages/validation/src/Rules/Custom.php | 6 ++---- packages/validation/tests/Rules/CustomTest.php | 10 ++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/validation/src/Rules/Custom.php b/packages/validation/src/Rules/Custom.php index 7669dc557..759ca45a2 100644 --- a/packages/validation/src/Rules/Custom.php +++ b/packages/validation/src/Rules/Custom.php @@ -4,13 +4,12 @@ namespace Tempest\Validation\Rules; -use Closure; use Attribute; +use Closure; use InvalidArgumentException; use ReflectionFunction; use Tempest\Validation\Rule; - /** * Custom validation rule defined by a closure. * @@ -19,7 +18,6 @@ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final readonly class Custom implements Rule { - private Closure $callback; public function __construct( @@ -30,7 +28,7 @@ public function __construct( $reflection = new ReflectionFunction($callback); // Must be static - if (!$reflection->isStatic()) { + if (! $reflection->isStatic()) { throw new InvalidArgumentException('Validation closures must be static'); } diff --git a/packages/validation/tests/Rules/CustomTest.php b/packages/validation/tests/Rules/CustomTest.php index 49352ba25..0ca6ce480 100644 --- a/packages/validation/tests/Rules/CustomTest.php +++ b/packages/validation/tests/Rules/CustomTest.php @@ -14,15 +14,14 @@ final class CustomTest extends TestCase { public function test_closure_validation_passes(): void { - $rule = new Custom(static fn(mixed $value): bool => str_contains((string) $value, '@')); - + $rule = new Custom(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 Custom(static fn(mixed $value): bool => str_contains((string) $value, '@')); + $rule = new Custom(static fn (mixed $value): bool => str_contains((string) $value, '@')); $this->assertFalse($rule->isValid('username')); $this->assertFalse($rule->isValid('example.com')); @@ -30,7 +29,7 @@ public function test_closure_validation_fails(): void public function test_non_string_value_fails(): void { - $rule = new Custom(static fn(mixed $value): bool => str_contains((string) $value, '@')); + $rule = new Custom(static fn (mixed $value): bool => str_contains((string) $value, '@')); $this->assertFalse($rule->isValid(12345)); $this->assertFalse($rule->isValid(null)); @@ -41,7 +40,6 @@ public function test_static_closure_required(): void { $this->expectException(\InvalidArgumentException::class); - // Non-static closure should throw exception - new Custom(fn(mixed $value): bool => str_contains((string) $value, '@')); + new Custom(fn (mixed $value): bool => str_contains((string) $value, '@')); } } From 30d35bd0b9470b85cab0b094a9d4c7b2b19d58c6 Mon Sep 17 00:00:00 2001 From: MohammadAlhallaq Date: Sun, 21 Dec 2025 16:48:09 +0300 Subject: [PATCH 5/5] fix: more descriptive class name --- .../validation/src/Rules/{Custom.php => Closure.php} | 7 +++---- .../tests/Rules/{CustomTest.php => ClosureTest.php} | 12 ++++++------ 2 files changed, 9 insertions(+), 10 deletions(-) rename packages/validation/src/Rules/{Custom.php => Closure.php} (89%) rename packages/validation/tests/Rules/{CustomTest.php => ClosureTest.php} (65%) diff --git a/packages/validation/src/Rules/Custom.php b/packages/validation/src/Rules/Closure.php similarity index 89% rename from packages/validation/src/Rules/Custom.php rename to packages/validation/src/Rules/Closure.php index 759ca45a2..cd6d60fd0 100644 --- a/packages/validation/src/Rules/Custom.php +++ b/packages/validation/src/Rules/Closure.php @@ -5,7 +5,6 @@ namespace Tempest\Validation\Rules; use Attribute; -use Closure; use InvalidArgumentException; use ReflectionFunction; use Tempest\Validation\Rule; @@ -16,12 +15,12 @@ * The closure receives the value and must return true if it is valid, false otherwise. */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final readonly class Custom implements Rule +final readonly class Closure implements Rule { - private Closure $callback; + private \Closure $callback; public function __construct( - Closure $callback, + \Closure $callback, ) { $this->callback = $callback; diff --git a/packages/validation/tests/Rules/CustomTest.php b/packages/validation/tests/Rules/ClosureTest.php similarity index 65% rename from packages/validation/tests/Rules/CustomTest.php rename to packages/validation/tests/Rules/ClosureTest.php index 0ca6ce480..ea81e9c0e 100644 --- a/packages/validation/tests/Rules/CustomTest.php +++ b/packages/validation/tests/Rules/ClosureTest.php @@ -5,23 +5,23 @@ namespace Tempest\Validation\Tests\Rules; use PHPUnit\Framework\TestCase; -use Tempest\Validation\Rules\Custom; +use Tempest\Validation\Rules\Closure; /** * @internal */ -final class CustomTest extends TestCase +final class ClosureTest extends TestCase { public function test_closure_validation_passes(): void { - $rule = new Custom(static fn (mixed $value): bool => str_contains((string) $value, '@')); + $rule = new Closure(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 Custom(static fn (mixed $value): bool => str_contains((string) $value, '@')); + $rule = new Closure(static fn (mixed $value): bool => str_contains((string) $value, '@')); $this->assertFalse($rule->isValid('username')); $this->assertFalse($rule->isValid('example.com')); @@ -29,7 +29,7 @@ public function test_closure_validation_fails(): void public function test_non_string_value_fails(): void { - $rule = new Custom(static fn (mixed $value): bool => str_contains((string) $value, '@')); + $rule = new Closure(static fn (mixed $value): bool => str_contains((string) $value, '@')); $this->assertFalse($rule->isValid(12345)); $this->assertFalse($rule->isValid(null)); @@ -40,6 +40,6 @@ public function test_static_closure_required(): void { $this->expectException(\InvalidArgumentException::class); - new Custom(fn (mixed $value): bool => str_contains((string) $value, '@')); + new Closure(fn (mixed $value): bool => str_contains((string) $value, '@')); } }