Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit b7e1580

Browse files
committed
feat: added Type rule
1 parent 4de3d23 commit b7e1580

File tree

3 files changed

+202
-0
lines changed

3 files changed

+202
-0
lines changed

src/Exception/TypeException.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Exception;
4+
5+
class TypeException extends ValidationException {}

src/Rule/Type.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Rule;
4+
5+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\TypeException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedValueException;
7+
8+
class Type extends AbstractRule implements RuleInterface
9+
{
10+
private const TYPE_FUNCTIONS = [
11+
'bool' => 'is_bool',
12+
'boolean' => 'is_bool',
13+
'int' => 'is_int',
14+
'integer' => 'is_int',
15+
'long' => 'is_int',
16+
'float' => 'is_float',
17+
'double' => 'is_float',
18+
'real' => 'is_float',
19+
'numeric' => 'is_numeric',
20+
'string' => 'is_string',
21+
'scalar' => 'is_scalar',
22+
'array' => 'is_array',
23+
'iterable' => 'is_iterable',
24+
'countable' => 'is_countable',
25+
'callable' => 'is_callable',
26+
'object' => 'is_object',
27+
'resource' => 'is_resource',
28+
'null' => 'is_null',
29+
'alphanumeric' => 'ctype_alnum',
30+
'alpha' => 'ctype_alpha',
31+
'digit' => 'ctype_digit',
32+
'control' => 'ctype_cntrl',
33+
'punctuation' => 'ctype_punct',
34+
'hexadecimal' => 'ctype_xdigit',
35+
'graph' => 'ctype_graph',
36+
'printable' => 'ctype_print',
37+
'whitespace' => 'ctype_space',
38+
'lowercase' => 'ctype_lower',
39+
'uppercase' => 'ctype_upper'
40+
];
41+
42+
public function __construct(
43+
private readonly string|array $constraint,
44+
private readonly string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.'
45+
) {}
46+
47+
public function assert(mixed $value, string $name): void
48+
{
49+
$constraints = (array) $this->constraint;
50+
51+
foreach ($constraints as $constraint) {
52+
if (isset(self::TYPE_FUNCTIONS[$constraint]) && (self::TYPE_FUNCTIONS[$constraint])($value)) {
53+
return;
54+
}
55+
56+
if ($value instanceof $constraint) {
57+
return;
58+
}
59+
60+
if (!isset(self::TYPE_FUNCTIONS[$constraint]) && !class_exists($constraint) && !interface_exists($constraint)) {
61+
throw new UnexpectedValueException(
62+
\sprintf(
63+
'Invalid constraint type "%s". Accepted values are: "%s"',
64+
$constraint,
65+
implode(', ', array_keys(self::TYPE_FUNCTIONS))
66+
)
67+
);
68+
}
69+
}
70+
71+
throw new TypeException(
72+
message: $this->message,
73+
parameters: [
74+
'name' => $name,
75+
'value' => $value,
76+
'constraint' => $this->constraint
77+
]
78+
);
79+
}
80+
}

