first commit

This commit is contained in:
2024-09-06 13:32:15 -04:00
commit 700e8a2948
2013 changed files with 447887 additions and 0 deletions
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* 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.
*
* @see \CodeIgniter\Commands\Utilities\Routes\AutoRouteCollectorTest
*/
final class AutoRouteCollector
{
/**
* @param string $namespace namespace to search
*/
public function __construct(private readonly string $namespace, private readonly string $defaultController, private readonly string $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;
}
}
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
/**
* 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.
*
* @see \CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollectorTest
*/
final class AutoRouteCollector
{
/**
* @param string $namespace namespace to search
* @param list<class-string> $protectedControllers List of controllers in Defined
* Routes that should not be accessed via Auto-Routing.
* @param list<string> $httpMethods
* @param string $prefix URI prefix for Module Routing
*/
public function __construct(
private readonly string $namespace,
private readonly string $defaultController,
private readonly string $defaultMethod,
private readonly array $httpMethods,
private readonly array $protectedControllers,
private string $prefix = ''
) {
}
/**
* @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) {
$route = $item['route'] . $item['route_params'];
// For module routing
if ($this->prefix !== '' && $route === '/') {
$route = $this->prefix;
} elseif ($this->prefix !== '') {
$route = $this->prefix . '/' . $route;
}
$tbody[] = [
strtoupper($item['method']) . '(auto)',
$route,
'',
$item['handler'],
$item['before'],
$item['after'],
];
}
}
return $tbody;
}
/**
* Adding Filters
*
* @param list<array<string, array|string>> $routes
*
* @return list<array<string, array|string>>
*/
private function addFilters($routes)
{
$filterCollector = new FilterCollector(true);
foreach ($routes as &$route) {
$routePath = $route['route'];
// For module routing
if ($this->prefix !== '' && $route === '/') {
$routePath = $this->prefix;
} elseif ($this->prefix !== '') {
$routePath = $this->prefix . '/' . $routePath;
}
// Search filters for the URI with all params
$sampleUri = $this->generateSampleUri($route);
$filtersLongest = $filterCollector->get($route['method'], $routePath . $sampleUri);
// Search filters for the URI without optional params
$sampleUri = $this->generateSampleUri($route, false);
$filtersShortest = $filterCollector->get($route['method'], $routePath . $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;
}
}
@@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
/**
* 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 Config\Routing;
use ReflectionClass;
use ReflectionMethod;
/**
* Reads a controller and returns a list of auto route listing.
*
* @see \CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\ControllerMethodReaderTest
*/
final class ControllerMethodReader
{
private readonly bool $translateURIDashes;
private readonly bool $translateUriToCamelCase;
/**
* @param string $namespace the default namespace
* @param list<string> $httpMethods
*/
public function __construct(
private readonly string $namespace,
private readonly array $httpMethods
) {
$config = config(Routing::class);
$this->translateURIDashes = $config->translateURIDashes;
$this->translateUriToCamelCase = $config->translateUriToCamelCase;
}
/**
* Returns found route info in the controller.
*
* @param class-string $class
*
* @return list<array<string, array|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 = [];
$classInUri = $this->convertClassNameToUri($classname);
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$methodName = $method->getName();
foreach ($this->httpMethods as $httpVerb) {
if (str_starts_with($methodName, strtolower($httpVerb))) {
// Remove HTTP verb prefix.
$methodInUri = $this->convertMethodNameToUri($httpVerb, $methodName);
// Check if it is the default method.
if ($methodInUri === $defaultMethod) {
$routeForDefaultController = $this->getRouteForDefaultController(
$classShortname,
$defaultController,
$classInUri,
$classname,
$methodName,
$httpVerb,
$method
);
if ($routeForDefaultController !== []) {
// The controller is the default controller. It only
// has a route for the default method. Other methods
// will not be routed even if they exist.
$output = [...$output, ...$routeForDefaultController];
continue;
}
[$params, $routeParams] = $this->getParameters($method);
// Route for the default method.
$output[] = [
'method' => $httpVerb,
'route' => $classInUri,
'route_params' => $routeParams,
'handler' => '\\' . $classname . '::' . $methodName,
'params' => $params,
];
continue;
}
$route = $classInUri . '/' . $methodInUri;
[$params, $routeParams] = $this->getParameters($method);
// If it is the default controller, the method will not be
// routed.
if ($classShortname === $defaultController) {
$route = 'x ' . $route;
}
$output[] = [
'method' => $httpVerb,
'route' => $route,
'route_params' => $routeParams,
'handler' => '\\' . $classname . '::' . $methodName,
'params' => $params,
];
}
}
}
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];
}
/**
* @param class-string $classname
*
* @return string URI path part from the folder(s) and controller
*/
private function convertClassNameToUri(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) . '/';
}
$classUri = rtrim($classPath, '/');
return $this->translateToUri($classUri);
}
/**
* @return string URI path part from the method
*/
private function convertMethodNameToUri(string $httpVerb, string $methodName): string
{
$methodUri = lcfirst(substr($methodName, strlen($httpVerb)));
return $this->translateToUri($methodUri);
}
/**
* @param string $string classname or method name
*/
private function translateToUri(string $string): string
{
if ($this->translateUriToCamelCase) {
$string = strtolower(
preg_replace('/([a-z\d])([A-Z])/', '$1-$2', $string)
);
} elseif ($this->translateURIDashes) {
$string = str_replace('_', '-', $string);
}
return $string;
}
/**
* Gets a route for the default controller.
*
* @return list<array>
*/
private function getRouteForDefaultController(
string $classShortname,
string $defaultController,
string $uriByClass,
string $classname,
string $methodName,
string $httpVerb,
ReflectionMethod $method
): array {
$output = [];
if ($classShortname === $defaultController) {
$pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#';
$routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/');
$routeWithoutController = $routeWithoutController ?: '/';
[$params, $routeParams] = $this->getParameters($method);
if ($routeWithoutController === '/' && $routeParams !== '') {
$routeWithoutController = '';
}
$output[] = [
'method' => $httpVerb,
'route' => $routeWithoutController,
'route_params' => $routeParams,
'handler' => '\\' . $classname . '::' . $methodName,
'params' => $params,
];
}
return $output;
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* 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\FileLocatorInterface;
/**
* Finds all controllers in a namespace for auto route listing.
*
* @see \CodeIgniter\Commands\Utilities\Routes\ControllerFinderTest
*/
final class ControllerFinder
{
private readonly FileLocatorInterface $locator;
/**
* @param string $namespace namespace to search
*/
public function __construct(
private readonly string $namespace
) {
$this->locator = service('locator');
}
/**
* @return list<class-string>
*/
public function find(): array
{
$nsArray = explode('\\', trim($this->namespace, '\\'));
$count = count($nsArray);
$ns = '';
$files = [];
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;
}
}
@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
/**
* 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.
*
* @see \CodeIgniter\Commands\Utilities\Routes\ControllerMethodReaderTest
*/
final class ControllerMethodReader
{
/**
* @param string $namespace the default namespace
*/
public function __construct(private readonly string $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 {
if ($classShortname !== $defaultController) {
return [];
}
$pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#';
$routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/');
$routeWithoutController = $routeWithoutController ?: '/';
return [[
'route' => $routeWithoutController,
'handler' => '\\' . $classname . '::' . $methodName,
]];
}
}
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
/**
* 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\Config\Services;
use CodeIgniter\Filters\Filters;
use CodeIgniter\HTTP\Method;
use CodeIgniter\HTTP\Request;
use CodeIgniter\Router\Router;
use Config\Filters as FiltersConfig;
/**
* Collects filters for a route.
*
* @see \CodeIgniter\Commands\Utilities\Routes\FilterCollectorTest
*/
final class FilterCollector
{
public function __construct(
/**
* Whether to reset Defined Routes.
*
* If set to true, route filters are not found.
*/
private readonly bool $resetRoutes = false
) {
}
/**
* Returns filters for the URI
*
* @param string $method HTTP verb like `GET`,`POST` or `CLI`.
* @param string $uri URI path to find filters for
*
* @return array{before: list<string>, after: list<string>} array of filter alias or classname
*/
public function get(string $method, string $uri): array
{
if ($method === strtolower($method)) {
@trigger_error(
'Passing lowercase HTTP method "' . $method . '" is deprecated.'
. ' Use uppercase HTTP method like "' . strtoupper($method) . '".',
E_USER_DEPRECATED
);
}
/**
* @deprecated 4.5.0
* @TODO Remove this in the future.
*/
$method = strtoupper($method);
if ($method === 'CLI') {
return [
'before' => [],
'after' => [],
];
}
$request = Services::incomingrequest(null, false);
$request->setMethod($method);
$router = $this->createRouter($request);
$filters = $this->createFilters($request);
$finder = new FilterFinder($router, $filters);
return $finder->find($uri);
}
/**
* Returns Required Filters
*
* @return array{before: list<string>, after: list<string>} array of filter alias or classname
*/
public function getRequiredFilters(): array
{
$request = Services::incomingrequest(null, false);
$request->setMethod(Method::GET);
$router = $this->createRouter($request);
$filters = $this->createFilters($request);
$finder = new FilterFinder($router, $filters);
return $finder->getRequiredFilters();
}
private function createRouter(Request $request): Router
{
$routes = service('routes');
if ($this->resetRoutes) {
$routes->resetRoutes();
}
return new Router($routes, $request);
}
private function createFilters(Request $request): Filters
{
$config = config(FiltersConfig::class);
return new Filters($config, $request, service('response'));
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* 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\Exceptions\PageNotFoundException;
use CodeIgniter\Filters\Filters;
use CodeIgniter\HTTP\Exceptions\BadRequestException;
use CodeIgniter\HTTP\Exceptions\RedirectException;
use CodeIgniter\Router\Router;
use Config\Feature;
/**
* Finds filters.
*
* @see \CodeIgniter\Commands\Utilities\Routes\FilterFinderTest
*/
final class FilterFinder
{
private readonly Router $router;
private readonly Filters $filters;
public function __construct(?Router $router = null, ?Filters $filters = null)
{
$this->router = $router ?? service('router');
$this->filters = $filters ?? service('filters');
}
private function getRouteFilters(string $uri): array
{
$this->router->handle($uri);
return $this->router->getFilters();
}
/**
* @param string $uri URI path to find filters for
*
* @return array{before: list<string>, after: list<string>} array of filter alias or classname
*/
public function find(string $uri): array
{
$this->filters->reset();
// Add route filters
try {
$routeFilters = $this->getRouteFilters($uri);
$this->filters->enableFilters($routeFilters, 'before');
$oldFilterOrder = config(Feature::class)->oldFilterOrder ?? false;
if (! $oldFilterOrder) {
$routeFilters = array_reverse($routeFilters);
}
$this->filters->enableFilters($routeFilters, 'after');
$this->filters->initialize($uri);
return $this->filters->getFilters();
} catch (RedirectException) {
return [
'before' => [],
'after' => [],
];
} catch (BadRequestException|PageNotFoundException) {
return [
'before' => ['<unknown>'],
'after' => ['<unknown>'],
];
}
}
/**
* Returns Required Filters
*
* @return array{before: list<string>, after:list<string>}
*/
public function getRequiredFilters(): array
{
[$requiredBefore] = $this->filters->getRequiredFilters('before');
[$requiredAfter] = $this->filters->getRequiredFilters('after');
return [
'before' => $requiredBefore,
'after' => $requiredAfter,
];
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* 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\Router\RouteCollection;
use Config\App;
/**
* Generate a sample URI path from route key regex.
*
* @see \CodeIgniter\Commands\Utilities\Routes\SampleURIGeneratorTest
*/
final class SampleURIGenerator
{
private readonly RouteCollection $routes;
/**
* Sample URI path for placeholder.
*
* @var array<string, string>
*/
private array $samples = [
'any' => '123/abc',
'segment' => 'abc_123',
'alphanum' => 'abc123',
'num' => '123',
'alpha' => 'abc',
'hash' => 'abc_123',
];
public function __construct(?RouteCollection $routes = null)
{
$this->routes = $routes ?? service('routes');
}
/**
* @param string $routeKey route key regex
*
* @return string sample URI path
*/
public function get(string $routeKey): string
{
$sampleUri = $routeKey;
if (str_contains($routeKey, '{locale}')) {
$sampleUri = str_replace(
'{locale}',
config(App::class)->defaultLocale,
$routeKey
);
}
foreach ($this->routes->getPlaceholders() as $placeholder => $regex) {
$sample = $this->samples[$placeholder] ?? '::unknown::';
$sampleUri = str_replace('(' . $regex . ')', $sample, $sampleUri);
}
// auto route
return str_replace('[/...]', '/1/2/3/4/5', $sampleUri);
}
}