@@ -33,11 +33,6 @@ final class AutoRouterImproved implements AutoRouterInterface
3333 */
3434 private ?string $ directory = null ;
3535
36- /**
37- * Sub-namespace that contains the requested controller class.
38- */
39- private ?string $ subNamespace = null ;
40-
4136 /**
4237 * The name of the controller class.
4338 */
@@ -112,70 +107,146 @@ private function createSegments(string $uri)
112107 }
113108
114109 /**
115- * Finds controller, method and params from the URI.
110+ * Search for the first controller corresponding to the URI segment .
116111 *
117- * @return array [directory_name, controller_name, controller_method, params]
112+ * If there is a controller corresponding to the first segment, the search
113+ * ends there. The remaining segments are parameters to the controller.
114+ *
115+ * @param array $segments URI segments
116+ *
117+ * @return bool true if a controller class is found.
118118 */
119- public function getRoute ( string $ uri ): array
119+ private function searchFirstController ( array $ segments ): bool
120120 {
121- $ segments = $ this ->createSegments ( $ uri );
121+ $ controller = '\\' . trim ( $ this ->namespace , '\\' );
122122
123- // WARNING: Directories get shifted out of the segments array.
124- $ nonDirSegments = $ this ->scanControllers ($ segments );
123+ while ($ segments !== []) {
124+ $ segment = array_shift ($ segments );
125+ $ class = $ this ->translateURIDashes (ucfirst ($ segment ));
125126
126- $ controllerSegment = '' ;
127- $ baseControllerName = $ this ->defaultController ;
127+ // as soon as we encounter any segment that is not PSR-4 compliant, stop searching
128+ if (! $ this ->isValidSegment ($ class )) {
129+ return false ;
130+ }
128131
129- // If we don't have any segments left - use the default controller;
130- // If not empty, then the first segment should be the controller
131- if (! empty ($ nonDirSegments )) {
132- $ controllerSegment = array_shift ($ nonDirSegments );
132+ $ controller .= '\\' . $ class ;
133133
134- $ baseControllerName = $ this ->translateURIDashes (ucfirst ($ controllerSegment ));
135- }
134+ if (class_exists ($ controller )) {
135+ $ this ->controller = $ controller ;
136+ // The first item may be a method name.
137+ $ this ->params = $ segments ;
136138
137- if (! $ this -> isValidSegment ( $ baseControllerName )) {
138- throw new PageNotFoundException ( $ baseControllerName . ' is not a valid controller name ' );
139+ return true ;
140+ }
139141 }
140142
141- // Prevent access to default controller path
142- if (
143- strtolower ($ baseControllerName ) === strtolower ($ this ->defaultController )
144- && strtolower ($ controllerSegment ) === strtolower ($ this ->defaultController )
145- ) {
146- throw new PageNotFoundException (
147- 'Cannot access the default controller " ' . $ baseControllerName . '" with the controller name URI path. '
143+ return false ;
144+ }
145+
146+ /**
147+ * Search for the last default controller corresponding to the URI segments.
148+ *
149+ * @param array $segments URI segments
150+ *
151+ * @return bool true if a controller class is found.
152+ */
153+ private function searchLastDefaultController (array $ segments ): bool
154+ {
155+ $ params = [];
156+
157+ while ($ segments !== []) {
158+ $ namespaces = array_map (
159+ fn ($ segment ) => $ this ->translateURIDashes (ucfirst ($ segment )),
160+ $ segments
148161 );
162+
163+ $ controller = '\\' . trim ($ this ->namespace , '\\' )
164+ . '\\' . implode ('\\' , $ namespaces )
165+ . '\\' . $ this ->defaultController ;
166+
167+ if (class_exists ($ controller )) {
168+ $ this ->controller = $ controller ;
169+ $ this ->params = $ params ;
170+
171+ return true ;
172+ }
173+
174+ // Prepend the last element in $segments to the beginning of $params.
175+ array_unshift ($ params , array_pop ($ segments ));
149176 }
150177
151- // Use the method name if it exists .
152- if (! empty ( $ nonDirSegments )) {
153- $ methodSegment = $ this ->translateURIDashes ( array_shift ( $ nonDirSegments )) ;
178+ // Check for the default controller in Controllers directory .
179+ $ controller = '\\' . trim ( $ this -> namespace , '\\' )
180+ . '\\' . $ this ->defaultController ;
154181
155- // Prefix HTTP verb
156- $ this ->method = $ this ->httpVerb . ucfirst ($ methodSegment );
182+ if (class_exists ($ controller )) {
183+ $ this ->controller = $ controller ;
184+ $ this ->params = $ params ;
157185
158- // Prevent access to default method path
159- if (strtolower ($ this ->method ) === strtolower ($ this ->defaultMethod )) {
186+ return true ;
187+ }
188+
189+ return false ;
190+ }
191+
192+ /**
193+ * Finds controller, method and params from the URI.
194+ *
195+ * @return array [directory_name, controller_name, controller_method, params]
196+ */
197+ public function getRoute (string $ uri ): array
198+ {
199+ $ segments = $ this ->createSegments ($ uri );
200+
201+ if ($ this ->searchFirstController ($ segments )) {
202+ // Controller is found.
203+ $ baseControllerName = class_basename ($ this ->controller );
204+
205+ // Prevent access to default controller path
206+ if (
207+ strtolower ($ baseControllerName ) === strtolower ($ this ->defaultController )
208+ ) {
160209 throw new PageNotFoundException (
161- 'Cannot access the default method " ' . $ this -> method . '" with the method name URI path. '
210+ 'Cannot access the default controller " ' . $ baseControllerName . '" with the controller name URI path. '
162211 );
163212 }
213+ } elseif ($ this ->searchLastDefaultController ($ segments )) {
214+ // The default Controller is found.
215+ $ baseControllerName = class_basename ($ this ->controller );
216+ } else {
217+ // No Controller is found.
218+ throw new PageNotFoundException ('No controller is found for: ' . $ uri );
164219 }
165220
166- if (! empty ($ nonDirSegments )) {
167- $ this ->params = $ nonDirSegments ;
221+ $ params = $ this ->params ;
222+
223+ $ methodParam = array_shift ($ params );
224+
225+ $ method = '' ;
226+ if ($ methodParam !== null ) {
227+ $ method = $ this ->httpVerb . ucfirst ($ this ->translateURIDashes ($ methodParam ));
168228 }
169229
170- // Ensure the controller stores the fully-qualified class name
171- $ this ->controller = '\\' . ltrim (
172- str_replace (
173- '/ ' ,
174- '\\' ,
175- $ this ->namespace . $ this ->subNamespace . $ baseControllerName
176- ),
177- '\\'
178- );
230+ if ($ methodParam !== null && method_exists ($ this ->controller , $ method )) {
231+ // Method is found.
232+ $ this ->method = $ method ;
233+ $ this ->params = $ params ;
234+
235+ // Prevent access to default method path
236+ if (strtolower ($ this ->method ) === strtolower ($ this ->defaultMethod )) {
237+ throw new PageNotFoundException (
238+ 'Cannot access the default method " ' . $ this ->method . '" with the method name URI path. '
239+ );
240+ }
241+ } else {
242+ if (method_exists ($ this ->controller , $ this ->defaultMethod )) {
243+ // The default method is found.
244+ $ this ->method = $ this ->defaultMethod ;
245+ } else {
246+ // No method is found.
247+ throw PageNotFoundException::forControllerNotFound ($ this ->controller , $ method );
248+ }
249+ }
179250
180251 // Ensure the controller is not defined in routes.
181252 $ this ->protectDefinedRoutes ();
@@ -187,25 +258,35 @@ public function getRoute(string $uri): array
187258 try {
188259 $ this ->checkParameters ($ uri );
189260 } catch (MethodNotFoundException $ e ) {
190- // Fallback to the default method
191- if (! isset ($ methodSegment )) {
192- throw PageNotFoundException::forControllerNotFound ($ this ->controller , $ this ->method );
193- }
194-
195- array_unshift ($ this ->params , $ methodSegment );
196- $ method = $ this ->method ;
197- $ this ->method = $ this ->defaultMethod ;
198-
199- try {
200- $ this ->checkParameters ($ uri );
201- } catch (MethodNotFoundException $ e ) {
202- throw PageNotFoundException::forControllerNotFound ($ this ->controller , $ method );
203- }
261+ throw PageNotFoundException::forControllerNotFound ($ this ->controller , $ this ->method );
204262 }
205263
264+ $ this ->setDirectory ();
265+
206266 return [$ this ->directory , $ this ->controller , $ this ->method , $ this ->params ];
207267 }
208268
269+ /**
270+ * Get the directory path from the controller and set it to the property.
271+ *
272+ * @return void
273+ */
274+ private function setDirectory ()
275+ {
276+ $ segments = explode ('\\' , trim ($ this ->controller , '\\' ));
277+
278+ // Remove short classname.
279+ array_pop ($ segments );
280+
281+ $ namespaces = implode ('\\' , $ segments );
282+
283+ $ dir = substr ($ namespaces , strlen ($ this ->namespace ));
284+
285+ if ($ dir !== '' ) {
286+ $ this ->directory = substr ($ namespaces , strlen ($ this ->namespace )) . '/ ' ;
287+ }
288+ }
289+
209290 private function protectDefinedRoutes (): void
210291 {
211292 $ controller = strtolower ($ this ->controller );
@@ -264,46 +345,6 @@ private function checkRemap(): void
264345 }
265346 }
266347
267- /**
268- * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments
269- *
270- * @param array $segments URI segments
271- *
272- * @return array returns an array of remaining uri segments that don't map onto a directory
273- */
274- private function scanControllers (array $ segments ): array
275- {
276- // Loop through our segments and return as soon as a controller
277- // is found or when such a directory doesn't exist
278- $ c = count ($ segments );
279-
280- while ($ c -- > 0 ) {
281- $ segmentConvert = $ this ->translateURIDashes (ucfirst ($ segments [0 ]));
282-
283- // as soon as we encounter any segment that is not PSR-4 compliant, stop searching
284- if (! $ this ->isValidSegment ($ segmentConvert )) {
285- return $ segments ;
286- }
287-
288- $ test = $ this ->namespace . $ this ->subNamespace . $ segmentConvert ;
289-
290- // as long as each segment is *not* a controller file, add it to $this->subNamespace
291- if (! class_exists ($ test )) {
292- $ this ->setSubNamespace ($ segmentConvert , true , false );
293- array_shift ($ segments );
294-
295- $ this ->directory .= $ this ->directory . $ segmentConvert . '/ ' ;
296-
297- continue ;
298- }
299-
300- return $ segments ;
301- }
302-
303- // This means that all segments were actually directories
304- return $ segments ;
305- }
306-
307348 /**
308349 * Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment
309350 *
@@ -314,30 +355,6 @@ private function isValidSegment(string $segment): bool
314355 return (bool ) preg_match ('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/ ' , $ segment );
315356 }
316357
317- /**
318- * Sets the sub-namespace that the controller is in.
319- *
320- * @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments
321- */
322- private function setSubNamespace (?string $ namespace = null , bool $ append = false , bool $ validate = true ): void
323- {
324- if ($ validate ) {
325- $ segments = explode ('/ ' , trim ($ namespace , '/ ' ));
326-
327- foreach ($ segments as $ segment ) {
328- if (! $ this ->isValidSegment ($ segment )) {
329- return ;
330- }
331- }
332- }
333-
334- if ($ append !== true || empty ($ this ->subNamespace )) {
335- $ this ->subNamespace = trim ($ namespace , '/ ' ) . '\\' ;
336- } else {
337- $ this ->subNamespace .= trim ($ namespace , '/ ' ) . '\\' ;
338- }
339- }
340-
341358 private function translateURIDashes (string $ classname ): string
342359 {
343360 return $ this ->translateURIDashes
0 commit comments