Skip to content

Commit a7af07e

Browse files
committed
[Validator] Html5 Email Validation
Currently we only support a very loose validation. There is now a standard HTML5 element with matching regex. This will add the ability to set a `mode` on the email validator. The mode will change the validation that is applied to the field as a whole. These modes are: * loose: The pattern from previous Symfony versions (default) * strict: Strictly matching the RFC * html5: The regex used for the HTML5 Element Deprecates the `strict=true` parameter in favour of `mode='strict'`
1 parent 98dae3e commit a7af07e

File tree

5 files changed

+312
-10
lines changed

5 files changed

+312
-10
lines changed

UPGRADE-3.4.md

+47
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,53 @@ Validator
413413
* Not setting the `strict` option of the `Choice` constraint to `true` is
414414
deprecated and will throw an exception in Symfony 4.0.
415415

416+
* The `strict` option of the `Email` constraint is deprecated and will
417+
be removed in Symfony 4.0, use the `mode` option instead.
418+
419+
Before:
420+
421+
```php
422+
use Symfony\Component\Validator\Constraints as Assert;
423+
424+
/**
425+
* @Assert\Email(strict=true)
426+
*/
427+
private $property;
428+
```
429+
430+
After:
431+
432+
```php
433+
use Symfony\Component\Validator\Constraints as Assert;
434+
435+
/**
436+
* @Assert\Email(mode="strict")
437+
*/
438+
private $property;
439+
```
440+
441+
* Calling the `EmailValidator` with a boolean constructor argument is deprecated and will
442+
be removed in Symfony 4.0.
443+
444+
Before:
445+
446+
```php
447+
use Symfony\Component\Validator\Constraints\EmailValidator;
448+
449+
$strictValidator = new EmailValidator(true);
450+
$looseValidator = new EmailValidator(false);
451+
```
452+
453+
After:
454+
455+
```php
456+
use Symfony\Component\Validator\Constraints\Email;
457+
use Symfony\Component\Validator\Constraints\EmailValidator;
458+
459+
$strictValidator = new EmailValidator(Email::VALIDATION_MODE_STRICT);
460+
$looseValidator = new EmailValidator(Email::VALIDATION_MODE_LOOSE);
461+
```
462+
416463
Yaml
417464
----
418465

src/Symfony/Component/Validator/Constraints/Email.php

+32
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
*/
2222
class Email extends Constraint
2323
{
24+
const VALIDATION_MODE_HTML5 = 'html5';
25+
const VALIDATION_MODE_STRICT = 'strict';
26+
const VALIDATION_MODE_LOOSE = 'loose';
27+
2428
const INVALID_FORMAT_ERROR = 'bd79c0ab-ddba-46cc-a703-a7a4b08de310';
2529
const MX_CHECK_FAILED_ERROR = 'bf447c1c-0266-4e10-9c6c-573df282e413';
2630
const HOST_CHECK_FAILED_ERROR = '7da53a8b-56f3-4288-bb3e-ee9ede4ef9a1';
@@ -31,8 +35,36 @@ class Email extends Constraint
3135
self::HOST_CHECK_FAILED_ERROR => 'HOST_CHECK_FAILED_ERROR',
3236
);
3337

38+
/**
39+
* @var string[]
40+
* @internal
41+
*/
42+
public static $validationModes = array(
43+
self::VALIDATION_MODE_HTML5,
44+
self::VALIDATION_MODE_STRICT,
45+
self::VALIDATION_MODE_LOOSE,
46+
);
47+
3448
public $message = 'This value is not a valid email address.';
3549
public $checkMX = false;
3650
public $checkHost = false;
51+
52+
/**
53+
* @deprecated since version 3.4, to be removed in 4.0. Set mode to "strict" instead.
54+
*/
3755
public $strict;
56+
public $mode;
57+
58+
public function __construct($options = null)
59+
{
60+
if (is_array($options) && array_key_exists('strict', $options)) {
61+
@trigger_error(sprintf('The \'strict\' property is deprecated since version 3.4 and will be removed in 4.0. Use \'mode\'=>"%s" instead.', self::VALIDATION_MODE_STRICT), E_USER_DEPRECATED);
62+
}
63+
64+
if (is_array($options) && array_key_exists('mode', $options) && !in_array($options['mode'], self::$validationModes, true)) {
65+
throw new \InvalidArgumentException('The \'mode\' parameter value is not valid.');
66+
}
67+
68+
parent::__construct($options);
69+
}
3870
}

src/Symfony/Component/Validator/Constraints/EmailValidator.php

+49-8
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,40 @@
2424
class EmailValidator extends ConstraintValidator
2525
{
2626
/**
27-
* @var bool
27+
* @internal
2828
*/
29-
private $isStrict;
29+
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])?)+$/';
30+
/**
31+
* @internal
32+
*/
33+
const PATTERN_LOOSE = '/^.+\@\S+\.\S+$/';
34+
35+
private static $emailPatterns = array(
36+
Email::VALIDATION_MODE_LOOSE => self::PATTERN_LOOSE,
37+
Email::VALIDATION_MODE_HTML5 => self::PATTERN_HTML5,
38+
);
39+
40+
/**
41+
* @var string
42+
*/
43+
private $defaultMode;
3044

31-
public function __construct($strict = false)
45+
/**
46+
* @param string $defaultMode
47+
*/
48+
public function __construct($defaultMode = Email::VALIDATION_MODE_LOOSE)
3249
{
33-
$this->isStrict = $strict;
50+
if (is_bool($defaultMode)) {
51+
@trigger_error(sprintf('Calling `new %s(%s)` is deprecated since version 3.4 and will be removed in 4.0, use `new %s("%s")` instead.', self::class, $defaultMode ? 'true' : 'false', self::class, $defaultMode ? Email::VALIDATION_MODE_STRICT : Email::VALIDATION_MODE_LOOSE), E_USER_DEPRECATED);
52+
53+
$defaultMode = $defaultMode ? Email::VALIDATION_MODE_STRICT : Email::VALIDATION_MODE_LOOSE;
54+
}
55+
56+
if (!in_array($defaultMode, Email::$validationModes, true)) {
57+
throw new \InvalidArgumentException('The "defaultMode" parameter value is not valid.');
58+
}
59+
60+
$this->defaultMode = $defaultMode;
3461
}
3562

