yiisoft /
router-fastroute
| 1 | <?php |
||||||
| 2 | |||||||
| 3 | declare(strict_types=1); |
||||||
| 4 | |||||||
| 5 | namespace Yiisoft\Router\FastRoute; |
||||||
| 6 | |||||||
| 7 | use FastRoute\DataGenerator\GroupCountBased as RouteGenerator; |
||||||
| 8 | use FastRoute\Dispatcher; |
||||||
| 9 | use FastRoute\Dispatcher\GroupCountBased; |
||||||
| 10 | use FastRoute\RouteCollector; |
||||||
| 11 | use FastRoute\RouteParser\Std as RouteParser; |
||||||
| 12 | use Psr\Http\Message\ServerRequestInterface; |
||||||
| 13 | use Psr\SimpleCache\CacheInterface; |
||||||
| 14 | use RuntimeException; |
||||||
| 15 | use Yiisoft\Http\Method; |
||||||
| 16 | use Yiisoft\Router\MatchingResult; |
||||||
| 17 | use Yiisoft\Router\RouteCollectionInterface; |
||||||
| 18 | use Yiisoft\Router\UrlMatcherInterface; |
||||||
| 19 | |||||||
| 20 | use function array_merge; |
||||||
| 21 | |||||||
| 22 | /** |
||||||
| 23 | * @psalm-type ResultNotFound = array{0:0} |
||||||
| 24 | * @psalm-type ResultMethodNotAllowed = array{0:2,1:string[]} |
||||||
| 25 | * @psalm-type ResultFound = array{0:1,1:string,2:array<string,string>} |
||||||
| 26 | * |
||||||
| 27 | * @psalm-type DispatcherCallback=Closure(array):Dispatcher |
||||||
| 28 | */ |
||||||
| 29 | final class UrlMatcher implements UrlMatcherInterface |
||||||
| 30 | { |
||||||
| 31 | /** |
||||||
| 32 | * Configuration key used to set the cache file path. |
||||||
| 33 | */ |
||||||
| 34 | public const CONFIG_CACHE_KEY = 'cache_key'; |
||||||
| 35 | |||||||
| 36 | /** |
||||||
| 37 | * Configuration key used to set the cache file path. |
||||||
| 38 | */ |
||||||
| 39 | private string $cacheKey = 'routes-cache'; |
||||||
| 40 | |||||||
| 41 | /** |
||||||
| 42 | * @var ?callable A factory callback that can return a dispatcher. |
||||||
| 43 | * @psalm-var DispatcherCallback|null |
||||||
| 44 | */ |
||||||
| 45 | private $dispatcherCallback; |
||||||
| 46 | |||||||
| 47 | /** |
||||||
| 48 | * @var array Cached data used by the dispatcher. |
||||||
| 49 | */ |
||||||
| 50 | private array $dispatchData = []; |
||||||
| 51 | |||||||
| 52 | /** |
||||||
| 53 | * @var bool Whether cache is enabled and valid dispatch data has been loaded from cache. |
||||||
| 54 | */ |
||||||
| 55 | private bool $hasCache = false; |
||||||
| 56 | |||||||
| 57 | private RouteCollector $fastRouteCollector; |
||||||
| 58 | private bool $hasInjectedRoutes = false; |
||||||
| 59 | |||||||
| 60 | /** |
||||||
| 61 | * Accepts optionally a FastRoute RouteCollector and a callable factory |
||||||
| 62 | * that can return a FastRoute dispatcher. |
||||||
| 63 | * |
||||||
| 64 | * If either is not provided defaults will be used: |
||||||
| 65 | * |
||||||
| 66 | * - A RouteCollector instance will be created composing a RouteParser and |
||||||
| 67 | * RouteGenerator. |
||||||
| 68 | * - A callable that returns a GroupCountBased dispatcher will be created. |
||||||
| 69 | * |
||||||
| 70 | * @param RouteCollector|null $fastRouteCollector If not provided, a default implementation will be used. |
||||||
| 71 | * @param callable|null $dispatcherFactory Callable that will return a FastRoute dispatcher. |
||||||
| 72 | * @param array|null $config Array of custom configuration options. |
||||||
| 73 | * |
||||||
| 74 | * @psalm-param DispatcherCallback|null $dispatcherFactory |
||||||
| 75 | */ |
||||||
| 76 | 26 | public function __construct( |
|||||
| 77 | private RouteCollectionInterface $routeCollection, |
||||||
| 78 | private ?CacheInterface $cache = null, |
||||||
| 79 | ?array $config = null, |
||||||
| 80 | ?RouteCollector $fastRouteCollector = null, |
||||||
| 81 | ?callable $dispatcherFactory = null |
||||||
| 82 | ) { |
||||||
| 83 | 26 | $this->fastRouteCollector = $fastRouteCollector ?? $this->createRouteCollector(); |
|||||
| 84 | 26 | $this->dispatcherCallback = $dispatcherFactory; |
|||||
| 85 | 26 | $this->loadConfig($config); |
|||||
| 86 | |||||||
| 87 | 26 | $this->loadDispatchData(); |
|||||
| 88 | } |
||||||
| 89 | |||||||
| 90 | 24 | public function match(ServerRequestInterface $request): MatchingResult |
|||||
| 91 | { |
||||||
| 92 | 24 | if (!$this->hasCache && !$this->hasInjectedRoutes) { |
|||||
| 93 | 23 | $this->injectRoutes(); |
|||||
| 94 | } |
||||||
| 95 | |||||||
| 96 | 23 | $dispatchData = $this->getDispatchData(); |
|||||
| 97 | 23 | $path = urldecode($request |
|||||
| 98 | 23 | ->getUri() |
|||||
| 99 | 23 | ->getPath()); |
|||||
| 100 | 23 | $method = $request->getMethod(); |
|||||
| 101 | |||||||
| 102 | /** |
||||||
| 103 | * @psalm-var ResultNotFound|ResultMethodNotAllowed|ResultFound $result |
||||||
| 104 | */ |
||||||
| 105 | 23 | $result = $this |
|||||
| 106 | 23 | ->getDispatcher($dispatchData) |
|||||
| 107 | 23 | ->dispatch($method, $request |
|||||
| 108 | 23 | ->getUri() |
|||||
| 109 | 23 | ->getHost() . $path); |
|||||
| 110 | |||||||
| 111 | /** @psalm-suppress ArgumentTypeCoercion Psalm can't determine correct type here */ |
||||||
| 112 | 23 | return $result[0] !== Dispatcher::FOUND |
|||||
| 113 | 9 | ? $this->marshalFailedRoute($result) |
|||||
| 114 | 23 | : $this->marshalMatchedRoute($result); |
|||||
| 115 | } |
||||||
| 116 | |||||||
| 117 | /** |
||||||
| 118 | * Load configuration parameters. |
||||||
| 119 | * |
||||||
| 120 | * @param array|null $config Array of custom configuration options. |
||||||
| 121 | */ |
||||||
| 122 | 26 | private function loadConfig(?array $config): void |
|||||
| 123 | { |
||||||
| 124 | 26 | if ($config === null) { |
|||||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||||||
| 125 | 3 | return; |
|||||
| 126 | } |
||||||
| 127 | |||||||
| 128 | 23 | if (isset($config[self::CONFIG_CACHE_KEY])) { |
|||||
| 129 | 23 | $this->cacheKey = (string) $config[self::CONFIG_CACHE_KEY]; |
|||||
| 130 | } |
||||||
| 131 | } |
||||||
| 132 | |||||||
| 133 | /** |
||||||
| 134 | * Retrieve the dispatcher instance. |
||||||
| 135 | * |
||||||
| 136 | * Uses the callable factory in $dispatcherCallback, passing it $data |
||||||
| 137 | * (which should be derived from the router's getData() method); this |
||||||
| 138 | * approach is done to allow testing against the dispatcher. |
||||||
| 139 | * |
||||||
| 140 | * @param array $data Data from {@see RouteCollector::getData()}. |
||||||
| 141 | */ |
||||||
| 142 | 23 | private function getDispatcher(array $data): Dispatcher |
|||||
| 143 | { |
||||||
| 144 | 23 | if (!$this->dispatcherCallback) { |
|||||
| 145 | 23 | $this->dispatcherCallback = $this->createDispatcherCallback(); |
|||||
| 146 | } |
||||||
| 147 | |||||||
| 148 | 23 | $factory = $this->dispatcherCallback; |
|||||
| 149 | |||||||
| 150 | 23 | return $factory($data); |
|||||
| 151 | } |
||||||
| 152 | |||||||
| 153 | /** |
||||||
| 154 | * Create a default FastRoute Collector instance. |
||||||
| 155 | */ |
||||||
| 156 | 26 | private function createRouteCollector(): RouteCollector |
|||||
| 157 | { |
||||||
| 158 | 26 | return new RouteCollector(new RouteParser(), new RouteGenerator()); |
|||||
| 159 | } |
||||||
| 160 | |||||||
| 161 | /** |
||||||
| 162 | * Returns a default implementation of a callback that can return a Dispatcher. |
||||||
| 163 | * |
||||||
| 164 | * @psalm-return DispatcherCallback |
||||||
| 165 | */ |
||||||
| 166 | 23 | private function createDispatcherCallback(): callable |
|||||
| 167 | { |
||||||
| 168 | 23 | return static fn (array $data) => new GroupCountBased($data); |
|||||
| 169 | } |
||||||
| 170 | |||||||
| 171 | /** |
||||||
| 172 | * Marshal a routing failure result. |
||||||
| 173 | * |
||||||
| 174 | * If the failure was due to the HTTP method, passes the allowed HTTP |
||||||
| 175 | * methods to the factory. |
||||||
| 176 | * |
||||||
| 177 | * @psalm-param ResultNotFound|ResultMethodNotAllowed $result |
||||||
| 178 | */ |
||||||
| 179 | 9 | private function marshalFailedRoute(array $result): MatchingResult |
|||||
| 180 | { |
||||||
| 181 | 9 | $resultCode = $result[0]; |
|||||
| 182 | 9 | if ($resultCode === Dispatcher::METHOD_NOT_ALLOWED) { |
|||||
| 183 | /** @psalm-var ResultMethodNotAllowed $result */ |
||||||
| 184 | 3 | return MatchingResult::fromFailure($result[1]); |
|||||
| 185 | } |
||||||
| 186 | |||||||
| 187 | 6 | return MatchingResult::fromFailure(Method::ALL); |
|||||
| 188 | } |
||||||
| 189 | |||||||
| 190 | /** |
||||||
| 191 | * Marshals a route result based on the results of matching. |
||||||
| 192 | * |
||||||
| 193 | * @psalm-param ResultFound $result |
||||||
| 194 | */ |
||||||
| 195 | 15 | private function marshalMatchedRoute(array $result): MatchingResult |
|||||
| 196 | { |
||||||
| 197 | 15 | [, $name, $arguments] = $result; |
|||||
| 198 | |||||||
| 199 | 15 | $route = $this->routeCollection->getRoute($name); |
|||||
| 200 | |||||||
| 201 | 15 | if (isset($arguments['_host'])) { |
|||||
| 202 | 14 | unset($arguments['_host']); |
|||||
| 203 | } |
||||||
| 204 | 15 | $arguments = array_merge($route->getData('defaults'), $arguments); |
|||||
| 205 | |||||||
| 206 | 15 | return MatchingResult::fromSuccess($route, $arguments); |
|||||
| 207 | } |
||||||
| 208 | |||||||
| 209 | /** |
||||||
| 210 | * Inject routes into the underlying router. |
||||||
| 211 | */ |
||||||
| 212 | 23 | private function injectRoutes(): void |
|||||
| 213 | { |
||||||
| 214 | 23 | foreach ($this->routeCollection->getRoutes() as $route) { |
|||||
| 215 | 22 | if (!$route->getData('hasMiddlewares')) { |
|||||
| 216 | 1 | continue; |
|||||
| 217 | } |
||||||
| 218 | |||||||
| 219 | 21 | $hosts = $route->getData('hosts'); |
|||||
| 220 | 21 | $count = count($hosts); |
|||||
| 221 | |||||||
| 222 | 21 | if ($count > 1) { |
|||||
| 223 | 3 | $hosts = implode('|', $hosts); |
|||||
| 224 | |||||||
| 225 | 3 | if (preg_match('~' . RouteParser::VARIABLE_REGEX . '~x', $hosts)) { |
|||||
| 226 | 1 | throw new RuntimeException('Placeholders are not allowed with multiple host names.'); |
|||||
| 227 | } |
||||||
| 228 | |||||||
| 229 | 2 | $hostPattern = '{_host:' . $hosts . '}'; |
|||||
| 230 | 19 | } elseif ($count === 1) { |
|||||
| 231 | 2 | $hostPattern = $hosts[0]; |
|||||
| 232 | } else { |
||||||
| 233 | 17 | $hostPattern = '{_host:[a-zA-Z0-9\.\-]*}'; |
|||||
| 234 | } |
||||||
| 235 | |||||||
| 236 | 20 | $this->fastRouteCollector->addRoute( |
|||||
| 237 | 20 | $route->getData('methods'), |
|||||
| 238 | 20 | $hostPattern . $route->getData('pattern'), |
|||||
| 239 | 20 | $route->getData('name') |
|||||
| 240 | 20 | ); |
|||||
| 241 | } |
||||||
| 242 | 22 | $this->hasInjectedRoutes = true; |
|||||
| 243 | } |
||||||
| 244 | |||||||
| 245 | /** |
||||||
| 246 | * Get the dispatch data either from cache or freshly generated by the |
||||||
| 247 | * FastRoute data generator. |
||||||
| 248 | * |
||||||
| 249 | * If caching is enabled, store the freshly generated data to file. |
||||||
| 250 | */ |
||||||
| 251 | 23 | private function getDispatchData(): array |
|||||
| 252 | { |
||||||
| 253 | 23 | if ($this->hasCache) { |
|||||
| 254 | 1 | return $this->dispatchData; |
|||||
| 255 | } |
||||||
| 256 | |||||||
| 257 | 22 | $dispatchData = $this->fastRouteCollector->getData(); |
|||||
| 258 | |||||||
| 259 | 22 | if ($this->cache !== null) { |
|||||
| 260 | 2 | $this->cacheDispatchData($dispatchData); |
|||||
| 261 | } |
||||||
| 262 | |||||||
| 263 | 22 | return $dispatchData; |
|||||
| 264 | } |
||||||
| 265 | |||||||
| 266 | /** |
||||||
| 267 | * Load dispatch data from cache. |
||||||
| 268 | */ |
||||||
| 269 | 26 | private function loadDispatchData(): void |
|||||
| 270 | { |
||||||
| 271 | 26 | if ($this->cache !== null && $this->cache->has($this->cacheKey)) { |
|||||
| 272 | /** @var array $dispatchData */ |
||||||
| 273 | 1 | $dispatchData = $this->cache->get($this->cacheKey); |
|||||
| 274 | |||||||
| 275 | 1 | $this->hasCache = true; |
|||||
| 276 | 1 | $this->dispatchData = $dispatchData; |
|||||
| 277 | 1 | return; |
|||||
| 278 | } |
||||||
| 279 | |||||||
| 280 | 25 | $this->hasCache = false; |
|||||
| 281 | } |
||||||
| 282 | |||||||
| 283 | /** |
||||||
| 284 | * Save dispatch data to cache. |
||||||
| 285 | * |
||||||
| 286 | * @psalm-suppress PossiblyNullReference |
||||||
| 287 | */ |
||||||
| 288 | 2 | private function cacheDispatchData(array $dispatchData): void |
|||||
| 289 | { |
||||||
| 290 | 2 | $this->cache->set($this->cacheKey, $dispatchData); |
|||||
|
0 ignored issues
–
show
The method
set() does not exist on null.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. Loading history...
|
|||||||
| 291 | } |
||||||
| 292 | } |
||||||
| 293 |