-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
fbfc4bb
docs: update PHPDocs
kenjis 1004321
docs: add comments
kenjis 56781cb
refactor: move property initialization to constructor
kenjis 0b505c6
refactor: extract AutoRouter class
kenjis eabb12c
style: break long lines
kenjis 43e11ba
refactor: remove uneeded `=== true`
kenjis dc912e0
feat: add RouteCollection::getRegisteredControllers()
kenjis c8169d6
fix: can access controller by auto-routing when adding cli route with…
kenjis 0e42760
docs: remove @deprecated
kenjis 98cc70c
refactor: add property type
kenjis 74a0364
refactor: instantiate AutoRouter only when auto routing is enabled
kenjis a98bde1
refactor: remove unneeded $this->params
kenjis 2c331f5
refactor: run rector and php-cs-fixer
kenjis 000c115
test: make Event simulate false in FeatureTestTraitTest tearDown()
kenjis 1ad510b
test: fix MigrationRunnerTest
kenjis 72ef968
docs: improve doc comment
kenjis d899e0b
refactor: remove unused variable
kenjis 3b405ad
docs: add changelog
kenjis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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,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; | ||||||
|
||||||
/** | ||||||
* The name of the method to use. | ||||||
*/ | ||||||
protected string $method; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
/** | ||||||
* 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; | ||||||
} | ||||||
} |
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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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?