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

refactor: [Router] extract a class for auto-routing #5877

Merged
merged 18 commits into from
Apr 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion phpstan-baseline.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -715,14 +715,19 @@ parameters:
count: 1
path: system/Router/Router.php

-
message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRegisteredControllers\\(\\)\\.$#"
count: 1
path: system/Router/Router.php

-
message: "#^Expression on left side of \\?\\? is not nullable\\.$#"
count: 1
path: system/Router/Router.php

-
message: "#^Method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutes\\(\\) invoked with 1 parameter, 0 required\\.$#"
count: 2
count: 1
path: system/Router/Router.php

-
Expand Down
2 changes: 2 additions & 0 deletions system/CodeIgniter.php
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,8 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
/**
* Determines the path to use for us to try to route to, based
* on user input (setPath), or the CLI/IncomingRequest path.
*
* @return string
*/
protected function determinePath()
{
Expand Down
285 changes: 285 additions & 0 deletions system/Router/AutoRouter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
<?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\Router;

use CodeIgniter\Exceptions\PageNotFoundException;

/**
* Router for Auto-Routing
*/
class AutoRouter
{
/**
* List of controllers registered for the CLI verb that should not be accessed in the web.
*/
protected array $protectedControllers;

/**
* Sub-directory that contains the requested controller class.
* Primarily used by 'autoRoute'.
*/
protected ?string $directory = null;

/**
* The name of the controller class.
*/
protected string $controller;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
protected string $controller;
private string $controller;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about other properties? And why do you recommend private for this?


/**
* The name of the method to use.
*/
protected string $method;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
protected string $method;
protected private $method;


/**
* Whether dashes in URI's should be converted
* to underscores when determining method names.
*/
protected bool $translateURIDashes;

/**
* HTTP verb for the request.
*/
protected string $httpVerb;

/**
* Default namespace for controllers.
*/
protected string $defaultNamespace;
kenjis marked this conversation as resolved.
Show resolved Hide resolved

public function __construct(
array $protectedControllers,
string $defaultNamespace,
string $defaultController,
string $defaultMethod,
bool $translateURIDashes,
string $httpVerb
) {
$this->protectedControllers = $protectedControllers;
$this->defaultNamespace = $defaultNamespace;
$this->translateURIDashes = $translateURIDashes;
$this->httpVerb = $httpVerb;

$this->controller = $defaultController;
$this->method = $defaultMethod;
}

/**
* Attempts to match a URI path against Controllers and directories
* found in APPPATH/Controllers, to find a matching route.
*
* @return array [directory_name, controller_name, controller_method, params]
*/
public function getRoute(string $uri): array
{
$segments = explode('/', $uri);

// WARNING: Directories get shifted out of the segments array.
$segments = $this->scanControllers($segments);

// If we don't have any segments left - use the default controller;
// If not empty, then the first segment should be the controller
if (! empty($segments)) {
$this->controller = ucfirst(array_shift($segments));
}

$controllerName = $this->controllerName();
kenjis marked this conversation as resolved.
Show resolved Hide resolved

if (! $this->isValidSegment($controllerName)) {
throw new PageNotFoundException($this->controller . ' is not a valid controller name');
}

// Use the method name if it exists.
// If it doesn't, no biggie - the default method name
// has already been set.
if (! empty($segments)) {
$this->method = array_shift($segments) ?: $this->method;
}

// Prevent access to initController method
if (strtolower($this->method) === 'initcontroller') {
throw PageNotFoundException::forPageNotFound();
}

/** @var array $params An array of params to the controller method. */
$params = [];

if (! empty($segments)) {
$params = $segments;
}

kenjis marked this conversation as resolved.
Show resolved Hide resolved
// Ensure routes registered via $routes->cli() are not accessible via web.
if ($this->httpVerb !== 'cli') {
kenjis marked this conversation as resolved.
Show resolved Hide resolved
$controller = '\\' . $this->defaultNamespace;
kenjis marked this conversation as resolved.
Show resolved Hide resolved

$controller .= $this->directory ? str_replace('/', '\\', $this->directory) : '';
$controller .= $controllerName;

$controller = strtolower($controller);

foreach ($this->protectedControllers as $controllerInRoute) {
if (! is_string($controllerInRoute)) {
continue;
}
if (strtolower($controllerInRoute) !== $controller) {
continue;
}

throw new PageNotFoundException(
'Cannot access the controller in a CLI Route. Controller: ' . $controllerInRoute
);
}
}

// Load the file so that it's available for CodeIgniter.
$file = APPPATH . 'Controllers/' . $this->directory . $controllerName . '.php';
if (is_file($file)) {
include_once $file;
}

// Ensure the controller stores the fully-qualified class name
// We have to check for a length over 1, since by default it will be '\'
if (strpos($this->controller, '\\') === false && strlen($this->defaultNamespace) > 1) {
kenjis marked this conversation as resolved.
Show resolved Hide resolved
$this->controller = '\\' . ltrim(
str_replace(
'/',
'\\',
$this->defaultNamespace . $this->directory . $controllerName
kenjis marked this conversation as resolved.
Show resolved Hide resolved
),
'\\'
);
}

return [$this->directory, $this->controllerName(), $this->methodName(), $params];
}

/**
* Tells the system whether we should translate URI dashes or not
* in the URI from a dash to an underscore.
*/
public function setTranslateURIDashes(bool $val = false): self
{
$this->translateURIDashes = $val;

return $this;
}

/**
* Scans the controller directory, attempting to locate a controller matching the supplied uri $segments
*
* @param array $segments URI segments
*
* @return array returns an array of remaining uri segments that don't map onto a directory
*/
protected function scanControllers(array $segments): array
{
$segments = array_filter($segments, static fn ($segment) => $segment !== '');
// numerically reindex the array, removing gaps
$segments = array_values($segments);

// if a prior directory value has been set, just return segments and get out of here
if (isset($this->directory)) {
return $segments;
}

// Loop through our segments and return as soon as a controller
// is found or when such a directory doesn't exist
$c = count($segments);

while ($c-- > 0) {
$segmentConvert = ucfirst(
$this->translateURIDashes ? str_replace('-', '_', $segments[0]) : $segments[0]
);
// as soon as we encounter any segment that is not PSR-4 compliant, stop searching
if (! $this->isValidSegment($segmentConvert)) {
return $segments;
}

$test = APPPATH . 'Controllers/' . $this->directory . $segmentConvert;

// as long as each segment is *not* a controller file but does match a directory, add it to $this->directory
if (! is_file($test . '.php') && is_dir($test)) {
$this->setDirectory($segmentConvert, true, false);
array_shift($segments);

continue;
}

return $segments;
}

// This means that all segments were actually directories
return $segments;
}

/**
* Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment
*
* regex comes from https://www.php.net/manual/en/language.variables.basics.php
*/
private function isValidSegment(string $segment): bool
{
return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment);
}

/**
* Sets the sub-directory that the controller is in.
*
* @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments
*/
public function setDirectory(?string $dir = null, bool $append = false, bool $validate = true)
{
if (empty($dir)) {
$this->directory = null;

return;
}

if ($validate) {
$segments = explode('/', trim($dir, '/'));

foreach ($segments as $segment) {
if (! $this->isValidSegment($segment)) {
return;
}
}
}

if ($append !== true || empty($this->directory)) {
$this->directory = trim($dir, '/') . '/';
} else {
$this->directory .= trim($dir, '/') . '/';
}
}

/**
* Returns the name of the sub-directory the controller is in,
* if any. Relative to APPPATH.'Controllers'.
*/
public function directory(): string
kenjis marked this conversation as resolved.
Show resolved Hide resolved
{
return ! empty($this->directory) ? $this->directory : '';
}

private function controllerName(): string
kenjis marked this conversation as resolved.
Show resolved Hide resolved
{
return $this->translateURIDashes
? str_replace('-', '_', $this->controller)
: $this->controller;
}

private function methodName(): string
kenjis marked this conversation as resolved.
Show resolved Hide resolved
{
return $this->translateURIDashes
? str_replace('-', '_', $this->method)
: $this->method;
}
}
51 changes: 51 additions & 0 deletions system/Router/RouteCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ class RouteCollection implements RouteCollectionInterface
* An array of all routes and their mappings.
*
* @var array
*
* [
* verb => [
* routeName => [
* 'route' => [
* routeKey => handler,
* ]
* ]
* ],
* ]
*/
protected $routes = [
'*' => [],
Expand Down Expand Up @@ -1403,4 +1413,45 @@ public function setPrioritize(bool $enabled = true)

return $this;
}

