Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,7 @@
name: Path alias
type: module
description: 'Provides the API allowing to rename URLs.'
package: Core
version: VERSION
required: true
hidden: true

View File

@ -0,0 +1,25 @@
<?php
/**
* @file
* Post update functions for Path Alias.
*/
/**
* Implements hook_removed_post_updates().
*/
function path_alias_removed_post_updates(): array {
return [
'path_alias_post_update_drop_path_alias_status_index' => '11.0.0',
];
}
/**
* Update the path_alias_revision indices.
*/
function path_alias_post_update_update_path_alias_revision_indexes(): void {
/** @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $update_manager */
$update_manager = \Drupal::service('entity.definition_update_manager');
$entity_type = $update_manager->getEntityType('path_alias');
$update_manager->updateEntityType($entity_type);
}

View File

@ -0,0 +1,36 @@
services:
_defaults:
autoconfigure: true
path_alias.subscriber:
class: Drupal\path_alias\EventSubscriber\PathAliasSubscriber
arguments: ['@path_alias.manager', '@path.current']
path_alias.path_processor:
class: Drupal\path_alias\PathProcessor\AliasPathProcessor
tags:
- { name: path_processor_inbound, priority: 100 }
- { name: path_processor_outbound, priority: 300 }
arguments: ['@path_alias.manager']
path_alias.manager:
class: Drupal\path_alias\AliasManager
arguments: ['@path_alias.repository', '@path_alias.prefix_list', '@language_manager', '@cache.data', '@datetime.time']
Drupal\path_alias\AliasManagerInterface: '@path_alias.manager'
path_alias.repository:
class: Drupal\path_alias\AliasRepository
arguments: ['@database']
tags:
- { name: backend_overridable }
Drupal\path_alias\AliasRepositoryInterface: '@path_alias.repository'
# cspell:ignore whitelist
path_alias.whitelist:
class: Drupal\path_alias\AliasWhitelist
tags:
- { name: needs_destruction }
arguments: [path_alias_whitelist, '@cache.bootstrap', '@lock', '@state', '@path_alias.repository']
deprecated: The "%service_id%" service is deprecated in drupal:11.1.0 and is removed from drupal:12.0.0. Use the 'router.prefix_list' service instead. See https://www.drupal.org/node/3467559
Drupal\path_alias\AliasWhitelistInterface: '@path_alias.whitelist'
path_alias.prefix_list:
class: Drupal\path_alias\AliasPrefixList
tags:
- { name: needs_destruction }
arguments: [path_alias_prefix_list, '@cache.bootstrap', '@lock', '@state', '@path_alias.repository']
Drupal\path_alias\AliasPrefixListInterface: '@path_alias.prefix_list'

View File

@ -0,0 +1,271 @@
<?php
namespace Drupal\path_alias;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
/**
* The default alias manager implementation.
*/
class AliasManager implements AliasManagerInterface {
/**
* The cache key to use when caching paths.
*
* @var string
*/
protected $cacheKey;
/**
* Whether the cache needs to be written.
*
* @var bool
*/
protected $cacheNeedsWriting = FALSE;
/**
* Holds the map of path lookups per language.
*
* @var array
*/
protected $lookupMap = [];
/**
* Holds an array of aliases for which no path was found.
*
* @var array
*/
protected $noPath = [];
/**
* Holds an array of paths that have no alias.
*
* @var array
*/
protected $noAlias = [];
/**
* Whether preloaded path lookups has already been loaded.
*
* @var array
*/
protected $langcodePreloaded = [];
/**
* Holds an array of previously looked up paths for the current request path.
*
* This will only get populated if a cache key has been set, which for example
* happens if the alias manager is used in the context of a request.
*
* @var array
*/
protected $preloadedPathLookups = FALSE;
public function __construct(
protected AliasRepositoryInterface $pathAliasRepository,
protected AliasPrefixListInterface $pathPrefixes,
protected LanguageManagerInterface $languageManager,
protected CacheBackendInterface $cache,
protected TimeInterface $time,
) {
}
/**
* {@inheritdoc}
*/
public function setCacheKey($key) {
// Prefix the cache key to avoid clashes with other caches.
$this->cacheKey = 'preload-paths:' . $key;
}
/**
* {@inheritdoc}
*
* Cache an array of the paths available on each page. We assume that aliases
* will be needed for the majority of these paths during subsequent requests,
* and load them in a single query during path alias lookup.
*/
public function writeCache() {
// Check if the paths for this page were loaded from cache in this request
// to avoid writing to cache on every request.
if ($this->cacheNeedsWriting && !empty($this->cacheKey)) {
// Start with the preloaded path lookups, so that cached entries for other
// languages will not be lost.
$path_lookups = $this->preloadedPathLookups ?: [];
foreach ($this->lookupMap as $langcode => $lookups) {
$path_lookups[$langcode] = array_keys($lookups);
if (!empty($this->noAlias[$langcode])) {
$path_lookups[$langcode] = array_merge($path_lookups[$langcode], array_keys($this->noAlias[$langcode]));
}
}
$twenty_four_hours = 60 * 60 * 24;
$this->cache->set($this->cacheKey, $path_lookups, $this->time->getRequestTime() + $twenty_four_hours);
}
}
/**
* {@inheritdoc}
*/
public function getPathByAlias($alias, $langcode = NULL) {
// If no language is explicitly specified we default to the current URL
// language. If we used a language different from the one conveyed by the
// requested URL, we might end up being unable to check if there is a path
// alias matching the URL path.
$langcode = $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
// If we already know that there are no paths for this alias simply return.
if (empty($alias) || !empty($this->noPath[$langcode][$alias])) {
return $alias;
}
// Look for the alias within the cached map.
if (isset($this->lookupMap[$langcode]) && ($path = array_search($alias, $this->lookupMap[$langcode]))) {
return $path;
}
// Look for path in storage.
if ($path_alias = $this->pathAliasRepository->lookupByAlias($alias, $langcode)) {
$this->lookupMap[$langcode][$path_alias['path']] = $alias;
return $path_alias['path'];
}
// We can't record anything into $this->lookupMap because we didn't find any
// paths for this alias. Thus cache to $this->noPath.
$this->noPath[$langcode][$alias] = TRUE;
return $alias;
}
/**
* {@inheritdoc}
*/
public function getAliasByPath($path, $langcode = NULL) {
if (!str_starts_with($path, '/')) {
throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $path));
}
// If no language is explicitly specified we default to the current URL
// language. If we used a language different from the one conveyed by the
// requested URL, we might end up being unable to check if there is a path
// alias matching the URL path.
$langcode = $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
// Check the path prefix, if the top-level part before the first / is not in
// the list, then there is no need to do anything further, it is not in the
// database.
if ($path === '/' || !$this->pathPrefixes->get(strtok(trim($path, '/'), '/'))) {
return $path;
}
// During the first call to this method per language, load the expected
// paths for the page from cache.
if (empty($this->langcodePreloaded[$langcode])) {
$this->langcodePreloaded[$langcode] = TRUE;
$this->lookupMap[$langcode] = [];
// Load the cached paths that should be used for preloading. This only
// happens if a cache key has been set.
if ($this->preloadedPathLookups === FALSE) {
$this->preloadedPathLookups = [];
if ($this->cacheKey) {
if ($cached = $this->cache->get($this->cacheKey)) {
$this->preloadedPathLookups = $cached->data;
}
else {
$this->cacheNeedsWriting = TRUE;
}
}
}
// Load paths from cache.
if (!empty($this->preloadedPathLookups[$langcode])) {
$this->lookupMap[$langcode] = $this->pathAliasRepository->preloadPathAlias($this->preloadedPathLookups[$langcode], $langcode);
// Keep a record of paths with no alias to avoid querying twice.
$this->noAlias[$langcode] = array_flip(array_diff($this->preloadedPathLookups[$langcode], array_keys($this->lookupMap[$langcode])));
}
}
// If we already know that there are no aliases for this path simply return.
if (!empty($this->noAlias[$langcode][$path])) {
return $path;
}
// If the alias has already been loaded, return it from static cache.
if (isset($this->lookupMap[$langcode][$path])) {
return $this->lookupMap[$langcode][$path];
}
// Try to load alias from storage.
if ($path_alias = $this->pathAliasRepository->lookupBySystemPath($path, $langcode)) {
$this->lookupMap[$langcode][$path] = $path_alias['alias'];
return $path_alias['alias'];
}
// We can't record anything into $this->lookupMap because we didn't find any
// aliases for this path. Thus cache to $this->noAlias.
$this->noAlias[$langcode][$path] = TRUE;
return $path;
}
/**
* {@inheritdoc}
*/
public function cacheClear($source = NULL) {
// Note this method does not flush the preloaded path lookup cache. This is
// because if a path is missing from this cache, it still results in the
// alias being loaded correctly, only less efficiently.
if ($source) {
foreach (array_keys($this->lookupMap) as $lang) {
unset($this->lookupMap[$lang][$source]);
}
}
else {
$this->lookupMap = [];
}
$this->noPath = [];
$this->noAlias = [];
$this->langcodePreloaded = [];
$this->preloadedPathLookups = [];
$this->pathAliasPrefixListRebuild($source);
}
/**
* Rebuild the path alias prefix list.
*
* @param string $path
* An optional path for which an alias is being inserted.
*/
protected function pathAliasPrefixListRebuild($path = NULL) {
// When paths are inserted, only rebuild the prefix list if the path has a
// top level component which is not already in the prefix list.
if (!empty($path)) {
if ($this->pathPrefixes->get(strtok($path, '/'))) {
return;
}
}
$this->pathPrefixes->clear();
}
/**
* Rebuild the path alias prefix list.
*
* @param string $path
* An optional path for which an alias is being inserted.
*
* @deprecated in drupal:11.1.0 and is removed from drupal:12.0.0.
* Use \Drupal\path_alias\AliasManager::pathAliasPrefixListRebuild instead.
*
* @see https://www.drupal.org/node/3467559
*
* cspell:ignore whitelist
*/
protected function pathAliasWhitelistRebuild($path = NULL) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:11.1.0 and is removed from drupal:12.0.0. Use \Drupal\path_alias\AliasManager::pathAliasPrefixListRebuild() instead. See https://www.drupal.org/node/3467559', E_USER_DEPRECATED);
$this->pathAliasPrefixListRebuild($path);
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Drupal\path_alias;
/**
* Find an alias for a path and vice versa.
*
* @see \Drupal\path_alias\AliasStorageInterface
*/
interface AliasManagerInterface {
/**
* Given the alias, return the path it represents.
*
* @param string $alias
* An alias.
* @param string $langcode
* An optional language code to look up the path in.
*
* @return string
* The path represented by alias, or the alias if no path was found.
*
* @throws \InvalidArgumentException
* Thrown when the path does not start with a slash.
*/
public function getPathByAlias($alias, $langcode = NULL);
/**
* Given a path, return the alias.
*
* @param string $path
* A path.
* @param string $langcode
* An optional language code to look up the path in.
*
* @return string
* An alias that represents the path, or path if no alias was found.
*
* @throws \InvalidArgumentException
* Thrown when the path does not start with a slash.
*/
public function getAliasByPath($path, $langcode = NULL);
/**
* Clears the static caches in alias manager and rebuilds the prefix list.
*
* @param string|null $source
* Source path of the alias that is being inserted/updated. If omitted, the
* entire lookup static cache will be cleared and the prefix list will be
* rebuilt.
*/
public function cacheClear($source = NULL);
}

