1+ <?php
2+
3+ namespace ProgrammatorDev \YetAnotherPhpValidator \Rule ;
4+
5+ use ProgrammatorDev \YetAnotherPhpValidator \Exception \UnexpectedTypeException ;
6+ use ProgrammatorDev \YetAnotherPhpValidator \Exception \UrlException ;
7+
8+ class Url extends AbstractRule implements RuleInterface
9+ {
10+ // https://github.com/symfony/validator/blob/7.0/Constraints/UrlValidator.php
11+ private const PATTERN = '~^
12+ (%s):// # protocol
13+ (((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+)@)? # basic auth
14+ (
15+ (?:
16+ (?:xn--[a-z0-9-]++\.)*+xn--[a-z0-9-]++ # a domain name using punycode
17+ |
18+ (?:[\pL\pN\pS\pM\-\_]++\.)+[\pL\pN\pM]++ # a multi-level domain name
19+ |
20+ [a-z0-9\-\_]++ # a single-level domain name
21+ )\.?
22+ | # or
23+ \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address
24+ | # or
25+ \[
26+ (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))
27+ \] # an IPv6 address
28+ )
29+ (:[0-9]+)? # a port (optional)
30+ (?:/ (?:[\pL\pN\-._\~!$& \'()*+,;=:@]|%%[0-9A-Fa-f]{2})* )* # a path
31+ (?:\? (?:[\pL\pN\-._\~!$& \'\[\]()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a query (optional)
32+ (?:\# (?:[\pL\pN\-._\~!$& \'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a fragment (optional)
33+ $~ixu ' ;
34+
35+ // Using array to bypass unallowed callable type in properties
36+ private array $ normalizer ;
37+
38+ public function __construct (
39+ private readonly array $ protocols = ['http ' , 'https ' ],
40+ private readonly bool $ allowRelativeProtocol = false ,
41+ ?callable $ normalizer = null ,
42+ private readonly string $ message = 'The {{ name }} value is not a valid URL address, {{ value }} given. '
43+ )
44+ {
45+ $ this ->normalizer ['callable ' ] = $ normalizer ;
46+ }
47+
48+ public function assert (mixed $ value , ?string $ name = null ): void
49+ {
50+ if (!\is_string ($ value )) {
51+ throw new UnexpectedTypeException ('string ' , get_debug_type ($ value ));
52+ }
53+
54+ if ($ this ->normalizer ['callable ' ] !== null ) {
55+ $ value = ($ this ->normalizer ['callable ' ])($ value );
56+ }
57+
58+ $ pattern = $ this ->allowRelativeProtocol ? \str_replace ('(%s): ' , '(?:(%s):)? ' , self ::PATTERN ) : self ::PATTERN ;
59+ $ pattern = \sprintf ($ pattern , \implode ('| ' , $ this ->protocols ));
60+
61+ if (!\preg_match ($ pattern , $ value )) {
62+ throw new UrlException (
63+ message: $ this ->message ,
64+ parameters: [
65+ 'value ' => $ value ,
66+ 'name ' => $ name ,
67+ 'protocols ' => $ this ->protocols
68+ ]
69+ );
70+ }
71+ }
72+ }
0 commit comments