slimslim-3

Multiple Slim routes with the same signature


We are looking at using Slim 3 as the framework for our API. I have searched SO and the Slim docs, but cannot find an answer to the issue. If we have different route files (e.g. v1, v2, etc.) and if two routes have the same signature, an error is thrown. Is there any way to cascade the routes so that the last loaded route for a particular signature is used?

For example, say v1.php has a route for GET ("/test") and v2.php also contains this route, can we use the latest version? Even simpler would be if a file of routes contains two routes with the same signature, is there a way of the latter method being used (and no error being thrown)?

A similar question is asked here but this uses hooks (which have been removed from Slim 3 as per here)


Solution

  • I looked at the Slim code and I didn't find a simple way of allowing duplicated routes (preventing the exception). The new Slim uses FastRoute as dependency. It calls FastRoute\simpleDispatcher and doesn't offer any configuration possiblity. Even if it did allow some configuration, FastRoute doesn't have any built-in option to allow duplicated routes. A custom implementation of a DataGenerator would be needed.

    But following the instructions above, we can get a custom DataGenerator by passing to Slim App a custom Router which instantiates some FastRoute::Dispatcher implementation which then uses the custom DataGenerator.

    First the CustomDataGenerator (let's go the easy way and do some copy and pasting from \FastRoute\RegexBasedAbstract and \FastRoute\GroupCountBased)

    <?php
    class CustomDataGenerator implements \FastRoute\DataGenerator {
        /*
         * 1. Copy over everything from the RegexBasedAbstract
         * 2. Replace abstract methods with implementations from GroupCountBased
         * 3. change the addStaticRoute and addVariableRoute
         * to the following implementations
         */
        private function addStaticRoute($httpMethod, $routeData, $handler) {
            $routeStr = $routeData[0];
    
            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
                        ));
                    }
                }
            }
            if (isset($this->staticRoutes[$httpMethod][$routeStr])) {
                unset($this->staticRoutes[$httpMethod][$routeStr]);
            }
            $this->staticRoutes[$httpMethod][$routeStr] = $handler;
        }
        private function addVariableRoute($httpMethod, $routeData, $handler) {
            list($regex, $variables) = $this->buildRegexForRoute($routeData);
            if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) {
                unset($this->methodToRegexToRoutesMap[$httpMethod][$regex]);
            }
            $this->methodToRegexToRoutesMap[$httpMethod][$regex] = new \FastRoute\Route(
                $httpMethod, $handler, $regex, $variables
            );
        }
    }
    

    Then the custom Router

    <?php
    class CustomRouter extends \Slim\Router {
        protected function createDispatcher() {
            return $this->dispatcher ?: \FastRoute\simpleDispatcher(function (\FastRoute\RouteCollector $r) {
                foreach ($this->getRoutes() as $route) {
                    $r->addRoute($route->getMethods(), $route->getPattern(), $route->getIdentifier());
                }
            }, [
                'routeParser' => $this->routeParser,
                'dataGenerator' => new CustomDataGenerator()
            ]);
        }
    }
    

    and finally instantiate the Slim app with the custom router

    <?php
    $app = new \Slim\App(array(
        'router' => new CustomRouter()
    ));
    

    The code above, if a duplicated route is detected, removes the previous route and stores the new one.

    I hope I didn't miss any simpler way of achieving this result.