View File

@ -0,0 +1,120 @@
<?php
namespace Drupal\path_alias;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\State\StateInterface;
/**
* Cache a list of valid alias prefixes.
*/
class AliasPrefixList extends CacheCollector implements AliasPrefixListInterface {
/**
* The Key/Value Store to use for state.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The path alias repository.
*
* @var \Drupal\path_alias\AliasRepositoryInterface
*/
protected $pathAliasRepository;
/**
* Constructs an AliasPrefixList object.
*
* @param string $cid
* The cache id to use.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\State\StateInterface $state
* The state keyvalue store.
* @param \Drupal\path_alias\AliasRepositoryInterface $alias_repository
* The path alias repository.
*/
public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, StateInterface $state, AliasRepositoryInterface $alias_repository) {
parent::__construct($cid, $cache, $lock);
$this->state = $state;
$this->pathAliasRepository = $alias_repository;
}
/**
* {@inheritdoc}
*/
protected function lazyLoadCache() {
parent::lazyLoadCache();
// On a cold start $this->storage will be empty and the prefix list will
// need to be rebuilt from scratch. The prefix list is initialized from the
// list of all valid path roots stored in the 'router.path_roots' state,
// with values initialized to NULL. During the request, each path requested
// that matches one of these keys will be looked up and the array value set
// to either TRUE or FALSE. This ensures that paths which do not exist in
// the router are not looked up, and that paths that do exist in the router
// are only looked up once.
if (empty($this->storage)) {
$this->loadMenuPathRoots();
}
}
/**
* Loads menu path roots to prepopulate cache.
*/
protected function loadMenuPathRoots() {
if ($roots = $this->state->get('router.path_roots')) {
foreach ($roots as $root) {
$this->storage[$root] = NULL;
$this->persist($root);
}
}
}
/**
* {@inheritdoc}
*/
public function get($offset) {
$this->lazyLoadCache();
// This may be called with paths that are not represented by menu router
// items such as paths that will be rewritten by hook_url_outbound_alter().
// Therefore internally TRUE is used to indicate valid paths. FALSE is
// used to indicate paths that have already been checked but are not
// valid, and NULL indicates paths that have not been checked yet.
if (isset($this->storage[$offset])) {
if ($this->storage[$offset]) {
return TRUE;
}
}
elseif (array_key_exists($offset, $this->storage)) {
return $this->resolveCacheMiss($offset);
}
}
/**
* {@inheritdoc}
*/
public function resolveCacheMiss($root) {
$exists = $this->pathAliasRepository->pathHasMatchingAlias('/' . $root);
$this->storage[$root] = $exists;
$this->persist($root);
if ($exists) {
return TRUE;
}
}
/**
* {@inheritdoc}
*/
public function clear() {
parent::clear();
$this->loadMenuPathRoots();
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Drupal\path_alias;
use Drupal\Core\Cache\CacheCollectorInterface;
/**
* Cache a list of valid alias prefixes.
*
* The list contains the first element of the router paths of all aliases. For
* example, if /node/12345 has an alias then "node" is added to the prefix list.
* This optimization allows skipping the lookup for every /user/{user} path if
* "user" is not in the list.
*/
interface AliasPrefixListInterface extends CacheCollectorInterface {}

View File

@ -0,0 +1,151 @@
<?php
namespace Drupal\path_alias;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Database\Statement\FetchAs;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides the default path alias lookup operations.
*/
class AliasRepository implements AliasRepositoryInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Constructs an AliasRepository object.
*
* @param \Drupal\Core\Database\Connection $connection
* A database connection for reading and writing path aliases.
*/
public function __construct(Connection $connection) {
$this->connection = $connection;
}
/**
* {@inheritdoc}
*/
public function preloadPathAlias($preloaded, $langcode) {
$select = $this->getBaseQuery()
->fields('base_table', ['path', 'alias']);
if (!empty($preloaded)) {
$conditions = $this->connection->condition('OR');
foreach ($preloaded as $preloaded_item) {
$conditions->condition('base_table.path', $this->connection->escapeLike($preloaded_item), 'LIKE');
}
$select->condition($conditions);
}
$this->addLanguageFallback($select, $langcode);
$select->orderBy('base_table.id', 'DESC');
// We want the most recently created alias for each source, however that
// will be at the start of the result-set, so fetch everything and reverse
// it. Note that it would not be sufficient to reverse the ordering of the
// 'base_table.id' column, as that would not guarantee other conditions
// added to the query, such as those in ::addLanguageFallback, would be
// reversed.
$results = $select->execute()->fetchAll(FetchAs::Associative);
$aliases = [];
foreach (array_reverse($results) as $result) {
$aliases[$result['path']] = $result['alias'];
}
return $aliases;
}
/**
* {@inheritdoc}
*/
public function lookupBySystemPath($path, $langcode) {
// See the queries above. Use LIKE for case-insensitive matching.
$select = $this->getBaseQuery()
->fields('base_table', ['id', 'path', 'alias', 'langcode'])
->condition('base_table.path', $this->connection->escapeLike($path), 'LIKE');
$this->addLanguageFallback($select, $langcode);
$select->orderBy('base_table.id', 'DESC');
return $select->execute()->fetchAssoc() ?: NULL;
}
/**
* {@inheritdoc}
*/
public function lookupByAlias($alias, $langcode) {
// See the queries above. Use LIKE for case-insensitive matching.
$select = $this->getBaseQuery()
->fields('base_table', ['id', 'path', 'alias', 'langcode'])
->condition('base_table.alias', $this->connection->escapeLike($alias), 'LIKE');
$this->addLanguageFallback($select, $langcode);
$select->orderBy('base_table.id', 'DESC');
return $select->execute()->fetchAssoc() ?: NULL;
}
/**
* {@inheritdoc}
*/
public function pathHasMatchingAlias($initial_substring) {
$query = $this->getBaseQuery();
$query->addExpression(1);
return (bool) $query
->condition('base_table.path', $this->connection->escapeLike($initial_substring) . '%', 'LIKE')
->range(0, 1)
->execute()
->fetchField();
}
/**
* Returns a SELECT query for the path_alias base table.
*
* @return \Drupal\Core\Database\Query\SelectInterface
* A Select query object.
*/
protected function getBaseQuery() {
$query = $this->connection->select('path_alias', 'base_table');
$query->condition('base_table.status', 1);
return $query;
}
/**
* Adds path alias language fallback conditions to a select query object.
*
* @param \Drupal\Core\Database\Query\SelectInterface $query
* A Select query object.
* @param string $langcode
* Language code to search the path with. If there's no path defined for
* that language it will search paths without language.
*/
protected function addLanguageFallback(SelectInterface $query, $langcode) {
// Always get the language-specific alias before the language-neutral one.
// For example 'de' is less than 'und' so the order needs to be ASC, while
// 'xx-lolspeak' is more than 'und' so the order needs to be DESC.
$langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
if ($langcode === LanguageInterface::LANGCODE_NOT_SPECIFIED) {
array_pop($langcode_list);
}
elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$query->orderBy('base_table.langcode', 'DESC');
}
else {
$query->orderBy('base_table.langcode', 'ASC');
}
$query->condition('base_table.langcode', $langcode_list, 'IN');
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Drupal\path_alias;
/**
* Provides an interface for path alias lookup operations.
*
* The path alias repository service is only used internally in order to
* optimize alias lookup queries needed in the critical path of each request.
* However, it is not marked as an internal service because alternative storage
* backends still need to override it if they provide a different storage class
* for the PathAlias entity type.
*
* Whenever you need to determine whether an alias exists for a system path, or
* whether a system path has an alias, the 'path_alias.manager' service should
* be used instead.
*/
interface AliasRepositoryInterface {
/**
* Pre-loads path alias information for a given list of system paths.
*
* @param array $preloaded
* System paths that need preloading of aliases.
* @param string $langcode
* Language code to search the path with. If there's no path defined for
* that language it will search paths without language.
*
* @return string[]
* System paths (keys) to alias (values) mapping.
*/
public function preloadPathAlias($preloaded, $langcode);
/**
* Searches a path alias for a given Drupal system path.
*
* The default implementation performs case-insensitive matching on the
* 'path' and 'alias' strings.
*
* @param string $path
* The system path to investigate for corresponding path aliases.
* @param string $langcode
* Language code to search the path with. If there's no path defined for
* that language it will search paths without language.
*
* @return array|null
* An array containing the 'id', 'path', 'alias' and 'langcode' properties
* of a path alias, or NULL if none was found.
*/
public function lookupBySystemPath($path, $langcode);
/**
* Searches a path alias for a given alias.
*
* The default implementation performs case-insensitive matching on the
* 'path' and 'alias' strings.
*
* @param string $alias
* The alias to investigate for corresponding system paths.
* @param string $langcode
* Language code to search the alias with. If there's no alias defined for
* that language it will search aliases without language.
*
* @return array|null
* An array containing the 'id', 'path', 'alias' and 'langcode' properties
* of a path alias, or NULL if none was found.
*/
public function lookupByAlias($alias, $langcode);
/**
* Check if any alias exists starting with $initial_substring.
*
* @param string $initial_substring
* Initial system path substring to test against.
*
* @return bool
* TRUE if any alias exists, FALSE otherwise.
*/
public function pathHasMatchingAlias($initial_substring);
}

View File

@ -0,0 +1,15 @@
<?php
namespace Drupal\path_alias;
// cspell:ignore whitelist
/**
* Cache a list of valid alias prefixes.
*
* @deprecated in drupal:11.1.0 and is removed from drupal:12.0.0. Use
* \Drupal\path_alias\AliasPrefixList instead.
*
* @see https://www.drupal.org/node/3467559
*/
class AliasWhitelist extends AliasPrefixList {}

View File

@ -0,0 +1,15 @@
<?php
namespace Drupal\path_alias;
// cspell:ignore whitelist
/**
* Cache a list of valid alias prefixes.
*
* @deprecated in drupal:11.1.0 and is removed from drupal:12.0.0.
* Use \Drupal\path_alias\AliasPrefixListInterface instead.
*
* @see https://www.drupal.org/node/3467559
*/
interface AliasWhitelistInterface extends AliasPrefixListInterface {}

View File

@ -0,0 +1,174 @@
<?php
namespace Drupal\path_alias\Entity;
use Drupal\Core\Entity\Attribute\ContentEntityType;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\path_alias\PathAliasInterface;
use Drupal\path_alias\PathAliasStorage;
use Drupal\path_alias\PathAliasStorageSchema;
/**
* Defines the path_alias entity class.
*/
#[ContentEntityType(
id: 'path_alias',
label: new TranslatableMarkup('URL alias'),
label_collection: new TranslatableMarkup('URL aliases'),
label_singular: new TranslatableMarkup('URL alias'),
label_plural: new TranslatableMarkup('URL aliases'),
entity_keys: [
'id' => 'id',
'revision' => 'revision_id',
'langcode' => 'langcode',
'uuid' => 'uuid',
'published' => 'status',
],
handlers: [
'storage' => PathAliasStorage::class,
'storage_schema' => PathAliasStorageSchema::class,
],
admin_permission: 'administer url aliases',
base_table: 'path_alias',
revision_table: 'path_alias_revision',
label_count: [
'singular' => '@count URL alias',
'plural' => '@count URL aliases',
],
list_cache_tags: ['route_match'],
constraints: [
'UniquePathAlias' => [],
],
)]
class PathAlias extends ContentEntityBase implements PathAliasInterface {
use EntityPublishedTrait;
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['path'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('System path'))
->setDescription(new TranslatableMarkup('The path that this alias belongs to.'))
->setRequired(TRUE)
->setRevisionable(TRUE)
->addPropertyConstraints('value', [
'Regex' => [
'pattern' => '/^\//i',
'message' => new TranslatableMarkup('The source path has to start with a slash.'),
],
])
->addPropertyConstraints('value', ['ValidPath' => []]);
$fields['alias'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('URL alias'))
->setDescription(new TranslatableMarkup('An alias used with this path.'))
->setRequired(TRUE)
->setRevisionable(TRUE)
->addPropertyConstraints('value', [
'Regex' => [
'pattern' => '/^\//i',
'message' => new TranslatableMarkup('The alias path has to start with a slash.'),
],
]);
$fields['langcode']->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED);
// Add the published field.
$fields += static::publishedBaseFieldDefinitions($entity_type);
$fields['status']->setTranslatable(FALSE);
return $fields;
}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
// Trim the alias value of whitespace and slashes. Ensure to not trim the
// slash on the left side.
$alias = rtrim(trim($this->getAlias()), "\\/");
$this->setAlias($alias);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
$alias_manager = \Drupal::service('path_alias.manager');
$alias_manager->cacheClear($this->getPath());
if ($update) {
$alias_manager->cacheClear($this->getOriginal()->getPath());
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
$alias_manager = \Drupal::service('path_alias.manager');
foreach ($entities as $entity) {
$alias_manager->cacheClear($entity->getPath());
}
}
/**
* {@inheritdoc}
*/
public function getPath() {
return $this->get('path')->value;
}
/**
* {@inheritdoc}
*/
public function setPath($path) {
$this->set('path', $path);
return $this;
}
/**
* {@inheritdoc}
*/
public function getAlias() {
return $this->get('alias')->value;
}
/**
* {@inheritdoc}
*/
public function setAlias($alias) {
$this->set('alias', $alias);
return $this;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->getAlias();
}
/**
* {@inheritdoc}
*/
public function getCacheTagsToInvalidate() {
return ['route_match'];
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Drupal\path_alias\EventSubscriber;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\path_alias\AliasManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
/**
* Provides a path subscriber that converts path aliases.
*/
class PathAliasSubscriber implements EventSubscriberInterface {
/**
* The alias manager that caches alias lookups based on the request.
*
* @var \Drupal\path_alias\AliasManagerInterface
*/
protected $aliasManager;
/**
* The current path.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPath;
/**
* Constructs a new PathSubscriber instance.
*
* @param \Drupal\path_alias\AliasManagerInterface $alias_manager
* The alias manager.
* @param \Drupal\Core\Path\CurrentPathStack $current_path
* The current path.
*/
public function __construct(AliasManagerInterface $alias_manager, CurrentPathStack $current_path) {
$this->aliasManager = $alias_manager;
$this->currentPath = $current_path;
}
/**
* Sets the cache key on the alias manager cache decorator.
*
* KernelEvents::CONTROLLER is used in order to be executed after routing.
*
* @param \Symfony\Component\HttpKernel\Event\ControllerEvent $event
* The Event to process.
*/
public function onKernelController(ControllerEvent $event) {
// Set the cache key on the alias manager cache decorator.
if ($event->isMainRequest()) {
$this->aliasManager->setCacheKey(rtrim($this->currentPath->getPath($event->getRequest()), '/'));
}
}
/**
* Ensures system paths for the request get cached.
*/
public function onKernelTerminate(TerminateEvent $event) {
$this->aliasManager->writeCache();
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
public static function getSubscribedEvents(): array {
$events[KernelEvents::CONTROLLER][] = ['onKernelController', 200];
$events[KernelEvents::TERMINATE][] = ['onKernelTerminate', 200];
return $events;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Drupal\path_alias;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
/**
* Provides an interface defining a path_alias entity.
*/
interface PathAliasInterface extends ContentEntityInterface, EntityPublishedInterface {
/**
* Gets the source path of the alias.
*
* @return string
* The source path.
*/
public function getPath();
/**
* Sets the source path of the alias.
*
* @param string $path
* The source path.
*
* @return $this
*/
public function setPath($path);
/**
* Gets the alias for this path.
*
* @return string
* The alias for this path.
*/
public function getAlias();
/**
* Sets the alias for this path.
*
* @param string $alias
* The path alias.
*
* @return $this
*/
public function setAlias($alias);
}

View File

@ -0,0 +1,22 @@
<?php
namespace Drupal\path_alias;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* Defines the storage handler class for path_alias entities.
*/
class PathAliasStorage extends SqlContentEntityStorage {
/**
* {@inheritdoc}
*/
public function createWithSampleValues($bundle = FALSE, array $values = []) {
$entity = parent::createWithSampleValues($bundle, ['path' => '/<front>'] + $values);
// Ensure the alias is only 255 characters long.
$entity->set('alias', substr('/' . $entity->get('alias')->value, 0, 255));
return $entity;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Drupal\path_alias;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
/**
* Defines the path_alias schema handler.
*/
class PathAliasStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$schema = parent::getEntitySchema($entity_type, $reset);
$base_table = $this->storage->getBaseTable();
$revision_table = $this->storage->getRevisionTable();
$schema[$base_table]['indexes'] += [
'path_alias__alias_langcode_id_status' => ['alias', 'langcode', 'id', 'status'],
'path_alias__path_langcode_id_status' => ['path', 'langcode', 'id', 'status'],
];
$schema[$revision_table]['indexes'] += [
'path_alias_revision__alias_langcode_id_status' => ['alias', 'langcode', 'id', 'status'],
'path_alias_revision__path_langcode_id_status' => ['path', 'langcode', 'id', 'status'],
];
// Unset the path_alias__status index as it is slower than the above
// indexes and MySQL 5.7 chooses to use it even though it is suboptimal.
unset($schema[$base_table]['indexes']['path_alias__status']);
return $schema;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Drupal\path_alias\PathProcessor;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\path_alias\AliasManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Processes the inbound and outbound path using path alias lookups.
*/
class AliasPathProcessor implements InboundPathProcessorInterface, OutboundPathProcessorInterface {
/**
* An alias manager for looking up the system path.
*
* @var \Drupal\path_alias\AliasManagerInterface
*/
protected $aliasManager;
/**
* Constructs a AliasPathProcessor object.
*
* @param \Drupal\path_alias\AliasManagerInterface $alias_manager
* An alias manager for looking up the system path.
*/
public function __construct(AliasManagerInterface $alias_manager) {
$this->aliasManager = $alias_manager;
}
/**
* {@inheritdoc}
*/
public function processInbound($path, Request $request) {
$path = $this->aliasManager->getPathByAlias($path);
return $path;
}
/**
* {@inheritdoc}
*/
public function processOutbound($path, &$options = [], ?Request $request = NULL, ?BubbleableMetadata $bubbleable_metadata = NULL) {
if (empty($options['alias'])) {
$langcode = isset($options['language']) ? $options['language']->getId() : NULL;
$path = $this->aliasManager->getAliasByPath($path, $langcode);
// Ensure the resulting path has at most one leading slash, to prevent it
// becoming an external URL without a protocol like //example.com. This
// is done in \Drupal\Core\Routing\UrlGenerator::generateFromRoute()
// also, to protect against this problem in arbitrary path processors,
// but it is duplicated here to protect any other URL generation code
// that might call this method separately.
if (str_starts_with($path, '//')) {
$path = '/' . ltrim($path, '/');
}
}
return $path;
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for path_alias.
*
* @group path_alias
*/
class GenericTest extends GenericModuleTestBase {}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* Test path_alias entities for unauthenticated JSON requests.
*
* @group path_alias
*/
class PathAliasJsonAnonTest extends PathAliasResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* Test path_alias entities for JSON requests via basic auth.
*
* @group path_alias
*/
class PathAliasJsonBasicAuthTest extends PathAliasResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* Test path_alias entities for JSON requests with cookie authentication.
*
* @group path_alias
*/
class PathAliasJsonCookieTest extends PathAliasResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional\Rest;
use Drupal\Core\Language\LanguageInterface;
use Drupal\path_alias\Entity\PathAlias;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
* Base class for path_alias EntityResource tests.
*/
abstract class PathAliasResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['path', 'path_alias'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'path_alias';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [];
/**
* {@inheritdoc}
*/
protected static $firstCreatedEntityId = 3;
/**
* {@inheritdoc}
*/
protected static $secondCreatedEntityId = 4;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer url aliases']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$path_alias = PathAlias::create([
'path' => '/<front>',
'alias' => '/frontpage1',
]);
$path_alias->save();
return $path_alias;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'id' => [
[
'value' => 1,
],
],
'revision_id' => [
[
'value' => 1,
],
],
'langcode' => [
[
'value' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
],
],
'path' => [
[
'value' => '/<front>',
],
],
'alias' => [
[
'value' => '/frontpage1',
],
],
'status' => [
[
'value' => TRUE,
],
],
'uuid' => [
[
'value' => $this->entity->uuid(),
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'path' => [
[
'value' => '/<front>',
],
],
'alias' => [
[
'value' => '/frontpage1',
],
],
];
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* Test path_alias entities for unauthenticated XML requests.
*
* @group path_alias
*/
class PathAliasXmlAnonTest extends PathAliasResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* Test path_alias entities for XML requests with cookie authentication.
*
* @group path_alias
*/
class PathAliasXmlBasicAuthTest extends PathAliasResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* Test path_alias entities for XML requests.
*
* @group path_alias
*/
class PathAliasXmlCookieTest extends PathAliasResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests the update path for the path_alias_revision table indices.
*
* @group path_alias
*/
class PathAliasRevisionIndexesUpdatePathTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles(): void {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-10.3.0.bare.standard.php.gz',
];
}
/**
* Tests the update path for the path_alias_revision table indices.
*/
public function testRunUpdates(): void {
$schema = \Drupal::database()->schema();
$this->assertFalse($schema->indexExists('path_alias_revision', 'path_alias_revision__alias_langcode_id_status'));
$this->assertFalse($schema->indexExists('path_alias_revision', 'path_alias_revision__path_langcode_id_status'));
$this->runUpdates();
$this->assertTrue($schema->indexExists('path_alias_revision', 'path_alias_revision__alias_langcode_id_status'));
$this->assertTrue($schema->indexExists('path_alias_revision', 'path_alias_revision__path_langcode_id_status'));
}
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Functional;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* Tests altering the inbound path and the outbound path.
*
* @group path_alias
*/
class UrlAlterFunctionalTest extends BrowserTestBase {
use PathAliasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['path', 'url_alter_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that URL altering works and that it occurs in the correct order.
*/
public function testUrlAlter(): void {
// Ensure that the path_alias table exists after Drupal installation.
$this->assertTrue(Database::getConnection()->schema()->tableExists('path_alias'), 'The path_alias table exists after Drupal installation.');
// User names can have quotes and plus signs so we should ensure that URL
// altering works with this.
$account = $this->drupalCreateUser(['administer url aliases'], "it's+bar");
$this->drupalLogin($account);
$uid = $account->id();
$name = $account->getAccountName();
// Test a single altered path.
$this->drupalGet("user/$name");
$this->assertSession()->statusCodeEquals(200);
$this->assertUrlOutboundAlter("/user/$uid", "/user/$name");
// Test that a path always uses its alias.
$this->createPathAlias("/user/$uid/test1", '/alias/test1');
$this->rebuildContainer();
$this->assertUrlInboundAlter('/alias/test1', "/user/$uid/test1");
$this->assertUrlOutboundAlter("/user/$uid/test1", '/alias/test1');
// Test adding an alias via the UI.
$edit = ['path[0][value]' => "/user/$uid/edit", 'alias[0][value]' => '/alias/test2'];
$this->drupalGet('admin/config/search/path/add');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The alias has been saved.');
$this->drupalGet('alias/test2');
$this->assertSession()->statusCodeEquals(200);
$this->assertUrlOutboundAlter("/user/$uid/edit", '/alias/test2');
// Test a non-existent user is not altered.
$uid++;
$this->assertUrlOutboundAlter("/user/$uid", "/user/$uid");
// Test outbound query string altering.
$url = Url::fromRoute('user.login');
$this->assertSame(\Drupal::request()->getBaseUrl() . '/user/login?foo=bar', $url->toString());
}
/**
* Assert that an outbound path is altered to an expected value.
*
* @param string $original
* A string with the original path that is run through generateFrommPath().
* @param string $final
* A string with the expected result after generateFrommPath().
*
* @internal
*/
protected function assertUrlOutboundAlter(string $original, string $final): void {
// Test outbound altering.
$result = $this->container->get('path_processor_manager')->processOutbound($original);
$this->assertSame($final, $result, "Altered outbound URL $original, expected $final, and got $result.");
}
/**
* Assert that an inbound path is altered to an expected value.
*
* @param string $original
* The original path before it has been altered by inbound URL processing.
* @param string $final
* A string with the expected result.
*
* @internal
*/
protected function assertUrlInboundAlter(string $original, string $final): void {
// Test inbound altering.
$result = $this->container->get('path_alias.manager')->getPathByAlias($original);
$this->assertSame($final, $result, "Altered inbound URL $original, expected $final, and got $result.");
}
}

View File

@ -0,0 +1,472 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Kernel;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\MemoryCounterBackend;
use Drupal\Core\Language\LanguageInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\path_alias\AliasManager;
use Drupal\path_alias\AliasPrefixList;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* Tests path alias CRUD and lookup functionality.
*
* @coversDefaultClass \Drupal\path_alias\AliasRepository
*
* @group path_alias
*/
class AliasTest extends KernelTestBase {
use PathAliasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['path_alias'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// The alias prefix list expects that the menu path roots are set by a
// menu router rebuild.
\Drupal::state()->set('router.path_roots', ['user', 'admin']);
$this->installEntitySchema('path_alias');
}
/**
* @covers ::preloadPathAlias
*/
public function testPreloadPathAlias(): void {
$path_alias_repository = $this->container->get('path_alias.repository');
// Every interesting language combination:
// Just unspecified.
$this->createPathAlias('/und/src', '/und/alias', LanguageInterface::LANGCODE_NOT_SPECIFIED);
// Just a single language.
$this->createPathAlias('/en/src', '/en/alias', 'en');
// A single language, plus unspecified.
$this->createPathAlias('/en-und/src', '/en-und/und', LanguageInterface::LANGCODE_NOT_SPECIFIED);
$this->createPathAlias('/en-und/src', '/en-und/en', 'en');
// Multiple languages.
$this->createPathAlias('/en-xx-lolspeak/src', '/en-xx-lolspeak/en', 'en');
$this->createPathAlias('/en-xx-lolspeak/src', '/en-xx-lolspeak/xx-lolspeak', 'xx-lolspeak');
// A duplicate alias for the same path. This is later, so should be
// preferred.
$this->createPathAlias('/en-xx-lolspeak/src', '/en-xx-lolspeak/en-dup', 'en');
// Multiple languages, plus unspecified.
$this->createPathAlias('/en-xx-lolspeak-und/src', '/en-xx-lolspeak-und/und', LanguageInterface::LANGCODE_NOT_SPECIFIED);
$this->createPathAlias('/en-xx-lolspeak-und/src', '/en-xx-lolspeak-und/en', 'en');
$this->createPathAlias('/en-xx-lolspeak-und/src', '/en-xx-lolspeak-und/xx-lolspeak', 'xx-lolspeak');
// Queries for unspecified language aliases.
// Ask for an empty array, get all results.
$this->assertEquals(
[
'/und/src' => '/und/alias',
'/en-und/src' => '/en-und/und',
'/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/und',
],
$path_alias_repository->preloadPathAlias([], LanguageInterface::LANGCODE_NOT_SPECIFIED)
);
// Ask for nonexistent source.
$this->assertEquals(
[],
$path_alias_repository->preloadPathAlias(['/nonexistent'], LanguageInterface::LANGCODE_NOT_SPECIFIED));
// Ask for each saved source, individually.
$this->assertEquals(
['/und/src' => '/und/alias'],
$path_alias_repository->preloadPathAlias(['/und/src'], LanguageInterface::LANGCODE_NOT_SPECIFIED)
);
$this->assertEquals(
[],
$path_alias_repository->preloadPathAlias(['/en/src'], LanguageInterface::LANGCODE_NOT_SPECIFIED)
);
$this->assertEquals(
['/en-und/src' => '/en-und/und'],
$path_alias_repository->preloadPathAlias(['/en-und/src'], LanguageInterface::LANGCODE_NOT_SPECIFIED)
);
$this->assertEquals(
[],
$path_alias_repository->preloadPathAlias(['/en-xx-lolspeak/src'], LanguageInterface::LANGCODE_NOT_SPECIFIED)
);
$this->assertEquals(
['/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/und'],
$path_alias_repository->preloadPathAlias(['/en-xx-lolspeak-und/src'], LanguageInterface::LANGCODE_NOT_SPECIFIED)
);
// Ask for multiple sources, all that are known.
$this->assertEquals(
[
'/und/src' => '/und/alias',
'/en-und/src' => '/en-und/und',
'/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/und',
],
$path_alias_repository->preloadPathAlias(
[
'/nonexistent',
'/und/src',
'/en/src',
'/en-und/src',
'/en-xx-lolspeak/src',
'/en-xx-lolspeak-und/src',
],
LanguageInterface::LANGCODE_NOT_SPECIFIED
)
);
// Ask for multiple sources, just a subset.
$this->assertEquals(
[
'/und/src' => '/und/alias',
'/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/und',
],
$path_alias_repository->preloadPathAlias(
[
'/und/src',
'/en-xx-lolspeak/src',
'/en-xx-lolspeak-und/src',
],
LanguageInterface::LANGCODE_NOT_SPECIFIED
)
);
// Queries for English aliases.
// Ask for an empty array, get all results.
$this->assertEquals(
[
'/und/src' => '/und/alias',
'/en/src' => '/en/alias',
'/en-und/src' => '/en-und/en',
'/en-xx-lolspeak/src' => '/en-xx-lolspeak/en-dup',
'/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/en',
],
$path_alias_repository->preloadPathAlias([], 'en')
);
// Ask for nonexistent source.
$this->assertEquals(
[],
$path_alias_repository->preloadPathAlias(['/nonexistent'], 'en'));
// Ask for each saved source, individually.
$this->assertEquals(
['/und/src' => '/und/alias'],
$path_alias_repository->preloadPathAlias(['/und/src'], 'en')
);
$this->assertEquals(
['/en/src' => '/en/alias'],
$path_alias_repository->preloadPathAlias(['/en/src'], 'en')
);
$this->assertEquals(
['/en-und/src' => '/en-und/en'],
$path_alias_repository->preloadPathAlias(['/en-und/src'], 'en')
);
$this->assertEquals(
['/en-xx-lolspeak/src' => '/en-xx-lolspeak/en-dup'],
$path_alias_repository->preloadPathAlias(['/en-xx-lolspeak/src'], 'en')
);
$this->assertEquals(
['/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/en'],
$path_alias_repository->preloadPathAlias(['/en-xx-lolspeak-und/src'], 'en')
);
// Ask for multiple sources, all that are known.
$this->assertEquals(
[
'/und/src' => '/und/alias',
'/en/src' => '/en/alias',
'/en-und/src' => '/en-und/en',
'/en-xx-lolspeak/src' => '/en-xx-lolspeak/en-dup',
'/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/en',
],
$path_alias_repository->preloadPathAlias(
[
'/nonexistent',
'/und/src',
'/en/src',
'/en-und/src',
'/en-xx-lolspeak/src',
'/en-xx-lolspeak-und/src',
],
'en'
)
);
// Ask for multiple sources, just a subset.
$this->assertEquals(
[
'/und/src' => '/und/alias',
'/en-xx-lolspeak/src' => '/en-xx-lolspeak/en-dup',
'/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/en',
],
$path_alias_repository->preloadPathAlias(
[
'/und/src',
'/en-xx-lolspeak/src',
'/en-xx-lolspeak-und/src',
],
'en'
)
);
// Queries for xx-lolspeak aliases.
// Ask for an empty array, get all results.
$this->assertEquals(
[
'/und/src' => '/und/alias',
'/en-und/src' => '/en-und/und',
'/en-xx-lolspeak/src' => '/en-xx-lolspeak/xx-lolspeak',
'/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/xx-lolspeak',
],
$path_alias_repository->preloadPathAlias([], 'xx-lolspeak')
);
// Ask for nonexistent source.
$this->assertEquals(
[],
$path_alias_repository->preloadPathAlias(['/nonexistent'], 'xx-lolspeak'));
// Ask for each saved source, individually.
$this->assertEquals(
['/und/src' => '/und/alias'],
$path_alias_repository->preloadPathAlias(['/und/src'], 'xx-lolspeak')
);
$this->assertEquals(
[],
$path_alias_repository->preloadPathAlias(['/en/src'], 'xx-lolspeak')
);
$this->assertEquals(
['/en-und/src' => '/en-und/und'],
$path_alias_repository->preloadPathAlias(['/en-und/src'], 'xx-lolspeak')
);
$this->assertEquals(
['/en-xx-lolspeak/src' => '/en-xx-lolspeak/xx-lolspeak'],
$path_alias_repository->preloadPathAlias(['/en-xx-lolspeak/src'], 'xx-lolspeak')
);
$this->assertEquals(
['/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/xx-lolspeak'],
$path_alias_repository->preloadPathAlias(['/en-xx-lolspeak-und/src'], 'xx-lolspeak')
);
// Ask for multiple sources, all that are known.
$this->assertEquals(
[
'/und/src' => '/und/alias',
'/en-und/src' => '/en-und/und',
'/en-xx-lolspeak/src' => '/en-xx-lolspeak/xx-lolspeak',
'/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/xx-lolspeak',
],
$path_alias_repository->preloadPathAlias(
[
'/nonexistent',
'/und/src',
'/en/src',
'/en-und/src',
'/en-xx-lolspeak/src',
'/en-xx-lolspeak-und/src',
],
'xx-lolspeak'
)
);
// Ask for multiple sources, just a subset.
$this->assertEquals(
[
'/und/src' => '/und/alias',
'/en-xx-lolspeak/src' => '/en-xx-lolspeak/xx-lolspeak',
'/en-xx-lolspeak-und/src' => '/en-xx-lolspeak-und/xx-lolspeak',
],
$path_alias_repository->preloadPathAlias(
[
'/und/src',
'/en-xx-lolspeak/src',
'/en-xx-lolspeak-und/src',
],
'xx-lolspeak'
)
);
}
/**
* @covers ::lookupBySystemPath
*/
public function testLookupBySystemPath(): void {
$this->createPathAlias('/test-source-Case', '/test-alias');
$path_alias_repository = $this->container->get('path_alias.repository');
$this->assertEquals('/test-alias', $path_alias_repository->lookupBySystemPath('/test-source-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED)['alias']);
$this->assertEquals('/test-alias', $path_alias_repository->lookupBySystemPath('/test-source-case', LanguageInterface::LANGCODE_NOT_SPECIFIED)['alias']);
}
/**
* @covers ::lookupByAlias
*/
public function testLookupByAlias(): void {
$this->createPathAlias('/test-source', '/test-alias-Case');
$path_alias_repository = $this->container->get('path_alias.repository');
$this->assertEquals('/test-source', $path_alias_repository->lookupByAlias('/test-alias-Case', LanguageInterface::LANGCODE_NOT_SPECIFIED)['path']);
$this->assertEquals('/test-source', $path_alias_repository->lookupByAlias('/test-alias-case', LanguageInterface::LANGCODE_NOT_SPECIFIED)['path']);
}
/**
* @covers \Drupal\path_alias\AliasManager::getPathByAlias
* @covers \Drupal\path_alias\AliasManager::getAliasByPath
*/
public function testLookupPath(): void {
// Create AliasManager and Path object.
$aliasManager = $this->container->get('path_alias.manager');
// Test the situation where the source is the same for multiple aliases.
// Start with a language-neutral alias, which we will override.
$path_alias = $this->createPathAlias('/user/1', '/foo');
$this->assertEquals($path_alias->getAlias(), $aliasManager->getAliasByPath($path_alias->getPath()), 'Basic alias lookup works.');
$this->assertEquals($path_alias->getPath(), $aliasManager->getPathByAlias($path_alias->getAlias()), 'Basic source lookup works.');
// Create a language specific alias for the default language (English).
$path_alias = $this->createPathAlias('/user/1', '/users/Dries', 'en');
$this->assertEquals($path_alias->getAlias(), $aliasManager->getAliasByPath($path_alias->getPath()), 'English alias overrides language-neutral alias.');
$this->assertEquals($path_alias->getPath(), $aliasManager->getPathByAlias($path_alias->getAlias()), 'English source overrides language-neutral source.');
// Create a language-neutral alias for the same path, again.
$path_alias = $this->createPathAlias('/user/1', '/bar');
$this->assertEquals("/users/Dries", $aliasManager->getAliasByPath($path_alias->getPath()), 'English alias still returned after entering a language-neutral alias.');
// Create a language-specific (xx-lolspeak) alias for the same path.
$path_alias = $this->createPathAlias('/user/1', '/LOL', 'xx-lolspeak');
$this->assertEquals("/users/Dries", $aliasManager->getAliasByPath($path_alias->getPath()), 'English alias still returned after entering a LOLspeak alias.');
// The LOLspeak alias should be returned if we really want LOLspeak.
$this->assertEquals('/LOL', $aliasManager->getAliasByPath($path_alias->getPath(), 'xx-lolspeak'), 'LOLspeak alias returned if we specify xx-lolspeak to the alias manager.');
// Create a new alias for this path in English, which should override the
// previous alias for "user/1".
$path_alias = $this->createPathAlias('/user/1', '/users/my-new-path', 'en');
$this->assertEquals($path_alias->getAlias(), $aliasManager->getAliasByPath($path_alias->getPath()), 'Recently created English alias returned.');
$this->assertEquals($path_alias->getPath(), $aliasManager->getPathByAlias($path_alias->getAlias()), 'Recently created English source returned.');
// Remove the English aliases, which should cause a fallback to the most
// recently created language-neutral alias, 'bar'.
$path_alias_storage = $this->container->get('entity_type.manager')->getStorage('path_alias');
$entities = $path_alias_storage->loadByProperties(['langcode' => 'en']);
$path_alias_storage->delete($entities);
$this->assertEquals('/bar', $aliasManager->getAliasByPath($path_alias->getPath()), 'Path lookup falls back to recently created language-neutral alias.');
// Test the situation where the alias and language are the same, but
// the source differs. The newer alias record should be returned.
$this->createPathAlias('/user/2', '/bar');
$aliasManager->cacheClear();
$this->assertEquals('/user/2', $aliasManager->getPathByAlias('/bar'), 'Newer alias record is returned when comparing two LanguageInterface::LANGCODE_NOT_SPECIFIED paths with the same alias.');
}
/**
* Tests the alias prefix.
*/
public function testPrefixList(): void {
$memoryCounterBackend = new MemoryCounterBackend(\Drupal::service(TimeInterface::class));
// Create AliasManager and Path object.
$prefix_list = new AliasPrefixList('path_alias_prefix_list', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $this->container->get('path_alias.repository'));
$aliasManager = new AliasManager($this->container->get('path_alias.repository'), $prefix_list, $this->container->get('language_manager'), $memoryCounterBackend, $this->container->get(TimeInterface::class));
// No alias for user and admin yet, so should be NULL.
$this->assertNull($prefix_list->get('user'));
$this->assertNull($prefix_list->get('admin'));
// Non-existing path roots should be NULL too. Use a length of 7 to avoid
// possible conflict with random aliases below.
$this->assertNull($prefix_list->get($this->randomMachineName()));
// Add an alias for user/1, user should get cached now.
$this->createPathAlias('/user/1', '/' . $this->randomMachineName());
$aliasManager->cacheClear();
$this->assertTrue($prefix_list->get('user'));
$this->assertNull($prefix_list->get('admin'));
$this->assertNull($prefix_list->get($this->randomMachineName()));
// Add an alias for admin, both should get cached now.
$this->createPathAlias('/admin/something', '/' . $this->randomMachineName());
$aliasManager->cacheClear();
$this->assertTrue($prefix_list->get('user'));
$this->assertTrue($prefix_list->get('admin'));
$this->assertNull($prefix_list->get($this->randomMachineName()));
// Remove the user alias again, prefix list entry should be removed.
$path_alias_storage = $this->container->get('entity_type.manager')->getStorage('path_alias');
$entities = $path_alias_storage->loadByProperties(['path' => '/user/1']);
$path_alias_storage->delete($entities);
$aliasManager->cacheClear();
$this->assertNull($prefix_list->get('user'));
$this->assertTrue($prefix_list->get('admin'));
$this->assertNull($prefix_list->get($this->randomMachineName()));
// Destruct the prefix list so that the caches are written.
$prefix_list->destruct();
$this->assertEquals(1, $memoryCounterBackend->getCounter('set', 'path_alias_prefix_list'));
$memoryCounterBackend->resetCounter();
// Re-initialize the prefix list using the same cache backend, should load
// from cache.
$prefix_list = new AliasPrefixList('path_alias_prefix_list', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $this->container->get('path_alias.repository'));
$this->assertNull($prefix_list->get('user'));
$this->assertTrue($prefix_list->get('admin'));
$this->assertNull($prefix_list->get($this->randomMachineName()));
$this->assertEquals(1, $memoryCounterBackend->getCounter('get', 'path_alias_prefix_list'));
$this->assertEquals(0, $memoryCounterBackend->getCounter('set', 'path_alias_prefix_list'));
// Destruct the prefix list, should not attempt to write the cache again.
$prefix_list->destruct();
$this->assertEquals(1, $memoryCounterBackend->getCounter('get', 'path_alias_prefix_list'));
$this->assertEquals(0, $memoryCounterBackend->getCounter('set', 'path_alias_prefix_list'));
}
/**
* Tests situation where the prefix list cache is deleted mid-request.
*/
public function testPrefixListCacheDeletionMidRequest() {
$memoryCounterBackend = new MemoryCounterBackend(\Drupal::service(TimeInterface::class));
// Create AliasManager and Path object.
$prefix_list = new AliasPrefixList('path_alias_prefix_list', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $this->container->get('path_alias.repository'));
// Prefix list cache should not exist at all yet.
$this->assertFalse($memoryCounterBackend->get('path_alias_prefix_list'));
// Add some aliases for both menu routes we have.
$this->createPathAlias('/admin/something', '/' . $this->randomMachineName());
$this->createPathAlias('/user/something', '/' . $this->randomMachineName());
// Lookup admin path in prefix list. It will query the DB and figure out
// that it indeed has an alias, and add it to the internal prefix list and
// flag it to be persisted to cache.
$this->assertTrue($prefix_list->get('admin'));
// Destruct the prefix list so it persists its cache.
$prefix_list->destruct();
$this->assertEquals(1, $memoryCounterBackend->getCounter('set', 'path_alias_prefix_list'));
// Cache data should have data for 'user' and 'admin', even though just
// 'admin' was looked up. This is because the cache is primed with all
// menu router base paths.
$this->assertEquals(['user' => FALSE, 'admin' => TRUE], $memoryCounterBackend->get('path_alias_prefix_list')->data);
$memoryCounterBackend->resetCounter();
// Re-initialize the prefix list and lookup an alias for the 'user' path.
// Prefix list should load data from its cache, see that it hasn't done a
// check for 'user' yet, perform the check, then mark the result to be
// persisted to cache.
$prefix_list = new AliasPrefixList('path_alias_prefix_list', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $this->container->get('path_alias.repository'));
$this->assertTrue($prefix_list->get('user'));
// Delete the prefix list cache. This could happen from an outside process,
// like a code deployment that performs a cache rebuild.
$memoryCounterBackend->delete('path_alias_prefix_list');
// Destruct prefix list so it attempts to save the prefix list data to
// cache. However it should recognize that the previous cache entry was
// deleted from underneath it and not save anything to cache, to protect
// from cache corruption.
$prefix_list->destruct();
$this->assertEquals(0, $memoryCounterBackend->getCounter('set', 'path_alias_prefix_list'));
$this->assertFalse($memoryCounterBackend->get('path_alias_prefix_list'));
$memoryCounterBackend->resetCounter();
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Kernel;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\Traits\Core\PathAliasTestTrait;
/**
* Tests path alias on entities.
*
* @group path_alias
*/
class EntityAliasTest extends KernelTestBase {
use PathAliasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'path_alias',
'entity_test',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('path_alias');
$this->installEntitySchema('user');
}
/**
* Tests transform.
*/
public function testEntityAlias(): void {
EntityTest::create(['id' => 1])->save();
$this->createPathAlias('/entity_test/1', '/entity-alias');
$entity = EntityTest::load(1);
$this->assertSame('/entity-alias', $entity->toUrl()->toString());
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the path_alias storage schema.
*
* @coversDefaultClass \Drupal\path_alias\PathAliasStorageSchema
*
* @group path_alias
*/
class PathAliasStorageSchemaTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['path_alias'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('path_alias');
}
/**
* Tests that the path_alias__status index is removed.
*
* @covers ::getEntitySchema
*/
public function testPathAliasStatusIndexRemoved(): void {
$schema = \Drupal::database()->schema();
$table_name = 'path_alias';
$this->assertTrue($schema->indexExists($table_name, 'path_alias__alias_langcode_id_status'));
$this->assertTrue($schema->indexExists($table_name, 'path_alias__path_langcode_id_status'));
$this->assertFalse($schema->indexExists($table_name, 'path_alias__status'));
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\path_alias\AliasManagerInterface;
use Drupal\path_alias\Entity\PathAlias;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\path_alias\Entity\PathAlias
*
* @group path_alias
*/
class PathHooksTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['path_alias'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('path_alias');
}
/**
* Tests that the PathAlias entity clears caches correctly.
*
* @covers ::postSave
* @covers ::postDelete
*/
public function testPathHooks(): void {
$path_alias = PathAlias::create([
'path' => '/' . $this->randomMachineName(),
'alias' => '/' . $this->randomMachineName(),
]);
// Check \Drupal\path_alias\Entity\PathAlias::postSave() for new path alias
// entities.
$alias_manager = $this->prophesize(AliasManagerInterface::class);
$alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(1);
$alias_manager->cacheClear($path_alias->getPath())->shouldBeCalledTimes(1);
\Drupal::getContainer()->set('path_alias.manager', $alias_manager->reveal());
$path_alias->save();
$new_source = '/' . $this->randomMachineName();
// Check \Drupal\path_alias\Entity\PathAlias::postSave() for existing path
// alias entities.
$alias_manager = $this->prophesize(AliasManagerInterface::class);
$alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(2);
$alias_manager->cacheClear($path_alias->getPath())->shouldBeCalledTimes(1);
$alias_manager->cacheClear($new_source)->shouldBeCalledTimes(1);
\Drupal::getContainer()->set('path_alias.manager', $alias_manager->reveal());
$path_alias->setPath($new_source);
$path_alias->save();
// Check \Drupal\path_alias\Entity\PathAlias::postDelete().
$alias_manager = $this->prophesize(AliasManagerInterface::class);
$alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(1);
$alias_manager->cacheClear($new_source)->shouldBeCalledTimes(1);
\Drupal::getContainer()->set('path_alias.manager', $alias_manager->reveal());
$path_alias->delete();
}
}

View File

@ -0,0 +1,565 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Unit;
use Drupal\Component\Datetime\Time;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\path_alias\AliasRepositoryInterface;
use Drupal\path_alias\AliasManager;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\path_alias\AliasManager
* @group path_alias
*/
class AliasManagerTest extends UnitTestCase {
/**
* The alias manager.
*
* @var \Drupal\path_alias\AliasManager
*/
protected $aliasManager;
/**
* Alias repository.
*
* @var \Drupal\path_alias\AliasRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $aliasRepository;
/**
* Alias prefix list.
*
* @var \Drupal\path_alias\AliasPrefixListInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $aliasPrefixList;
/**
* Language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $languageManager;
/**
* Cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $cache;
/**
* The internal cache key used by the alias manager.
*
* @var string
*/
protected $cacheKey = 'preload-paths:key';
/**
* The cache key passed to the alias manager.
*
* @var string
*/
protected $path = 'key';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->aliasRepository = $this->createMock(AliasRepositoryInterface::class);
$this->aliasPrefixList = $this->createMock('Drupal\path_alias\AliasPrefixListInterface');
$this->languageManager = $this->createMock('Drupal\Core\Language\LanguageManagerInterface');
$this->cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface');
$this->aliasManager = new AliasManager($this->aliasRepository, $this->aliasPrefixList, $this->languageManager, $this->cache, new Time());
}
/**
* Tests the getPathByAlias method for an alias that have no matching path.
*
* @covers ::getPathByAlias
*/
public function testGetPathByAliasNoMatch(): void {
$alias = '/' . $this->randomMachineName();
$language = new Language(['id' => 'en']);
$this->languageManager->expects($this->any())
->method('getCurrentLanguage')
->with(LanguageInterface::TYPE_URL)
->willReturn($language);
$this->aliasRepository->expects($this->once())
->method('lookupByAlias')
->with($alias, $language->getId())
->willReturn(NULL);
$this->assertEquals($alias, $this->aliasManager->getPathByAlias($alias));
// Call it twice to test the static cache.
$this->assertEquals($alias, $this->aliasManager->getPathByAlias($alias));
}
/**
* Tests the getPathByAlias method for an alias that have a matching path.
*
* @covers ::getPathByAlias
*/
public function testGetPathByAliasMatch(): void {
$alias = $this->randomMachineName();
$path = $this->randomMachineName();
$language = $this->setUpCurrentLanguage();
$this->aliasRepository->expects($this->once())
->method('lookupByAlias')
->with($alias, $language->getId())
->willReturn(['path' => $path]);
$this->assertEquals($path, $this->aliasManager->getPathByAlias($alias));
// Call it twice to test the static cache.
$this->assertEquals($path, $this->aliasManager->getPathByAlias($alias));
}
/**
* Tests the getPathByAlias method when a langcode is passed explicitly.
*
* @covers ::getPathByAlias
*/
public function testGetPathByAliasLangcode(): void {
$alias = $this->randomMachineName();
$path = $this->randomMachineName();
$this->languageManager->expects($this->never())
->method('getCurrentLanguage');
$this->aliasRepository->expects($this->once())
->method('lookupByAlias')
->with($alias, 'de')
->willReturn(['path' => $path]);
$this->assertEquals($path, $this->aliasManager->getPathByAlias($alias, 'de'));
// Call it twice to test the static cache.
$this->assertEquals($path, $this->aliasManager->getPathByAlias($alias, 'de'));
}
/**
* Tests the getAliasByPath method for a path that is not in the prefix list.
*
* @covers ::getAliasByPath
*/
public function testGetAliasByPathPrefixList() {
$path_part1 = $this->randomMachineName();
$path_part2 = $this->randomMachineName();
$path = '/' . $path_part1 . '/' . $path_part2;
$this->setUpCurrentLanguage();
$this->aliasPrefixList->expects($this->any())
->method('get')
->with($path_part1)
->willReturn(FALSE);
// The prefix list returns FALSE for that path part, so the storage should
// never be called.
$this->aliasRepository->expects($this->never())
->method('lookupBySystemPath');
$this->assertEquals($path, $this->aliasManager->getAliasByPath($path));
}
/**
* Tests the getAliasByPath method for a path that has no matching alias.
*
* @covers ::getAliasByPath
*/
public function testGetAliasByPathNoMatch(): void {
$path_part1 = $this->randomMachineName();
$path_part2 = $this->randomMachineName();
$path = '/' . $path_part1 . '/' . $path_part2;
$language = $this->setUpCurrentLanguage();
$this->aliasManager->setCacheKey($this->path);
$this->aliasPrefixList->expects($this->any())
->method('get')
->with($path_part1)
->willReturn(TRUE);
$this->aliasRepository->expects($this->once())
->method('lookupBySystemPath')
->with($path, $language->getId())
->willReturn(NULL);
$this->assertEquals($path, $this->aliasManager->getAliasByPath($path));
// Call it twice to test the static cache.
$this->assertEquals($path, $this->aliasManager->getAliasByPath($path));
// This needs to write out the cache.
$this->cache->expects($this->once())
->method('set')
->with($this->cacheKey, [$language->getId() => [$path]], (int) $_SERVER['REQUEST_TIME'] + (60 * 60 * 24));
$this->aliasManager->writeCache();
}
/**
* Tests the getAliasByPath method exception.
*
* @covers ::getAliasByPath
*/
public function testGetAliasByPathException(): void {
$this->expectException(\InvalidArgumentException::class);
$this->aliasManager->getAliasByPath('no-leading-slash-here');
}
/**
* Tests the getAliasByPath method for a path that has a matching alias.
*
* @covers ::getAliasByPath
* @covers ::writeCache
*/
public function testGetAliasByPathMatch(): void {
$path_part1 = $this->randomMachineName();
$path_part2 = $this->randomMachineName();
$path = '/' . $path_part1 . '/' . $path_part2;
$alias = $this->randomMachineName();
$language = $this->setUpCurrentLanguage();
$this->aliasManager->setCacheKey($this->path);
$this->aliasPrefixList->expects($this->any())
->method('get')
->with($path_part1)
->willReturn(TRUE);
$this->aliasRepository->expects($this->once())
->method('lookupBySystemPath')
->with($path, $language->getId())
->willReturn(['alias' => $alias]);
$this->assertEquals($alias, $this->aliasManager->getAliasByPath($path));
// Call it twice to test the static cache.
$this->assertEquals($alias, $this->aliasManager->getAliasByPath($path));
// This needs to write out the cache.
$this->cache->expects($this->once())
->method('set')
->with($this->cacheKey, [$language->getId() => [$path]], (int) $_SERVER['REQUEST_TIME'] + (60 * 60 * 24));
$this->aliasManager->writeCache();
}
/**
* Tests the getAliasByPath method for a path that is preloaded.
*
* @covers ::getAliasByPath
* @covers ::writeCache
*/
public function testGetAliasByPathCachedMatch(): void {
$path_part1 = $this->randomMachineName();
$path_part2 = $this->randomMachineName();
$path = '/' . $path_part1 . '/' . $path_part2;
$alias = $this->randomMachineName();
$language = $this->setUpCurrentLanguage();
// Use a set of cached paths where the tested path is in any position, not
// only in the first one.
$cached_paths = [
$language->getId() => [
'/another/path',
$path,
],
];
$this->cache->expects($this->once())
->method('get')
->with($this->cacheKey)
->willReturn((object) ['data' => $cached_paths]);
// Simulate a request so that the preloaded paths are fetched.
$this->aliasManager->setCacheKey($this->path);
$this->aliasPrefixList->expects($this->any())
->method('get')
->with($path_part1)
->willReturn(TRUE);
$this->aliasRepository->expects($this->once())
->method('preloadPathAlias')
->with($cached_paths[$language->getId()], $language->getId())
->willReturn([$path => $alias]);
// LookupPathAlias should not be called.
$this->aliasRepository->expects($this->never())
->method('lookupBySystemPath');
$this->assertEquals($alias, $this->aliasManager->getAliasByPath($path));
// Call it twice to test the static cache.
$this->assertEquals($alias, $this->aliasManager->getAliasByPath($path));
// This must not write to the cache again.
$this->cache->expects($this->never())
->method('set');
$this->aliasManager->writeCache();
}
/**
* Tests the getAliasByPath cache when a different language is requested.
*
* @covers ::getAliasByPath
* @covers ::writeCache
*/
public function testGetAliasByPathCachedMissLanguage(): void {
$path_part1 = $this->randomMachineName();
$path_part2 = $this->randomMachineName();
$path = '/' . $path_part1 . '/' . $path_part2;
$alias = $this->randomMachineName();
$language = $this->setUpCurrentLanguage();
$cached_language = new Language(['id' => 'de']);
$cached_paths = [$cached_language->getId() => [$path]];
$this->cache->expects($this->once())
->method('get')
->with($this->cacheKey)
->willReturn((object) ['data' => $cached_paths]);
// Simulate a request so that the preloaded paths are fetched.
$this->aliasManager->setCacheKey($this->path);
$this->aliasPrefixList->expects($this->any())
->method('get')
->with($path_part1)
->willReturn(TRUE);
// The requested language is different than the cached, so this will
// need to load.
$this->aliasRepository->expects($this->never())
->method('preloadPathAlias');
$this->aliasRepository->expects($this->once())
->method('lookupBySystemPath')
->with($path, $language->getId())
->willReturn(['alias' => $alias]);
$this->assertEquals($alias, $this->aliasManager->getAliasByPath($path));
// Call it twice to test the static cache.
$this->assertEquals($alias, $this->aliasManager->getAliasByPath($path));
// There is already a cache entry, so this should not write out to the
// cache.
$this->cache->expects($this->never())
->method('set');
$this->aliasManager->writeCache();
}
/**
* Tests the getAliasByPath cache with a preloaded path without alias.
*
* @covers ::getAliasByPath
* @covers ::writeCache
*/
public function testGetAliasByPathCachedMissNoAlias(): void {
$path_part1 = $this->randomMachineName();
$path_part2 = $this->randomMachineName();
$path = '/' . $path_part1 . '/' . $path_part2;
$cached_path = $this->randomMachineName();
$cached_alias = $this->randomMachineName();
$language = $this->setUpCurrentLanguage();
$cached_paths = [$language->getId() => [$cached_path, $path]];
$this->cache->expects($this->once())
->method('get')
->with($this->cacheKey)
->willReturn((object) ['data' => $cached_paths]);
// Simulate a request so that the preloaded paths are fetched.
$this->aliasManager->setCacheKey($this->path);
$this->aliasPrefixList->expects($this->any())
->method('get')
->with($path_part1)
->willReturn(TRUE);
$this->aliasRepository->expects($this->once())
->method('preloadPathAlias')
->with($cached_paths[$language->getId()], $language->getId())
->willReturn([$cached_path => $cached_alias]);
// LookupPathAlias() should not be called.
$this->aliasRepository->expects($this->never())
->method('lookupBySystemPath');
$this->assertEquals($path, $this->aliasManager->getAliasByPath($path));
// Call it twice to test the static cache.
$this->assertEquals($path, $this->aliasManager->getAliasByPath($path));
// This must not write to the cache again.
$this->cache->expects($this->never())
->method('set');
$this->aliasManager->writeCache();
}
/**
* Tests the getAliasByPath cache with an un-preloaded path without alias.
*
* @covers ::getAliasByPath
* @covers ::writeCache
*/
public function testGetAliasByPathUncachedMissNoAlias(): void {
$path_part1 = $this->randomMachineName();
$path_part2 = $this->randomMachineName();
$path = '/' . $path_part1 . '/' . $path_part2;
$cached_path = $this->randomMachineName();
$cached_alias = $this->randomMachineName();
$language = $this->setUpCurrentLanguage();
$cached_paths = [$language->getId() => [$cached_path]];
$this->cache->expects($this->once())
->method('get')
->with($this->cacheKey)
->willReturn((object) ['data' => $cached_paths]);
// Simulate a request so that the preloaded paths are fetched.
$this->aliasManager->setCacheKey($this->path);
$this->aliasPrefixList->expects($this->any())
->method('get')
->with($path_part1)
->willReturn(TRUE);
$this->aliasRepository->expects($this->once())
->method('preloadPathAlias')
->with($cached_paths[$language->getId()], $language->getId())
->willReturn([$cached_path => $cached_alias]);
$this->aliasRepository->expects($this->once())
->method('lookupBySystemPath')
->with($path, $language->getId())
->willReturn(NULL);
$this->assertEquals($path, $this->aliasManager->getAliasByPath($path));
// Call it twice to test the static cache.
$this->assertEquals($path, $this->aliasManager->getAliasByPath($path));
// There is already a cache entry, so this should not write out to the
// cache.
$this->cache->expects($this->never())
->method('set');
$this->aliasManager->writeCache();
}
/**
* @covers ::cacheClear
*/
public function testCacheClear(): void {
$path = '/path';
$alias = '/alias';
$language = $this->setUpCurrentLanguage();
$this->aliasRepository->expects($this->exactly(2))
->method('lookupBySystemPath')
->with($path, $language->getId())
->willReturn(['alias' => $alias]);
$this->aliasPrefixList->expects($this->any())
->method('get')
->willReturn(TRUE);
// Populate the lookup map.
$this->assertEquals($alias, $this->aliasManager->getAliasByPath($path, $language->getId()));
// Check that the cache is populated.
$this->aliasRepository->expects($this->never())
->method('lookupByAlias');
$this->assertEquals($path, $this->aliasManager->getPathByAlias($alias, $language->getId()));
// Clear specific source.
$this->aliasManager->cacheClear($path);
// Ensure cache has been cleared (this will be the 2nd call to
// `lookupPathAlias` if cache is cleared).
$this->assertEquals($alias, $this->aliasManager->getAliasByPath($path, $language->getId()));
// Clear non-existent source.
$this->aliasManager->cacheClear('non-existent');
}
/**
* Tests the getAliasByPath cache with an un-preloaded path with alias.
*
* @covers ::getAliasByPath
* @covers ::writeCache
*/
public function testGetAliasByPathUncachedMissWithAlias(): void {
$path_part1 = $this->randomMachineName();
$path_part2 = $this->randomMachineName();
$path = '/' . $path_part1 . '/' . $path_part2;
$cached_path = $this->randomMachineName();
$cached_no_alias_path = $this->randomMachineName();
$cached_alias = $this->randomMachineName();
$new_alias = $this->randomMachineName();
$language = $this->setUpCurrentLanguage();
$cached_paths = [$language->getId() => [$cached_path, $cached_no_alias_path]];
$this->cache->expects($this->once())
->method('get')
->with($this->cacheKey)
->willReturn((object) ['data' => $cached_paths]);
// Simulate a request so that the preloaded paths are fetched.
$this->aliasManager->setCacheKey($this->path);
$this->aliasPrefixList->expects($this->any())
->method('get')
->with($path_part1)
->willReturn(TRUE);
$this->aliasRepository->expects($this->once())
->method('preloadPathAlias')
->with($cached_paths[$language->getId()], $language->getId())
->willReturn([$cached_path => $cached_alias]);
$this->aliasRepository->expects($this->once())
->method('lookupBySystemPath')
->with($path, $language->getId())
->willReturn(['alias' => $new_alias]);
$this->assertEquals($new_alias, $this->aliasManager->getAliasByPath($path));
// Call it twice to test the static cache.
$this->assertEquals($new_alias, $this->aliasManager->getAliasByPath($path));
// There is already a cache entry, so this should not write out to the
// cache.
$this->cache->expects($this->never())
->method('set');
$this->aliasManager->writeCache();
}
/**
* Sets up the current language.
*
* @return \Drupal\Core\Language\LanguageInterface
* The current language object.
*/
protected function setUpCurrentLanguage() {
$language = new Language(['id' => 'en']);
$this->languageManager->expects($this->any())
->method('getCurrentLanguage')
->with(LanguageInterface::TYPE_URL)
->willReturn($language);
return $language;
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\path_alias\Unit\PathProcessor;
use Drupal\Core\Cache\Cache;
use Drupal\path_alias\PathProcessor\AliasPathProcessor;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
/**
* @coversDefaultClass \Drupal\path_alias\PathProcessor\AliasPathProcessor
* @group PathProcessor
* @group path_alias
*/
class AliasPathProcessorTest extends UnitTestCase {
/**
* The mocked alias manager.
*
* @var \Drupal\path_alias\AliasManagerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $aliasManager;
/**
* The tested path processor.
*
* @var \Drupal\path_alias\PathProcessor\AliasPathProcessor
*/
protected $pathProcessor;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->aliasManager = $this->createMock('Drupal\path_alias\AliasManagerInterface');
$this->pathProcessor = new AliasPathProcessor($this->aliasManager);
}
/**
* Tests the processInbound method.
*
* @see \Drupal\path_alias\PathProcessor\AliasPathProcessor::processInbound
*/
public function testProcessInbound(): void {
$this->aliasManager->expects($this->exactly(2))
->method('getPathByAlias')
->willReturnMap([
['url-alias', NULL, 'internal-url'],
['url', NULL, 'url'],
]);
$request = Request::create('/url-alias');
$this->assertEquals('internal-url', $this->pathProcessor->processInbound('url-alias', $request));
$request = Request::create('/url');
$this->assertEquals('url', $this->pathProcessor->processInbound('url', $request));
}
/**
* @covers ::processOutbound
*
* @dataProvider providerTestProcessOutbound
*/
public function testProcessOutbound($path, array $options, $expected_path): void {
$this->aliasManager->expects($this->any())
->method('getAliasByPath')
->willReturnMap([
['internal-url', NULL, 'url-alias'],
['url', NULL, 'url'],
]);
$bubbleable_metadata = new BubbleableMetadata();
$this->assertEquals($expected_path, $this->pathProcessor->processOutbound($path, $options, NULL, $bubbleable_metadata));
// Cacheability of paths replaced with path aliases is permanent.
// @todo https://www.drupal.org/node/2480077
$this->assertEquals((new BubbleableMetadata())->setCacheMaxAge(Cache::PERMANENT), $bubbleable_metadata);
}
/**
* Provides data for testing outbound processing.
*
* @return array
* The data provider for testProcessOutbound.
*/
public static function providerTestProcessOutbound() {
return [
['internal-url', [], 'url-alias'],
['internal-url', ['alias' => TRUE], 'internal-url'],
['url', [], 'url'],
];
}
}