3663
/**
@@ -52,11 +79,25 @@ public function validate($value, Constraint $constraint)
5279

5380
$value = (string) $value;
5481

55-
if (null === $constraint->strict) {
56-
$constraint->strict = $this->isStrict;
82+
if (null !== $constraint->strict) {
83+
@trigger_error(sprintf('The %s::$strict property is deprecated since version 3.4 and will be removed in 4.0. Use %s::mode="%s" instead.', Email::class, Email::class, Email::VALIDATION_MODE_STRICT), E_USER_DEPRECATED);
84+
85+
if ($constraint->strict) {
86+
$constraint->mode = Email::VALIDATION_MODE_STRICT;
87+
} else {
88+
$constraint->mode = Email::VALIDATION_MODE_LOOSE;
89+
}
90+
}
91+
92+
if (null === $constraint->mode) {
93+
$constraint->mode = $this->defaultMode;
94+
}
95+
96+
if (!in_array($constraint->mode, Email::$validationModes, true)) {
97+
throw new \InvalidArgumentException(sprintf('The %s::$mode parameter value is not valid.', get_class($constraint)));
5798
}
5899

59-
if ($constraint->strict) {
100+
if (Email::VALIDATION_MODE_STRICT === $constraint->mode) {
60101
if (!class_exists('\Egulias\EmailValidator\EmailValidator')) {
61102
throw new RuntimeException('Strict email validation requires egulias/email-validator ~1.2|~2.0');
62103
}
@@ -78,7 +119,7 @@ public function validate($value, Constraint $constraint)
78119

79120
return;
80121
}
81-
} elseif (!preg_match('/^.+\@\S+\.\S+$/', $value)) {
122+
} elseif (!preg_match(self::$emailPatterns[$constraint->mode], $value)) {
82123
$this->context->buildViolation($constraint->message)
83124
->setParameter('{{ value }}', $this->formatValue($value))
84125
->setCode(Email::INVALID_FORMAT_ERROR)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Validator\Constraints\Email;
16+
use Symfony\Component\Validator\Constraints\File;
17+
18+
class EmailTest extends TestCase
19+
{
20+
/**
21+
* @expectedDeprecation The 'strict' property is deprecated since version 3.4 and will be removed in 4.0. Use 'mode'=>"strict" instead.
22+
* @group legacy
23+
*/
24+
public function testLegacyConstructorStrict()
25+
{
26+
$subject = new Email(array('strict' => true));
27+
28+
$this->assertTrue($subject->strict);
29+
}
30+
31+
public function testConstructorStrict()
32+
{
33+
$subject = new Email(array('mode' => Email::VALIDATION_MODE_STRICT));
34+
35+
$this->assertEquals(Email::VALIDATION_MODE_STRICT, $subject->mode);
36+
}
37+
38+
/**
39+
* @expectedException \InvalidArgumentException
40+
* @expectedExceptionMessage The 'mode' parameter value is not valid.
41+
*/
42+
public function testUnknownModesTriggerException()
43+
{
44+
new Email(array('mode' => 'Unknown Mode'));
45+
}
46+
}

src/Symfony/Component/Validator/Tests/Constraints/EmailValidatorTest.php

+138-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,29 @@ class EmailValidatorTest extends ConstraintValidatorTestCase
2323
{
2424
protected function createValidator()
2525
{
26-
return new EmailValidator(false);
26+
return new EmailValidator(Email::VALIDATION_MODE_LOOSE);
27+
}
28+
29+
/**
30+
* @expectedDeprecation Calling `new Symfony\Component\Validator\Constraints\EmailValidator(true)` is deprecated since version 3.4 and will be removed in 4.0, use `new Symfony\Component\Validator\Constraints\EmailValidator("strict")` instead.
31+
* @group legacy
32+
*/
33+
public function testLegacyValidatorConstructorStrict()
34+
{
35+
$this->validator = new EmailValidator(true);
36+
$this->validator->initialize($this->context);
37+
$this->validator->validate('example@localhost', new Email());
38+
39+
$this->assertNoViolation();
40+
}
41+
42+
/**
43+
* @expectedException \InvalidArgumentException
44+
* @expectedExceptionMessage The "defaultMode" parameter value is not valid.
45+
*/
46+
public function testUnknownDefaultModeTriggerException()
47+
{
48+
new EmailValidator('Unknown Mode');
2749
}
2850

2951
public function testNullIsValid()
@@ -64,6 +86,31 @@ public function getValidEmails()
6486
array('fabien@symfony.com'),
6587
array('example@example.co.uk'),
6688
array('fabien_potencier@example.fr'),
89+
array('example@example.co..uk'),
90+
array('{}~!@!@£$%%^&*().!@£$%^&*()'),
91+
array('example@example.co..uk'),
92+
array('example@-example.com'),
93+
array(sprintf('example@%s.com', str_repeat('a', 64))),
94+
);
95+
}
96+
97+
/**
98+
* @dataProvider getValidEmailsHtml5
99+
*/
100+
public function testValidEmailsHtml5($email)
101+
{
102+
$this->validator->validate($email, new Email(array('mode' => Email::VALIDATION_MODE_HTML5)));
103+
104+
$this->assertNoViolation();
105+
}
106+
107+
public function getValidEmailsHtml5()
108+
{
109+
return array(
110+
array('fabien@symfony.com'),
111+
array('example@example.co.uk'),
112+
array('fabien_potencier@example.fr'),
113+
array('{}~!@example.com'),
67114
);
68115
}
69116

