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