nikic / FastRoute

Fast request router for PHP
Other
5.1k stars 446 forks source link

Case insensitive route static part. #252

Closed Vitsen15 closed 2 years ago

Vitsen15 commented 2 years ago

Hello,

I'm trying to use FastRoute based league/route package to handle routing on legacy project. But old routes are case insensitive in static parts. Routes parsing is handeld by FastRoute, so I decided to post question here.

I.e, I have route /db/preferences/{id}, which can work with url like /db/Preferences/123 on legacy mode, but don't works with new routing cause it case sensitive.

How can I configure routing to handle such requests?

FYI, I know about #175.

Vitsen15 commented 2 years ago

So, I did it by overriding a lot of vendor methods, I'd started from DataGenerator. I just needed to use 'i' flag in regex, but there wasn't any option to set it, so I had to redefine FastRoute\DataGenerator\GroupCountBased DataGenerator with only one character difference:

class CaseInsensitiveGroupCountBased extends \FastRoute\DataGenerator\RegexBasedAbstract
{
    protected function getApproxChunkSize(): int
    {
        return 10;
    }

    protected function processChunk($regexToRoutesMap): array
    {
        $routeMap = [];
        $regexes = [];
        $numGroups = 0;
        foreach ($regexToRoutesMap as $regex => $route) {
            $numVariables = count($route->variables);
            $numGroups = max($numGroups, $numVariables);

            $regexes[] = $regex . str_repeat('()', $numGroups - $numVariables);
            $routeMap[$numGroups + 1] = [$route->handler, $route->variables];

            ++$numGroups;
        }

        $regex = '~^(?|' . implode('|', $regexes) . ')$~i'; //changed line
        return ['regex' => $regex, 'routeMap' => $routeMap];
    }
}

Next section is for league/route package changes.

All worked fine, except fully static routes... For it I had to redefine dispatcher logic in method dispatch of \League\Route\Dispatcher (pls see // End of Override ):

class Dispatcher extends \League\Route\Dispatcher
{
    /** @inheritDoc */
    public function dispatch($httpMethod, $uri): array
    {
        // Lowercase static routes and uri before comparing.
        $lowercaseUri = strtolower($uri);
        $lowercaseStaticRoutes = array_change_key_case($this->staticRouteMap[$httpMethod]);

        if (isset($lowercaseStaticRoutes[$lowercaseUri])) {
            $handler = $lowercaseStaticRoutes[$lowercaseUri];
            return [self::FOUND, $handler, []];
        }// End of Override

        $varRouteData = $this->variableRouteData;
        if (isset($varRouteData[$httpMethod])) {
            $result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri);
            if ($result[0] === self::FOUND) {
                return $result;
            }
        }

        // For HEAD requests, attempt fallback to GET
        if ($httpMethod === 'HEAD') {
            if (isset($this->staticRouteMap['GET'][$uri])) {
                $handler = $this->staticRouteMap['GET'][$uri];
                return [self::FOUND, $handler, []];
            }
            if (isset($varRouteData['GET'])) {
                $result = $this->dispatchVariableRoute($varRouteData['GET'], $uri);
                if ($result[0] === self::FOUND) {
                    return $result;
                }
            }
        }

        // If nothing else matches, try fallback routes
        if (isset($this->staticRouteMap['*'][$uri])) {
            $handler = $this->staticRouteMap['*'][$uri];
            return [self::FOUND, $handler, []];
        }
        if (isset($varRouteData['*'])) {
            $result = $this->dispatchVariableRoute($varRouteData['*'], $uri);
            if ($result[0] === self::FOUND) {
                return $result;
            }
        }

        // Find allowed methods for this URI by matching against all other HTTP methods as well
        $allowedMethods = [];

        foreach ($this->staticRouteMap as $method => $uriMap) {
            if ($method !== $httpMethod && isset($uriMap[$uri])) {
                $allowedMethods[] = $method;
            }
        }

        foreach ($varRouteData as $method => $routeData) {
            if ($method === $httpMethod) {
                continue;
            }

            $result = $this->dispatchVariableRoute($routeData, $uri);
            if ($result[0] === self::FOUND) {
                $allowedMethods[] = $method;
            }
        }

        // If there are no allowed methods the route simply does not exist
        if ($allowedMethods) {
            return [self::METHOD_NOT_ALLOWED, $allowedMethods];
        }

        return [self::NOT_FOUND];
    }
}

But \League\Route\Router doesn't allow to set your own dispatcher, it uses composition instead of aggregation and initialize dispatcher right in dispatch method. So I had to override it also (overided only custom dispatcher initialization):

class Router extends \League\Route\Router
{
    /**
     * The same as parent class method, except usage of custom dispatcher.
     * The purpose is to handle case-insensitive static routes.
     * Parametrized routes are covered by: RestApi\Kernel\Router\DataGenerator\CaseInsensitiveGroupCountBased.
     *
     * {@inheritdoc}
     */
    public function dispatch(ServerRequestInterface $request): ResponseInterface
    {
        if ($this->getStrategy() === null) {
            $this->setStrategy(new ApplicationStrategy);
        }

        $this->prepRoutes($request);

        /**
         * Use custom dispatcher
         * @var Dispatcher $dispatcher
         */
        $dispatcher = (new Dispatcher($this->getData()))->setStrategy($this->getStrategy()); //changed line

        foreach ($this->getMiddlewareStack() as $middleware) {
            if (is_string($middleware)) {
                $dispatcher->lazyMiddleware($middleware);
                continue;
            }

            $dispatcher->middleware($middleware);
        }

        return $dispatcher->dispatchRequest($request);
    }
}

Summary

I'd recommend to add ability to pass regex options to DataGenerators.

FYI

Packages versions: