dispatcher = $dispatcher ?: $this->getDefaultDispatcher(); $this->pathVariablesAttributeName = $pathVariablesAttributeName; $this->factory = $this->getRouteFactory($this->dispatcher); $this->routes = []; $this->staticRoutes = []; $this->prefixRoutes = []; $this->patternRoutes = []; $this->stack = []; } public function __invoke( ServerRequestInterface $request, ResponseInterface $response, callable $next ): ResponseInterface { // Use only the path for routing. $requestTarget = parse_url($request->getRequestTarget(), PHP_URL_PATH); $route = $this->getStaticRoute($requestTarget); if ($route) { return $this->dispatch($route, $request, $response, $next); } $route = $this->getPrefixRoute($requestTarget); if ($route) { return $this->dispatch($route, $request, $response, $next); } // Try each of the routes. foreach ($this->patternRoutes as $route) { if ($route->matchesRequestTarget($requestTarget)) { $pathVariables = $route->getPathVariables(); if ($this->pathVariablesAttributeName) { $request = $request->withAttribute($this->pathVariablesAttributeName, $pathVariables); } else { foreach ($pathVariables as $name => $value) { $request = $request->withAttribute($name, $value); } } return $this->dispatch($route, $request, $response, $next); } } if (!$this->continueOnNotFound) { return $response->withStatus(404); } return $next($request, $response); } private function dispatch( callable $route, ServerRequestInterface $request, ResponseInterface $response, callable $next ): ResponseInterface { if (!$this->stack) { return $route($request, $response, $next); } $stack = array_merge($this->stack, [$route]); return $this->dispatcher->dispatch( $stack, $request, $response, $next ); } /** * Register handlers and middleware with the router for a given path and * method. * * $method may be: * - A single verb ("GET"), * - A comma-separated list of verbs ("GET,PUT,DELETE") * - "*" to indicate any method. * * $target may be: * - An exact path (e.g., "/path/") * - A prefix path ending with "*"" ("/path/*"") * - A URI template with variables enclosed in "{}" ("/path/{id}") * - A regular expression ("~/cat/([0-9]+)~") * * $dispatchable may be: * - An instance implementing one of these interfaces: * - Psr\Http\Server\RequestHandlerInterface * - Psr\Http\Server\MiddlewareInterface * - WellRESTed\MiddlewareInterface * - Psr\Http\Message\ResponseInterface * - A string containing the fully qualified class name of a class * implementing one of the interfaces listed above. * - A callable that returns an instance implementing one of the * interfaces listed above. * - A callable with a signature matching the signature of * WellRESTed\MiddlewareInterface::__invoke * - An array containing any of the items in this list. * @see DispatchedInterface::dispatch * * @param string $method HTTP method(s) to match * @param string $target Request target or pattern to match * @param mixed $dispatchable Handler or middleware to dispatch * @return static */ public function register(string $method, string $target, $dispatchable): Router { $route = $this->getRouteForTarget($target); $route->register($method, $dispatchable); return $this; } /** * Push a new middleware onto the stack. * * Middleware for a router runs before the middleware and handler for the * matched route and runs only when a route matched. * * $middleware may be: * - An instance implementing MiddlewareInterface * - A string containing the fully qualified class name of a class * implementing MiddlewareInterface * - A callable that returns an instance implementing MiddleInterface * - A callable matching the signature of MiddlewareInterface::dispatch * @see DispatchedInterface::dispatch * * @param mixed $middleware Middleware to dispatch in sequence * @return static */ public function add($middleware): Router { $this->stack[] = $middleware; return $this; } /** * Configure the instance to delegate to the next middleware when no route * matches. * * @return static */ public function continueOnNotFound(): Router { $this->continueOnNotFound = true; return $this; } protected function getDefaultDispatcher(): DispatcherInterface { return new Dispatcher(); } protected function getRouteFactory(DispatcherInterface $dispatcher): RouteFactoryInterface { return new RouteFactory($dispatcher); } private function getRouteForTarget(string $target): Route { if (isset($this->routes[$target])) { $route = $this->routes[$target]; } else { $route = $this->factory->create($target); $this->registerRouteForTarget($route, $target); } return $route; } private function registerRouteForTarget(Route $route, string $target): void { // Store the route to the hash indexed by original target. $this->routes[$target] = $route; // Store the route to the array of routes for its type. switch ($route->getType()) { case Route::TYPE_STATIC: $this->staticRoutes[$route->getTarget()] = $route; break; case Route::TYPE_PREFIX: $this->prefixRoutes[rtrim($route->getTarget(), '*')] = $route; break; case Route::TYPE_PATTERN: $this->patternRoutes[] = $route; break; } } private function getStaticRoute(string $requestTarget): ?Route { if (isset($this->staticRoutes[$requestTarget])) { return $this->staticRoutes[$requestTarget]; } return null; } private function getPrefixRoute(string $requestTarget): ?Route { // Find all prefixes that match the start of this path. $prefixes = array_keys($this->prefixRoutes); $matches = array_filter( $prefixes, function ($prefix) use ($requestTarget) { return $this->startsWith($requestTarget, $prefix); } ); if (!$matches) { return null; } // If there are multiple matches, sort them to find the one with the // longest string length. if (count($matches) > 1) { $compareByLength = function (string $a, string $b): int { return strlen($b) - strlen($a); }; usort($matches, $compareByLength); } $bestMatch = $matches[0]; return $this->prefixRoutes[$bestMatch]; } private function startsWith(string $haystack, string $needle): bool { $length = strlen($needle); return substr($haystack, 0, $length) === $needle; } }