187 lines
5.1 KiB
PHP
187 lines
5.1 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace FastRoute\DataGenerator;
|
||
|
|
||
|
use FastRoute\BadRouteException;
|
||
|
use FastRoute\DataGenerator;
|
||
|
use FastRoute\Route;
|
||
|
|
||
|
abstract class RegexBasedAbstract implements DataGenerator
|
||
|
{
|
||
|
/** @var mixed[][] */
|
||
|
protected $staticRoutes = [];
|
||
|
|
||
|
/** @var Route[][] */
|
||
|
protected $methodToRegexToRoutesMap = [];
|
||
|
|
||
|
/**
|
||
|
* @return int
|
||
|
*/
|
||
|
abstract protected function getApproxChunkSize();
|
||
|
|
||
|
/**
|
||
|
* @return mixed[]
|
||
|
*/
|
||
|
abstract protected function processChunk($regexToRoutesMap);
|
||
|
|
||
|
public function addRoute($httpMethod, $routeData, $handler)
|
||
|
{
|
||
|
if ($this->isStaticRoute($routeData)) {
|
||
|
$this->addStaticRoute($httpMethod, $routeData, $handler);
|
||
|
} else {
|
||
|
$this->addVariableRoute($httpMethod, $routeData, $handler);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return mixed[]
|
||
|
*/
|
||
|
public function getData()
|
||
|
{
|
||
|
if (empty($this->methodToRegexToRoutesMap)) {
|
||
|
return [$this->staticRoutes, []];
|
||
|
}
|
||
|
|
||
|
return [$this->staticRoutes, $this->generateVariableRouteData()];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return mixed[]
|
||
|
*/
|
||
|
private function generateVariableRouteData()
|
||
|
{
|
||
|
$data = [];
|
||
|
foreach ($this->methodToRegexToRoutesMap as $method => $regexToRoutesMap) {
|
||
|
$chunkSize = $this->computeChunkSize(count($regexToRoutesMap));
|
||
|
$chunks = array_chunk($regexToRoutesMap, $chunkSize, true);
|
||
|
$data[$method] = array_map([$this, 'processChunk'], $chunks);
|
||
|
}
|
||
|
return $data;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param int
|
||
|
* @return int
|
||
|
*/
|
||
|
private function computeChunkSize($count)
|
||
|
{
|
||
|
$numParts = max(1, round($count / $this->getApproxChunkSize()));
|
||
|
return (int) ceil($count / $numParts);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param mixed[]
|
||
|
* @return bool
|
||
|
*/
|
||
|
private function isStaticRoute($routeData)
|
||
|
{
|
||
|
return count($routeData) === 1 && is_string($routeData[0]);
|
||
|
}
|
||
|
|
||
|
private function addStaticRoute($httpMethod, $routeData, $handler)
|
||
|
{
|
||
|
$routeStr = $routeData[0];
|
||
|
|
||
|
if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
|
||
|
throw new BadRouteException(sprintf(
|
||
|
'Cannot register two routes matching "%s" for method "%s"',
|
||
|
$routeStr, $httpMethod
|
||
|
));
|
||
|
}
|
||
|
|
||
|
if (isset($this->methodToRegexToRoutesMap[$httpMethod])) {
|
||
|
foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) {
|
||
|
if ($route->matches($routeStr)) {
|
||
|
throw new BadRouteException(sprintf(
|
||
|
'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"',
|
||
|
$routeStr, $route->regex, $httpMethod
|
||
|
));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->staticRoutes[$httpMethod][$routeStr] = $handler;
|
||
|
}
|
||
|
|
||
|
private function addVariableRoute($httpMethod, $routeData, $handler)
|
||
|
{
|
||
|
list($regex, $variables) = $this->buildRegexForRoute($routeData);
|
||
|
|
||
|
if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
|
||
|
throw new BadRouteException(sprintf(
|
||
|
'Cannot register two routes matching "%s" for method "%s"',
|
||
|
$regex, $httpMethod
|
||
|
));
|
||
|
}
|
||
|
|
||
|
$this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route(
|
||
|
$httpMethod, $handler, $regex, $variables
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param mixed[]
|
||
|
* @return mixed[]
|
||
|
*/
|
||
|
private function buildRegexForRoute($routeData)
|
||
|
{
|
||
|
$regex = '';
|
||
|
$variables = [];
|
||
|
foreach ($routeData as $part) {
|
||
|
if (is_string($part)) {
|
||
|
$regex .= preg_quote($part, '~');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
list($varName, $regexPart) = $part;
|
||
|
|
||
|
if (isset($variables[$varName])) {
|
||
|
throw new BadRouteException(sprintf(
|
||
|
'Cannot use the same placeholder "%s" twice', $varName
|
||
|
));
|
||
|
}
|
||
|
|
||
|
if ($this->regexHasCapturingGroups($regexPart)) {
|
||
|
throw new BadRouteException(sprintf(
|
||
|
'Regex "%s" for parameter "%s" contains a capturing group',
|
||
|
$regexPart, $varName
|
||
|
));
|
||
|
}
|
||
|
|
||
|
$variables[$varName] = $varName;
|
||
|
$regex .= '(' . $regexPart . ')';
|
||
|
}
|
||
|
|
||
|
return [$regex, $variables];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string
|
||
|
* @return bool
|
||
|
*/
|
||
|
private function regexHasCapturingGroups($regex)
|
||
|
{
|
||
|
if (false === strpos($regex, '(')) {
|
||
|
// Needs to have at least a ( to contain a capturing group
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Semi-accurate detection for capturing groups
|
||
|
return (bool) preg_match(
|
||
|
'~
|
||
|
(?:
|
||
|
\(\?\(
|
||
|
| \[ [^\]\\\\]* (?: \\\\ . [^\]\\\\]* )* \]
|
||
|
| \\\\ .
|
||
|
) (*SKIP)(*FAIL) |
|
||
|
\(
|
||
|
(?!
|
||
|
\? (?! <(?![!=]) | P< | \' )
|
||
|
| \*
|
||
|
)
|
||
|
~x',
|
||
|
$regex
|
||
|
);
|
||
|
}
|
||
|
}
|