Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto routes listing #5590

Merged
merged 15 commits into from
Jan 27, 2022
Merged
16 changes: 13 additions & 3 deletions system/Commands/Utilities/Routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@

use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Commands\Utilities\Routes\AutoRouteCollector;
use Config\Services;

/**
* Lists all of the user-defined routes. This will include any Routes files
* that can be discovered, but will NOT include any routes that are not defined
* in a routes file, but are instead discovered through auto-routing.
* Lists all the routes. This will include any Routes files
* that can be discovered, and will include routes that are not defined
* in routes files, but are instead discovered through auto-routing.
*/
class Routes extends BaseCommand
{
Expand Down Expand Up @@ -101,6 +102,15 @@ public function run(array $params)
}
}

if ($collection->shouldAutoRoute()) {
$autoRouteCollector = new AutoRouteCollector(
$collection->getDefaultNamespace(),
$collection->getDefaultController(),
$collection->getDefaultMethod()
);
$tbody = [...$tbody, ...$autoRouteCollector->get()];
}

$thead = [
'Method',
'Route',
Expand Down
65 changes: 65 additions & 0 deletions system/Commands/Utilities/Routes/AutoRouteCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?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;

/**
* Collects data for auto route listing.
*/
final class AutoRouteCollector
{
/**
* @var string namespace to search
*/
private string $namespace;

private string $defaultController;
private string $defaultMethod;

/**
* @param string $namespace namespace to search
*/
public function __construct(string $namespace, string $defaultController, string $defaultMethod)
{
$this->namespace = $namespace;
$this->defaultController = $defaultController;
$this->defaultMethod = $defaultMethod;
}

/**
* @return list<list<string>>
*/
public function get(): array
{
$finder = new ControllerFinder($this->namespace);
$reader = new ControllerMethodReader($this->namespace);

$tbody = [];

foreach ($finder->find() as $class) {
$output = $reader->read(
$class,
$this->defaultController,
$this->defaultMethod
);

foreach ($output as $item) {
$tbody[] = [
'auto',
$item['route'],
$item['handler'],
];
}
}

return $tbody;
}
}
75 changes: 75 additions & 0 deletions system/Commands/Utilities/Routes/ControllerFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?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;

use CodeIgniter\Autoloader\FileLocator;
use CodeIgniter\Config\Services;

/**
* Finds all controllers in a namespace for auto route listing.
*/
final class ControllerFinder
{
/**
* @var string namespace to search
*/
private string $namespace;

private FileLocator $locator;

/**
* @param string $namespace namespace to search
*/
public function __construct(string $namespace)
{
$this->namespace = $namespace;
$this->locator = Services::locator();
}

/**
* @return class-string[]
*/
public function find(): array
{
$nsArray = explode('\\', trim($this->namespace, '\\'));
$count = count($nsArray);
$ns = '';

for ($i = 0; $i < $count; $i++) {
$ns .= '\\' . array_shift($nsArray);
$path = implode('\\', $nsArray);

$files = $this->locator->listNamespaceFiles($ns, $path);

if ($files !== []) {
break;
}
}

$classes = [];

foreach ($files as $file) {
if (\is_file($file)) {
$classnameOrEmpty = $this->locator->getClassname($file);

if ($classnameOrEmpty !== '') {
/** @var class-string $classname */
$classname = $classnameOrEmpty;

$classes[] = $classname;
}
}
}

return $classes;
}
}
175 changes: 175 additions & 0 deletions system/Commands/Utilities/Routes/ControllerMethodReader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?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;

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;

/**
* @param string $namespace the default namespace
*/
public function __construct(string $namespace)
{
$this->namespace = $namespace;
}

/**
* @param class-string $class
*
* @return list<array{route: string, handler: string}>
*/
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 = [];
$uriByClass = $this->getUriByClass($classname);

if ($this->hasRemap($reflection)) {
$methodName = '_remap';

$routeWithoutController = $this->getRouteWithoutController(
$classShortname,
$defaultController,
$uriByClass,
$classname,
$methodName
);
$output = [...$output, ...$routeWithoutController];

$output[] = [
'route' => $uriByClass . '[/...]',
'handler' => '\\' . $classname . '::' . $methodName,
];

return $output;
}

foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$methodName = $method->getName();

$route = $uriByClass . '/' . $methodName;

// Exclude BaseController and initController
// See system/Config/Routes.php
if (preg_match('#\AbaseController.*#', $route) === 1) {
continue;
}
if (preg_match('#.*/initController\z#', $route) === 1) {
continue;
}

if ($methodName === $defaultMethod) {
$routeWithoutController = $this->getRouteWithoutController(
$classShortname,
$defaultController,
$uriByClass,
$classname,
$methodName
);
$output = [...$output, ...$routeWithoutController];

$output[] = [
'route' => $uriByClass,
'handler' => '\\' . $classname . '::' . $methodName,
];
}

$output[] = [
'route' => $route . '[/...]',
'handler' => '\\' . $classname . '::' . $methodName,
];
}

return $output;
}

/**
* Whether the class has a _remap() method.
*/
private function hasRemap(ReflectionClass $class): bool
{
if ($class->hasMethod('_remap')) {
$remap = $class->getMethod('_remap');

return $remap->isPublic();
}

return false;
}

/**
* @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
): array {
$output = [];

if ($classShortname === $defaultController) {
$pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#';
$routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/');
$routeWithoutController = $routeWithoutController ?: '/';

$output[] = [
'route' => $routeWithoutController,
'handler' => '\\' . $classname . '::' . $methodName,
];
}

return $output;
}
}
2 changes: 1 addition & 1 deletion tests/_support/Controllers/Hello.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* the LICENSE file that was distributed with this source code.
*/

namespace App\Controllers;
namespace Tests\Support\Controllers;

use CodeIgniter\Controller;

Expand Down
36 changes: 36 additions & 0 deletions tests/_support/Controllers/Remap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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 Tests\Support\Controllers;

use CodeIgniter\Controller;

class Remap extends Controller
{
public function _remap($method, ...$params)
{
if ($method === 'xyz') {
return $this->abc();
}

return $this->index();
}

public function index()
{
return 'index';
}

public function abc()
{
return 'abc';
}
}
Loading