@@ -70,6 +70,29 @@ final class AutoRouterImproved implements AutoRouterInterface
7070 */
7171 private string $ defaultMethod ;
7272
73+ /**
74+ * The URI segments.
75+ */
76+ private array $ segments = [];
77+
78+ /**
79+ * The position of the Controller in the URI segments.
80+ * Null for the default controller.
81+ */
82+ private ?int $ controllerPos = null ;
83+
84+ /**
85+ * The position of the Method in the URI segments.
86+ * Null for the default method.
87+ */
88+ private ?int $ methodPos = null ;
89+
90+ /**
91+ * The position of the first Parameter in the URI segments.
92+ * Null for the no parameters.
93+ */
94+ private ?int $ paramPos = null ;
95+
7396 /**
7497 * @param class-string[] $protectedControllers
7598 * @param string $defaultController Short classname
@@ -108,17 +131,21 @@ private function createSegments(string $uri)
108131 * If there is a controller corresponding to the first segment, the search
109132 * ends there. The remaining segments are parameters to the controller.
110133 *
111- * @param array $segments URI segments
112- *
113134 * @return bool true if a controller class is found.
114135 */
115- private function searchFirstController (array $ segments ): bool
136+ private function searchFirstController (): bool
116137 {
138+ $ segments = $ this ->segments ;
139+
117140 $ controller = '\\' . $ this ->namespace ;
118141
142+ $ controllerPos = -1 ;
143+
119144 while ($ segments !== []) {
120145 $ segment = array_shift ($ segments );
121- $ class = $ this ->translateURIDashes (ucfirst ($ segment ));
146+ $ controllerPos ++;
147+
148+ $ class = $ this ->translateURIDashes (ucfirst ($ segment ));
122149
123150 // as soon as we encounter any segment that is not PSR-4 compliant, stop searching
124151 if (! $ this ->isValidSegment ($ class )) {
@@ -128,9 +155,14 @@ private function searchFirstController(array $segments): bool
128155 $ controller .= '\\' . $ class ;
129156
130157 if (class_exists ($ controller )) {
131- $ this ->controller = $ controller ;
158+ $ this ->controller = $ controller ;
159+ $ this ->controllerPos = $ controllerPos ;
160+
132161 // The first item may be a method name.
133162 $ this ->params = $ segments ;
163+ if ($ segments !== []) {
164+ $ this ->paramPos = $ this ->controllerPos + 1 ;
165+ }
134166
135167 return true ;
136168 }
@@ -142,15 +174,21 @@ private function searchFirstController(array $segments): bool
142174 /**
143175 * Search for the last default controller corresponding to the URI segments.
144176 *
145- * @param array $segments URI segments
146- *
147177 * @return bool true if a controller class is found.
148178 */
149- private function searchLastDefaultController (array $ segments ): bool
179+ private function searchLastDefaultController (): bool
150180 {
151- $ params = [];
181+ $ segments = $ this ->segments ;
182+
183+ $ segmentCount = count ($ this ->segments );
184+ $ paramPos = null ;
185+ $ params = [];
152186
153187 while ($ segments !== []) {
188+ if ($ segmentCount > count ($ segments )) {
189+ $ paramPos = count ($ segments );
190+ }
191+
154192 $ namespaces = array_map (
155193 fn ($ segment ) => $ this ->translateURIDashes (ucfirst ($ segment )),
156194 $ segments
@@ -164,6 +202,10 @@ private function searchLastDefaultController(array $segments): bool
164202 $ this ->controller = $ controller ;
165203 $ this ->params = $ params ;
166204
205+ if ($ params !== []) {
206+ $ this ->paramPos = $ paramPos ;
207+ }
208+
167209 return true ;
168210 }
169211
@@ -179,6 +221,10 @@ private function searchLastDefaultController(array $segments): bool
179221 $ this ->controller = $ controller ;
180222 $ this ->params = $ params ;
181223
224+ if ($ params !== []) {
225+ $ this ->paramPos = 0 ;
226+ }
227+
182228 return true ;
183229 }
184230
@@ -200,19 +246,19 @@ public function getRoute(string $uri, string $httpVerb): array
200246 $ defaultMethod = $ httpVerb . ucfirst ($ this ->defaultMethod );
201247 $ this ->method = $ defaultMethod ;
202248
203- $ segments = $ this ->createSegments ($ uri );
249+ $ this -> segments = $ this ->createSegments ($ uri );
204250
205251 // Check for Module Routes.
206252 if (
207- $ segments !== []
253+ $ this -> segments !== []
208254 && ($ routingConfig = config (Routing::class))
209- && array_key_exists ($ segments [0 ], $ routingConfig ->moduleRoutes )
255+ && array_key_exists ($ this -> segments [0 ], $ routingConfig ->moduleRoutes )
210256 ) {
211- $ uriSegment = array_shift ($ segments );
257+ $ uriSegment = array_shift ($ this -> segments );
212258 $ this ->namespace = rtrim ($ routingConfig ->moduleRoutes [$ uriSegment ], '\\' );
213259 }
214260
215- if ($ this ->searchFirstController ($ segments )) {
261+ if ($ this ->searchFirstController ()) {
216262 // Controller is found.
217263 $ baseControllerName = class_basename ($ this ->controller );
218264
@@ -224,14 +270,15 @@ public function getRoute(string $uri, string $httpVerb): array
224270 'Cannot access the default controller " ' . $ this ->controller . '" with the controller name URI path. '
225271 );
226272 }
227- } elseif ($ this ->searchLastDefaultController ($ segments )) {
273+ } elseif ($ this ->searchLastDefaultController ()) {
228274 // The default Controller is found.
229275 $ baseControllerName = class_basename ($ this ->controller );
230276 } else {
231277 // No Controller is found.
232278 throw new PageNotFoundException ('No controller is found for: ' . $ uri );
233279 }
234280
281+ // The first item may be a method name.
235282 $ params = $ this ->params ;
236283
237284 $ methodParam = array_shift ($ params );
@@ -246,6 +293,15 @@ public function getRoute(string $uri, string $httpVerb): array
246293 $ this ->method = $ method ;
247294 $ this ->params = $ params ;
248295
296+ // Update the positions.
297+ $ this ->methodPos = $ this ->paramPos ;
298+ if ($ params === []) {
299+ $ this ->paramPos = null ;
300+ }
301+ if ($ this ->paramPos !== null ) {
302+ $ this ->paramPos ++;
303+ }
304+
249305 // Prevent access to default controller's method
250306 if (strtolower ($ baseControllerName ) === strtolower ($ this ->defaultController )) {
251307 throw new PageNotFoundException (
@@ -273,6 +329,10 @@ public function getRoute(string $uri, string $httpVerb): array
273329 // Ensure the controller does not have _remap() method.
274330 $ this ->checkRemap ();
275331
332+ // Ensure the URI segments for the controller and method do not contain
333+ // underscores when $translateURIDashes is true.
334+ $ this ->checkUnderscore ($ uri );
335+
276336 // Check parameter count
277337 try {
278338 $ this ->checkParameters ($ uri );
@@ -285,6 +345,20 @@ public function getRoute(string $uri, string $httpVerb): array
285345 return [$ this ->directory , $ this ->controller , $ this ->method , $ this ->params ];
286346 }
287347
348+ /**
349+ * @internal For test purpose only.
350+ *
351+ * @return array<string, int|null>
352+ */
353+ public function getPos (): array
354+ {
355+ return [
356+ 'controller ' => $ this ->controllerPos ,
357+ 'method ' => $ this ->methodPos ,
358+ 'params ' => $ this ->paramPos ,
359+ ];
360+ }
361+
288362 /**
289363 * Get the directory path from the controller and set it to the property.
290364 *
@@ -368,6 +442,28 @@ private function checkRemap(): void
368442 }
369443 }
370444
445+ private function checkUnderscore (string $ uri ): void
446+ {
447+ if ($ this ->translateURIDashes === false ) {
448+ return ;
449+ }
450+
451+ $ paramPos = $ this ->paramPos ?? count ($ this ->segments );
452+
453+ for ($ i = 0 ; $ i < $ paramPos ; $ i ++) {
454+ if (strpos ($ this ->segments [$ i ], '_ ' ) !== false ) {
455+ throw new PageNotFoundException (
456+ 'AutoRouterImproved prohibits access to the URI '
457+ . ' containing underscores (" ' . $ this ->segments [$ i ] . '") '
458+ . ' when $translateURIDashes is enabled. '
459+ . ' Please use the dash. '
460+ . ' Handler: ' . $ this ->controller . ':: ' . $ this ->method
461+ . ', URI: ' . $ uri
462+ );
463+ }
464+ }
465+ }
466+
371467 /**
372468 * Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment
373469 *
@@ -378,10 +474,10 @@ private function isValidSegment(string $segment): bool
378474 return (bool ) preg_match ('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/ ' , $ segment );
379475 }
380476
381- private function translateURIDashes (string $ classname ): string
477+ private function translateURIDashes (string $ segment ): string
382478 {
383479 return $ this ->translateURIDashes
384- ? str_replace ('- ' , '_ ' , $ classname )
385- : $ classname ;
480+ ? str_replace ('- ' , '_ ' , $ segment )
481+ : $ segment ;
386482 }
387483}
0 commit comments