gmarche/vendor/zendframework/zend-expressive-fastroute/src/FastRouteRouter.php

332 lines
9.6 KiB
PHP

<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see https://github.com/zendframework/zend-expressive for the canonical source repository
* @copyright Copyright (c) 2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive/blob/master/LICENSE.md New BSD License
*/
namespace Zend\Expressive\Router;
use FastRoute\DataGenerator\GroupCountBased as RouteGenerator;
use FastRoute\Dispatcher\GroupCountBased as Dispatcher;
use FastRoute\RouteCollector;
use FastRoute\RouteParser\Std as RouteParser;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Expressive\Router\Exception;
/**
* Router implementation bridging nikic/fast-route.
*/
class FastRouteRouter implements RouterInterface
{
/**
* Regular expression pattern for identifying a variable subsititution.
*
* This is an sprintf pattern; the variable name will be substituted for
* the final pattern.
*
* @see \FastRoute\RouteParser\Std for mirror, generic representation.
*/
const VARIABLE_REGEX = <<<'REGEX'
\{
\s* %s \s*
(?:
: \s* ([^{}]*(?:\{(?-1)\}[^{}]*)*)
)?
\}
REGEX;
/**
* @var callable A factory callback that can return a dispatcher.
*/
private $dispatcherCallback;
/**
* FastRoute router
*
* @var RouteCollector
*/
private $router;
/**
* All attached routes as Route instances
*
* @var Route[]
*/
private $routes;
/**
* Routes to inject into the underlying RouteCollector.
*
* @var Route[]
*/
private $routesToInject = [];
/**
* Constructor
*
* Accepts optionally a FastRoute RouteCollector and a callable factory
* that can return a FastRoute dispatcher.
*
* If either is not provided defaults will be used:
*
* - A RouteCollector instance will be created composing a RouteParser and
* RouteGenerator.
* - A callable that returns a GroupCountBased dispatcher will be created.
*
* @param null|RouteCollector $router If not provided, a default
* implementation will be used.
* @param null|callable $dispatcherFactory Callable that will return a
* FastRoute dispatcher.
*/
public function __construct(RouteCollector $router = null, callable $dispatcherFactory = null)
{
if (null === $router) {
$router = $this->createRouter();
}
$this->router = $router;
$this->dispatcherCallback = $dispatcherFactory;
}
/**
* Add a route to the collection.
*
* Uses the HTTP methods associated (creating sane defaults for an empty
* list or Route::HTTP_METHOD_ANY) and the path, and uses the path as
* the name (to allow later lookup of the middleware).
*
* @param Route $route
*/
public function addRoute(Route $route)
{
$this->routesToInject[] = $route;
}
/**
* @param Request $request
* @return RouteResult
*/
public function match(Request $request)
{
// Inject any pending routes
$this->injectRoutes();
$path = $request->getUri()->getPath();
$method = $request->getMethod();
$dispatcher = $this->getDispatcher($this->router->getData());
$result = $dispatcher->dispatch($method, $path);
if ($result[0] != Dispatcher::FOUND) {
return $this->marshalFailedRoute($result);
}
return $this->marshalMatchedRoute($result, $method);
}
/**
* Generate a URI based on a given route.
*
* Replacements in FastRoute are written as `{name}` or `{name:<pattern>}`;
* this method uses a regular expression to search for substitutions that
* match, and replaces them with the value provided.
*
* It does *not* use the pattern to validate that the substitution value is
* valid beforehand, however.
*
* @param string $name Route name.
* @param array $substitutions Key/value pairs to substitute into the route
* pattern.
* @return string URI path generated.
* @throws Exception\InvalidArgumentException if the route name is not
* known.
*/
public function generateUri($name, array $substitutions = [])
{
// Inject any pending routes
$this->injectRoutes();
if (! array_key_exists($name, $this->routes)) {
throw new Exception\InvalidArgumentException(sprintf(
'Cannot generate URI for route "%s"; route not found',
$name
));
}
$route = $this->routes[$name];
$path = $route->getPath();
$options = $route->getOptions();
if (! empty($options['defaults'])) {
$substitutions = array_merge($options['defaults'], $substitutions);
}
foreach ($substitutions as $key => $value) {
$pattern = sprintf(
'~%s~x',
sprintf(self::VARIABLE_REGEX, preg_quote($key))
);
$path = preg_replace($pattern, $value, $path);
}
// 1. remove optional segments' ending delimiters
// 2. split path into an array of optional segments and remove those
// containing unsubstituted parameters starting from the last segment
$path = str_replace(']', '', $path);
$segs = array_reverse(explode('[', $path));
foreach ($segs as $n => $seg) {
if (strpos($seg, '{') !== false) {
if (isset($segs[$n - 1])) {
throw new Exception\InvalidArgumentException(
'Optional segments with unsubstituted parameters cannot '
. 'contain segments with substituted parameters when using FastRoute'
);
}
unset($segs[$n]);
}
}
$path = implode('', array_reverse($segs));
return $path;
}
/**
* Create a default FastRoute Collector instance
*
* @return RouteCollector
*/
private function createRouter()
{
return new RouteCollector(new RouteParser, new RouteGenerator);
}
/**
* Retrieve the dispatcher instance.
*
* Uses the callable factory in $dispatcherCallback, passing it $data
* (which should be derived from the router's getData() method); this
* approach is done to allow testing against the dispatcher.
*
* @param array|object $data Data from RouteCollection::getData()
* @return Dispatcher
*/
private function getDispatcher($data)
{
if (! $this->dispatcherCallback) {
$this->dispatcherCallback = $this->createDispatcherCallback();
}
$factory = $this->dispatcherCallback;
return $factory($data);
}
/**
* Return a default implementation of a callback that can return a Dispatcher.
*
* @return callable
*/
private function createDispatcherCallback()
{
return function ($data) {
return new Dispatcher($data);
};
}
/**
* Marshal a routing failure result.
*
* If the failure was due to the HTTP method, passes the allowed HTTP
* methods to the factory.
*
* @return RouteResult
*/
private function marshalFailedRoute(array $result)
{
if ($result[0] === Dispatcher::METHOD_NOT_ALLOWED) {
return RouteResult::fromRouteFailure($result[1]);
}
return RouteResult::fromRouteFailure();
}
/**
* Marshals a route result based on the results of matching and the current HTTP method.
*
* @param array $result
* @param string $method
* @return RouteResult
*/
private function marshalMatchedRoute(array $result, $method)
{
$path = $result[1];
$route = array_reduce($this->routes, function ($matched, $route) use ($path, $method) {
if ($matched) {
return $matched;
}
if ($path !== $route->getPath()) {
return $matched;
}
if (! $route->allowsMethod($method)) {
return $matched;
}
return $route;
}, false);
if (false === $route) {
// This likely should never occur, but is present for completeness.
return RouteResult::fromRouteFailure();
}
$params = $result[2];
$options = $route->getOptions();
if (! empty($options['defaults'])) {
$params = array_merge($options['defaults'], $params);
}
return RouteResult::fromRouteMatch(
$route->getName(),
$route->getMiddleware(),
$params
);
}
/**
* Inject queued Route instances into the underlying router.
*
* @param Route $route
*/
private function injectRoutes()
{
foreach ($this->routesToInject as $index => $route) {
$this->injectRoute($route);
unset($this->routesToInject[$index]);
}
}
/**
* Inject a Route instance into the underlying router.
*
* @param Route $route
*/
private function injectRoute(Route $route)
{
$methods = $route->getAllowedMethods();
if ($methods === Route::HTTP_METHOD_ANY) {
$methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE'];
}
if (empty($methods)) {
$methods = ['GET', 'HEAD', 'OPTIONS'];
}
$this->router->addRoute($methods, $route->getPath(), $route->getPath());
$this->routes[$route->getName()] = $route;
}
}