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