/**
* Get all controllers in Route Handlers
*
* @param string|null $verb HTTP verb. `'*'` returns all controllers in any verb.
*/
public function getRegisteredControllers(?string $verb = '*'): array
{
$routes = [];

if ($verb === '*') {
$rawRoutes = [];

foreach ($this->defaultHTTPMethods as $tmpVerb) {
$rawRoutes = array_merge($rawRoutes, $this->routes[$tmpVerb]);
}

foreach ($rawRoutes as $route) {
$key = key($route['route']);
$handler = $route['route'][$key];

$routes[$key] = $handler;
}
} else {
$routes = $this->getRoutes($verb);
}

$controllers = [];

foreach ($routes as $handler) {
if (! is_string($handler)) {
continue;
}

[$controller] = explode('::', $handler, 2);

$controllers[] = $controller;
}

return array_unique($controllers);
}
}
4 changes: 2 additions & 2 deletions system/Router/RouteCollectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public function getDefaultMethod();
/**
* Returns the current value of the translateURIDashes setting.
*
* @return mixed
* @return bool
*/
public function shouldTranslateURIDashes();

Expand All @@ -145,7 +145,7 @@ public function shouldAutoRoute();
/**
* Returns the raw array of available routes.
*
* @return mixed
* @return array
*/
public function getRoutes();

Expand Down
Loading