From 9575724bf4ca190ee8c482b4b86a0ed2ffc3fd03 Mon Sep 17 00:00:00 2001 From: kenjis Date: Mon, 3 Apr 2023 18:35:38 +0900 Subject: [PATCH 01/14] refactor: extract method --- system/Router/AutoRouterImproved.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 000afda126bd..5eca3c5a25a2 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -104,6 +104,14 @@ public function __construct( $this->method = $this->defaultMethod; } + private function createSegments(string $uri) + { + $segments = explode('/', $uri); + $segments = array_filter($segments, static fn ($segment) => $segment !== ''); + // numerically reindex the array, removing gaps + return array_values($segments); + } + /** * Finds controller, method and params from the URI. * @@ -111,7 +119,7 @@ public function __construct( */ public function getRoute(string $uri): array { - $segments = explode('/', $uri); + $segments = $this->createSegments($uri); // Check for Module Routes. if ( @@ -275,10 +283,6 @@ private function checkRemap(): void */ private function scanControllers(array $segments): array { - $segments = array_filter($segments, static fn ($segment) => $segment !== ''); - // numerically reindex the array, removing gaps - $segments = array_values($segments); - // Loop through our segments and return as soon as a controller // is found or when such a directory doesn't exist $c = count($segments); From ddc6e99fcf8d116a890be6b950f5326321933d4d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 14:02:47 +0900 Subject: [PATCH 02/14] feat: fallback to default controller --- system/Router/AutoRouterImproved.php | 267 ++++++++++++++------------- 1 file changed, 142 insertions(+), 125 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 5eca3c5a25a2..56c524bef03d 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -34,11 +34,6 @@ final class AutoRouterImproved implements AutoRouterInterface */ private ?string $directory = null; - /** - * Sub-namespace that contains the requested controller class. - */ - private ?string $subNamespace = null; - /** * The name of the controller class. */ @@ -112,6 +107,89 @@ private function createSegments(string $uri) return array_values($segments); } + /** + * Search for the first controller corresponding to the URI segment. + * + * If there is a controller corresponding to the first segment, the search + * ends there. The remaining segments are parameters to the controller. + * + * @param array $segments URI segments + * + * @return bool true if a controller class is found. + */ + private function searchFirstController(array $segments): bool + { + $controller = '\\' . trim($this->namespace, '\\'); + + while ($segments !== []) { + $segment = array_shift($segments); + $class = $this->translateURIDashes(ucfirst($segment)); + + // as soon as we encounter any segment that is not PSR-4 compliant, stop searching + if (! $this->isValidSegment($class)) { + return false; + } + + $controller .= '\\' . $class; + + if (class_exists($controller)) { + $this->controller = $controller; + // The first item may be a method name. + $this->params = $segments; + + return true; + } + } + + return false; + } + + /** + * Search for the last default controller corresponding to the URI segments. + * + * @param array $segments URI segments + * + * @return bool true if a controller class is found. + */ + private function searchLastDefaultController(array $segments): bool + { + $params = []; + + while ($segments !== []) { + $namespaces = array_map( + fn ($segment) => $this->translateURIDashes(ucfirst($segment)), + $segments + ); + + $controller = '\\' . trim($this->namespace, '\\') + . '\\' . implode('\\', $namespaces) + . '\\' . $this->defaultController; + + if (class_exists($controller)) { + $this->controller = $controller; + $this->params = $params; + + return true; + } + + // Prepend the last element in $segments to the beginning of $params. + array_unshift($params, array_pop($segments)); + } + + // Check for the default controller in Controllers directory. + $controller = '\\' . trim($this->namespace, '\\') + . '\\' . $this->defaultController; + + if (class_exists($controller)) { + $this->controller = $controller; + $this->params = $params; + + return true; + } + + return false; + } + /** * Finds controller, method and params from the URI. * @@ -130,40 +208,39 @@ public function getRoute(string $uri): array $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\') . '\\'; } - // WARNING: Directories get shifted out of the segments array. - $nonDirSegments = $this->scanControllers($segments); - - $controllerSegment = ''; - $baseControllerName = $this->defaultController; + if ($this->searchFirstController($segments)) { + // Controller is found. + $baseControllerName = class_basename($this->controller); - // If we don't have any segments left - use the default controller; - // If not empty, then the first segment should be the controller - if (! empty($nonDirSegments)) { - $controllerSegment = array_shift($nonDirSegments); - - $baseControllerName = $this->translateURIDashes(ucfirst($controllerSegment)); + // Prevent access to default controller path + if ( + strtolower($baseControllerName) === strtolower($this->defaultController) + ) { + throw new PageNotFoundException( + 'Cannot access the default controller "' . $baseControllerName . '" with the controller name URI path.' + ); + } + } elseif ($this->searchLastDefaultController($segments)) { + // The default Controller is found. + $baseControllerName = class_basename($this->controller); + } else { + // No Controller is found. + throw new PageNotFoundException('No controller is found for: ' . $uri); } - if (! $this->isValidSegment($baseControllerName)) { - throw new PageNotFoundException($baseControllerName . ' is not a valid controller name'); - } + $params = $this->params; - // Prevent access to default controller path - if ( - strtolower($baseControllerName) === strtolower($this->defaultController) - && strtolower($controllerSegment) === strtolower($this->defaultController) - ) { - throw new PageNotFoundException( - 'Cannot access the default controller "' . $baseControllerName . '" with the controller name URI path.' - ); - } + $methodParam = array_shift($params); - // Use the method name if it exists. - if (! empty($nonDirSegments)) { - $methodSegment = $this->translateURIDashes(array_shift($nonDirSegments)); + $method = ''; + if ($methodParam !== null) { + $method = $this->httpVerb . ucfirst($this->translateURIDashes($methodParam)); + } - // Prefix HTTP verb - $this->method = $this->httpVerb . ucfirst($methodSegment); + if ($methodParam !== null && method_exists($this->controller, $method)) { + // Method is found. + $this->method = $method; + $this->params = $params; // Prevent access to default method path if (strtolower($this->method) === strtolower($this->defaultMethod)) { @@ -171,22 +248,16 @@ public function getRoute(string $uri): array 'Cannot access the default method "' . $this->method . '" with the method name URI path.' ); } + } else { + if (method_exists($this->controller, $this->defaultMethod)) { + // The default method is found. + $this->method = $this->defaultMethod; + } else { + // No method is found. + throw PageNotFoundException::forControllerNotFound($this->controller, $method); + } } - if (! empty($nonDirSegments)) { - $this->params = $nonDirSegments; - } - - // Ensure the controller stores the fully-qualified class name - $this->controller = '\\' . ltrim( - str_replace( - '/', - '\\', - $this->namespace . $this->subNamespace . $baseControllerName - ), - '\\' - ); - // Ensure the controller is not defined in routes. $this->protectDefinedRoutes(); @@ -197,25 +268,35 @@ public function getRoute(string $uri): array try { $this->checkParameters($uri); } catch (MethodNotFoundException $e) { - // Fallback to the default method - if (! isset($methodSegment)) { - throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); - } - - array_unshift($this->params, $methodSegment); - $method = $this->method; - $this->method = $this->defaultMethod; - - try { - $this->checkParameters($uri); - } catch (MethodNotFoundException $e) { - throw PageNotFoundException::forControllerNotFound($this->controller, $method); - } + throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); } + $this->setDirectory(); + return [$this->directory, $this->controller, $this->method, $this->params]; } + /** + * Get the directory path from the controller and set it to the property. + * + * @return void + */ + private function setDirectory() + { + $segments = explode('\\', trim($this->controller, '\\')); + + // Remove short classname. + array_pop($segments); + + $namespaces = implode('\\', $segments); + + $dir = substr($namespaces, strlen($this->namespace)); + + if ($dir !== '') { + $this->directory = substr($namespaces, strlen($this->namespace)) . '/'; + } + } + private function protectDefinedRoutes(): void { $controller = strtolower($this->controller); @@ -274,46 +355,6 @@ private function checkRemap(): void } } - /** - * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments - * - * @param array $segments URI segments - * - * @return array returns an array of remaining uri segments that don't map onto a directory - */ - private function scanControllers(array $segments): array - { - // Loop through our segments and return as soon as a controller - // is found or when such a directory doesn't exist - $c = count($segments); - - while ($c-- > 0) { - $segmentConvert = $this->translateURIDashes(ucfirst($segments[0])); - - // as soon as we encounter any segment that is not PSR-4 compliant, stop searching - if (! $this->isValidSegment($segmentConvert)) { - return $segments; - } - - $test = $this->namespace . $this->subNamespace . $segmentConvert; - - // as long as each segment is *not* a controller file, add it to $this->subNamespace - if (! class_exists($test)) { - $this->setSubNamespace($segmentConvert, true, false); - array_shift($segments); - - $this->directory .= $this->directory . $segmentConvert . '/'; - - continue; - } - - return $segments; - } - - // This means that all segments were actually directories - return $segments; - } - /** * Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment * @@ -324,30 +365,6 @@ private function isValidSegment(string $segment): bool return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment); } - /** - * Sets the sub-namespace that the controller is in. - * - * @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments - */ - private function setSubNamespace(?string $namespace = null, bool $append = false, bool $validate = true): void - { - if ($validate) { - $segments = explode('/', trim($namespace, '/')); - - foreach ($segments as $segment) { - if (! $this->isValidSegment($segment)) { - return; - } - } - } - - if ($append !== true || empty($this->subNamespace)) { - $this->subNamespace = trim($namespace, '/') . '\\'; - } else { - $this->subNamespace .= trim($namespace, '/') . '\\'; - } - } - private function translateURIDashes(string $classname): string { return $this->translateURIDashes From dde702f4914a627b9fe6c5c3fe0511a018855ea1 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 14:36:53 +0900 Subject: [PATCH 03/14] test: add tests for fallback to default controller --- .../system/Router/AutoRouterImprovedTest.php | 39 +++++++++++++++++++ .../Router/Controllers/Subfolder/Home.php | 21 ++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/system/Router/Controllers/Subfolder/Home.php diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 75a5f49b8648..0042ed099cb6 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -235,6 +235,45 @@ public function testAutoRouteFallbackToDefaultMethod() $this->assertSame(['15'], $params); } + public function testAutoRouteFallbackToDefaultControllerOneParam() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('subfolder/15'); + + $this->assertSame('Subfolder/', $directory); + $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('getIndex', $method); + $this->assertSame(['15'], $params); + } + + public function testAutoRouteFallbackToDefaultControllerTwoParams() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('subfolder/15/20'); + + $this->assertSame('Subfolder/', $directory); + $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('getIndex', $method); + $this->assertSame(['15', '20'], $params); + } + + public function testAutoRouteFallbackToDefaultControllerNoParams() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('subfolder'); + + $this->assertSame('Subfolder/', $directory); + $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('getIndex', $method); + $this->assertSame([], $params); + } + public function testAutoRouteRejectsSingleDot() { $this->expectException(PageNotFoundException::class); diff --git a/tests/system/Router/Controllers/Subfolder/Home.php b/tests/system/Router/Controllers/Subfolder/Home.php new file mode 100644 index 000000000000..b249971f2516 --- /dev/null +++ b/tests/system/Router/Controllers/Subfolder/Home.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router\Controllers\Subfolder; + +use CodeIgniter\Controller; + +class Home extends Controller +{ + public function getIndex($p1 = null, $p2 = null) + { + } +} From 488c42c3cdf034632cce8a55ab88597050ab25a6 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 15:43:39 +0900 Subject: [PATCH 04/14] refactor: extract method --- .../ControllerMethodReader.php | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php index 98e0eddfa8a1..936a1ba9c973 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php @@ -85,23 +85,7 @@ public function read(string $class, string $defaultController = 'Home', string $ continue; } - $params = []; - $routeParams = ''; - $refParams = $method->getParameters(); - - foreach ($refParams as $param) { - $required = true; - if ($param->isOptional()) { - $required = false; - - $routeParams .= '[/..]'; - } else { - $routeParams .= '/..'; - } - - // [variable_name => required?] - $params[$param->getName()] = $required; - } + [$params, $routeParams] = $this->getParameters($method); // Route for the default method. $output[] = [ @@ -117,23 +101,7 @@ public function read(string $class, string $defaultController = 'Home', string $ $route = $classInUri . '/' . $methodInUri; - $params = []; - $routeParams = ''; - $refParams = $method->getParameters(); - - foreach ($refParams as $param) { - $required = true; - if ($param->isOptional()) { - $required = false; - - $routeParams .= '[/..]'; - } else { - $routeParams .= '/..'; - } - - // [variable_name => required?] - $params[$param->getName()] = $required; - } + [$params, $routeParams] = $this->getParameters($method); // If it is the default controller, the method will not be // routed. @@ -155,6 +123,29 @@ public function read(string $class, string $defaultController = 'Home', string $ return $output; } + private function getParameters(ReflectionMethod $method): array + { + $params = []; + $routeParams = ''; + $refParams = $method->getParameters(); + + foreach ($refParams as $param) { + $required = true; + if ($param->isOptional()) { + $required = false; + + $routeParams .= '[/..]'; + } else { + $routeParams .= '/..'; + } + + // [variable_name => required?] + $params[$param->getName()] = $required; + } + + return [$params, $routeParams]; + } + /** * @phpstan-param class-string $classname * From 941a258ad7e61257016fb7baf6477c729540d7e9 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 15:51:25 +0900 Subject: [PATCH 05/14] feat: parameter info for default controller --- .../ControllerMethodReader.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php index 936a1ba9c973..c91219c13a8e 100644 --- a/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php +++ b/system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php @@ -73,7 +73,8 @@ public function read(string $class, string $defaultController = 'Home', string $ $classInUri, $classname, $methodName, - $httpVerb + $httpVerb, + $method ); if ($routeForDefaultController !== []) { @@ -180,7 +181,8 @@ private function getRouteForDefaultController( string $uriByClass, string $classname, string $methodName, - string $httpVerb + string $httpVerb, + ReflectionMethod $method ): array { $output = []; @@ -189,12 +191,18 @@ private function getRouteForDefaultController( $routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/'); $routeWithoutController = $routeWithoutController ?: '/'; + [$params, $routeParams] = $this->getParameters($method); + + if ($routeWithoutController === '/' && $routeParams !== '') { + $routeWithoutController = ''; + } + $output[] = [ 'method' => $httpVerb, 'route' => $routeWithoutController, - 'route_params' => '', + 'route_params' => $routeParams, 'handler' => '\\' . $classname . '::' . $methodName, - 'params' => [], + 'params' => $params, ]; } From b6e93adca0f858ec45df6304b30bbaa1422cf000 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 16:52:14 +0900 Subject: [PATCH 06/14] docs: add docs --- user_guide_src/source/changelogs/v4.4.0.rst | 5 ++- .../source/incoming/controllers.rst | 42 ++++++++++++++++++- .../source/incoming/controllers/025.php | 18 ++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 user_guide_src/source/incoming/controllers/025.php diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index df5b2c6da321..18713dfec84a 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -96,7 +96,10 @@ Others - **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. - **Auto Routing (Improved)**: Now you can route to Modules. See :ref:`auto-routing-improved-module-routing` for details. -- **Auto Routing (Improved)**: Now you can use URI without a method name like +- **Auto Routing (Improved):** Now you can pass arguments to the the default + controller that is omitted in the URI. + See :ref:`controller-default-controller-fallback` for details. +- **Auto Routing (Improved):** Now you can use URI without a method name like ``product/15`` where ``15`` is an arbitrary number. See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 18388e76b6a5..480b7c5f0c46 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -279,6 +279,44 @@ Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): .. literalinclude:: controllers/022.php +.. _controller-default-controller-fallback: + +Default Controller Fallback +=========================== + +.. versionadded:: 4.4.0 + +If the controller corresponding to the URI segment of the controller name +does not exist, and if the default controller (``Home`` by default) exists in +the directory, the remaining URI segments are passed to the default controller +for execution. + +For example, when you have the following default controller ``Home`` in the +**app/Controllers/News** directory: + +.. literalinclude:: controllers/025.php + +Load the following URL:: + + example.com/index.php/news/list + +The ``News\Home`` controller will be found, and the ``getList()`` method will be +executed. + +.. note:: If there is ``App\Controllers\News`` controller, it takes precedence. + The URI segments are searched sequentially and the first controller found + is used. + +Load the following URL:: + + example.com/index.php/news/101 + +The default ``getIndex()`` method will be passed URI segments 2 (``'101'``): + +.. note:: If there are more parameters in the URI than the method parameters, + Auto Routing (Improved) does not execute the method, and it results in 404 + Not Found. + .. _controller-default-method-fallback: Default Method Fallback @@ -287,8 +325,8 @@ Default Method Fallback .. versionadded:: 4.4.0 If the controller method corresponding to the URI segment of the method name -does not exist, and if the default method is defined, the URI segments are -passed to the default method for execution. +does not exist, and if the default method is defined, the remaining URI segments +are passed to the default method for execution. .. literalinclude:: controllers/024.php diff --git a/user_guide_src/source/incoming/controllers/025.php b/user_guide_src/source/incoming/controllers/025.php new file mode 100644 index 000000000000..2c88ca92ad13 --- /dev/null +++ b/user_guide_src/source/incoming/controllers/025.php @@ -0,0 +1,18 @@ + Date: Tue, 4 Apr 2023 17:26:47 +0900 Subject: [PATCH 07/14] refactor: by rector --- system/Router/AutoRouterImproved.php | 12 +++++------- tests/system/Router/AutoRouterImprovedTest.php | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index 56c524bef03d..da187a4980ad 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -248,14 +248,12 @@ public function getRoute(string $uri): array 'Cannot access the default method "' . $this->method . '" with the method name URI path.' ); } + } elseif (method_exists($this->controller, $this->defaultMethod)) { + // The default method is found. + $this->method = $this->defaultMethod; } else { - if (method_exists($this->controller, $this->defaultMethod)) { - // The default method is found. - $this->method = $this->defaultMethod; - } else { - // No method is found. - throw PageNotFoundException::forControllerNotFound($this->controller, $method); - } + // No method is found. + throw PageNotFoundException::forControllerNotFound($this->controller, $method); } // Ensure the controller is not defined in routes. diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 0042ed099cb6..9f22260d98dc 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -243,7 +243,7 @@ public function testAutoRouteFallbackToDefaultControllerOneParam() = $router->getRoute('subfolder/15'); $this->assertSame('Subfolder/', $directory); - $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame(['15'], $params); } @@ -256,7 +256,7 @@ public function testAutoRouteFallbackToDefaultControllerTwoParams() = $router->getRoute('subfolder/15/20'); $this->assertSame('Subfolder/', $directory); - $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame(['15', '20'], $params); } @@ -269,7 +269,7 @@ public function testAutoRouteFallbackToDefaultControllerNoParams() = $router->getRoute('subfolder'); $this->assertSame('Subfolder/', $directory); - $this->assertSame('\CodeIgniter\Router\Controllers\Subfolder\Home', $controller); + $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Home::class, $controller); $this->assertSame('getIndex', $method); $this->assertSame([], $params); } From fce8923bf8f1e20126fcbf49282850f869922c8d Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 4 Apr 2023 18:14:21 +0900 Subject: [PATCH 08/14] fix: directory is not correct --- system/Router/AutoRouterImproved.php | 16 ++++++++------ .../system/Router/AutoRouterImprovedTest.php | 13 ++++++++++++ .../Subfolder/Sub/Mycontroller.php | 21 +++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index da187a4980ad..b4a85e3648b8 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -88,7 +88,7 @@ public function __construct( string $httpVerb ) { $this->protectedControllers = $protectedControllers; - $this->namespace = rtrim($namespace, '\\') . '\\'; + $this->namespace = rtrim($namespace, '\\'); $this->translateURIDashes = $translateURIDashes; $this->httpVerb = $httpVerb; $this->defaultController = $defaultController; @@ -119,7 +119,7 @@ private function createSegments(string $uri) */ private function searchFirstController(array $segments): bool { - $controller = '\\' . trim($this->namespace, '\\'); + $controller = '\\' . $this->namespace; while ($segments !== []) { $segment = array_shift($segments); @@ -161,7 +161,7 @@ private function searchLastDefaultController(array $segments): bool $segments ); - $controller = '\\' . trim($this->namespace, '\\') + $controller = '\\' . $this->namespace . '\\' . implode('\\', $namespaces) . '\\' . $this->defaultController; @@ -177,7 +177,7 @@ private function searchLastDefaultController(array $segments): bool } // Check for the default controller in Controllers directory. - $controller = '\\' . trim($this->namespace, '\\') + $controller = '\\' . $this->namespace . '\\' . $this->defaultController; if (class_exists($controller)) { @@ -288,10 +288,14 @@ private function setDirectory() $namespaces = implode('\\', $segments); - $dir = substr($namespaces, strlen($this->namespace)); + $dir = str_replace( + '\\', + '/', + ltrim(substr($namespaces, strlen($this->namespace)), '\\') + ); if ($dir !== '') { - $this->directory = substr($namespaces, strlen($this->namespace)) . '/'; + $this->directory = $dir . '/'; } } diff --git a/tests/system/Router/AutoRouterImprovedTest.php b/tests/system/Router/AutoRouterImprovedTest.php index 9f22260d98dc..803407955770 100644 --- a/tests/system/Router/AutoRouterImprovedTest.php +++ b/tests/system/Router/AutoRouterImprovedTest.php @@ -167,6 +167,19 @@ public function testAutoRouteFindsControllerWithSubfolder() $this->assertSame([], $params); } + public function testAutoRouteFindsControllerWithSubSubfolder() + { + $router = $this->createNewAutoRouter(); + + [$directory, $controller, $method, $params] + = $router->getRoute('subfolder/sub/mycontroller/somemethod'); + + $this->assertSame('Subfolder/Sub/', $directory); + $this->assertSame('\\' . \CodeIgniter\Router\Controllers\Subfolder\Sub\Mycontroller::class, $controller); + $this->assertSame('getSomemethod', $method); + $this->assertSame([], $params); + } + public function testAutoRouteFindsDashedSubfolder() { $router = $this->createNewAutoRouter(); diff --git a/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php b/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php new file mode 100644 index 000000000000..7bd80203b914 --- /dev/null +++ b/tests/system/Router/Controllers/Subfolder/Sub/Mycontroller.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Router\Controllers\Subfolder\Sub; + +use CodeIgniter\Controller; + +class Mycontroller extends Controller +{ + public function getSomemethod() + { + } +} From 4772589365594270cfc0295cbfb11f8daf2495ba Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 16:50:49 +0900 Subject: [PATCH 09/14] feat: improve error message --- system/Router/AutoRouterImproved.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index b4a85e3648b8..afa602ef9626 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -217,7 +217,7 @@ public function getRoute(string $uri): array strtolower($baseControllerName) === strtolower($this->defaultController) ) { throw new PageNotFoundException( - 'Cannot access the default controller "' . $baseControllerName . '" with the controller name URI path.' + 'Cannot access the default controller "' . $this->controller . '" with the controller name URI path.' ); } } elseif ($this->searchLastDefaultController($segments)) { From ff1e958528cd17e4597b7dc044c99fc9cafd7a2b Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 16:51:33 +0900 Subject: [PATCH 10/14] feat: prevent access to default controller's method The default method is still accesible. --- system/Router/AutoRouterImproved.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index afa602ef9626..eec6bcbd3374 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -242,6 +242,13 @@ public function getRoute(string $uri): array $this->method = $method; $this->params = $params; + // Prevent access to default controller's method + if (strtolower($baseControllerName) === strtolower($this->defaultController)) { + throw new PageNotFoundException( + 'Cannot access the default controller "' . $this->controller . '::' . $this->method . '"' + ); + } + // Prevent access to default method path if (strtolower($this->method) === strtolower($this->defaultMethod)) { throw new PageNotFoundException( From edbedc1c270dca776a78b97fe8f2181871232687 Mon Sep 17 00:00:00 2001 From: kenjis Date: Tue, 11 Apr 2023 17:23:19 +0900 Subject: [PATCH 11/14] docs: update docs --- user_guide_src/source/changelogs/v4.4.0.rst | 9 ++- .../source/incoming/controllers.rst | 61 ++++++++----------- .../source/incoming/controllers/025.php | 5 -- 3 files changed, 28 insertions(+), 47 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 18713dfec84a..5359c4586faf 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -96,11 +96,10 @@ Others - **View:** Added optional 2nd parameter ``$saveData`` on ``renderSection()`` to prevent from auto cleans the data after displaying. See :ref:`View Layouts ` for details. - **Auto Routing (Improved)**: Now you can route to Modules. See :ref:`auto-routing-improved-module-routing` for details. -- **Auto Routing (Improved):** Now you can pass arguments to the the default - controller that is omitted in the URI. - See :ref:`controller-default-controller-fallback` for details. -- **Auto Routing (Improved):** Now you can use URI without a method name like - ``product/15`` where ``15`` is an arbitrary number. +- **Auto Routing (Improved):** If a controller is found that corresponds to a URI + segment and that controller does not have a method defined for the URI segment, + the default method will now be executed. This addition allows for more flexible + handling of URIs in auto routing. See :ref:`controller-default-method-fallback` for details. - **Filters:** Now you can use Filter Arguments with :ref:`$filters property `. - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 480b7c5f0c46..0f3f8d518da4 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -279,17 +279,33 @@ Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): .. literalinclude:: controllers/022.php -.. _controller-default-controller-fallback: +.. _controller-default-method-fallback: -Default Controller Fallback -=========================== +Default Method Fallback +======================= .. versionadded:: 4.4.0 +If the controller method corresponding to the URI segment of the method name +does not exist, and if the default method is defined, the remaining URI segments +are passed to the default method for execution. + +.. literalinclude:: controllers/024.php + +Load the following URL:: + + example.com/index.php/product/15/edit + +The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): + +.. important:: If there are more parameters in the URI than the method parameters, + Auto Routing (Improved) does not execute the method, and it results in 404 + Not Found. + If the controller corresponding to the URI segment of the controller name does not exist, and if the default controller (``Home`` by default) exists in -the directory, the remaining URI segments are passed to the default controller -for execution. +the directory, the remaining URI segments are passed to the default controller's +default method. For example, when you have the following default controller ``Home`` in the **app/Controllers/News** directory: @@ -298,48 +314,19 @@ For example, when you have the following default controller ``Home`` in the Load the following URL:: - example.com/index.php/news/list + example.com/index.php/news/101 -The ``News\Home`` controller will be found, and the ``getList()`` method will be -executed. +The ``News\Home`` controller and the default ``getIndex()`` method will be found. +So the default method will be passed URI segments 2 (``'101'``): .. note:: If there is ``App\Controllers\News`` controller, it takes precedence. The URI segments are searched sequentially and the first controller found is used. -Load the following URL:: - - example.com/index.php/news/101 - -The default ``getIndex()`` method will be passed URI segments 2 (``'101'``): - .. note:: If there are more parameters in the URI than the method parameters, Auto Routing (Improved) does not execute the method, and it results in 404 Not Found. -.. _controller-default-method-fallback: - -Default Method Fallback -======================= - -.. versionadded:: 4.4.0 - -If the controller method corresponding to the URI segment of the method name -does not exist, and if the default method is defined, the remaining URI segments -are passed to the default method for execution. - -.. literalinclude:: controllers/024.php - -Load the following URL:: - - example.com/index.php/product/15/edit - -The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): - -.. important:: If there are more parameters in the URI than the method parameters, - Auto Routing (Improved) does not execute the method, and it results in 404 - Not Found. - Default Controller ================== diff --git a/user_guide_src/source/incoming/controllers/025.php b/user_guide_src/source/incoming/controllers/025.php index 2c88ca92ad13..732c6db94c01 100644 --- a/user_guide_src/source/incoming/controllers/025.php +++ b/user_guide_src/source/incoming/controllers/025.php @@ -6,11 +6,6 @@ class Home extends BaseController { - public function getList() - { - // ... - } - public function getIndex($id = null) { // ... From d96f0ad514fd3e6249aeb49eefa4ce17d5aaba3e Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 16:25:28 +0900 Subject: [PATCH 12/14] docs: move section up --- .../source/incoming/controllers.rst | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 0f3f8d518da4..03ef53c8e3cd 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -279,6 +279,40 @@ Your method will be passed URI segments 3 and 4 (``'sandals'`` and ``'123'``): .. literalinclude:: controllers/022.php +Default Controller +================== + +The Default Controller is a special controller that is used when a URI ends with +a directory name or when a URI is not present, as will be the case when only your +site root URL is requested. + +Defining a Default Controller +----------------------------- + +Let's try it with the ``Helloworld`` controller. + +To specify a default controller open your **app/Config/Routes.php** +file and set this variable: + +.. literalinclude:: controllers/015.php + +Where ``Helloworld`` is the name of the controller class you want to be used. + +A few lines further down **Routes.php** in the "Route Definitions" section, comment out the line: + +.. literalinclude:: controllers/016.php + +If you now browse to your site without specifying any URI segments you'll +see the "Hello World" message. + +.. important:: When you use Auto Routing (Improved), you must remove the line + ``$routes->get('/', 'Home::index');``. Because defined routes take + precedence over Auto Routing, and controllers defined in the defined routes + are denied access by Auto Routing (Improved) for security reasons. + +For more information, please refer to the :ref:`routes-configuration-options` section of the +:ref:`URI Routing ` documentation. + .. _controller-default-method-fallback: Default Method Fallback @@ -327,40 +361,6 @@ So the default method will be passed URI segments 2 (``'101'``): Auto Routing (Improved) does not execute the method, and it results in 404 Not Found. -Default Controller -================== - -The Default Controller is a special controller that is used when a URI ends with -a directory name or when a URI is not present, as will be the case when only your -site root URL is requested. - -Defining a Default Controller ------------------------------ - -Let's try it with the ``Helloworld`` controller. - -To specify a default controller open your **app/Config/Routes.php** -file and set this variable: - -.. literalinclude:: controllers/015.php - -Where ``Helloworld`` is the name of the controller class you want to be used. - -A few lines further down **Routes.php** in the "Route Definitions" section, comment out the line: - -.. literalinclude:: controllers/016.php - -If you now browse to your site without specifying any URI segments you'll -see the "Hello World" message. - -.. important:: When you use Auto Routing (Improved), you must remove the line - ``$routes->get('/', 'Home::index');``. Because defined routes take - precedence over Auto Routing, and controllers defined in the defined routes - are denied access by Auto Routing (Improved) for security reasons. - -For more information, please refer to the :ref:`routes-configuration-options` section of the -:ref:`URI Routing ` documentation. - Organizing Your Controllers into Sub-directories ================================================ From cc6b2d9bbbf8526f348561a66a973b2956e5d523 Mon Sep 17 00:00:00 2001 From: kenjis Date: Wed, 12 Apr 2023 16:28:16 +0900 Subject: [PATCH 13/14] docs: add section title --- user_guide_src/source/incoming/controllers.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user_guide_src/source/incoming/controllers.rst b/user_guide_src/source/incoming/controllers.rst index 03ef53c8e3cd..8bc9e90d581f 100644 --- a/user_guide_src/source/incoming/controllers.rst +++ b/user_guide_src/source/incoming/controllers.rst @@ -336,6 +336,9 @@ The method will be passed URI segments 2 and 3 (``'15'`` and ``'edit'``): Auto Routing (Improved) does not execute the method, and it results in 404 Not Found. +Fallback to Default Controller +------------------------------ + If the controller corresponding to the URI segment of the controller name does not exist, and if the default controller (``Home`` by default) exists in the directory, the remaining URI segments are passed to the default controller's From 15ee8d64b2fa78aef6547b49083c9eb9de013c01 Mon Sep 17 00:00:00 2001 From: kenjis Date: Thu, 13 Apr 2023 19:23:56 +0900 Subject: [PATCH 14/14] fix: Fatal error Cannot declare class CodeIgniter\Router\Controllers\Index --- system/Router/AutoRouterImproved.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/system/Router/AutoRouterImproved.php b/system/Router/AutoRouterImproved.php index eec6bcbd3374..0999f47eb26b 100644 --- a/system/Router/AutoRouterImproved.php +++ b/system/Router/AutoRouterImproved.php @@ -201,11 +201,12 @@ public function getRoute(string $uri): array // Check for Module Routes. if ( - ($routingConfig = config(Routing::class)) + $segments !== [] + && ($routingConfig = config(Routing::class)) && array_key_exists($segments[0], $routingConfig->moduleRoutes) ) { $uriSegment = array_shift($segments); - $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\') . '\\'; + $this->namespace = rtrim($routingConfig->moduleRoutes[$uriSegment], '\\'); } if ($this->searchFirstController($segments)) {