Skip to content

Commit 4436611

Browse files
committed
feature #54408 [Validator] Add a requireTld option to Url constraint (javiereguiluz)
This PR was squashed before being merged into the 7.1 branch. Discussion ---------- [Validator] Add a `requireTld` option to `Url` constraint | Q | A | ------------- | --- | Branch? | 7.1 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | #50871 | License | MIT This implements the last part of #50871 following the suggestions from Wouter and Christian in symfony/symfony#50871 (comment) Commits ------- d39da0dd00 [Validator] Add a `requireTld` option to `Url` constraint
2 parents f93e9b0 + c0271c8 commit 4436611

File tree

5 files changed

+72
-0
lines changed

5 files changed

+72
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Possibility to use all `Ip` constraint versions for `Cidr` constraint
1212
* Add `list` and `associative_array` types to `Type` constraint
1313
* Add the `Charset` constraint
14+
* Add the `requireTld` option to the `Url` constraint
1415

1516
7.0
1617
---

Constraints/Url.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@
2323
class Url extends Constraint
2424
{
2525
public const INVALID_URL_ERROR = '57c2f299-1154-4870-89bb-ef3b1f5ad229';
26+
public const MISSING_TLD_ERROR = '8a5d387f-0716-46b4-844b-67367faf435a';
2627

2728
protected const ERROR_NAMES = [
2829
self::INVALID_URL_ERROR => 'INVALID_URL_ERROR',
30+
self::MISSING_TLD_ERROR => 'MISSING_TLD_ERROR',
2931
];
3032

3133
public string $message = 'This value is not a valid URL.';
34+
public string $tldMessage = 'This URL does not contain a TLD.';
3235
public array $protocols = ['http', 'https'];
3336
public bool $relativeProtocol = false;
37+
public bool $requireTld = false;
3438
/** @var callable|null */
3539
public $normalizer;
3640

@@ -39,6 +43,7 @@ class Url extends Constraint
3943
* @param string[]|null $protocols The protocols considered to be valid for the URL (e.g. http, https, ftp, etc.) (defaults to ['http', 'https']
4044
* @param bool|null $relativeProtocol Whether to accept URL without the protocol (i.e. //example.com) (defaults to false)
4145
* @param string[]|null $groups
46+
* @param bool|null $requireTld Whether to require the URL to include a top-level domain (defaults to false)
4247
*/
4348
public function __construct(
4449
?array $options = null,
@@ -48,13 +53,15 @@ public function __construct(
4853
?callable $normalizer = null,
4954
?array $groups = null,
5055
mixed $payload = null,
56+
?bool $requireTld = null,
5157
) {
5258
parent::__construct($options, $groups, $payload);
5359

5460
$this->message = $message ?? $this->message;
5561
$this->protocols = $protocols ?? $this->protocols;
5662
$this->relativeProtocol = $relativeProtocol ?? $this->relativeProtocol;
5763
$this->normalizer = $normalizer ?? $this->normalizer;
64+
$this->requireTld = $requireTld ?? $this->requireTld;
5865

5966
if (null !== $this->normalizer && !\is_callable($this->normalizer)) {
6067
throw new InvalidArgumentException(sprintf('The "normalizer" option must be a valid callable ("%s" given).', get_debug_type($this->normalizer)));

Constraints/UrlValidator.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,18 @@ public function validate(mixed $value, Constraint $constraint): void
7979

8080
return;
8181
}
82+
83+
if ($constraint->requireTld) {
84+
$urlHost = parse_url($value, \PHP_URL_HOST);
85+
// the host of URLs with a TLD must include at least a '.' (but it can't be an IP address like '127.0.0.1')
86+
if (!str_contains($urlHost, '.') || filter_var($urlHost, \FILTER_VALIDATE_IP)) {
87+
$this->context->buildViolation($constraint->tldMessage)
88+
->setParameter('{{ value }}', $this->formatValue($value))
89+
->setCode(Url::MISSING_TLD_ERROR)
90+
->addViolation();
91+
92+
return;
93+
}
94+
}
8295
}
8396
}

Tests/Constraints/UrlTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,26 @@ public function testAttributes()
5252
self::assertSame(['http', 'https'], $aConstraint->protocols);
5353
self::assertFalse($aConstraint->relativeProtocol);
5454
self::assertNull($aConstraint->normalizer);
55+
self::assertFalse($aConstraint->requireTld);
5556

5657
[$bConstraint] = $metadata->properties['b']->getConstraints();
5758
self::assertSame(['ftp', 'gopher'], $bConstraint->protocols);
5859
self::assertSame('trim', $bConstraint->normalizer);
5960
self::assertSame('myMessage', $bConstraint->message);
6061
self::assertSame(['Default', 'UrlDummy'], $bConstraint->groups);
62+
self::assertFalse($bConstraint->requireTld);
6163

6264
[$cConstraint] = $metadata->properties['c']->getConstraints();
6365
self::assertTrue($cConstraint->relativeProtocol);
6466
self::assertSame(['my_group'], $cConstraint->groups);
6567
self::assertSame('some attached data', $cConstraint->payload);
68+
self::assertFalse($cConstraint->requireTld);
69+
70+
[$dConstraint] = $metadata->properties['d']->getConstraints();
71+
self::assertSame(['http', 'https'], $aConstraint->protocols);
72+
self::assertFalse($aConstraint->relativeProtocol);
73+
self::assertNull($aConstraint->normalizer);
74+
self::assertTrue($dConstraint->requireTld);
6675
}
6776
}
6877

@@ -76,4 +85,7 @@ class UrlDummy
7685

7786
#[Url(relativeProtocol: true, groups: ['my_group'], payload: 'some attached data')]
7887
private $c;
88+
89+
#[Url(requireTld: true)]
90+
private $d;
7991
}

Tests/Constraints/UrlValidatorTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,45 @@ public static function getValidCustomUrls()
311311
['git://[::1]/'],
312312
];
313313
}
314+
315+
/**
316+
* @dataProvider getUrlsForRequiredTld
317+
*/
318+
public function testRequiredTld(string $url, bool $requireTld, bool $isValid)
319+
{
320+
$constraint = new Url([
321+
'requireTld' => $requireTld,
322+
]);
323+
324+
$this->validator->validate($url, $constraint);
325+
326+
if ($isValid) {
327+
$this->assertNoViolation();
328+
} else {
329+
$this->buildViolation($constraint->tldMessage)
330+
->setParameter('{{ value }}', '"'.$url.'"')
331+
->setCode(Url::MISSING_TLD_ERROR)
332+
->assertRaised();
333+
}
334+
}
335+
336+
public static function getUrlsForRequiredTld(): iterable
337+
{
338+
yield ['https://aaa', true, false];
339+
yield ['https://aaa', false, true];
340+
yield ['https://localhost', true, false];
341+
yield ['https://localhost', false, true];
342+
yield ['http://127.0.0.1', false, true];
343+
yield ['http://127.0.0.1', true, false];
344+
yield ['http://user.pass@local', false, true];
345+
yield ['http://user.pass@local', true, false];
346+
yield ['https://example.com', true, true];
347+
yield ['https://example.com', false, true];
348+
yield ['http://foo/bar.png', false, true];
349+
yield ['http://foo/bar.png', true, false];
350+
yield ['https://example.com.org', true, true];
351+
yield ['https://example.com.org', false, true];
352+
}
314353
}
315354

316355
class EmailProvider

0 commit comments

Comments
 (0)