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

Commit e69cf72

Browse files
authored
Merge pull request #28 from programmatordev/YAPV-18-create-type-rule
Create Type rule
2 parents 5d4e6c9 + 2ec5740 commit e69cf72

File tree

9 files changed

+304
-2
lines changed

9 files changed

+304
-2
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
}
1313
],
1414
"require": {
15-
"php": ">=8.1"
15+
"php": ">=8.1",
16+
"symfony/polyfill-ctype": "^1.27"
1617
},
1718
"require-dev": {
1819
"phpunit/phpunit": "^10.0",

docs/03-rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
## Basic Rules
99

1010
- [NotBlank](03x-rules-not-blank.md)
11+
- [Type](03x-rules-type.md)
1112

1213
## Comparison Rules
1314

docs/03x-rules-type.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Type
2+
3+
Validates that a value is of a specific type.
4+
5+
If an array with multiple types is provided, it will validate if the value is of at least one of the given types.
6+
For example, if `['alpha', 'numeric']` is provided, it will validate if the value is of type `alpha` or of type `numeric`.
7+
8+
```php
9+
Type(
10+
string|array $constraint,
11+
string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.'
12+
)
13+
```
14+
15+
## Basic Usage
16+
17+
```php
18+
// Single data type
19+
Validator::type('string')->validate('green'); // true
20+
Validator::type('alphanumeric')->validate('gr33n'); // true
21+
22+
// Multiple data types
23+
// Validates if value is of at least one of the provided types
24+
Validator::type(['alpha', 'numeric'])->validate('green'); // true (alpha)
25+
Validator::type(['alpha', 'numeric'])->validate('33'); // true (numeric)
26+
Validator::type(['alpha', 'numeric'])->validate('gr33n'); // false (not alpha nor numeric)
27+
28+
// Class or interface type
29+
Validator::type(\DateTime::class)->validate(new \DateTime()); // true
30+
Validator::type(\DateTimeInterface::class)->validate(new \DateTime()); // true
31+
```
32+
33+
> **Note**
34+
> An `UnexpectedValueException` will be thrown when a constraint type, class and interface is invalid.
35+
36+
## Options
37+
38+
### `constraint`
39+
40+
type: `string`|`array` `required`
41+
42+
Type(s) to validate the input value type.
43+
Can validate instances of classes and interfaces.
44+
45+
If an array with multiple types is provided, it will validate if the value is of at least one of the given types.
46+
For example, if `['alpha', 'numeric']` is provided, it will validate if the value is of type `alpha` or of type `numeric`.
47+
48+
Available constraint types:
49+
50+
- [`bool`](https://www.php.net/manual/en/function.is-bool.php), [`boolean`](https://www.php.net/manual/en/function.is-bool.php)
51+
- [`int`](https://www.php.net/manual/en/function.is-int.php), [`integer`](https://www.php.net/manual/en/function.is-int.php), [`long`](https://www.php.net/manual/en/function.is-int.php)
52+
- [`float`](https://www.php.net/manual/en/function.is-float.php), [`double`](https://www.php.net/manual/en/function.is-float.php), [`real`](https://www.php.net/manual/en/function.is-float.php)
53+
- [`numeric`](https://www.php.net/manual/en/function.is-numeric.php)
54+
- [`string`](https://www.php.net/manual/en/function.is-string.php)
55+
- [`scalar`](https://www.php.net/manual/en/function.is-scalar.php)
56+
- [`array`](https://www.php.net/manual/en/function.is-array.php)
57+
- [`iterable`](https://www.php.net/manual/en/function.is-iterable.php)
58+
- [`countable`](https://www.php.net/manual/en/function.is-countable.php)
59+
- [`callable`](https://www.php.net/manual/en/function.is-callable.php)
60+
- [`object`](https://www.php.net/manual/en/function.is-object.php)
61+
- [`resource`](https://www.php.net/manual/en/function.is-resource.php)
62+
- [`null`](https://www.php.net/manual/en/function.is-null.php)
63+
- [`alphanumeric`](https://www.php.net/manual/en/function.ctype-alnum)
64+
- [`alpha`](https://www.php.net/manual/en/function.ctype-alpha.php)
65+
- [`digit`](https://www.php.net/manual/en/function.ctype-digit.php)
66+
- [`control`](https://www.php.net/manual/en/function.ctype-cntrl.php)
67+
- [`punctuation`](https://www.php.net/manual/en/function.ctype-punct.php)
68+
- [`hexadecimal`](https://www.php.net/manual/en/function.ctype-xdigit.php)
69+
- [`graph`](https://www.php.net/manual/en/function.ctype-graph.php)
70+
- [`printable`](https://www.php.net/manual/en/function.ctype-print.php)
71+
- [`whitespace`](https://www.php.net/manual/en/function.ctype-space.php)
72+
- [`lowercase`](https://www.php.net/manual/en/function.ctype-lower.php)
73+
- [`uppercase`](https://www.php.net/manual/en/function.ctype-upper.php)
74+
75+
### `message`
76+
77+
type `string` default: `The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.`
78+
79+
Message that will be shown if input value is not of a specific type.
80+
81+
The following parameters are available:
82+
83+
| Parameter | Description |
84+
|--------------------|---------------------------|
85+
| `{{ value }}` | The current invalid value |
86+
| `{{ name }}` | Name of the invalid value |
87+
| `{{ constraint }}` | The valid type(s) |

src/ChainedValidatorInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,9 @@ public function range(
6464
): ChainedValidatorInterface;
6565

6666
public function rule(RuleInterface $constraint): ChainedValidatorInterface;
67+
68+
public function type(
69+
string|array $constraint,
70+
string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.'
71+
): ChainedValidatorInterface;
6772
}

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/Exception/ValidationException.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ private function formatValue(mixed $value): string
3838
}
3939

4040
if (\is_string($value)) {
41-
return $value;
41+
// Replace line breaks and tabs with single space
42+
return str_replace(["\n", "\r", "\t", "\v", "\x00"], ' ', $value);
4243
}
4344

4445
if (\is_resource($value)) {

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+
}

src/StaticValidatorInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,9 @@ public static function range(
5454
): ChainedValidatorInterface;
5555

5656
public static function rule(RuleInterface $constraint): ChainedValidatorInterface;
57+
58+
public static function type(
59+
string|array $constraint,
60+
string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.'
61+
): ChainedValidatorInterface;
5762
}

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)