Skip to content

Commit 3dd6b45

Browse files
wip
1 parent 1af376a commit 3dd6b45

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
}

0 commit comments

Comments
 (0)