File tree Expand file tree Collapse file tree 2 files changed +94
-0
lines changed
Expand file tree Collapse file tree 2 files changed +94
-0
lines changed Original file line number Diff line number Diff line change 1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace Tempest \Validation \Rules ;
6+
7+ use Closure ;
8+ use Attribute ;
9+ use InvalidArgumentException ;
10+ use ReflectionFunction ;
11+ use Tempest \Validation \Rule ;
12+
13+
14+ /**
15+ * Custom validation rule defined by a closure.
16+ *
17+ * The closure receives the value and must return true if it is valid, false otherwise.
18+ */
19+ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE )]
20+ final readonly class Custom implements Rule
21+ {
22+
23+ private Closure $ callback ;
24+
25+ public function __construct (
26+ Closure $ callback ,
27+ ) {
28+ $ this ->callback = $ callback ;
29+
30+ $ reflection = new ReflectionFunction ($ callback );
31+
32+ // Must be static
33+ if (!$ reflection ->isStatic ()) {
34+ throw new InvalidArgumentException ('Validation closures must be static ' );
35+ }
36+
37+ // Must not capture variables
38+ if ($ reflection ->getStaticVariables () !== []) {
39+ throw new InvalidArgumentException ('Validation closures may not capture variables. ' );
40+ }
41+ }
42+
43+ public function isValid (mixed $ value ): bool
44+ {
45+ return ($ this ->callback )($ value );
46+ }
47+ }
Original file line number Diff line number Diff line change 1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace Tempest \Validation \Tests \Rules ;
6+
7+ use PHPUnit \Framework \TestCase ;
8+ use Tempest \Validation \Rules \Custom ;
9+
10+ /**
11+ * @internal
12+ */
13+ final class CustomTest extends TestCase
14+ {
15+ public function test_closure_validation_passes (): void
16+ {
17+ $ rule = new Custom (static fn (mixed $ value ): bool => str_contains ((string ) $ value , '@ ' ));
18+
19+ $ this ->
assertTrue (
$ rule->
isValid (
'[email protected] ' ));
20+ $ this ->
assertTrue (
$ rule->
isValid (
'[email protected] ' ));
21+ }
22+
23+ public function test_closure_validation_fails (): void
24+ {
25+ $ rule = new Custom (static fn (mixed $ value ): bool => str_contains ((string ) $ value , '@ ' ));
26+
27+ $ this ->assertFalse ($ rule ->isValid ('username ' ));
28+ $ this ->assertFalse ($ rule ->isValid ('example.com ' ));
29+ }
30+
31+ public function test_non_string_value_fails (): void
32+ {
33+ $ rule = new Custom (static fn (mixed $ value ): bool => str_contains ((string ) $ value , '@ ' ));
34+
35+ $ this ->assertFalse ($ rule ->isValid (12345 ));
36+ $ this ->assertFalse ($ rule ->isValid (null ));
37+ $ this ->assertFalse ($ rule ->isValid (false ));
38+ }
39+
40+ public function test_static_closure_required (): void
41+ {
42+ $ this ->expectException (\InvalidArgumentException::class);
43+
44+ // Non-static closure should throw exception
45+ new Custom (fn (mixed $ value ): bool => str_contains ((string ) $ value , '@ ' ));
46+ }
47+ }
You can’t perform that action at this time.
0 commit comments