Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/migrating-from-v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,14 @@ _strict typing_.

For more information, refer to [case-sensitiveness.md](case-sensitiveness.md)

##### `Url` now validates domains, IP addresses and domains when appropriate.

- `news` scheme is now unsupported. Consult the validator documentation for
the list of supported schemes.
- `mailto` URLs now are also validated with the `Email` validator.
- Supported schemes now validate using `Ip` and `Domain` when appropriate.


##### New package dependencies

Some validators now require additional packages:
Expand Down
2 changes: 1 addition & 1 deletion docs/validators.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ In this page you will find a list of validators by their category.
[UndefOr]: validators/UndefOr.md "Validates the input using a defined validator when the input is not `null` or an empty string (`''`)."
[Unique]: validators/Unique.md "Validates whether the input array contains only unique values."
[Uppercase]: validators/Uppercase.md "Validates whether the characters in the input are uppercase."
[Url]: validators/Url.md "Validates whether the input is a URL."
[Url]: validators/Url.md "Validates whether the input is a valid URL in a popular internet format."
[Uuid]: validators/Uuid.md "Validates whether the input is a valid UUID. It also supports validation of"
[Version]: validators/Version.md "Validates version numbers using Semantic Versioning."
[Vowel]: validators/Vowel.md "Validates whether the input contains only vowels."
Expand Down
34 changes: 25 additions & 9 deletions docs/validators/Url.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SPDX-License-Identifier: MIT

- `Url()`

Validates whether the input is a URL.
Validates whether the input is a valid URL in a popular internet format.

```php
v::url()->assert('http://example.com');
Expand All @@ -22,18 +22,32 @@ v::url()->assert('ldap://[::1]');
v::url()->assert('mailto:john.doe@example.com');
// Validation passes successfully

v::url()->assert('news:new.example.com');
v::url()->assert('http://example.this_top_level_domain_does_not_exist');
// → "http://example.this_top_level_domain_does_not_exist" must be a valid URL
```

This validator uses [Ip][Ip.md], [Domain][Domain.md] and [Email][Email.md] internally,
activating them depending on the input scheme.

If you want to restrict URLs to a specific scheme, you can use [StartsWith][StartsWith.md]
or any other verifier:

```php
v::startsWith('http')->url()->assert('http://example.com');
// Validation passes successfully

v::startsWith('http')->url()->assert('ftp://example.com');
// → "ftp://example.com" must start with "http"
```

## Templates

### `Url::TEMPLATE_STANDARD`

| Mode | Template |
| ---------: | :---------------------------- |
| `default` | {{subject}} must be a URL |
| `inverted` | {{subject}} must not be a URL |
| Mode | Template |
| ---------: | :---------------------------------- |
| `default` | {{subject}} must be a valid URL |
| `inverted` | {{subject}} must not be a valid URL |

## Template placeholders

Expand All @@ -47,13 +61,15 @@ v::url()->assert('news:new.example.com');

## Changelog

| Version | Description |
| ------: | :---------- |
| 0.8.0 | Created |
| Version | Description |
| ------: | :-------------------------------------------------------------------------- |
| 3.0.0 | Stricter use of `Ip`, `Domain` and `Email` internally. Select schemes only. |
| 0.8.0 | Created |

## See Also

- [Domain](Domain.md)
- [Email](Email.md)
- [Ip](Ip.md)
- [Phone](Phone.md)
- [Slug](Slug.md)
4 changes: 1 addition & 3 deletions src/Validators/Call.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
use Respect\Validation\Result;
use Respect\Validation\Validator;

