pruvodce/lib/FastRoute/DataGenerator/RegexBasedAbstract.php

212 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace FastRoute\DataGenerator;
use FastRoute\BadRouteException;
use FastRoute\DataGenerator;
use FastRoute\Route;
use function array_chunk;
use function array_map;
use function ceil;
use function count;
use function is_string;
use function max;
use function preg_match;
use function preg_quote;
use function round;
use function sprintf;
use function strpos;
abstract class RegexBasedAbstract implements DataGenerator
{
/** @var mixed[][] */
protected $staticRoutes = [];
/** @var Route[][] */
protected $methodToRegexToRoutesMap = [];
abstract protected function getApproxChunkSize(): int;
/**
* @param array<string, Route> $regexToRoutesMap
*
* @return mixed[]
*/
abstract protected function processChunk(array $regexToRoutesMap): array;
/**
* {@inheritDoc}
*/
public function addRoute(string $httpMethod, array $routeData, $handler): void
{
if ($this->isStaticRoute($routeData)) {
$this->addStaticRoute($httpMethod, $routeData, $handler);
} else {
$this->addVariableRoute($httpMethod, $routeData, $handler);
}
}
/**
* {@inheritDoc}
*/
public function getData(): array
{
if ($this->methodToRegexToRoutesMap === []) {
return [$this->staticRoutes, []];
}
return [$this->staticRoutes, $this->generateVariableRouteData()];
}
/**
* @return mixed[]
*/
private function generateVariableRouteData(): array
{
$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;
}
private function computeChunkSize(int $count): int
{
$numParts = max(1, round($count / $this->getApproxChunkSize()));
return (int) ceil($count / $numParts);
}
/**
* @param array<int, mixed> $routeData
*/
private function isStaticRoute(array $routeData): bool
{
return count($routeData) === 1 && is_string($routeData[0]);
}
/**
* @param array<int, mixed> $routeData
* @param mixed $handler
*/
private function addStaticRoute(string $httpMethod, array $routeData, $handler): void
{
$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;
}
/**
* @param array<int, mixed> $routeData
* @param mixed $handler
*/
private function addVariableRoute(string $httpMethod, array $routeData, $handler): void
{
[$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[] $routeData
*
* @return mixed[]
*/
private function buildRegexForRoute(array $routeData): array
{
$regex = '';
$variables = [];
foreach ($routeData as $part) {
if (is_string($part)) {
$regex .= preg_quote($part, '~');
continue;
}
[$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];
}
private function regexHasCapturingGroups(string $regex): bool
{
if (strpos($regex, '(') === false) {
// 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
);
}
}