Newer
Older
use Drupal\Core\Database\DatabaseException;
Alex Pott
committed
use Drupal\Core\State\StateInterface;
Lee Rowlands
committed
use Drupal\Core\Utility\Error;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\RouteCollection;
use Drupal\Core\Database\Connection;
/**
Larry Garfield
committed
* Dumps Route information to a database table.
catch
committed
*
* @see \Drupal\Core\Routing\RouteProvider
Larry Garfield
committed
class MatcherDumper implements MatcherDumperInterface {
/**
* The database connection to which to dump route information.
*
Alex Pott
committed
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The routes to be dumped.
*
* @var \Symfony\Component\Routing\RouteCollection
/**
* The state.
*
Alex Pott
committed
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The name of the SQL table to which to dump the routes.
*
* @var string
*/
protected $tableName;
Lee Rowlands
committed
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected LoggerInterface $logger;
/**
* Construct the MatcherDumper.
*
Alex Pott
committed
* @param \Drupal\Core\Database\Connection $connection
* The database connection which will be used to store the route
* information.
Alex Pott
committed
* @param \Drupal\Core\State\StateInterface $state
* The state.
Lee Rowlands
committed
* @param \Psr\Log\LoggerInterface|null $logger
* The logger.
* @param string $table
* (optional) The table to store the route info in. Defaults to 'router'.
*/
Lee Rowlands
committed
public function __construct(Connection $connection, StateInterface $state, LoggerInterface|string|null $logger = NULL, $table = 'router') {
$this->state = $state;
Lee Rowlands
committed
if (is_string($logger) || is_null($logger)) {
@trigger_error('Calling ' . __METHOD__ . '() without the $logger argument is deprecated in drupal:10.1.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/2932520', E_USER_DEPRECATED);
$this->logger = \Drupal::service('logger.channel.router');
$this->tableName = $logger;
}
else {
$this->logger = $logger;
}
if (is_null($this->tableName)) {
$this->tableName = $table;
}
* {@inheritdoc}
*/
public function addRoutes(RouteCollection $routes) {
if (empty($this->routes)) {
$this->routes = $routes;
}
else {
$this->routes->addCollection($routes);
}
}
/**
* Dumps a set of routes to the router table in the database.
* - provider: The route grouping that is being dumped. All existing
* routes with this provider will be deleted on dump.
* - base_class: The base class name.
* An array of options.
*
* @throws \Exception
* Thrown if the table could not be created or the database connection
* failed.
catch
committed
public function dump(array $options = []): string {
// Convert all of the routes into database records.
// Accumulate the menu masks on top of any we found before.
$masks = array_flip($this->state->get('routing.menu_masks.' . $this->tableName, []));
// Delete any old records first, then insert the new ones. That avoids
// stale data. The transaction makes it atomic to avoid unstable router
// states due to random failures.
try {
$transaction = $this->connection->startTransaction();
// We don't use truncate, because it is not guaranteed to be transaction
// safe.
catch
committed
try {
$this->connection->delete($this->tableName)
->execute();
}
catch (\Exception $e) {
if (!$this->ensureTableExists()) {
throw $e;
}
catch
committed
}
// Split the routes into chunks to avoid big INSERT queries.
$route_chunks = array_chunk($this->routes->all(), 50, TRUE);
foreach ($route_chunks as $routes) {
$insert = $this->connection->insert($this->tableName)->fields([
'name',
'fit',
'path',
'pattern_outline',
'number_parts',
'route',
]);
$names = [];
foreach ($routes as $name => $route) {
/** @var \Symfony\Component\Routing\Route $route */
Lee Rowlands
committed
$route->setOption('compiler_class', RouteCompiler::class);
/** @var \Drupal\Core\Routing\CompiledRoute $compiled */
$compiled = $route->compile();
// The fit value is a binary number which has 1 at every fixed path
// position and 0 where there is a wildcard. We keep track of all such
// patterns that exist so that we can minimize the number of path
// patterns we need to check in the RouteProvider.
$masks[$compiled->getFit()] = 1;
$names[] = $name;
$values = [
'name' => $name,
'fit' => $compiled->getFit(),
'path' => $route->getPath(),
'pattern_outline' => $compiled->getPatternOutline(),
'number_parts' => $compiled->getNumParts(),
'route' => serialize($route),
];
$insert->values($values);
}
// Insert all new routes.
$insert->execute();
}
}
catch (\Exception $e) {
if (isset($transaction)) {
$transaction->rollBack();
}
Lee Rowlands
committed
Error::logException($this->logger, $e);
throw $e;
}
// Sort the masks so they are in order of descending fit.
$masks = array_keys($masks);
rsort($masks);
$this->state->set('routing.menu_masks.' . $this->tableName, $masks);
$this->routes = NULL;
catch
committed
return '';
}
/**
* Gets the routes to match.
*
* @return \Symfony\Component\Routing\RouteCollection
* A RouteCollection instance representing all routes currently in the
* dumper.
catch
committed
public function getRoutes(): RouteCollection {
catch
committed
/**
* Checks if the tree table exists and create it if not.
*
* @return bool
* TRUE if the table was created, FALSE otherwise.
*/
protected function ensureTableExists() {
try {
$this->connection->schema()->createTable($this->tableName, $this->schemaDefinition());
catch
committed
}
catch (DatabaseException $e) {
catch
committed
// If another process has already created the config table, attempting to
// recreate it will throw an exception. In this case just catch the
// exception and do nothing.
}
catch (\Exception $e) {
return FALSE;
}
return TRUE;
catch
committed
}
/**
* Defines the schema for the router table.
*
* @return array
* The schema API definition for the SQL storage table.
Lauri Eskola
committed
*
* @internal
catch
committed
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
*/
protected function schemaDefinition() {
$schema = [
'description' => 'Maps paths to various callbacks (access, page and title)',
'fields' => [
'name' => [
'description' => 'Primary Key: Machine name of this route',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'path' => [
'description' => 'The path for this URI',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'pattern_outline' => [
'description' => 'The pattern',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'fit' => [
'description' => 'A numeric representation of how specific the path is.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
],
'route' => [
'description' => 'A serialized Route object',
'type' => 'blob',
'size' => 'big',
],
'number_parts' => [
'description' => 'Number of parts in this router path.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'small',
],
],
'indexes' => [
'pattern_outline_parts' => ['pattern_outline', 'number_parts'],
],
'primary key' => ['name'],
];
return $schema;
}