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

Commit 343c7ef

Browse files
authored
Merge pull request #29 from programmatordev/YAPV-44-create-country-rule
Create Country rule
2 parents e69cf72 + 95d1f76 commit 343c7ef

File tree

9 files changed

+254
-44
lines changed

9 files changed

+254
-44
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
],
1414
"require": {
1515
"php": ">=8.1",
16+
"symfony/intl": "^6.3",
1617
"symfony/polyfill-ctype": "^1.27"
1718
},
1819
"require-dev": {

docs/03x-rules-choice.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Validator::choice(['red', 'green', 'blue'], multiple: true, minConstraint: 2, ma
4040
```
4141

4242
> **Note**
43-
> An `UnexpectedValueException` will be thrown when `multiple` is `true` and value to be validated is not an `array`.
43+
> An `UnexpectedValueException` will be thrown when `multiple` is `true` and the input value is not an `array`.
4444
4545
> **Note**
4646
> An `UnexpectedValueException` will be thrown when the `minConstraint` value is greater than or equal to the `maxConstraint` value.

docs/03x-rules-country.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Country
2+
3+
Validates that a value is a valid country code.
4+
5+
```php
6+
Country(
7+
string $code = 'alpha-2',
8+
string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.'
9+
);
10+
```
11+
12+
## Basic Usage
13+
14+
```php
15+
// Default alpha-2 code
16+
Validator::country()->validate('PT'); // true
17+
Validator::country(code: 'alpha-2')->validate('PT'); // true
18+
19+
// Alpha-3 code
20+
Validator::country(code: 'alpha-3')->validate('PRT'); // true
21+
```
22+
23+
> **Note**
24+
> An `UnexpectedValueException` will be thrown when the `code` value is not a valid option.
25+
26+
> **Note**
27+
> An `UnexpectedValueException` will be thrown when the input value is not a `string`.
28+
29+
## Options
30+
31+
### `code`
32+
33+
type: `string` default: `alpha-2`
34+
35+
Set code type to validate the country.
36+
Check the [official country codes](https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes) list for more information.
37+
38+
Available options:
39+
40+
- `alpha-2`: two-letter code
41+
- `alpha-3`: three-letter code
42+
43+
### `message`
44+
45+
type `string` default: `The "{{ name }}" value is not a valid country code, "{{ value }}" given.`
46+
47+
Message that will be shown if the input value is not a valid country code.
48+
49+
The following parameters are available:
50+
51+
| Parameter | Description |
52+
|---------------|---------------------------|
53+
| `{{ value }}` | The current invalid value |
54+
| `{{ name }}` | Name of the invalid value |
55+
| `{{ code }}` | Selected code type |

docs/03x-rules-type.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ For example, if `['alpha', 'numeric']` is provided, it will validate if the valu
99
Type(
1010
string|array $constraint,
1111
string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.'
12-
)
12+
);
1313
```
1414

1515
## Basic Usage
1616

1717
```php
18-
// Single data type
18+
// Single type
1919
Validator::type('string')->validate('green'); // true
2020
Validator::type('alphanumeric')->validate('gr33n'); // true
2121

22-
// Multiple data types
22+
// Multiple types
2323
// Validates if value is of at least one of the provided types
2424
Validator::type(['alpha', 'numeric'])->validate('green'); // true (alpha)
2525
Validator::type(['alpha', 'numeric'])->validate('33'); // true (numeric)
@@ -31,7 +31,7 @@ Validator::type(\DateTimeInterface::class)->validate(new \DateTime()); // true
3131
```
3232

3333
> **Note**
34-
> An `UnexpectedValueException` will be thrown when a constraint type, class and interface is invalid.
34+
> An `UnexpectedValueException` will be thrown when a constraint type, class or interface is invalid.
3535
3636
## Options
3737

@@ -45,7 +45,7 @@ Can validate instances of classes and interfaces.
4545
If an array with multiple types is provided, it will validate if the value is of at least one of the given types.
4646
For example, if `['alpha', 'numeric']` is provided, it will validate if the value is of type `alpha` or of type `numeric`.
4747

48-
Available constraint types:
48+
Available data type constraints:
4949

5050
- [`bool`](https://www.php.net/manual/en/function.is-bool.php), [`boolean`](https://www.php.net/manual/en/function.is-bool.php)
5151
- [`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)
@@ -60,6 +60,9 @@ Available constraint types:
6060
- [`object`](https://www.php.net/manual/en/function.is-object.php)
6161
- [`resource`](https://www.php.net/manual/en/function.is-resource.php)
6262
- [`null`](https://www.php.net/manual/en/function.is-null.php)
63+
64+
Available character type constraints:
65+
6366
- [`alphanumeric`](https://www.php.net/manual/en/function.ctype-alnum)
6467
- [`alpha`](https://www.php.net/manual/en/function.ctype-alpha.php)
6568
- [`digit`](https://www.php.net/manual/en/function.ctype-digit.php)

src/Exception/CountryException.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 CountryException extends ValidationException {}

src/Rule/Country.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Rule;
4+
5+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\CountryException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedValueException;
7+
use Symfony\Component\Intl\Countries;
8+
9+
class Country extends AbstractRule implements RuleInterface
10+
{
11+
public const ALPHA_2_CODE = 'alpha-2';
12+
public const ALPHA_3_CODE = 'alpha-3';
13+
14+
private const CODE_OPTIONS = [
15+
self::ALPHA_2_CODE,
16+
self::ALPHA_3_CODE
17+
];
18+
19+
public function __construct(
20+
private readonly string $code = self::ALPHA_2_CODE,
21+
private readonly string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.'
22+
) {}
23+
24+
public function assert(mixed $value, string $name): void
25+
{
26+
if (!\in_array($this->code, self::CODE_OPTIONS)) {
27+
throw new UnexpectedValueException(
28+
\sprintf(
29+
'Invalid code "%s". Accepted values are: "%s".',
30+
$this->code,
31+
\implode(", ", self::CODE_OPTIONS)
32+
)
33+
);
34+
}
35+
36+
if (!\is_string($value)) {
37+
throw new UnexpectedValueException(
38+
\sprintf('Expected value of type "string", "%s" given.', get_debug_type($value))
39+
);
40+
}
41+
42+
// Keep original value for parameters
43+
$input = strtoupper($value);
44+
45+
if (
46+
($this->code === self::ALPHA_2_CODE && !Countries::exists($input))
47+
|| ($this->code === self::ALPHA_3_CODE && !Countries::alpha3CodeExists($input))
48+
) {
49+
throw new CountryException(
50+
message: $this->message,
51+
parameters: [
52+
'name' => $name,
53+
'value' => $value,
54+
'code' => $this->code
55+
]
56+
);
57+
}
58+
}
59+
}

src/Rule/Type.php

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,66 @@
77

88
class Type extends AbstractRule implements RuleInterface
99
{
10+
public const BOOL = 'bool';
11+
public const BOOLEAN = 'boolean';
12+
public const INT = 'int';
13+
public const INTEGER = 'integer';
14+
public const LONG = 'long';
15+
public const FLOAT = 'float';
16+
public const DOUBLE = 'double';
17+
public const REAL = 'real';
18+
public const NUMERIC = 'numeric';
19+
public const STRING = 'string';
20+
public const SCALAR = 'scalar';
21+
public const ARRAY = 'array';
22+
public const ITERABLE = 'iterable';
23+
public const COUNTABLE = 'countable';
24+
public const CALLABLE = 'callable';
25+
public const OBJECT = 'object';
26+
public const RESOURCE = 'resource';
27+
public const NULL = 'null';
28+
public const ALPHANUMERIC = 'alphanumeric';
29+
public const ALPHA = 'alpha';
30+
public const DIGIT = 'digit';
31+
public const CONTROL = 'control';
32+
public const PUNCTUATION = 'punctuation';
33+
public const HEXADECIMAL = 'hexadecimal';
34+
public const GRAPH = 'graph';
35+
public const PRINTABLE = 'printable';
36+
public const WHITESPACE = 'whitespace';
37+
public const LOWERCASE = 'lowercase';
38+
public const UPPERCASE = 'uppercase';
39+
1040
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'
41+
self::BOOL => 'is_bool',
42+
self::BOOLEAN => 'is_bool',
43+
self::INT => 'is_int',
44+
self::INTEGER => 'is_int',
45+
self::LONG => 'is_int',
46+
self::FLOAT => 'is_float',
47+
self::DOUBLE => 'is_float',
48+
self::REAL => 'is_float',
49+
self::NUMERIC => 'is_numeric',
50+
self::STRING => 'is_string',
51+
self::SCALAR => 'is_scalar',
52+
self::ARRAY => 'is_array',
53+
self::ITERABLE => 'is_iterable',
54+
self::COUNTABLE => 'is_countable',
55+
self::CALLABLE => 'is_callable',
56+
self::OBJECT => 'is_object',
57+
self::RESOURCE => 'is_resource',
58+
self::NULL => 'is_null',
59+
self::ALPHANUMERIC => 'ctype_alnum',
60+
self::ALPHA => 'ctype_alpha',
61+
self::DIGIT => 'ctype_digit',
62+
self::CONTROL => 'ctype_cntrl',
63+
self::PUNCTUATION => 'ctype_punct',
64+
self::HEXADECIMAL => 'ctype_xdigit',
65+
self::GRAPH => 'ctype_graph',
66+
self::PRINTABLE => 'ctype_print',
67+
self::WHITESPACE => 'ctype_space',
68+
self::LOWERCASE => 'ctype_lower',
69+
self::UPPERCASE => 'ctype_upper'
4070
];
4171

4272
public function __construct(
@@ -57,12 +87,12 @@ public function assert(mixed $value, string $name): void
5787
return;
5888
}
5989

60-
if (!isset(self::TYPE_FUNCTIONS[$constraint]) && !class_exists($constraint) && !interface_exists($constraint)) {
90+
if (!isset(self::TYPE_FUNCTIONS[$constraint]) && !\class_exists($constraint) && !\interface_exists($constraint)) {
6191
throw new UnexpectedValueException(
6292
\sprintf(
6393
'Invalid constraint type "%s". Accepted values are: "%s"',
6494
$constraint,
65-
implode(', ', array_keys(self::TYPE_FUNCTIONS))
95+
\implode('", "', \array_keys(self::TYPE_FUNCTIONS))
6696
)
6797
);
6898
}

tests/CountryTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Test;
4+
5+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\CountryException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Rule\Country;
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 CountryTest extends AbstractTest
13+
{
14+
use TestRuleUnexpectedValueTrait;
15+
use TestRuleFailureConditionTrait;
16+
use TestRuleSuccessConditionTrait;
17+
use TestRuleMessageOptionTrait;
18+
19+
public static function provideRuleUnexpectedValueData(): \Generator
20+
{
21+
$codeMessage = '/Invalid code "(.*)". Accepted values are: "(.*)"./';
22+
$typeMessage = '/Expected value of type "string", "(.*)" given./';
23+
24+
yield 'invalid code' => [new Country('invalid'), 'PT', $codeMessage];
25+
yield 'invalid type' => [new Country(), 123, $typeMessage];
26+
}
27+
28+
public static function provideRuleFailureConditionData(): \Generator
29+
{
30+
$exception = CountryException::class;
31+
$message = '/The "(.*)" value is not a valid country code, "(.*)" given./';
32+
33+
yield 'default' => [new Country(), 'PRT', $exception, $message];
34+
yield 'alpha2' => [new Country(code: 'alpha-2'), 'PRT', $exception, $message];
35+
yield 'alpha3' => [new Country(code: 'alpha-3'), 'PT', $exception, $message];
36+
}
37+
38+
public static function provideRuleSuccessConditionData(): \Generator
39+
{
40+
yield 'default' => [new Country(), 'PT'];
41+
yield 'alpha2' => [new Country(code: 'alpha-2'), 'PT'];
42+
yield 'alpha2 lowercase' => [new Country(code: 'alpha-2'), 'pt'];
43+
yield 'alpha3' => [new Country(code: 'alpha-3'), 'PRT'];
44+
yield 'alpha3 lowercase' => [new Country(code: 'alpha-3'), 'prt'];
45+
}
46+
47+
public static function provideRuleMessageOptionData(): \Generator
48+
{
49+
yield 'message' => [
50+
new Country(
51+
message: 'The "{{ name }}" value "{{ value }}" is not a valid "{{ code }}" country code.'
52+
),
53+
'invalid',
54+
'The "test" value "invalid" is not a valid "alpha-2" country code.'
55+
];
56+
}
57+
}

tests/NotBlankTest.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ class NotBlankTest extends AbstractTest
1717
public static function provideRuleFailureConditionData(): \Generator
1818
{
1919
$exception = NotBlankException::class;
20-
$exceptionMessage = '/The "(.*)" value should not be blank, "(.*)" given./';
20+
$message = '/The "(.*)" value should not be blank, "(.*)" given./';
2121

22-
yield 'null' => [new NotBlank(), null, $exception, $exceptionMessage];
23-
yield 'false' => [new NotBlank(), false, $exception, $exceptionMessage];
24-
yield 'blank string' => [new NotBlank(), '', $exception, $exceptionMessage];
25-
yield 'blank array' => [new NotBlank(), [], $exception, $exceptionMessage];
22+
yield 'null' => [new NotBlank(), null, $exception, $message];
23+
yield 'false' => [new NotBlank(), false, $exception, $message];
24+
yield 'blank string' => [new NotBlank(), '', $exception, $message];
25+
yield 'blank array' => [new NotBlank(), [], $exception, $message];
2626

27-
yield 'normalizer whitespace' => [new NotBlank(normalizer: 'trim'), ' ', $exception, $exceptionMessage];
28-
yield 'normalizer whitespace function' => [new NotBlank(normalizer: fn($value) => trim($value)), ' ', $exception, $exceptionMessage];
27+
yield 'normalizer whitespace' => [new NotBlank(normalizer: 'trim'), ' ', $exception, $message];
28+
yield 'normalizer whitespace function' => [new NotBlank(normalizer: fn($value) => trim($value)), ' ', $exception, $message];
2929
}
3030

3131
public static function provideRuleSuccessConditionData(): \Generator

0 commit comments

Comments
 (0)