Skip to content

Commit 1807ede

Browse files
authored
Merge pull request #5953 from kenjis/feat-new-auto-router-show-routes
feat: new improved auto router `spark routes` command
2 parents 45e8278 + 5194766 commit 1807ede

11 files changed

Lines changed: 620 additions & 38 deletions

File tree

app/Config/Routes.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
$routes->setDefaultMethod('index');
2222
$routes->setTranslateURIDashes(false);
2323
$routes->set404Override();
24-
// The auto-routing is very dangerous. It is easy to create vulnerable apps
24+
// The Auto Routing (Legacy) is very dangerous. It is easy to create vulnerable apps
2525
// where controller filters or CSRF protection are bypassed.
26-
// It is recommended that you do not set it to `true`.
26+
// If you don't want to define all routes, please use the Auto Routing (Improved).
27+
// Set `$autoRoutesImproved` to true in `app/Config/Feature.php` and set the following to true.
2728
//$routes->setAutoRoute(false);
2829

2930
/*

system/Commands/Utilities/Routes.php

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use CodeIgniter\CLI\BaseCommand;
1616
use CodeIgniter\CLI\CLI;
1717
use CodeIgniter\Commands\Utilities\Routes\AutoRouteCollector;
18+
use CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollector as AutoRouteCollectorImproved;
1819
use CodeIgniter\Commands\Utilities\Routes\FilterCollector;
1920
use CodeIgniter\Commands\Utilities\Routes\SampleURIGenerator;
2021
use Config\Services;
@@ -112,19 +113,34 @@ public function run(array $params)
112113
}
113114

114115
if ($collection->shouldAutoRoute()) {
115-
$autoRouteCollector = new AutoRouteCollector(
116-
$collection->getDefaultNamespace(),
117-
$collection->getDefaultController(),
118-
$collection->getDefaultMethod()
119-
);
120-
$autoRoutes = $autoRouteCollector->get();
121-
122-
foreach ($autoRoutes as &$routes) {
123-
// There is no `auto` method, but it is intentional not to get route filters.
124-
$filters = $filterCollector->get('auto', $uriGenerator->get($routes[1]));
125-
126-
$routes[] = implode(' ', array_map('class_basename', $filters['before']));
127-
$routes[] = implode(' ', array_map('class_basename', $filters['after']));
116+
$autoRoutesImproved = config('Feature')->autoRoutesImproved ?? false;
117+
118+
if ($autoRoutesImproved) {
119+
$autoRouteCollector = new AutoRouteCollectorImproved(
120+
$collection->getDefaultNamespace(),
121+
$collection->getDefaultController(),
122+
$collection->getDefaultMethod(),
123+
$methods,
124+
$collection->getRegisteredControllers('*')
125+
);
126+
127+
$autoRoutes = $autoRouteCollector->get();
128+
} else {
129+
$autoRouteCollector = new AutoRouteCollector(
130+
$collection->getDefaultNamespace(),
131+
$collection->getDefaultController(),
132+
$collection->getDefaultMethod()
133+
);
134+
135+
$autoRoutes = $autoRouteCollector->get();
136+
137+
foreach ($autoRoutes as &$routes) {
138+
// There is no `auto` method, but it is intentional not to get route filters.
139+
$filters = $filterCollector->get('auto', $uriGenerator->get($routes[1]));
140+
141+
$routes[] = implode(' ', array_map('class_basename', $filters['before']));
142+
$routes[] = implode(' ', array_map('class_basename', $filters['after']));
143+
}
128144
}
129145

130146
$tbody = [...$tbody, ...$autoRoutes];
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved;
13+
14+
use CodeIgniter\Commands\Utilities\Routes\ControllerFinder;
15+
use CodeIgniter\Commands\Utilities\Routes\FilterCollector;
16+
17+
/**
18+
* Collects data for Auto Routing Improved.
19+
*/
20+
final class AutoRouteCollector
21+
{
22+
/**
23+
* @var string namespace to search
24+
*/
25+
private string $namespace;
26+
27+
private string $defaultController;
28+
private string $defaultMethod;
29+
private array $httpMethods;
30+
31+
/**
32+
* List of controllers in Defined Routes that should not be accessed via Auto-Routing.
33+
*
34+
* @var class-string[]
35+
*/
36+
private array $protectedControllers;
37+
38+
/**
39+
* @param string $namespace namespace to search
40+
*/
41+
public function __construct(
42+
string $namespace,
43+
string $defaultController,
44+
string $defaultMethod,
45+
array $httpMethods,
46+
array $protectedControllers
47+
) {
48+
$this->namespace = $namespace;
49+
$this->defaultController = $defaultController;
50+
$this->defaultMethod = $defaultMethod;
51+
$this->httpMethods = $httpMethods;
52+
$this->protectedControllers = $protectedControllers;
53+
}
54+
55+
/**
56+
* @return array<int, array<int, string>>
57+
* @phpstan-return list<list<string>>
58+
*/
59+
public function get(): array
60+
{
61+
$finder = new ControllerFinder($this->namespace);
62+
$reader = new ControllerMethodReader($this->namespace, $this->httpMethods);
63+
64+
$tbody = [];
65+
66+
foreach ($finder->find() as $class) {
67+
// Exclude controllers in Defined Routes.
68+
if (in_array($class, $this->protectedControllers, true)) {
69+
continue;
70+
}
71+
72+
$routes = $reader->read(
73+
$class,
74+
$this->defaultController,
75+
$this->defaultMethod
76+
);
77+
78+
if ($routes === []) {
79+
continue;
80+
}
81+
82+
$routes = $this->addFilters($routes);
83+
84+
foreach ($routes as $item) {
85+
$tbody[] = [
86+
strtoupper($item['method']) . '(auto)',
87+
$item['route'] . $item['route_params'],
88+
$item['handler'],
89+
$item['before'],
90+
$item['after'],
91+
];
92+
}
93+
}
94+
95+
return $tbody;
96+
}
97+
98+
private function addFilters($routes)
99+
{
100+
$filterCollector = new FilterCollector(true);
101+
102+
foreach ($routes as &$route) {
103+
// Search filters for the URI with all params
104+
$sampleUri = $this->generateSampleUri($route);
105+
$filtersLongest = $filterCollector->get($route['method'], $route['route'] . $sampleUri);
106+
107+
// Search filters for the URI without optional params
108+
$sampleUri = $this->generateSampleUri($route, false);
109+
$filtersShortest = $filterCollector->get($route['method'], $route['route'] . $sampleUri);
110+
111+
// Get common array elements
112+
$filters['before'] = array_intersect($filtersLongest['before'], $filtersShortest['before']);
113+
$filters['after'] = array_intersect($filtersLongest['after'], $filtersShortest['after']);
114+
115+
$route['before'] = implode(' ', array_map('class_basename', $filters['before']));
116+
$route['after'] = implode(' ', array_map('class_basename', $filters['after']));
117+
}
118+
119+
return $routes;
120+
}
121+
122+
private function generateSampleUri(array $route, bool $longest = true): string
123+
{
124+
$sampleUri = '';
125+
126+
if (isset($route['params'])) {
127+
$i = 1;
128+
129+
foreach ($route['params'] as $required) {
130+
if ($longest && ! $required) {
131+
$sampleUri .= '/' . $i++;
132+
}
133+
}
134+
}
135+
136+
return $sampleUri;
137+
}
138+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved;
13+
14+
use ReflectionClass;
15+
use ReflectionMethod;
16+
17+
/**
18+
* Reads a controller and returns a list of auto route listing.
19+
*/
20+
final class ControllerMethodReader
21+
{
22+
/**
23+
* @var string the default namespace
24+
*/
25+
private string $namespace;
26+
27+
private array $httpMethods;
28+
29+
/**
30+
* @param string $namespace the default namespace
31+
*/
32+
public function __construct(string $namespace, array $httpMethods)
33+
{
34+
$this->namespace = $namespace;
35+
$this->httpMethods = $httpMethods;
36+
}
37+
38+
/**
39+
* Returns found route info in the controller.
40+
*
41+
* @phpstan-param class-string $class
42+
*
43+
* @return array<int, array<string, array|string>>
44+
* @phpstan-return list<array<string, string|array>>
45+
*/
46+
public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array
47+
{
48+
$reflection = new ReflectionClass($class);
49+
50+
if ($reflection->isAbstract()) {
51+
return [];
52+
}
53+
54+
$classname = $reflection->getName();
55+
$classShortname = $reflection->getShortName();
56+
57+
$output = [];
58+
$classInUri = $this->getUriByClass($classname);
59+
60+
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
61+
$methodName = $method->getName();
62+
63+
foreach ($this->httpMethods as $httpVerb) {
64+
if (strpos($methodName, $httpVerb) === 0) {
65+
// Remove HTTP verb prefix.
66+
$methodInUri = lcfirst(substr($methodName, strlen($httpVerb)));
67+
68+
if ($methodInUri === $defaultMethod) {
69+
$routeWithoutController = $this->getRouteWithoutController(
70+
$classShortname,
71+
$defaultController,
72+
$classInUri,
73+
$classname,
74+
$methodName,
75+
$httpVerb
76+
);
77+
78+
if ($routeWithoutController !== []) {
79+
$output = [...$output, ...$routeWithoutController];
80+
81+
continue;
82+
}
83+
84+
// Route for the default method.
85+
$output[] = [
86+
'method' => $httpVerb,
87+
'route' => $classInUri,
88+
'route_params' => '',
89+
'handler' => '\\' . $classname . '::' . $methodName,
90+
'params' => [],
91+
];
92+
93+
continue;
94+
}
95+
96+
$route = $classInUri . '/' . $methodInUri;
97+
98+
$params = [];
99+
$routeParams = '';
100+
$refParams = $method->getParameters();
101+
102+
foreach ($refParams as $param) {
103+
$required = true;
104+
if ($param->isOptional()) {
105+
$required = false;
106+
107+
$routeParams .= '[/..]';
108+
} else {
109+
$routeParams .= '/..';
110+
}
111+
112+
// [variable_name => required?]
113+
$params[$param->getName()] = $required;
114+
}
115+
116+
$output[] = [
117+
'method' => $httpVerb,
118+
'route' => $route,
119+
'route_params' => $routeParams,
120+
'handler' => '\\' . $classname . '::' . $methodName,
121+
'params' => $params,
122+
];
123+
}
124+
}
125+
}
126+
127+
return $output;
128+
}
129+
130+
/**
131+
* @phpstan-param class-string $classname
132+
*
133+
* @return string URI path part from the folder(s) and controller
134+
*/
135+
private function getUriByClass(string $classname): string
136+
{
137+
// remove the namespace
138+
$pattern = '/' . preg_quote($this->namespace, '/') . '/';
139+
$class = ltrim(preg_replace($pattern, '', $classname), '\\');
140+
141+
$classParts = explode('\\', $class);
142+
$classPath = '';
143+
144+
foreach ($classParts as $part) {
145+
// make the first letter lowercase, because auto routing makes
146+
// the URI path's first letter uppercase and search the controller
147+
$classPath .= lcfirst($part) . '/';
148+
}
149+
150+
return rtrim($classPath, '/');
151+
}
152+
153+
/**
154+
* Gets a route without default controller.
155+
*/
156+
private function getRouteWithoutController(
157+
string $classShortname,
158+
string $defaultController,
159+
string $uriByClass,
160+
string $classname,
161+
string $methodName,
162+
string $httpVerb
163+
): array {
164+
$output = [];
165+
166+
if ($classShortname === $defaultController) {
167+
$pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#';
168+
$routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/');
169+
$routeWithoutController = $routeWithoutController ?: '/';
170+
171+
$output[] = [
172+
'method' => $httpVerb,
173+
'route' => $routeWithoutController,
174+
'route_params' => '',
175+
'handler' => '\\' . $classname . '::' . $methodName,
176+
'params' => [],
177+
];
178+
}
179+
180+
return $output;
181+
}
182+
}

0 commit comments

Comments
 (0)