@@ -94,6 +141,95 @@ public function getInvalidEmails()
94141
);
95142
}
96143

144+
/**
145+
* @dataProvider getInvalidHtml5Emails
146+
*/
147+
public function testInvalidHtml5Emails($email)
148+
{
149+
$constraint = new Email(
150+
array(
151+
'message' => 'myMessage',
152+
'mode' => Email::VALIDATION_MODE_HTML5,
153+
)
154+
);
155+
156+
$this->validator->validate($email, $constraint);
157+
158+
$this->buildViolation('myMessage')
159+
->setParameter('{{ value }}', '"'.$email.'"')
160+
->setCode(Email::INVALID_FORMAT_ERROR)
161+
->assertRaised();
162+
}
163+
164+
public function getInvalidHtml5Emails()
165+
{
166+
return array(
167+
array('example'),
168+
array('example@'),
169+
array('example@localhost'),
170+
array('example@example.co..uk'),
171+
array('foo@example.com bar'),
172+
array('example@example.'),
173+
array('example@.fr'),
174+
array('@example.com'),
175+
array('example@example.com;example@example.com'),
176+
array('example@.'),
177+
array(' example@example.com'),
178+
array('example@ '),
179+
array(' example@example.com '),
180+
array(' example @example .com '),
181+
array('example@-example.com'),
182+
array(sprintf('example@%s.com', str_repeat('a', 64))),
183+
);
184+
}
185+
186+
public function testModeStrict()
187+
{
188+
$constraint = new Email(array('mode' => Email::VALIDATION_MODE_STRICT));
189+
190+
$this->validator->validate('example@localhost', $constraint);
191+
192+
$this->assertNoViolation();
193+
}
194+
195+
public function testModeHtml5()
196+
{
197+
$constraint = new Email(array('mode' => Email::VALIDATION_MODE_HTML5));
198+
199+
$this->validator->validate('example@example..com', $constraint);
200+
201+
$this->buildViolation('This value is not a valid email address.')
202+
->setParameter('{{ value }}', '"example@example..com"')
203+
->setCode(Email::INVALID_FORMAT_ERROR)
204+
->assertRaised();
205+
}
206+
207+
public function testModeLoose()
208+
{
209+
$constraint = new Email(array('mode' => Email::VALIDATION_MODE_LOOSE));
210+
211+
$this->validator->validate('example@example..com', $constraint);
212+
213+
$this->assertNoViolation();
214+
}
215+
216+
/**
217+
* @expectedException \InvalidArgumentException
218+
* @expectedExceptionMessage The Symfony\Component\Validator\Constraints\Email::$mode parameter value is not valid.
219+
*/
220+
public function testUnknownModesOnValidateTriggerException()
221+
{
222+
$constraint = new Email();
223+
$constraint->mode = 'Unknown Mode';
224+
225+
$this->validator->validate('example@example..com', $constraint);
226+
}
227+
228+
/**
229+
* @expectedDeprecation The 'strict' property is deprecated since version 3.4 and will be removed in 4.0. Use 'mode'=>"strict" instead.
230+
* @expectedDeprecation The Symfony\Component\Validator\Constraints\Email::$strict property is deprecated since version 3.4 and will be removed in 4.0. Use Symfony\Component\Validator\Constraints\Email::mode="strict" instead.
231+
* @group legacy
232+
*/
97233
public function testStrict()
98234
{
99235
$constraint = new Email(array('strict' => true));
@@ -110,7 +246,7 @@ public function testStrictWithInvalidEmails($email)
110246
{
111247
$constraint = new Email(array(
112248
'message' => 'myMessage',
113-
'strict' => true,
249+
'mode' => Email::VALIDATION_MODE_STRICT,
114250
));
115251

116252
$this->validator->validate($email, $constraint);

0 commit comments

Comments
 (0)