<?php 
 
/* 
 * This file is part of the Symfony CMF package. 
 * 
 * (c) Symfony CMF 
 * 
 * For the full copyright and license information, please view the LICENSE 
 * file that was distributed with this source code. 
 */ 
 
namespace Symfony\Cmf\Component\Routing; 
 
use Psr\Log\LoggerInterface; 
use Symfony\Component\HttpFoundation\Request; 
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; 
use Symfony\Component\Routing\Exception\MethodNotAllowedException; 
use Symfony\Component\Routing\Exception\ResourceNotFoundException; 
use Symfony\Component\Routing\Exception\RouteNotFoundException; 
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 
use Symfony\Component\Routing\Matcher\RequestMatcherInterface; 
use Symfony\Component\Routing\RequestContext; 
use Symfony\Component\Routing\RequestContextAwareInterface; 
use Symfony\Component\Routing\RouteCollection; 
use Symfony\Component\Routing\RouterInterface; 
 
/** 
 * The ChainRouter allows to combine several routers to try in a defined order. 
 * 
 * @author Henrik Bjornskov <henrik@bjrnskov.dk> 
 * @author Magnus Nordlander <magnus@e-butik.se> 
 */ 
class ChainRouter implements ChainRouterInterface, WarmableInterface 
{ 
    /** 
     * @var RequestContext|null 
     */ 
    private $context; 
 
    /** 
     * Array of arrays of routers grouped by priority. 
     * 
     * @var RouterInterface[][] Priority => RouterInterface[] 
     */ 
    private $routers = []; 
 
    /** 
     * @var RouterInterface[] List of routers, sorted by priority 
     */ 
    private $sortedRouters = []; 
 
    /** 
     * @var RouteCollection 
     */ 
    private $routeCollection; 
 
    /** 
     * @var null|LoggerInterface 
     */ 
    protected $logger; 
 
    /** 
     * @param LoggerInterface $logger 
     */ 
    public function __construct(LoggerInterface $logger = null) 
    { 
        $this->logger = $logger; 
    } 
 
    /** 
     * @return RequestContext 
     */ 
    public function getContext() 
    { 
        if (!$this->context) { 
            $this->context = new RequestContext(); 
        } 
 
        return $this->context; 
    } 
 
    /** 
     * {@inheritdoc} 
     */ 
    public function add($router, $priority = 0) 
    { 
        if (!$router instanceof RouterInterface 
            && !($router instanceof RequestMatcherInterface && $router instanceof UrlGeneratorInterface) 
        ) { 
            throw new \InvalidArgumentException(sprintf('%s is not a valid router.', get_class($router))); 
        } 
        if (empty($this->routers[$priority])) { 
            $this->routers[$priority] = []; 
        } 
 
        $this->routers[$priority][] = $router; 
        $this->sortedRouters = []; 
    } 
 
    /** 
     * {@inheritdoc} 
     */ 
    public function all() 
    { 
        if (0 === count($this->sortedRouters)) { 
            $this->sortedRouters = $this->sortRouters(); 
 
            // setContext() is done here instead of in add() to avoid fatal errors when clearing and warming up caches 
            // See https://github.com/symfony-cmf/Routing/pull/18 
            if (null !== $this->context) { 
                foreach ($this->sortedRouters as $router) { 
                    if ($router instanceof RequestContextAwareInterface) { 
                        $router->setContext($this->context); 
                    } 
                } 
            } 
        } 
 
        return $this->sortedRouters; 
    } 
 
    /** 
     * Sort routers by priority. 
     * The highest priority number is the highest priority (reverse sorting). 
     * 
     * @return RouterInterface[] 
     */ 
    protected function sortRouters() 
    { 
        if (0 === count($this->routers)) { 
            return []; 
        } 
 
        krsort($this->routers); 
 
        return call_user_func_array('array_merge', $this->routers); 
    } 
 
    /** 
     * {@inheritdoc} 
     * 
     * Loops through all routes and tries to match the passed url. 
     * 
     * Note: You should use matchRequest if you can. 
     */ 
    public function match($pathinfo) 
    { 
        return $this->doMatch($pathinfo); 
    } 
 
    /** 
     * {@inheritdoc} 
     * 
     * Loops through all routes and tries to match the passed request. 
     */ 
    public function matchRequest(Request $request) 
    { 
        return $this->doMatch($request->getPathInfo(), $request); 
    } 
 
    /** 
     * Loops through all routers and tries to match the passed request or url. 
     * 
     * At least the  url must be provided, if a request is additionally provided 
     * the request takes precedence. 
     * 
     * @param string  $pathinfo 
     * @param Request $request 
     * 
     * @return array An array of parameters 
     * 
     * @throws ResourceNotFoundException If no router matched 
     */ 
    private function doMatch($pathinfo, Request $request = null) 
    { 
        $methodNotAllowed = null; 
 
        $requestForMatching = $request; 
        foreach ($this->all() as $router) { 
            try { 
                // the request/url match logic is the same as in Symfony/Component/HttpKernel/EventListener/RouterListener.php 
                // matching requests is more powerful than matching URLs only, so try that first 
                if ($router instanceof RequestMatcherInterface) { 
                    if (null === $requestForMatching) { 
                        $requestForMatching = $this->rebuildRequest($pathinfo); 
                    } 
 
                    return $router->matchRequest($requestForMatching); 
                } 
 
                // every router implements the match method 
                return $router->match($pathinfo); 
            } catch (ResourceNotFoundException $e) { 
                if ($this->logger) { 
                    $this->logger->debug('Router '.get_class($router).' was not able to match, message "'.$e->getMessage().'"'); 
                } 
                // Needs special care 
            } catch (MethodNotAllowedException $e) { 
                if ($this->logger) { 
                    $this->logger->debug('Router '.get_class($router).' throws MethodNotAllowedException with message "'.$e->getMessage().'"'); 
                } 
                $methodNotAllowed = $e; 
            } 
        } 
 
        $info = $request 
            ? "this request\n$request" 
            : "url '$pathinfo'"; 
 
        throw $methodNotAllowed ?: new ResourceNotFoundException("None of the routers in the chain matched $info"); 
    } 
 
