Mercurial > hg > isophonics-drupal-site
comparison core/lib/Drupal/Core/Routing/RouteProvider.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 7a779792577d |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Core\Routing; | |
4 | |
5 use Drupal\Component\Utility\Unicode; | |
6 use Drupal\Core\Cache\Cache; | |
7 use Drupal\Core\Cache\CacheBackendInterface; | |
8 use Drupal\Core\Cache\CacheTagsInvalidatorInterface; | |
9 use Drupal\Core\Path\CurrentPathStack; | |
10 use Drupal\Core\PathProcessor\InboundPathProcessorInterface; | |
11 use Drupal\Core\State\StateInterface; | |
12 use Symfony\Cmf\Component\Routing\PagedRouteCollection; | |
13 use Symfony\Cmf\Component\Routing\PagedRouteProviderInterface; | |
14 use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
15 use Symfony\Component\HttpFoundation\Request; | |
16 use Symfony\Component\Routing\Exception\RouteNotFoundException; | |
17 use Symfony\Component\Routing\RouteCollection; | |
18 use Drupal\Core\Database\Connection; | |
19 | |
20 /** | |
21 * A Route Provider front-end for all Drupal-stored routes. | |
22 */ | |
23 class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProviderInterface, EventSubscriberInterface { | |
24 | |
25 /** | |
26 * The database connection from which to read route information. | |
27 * | |
28 * @var \Drupal\Core\Database\Connection | |
29 */ | |
30 protected $connection; | |
31 | |
32 /** | |
33 * The name of the SQL table from which to read the routes. | |
34 * | |
35 * @var string | |
36 */ | |
37 protected $tableName; | |
38 | |
39 /** | |
40 * The state. | |
41 * | |
42 * @var \Drupal\Core\State\StateInterface | |
43 */ | |
44 protected $state; | |
45 | |
46 /** | |
47 * A cache of already-loaded routes, keyed by route name. | |
48 * | |
49 * @var \Symfony\Component\Routing\Route[] | |
50 */ | |
51 protected $routes = []; | |
52 | |
53 /** | |
54 * A cache of already-loaded serialized routes, keyed by route name. | |
55 * | |
56 * @var string[] | |
57 */ | |
58 protected $serializedRoutes = []; | |
59 | |
60 /** | |
61 * The current path. | |
62 * | |
63 * @var \Drupal\Core\Path\CurrentPathStack | |
64 */ | |
65 protected $currentPath; | |
66 | |
67 /** | |
68 * The cache backend. | |
69 * | |
70 * @var \Drupal\Core\Cache\CacheBackendInterface | |
71 */ | |
72 protected $cache; | |
73 | |
74 /** | |
75 * The cache tag invalidator. | |
76 * | |
77 * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface | |
78 */ | |
79 protected $cacheTagInvalidator; | |
80 | |
81 /** | |
82 * A path processor manager for resolving the system path. | |
83 * | |
84 * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface | |
85 */ | |
86 protected $pathProcessor; | |
87 | |
88 /** | |
89 * Cache ID prefix used to load routes. | |
90 */ | |
91 const ROUTE_LOAD_CID_PREFIX = 'route_provider.route_load:'; | |
92 | |
93 /** | |
94 * Constructs a new PathMatcher. | |
95 * | |
96 * @param \Drupal\Core\Database\Connection $connection | |
97 * A database connection object. | |
98 * @param \Drupal\Core\State\StateInterface $state | |
99 * The state. | |
100 * @param \Drupal\Core\Path\CurrentPathStack $current_path | |
101 * The current path. | |
102 * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend | |
103 * The cache backend. | |
104 * @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor | |
105 * The path processor. | |
106 * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tag_invalidator | |
107 * The cache tag invalidator. | |
108 * @param string $table | |
109 * (Optional) The table in the database to use for matching. Defaults to 'router' | |
110 */ | |
111 public function __construct(Connection $connection, StateInterface $state, CurrentPathStack $current_path, CacheBackendInterface $cache_backend, InboundPathProcessorInterface $path_processor, CacheTagsInvalidatorInterface $cache_tag_invalidator, $table = 'router') { | |
112 $this->connection = $connection; | |
113 $this->state = $state; | |
114 $this->currentPath = $current_path; | |
115 $this->cache = $cache_backend; | |
116 $this->cacheTagInvalidator = $cache_tag_invalidator; | |
117 $this->pathProcessor = $path_processor; | |
118 $this->tableName = $table; | |
119 } | |
120 | |
121 /** | |
122 * Finds routes that may potentially match the request. | |
123 * | |
124 * This may return a mixed list of class instances, but all routes returned | |
125 * must extend the core symfony route. The classes may also implement | |
126 * RouteObjectInterface to link to a content document. | |
127 * | |
128 * This method may not throw an exception based on implementation specific | |
129 * restrictions on the url. That case is considered a not found - returning | |
130 * an empty array. Exceptions are only used to abort the whole request in | |
131 * case something is seriously broken, like the storage backend being down. | |
132 * | |
133 * Note that implementations may not implement an optimal matching | |
134 * algorithm, simply a reasonable first pass. That allows for potentially | |
135 * very large route sets to be filtered down to likely candidates, which | |
136 * may then be filtered in memory more completely. | |
137 * | |
138 * @param Request $request | |
139 * A request against which to match. | |
140 * | |
141 * @return \Symfony\Component\Routing\RouteCollection | |
142 * RouteCollection with all urls that could potentially match $request. | |
143 * Empty collection if nothing can match. The collection will be sorted from | |
144 * highest to lowest fit (match of path parts) and then in ascending order | |
145 * by route name for routes with the same fit. | |
146 */ | |
147 public function getRouteCollectionForRequest(Request $request) { | |
148 // Cache both the system path as well as route parameters and matching | |
149 // routes. | |
150 $cid = 'route:' . $request->getPathInfo() . ':' . $request->getQueryString(); | |
151 if ($cached = $this->cache->get($cid)) { | |
152 $this->currentPath->setPath($cached->data['path'], $request); | |
153 $request->query->replace($cached->data['query']); | |
154 return $cached->data['routes']; | |
155 } | |
156 else { | |
157 // Just trim on the right side. | |
158 $path = $request->getPathInfo(); | |
159 $path = $path === '/' ? $path : rtrim($request->getPathInfo(), '/'); | |
160 $path = $this->pathProcessor->processInbound($path, $request); | |
161 $this->currentPath->setPath($path, $request); | |
162 // Incoming path processors may also set query parameters. | |
163 $query_parameters = $request->query->all(); | |
164 $routes = $this->getRoutesByPath(rtrim($path, '/')); | |
165 $cache_value = [ | |
166 'path' => $path, | |
167 'query' => $query_parameters, | |
168 'routes' => $routes, | |
169 ]; | |
170 $this->cache->set($cid, $cache_value, CacheBackendInterface::CACHE_PERMANENT, ['route_match']); | |
171 return $routes; | |
172 } | |
173 } | |
174 | |
175 /** | |
176 * Find the route using the provided route name (and parameters). | |
177 * | |
178 * @param string $name | |
179 * The route name to fetch | |
180 * | |
181 * @return \Symfony\Component\Routing\Route | |
182 * The found route. | |
183 * | |
184 * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException | |
185 * Thrown if there is no route with that name in this repository. | |
186 */ | |
187 public function getRouteByName($name) { | |
188 $routes = $this->getRoutesByNames([$name]); | |
189 if (empty($routes)) { | |
190 throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name)); | |
191 } | |
192 | |
193 return reset($routes); | |
194 } | |
195 | |
196 /** | |
197 * {@inheritdoc} | |
198 */ | |
199 public function preLoadRoutes($names) { | |
200 if (empty($names)) { | |
201 throw new \InvalidArgumentException('You must specify the route names to load'); | |
202 } | |
203 | |
204 $routes_to_load = array_diff($names, array_keys($this->routes), array_keys($this->serializedRoutes)); | |
205 if ($routes_to_load) { | |
206 | |
207 $cid = static::ROUTE_LOAD_CID_PREFIX . hash('sha512', serialize($routes_to_load)); | |
208 if ($cache = $this->cache->get($cid)) { | |
209 $routes = $cache->data; | |
210 } | |
211 else { | |
212 try { | |
213 $result = $this->connection->query('SELECT name, route FROM {' . $this->connection->escapeTable($this->tableName) . '} WHERE name IN ( :names[] )', [':names[]' => $routes_to_load]); | |
214 $routes = $result->fetchAllKeyed(); | |
215 | |
216 $this->cache->set($cid, $routes, Cache::PERMANENT, ['routes']); | |
217 } | |
218 catch (\Exception $e) { | |
219 $routes = []; | |
220 } | |
221 } | |
222 | |
223 $this->serializedRoutes += $routes; | |
224 } | |
225 } | |
226 | |
227 /** | |
228 * {@inheritdoc} | |
229 */ | |
230 public function getRoutesByNames($names) { | |
231 $this->preLoadRoutes($names); | |
232 | |
233 foreach ($names as $name) { | |
234 // The specified route name might not exist or might be serialized. | |
235 if (!isset($this->routes[$name]) && isset($this->serializedRoutes[$name])) { | |
236 $this->routes[$name] = unserialize($this->serializedRoutes[$name]); | |
237 unset($this->serializedRoutes[$name]); | |
238 } | |
239 } | |
240 | |
241 return array_intersect_key($this->routes, array_flip($names)); | |
242 } | |
243 | |
244 /** | |
245 * Returns an array of path pattern outlines that could match the path parts. | |
246 * | |
247 * @param array $parts | |
248 * The parts of the path for which we want candidates. | |
249 * | |
250 * @return array | |
251 * An array of outlines that could match the specified path parts. | |
252 */ | |
253 protected function getCandidateOutlines(array $parts) { | |
254 $number_parts = count($parts); | |
255 $ancestors = []; | |
256 $length = $number_parts - 1; | |
257 $end = (1 << $number_parts) - 1; | |
258 | |
259 // The highest possible mask is a 1 bit for every part of the path. We will | |
260 // check every value down from there to generate a possible outline. | |
261 if ($number_parts == 1) { | |
262 $masks = [1]; | |
263 } | |
264 elseif ($number_parts <= 3 && $number_parts > 0) { | |
265 // Optimization - don't query the state system for short paths. This also | |
266 // insulates against the state entry for masks going missing for common | |
267 // user-facing paths since we generate all values without checking state. | |
268 $masks = range($end, 1); | |
269 } | |
270 elseif ($number_parts <= 0) { | |
271 // No path can match, short-circuit the process. | |
272 $masks = []; | |
273 } | |
274 else { | |
275 // Get the actual patterns that exist out of state. | |
276 $masks = (array) $this->state->get('routing.menu_masks.' . $this->tableName, []); | |
277 } | |
278 | |
279 // Only examine patterns that actually exist as router items (the masks). | |
280 foreach ($masks as $i) { | |
281 if ($i > $end) { | |
282 // Only look at masks that are not longer than the path of interest. | |
283 continue; | |
284 } | |
285 elseif ($i < (1 << $length)) { | |
286 // We have exhausted the masks of a given length, so decrease the length. | |
287 --$length; | |
288 } | |
289 $current = ''; | |
290 for ($j = $length; $j >= 0; $j--) { | |
291 // Check the bit on the $j offset. | |
292 if ($i & (1 << $j)) { | |
293 // Bit one means the original value. | |
294 $current .= $parts[$length - $j]; | |
295 } | |
296 else { | |
297 // Bit zero means means wildcard. | |
298 $current .= '%'; | |
299 } | |
300 // Unless we are at offset 0, add a slash. | |
301 if ($j) { | |
302 $current .= '/'; | |
303 } | |
304 } | |
305 $ancestors[] = '/' . $current; | |
306 } | |
307 return $ancestors; | |
308 } | |
309 | |
310 /** | |
311 * {@inheritdoc} | |
312 */ | |
313 public function getRoutesByPattern($pattern) { | |
314 $path = RouteCompiler::getPatternOutline($pattern); | |
315 | |
316 return $this->getRoutesByPath($path); | |
317 } | |
318 | |
319 /** | |
320 * Get all routes which match a certain pattern. | |
321 * | |
322 * @param string $path | |
323 * The route pattern to search for. | |
324 * | |
325 * @return \Symfony\Component\Routing\RouteCollection | |
326 * Returns a route collection of matching routes. The collection may be | |
327 * empty and will be sorted from highest to lowest fit (match of path parts) | |
328 * and then in ascending order by route name for routes with the same fit. | |
329 */ | |
330 protected function getRoutesByPath($path) { | |
331 // Split the path up on the slashes, ignoring multiple slashes in a row | |
332 // or leading or trailing slashes. Convert to lower case here so we can | |
333 // have a case-insensitive match from the incoming path to the lower case | |
334 // pattern outlines from \Drupal\Core\Routing\RouteCompiler::compile(). | |
335 // @see \Drupal\Core\Routing\CompiledRoute::__construct() | |
336 $parts = preg_split('@/+@', Unicode::strtolower($path), NULL, PREG_SPLIT_NO_EMPTY); | |
337 | |
338 $collection = new RouteCollection(); | |
339 | |
340 $ancestors = $this->getCandidateOutlines($parts); | |
341 if (empty($ancestors)) { | |
342 return $collection; | |
343 } | |
344 | |
345 // The >= check on number_parts allows us to match routes with optional | |
346 // trailing wildcard parts as long as the pattern matches, since we | |
347 // dump the route pattern without those optional parts. | |
348 try { | |
349 $routes = $this->connection->query("SELECT name, route, fit FROM {" . $this->connection->escapeTable($this->tableName) . "} WHERE pattern_outline IN ( :patterns[] ) AND number_parts >= :count_parts", [ | |
350 ':patterns[]' => $ancestors, | |
351 ':count_parts' => count($parts), | |
352 ]) | |
353 ->fetchAll(\PDO::FETCH_ASSOC); | |
354 } | |
355 catch (\Exception $e) { | |
356 $routes = []; | |
357 } | |
358 | |
359 // We sort by fit and name in PHP to avoid a SQL filesort and avoid any | |
360 // difference in the sorting behavior of SQL back-ends. | |
361 usort($routes, [$this, 'routeProviderRouteCompare']); | |
362 | |
363 foreach ($routes as $row) { | |
364 $collection->add($row['name'], unserialize($row['route'])); | |
365 } | |
366 | |
367 return $collection; | |
368 } | |
369 | |
370 /** | |
371 * Comparison function for usort on routes. | |
372 */ | |
373 protected function routeProviderRouteCompare(array $a, array $b) { | |
374 if ($a['fit'] == $b['fit']) { | |
375 return strcmp($a['name'], $b['name']); | |
376 } | |
377 // Reverse sort from highest to lowest fit. PHP should cast to int, but | |
378 // the explicit cast makes this sort more robust against unexpected input. | |
379 return (int) $a['fit'] < (int) $b['fit'] ? 1 : -1; | |
380 } | |
381 | |
382 /** | |
383 * {@inheritdoc} | |
384 */ | |
385 public function getAllRoutes() { | |
386 return new PagedRouteCollection($this); | |
387 } | |
388 | |
389 /** | |
390 * {@inheritdoc} | |
391 */ | |
392 public function reset() { | |
393 $this->routes = []; | |
394 $this->serializedRoutes = []; | |
395 $this->cacheTagInvalidator->invalidateTags(['routes']); | |
396 } | |
397 | |
398 /** | |
399 * {@inheritdoc} | |
400 */ | |
401 public static function getSubscribedEvents() { | |
402 $events[RoutingEvents::FINISHED][] = ['reset']; | |
403 return $events; | |
404 } | |
405 | |
406 /** | |
407 * {@inheritdoc} | |
408 */ | |
409 public function getRoutesPaged($offset, $length = NULL) { | |
410 $select = $this->connection->select($this->tableName, 'router') | |
411 ->fields('router', ['name', 'route']); | |
412 | |
413 if (isset($length)) { | |
414 $select->range($offset, $length); | |
415 } | |
416 | |
417 $routes = $select->execute()->fetchAllKeyed(); | |
418 | |
419 $result = []; | |
420 foreach ($routes as $name => $route) { | |
421 $result[$name] = unserialize($route); | |
422 } | |
423 | |
424 return $result; | |
425 } | |
426 | |
427 /** | |
428 * {@inheritdoc} | |
429 */ | |
430 public function getRoutesCount() { | |
431 return $this->connection->query("SELECT COUNT(*) FROM {" . $this->connection->escapeTable($this->tableName) . "}")->fetchField(); | |
432 } | |
433 | |
434 } |