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 }
|