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

Commit 57a16e9

Browse files
committed
feat: added Email rule
1 parent 1d305f4 commit 57a16e9

File tree

5 files changed

+148
-6
lines changed

5 files changed

+148
-6
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+
"egulias/email-validator": "^4.0",
1617
"symfony/intl": "^6.3",
1718
"symfony/polyfill-ctype": "^1.27"
1819
},

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

src/Rule/Email.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 PATTERN_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])?)+$/';
24+
private const PATTERN_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])?)*$/';
25+
26+
private const EMAIL_PATTERNS = [
27+
self::MODE_HTML5 => self::PATTERN_HTML5,
28+
self::MODE_HTML5_ALLOW_NO_TLD => self::PATTERN_HTML5_ALLOW_NO_TLD
29+
];
30+
31+
// Using array to bypass unallowed callable type in properties
32+
private array $normalizer;
33+
34+
public function __construct(
35+
private readonly string $mode = self::MODE_HTML5,
36+
?callable $normalizer = null,
37+
private readonly string $message = 'The {{ name }} value is not a valid email address, {{ value }} given.'
38+
)
39+
{
40+
$this->normalizer['callable'] = $normalizer;
41+
}
42+
43+
public function assert(mixed $value, ?string $name = null): void
44+
{
45+
if (!\in_array($this->mode, self::EMAIL_MODES, true)) {
46+
throw new UnexpectedOptionException('mode', self::EMAIL_MODES, $this->mode);
47+
}
48+
49+
if (!\is_string($value)) {
50+
throw new UnexpectedTypeException('string', get_debug_type($value));
51+
}
52+
53+
if ($this->normalizer['callable'] !== null) {
54+
$value = ($this->normalizer['callable'])($value);
55+
}
56+
57+
if ($this->mode === self::MODE_STRICT) {
58+
$emailValidator = new EmailValidator();
59+
60+
if (!$emailValidator->isValid($value, new NoRFCWarningsValidation())) {
61+
throw new EmailException(
62+
message: $this->message,
63+
parameters: [
64+
'value' => $value,
65+
'name' => $name,
66+
'mode' => $this->mode
67+
]
68+
);
69+
}
70+
}
71+
else if (!\preg_match(self::EMAIL_PATTERNS[$this->mode], $value)) {
72+
throw new EmailException(
73+
message: $this->message,
74+
parameters: [
75+
'value' => $value,
76+
'name' => $name,
77+
'mode' => $this->mode
78+
]
79+
);
80+
}
81+
}
82+
}

src/Rule/NotBlank.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,12 @@ public function __construct(
2222
*/
2323
public function assert(mixed $value, ?string $name = null): void
2424
{
25-
// Keep original value for parameter
26-
$input = $value;
27-
28-
// Call normalizer if provided
2925
if ($this->normalizer['callable'] !== null) {
30-
$input = ($this->normalizer['callable'])($input);
26+
$value = ($this->normalizer['callable'])($value);
3127
}
3228

3329
// Do not allow null, false, [] and ''
34-
if ($input === false || (empty($input) && $input != '0')) {
30+
if ($value === false || (empty($value) && $value != '0')) {
3531
throw new NotBlankException(
3632
message: $this->message,
3733
parameters: [

tests/EmailTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Test;
4+
5+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\EmailException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Rule\Email;
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 EmailTest extends AbstractTest
13+
{
14+
use TestRuleUnexpectedValueTrait;
15+
use TestRuleFailureConditionTrait;
16+
use TestRuleSuccessConditionTrait;
17+
use TestRuleMessageOptionTrait;
18+
19+
public static function provideRuleUnexpectedValueData(): \Generator
20+
{
21+
$optionMessage = '/Invalid (.*) "(.*)". Accepted values are: "(.*)"./';
22+
$typeMessage = '/Expected value of type "string", "(.*)" given./';
23+
24+
yield 'invalid option' => [new Email('invalid'), 'test@example.com', $optionMessage];
25+
yield 'invalid type' => [new Email(), 1, $typeMessage];
26+
}
27+
28+
public static function provideRuleFailureConditionData(): \Generator
29+
{
30+
$exception = EmailException::class;
31+
$message = '/The (.*) value is not a valid email address, (.*) given./';
32+
33+
yield 'html5' => [new Email('html5'), 'invalid', $exception, $message];
34+
yield 'html5 without tld' => [new Email('html5'), 'test@example', $exception, $message];
35+
yield 'html5-allow-no-tld' => [new Email('html5-allow-no-tld'), 'invalid', $exception, $message];
36+
yield 'strict' => [new Email('strict'), 'invalid', $exception, $message];
37+
}
38+
39+
public static function provideRuleSuccessConditionData(): \Generator
40+
{
41+
yield 'html5' => [new Email('html5'), 'test@example.com'];
42+
yield 'html5-allow-no-tld' => [new Email('html5-allow-no-tld'), 'test@example.com'];
43+
yield 'html5-allow-no-tld without tld' => [new Email('html5-allow-no-tld'), 'test@example'];
44+
yield 'strict' => [new Email('strict'), 'test@example.com'];
45+
yield 'normalizer' => [new Email(normalizer: 'trim'), 'test@example.com '];
46+
}
47+
48+
public static function provideRuleMessageOptionData(): \Generator
49+
{
50+
yield 'message' => [
51+
new Email(
52+
message: 'The {{ name }} value {{ value }} in {{ mode }} mode is not a valid email address.'
53+
),
54+
'invalid',
55+
'The test value "invalid" in "html5" mode is not a valid email address.'
56+
];
57+
}
58+
}

0 commit comments

Comments
 (0)