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

Commit 6811ce0

Browse files
authored
Merge pull request #46 from programmatordev/YAPV-19-create-email-rule
Create Email rule
2 parents 9209213 + 71ca5c4 commit 6811ce0

21 files changed

+281
-43
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
/vendor/
66
/logs/
77
/.idea
8+
/.ddev
89
/index.php

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
],
1414
"require": {
1515
"php": ">=8.1",
16+
"egulias/email-validator": "^4.0",
1617
"symfony/intl": "^6.3",
1718
"symfony/polyfill-ctype": "^1.27"
1819
},

docs/03-rules.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Rules
22

33
- [Basic Rules](#basic-rules)
4+
- [String Rules](#string-rules)
45
- [Comparison Rules](#comparison-rules)
56
- [Date Rules](#date-rules)
67
- [Choice Rules](#choice-rules)
@@ -11,6 +12,10 @@
1112
- [NotBlank](03x-rules-not-blank.md)
1213
- [Type](03x-rules-type.md)
1314

15+
## String Rules
16+
17+
- [Email](03x-rules-email.md)
18+
1419
## Comparison Rules
1520

1621
- [GreaterThan](03x-rules-greater-than.md)

docs/03x-rules-choice.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ For example, if `maxConstraint` is 2, the input array must have at most 2 values
7878

7979
### `message`
8080

81-
type `string` default: `The {{ name }} value is not a valid choice, {{ value }} given. Accepted values are: {{ constraints }}.`
81+
type: `string` default: `The {{ name }} value is not a valid choice, {{ value }} given. Accepted values are: {{ constraints }}.`
8282

8383
Message that will be shown if input value is not a valid choice.
8484

@@ -92,7 +92,7 @@ The following parameters are available:
9292

9393
### `multipleMessage`
9494

95-
type `string` default: `The {{ name }} value has one or more invalid choices, {{ value }} given. Accepted values are: {{ constraints }}.`
95+
type: `string` default: `The {{ name }} value has one or more invalid choices, {{ value }} given. Accepted values are: {{ constraints }}.`
9696

9797
Message that will be shown when `multiple` is `true` and at least one of the input array values is not a valid choice.
9898

@@ -106,7 +106,7 @@ The following parameters are available:
106106

107107
### `minMessage`
108108

109-
type `string` default: `The {{ name }} value must have at least {{ minConstraint }} choices, {{ numValues }} choices given.`
109+
type: `string` default: `The {{ name }} value must have at least {{ minConstraint }} choices, {{ numValues }} choices given.`
110110

111111
Message that will be shown when `multiple` is `true` and input array has fewer values than the defined in `minConstraint`.
112112

@@ -123,7 +123,7 @@ The following parameters are available:
123123

124124
### `maxMessage`
125125

126-
type `string` default: `The {{ name }} value must have at most {{ maxConstraint }} choices, {{ numValues }} choices given.`
126+
type: `string` default: `The {{ name }} value must have at most {{ maxConstraint }} choices, {{ numValues }} choices given.`
127127

128128
Message that will be shown when `multiple` is `true` and input array has more values than the defined in `maxConstraint`.
129129

docs/03x-rules-email.md

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Email
2+
3+
Validates that a value is a valid email address.
4+
5+
```php
6+
Email(
7+
string $mode = 'html5',
8+
?callable $normalizer = null,
9+
string $message = 'The {{ name }} value is not a valid email address, {{ value }} given.'
10+
);
11+
```
12+
13+
## Basic Usage
14+
15+
```php
16+
// html5 mode (default)
17+
Validator::email()->validate('[email protected]'); // true
18+
Validator::email()->validate('test@example'); // false
19+
20+
// html5-allow-no-tld mode
21+
Validator::email(mode: 'html5-allow-no-tld')->validate('[email protected]'); // true
22+
Validator::email(mode: 'html5-allow-no-tld')->validate('test@example'); // true
23+
```
24+
25+
> **Note**
26+
> An `UnexpectedValueException` will be thrown when a `mode` option is invalid.
27+
28+
## Options
29+
30+
### `mode`
31+
32+
type: `string` default: `html5`
33+
34+
Set this option to define the validation mode.
35+
36+
Available options are:
37+
38+
- `html5` uses the regular expression of an HTML5 email input element, but enforces it to have a TLD extension.
39+
- `html5-allow-no-tld` uses the regular expression of an HTML5 email input element, which allows addresses without a TLD extension.
40+
- `strict` validates an address according to the [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322) specification.
41+
42+
### `normalizer`
43+
44+
type: `callable` default: `null`
45+
46+
Allows to define a `callable` that will be applied to the value before checking if it is valid.
47+
48+
For example, use `trim`, or pass your own function, to ignore whitespace in the beginning or end of an email address:
49+
50+
```php
51+
Validator::email()->validate('[email protected] '); // false
52+
53+
Validator::email(normalizer: 'trim')->validate('[email protected] '); // true
54+
Validator::email(normalizer: fn($value) => trim($value))->validate('[email protected] '); // true
55+
```
56+
57+
### `message`
58+
59+
type `string` default: `The {{ name }} value is not a valid email address, {{ value }} given.`
60+
61+
Message that will be shown if the input value is not a valid email address.
62+
63+
The following parameters are available:
64+
65+
| Parameter | Description |
66+
|---------------|---------------------------|
67+
| `{{ value }}` | The current invalid value |
68+
| `{{ name }}` | Name of the invalid value |
69+
| `{{ mode }}` | Selected validation mode |
70+
71+
## Changelog
72+
73+
- `0.6.0` Created

docs/03x-rules-timezone.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Check the [official country codes](https://en.wikipedia.org/wiki/ISO_3166-1#Curr
7575

7676
### `message`
7777

78-
type `string` default: `The {{ name }} value is not a valid timezone, {{ value }} given.`
78+
type: `string` default: `The {{ name }} value is not a valid timezone, {{ value }} given.`
7979

8080
Message that will be shown if the input value is not a valid timezone.
8181

docs/03x-rules-type.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Available character type constraints:
7777

7878
### `message`
7979

80-
type `string` default: `The {{ name }} value should be of type {{ constraint }}, {{ value }} given.`
80+
type: `string` default: `The {{ name }} value should be of type {{ constraint }}, {{ value }} given.`
8181

8282
Message that will be shown if input value is not of a specific type.
8383

src/ChainedValidatorInterface.php

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ public function eachValue(
3333
string $message = 'At key {{ key }}: {{ message }}'
3434
): ChainedValidatorInterface&Validator;
3535

36+
static function email(
37+
string $mode = 'html5',
38+
?callable $normalizer = null,
39+
string $message = 'The {{ name }} value is not a valid email address, {{ value }} given.'
40+
): ChainedValidatorInterface&Validator;
41+
3642
public function greaterThan(
3743
mixed $constraint,
3844
string $message = 'The {{ name }} value should be greater than {{ constraint }}, {{ value }} given.'

src/Exception/EmailException.php

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Exception;
4+
5+
class EmailException extends ValidationException {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Exception;
4+
5+
class UnexpectedOptionException extends UnexpectedValueException
6+
{
7+
public function __construct(string $name, array $expected, string $given)
8+
{
9+
$message = \sprintf('Invalid %s "%s". Accepted values are: "%s".', $name, $given, \implode('", "', $expected));
10+
11+
parent::__construct($message);
12+
}
13+
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Exception;
4+
5+
class UnexpectedTypeException extends UnexpectedValueException
6+
{
7+
public function __construct(string $expected, string $given)
8+
{
9+
$message = \sprintf('Expected value of type "%s", "%s" given.', $expected, $given);
10+
11+
parent::__construct($message);
12+
}
13+
}

src/Rule/Choice.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace ProgrammatorDev\YetAnotherPhpValidator\Rule;
44

55
use ProgrammatorDev\YetAnotherPhpValidator\Exception\ChoiceException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedTypeException;
67
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedValueException;
78

89
class Choice extends AbstractRule implements RuleInterface
@@ -21,9 +22,7 @@ public function __construct(
2122
public function assert(mixed $value, ?string $name = null): void
2223
{
2324
if ($this->multiple && !\is_array($value)) {
24-
throw new UnexpectedValueException(
25-
\sprintf('Expected value of type "array" when using multiple choices, "%s" given', get_debug_type($value))
26-
);
25+
throw new UnexpectedTypeException('array', get_debug_type($value));
2726
}
2827

2928
if (

src/Rule/Country.php

+4-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
namespace ProgrammatorDev\YetAnotherPhpValidator\Rule;
44

55
use ProgrammatorDev\YetAnotherPhpValidator\Exception\CountryException;
6-
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedValueException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedOptionException;
7+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedTypeException;
78
use Symfony\Component\Intl\Countries;
89

910
class Country extends AbstractRule implements RuleInterface
@@ -24,19 +25,11 @@ public function __construct(
2425
public function assert(mixed $value, ?string $name = null): void
2526
{
2627
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-
);
28+
throw new UnexpectedOptionException('code', self::CODE_OPTIONS, $this->code);
3429
}
3530

3631
if (!\is_string($value)) {
37-
throw new UnexpectedValueException(
38-
\sprintf('Expected value of type "string", "%s" given.', get_debug_type($value))
39-
);
32+
throw new UnexpectedTypeException('string', get_debug_type($value));
4033
}
4134

4235
// Keep original value for parameters

src/Rule/EachKey.php

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace ProgrammatorDev\YetAnotherPhpValidator\Rule;
44

55
use ProgrammatorDev\YetAnotherPhpValidator\Exception\EachKeyException;
6-
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedValueException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedTypeException;
77
use ProgrammatorDev\YetAnotherPhpValidator\Exception\ValidationException;
88
use ProgrammatorDev\YetAnotherPhpValidator\Validator;
99

@@ -17,9 +17,7 @@ public function __construct(
1717
public function assert(mixed $value, ?string $name = null): void
1818
{
1919
if (!\is_iterable($value)) {
20-
throw new UnexpectedValueException(
21-
\sprintf('Expected value of type "array|\Traversable", "%s" given.', get_debug_type($value))
22-
);
20+
throw new UnexpectedTypeException('array|\Traversable', get_debug_type($value));
2321
}
2422

2523
try {

src/Rule/EachValue.php

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace ProgrammatorDev\YetAnotherPhpValidator\Rule;
44

55
use ProgrammatorDev\YetAnotherPhpValidator\Exception\EachValueException;
6-
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedValueException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedTypeException;
77
use ProgrammatorDev\YetAnotherPhpValidator\Exception\ValidationException;
88
use ProgrammatorDev\YetAnotherPhpValidator\Validator;
99

@@ -17,9 +17,7 @@ public function __construct(
1717
public function assert(mixed $value, ?string $name = null): void
1818
{
1919
if (!\is_iterable($value)) {
20-
throw new UnexpectedValueException(
21-
\sprintf('Expected value of type "array|\Traversable", "%s" given.', get_debug_type($value))
22-
);
20+
throw new UnexpectedTypeException('array|\Traversable', get_debug_type($value));
2321
}
2422

2523
try {

src/Rule/Email.php

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Rule;
4+
5+
use Egulias\EmailValidator\EmailValidator;
6+
use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
7+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\EmailException;
8+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedOptionException;
9+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedTypeException;
10+
11+
class Email extends AbstractRule implements RuleInterface
12+
{
13+
public const MODE_HTML5 = 'html5';
14+
public const MODE_HTML5_ALLOW_NO_TLD = 'html5-allow-no-tld';
15+
public const MODE_STRICT = 'strict';
16+
17+
private const EMAIL_MODES = [
18+
self::MODE_HTML5,
19+
self::MODE_HTML5_ALLOW_NO_TLD,
20+
self::MODE_STRICT
21+
];
22+
23+
private const EMAIL_PATTERNS = [
24+
self::MODE_HTML5 => '/^[a-zA-Z0-9.!#$%&\'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/',
25+
self::MODE_HTML5_ALLOW_NO_TLD => '/^[a-zA-Z0-9.!#$%&\'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/'
26+
];
27+
28+
// Using array to bypass unallowed callable type in properties
29+
private array $normalizer;
30+
31+
public function __construct(
32+
private readonly string $mode = self::MODE_HTML5,
33+
?callable $normalizer = null,
34+
private readonly string $message = 'The {{ name }} value is not a valid email address, {{ value }} given.'
35+
)
36+
{
37+
$this->normalizer['callable'] = $normalizer;
38+
}
39+
40+
public function assert(mixed $value, ?string $name = null): void
41+
{
42+
if (!\in_array($this->mode, self::EMAIL_MODES, true)) {
43+
throw new UnexpectedOptionException('mode', self::EMAIL_MODES, $this->mode);
44+
}
45+
46+
if (!\is_string($value)) {
47+
throw new UnexpectedTypeException('string', get_debug_type($value));
48+
}
49+
50+
if ($this->normalizer['callable'] !== null) {
51+
$value = ($this->normalizer['callable'])($value);
52+
}
53+
54+
if ($this->mode === self::MODE_STRICT) {
55+
$emailValidator = new EmailValidator();
56+
57+
if (!$emailValidator->isValid($value, new NoRFCWarningsValidation())) {
58+
throw new EmailException(
59+
message: $this->message,
60+
parameters: [
61+
'value' => $value,
62+
'name' => $name,
63+
'mode' => $this->mode
64+
]
65+
);
66+
}
67+
}
68+
else if (!\preg_match(self::EMAIL_PATTERNS[$this->mode], $value)) {
69+
throw new EmailException(
70+
message: $this->message,
71+
parameters: [
72+
'value' => $value,
73+
'name' => $name,
74+
'mode' => $this->mode
75+
]
76+
);
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)