3 namespace Drupal\Core\Path;
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\Database\SchemaObjectExistsException;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\Language\LanguageInterface;
10 use Drupal\Core\Database\Query\Condition;
13 * Provides a class for CRUD operations on path aliases.
15 * All queries perform case-insensitive matching on the 'source' and 'alias'
16 * fields, so the aliases '/test-alias' and '/test-Alias' are considered to be
17 * the same, and will both refer to the same internal system path.
19 class AliasStorage implements AliasStorageInterface {
22 * The table for the url_alias storage.
24 const TABLE = 'url_alias';
27 * The database connection.
29 * @var \Drupal\Core\Database\Connection
31 protected $connection;
36 * @var \Drupal\Core\Extension\ModuleHandlerInterface
38 protected $moduleHandler;
41 * Constructs a Path CRUD object.
43 * @param \Drupal\Core\Database\Connection $connection
44 * A database connection for reading and writing path aliases.
45 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
48 public function __construct(Connection $connection, ModuleHandlerInterface $module_handler) {
49 $this->connection = $connection;
50 $this->moduleHandler = $module_handler;
56 public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, $pid = NULL) {
58 if ($source[0] !== '/') {
59 throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $source));
62 if ($alias[0] !== '/') {
63 throw new \InvalidArgumentException(sprintf('Alias path %s has to start with a slash.', $alias));
69 'langcode' => $langcode,
72 // Insert or update the alias.
76 $query = $this->connection->insert(static::TABLE)
78 $pid = $query->execute();
80 catch (\Exception $e) {
81 // If there was an exception, try to create the table.
82 if (!$try_again = $this->ensureTableExists()) {
83 // If the exception happened for other reason than the missing table,
84 // propagate the exception.
88 // Now that the table has been created, try again if necessary.
90 $query = $this->connection->insert(static::TABLE)
92 $pid = $query->execute();
95 $fields['pid'] = $pid;
96 $operation = 'insert';
99 // Fetch the current values so that an update hook can identify what
102 $original = $this->connection->query('SELECT source, alias, langcode FROM {url_alias} WHERE pid = :pid', [':pid' => $pid])
105 catch (\Exception $e) {
106 $this->catchException($e);
109 $query = $this->connection->update(static::TABLE)
111 ->condition('pid', $pid);
112 $pid = $query->execute();
113 $fields['pid'] = $pid;
114 $fields['original'] = $original;
115 $operation = 'update';
118 // @todo Switch to using an event for this instead of a hook.
119 $this->moduleHandler->invokeAll('path_' . $operation, [$fields]);
120 Cache::invalidateTags(['route_match']);
129 public function load($conditions) {
130 $select = $this->connection->select(static::TABLE);
131 foreach ($conditions as $field => $value) {
132 if ($field == 'source' || $field == 'alias') {
133 // Use LIKE for case-insensitive matching.
134 $select->condition($field, $this->connection->escapeLike($value), 'LIKE');
137 $select->condition($field, $value);
142 ->fields(static::TABLE)
143 ->orderBy('pid', 'DESC')
148 catch (\Exception $e) {
149 $this->catchException($e);
157 public function delete($conditions) {
158 $path = $this->load($conditions);
159 $query = $this->connection->delete(static::TABLE);
160 foreach ($conditions as $field => $value) {
161 if ($field == 'source' || $field == 'alias') {
162 // Use LIKE for case-insensitive matching.
163 $query->condition($field, $this->connection->escapeLike($value), 'LIKE');
166 $query->condition($field, $value);
170 $deleted = $query->execute();
172 catch (\Exception $e) {
173 $this->catchException($e);
176 // @todo Switch to using an event for this instead of a hook.
177 $this->moduleHandler->invokeAll('path_delete', [$path]);
178 Cache::invalidateTags(['route_match']);
185 public function preloadPathAlias($preloaded, $langcode) {
186 $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
187 $select = $this->connection->select(static::TABLE)
188 ->fields(static::TABLE, ['source', 'alias']);
190 if (!empty($preloaded)) {
191 $conditions = new Condition('OR');
192 foreach ($preloaded as $preloaded_item) {
193 $conditions->condition('source', $this->connection->escapeLike($preloaded_item), 'LIKE');
195 $select->condition($conditions);
198 // Always get the language-specific alias before the language-neutral one.
199 // For example 'de' is less than 'und' so the order needs to be ASC, while
200 // 'xx-lolspeak' is more than 'und' so the order needs to be DESC. We also
201 // order by pid ASC so that fetchAllKeyed() returns the most recently
202 // created alias for each source. Subsequent queries using fetchField() must
203 // use pid DESC to have the same effect.
204 if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
205 array_pop($langcode_list);
207 elseif ($langcode < LanguageInterface::LANGCODE_NOT_SPECIFIED) {
208 $select->orderBy('langcode', 'ASC');
211 $select->orderBy('langcode', 'DESC');
214 $select->orderBy('pid', 'ASC');
215 $select->condition('langcode', $langcode_list, 'IN');
217 return $select->execute()->fetchAllKeyed();
219 catch (\Exception $e) {
220 $this->catchException($e);
228 public function lookupPathAlias($path, $langcode) {
229 $source = $this->connection->escapeLike($path);
230 $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
232 // See the queries above. Use LIKE for case-insensitive matching.
233 $select = $this->connection->select(static::TABLE)
234 ->fields(static::TABLE, ['alias'])
235 ->condition('source', $source, 'LIKE');
236 if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
237 array_pop($langcode_list);
239 elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
240 $select->orderBy('langcode', 'DESC');
243 $select->orderBy('langcode', 'ASC');
246 $select->orderBy('pid', 'DESC');
247 $select->condition('langcode', $langcode_list, 'IN');
249 return $select->execute()->fetchField();
251 catch (\Exception $e) {
252 $this->catchException($e);
260 public function lookupPathSource($path, $langcode) {
261 $alias = $this->connection->escapeLike($path);
262 $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
264 // See the queries above. Use LIKE for case-insensitive matching.
265 $select = $this->connection->select(static::TABLE)
266 ->fields(static::TABLE, ['source'])
267 ->condition('alias', $alias, 'LIKE');
268 if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
269 array_pop($langcode_list);
271 elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
272 $select->orderBy('langcode', 'DESC');
275 $select->orderBy('langcode', 'ASC');
278 $select->orderBy('pid', 'DESC');
279 $select->condition('langcode', $langcode_list, 'IN');
281 return $select->execute()->fetchField();
283 catch (\Exception $e) {
284 $this->catchException($e);
292 public function aliasExists($alias, $langcode, $source = NULL) {
293 // Use LIKE and NOT LIKE for case-insensitive matching.
294 $query = $this->connection->select(static::TABLE)
295 ->condition('alias', $this->connection->escapeLike($alias), 'LIKE')
296 ->condition('langcode', $langcode);
297 if (!empty($source)) {
298 $query->condition('source', $this->connection->escapeLike($source), 'NOT LIKE');
300 $query->addExpression('1');
303 return (bool) $query->execute()->fetchField();
305 catch (\Exception $e) {
306 $this->catchException($e);
314 public function languageAliasExists() {
316 return (bool) $this->connection->queryRange('SELECT 1 FROM {url_alias} WHERE langcode <> :langcode', 0, 1, [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])->fetchField();
318 catch (\Exception $e) {
319 $this->catchException($e);
327 public function getAliasesForAdminListing($header, $keys = NULL) {
328 $query = $this->connection->select(static::TABLE)
329 ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
330 ->extend('Drupal\Core\Database\Query\TableSortExtender');
332 // Replace wildcards with PDO wildcards.
333 $query->condition('alias', '%' . preg_replace('!\*+!', '%', $keys) . '%', 'LIKE');
337 ->fields(static::TABLE)
338 ->orderByHeader($header)
343 catch (\Exception $e) {
344 $this->catchException($e);
352 public function pathHasMatchingAlias($initial_substring) {
353 $query = $this->connection->select(static::TABLE, 'u');
354 $query->addExpression(1);
357 ->condition('u.source', $this->connection->escapeLike($initial_substring) . '%', 'LIKE')
362 catch (\Exception $e) {
363 $this->catchException($e);
369 * Check if the table exists and create it if not.
371 protected function ensureTableExists() {
373 $database_schema = $this->connection->schema();
374 if (!$database_schema->tableExists(static::TABLE)) {
375 $schema_definition = $this->schemaDefinition();
376 $database_schema->createTable(static::TABLE, $schema_definition);
380 // If another process has already created the table, attempting to recreate
381 // it will throw an exception. In this case just catch the exception and do
383 catch (SchemaObjectExistsException $e) {
390 * Act on an exception when url_alias might be stale.
392 * If the table does not yet exist, that's fine, but if the table exists and
393 * yet the query failed, then the url_alias is stale and the exception needs
401 protected function catchException(\Exception $e) {
402 if ($this->connection->schema()->tableExists(static::TABLE)) {
408 * Defines the schema for the {url_alias} table.
412 public static function schemaDefinition() {
414 'description' => 'A list of URL aliases for Drupal paths; a user may visit either the source or destination path.',
417 'description' => 'A unique path alias identifier.',
423 'description' => 'The Drupal path this alias is for; e.g. node/12.',
430 'description' => 'The alias for this path; e.g. title-of-the-story.',
437 'description' => "The language code this alias is for; if 'und', the alias will be used for unknown languages. Each Drupal path can have an alias for each supported language.",
438 'type' => 'varchar_ascii',
444 'primary key' => ['pid'],
446 'alias_langcode_pid' => ['alias', 'langcode', 'pid'],
447 'source_langcode_pid' => ['source', 'langcode', 'pid'],