Skip to content

Commit

Permalink
Merge pull request #5953 from kenjis/feat-new-auto-router-show-routes
Browse files Browse the repository at this point in the history
feat: new improved auto router `spark routes` command
  • Loading branch information
kenjis committed May 7, 2022
2 parents 45e8278 + 5194766 commit 1807ede
Show file tree
Hide file tree
Showing 11 changed files with 620 additions and 38 deletions.
5 changes: 3 additions & 2 deletions app/Config/Routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
$routes->setDefaultMethod('index');
$routes->setTranslateURIDashes(false);
$routes->set404Override();
// The auto-routing is very dangerous. It is easy to create vulnerable apps
// The Auto Routing (Legacy) is very dangerous. It is easy to create vulnerable apps
// where controller filters or CSRF protection are bypassed.
// It is recommended that you do not set it to `true`.
// If you don't want to define all routes, please use the Auto Routing (Improved).
// Set `$autoRoutesImproved` to true in `app/Config/Feature.php` and set the following to true.
//$routes->setAutoRoute(false);

/*
Expand Down
42 changes: 29 additions & 13 deletions system/Commands/Utilities/Routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Commands\Utilities\Routes\AutoRouteCollector;
use CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollector as AutoRouteCollectorImproved;
use CodeIgniter\Commands\Utilities\Routes\FilterCollector;
use CodeIgniter\Commands\Utilities\Routes\SampleURIGenerator;
use Config\Services;
Expand Down Expand Up @@ -112,19 +113,34 @@ public function run(array $params)
}

if ($collection->shouldAutoRoute()) {
$autoRouteCollector = new AutoRouteCollector(
$collection->getDefaultNamespace(),
$collection->getDefaultController(),
$collection->getDefaultMethod()
);
$autoRoutes = $autoRouteCollector->get();

foreach ($autoRoutes as &$routes) {
// There is no `auto` method, but it is intentional not to get route filters.
$filters = $filterCollector->get('auto', $uriGenerator->get($routes[1]));

$routes[] = implode(' ', array_map('class_basename', $filters['before']));
$routes[] = implode(' ', array_map('class_basename', $filters['after']));
$autoRoutesImproved = config('Feature')->autoRoutesImproved ?? false;

if ($autoRoutesImproved) {
$autoRouteCollector = new AutoRouteCollectorImproved(
$collection->getDefaultNamespace(),
$collection->getDefaultController(),
$collection->getDefaultMethod(),
$methods,
$collection->getRegisteredControllers('*')
);

$autoRoutes = $autoRouteCollector->get();
} else {
$autoRouteCollector = new AutoRouteCollector(
$collection->getDefaultNamespace(),
$collection->getDefaultController(),
$collection->getDefaultMethod()
);

$autoRoutes = $autoRouteCollector->get();

foreach ($autoRoutes as &$routes) {
// There is no `auto` method, but it is intentional not to get route filters.
$filters = $filterCollector->get('auto', $uriGenerator->get($routes[1]));

$routes[] = implode(' ', array_map('class_basename', $filters['before']));
$routes[] = implode(' ', array_map('class_basename', $filters['after']));
}
}

$tbody = [...$tbody, ...$autoRoutes];
Expand Down
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;
}
}
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;
}
}
Loading

0 comments on commit 1807ede

Please sign in to comment.