MatcherDumper.php 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. namespace Drupal\Core\Routing;
  3. use Drupal\Core\Database\DatabaseException;
  4. use Drupal\Core\State\StateInterface;
  5. use Symfony\Component\Routing\RouteCollection;
  6. use Drupal\Core\Database\Connection;
  7. /**
  8. * Dumps Route information to a database table.
  9. *
  10. * @see \Drupal\Core\Routing\RouteProvider
  11. */
  12. class MatcherDumper implements MatcherDumperInterface {
  13. /**
  14. * The database connection to which to dump route information.
  15. *
  16. * @var \Drupal\Core\Database\Connection
  17. */
  18. protected $connection;
  19. /**
  20. * The routes to be dumped.
  21. *
  22. * @var \Symfony\Component\Routing\RouteCollection
  23. */
  24. protected $routes;
  25. /**
  26. * The state.
  27. *
  28. * @var \Drupal\Core\State\StateInterface
  29. */
  30. protected $state;
  31. /**
  32. * The name of the SQL table to which to dump the routes.
  33. *
  34. * @var string
  35. */
  36. protected $tableName;
  37. /**
  38. * Construct the MatcherDumper.
  39. *
  40. * @param \Drupal\Core\Database\Connection $connection
  41. * The database connection which will be used to store the route
  42. * information.
  43. * @param \Drupal\Core\State\StateInterface $state
  44. * The state.
  45. * @param string $table
  46. * (optional) The table to store the route info in. Defaults to 'router'.
  47. */
  48. public function __construct(Connection $connection, StateInterface $state, $table = 'router') {
  49. $this->connection = $connection;
  50. $this->state = $state;
  51. $this->tableName = $table;
  52. }
  53. /**
  54. * {@inheritdoc}
  55. */
  56. public function addRoutes(RouteCollection $routes) {
  57. if (empty($this->routes)) {
  58. $this->routes = $routes;
  59. }
  60. else {
  61. $this->routes->addCollection($routes);
  62. }
  63. }
  64. /**
  65. * Dumps a set of routes to the router table in the database.
  66. *
  67. * Available options:
  68. * - provider: The route grouping that is being dumped. All existing
  69. * routes with this provider will be deleted on dump.
  70. * - base_class: The base class name.
  71. *
  72. * @param array $options
  73. * An array of options.
  74. */
  75. public function dump(array $options = []) {
  76. // Convert all of the routes into database records.
  77. // Accumulate the menu masks on top of any we found before.
  78. $masks = array_flip($this->state->get('routing.menu_masks.' . $this->tableName, []));
  79. // Delete any old records first, then insert the new ones. That avoids
  80. // stale data. The transaction makes it atomic to avoid unstable router
  81. // states due to random failures.
  82. $transaction = $this->connection->startTransaction();
  83. try {
  84. // We don't use truncate, because it is not guaranteed to be transaction
  85. // safe.
  86. try {
  87. $this->connection->delete($this->tableName)
  88. ->execute();
  89. }
  90. catch (\Exception $e) {
  91. $this->ensureTableExists();
  92. }
  93. // Split the routes into chunks to avoid big INSERT queries.
  94. $route_chunks = array_chunk($this->routes->all(), 50, TRUE);
  95. foreach ($route_chunks as $routes) {
  96. $insert = $this->connection->insert($this->tableName)->fields([
  97. 'name',
  98. 'fit',
  99. 'path',
  100. 'pattern_outline',
  101. 'number_parts',
  102. 'route',
  103. ]);
  104. $names = [];
  105. foreach ($routes as $name => $route) {
  106. /** @var \Symfony\Component\Routing\Route $route */
  107. $route->setOption('compiler_class', RouteCompiler::class);
  108. /** @var \Drupal\Core\Routing\CompiledRoute $compiled */
  109. $compiled = $route->compile();
  110. // The fit value is a binary number which has 1 at every fixed path
  111. // position and 0 where there is a wildcard. We keep track of all such
  112. // patterns that exist so that we can minimize the number of path
  113. // patterns we need to check in the RouteProvider.
  114. $masks[$compiled->getFit()] = 1;
  115. $names[] = $name;
  116. $values = [
  117. 'name' => $name,
  118. 'fit' => $compiled->getFit(),
  119. 'path' => $route->getPath(),
  120. 'pattern_outline' => $compiled->getPatternOutline(),
  121. 'number_parts' => $compiled->getNumParts(),
  122. 'route' => serialize($route),
  123. ];
  124. $insert->values($values);
  125. }
  126. // Insert all new routes.
  127. $insert->execute();
  128. }
  129. }
  130. catch (\Exception $e) {
  131. $transaction->rollBack();
  132. watchdog_exception('Routing', $e);
  133. throw $e;
  134. }
  135. // Sort the masks so they are in order of descending fit.
  136. $masks = array_keys($masks);
  137. rsort($masks);
  138. $this->state->set('routing.menu_masks.' . $this->tableName, $masks);
  139. $this->routes = NULL;
  140. }
  141. /**
  142. * Gets the routes to match.
  143. *
  144. * @return \Symfony\Component\Routing\RouteCollection
  145. * A RouteCollection instance representing all routes currently in the
  146. * dumper.
  147. */
  148. public function getRoutes() {
  149. return $this->routes;
  150. }
  151. /**
  152. * Checks if the tree table exists and create it if not.
  153. *
  154. * @return bool
  155. * TRUE if the table was created, FALSE otherwise.
  156. */
  157. protected function ensureTableExists() {
  158. try {
  159. if (!$this->connection->schema()->tableExists($this->tableName)) {
  160. $this->connection->schema()->createTable($this->tableName, $this->schemaDefinition());
  161. return TRUE;
  162. }
  163. }
  164. catch (DatabaseException $e) {
  165. // If another process has already created the config table, attempting to
  166. // recreate it will throw an exception. In this case just catch the
  167. // exception and do nothing.
  168. return TRUE;
  169. }
  170. return FALSE;
  171. }
  172. /**
  173. * Defines the schema for the router table.
  174. *
  175. * @return array
  176. * The schema API definition for the SQL storage table.
  177. *
  178. * @internal
  179. */
  180. protected function schemaDefinition() {
  181. $schema = [
  182. 'description' => 'Maps paths to various callbacks (access, page and title)',
  183. 'fields' => [
  184. 'name' => [
  185. 'description' => 'Primary Key: Machine name of this route',
  186. 'type' => 'varchar_ascii',
  187. 'length' => 255,
  188. 'not null' => TRUE,
  189. 'default' => '',
  190. ],
  191. 'path' => [
  192. 'description' => 'The path for this URI',
  193. 'type' => 'varchar',
  194. 'length' => 255,
  195. 'not null' => TRUE,
  196. 'default' => '',
  197. ],
  198. 'pattern_outline' => [
  199. 'description' => 'The pattern',
  200. 'type' => 'varchar',
  201. 'length' => 255,
  202. 'not null' => TRUE,
  203. 'default' => '',
  204. ],
  205. 'fit' => [
  206. 'description' => 'A numeric representation of how specific the path is.',
  207. 'type' => 'int',
  208. 'not null' => TRUE,
  209. 'default' => 0,
  210. ],
  211. 'route' => [
  212. 'description' => 'A serialized Route object',
  213. 'type' => 'blob',
  214. 'size' => 'big',
  215. ],
  216. 'number_parts' => [
  217. 'description' => 'Number of parts in this router path.',
  218. 'type' => 'int',
  219. 'not null' => TRUE,
  220. 'default' => 0,
  221. 'size' => 'small',
  222. ],
  223. ],
  224. 'indexes' => [
  225. 'pattern_outline_parts' => ['pattern_outline', 'number_parts'],
  226. ],
  227. 'primary key' => ['name'],
  228. ];
  229. return $schema;
  230. }
  231. }