tests/TypeTest.php

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Test;
4+
5+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\TypeException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Rule\Type;
7+
use ProgrammatorDev\YetAnotherPhpValidator\Test\Util\TestRuleFailureConditionTrait;
8+
use ProgrammatorDev\YetAnotherPhpValidator\Test\Util\TestRuleMessageOptionTrait;
9+
use ProgrammatorDev\YetAnotherPhpValidator\Test\Util\TestRuleSuccessConditionTrait;
10+
use ProgrammatorDev\YetAnotherPhpValidator\Test\Util\TestRuleUnexpectedValueTrait;
11+
12+
class TypeTest extends AbstractTest
13+
{
14+
use TestRuleUnexpectedValueTrait;
15+
use TestRuleFailureConditionTrait;
16+
use TestRuleSuccessConditionTrait;
17+
use TestRuleMessageOptionTrait;
18+
19+
public static function provideRuleUnexpectedValueData(): \Generator
20+
{
21+
$message = '/Invalid constraint type "(.*)". Accepted values are: "(.*)"/';
22+
23+
yield 'invalid type' => [new Type('invalid'), 'string', $message];
24+
}
25+
26+
public static function provideRuleFailureConditionData(): \Generator
27+
{
28+
$exception = TypeException::class;
29+
$message = '/The "(.*)" value should be of type "(.*)", "(.*)" given./';
30+
31+
yield 'bool' => [new Type('bool'), 'invalid', $exception, $message];
32+
yield 'boolean' => [new Type('boolean'), 'invalid', $exception, $message];
33+
yield 'int' => [new Type('int'), 'invalid', $exception, $message];
34+
yield 'integer' => [new Type('integer'), 'invalid', $exception, $message];
35+
yield 'long' => [new Type('long'), 'invalid', $exception, $message];
36+
yield 'float' => [new Type('float'), 'invalid', $exception, $message];
37+
yield 'double' => [new Type('double'), 'invalid', $exception, $message];
38+
yield 'real' => [new Type('real'), 'invalid', $exception, $message];
39+
yield 'numeric' => [new Type('numeric'), 'invalid', $exception, $message];
40+
yield 'string' => [new Type('string'), 123, $exception, $message];
41+
yield 'scalar' => [new Type('scalar'), [], $exception, $message];
42+
yield 'array' => [new Type('array'), 'invalid', $exception, $message];
43+
yield 'iterable' => [new Type('iterable'), 'invalid', $exception, $message];
44+
yield 'countable' => [new Type('countable'), 'invalid', $exception, $message];
45+
yield 'callable' => [new Type('callable'), 'invalid', $exception, $message];
46+
yield 'object' => [new Type('object'), 'invalid', $exception, $message];
47+
yield 'resource' => [new Type('resource'), 'invalid', $exception, $message];
48+
yield 'null' => [new Type('null'), 'invalid', $exception, $message];
49+
yield 'alphanumeric' => [new Type('alphanumeric'), 'foo!#$bar', $exception, $message];
50+
yield 'alpha' => [new Type('alpha'), 'arf12', $exception, $message];
51+
yield 'digit' => [new Type('digit'), 'invalid', $exception, $message];
52+
yield 'control' => [new Type('control'), 'arf12', $exception, $message];
53+
yield 'punctuation' => [new Type('punctuation'), 'ABasdk!@!$#', $exception, $message];
54+
yield 'hexadecimal' => [new Type('hexadecimal'), 'AR1012', $exception, $message];
55+
yield 'graph' => [new Type('graph'), "asdf\n\r\t", $exception, $message];
56+
yield 'printable' => [new Type('printable'), "asdf\n\r\t", $exception, $message];
57+
yield 'whitespace' => [new Type('whitespace'), "\narf12", $exception, $message];
58+
yield 'lowercase' => [new Type('lowercase'), 'Invalid', $exception, $message];
59+
yield 'uppercase' => [new Type('uppercase'), 'invalid', $exception, $message];
60+
61+
yield 'class' => [new Type(\DateTime::class), 'invalid', $exception, $message];
62+
yield 'interface' => [new Type(\DateTimeInterface::class), 'invalid', $exception, $message];
63+
64+
yield 'multiple types' => [new Type(['digit', 'numeric']), 'invalid', $exception, $message];
65+
}
66+
67+
public static function provideRuleSuccessConditionData(): \Generator
68+
{
69+
yield 'bool' => [new Type('bool'), true];
70+
yield 'boolean' => [new Type('boolean'), false];
71+
yield 'int' => [new Type('int'), 1];
72+
yield 'integer' => [new Type('integer'), 2];
73+
yield 'long' => [new Type('long'), 3];
74+
yield 'float' => [new Type('float'), 1.1];
75+
yield 'double' => [new Type('double'), 1.2];
76+
yield 'real' => [new Type('real'), 1.3];
77+
yield 'numeric' => [new Type('numeric'), 123];
78+
yield 'string' => [new Type('string'), 'string'];
79+
yield 'scalar' => [new Type('scalar'), 'string'];
80+
yield 'array' => [new Type('array'), [1, 2, 3]];
81+
yield 'iterable' => [new Type('iterable'), new \ArrayIterator([1, 2, 3])];
82+
yield 'countable' => [new Type('countable'), new \ArrayIterator([1, 2, 3])];
83+
yield 'callable' => [new Type('callable'), 'trim'];
84+
yield 'object' => [new Type('object'), new \stdClass()];
85+
yield 'resource' => [new Type('resource'), fopen('php://stdout', 'r')];
86+
yield 'null' => [new Type('null'), null];
87+
yield 'alphanumeric' => [new Type('alphanumeric'), 'abc123'];
88+
yield 'alpha' => [new Type('alpha'), 'abc'];
89+
yield 'digit' => [new Type('digit'), '123'];
90+
yield 'control' => [new Type('control'), "\n\r\t"];
91+
yield 'punctuation' => [new Type('punctuation'), '*&$()'];
92+
yield 'hexadecimal' => [new Type('hexadecimal'), 'AB10BC99'];
93+
yield 'graph' => [new Type('graph'), 'LKA#@%.54'];
94+
yield 'printable' => [new Type('printable'), 'LKA#@%.54'];
95+
yield 'whitespace' => [new Type('whitespace'), "\n\r\t"];
96+
yield 'lowercase' => [new Type('lowercase'), 'string'];
97+
yield 'uppercase' => [new Type('uppercase'), 'STRING'];
98+
99+
yield 'class' => [new Type(\DateTime::class), new \DateTime()];
100+
yield 'interface' => [new Type(\DateTimeInterface::class), new \DateTime()];
101+
102+
yield 'multiple types' => [new Type(['alpha', 'numeric']), '123'];
103+
}
104+
105+
public static function provideRuleMessageOptionData(): \Generator
106+
{
107+
yield 'message' => [
108+
new Type(
109+
constraint: 'int',
110+
message: 'The "{{ name }}" value is not of type "{{ constraint }}".'
111+
),
112+
'string',
113+
'The "test" value is not of type "int".'
114+
];
115+
}
116+
117+
}

0 commit comments

Comments
 (0)