Skip to content

Commit 29e293a

Browse files
committed
feat: fallback to default controller
1 parent b426a4a commit 29e293a

File tree

1 file changed

+142
-125
lines changed

1 file changed

+142
-125
lines changed

system/Router/AutoRouterImproved.php

Lines changed: 142 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)