    /** 
     * {@inheritdoc} 
     * 
     * Loops through all registered routers and returns a router if one is found. 
     * It will always return the first route generated. 
     */ 
    public function generate($name, $parameters = [], $absolute = UrlGeneratorInterface::ABSOLUTE_PATH) 
    { 
        $debug = []; 
 
        foreach ($this->all() as $router) { 
            // if $router does not announce it is capable of handling 
            // non-string routes and $name is not a string, continue 
            if ($name && !is_string($name) && !$router instanceof VersatileGeneratorInterface) { 
                continue; 
            } 
 
            // If $router is versatile and doesn't support this route name, continue 
            if ($router instanceof VersatileGeneratorInterface && !$router->supports($name)) { 
                continue; 
            } 
 
            try { 
                return $router->generate($name, $parameters, $absolute); 
            } catch (RouteNotFoundException $e) { 
                $hint = $this->getErrorMessage($name, $router, $parameters); 
                $debug[] = $hint; 
                if ($this->logger) { 
                    $this->logger->debug('Router '.get_class($router)." was unable to generate route. Reason: '$hint': ".$e->getMessage()); 
                } 
            } 
        } 
 
        if ($debug) { 
            $debug = array_unique($debug); 
            $info = implode(', ', $debug); 
        } else { 
            $info = $this->getErrorMessage($name); 
        } 
 
        throw new RouteNotFoundException(sprintf('None of the chained routers were able to generate route: %s', $info)); 
    } 
 
    /** 
     * Rebuild the request object from a URL with the help of the RequestContext. 
     * 
     * If the request context is not set, this returns the request object built from $pathinfo. 
     * 
     * @param string $pathinfo 
     * 
     * @return Request 
     */ 
    private function rebuildRequest($pathinfo) 
    { 
        $context = $this->getContext(); 
 
        $uri = $pathinfo; 
 
        $server = []; 
        if ($context->getBaseUrl()) { 
            $uri = $context->getBaseUrl().$pathinfo; 
            $server['SCRIPT_FILENAME'] = $context->getBaseUrl(); 
            $server['PHP_SELF'] = $context->getBaseUrl(); 
        } 
        $host = $context->getHost() ?: 'localhost'; 
        if ('https' === $context->getScheme() && 443 !== $context->getHttpsPort()) { 
            $host .= ':'.$context->getHttpsPort(); 
        } 
        if ('http' === $context->getScheme() && 80 !== $context->getHttpPort()) { 
            $host .= ':'.$context->getHttpPort(); 
        } 
        $uri = $context->getScheme().'://'.$host.$uri.'?'.$context->getQueryString(); 
 
        return Request::create($uri, $context->getMethod(), $context->getParameters(), [], [], $server); 
    } 
 
    private function getErrorMessage($name, $router = null, $parameters = null) 
    { 
        if ($router instanceof VersatileGeneratorInterface) { 
            // the $parameters are not forced to be array, but versatile generator does typehint it 
            if (!is_array($parameters)) { 
                $parameters = []; 
            } 
            $displayName = $router->getRouteDebugMessage($name, $parameters); 
        } elseif (is_object($name)) { 
            $displayName = method_exists($name, '__toString') 
                ? (string) $name 
                : get_class($name) 
            ; 
        } else { 
            $displayName = (string) $name; 
        } 
 
        return "Route '$displayName' not found"; 
    } 
 
    /** 
     * {@inheritdoc} 
     */ 
    public function setContext(RequestContext $context) 
    { 
        foreach ($this->all() as $router) { 
            if ($router instanceof RequestContextAwareInterface) { 
                $router->setContext($context); 
            } 
        } 
 
        $this->context = $context; 
    } 
 
    /** 
     * {@inheritdoc} 
     * 
     * check for each contained router if it can warmup 
     */ 
    public function warmUp($cacheDir) 
    { 
        foreach ($this->all() as $router) { 
            if ($router instanceof WarmableInterface) { 
                $router->warmUp($cacheDir); 
            } 
        } 
    } 
 
    /** 
     * {@inheritdoc} 
     */ 
    public function getRouteCollection() 
    { 
        if (!$this->routeCollection instanceof RouteCollection) { 
            $this->routeCollection = new ChainRouteCollection(); 
            foreach ($this->all() as $router) { 
                $this->routeCollection->addCollection($router->getRouteCollection()); 
            } 
        } 
 
        return $this->routeCollection; 
    } 
 
    /** 
     * Identify if any routers have been added into the chain yet. 
     * 
     * @return bool 
     */ 
    public function hasRouters() 
    { 
        return 0 < count($this->routers); 
    } 
}