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

Commit 3ae68bf

Browse files
authored
Merge pull request #36 from programmatordev/1.x
1.x
2 parents 6b8c69e + 5acd74b commit 3ae68bf

File tree

10 files changed

+223
-6
lines changed

10 files changed

+223
-6
lines changed

docs/03-rules.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- [Basic Rules](#basic-rules)
44
- [Comparison Rules](#comparison-rules)
5+
- [Date Rules](#date-rules)
56
- [Choice Rules](#choice-rules)
67
- [Other Rules](#other-rules)
78

@@ -18,6 +19,10 @@
1819
- [LessThanOrEqual](03x-rules-less-than-or-equal.md)
1920
- [Range](03x-rules-range.md)
2021

22+
## Date Rules
23+
24+
- [Timezone](03x-rules-timezone.md)
25+
2126
## Choice Rules
2227

2328
- [Choice](03x-rules-choice.md)

docs/03x-rules-country.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Validates that a value is a valid country code.
55
```php
66
Country(
77
string $code = 'alpha-2',
8-
string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.'
8+
string $message = 'The "{{ name }}" value is not a valid "{{ code }}" country code, "{{ value }}" given.'
99
);
1010
```
1111

@@ -42,7 +42,7 @@ Available options:
4242

4343
### `message`
4444

45-
type: `string` default: `The "{{ name }}" value is not a valid country code, "{{ value }}" given.`
45+
type: `string` default: `The "{{ name }}" value is not a valid "{{ code }}" country code, "{{ value }}" given.`
4646

4747
Message that will be shown if the input value is not a valid country code.
4848

docs/03x-rules-timezone.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Timezone
2+
3+
Validates that a value is a valid timezone identifier.
4+
5+
```php
6+
Timezone(
7+
string $timezoneGroup = \DateTimeZone::ALL,
8+
?string $countryCode = null,
9+
string $message = 'The "{{ name }}" value is not a valid timezone, "{{ value }}" given.'
10+
);
11+
```
12+
13+
## Basic Usage
14+
15+
```php
16+
// All timezone identifiers
17+
Validator::timezone()->validate('Europe/Lisbon'); // true
18+
19+
// Restrict timezone identifiers to a specific geographical zone
20+
Validator::timezone(timezoneGroup: \DateTimeZone::EUROPE)->validate('Europe/Lisbon'); // true
21+
Validator::timezone(timezoneGroup: \DateTimeZone::AFRICA)->validate('Europe/Lisbon'); // false
22+
// Or multiple geographical zones
23+
Validator::timezone(timezoneGroup: \DateTimeZone::AFRICA | \DateTimeZone::EUROPE)->validate('Europe/Lisbon'); // true
24+
25+
// Restrict timezone identifiers to a specific country
26+
Validator::timezone(timezoneGroup: \DateTimeZone::PER_COUNTRY, countryCode: 'pt')->validate('Europe/Lisbon'); // true
27+
Validator::timezone(timezoneGroup: \DateTimeZone::PER_COUNTRY, countryCode: 'en')->validate('Europe/Lisbon'); // false
28+
```
29+
30+
> **Note**
31+
> An `UnexpectedValueException` will be thrown when the `timezoneGroup` value is `\DateTimeZone::PER_COUNTRY`
32+
> and the `countryCode` value is `null` (not provided).
33+
34+
> **Note**
35+
> An `UnexpectedValueException` will be thrown when the `countryCode` value is not valid.
36+
> Only if the `timezoneGroup` value is `\DateTimeZone::PER_COUNTRY`, otherwise it is ignored.
37+
38+
## Options
39+
40+
### `timezoneGroup`
41+
42+
type: `int` default: `\DateTimeZone::ALL`
43+
44+
Set this option to restrict timezone identifiers to a specific geographical zone.
45+
46+
Available timezone groups:
47+
48+
- `\DateTimeZone::AFRICA`
49+
- `\DateTimeZone::AMERICA`
50+
- `\DateTimeZone::ANTARCTICA`
51+
- `\DateTimeZone::ARCTIC`
52+
- `\DateTimeZone::ASIA`
53+
- `\DateTimeZone::ATLANTIC`
54+
- `\DateTimeZone::AUSTRALIA`
55+
- `\DateTimeZone::EUROPE`
56+
- `\DateTimeZone::INDIAN`
57+
- `\DateTimeZone::PACIFIC`
58+
59+
In addition, there are special timezone groups:
60+
61+
- `\DateTimeZone::ALL` all timezones;
62+
- `\DateTimeZone::ALL_WITH_BC` all timezones including deprecated timezones;
63+
- `\DateTimeZone::PER_COUNTRY` timezones per country (must be used together with the [`countryCode`](#countrycode) option);
64+
- `\DateTimeZone::UTC` UTC timezones.
65+
66+
### `countryCode`
67+
68+
type: `?string` default: `null`
69+
70+
If the `timezoneGroup` option value is `\DateTimeZone::PER_COUNTRY`,
71+
this option is required to restrict valid timezone identifiers to the ones that belong to the given country.
72+
73+
Must be a valid alpha-2 country code.
74+
Check the [official country codes](https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes) list for more information.
75+
76+
### `message`
77+
78+
type `string` default: `The "{{ name }}" value is not a valid timezone, "{{ value }}" given.`
79+
80+
Message that will be shown if the input value is not a valid timezone.
81+
82+
The following parameters are available:
83+
84+
| Parameter | Description |
85+
|---------------------|---------------------------|
86+
| `{{ value }}` | The current invalid value |
87+
| `{{ name }}` | Name of the invalid value |
88+
| `{{ countryCode }}` | Selected country code |

src/ChainedValidatorInterface.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function choice(
3434

3535
public function country(
3636
string $code = 'alpha-2',
37-
string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.'
37+
string $message = 'The "{{ name }}" value is not a valid "{{ code }}" country code, "{{ value }}" given.'
3838
): ChainedValidatorInterface;
3939

4040
public function greaterThan(
@@ -70,6 +70,12 @@ public function range(
7070

7171
public function rule(RuleInterface $constraint): ChainedValidatorInterface;
7272

73+
public function timezone(
74+
int $timezoneGroup = \DateTimeZone::ALL,
75+
?string $countryCode = null,
76+
string $message = 'The "{{ name }}" value is not a valid timezone, "{{ value }}" given.'
77+
): ChainedValidatorInterface;
78+
7379
public function type(
7480
string|array $constraint,
7581
string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.'
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 TimezoneException extends ValidationException {}

src/Rule/Country.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Country extends AbstractRule implements RuleInterface
1818

1919
public function __construct(
2020
private readonly string $code = self::ALPHA_2_CODE,
21-
private readonly string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.'
21+
private readonly string $message = 'The "{{ name }}" value is not a valid "{{ code }}" country code, "{{ value }}" given.'
2222
) {}
2323

2424
public function assert(mixed $value, string $name): void

src/Rule/Timezone.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Rule;
4+
5+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\TimezoneException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedValueException;
7+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\ValidationException;
8+
use ProgrammatorDev\YetAnotherPhpValidator\Validator;
9+
10+
class Timezone extends AbstractRule implements RuleInterface
11+
{
12+
public function __construct(
13+
private readonly int $timezoneGroup = \DateTimeZone::ALL,
14+
private ?string $countryCode = null,
15+
private readonly string $message = 'The "{{ name }}" value is not a valid timezone, "{{ value }}" given.'
16+
) {}
17+
18+
public function assert(mixed $value, string $name): void
19+
{
20+
if ($this->timezoneGroup === \DateTimeZone::PER_COUNTRY) {
21+
// Country code is required when using PER_COUNTRY timezone group
22+
if ($this->countryCode === null) {
23+
throw new UnexpectedValueException(
24+
'A country code is required when timezone group is \DateTimeZone::PER_COUNTRY.'
25+
);
26+
}
27+
28+
// Normalize country code
29+
$this->countryCode = strtoupper($this->countryCode);
30+
31+
try {
32+
Validator::country()->assert($this->countryCode, 'countryCode');
33+
}
34+
catch (ValidationException $exception) {
35+
throw new UnexpectedValueException($exception->getMessage());
36+
}
37+
}
38+
39+
if (!\in_array($value, \DateTimeZone::listIdentifiers($this->timezoneGroup, $this->countryCode), true)) {
40+
throw new TimezoneException(
41+
message: $this->message,
42+
parameters: [
43+
'value' => $value,
44+
'name' => $name,
45+
'countryCode' => $this->countryCode
46+
]
47+
);
48+
}
49+
}
50+
}

src/StaticValidatorInterface.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static function choice(
2424

2525
public static function country(
2626
string $code = 'alpha-2',
27-
string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.'
27+
string $message = 'The "{{ name }}" value is not a valid "{{ code }}" country code, "{{ value }}" given.'
2828
): ChainedValidatorInterface;
2929

3030
public static function greaterThan(
@@ -60,6 +60,12 @@ public static function range(
6060

6161
public static function rule(RuleInterface $constraint): ChainedValidatorInterface;
6262

63+
public static function timezone(
64+
int $timezoneGroup = \DateTimeZone::ALL,
65+
?string $countryCode = null,
66+
string $message = 'The "{{ name }}" value is not a valid timezone, "{{ value }}" given.'
67+
): ChainedValidatorInterface;
68+
6369
public static function type(
6470
string|array $constraint,
6571
string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.'

tests/CountryTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public static function provideRuleUnexpectedValueData(): \Generator
2828
public static function provideRuleFailureConditionData(): \Generator
2929
{
3030
$exception = CountryException::class;
31-
$message = '/The "(.*)" value is not a valid country code, "(.*)" given./';
31+
$message = '/The "(.*)" value is not a valid "(.*)" country code, "(.*)" given./';
3232

3333
yield 'default' => [new Country(), 'PRT', $exception, $message];
3434
yield 'alpha2' => [new Country(code: 'alpha-2'), 'PRT', $exception, $message];

tests/TimezoneTest.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\TimezoneException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Rule\Timezone;
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 TimezoneTest extends AbstractTest
13+
{
14+
use TestRuleUnexpectedValueTrait;
15+
use TestRuleFailureConditionTrait;
16+
use TestRuleSuccessConditionTrait;
17+
use TestRuleMessageOptionTrait;
18+
19+
public static function provideRuleUnexpectedValueData(): \Generator
20+
{
21+
$missingCountryCodeMessage = '/A country code is required when timezone group is \\\DateTimeZone::PER_COUNTRY./';
22+
$invalidCountryCodeMessage = '/The "(.*)" value is not a valid "(.*)" country code, "(.*)" given./';
23+
24+
yield 'missing country code' => [new Timezone(\DateTimeZone::PER_COUNTRY), 'Europe/Lisbon', $missingCountryCodeMessage];
25+
yield 'invalid country code' => [new Timezone(\DateTimeZone::PER_COUNTRY, 'PRT'), 'Europe/Lisbon', $invalidCountryCodeMessage];
26+
}
27+
28+
public static function provideRuleFailureConditionData(): \Generator
29+
{
30+
$exception = TimezoneException::class;
31+
$message = '/The "(.*)" value is not a valid timezone, "(.*)" given./';
32+
33+
yield 'invalid timezone' => [new Timezone(), 'Invalid/Timezone', $exception, $message];
34+
yield 'not of timezone group' => [new Timezone(\DateTimeZone::AFRICA), 'Europe/Lisbon', $exception, $message];
35+
yield 'not of timezone country' => [new Timezone(\DateTimeZone::PER_COUNTRY, 'es'), 'Europe/Lisbon', $exception, $message];
36+
}
37+
38+
public static function provideRuleSuccessConditionData(): \Generator
39+
{
40+
yield 'valid timezone' => [new Timezone(), 'Europe/Lisbon'];
41+
yield 'is of timezone group' => [new Timezone(\DateTimeZone::EUROPE), 'Europe/Lisbon'];
42+
yield 'is of multiple timezone groups' => [new Timezone(\DateTimeZone::EUROPE | \DateTimeZone::UTC), 'UTC'];
43+
yield 'is of timezone country' => [new Timezone(\DateTimeZone::PER_COUNTRY, 'pt'), 'Europe/Lisbon'];
44+
}
45+
46+
public static function provideRuleMessageOptionData(): \Generator
47+
{
48+
yield 'message' => [
49+
new Timezone(
50+
message: 'The "{{ name }}" value "{{ value }}" is not a valid timezone.'
51+
),
52+
'Invalid/Timezone',
53+
'The "test" value "Invalid/Timezone" is not a valid timezone.'
54+
];
55+
}
56+
57+
}

0 commit comments

Comments
 (0)