use function call_user_func;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Call implements Validator
{
Expand All @@ -37,6 +35,6 @@ public function __construct(

public function evaluate(mixed $input): Result
{
return $this->validator->evaluate(call_user_func($this->callable, $input));
return $this->validator->evaluate(($this->callable)($input));
}
}
49 changes: 43 additions & 6 deletions src/Validators/Url.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,58 @@

use Attribute;
use Respect\Validation\Message\Template;
use Respect\Validation\Validators\Core\Simple;
use Respect\Validation\Result;
use Respect\Validation\Validator;

use function filter_var;
use function trim;

use const FILTER_VALIDATE_URL;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Template(
'{{subject}} must be a URL',
'{{subject}} must not be a URL',
'{{subject}} must be a valid URL',
'{{subject}} must not be a valid URL',
)]
final class Url extends Simple
final class Url implements Validator
{
public function isValid(mixed $input): bool
private readonly Validator $validator;

public function __construct()
{
$this->validator = new Call(
'parse_url',
new Circuit(
new ArrayType(),
new OneOf(
new Circuit(
new Key('scheme', new In(['http', 'https', 'ftp', 'telnet', 'gopher', 'ldap'])),
new Key('host', new OneOf(
new Domain(),
new Call([self::class, 'formatIp'], new Ip()),
)),
),
new Circuit(
new Key('scheme', new Equals('mailto')),
new Key('path', new Email()),
),
),
),
);
}

public function evaluate(mixed $input): Result
{
if (!filter_var($input, FILTER_VALIDATE_URL)) {
return Result::failed($input, $this);
}

return Result::of($this->validator->evaluate($input)->hasPassed, $input, $this, []);
}

/** @internal public so it can be accessed by unserialized instance */
public static function formatIp(string $ip): string
{
return filter_var($input, FILTER_VALIDATE_URL) !== false;
return trim($ip, '[]');
}
}
6 changes: 3 additions & 3 deletions tests/feature/Transformers/PrefixTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
test('UndefOr', catchAll(
fn() => v::undefOrUrl()->assert('string'),
fn(string $message, string $fullMessage, array $messages) => expect()
->and($message)->toBe('"string" must be a URL or must be undefined')
->and($fullMessage)->toBe('- "string" must be a URL or must be undefined')
->and($messages)->toBe(['undefOrUrl' => '"string" must be a URL or must be undefined']),
->and($message)->toBe('"string" must be a valid URL or must be undefined')
->and($fullMessage)->toBe('- "string" must be a valid URL or must be undefined')
->and($messages)->toBe(['undefOrUrl' => '"string" must be a valid URL or must be undefined']),
));
8 changes: 4 additions & 4 deletions tests/feature/Validators/UrlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@

test('Scenario #1', catchMessage(
fn() => v::url()->assert('example.com'),
fn(string $message) => expect($message)->toBe('"example.com" must be a URL'),
fn(string $message) => expect($message)->toBe('"example.com" must be a valid URL'),
));

test('Scenario #2', catchMessage(
fn() => v::not(v::url())->assert('http://example.com'),
fn(string $message) => expect($message)->toBe('"http://example.com" must not be a URL'),
fn(string $message) => expect($message)->toBe('"http://example.com" must not be a valid URL'),
));

test('Scenario #3', catchFullMessage(
fn() => v::url()->assert('example.com'),
fn(string $fullMessage) => expect($fullMessage)->toBe('- "example.com" must be a URL'),
fn(string $fullMessage) => expect($fullMessage)->toBe('- "example.com" must be a valid URL'),
));

test('Scenario #4', catchFullMessage(
fn() => v::not(v::url())->assert('http://example.com'),
fn(string $fullMessage) => expect($fullMessage)->toBe('- "http://example.com" must not be a URL'),
fn(string $fullMessage) => expect($fullMessage)->toBe('- "http://example.com" must not be a valid URL'),
));
6 changes: 4 additions & 2 deletions tests/unit/Validators/UrlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ public static function providerForValidInput(): iterable
[$validator, 'ldap://[2001:db8::7]/c=GB?objectClass?one'],
[$validator, 'mailto:John.Doe@example.com'],
[$validator, 'mailto:mduerst@ifi.unizh.example.gov'],
[$validator, 'news:comp.infosystems.www.servers.unix'],
[$validator, 'news:comp.infosystems.www.servers.unix'],
[$validator, 'telnet://192.0.2.16:80/'],
[$validator, 'telnet://melvyl.ucop.example.edu/'],
];
Expand All @@ -48,6 +46,10 @@ public static function providerForInvalidInput(): iterable

return [
[$validator, 'example.com'],
[$validator, 'news:comp.infosystems.www.servers.unix'],
[$validator, 'news:comp.infosystems.www.servers.unix'],
[$validator, 'mailto:not_an_actual_email'],
[$validator, 'https://example.this_tld_is_invalid'],
[$validator, 'http:/example.com/'],
[$validator, 'tel:+1-816-555-1212'],
[$validator, 'urn:oasis:names:specification:docbook:dtd:xml:4.1.2'],
Expand Down