1- <?php
2-
3- namespace Codemonster \Router ;
4-
1+ <?php
2+
3+ namespace Codemonster \Router ;
4+
55class Route
66{
77 /** @var list<string> */
@@ -10,26 +10,156 @@ class Route
1010 public mixed $ handler ;
1111 /** @var list<list<string|array<mixed>>> */
1212 protected array $ middleware = [];
13+ protected ?string $ name = null ;
14+ /** @var array<string, string> */
15+ protected array $ parameters = [];
16+ /** @var array<string, string> */
17+ protected array $ constraints = [];
1318
1419 /** @param string|list<string> $methods */
1520 public function __construct (array |string $ methods , string $ path , mixed $ handler )
16- {
17- $ this ->methods = ( array )$ methods ;
18- $ this ->path = $ path ;
19- $ this ->handler = $ handler ;
20- }
21-
21+ {
22+ $ this ->methods = array_values ( array_map ( ' strtoupper ' , ( array ) $ methods)) ;
23+ $ this ->path = $ path ;
24+ $ this ->handler = $ handler ;
25+ }
26+
2227 /** @param string|array<mixed> ...$middleware */
2328 public function middleware (string |array ...$ middleware ): static
24- {
29+ {
2530 $ this ->middleware [] = array_values ($ middleware );
26-
27- return $ this ;
28- }
29-
31+
32+ return $ this ;
33+ }
34+
3035 /** @return list<list<string|array<mixed>>> */
3136 public function getMiddleware (): array
32- {
33- return $ this ->middleware ;
34- }
35- }
37+ {
38+ return $ this ->middleware ;
39+ }
40+
41+ public function name (string $ name ): static
42+ {
43+ $ this ->name = $ name ;
44+
45+ return $ this ;
46+ }
47+
48+ public function getName (): ?string
49+ {
50+ return $ this ->name ;
51+ }
52+
53+ /** @return array<string, string> */
54+ public function getConstraints (): array
55+ {
56+ return $ this ->constraints ;
57+ }
58+
59+ /** @param array<string, string>|string $name */
60+ public function where (array |string $ name , ?string $ pattern = null ): static
61+ {
62+ if (is_array ($ name )) {
63+ foreach ($ name as $ key => $ value ) {
64+ $ this ->constraints [$ key ] = $ value ;
65+ }
66+
67+ return $ this ;
68+ }
69+
70+ if ($ pattern === null ) {
71+ throw new \InvalidArgumentException ('Route constraint pattern is required. ' );
72+ }
73+
74+ $ this ->constraints [$ name ] = $ pattern ;
75+
76+ return $ this ;
77+ }
78+
79+ public function matches (string $ method , string $ uri ): bool
80+ {
81+ $ this ->parameters = [];
82+
83+ if (!in_array (strtoupper ($ method ), $ this ->methods , true )) {
84+ return false ;
85+ }
86+
87+ $ matches = [];
88+ if (preg_match ($ this ->regex (), $ uri , $ matches ) !== 1 ) {
89+ return false ;
90+ }
91+
92+ foreach ($ this ->parameterNames () as $ name ) {
93+ if (isset ($ matches [$ name ]) && is_string ($ matches [$ name ])) {
94+ $ this ->parameters [$ name ] = rawurldecode ($ matches [$ name ]);
95+ }
96+ }
97+
98+ return true ;
99+ }
100+
101+ /** @return array<string, string> */
102+ public function parameters (): array
103+ {
104+ return $ this ->parameters ;
105+ }
106+
107+ /** @param array<string, scalar|null> $parameters */
108+ public function uri (array $ parameters = []): string
109+ {
110+ $ used = [];
111+ $ uri = preg_replace_callback (
112+ '/\{([A-Za-z_][A-Za-z0-9_]*)\}/ ' ,
113+ function (array $ matches ) use ($ parameters , &$ used ): string {
114+ $ name = $ matches [1 ];
115+
116+ if (!array_key_exists ($ name , $ parameters )) {
117+ throw new \InvalidArgumentException ("Missing route parameter [ {$ name }]. " );
118+ }
119+
120+ $ used [] = $ name ;
121+
122+ return rawurlencode ((string ) $ parameters [$ name ]);
123+ },
124+ $ this ->path ,
125+ );
126+
127+ if (!is_string ($ uri )) {
128+ throw new \RuntimeException ("Unable to generate URI for route [ {$ this ->path }]. " );
129+ }
130+
131+ $ query = array_diff_key ($ parameters , array_flip ($ used ));
132+ if ($ query !== []) {
133+ $ uri .= '? ' . http_build_query ($ query );
134+ }
135+
136+ return $ uri ;
137+ }
138+
139+ protected function regex (): string
140+ {
141+ $ regex = '' ;
142+ $ offset = 0 ;
143+ preg_match_all ('/\{([A-Za-z_][A-Za-z0-9_]*)\}/ ' , $ this ->path , $ matches , PREG_OFFSET_CAPTURE );
144+
145+ foreach ($ matches [0 ] as $ index => $ match ) {
146+ [$ placeholder , $ position ] = $ match ;
147+ $ name = $ matches [1 ][$ index ][0 ];
148+ $ regex .= preg_quote (substr ($ this ->path , $ offset , $ position - $ offset ), '# ' );
149+ $ regex .= '(?P< ' . $ name . '> ' . ($ this ->constraints [$ name ] ?? '[^/]+ ' ) . ') ' ;
150+ $ offset = $ position + strlen ($ placeholder );
151+ }
152+
153+ $ regex .= preg_quote (substr ($ this ->path , $ offset ), '# ' );
154+
155+ return '#^ ' . $ regex . '$# ' ;
156+ }
157+
158+ /** @return list<string> */
159+ protected function parameterNames (): array
160+ {
161+ preg_match_all ('/\{([A-Za-z_][A-Za-z0-9_]*)\}/ ' , $ this ->path , $ matches );
162+
163+ return $ matches [1 ];
164+ }
165+ }
0 commit comments