-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5953 from kenjis/feat-new-auto-router-show-routes
feat: new improved auto router `spark routes` command
- Loading branch information
Showing
11 changed files
with
620 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
system/Commands/Utilities/Routes/AutoRouterImproved/AutoRouteCollector.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
<?php | ||
|
||
/** | ||
* This file is part of CodeIgniter 4 framework. | ||
* | ||
* (c) CodeIgniter Foundation <admin@codeigniter.com> | ||
* | ||
* For the full copyright and license information, please view | ||
* the LICENSE file that was distributed with this source code. | ||
*/ | ||
|
||
namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved; | ||
|
||
use CodeIgniter\Commands\Utilities\Routes\ControllerFinder; | ||
use CodeIgniter\Commands\Utilities\Routes\FilterCollector; | ||
|
||
/** | ||
* Collects data for Auto Routing Improved. | ||
*/ | ||
final class AutoRouteCollector | ||
{ | ||
/** | ||
* @var string namespace to search | ||
*/ | ||
private string $namespace; | ||
|
||
private string $defaultController; | ||
private string $defaultMethod; | ||
private array $httpMethods; | ||
|
||
/** | ||
* List of controllers in Defined Routes that should not be accessed via Auto-Routing. | ||
* | ||
* @var class-string[] | ||
*/ | ||
private array $protectedControllers; | ||
|
||
/** | ||
* @param string $namespace namespace to search | ||
*/ | ||
public function __construct( | ||
string $namespace, | ||
string $defaultController, | ||
string $defaultMethod, | ||
array $httpMethods, | ||
array $protectedControllers | ||
) { | ||
$this->namespace = $namespace; | ||
$this->defaultController = $defaultController; | ||
$this->defaultMethod = $defaultMethod; | ||
$this->httpMethods = $httpMethods; | ||
$this->protectedControllers = $protectedControllers; | ||
} | ||
|
||
/** | ||
* @return array<int, array<int, string>> | ||
* @phpstan-return list<list<string>> | ||
*/ | ||
public function get(): array | ||
{ | ||
$finder = new ControllerFinder($this->namespace); | ||
$reader = new ControllerMethodReader($this->namespace, $this->httpMethods); | ||
|
||
$tbody = []; | ||
|
||
foreach ($finder->find() as $class) { | ||
// Exclude controllers in Defined Routes. | ||
if (in_array($class, $this->protectedControllers, true)) { | ||
continue; | ||
} | ||
|
||
$routes = $reader->read( | ||
$class, | ||
$this->defaultController, | ||
$this->defaultMethod | ||
); | ||
|
||
if ($routes === []) { | ||
continue; | ||
} | ||
|
||
$routes = $this->addFilters($routes); | ||
|
||
foreach ($routes as $item) { | ||
$tbody[] = [ | ||
strtoupper($item['method']) . '(auto)', | ||
$item['route'] . $item['route_params'], | ||
$item['handler'], | ||
$item['before'], | ||
$item['after'], | ||
]; | ||
} | ||
} | ||
|
||
return $tbody; | ||
} | ||
|
||
private function addFilters($routes) | ||
{ | ||
$filterCollector = new FilterCollector(true); | ||
|
||
foreach ($routes as &$route) { | ||
// Search filters for the URI with all params | ||
$sampleUri = $this->generateSampleUri($route); | ||
$filtersLongest = $filterCollector->get($route['method'], $route['route'] . $sampleUri); | ||
|
||
// Search filters for the URI without optional params | ||
$sampleUri = $this->generateSampleUri($route, false); | ||
$filtersShortest = $filterCollector->get($route['method'], $route['route'] . $sampleUri); | ||
|
||
// Get common array elements | ||
$filters['before'] = array_intersect($filtersLongest['before'], $filtersShortest['before']); | ||
$filters['after'] = array_intersect($filtersLongest['after'], $filtersShortest['after']); | ||
|
||
$route['before'] = implode(' ', array_map('class_basename', $filters['before'])); | ||
$route['after'] = implode(' ', array_map('class_basename', $filters['after'])); | ||
} | ||
|
||
return $routes; | ||
} | ||
|
||
private function generateSampleUri(array $route, bool $longest = true): string | ||
{ | ||
$sampleUri = ''; | ||
|
||
if (isset($route['params'])) { | ||
$i = 1; | ||
|
||
foreach ($route['params'] as $required) { | ||
if ($longest && ! $required) { | ||
$sampleUri .= '/' . $i++; | ||
} | ||
} | ||
} | ||
|
||
return $sampleUri; | ||
} | ||
} |
182 changes: 182 additions & 0 deletions
182
system/Commands/Utilities/Routes/AutoRouterImproved/ControllerMethodReader.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
<?php | ||
|
||
/** | ||
* This file is part of CodeIgniter 4 framework. | ||
* | ||
* (c) CodeIgniter Foundation <admin@codeigniter.com> | ||
* | ||
* For the full copyright and license information, please view | ||
* the LICENSE file that was distributed with this source code. | ||
*/ | ||
|
||
namespace CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved; | ||
|
||
use ReflectionClass; | ||
use ReflectionMethod; | ||
|
||
/** | ||
* Reads a controller and returns a list of auto route listing. | ||
*/ | ||
final class ControllerMethodReader | ||
{ | ||
/** | ||
* @var string the default namespace | ||
*/ | ||
private string $namespace; | ||
|
||
private array $httpMethods; | ||
|
||
/** | ||
* @param string $namespace the default namespace | ||
*/ | ||
public function __construct(string $namespace, array $httpMethods) | ||
{ | ||
$this->namespace = $namespace; | ||
$this->httpMethods = $httpMethods; | ||
} | ||
|
||
/** | ||
* Returns found route info in the controller. | ||
* | ||
* @phpstan-param class-string $class | ||
* | ||
* @return array<int, array<string, array|string>> | ||
* @phpstan-return list<array<string, string|array>> | ||
*/ | ||
public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array | ||
{ | ||
$reflection = new ReflectionClass($class); | ||
|
||
if ($reflection->isAbstract()) { | ||
return []; | ||
} | ||
|
||
$classname = $reflection->getName(); | ||
$classShortname = $reflection->getShortName(); | ||
|
||
$output = []; | ||
$classInUri = $this->getUriByClass($classname); | ||
|
||
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { | ||
$methodName = $method->getName(); | ||
|
||
foreach ($this->httpMethods as $httpVerb) { | ||
if (strpos($methodName, $httpVerb) === 0) { | ||
// Remove HTTP verb prefix. | ||
$methodInUri = lcfirst(substr($methodName, strlen($httpVerb))); | ||
|
||
if ($methodInUri === $defaultMethod) { | ||
$routeWithoutController = $this->getRouteWithoutController( | ||
$classShortname, | ||
$defaultController, | ||
$classInUri, | ||
$classname, | ||
$methodName, | ||
$httpVerb | ||
); | ||
|
||
if ($routeWithoutController !== []) { | ||
$output = [...$output, ...$routeWithoutController]; | ||
|
||
continue; | ||
} | ||
|
||
// Route for the default method. | ||
$output[] = [ | ||
'method' => $httpVerb, | ||
'route' => $classInUri, | ||
'route_params' => '', | ||
'handler' => '\\' . $classname . '::' . $methodName, | ||
'params' => [], | ||
]; | ||
|
||
continue; | ||
} | ||
|
||
$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; | ||
} | ||
|
||
$output[] = [ | ||
'method' => $httpVerb, | ||
'route' => $route, | ||
'route_params' => $routeParams, | ||
'handler' => '\\' . $classname . '::' . $methodName, | ||
'params' => $params, | ||
]; | ||
} | ||
} | ||
} | ||
|
||
return $output; | ||
} | ||
|
||
/** | ||
* @phpstan-param class-string $classname | ||
* | ||
* @return string URI path part from the folder(s) and controller | ||
*/ | ||
private function getUriByClass(string $classname): string | ||
{ | ||
// remove the namespace | ||
$pattern = '/' . preg_quote($this->namespace, '/') . '/'; | ||
$class = ltrim(preg_replace($pattern, '', $classname), '\\'); | ||
|
||
$classParts = explode('\\', $class); | ||
$classPath = ''; | ||
|
||
foreach ($classParts as $part) { | ||
// make the first letter lowercase, because auto routing makes | ||
// the URI path's first letter uppercase and search the controller | ||
$classPath .= lcfirst($part) . '/'; | ||
} | ||
|
||
return rtrim($classPath, '/'); | ||
} | ||
|
||
/** | ||
* Gets a route without default controller. | ||
*/ | ||
private function getRouteWithoutController( | ||
string $classShortname, | ||
string $defaultController, | ||
string $uriByClass, | ||
string $classname, | ||
string $methodName, | ||
string $httpVerb | ||
): array { | ||
$output = []; | ||
|
||
if ($classShortname === $defaultController) { | ||
$pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#'; | ||
$routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/'); | ||
$routeWithoutController = $routeWithoutController ?: '/'; | ||
|
||
$output[] = [ | ||
'method' => $httpVerb, | ||
'route' => $routeWithoutController, | ||
'route_params' => '', | ||
'handler' => '\\' . $classname . '::' . $methodName, | ||
'params' => [], | ||
]; | ||
} | ||
|
||
return $output; | ||
} | ||
} |
Oops, something went wrong.