annotate vendor/symfony-cmf/routing/ChainRouter.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 4c8ae668cc8c
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /*
Chris@0 4 * This file is part of the Symfony CMF package.
Chris@0 5 *
Chris@0 6 * (c) 2011-2015 Symfony CMF
Chris@0 7 *
Chris@0 8 * For the full copyright and license information, please view the LICENSE
Chris@0 9 * file that was distributed with this source code.
Chris@0 10 */
Chris@0 11
Chris@0 12 namespace Symfony\Cmf\Component\Routing;
Chris@0 13
Chris@0 14 use Symfony\Component\Routing\RouterInterface;
Chris@0 15 use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
Chris@0 16 use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
Chris@0 17 use Symfony\Component\Routing\RequestContext;
Chris@0 18 use Symfony\Component\Routing\RequestContextAwareInterface;
Chris@0 19 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
Chris@0 20 use Symfony\Component\Routing\Exception\RouteNotFoundException;
Chris@0 21 use Symfony\Component\Routing\Exception\MethodNotAllowedException;
Chris@0 22 use Symfony\Component\Routing\RouteCollection;
Chris@0 23 use Symfony\Component\HttpFoundation\Request;
Chris@0 24 use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
Chris@0 25 use Psr\Log\LoggerInterface;
Chris@0 26
Chris@0 27 /**
Chris@0 28 * The ChainRouter allows to combine several routers to try in a defined order.
Chris@0 29 *
Chris@0 30 * @author Henrik Bjornskov <henrik@bjrnskov.dk>
Chris@0 31 * @author Magnus Nordlander <magnus@e-butik.se>
Chris@0 32 */
Chris@0 33 class ChainRouter implements ChainRouterInterface, WarmableInterface
Chris@0 34 {
Chris@0 35 /**
Chris@0 36 * @var RequestContext
Chris@0 37 */
Chris@0 38 private $context;
Chris@0 39
Chris@0 40 /**
Chris@0 41 * Array of arrays of routers grouped by priority.
Chris@0 42 *
Chris@0 43 * @var array
Chris@0 44 */
Chris@0 45 private $routers = array();
Chris@0 46
Chris@0 47 /**
Chris@0 48 * @var RouterInterface[] Array of routers, sorted by priority
Chris@0 49 */
Chris@0 50 private $sortedRouters;
Chris@0 51
Chris@0 52 /**
Chris@0 53 * @var RouteCollection
Chris@0 54 */
Chris@0 55 private $routeCollection;
Chris@0 56
Chris@0 57 /**
Chris@0 58 * @var null|LoggerInterface
Chris@0 59 */
Chris@0 60 protected $logger;
Chris@0 61
Chris@0 62 /**
Chris@0 63 * @param LoggerInterface $logger
Chris@0 64 */
Chris@0 65 public function __construct(LoggerInterface $logger = null)
Chris@0 66 {
Chris@0 67 $this->logger = $logger;
Chris@0 68 }
Chris@0 69
Chris@0 70 /**
Chris@0 71 * @return RequestContext
Chris@0 72 */
Chris@0 73 public function getContext()
Chris@0 74 {
Chris@0 75 return $this->context;
Chris@0 76 }
Chris@0 77
Chris@0 78 /**
Chris@0 79 * {@inheritdoc}
Chris@0 80 */
Chris@0 81 public function add($router, $priority = 0)
Chris@0 82 {
Chris@0 83 if (!$router instanceof RouterInterface
Chris@0 84 && !($router instanceof RequestMatcherInterface && $router instanceof UrlGeneratorInterface)
Chris@0 85 ) {
Chris@0 86 throw new \InvalidArgumentException(sprintf('%s is not a valid router.', get_class($router)));
Chris@0 87 }
Chris@0 88 if (empty($this->routers[$priority])) {
Chris@0 89 $this->routers[$priority] = array();
Chris@0 90 }
Chris@0 91
Chris@0 92 $this->routers[$priority][] = $router;
Chris@0 93 $this->sortedRouters = array();
Chris@0 94 }
Chris@0 95
Chris@0 96 /**
Chris@0 97 * {@inheritdoc}
Chris@0 98 */
Chris@0 99 public function all()
Chris@0 100 {
Chris@0 101 if (empty($this->sortedRouters)) {
Chris@0 102 $this->sortedRouters = $this->sortRouters();
Chris@0 103
Chris@0 104 // setContext() is done here instead of in add() to avoid fatal errors when clearing and warming up caches
Chris@0 105 // See https://github.com/symfony-cmf/Routing/pull/18
Chris@0 106 $context = $this->getContext();
Chris@0 107 if (null !== $context) {
Chris@0 108 foreach ($this->sortedRouters as $router) {
Chris@0 109 if ($router instanceof RequestContextAwareInterface) {
Chris@0 110 $router->setContext($context);
Chris@0 111 }
Chris@0 112 }
Chris@0 113 }
Chris@0 114 }
Chris@0 115
Chris@0 116 return $this->sortedRouters;
Chris@0 117 }
Chris@0 118
Chris@0 119 /**
Chris@0 120 * Sort routers by priority.
Chris@0 121 * The highest priority number is the highest priority (reverse sorting).
Chris@0 122 *
Chris@0 123 * @return RouterInterface[]
Chris@0 124 */
Chris@0 125 protected function sortRouters()
Chris@0 126 {
Chris@0 127 $sortedRouters = array();
Chris@0 128 krsort($this->routers);
Chris@0 129
Chris@0 130 foreach ($this->routers as $routers) {
Chris@0 131 $sortedRouters = array_merge($sortedRouters, $routers);
Chris@0 132 }
Chris@0 133
Chris@0 134 return $sortedRouters;
Chris@0 135 }
Chris@0 136
Chris@0 137 /**
Chris@0 138 * {@inheritdoc}
Chris@0 139 *
Chris@0 140 * Loops through all routes and tries to match the passed url.
Chris@0 141 *
Chris@0 142 * Note: You should use matchRequest if you can.
Chris@0 143 */
Chris@0 144 public function match($pathinfo)
Chris@0 145 {
Chris@0 146 return $this->doMatch($pathinfo);
Chris@0 147 }
Chris@0 148
Chris@0 149 /**
Chris@0 150 * {@inheritdoc}
Chris@0 151 *
Chris@0 152 * Loops through all routes and tries to match the passed request.
Chris@0 153 */
Chris@0 154 public function matchRequest(Request $request)
Chris@0 155 {
Chris@0 156 return $this->doMatch($request->getPathInfo(), $request);
Chris@0 157 }
Chris@0 158
Chris@0 159 /**
Chris@0 160 * Loops through all routers and tries to match the passed request or url.
Chris@0 161 *
Chris@0 162 * At least the url must be provided, if a request is additionally provided
Chris@0 163 * the request takes precedence.
Chris@0 164 *
Chris@0 165 * @param string $pathinfo
Chris@0 166 * @param Request $request
Chris@0 167 *
Chris@0 168 * @return array An array of parameters
Chris@0 169 *
Chris@0 170 * @throws ResourceNotFoundException If no router matched.
Chris@0 171 */
Chris@0 172 private function doMatch($pathinfo, Request $request = null)
Chris@0 173 {
Chris@0 174 $methodNotAllowed = null;
Chris@0 175
Chris@0 176 $requestForMatching = $request;
Chris@0 177 foreach ($this->all() as $router) {
Chris@0 178 try {
Chris@0 179 // the request/url match logic is the same as in Symfony/Component/HttpKernel/EventListener/RouterListener.php
Chris@0 180 // matching requests is more powerful than matching URLs only, so try that first
Chris@0 181 if ($router instanceof RequestMatcherInterface) {
Chris@0 182 if (empty($requestForMatching)) {
Chris@0 183 $requestForMatching = $this->rebuildRequest($pathinfo);
Chris@0 184 }
Chris@0 185
Chris@0 186 return $router->matchRequest($requestForMatching);
Chris@0 187 }
Chris@0 188
Chris@0 189 // every router implements the match method
Chris@0 190 return $router->match($pathinfo);
Chris@0 191 } catch (ResourceNotFoundException $e) {
Chris@0 192 if ($this->logger) {
Chris@0 193 $this->logger->debug('Router '.get_class($router).' was not able to match, message "'.$e->getMessage().'"');
Chris@0 194 }
Chris@0 195 // Needs special care
Chris@0 196 } catch (MethodNotAllowedException $e) {
Chris@0 197 if ($this->logger) {
Chris@0 198 $this->logger->debug('Router '.get_class($router).' throws MethodNotAllowedException with message "'.$e->getMessage().'"');
Chris@0 199 }
Chris@0 200 $methodNotAllowed = $e;
Chris@0 201 }
Chris@0 202 }
Chris@0 203
Chris@0 204 $info = $request
Chris@0 205 ? "this request\n$request"
Chris@0 206 : "url '$pathinfo'";
Chris@0 207 throw $methodNotAllowed ?: new ResourceNotFoundException("None of the routers in the chain matched $info");
Chris@0 208 }
Chris@0 209
Chris@0 210 /**
Chris@0 211 * {@inheritdoc}
Chris@0 212 *
Chris@0 213 * Loops through all registered routers and returns a router if one is found.
Chris@0 214 * It will always return the first route generated.
Chris@0 215 */
Chris@0 216 public function generate($name, $parameters = array(), $absolute = UrlGeneratorInterface::ABSOLUTE_PATH)
Chris@0 217 {
Chris@0 218 $debug = array();
Chris@0 219
Chris@0 220 foreach ($this->all() as $router) {
Chris@0 221 // if $router does not announce it is capable of handling
Chris@0 222 // non-string routes and $name is not a string, continue
Chris@0 223 if ($name && !is_string($name) && !$router instanceof VersatileGeneratorInterface) {
Chris@0 224 continue;
Chris@0 225 }
Chris@0 226
Chris@0 227 // If $router is versatile and doesn't support this route name, continue
Chris@0 228 if ($router instanceof VersatileGeneratorInterface && !$router->supports($name)) {
Chris@0 229 continue;
Chris@0 230 }
Chris@0 231
Chris@0 232 try {
Chris@0 233 return $router->generate($name, $parameters, $absolute);
Chris@0 234 } catch (RouteNotFoundException $e) {
Chris@0 235 $hint = $this->getErrorMessage($name, $router, $parameters);
Chris@0 236 $debug[] = $hint;
Chris@0 237 if ($this->logger) {
Chris@0 238 $this->logger->debug('Router '.get_class($router)." was unable to generate route. Reason: '$hint': ".$e->getMessage());
Chris@0 239 }
Chris@0 240 }
Chris@0 241 }
Chris@0 242
Chris@0 243 if ($debug) {
Chris@0 244 $debug = array_unique($debug);
Chris@0 245 $info = implode(', ', $debug);
Chris@0 246 } else {
Chris@0 247 $info = $this->getErrorMessage($name);
Chris@0 248 }
Chris@0 249
Chris@0 250 throw new RouteNotFoundException(sprintf('None of the chained routers were able to generate route: %s', $info));
Chris@0 251 }
Chris@0 252
Chris@0 253 /**
Chris@0 254 * Rebuild the request object from a URL with the help of the RequestContext.
Chris@0 255 *
Chris@0 256 * If the request context is not set, this simply returns the request object built from $uri.
Chris@0 257 *
Chris@0 258 * @param string $pathinfo
Chris@0 259 *
Chris@0 260 * @return Request
Chris@0 261 */
Chris@0 262 private function rebuildRequest($pathinfo)
Chris@0 263 {
Chris@0 264 if (!$this->context) {
Chris@0 265 return Request::create('http://localhost'.$pathinfo);
Chris@0 266 }
Chris@0 267
Chris@0 268 $uri = $pathinfo;
Chris@0 269
Chris@0 270 $server = array();
Chris@0 271 if ($this->context->getBaseUrl()) {
Chris@0 272 $uri = $this->context->getBaseUrl().$pathinfo;
Chris@0 273 $server['SCRIPT_FILENAME'] = $this->context->getBaseUrl();
Chris@0 274 $server['PHP_SELF'] = $this->context->getBaseUrl();
Chris@0 275 }
Chris@0 276 $host = $this->context->getHost() ?: 'localhost';
Chris@0 277 if ('https' === $this->context->getScheme() && 443 !== $this->context->getHttpsPort()) {
Chris@0 278 $host .= ':'.$this->context->getHttpsPort();
Chris@0 279 }
Chris@0 280 if ('http' === $this->context->getScheme() && 80 !== $this->context->getHttpPort()) {
Chris@0 281 $host .= ':'.$this->context->getHttpPort();
Chris@0 282 }
Chris@0 283 $uri = $this->context->getScheme().'://'.$host.$uri.'?'.$this->context->getQueryString();
Chris@0 284
Chris@0 285 return Request::create($uri, $this->context->getMethod(), $this->context->getParameters(), array(), array(), $server);
Chris@0 286 }
Chris@0 287
Chris@0 288 private function getErrorMessage($name, $router = null, $parameters = null)
Chris@0 289 {
Chris@0 290 if ($router instanceof VersatileGeneratorInterface) {
Chris@0 291 $displayName = $router->getRouteDebugMessage($name, $parameters);
Chris@0 292 } elseif (is_object($name)) {
Chris@0 293 $displayName = method_exists($name, '__toString')
Chris@0 294 ? (string) $name
Chris@0 295 : get_class($name)
Chris@0 296 ;
Chris@0 297 } else {
Chris@0 298 $displayName = (string) $name;
Chris@0 299 }
Chris@0 300
Chris@0 301 return "Route '$displayName' not found";
Chris@0 302 }
Chris@0 303
Chris@0 304 /**
Chris@0 305 * {@inheritdoc}
Chris@0 306 */
Chris@0 307 public function setContext(RequestContext $context)
Chris@0 308 {
Chris@0 309 foreach ($this->all() as $router) {
Chris@0 310 if ($router instanceof RequestContextAwareInterface) {
Chris@0 311 $router->setContext($context);
Chris@0 312 }
Chris@0 313 }
Chris@0 314
Chris@0 315 $this->context = $context;
Chris@0 316 }
Chris@0 317
Chris@0 318 /**
Chris@0 319 * {@inheritdoc}
Chris@0 320 *
Chris@0 321 * check for each contained router if it can warmup
Chris@0 322 */
Chris@0 323 public function warmUp($cacheDir)
Chris@0 324 {
Chris@0 325 foreach ($this->all() as $router) {
Chris@0 326 if ($router instanceof WarmableInterface) {
Chris@0 327 $router->warmUp($cacheDir);
Chris@0 328 }
Chris@0 329 }
Chris@0 330 }
Chris@0 331
Chris@0 332 /**
Chris@0 333 * {@inheritdoc}
Chris@0 334 */
Chris@0 335 public function getRouteCollection()
Chris@0 336 {
Chris@0 337 if (!$this->routeCollection instanceof RouteCollection) {
Chris@0 338 $this->routeCollection = new ChainRouteCollection();
Chris@0 339 foreach ($this->all() as $router) {
Chris@0 340 $this->routeCollection->addCollection($router->getRouteCollection());
Chris@0 341 }
Chris@0 342 }
Chris@0 343
Chris@0 344 return $this->routeCollection;
Chris@0 345 }
Chris@0 346
Chris@0 347 /**
Chris@0 348 * Identify if any routers have been added into the chain yet.
Chris@0 349 *
Chris@0 350 * @return bool
Chris@0 351 */
Chris@0 352 public function hasRouters()
Chris@0 353 {
Chris@0 354 return !empty($this->routers);
Chris@0 355 }
Chris@0 356 }