Issue #2336597 by amateescu, slashrsm, jibran, Wim Leers, saki007ster, Berdir, catch, plach, Charlie ChX Negyesi, dawehner, gabesullice: Convert path aliases to full featured entities

merge-requests/55/head
catch 2019-10-01 11:30:19 +01:00
parent 56895fda5c
commit 099ce08ac8
56 changed files with 1579 additions and 557 deletions

View File

@ -955,7 +955,7 @@ services:
- { name: event_subscriber }
path.alias_storage:
class: Drupal\Core\Path\AliasStorage
arguments: ['@database', '@module_handler']
arguments: ['@database', '@module_handler', '@entity_type.manager']
tags:
- { name: backend_overridable }
path.matcher:

View File

@ -2,12 +2,12 @@
namespace Drupal\Core\Path;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Database\Query\Condition;
/**
* Provides a class for CRUD operations on path aliases.
@ -21,7 +21,7 @@ class AliasStorage implements AliasStorageInterface {
/**
* The table for the url_alias storage.
*/
const TABLE = 'url_alias';
const TABLE = 'path_alias';
/**
* The database connection.
@ -37,6 +37,13 @@ class AliasStorage implements AliasStorageInterface {
*/
protected $moduleHandler;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a Path CRUD object.
*
@ -44,17 +51,19 @@ class AliasStorage implements AliasStorageInterface {
* A database connection for reading and writing path aliases.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(Connection $connection, ModuleHandlerInterface $module_handler) {
public function __construct(Connection $connection, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager = NULL) {
$this->connection = $connection;
$this->moduleHandler = $module_handler;
$this->entityTypeManager = $entity_type_manager ?: \Drupal::entityTypeManager();
}
/**
* {@inheritdoc}
*/
public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, $pid = NULL) {
if ($source[0] !== '/') {
throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $source));
}
@ -63,227 +72,186 @@ class AliasStorage implements AliasStorageInterface {
throw new \InvalidArgumentException(sprintf('Alias path %s has to start with a slash.', $alias));
}
$fields = [
'source' => $source,
'alias' => $alias,
'langcode' => $langcode,
];
if ($pid) {
/** @var \Drupal\Core\Path\PathAliasInterface $path_alias */
$path_alias = $this->getPathAliasEntityStorage()->load($pid);
$original_values = [
'source' => $path_alias->getPath(),
'alias' => $path_alias->getAlias(),
'langcode' => $path_alias->get('langcode')->value,
];
// Insert or update the alias.
if (empty($pid)) {
$try_again = FALSE;
try {
$query = $this->connection->insert(static::TABLE)
->fields($fields);
$pid = $query->execute();
}
catch (\Exception $e) {
// If there was an exception, try to create the table.
if (!$try_again = $this->ensureTableExists()) {
// If the exception happened for other reason than the missing table,
// propagate the exception.
throw $e;
}
}
// Now that the table has been created, try again if necessary.
if ($try_again) {
$query = $this->connection->insert(static::TABLE)
->fields($fields);
$pid = $query->execute();
}
$fields['pid'] = $pid;
$operation = 'insert';
$path_alias->setPath($source);
$path_alias->setAlias($alias);
$path_alias->set('langcode', $langcode);
}
else {
// Fetch the current values so that an update hook can identify what
// exactly changed.
try {
$original = $this->connection->query('SELECT source, alias, langcode FROM {url_alias} WHERE pid = :pid', [':pid' => $pid])
->fetchAssoc();
}
catch (\Exception $e) {
$this->catchException($e);
$original = FALSE;
}
$query = $this->connection->update(static::TABLE)
->fields($fields)
->condition('pid', $pid);
$pid = $query->execute();
$fields['pid'] = $pid;
$fields['original'] = $original;
$operation = 'update';
$path_alias = $this->getPathAliasEntityStorage()->create([
'path' => $source,
'alias' => $alias,
'langcode' => $langcode,
]);
}
if ($pid) {
// @todo Switch to using an event for this instead of a hook.
$this->moduleHandler->invokeAll('path_' . $operation, [$fields]);
Cache::invalidateTags(['route_match']);
return $fields;
$path_alias->save();
$path_alias_values = [
'pid' => $path_alias->id(),
'source' => $path_alias->getPath(),
'alias' => $path_alias->getAlias(),
'langcode' => $path_alias->get('langcode')->value,
];
if (isset($original_values)) {
$path_alias_values['original'] = $original_values;
}
return FALSE;
return $path_alias_values;
}
/**
* {@inheritdoc}
*/
public function load($conditions) {
$select = $this->connection->select(static::TABLE);
$query = $this->getPathAliasEntityStorage()->getQuery();
// Ignore access restrictions for this API.
$query->accessCheck(FALSE);
foreach ($conditions as $field => $value) {
if ($field == 'source' || $field == 'alias') {
// Use LIKE for case-insensitive matching.
$select->condition($field, $this->connection->escapeLike($value), 'LIKE');
if ($field === 'source') {
$field = 'path';
}
else {
$select->condition($field, $value);
elseif ($field === 'pid') {
$field = 'id';
}
$query->condition($field, $value, '=');
}
try {
return $select
->fields(static::TABLE)
->orderBy('pid', 'DESC')
->range(0, 1)
->execute()
->fetchAssoc();
}
catch (\Exception $e) {
$this->catchException($e);
return FALSE;
$result = $query
->sort('id', 'DESC')
->range(0, 1)
->execute();
$entities = $this->getPathAliasEntityStorage()->loadMultiple($result);
/** @var \Drupal\Core\Path\PathAliasInterface $path_alias */
$path_alias = reset($entities);
if ($path_alias) {
return [
'pid' => $path_alias->id(),
'source' => $path_alias->getPath(),
'alias' => $path_alias->getAlias(),
'langcode' => $path_alias->get('langcode')->value,
];
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function delete($conditions) {
$path = $this->load($conditions);
$query = $this->connection->delete(static::TABLE);
$storage = $this->getPathAliasEntityStorage();
$query = $storage->getQuery();
// API functions should be able to access all entities regardless of access
// restrictions. Those need to happen on a higher level.
$query->accessCheck(FALSE);
foreach ($conditions as $field => $value) {
if ($field == 'source' || $field == 'alias') {
// Use LIKE for case-insensitive matching.
$query->condition($field, $this->connection->escapeLike($value), 'LIKE');
if ($field === 'source') {
$field = 'path';
}
else {
$query->condition($field, $value);
elseif ($field === 'pid') {
$field = 'id';
}
$query->condition($field, $value, '=');
}
try {
$deleted = $query->execute();
}
catch (\Exception $e) {
$this->catchException($e);
$deleted = FALSE;
}
// @todo Switch to using an event for this instead of a hook.
$this->moduleHandler->invokeAll('path_delete', [$path]);
Cache::invalidateTags(['route_match']);
return $deleted;
$result = $query->execute();
$storage->delete($storage->loadMultiple($result));
}
/**
* {@inheritdoc}
*/
public function preloadPathAlias($preloaded, $langcode) {
$langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
$select = $this->connection->select(static::TABLE)
->fields(static::TABLE, ['source', 'alias']);
->fields(static::TABLE, ['path', 'alias']);
if (!empty($preloaded)) {
$conditions = new Condition('OR');
foreach ($preloaded as $preloaded_item) {
$conditions->condition('source', $this->connection->escapeLike($preloaded_item), 'LIKE');
$conditions->condition('path', $this->connection->escapeLike($preloaded_item), 'LIKE');
}
$select->condition($conditions);
}
// 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. We also
// order by pid ASC so that fetchAllKeyed() returns the most recently
// created alias for each source. Subsequent queries using fetchField() must
// use pid DESC to have the same effect.
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
array_pop($langcode_list);
}
elseif ($langcode < LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$select->orderBy('langcode', 'ASC');
}
else {
$select->orderBy('langcode', 'DESC');
}
$this->addLanguageFallback($select, $langcode);
$select->orderBy('pid', 'ASC');
$select->condition('langcode', $langcode_list, 'IN');
try {
return $select->execute()->fetchAllKeyed();
}
catch (\Exception $e) {
$this->catchException($e);
return FALSE;
}
// We order by ID ASC so that fetchAllKeyed() returns the most recently
// created alias for each source. Subsequent queries using fetchField() must
// use ID DESC to have the same effect.
$select->orderBy('id', 'ASC');
return $select->execute()->fetchAllKeyed();
}
/**
* {@inheritdoc}
*/
public function lookupPathAlias($path, $langcode) {
$source = $this->connection->escapeLike($path);
$langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
// See the queries above. Use LIKE for case-insensitive matching.
$select = $this->connection->select(static::TABLE)
->fields(static::TABLE, ['alias'])
->condition('source', $source, 'LIKE');
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
array_pop($langcode_list);
}
elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$select->orderBy('langcode', 'DESC');
}
else {
$select->orderBy('langcode', 'ASC');
}
->condition('path', $this->connection->escapeLike($path), 'LIKE');
$select->orderBy('pid', 'DESC');
$select->condition('langcode', $langcode_list, 'IN');
try {
return $select->execute()->fetchField();
}
catch (\Exception $e) {
$this->catchException($e);
return FALSE;
}
$this->addLanguageFallback($select, $langcode);
$select->orderBy('id', 'DESC');
return $select->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function lookupPathSource($path, $langcode) {
$alias = $this->connection->escapeLike($path);
$langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
public function lookupPathSource($alias, $langcode) {
// See the queries above. Use LIKE for case-insensitive matching.
$select = $this->connection->select(static::TABLE)
->fields(static::TABLE, ['source'])
->condition('alias', $alias, 'LIKE');
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
->fields(static::TABLE, ['path'])
->condition('alias', $this->connection->escapeLike($alias), 'LIKE');
$this->addLanguageFallback($select, $langcode);
$select->orderBy('id', 'DESC');
return $select->execute()->fetchField();
}
/**
* 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) {
$select->orderBy('langcode', 'DESC');
$query->orderBy('langcode', 'DESC');
}
else {
$select->orderBy('langcode', 'ASC');
}
$select->orderBy('pid', 'DESC');
$select->condition('langcode', $langcode_list, 'IN');
try {
return $select->execute()->fetchField();
}
catch (\Exception $e) {
$this->catchException($e);
return FALSE;
$query->orderBy('langcode', 'ASC');
}
$query->condition('langcode', $langcode_list, 'IN');
}
/**
@ -295,30 +263,19 @@ class AliasStorage implements AliasStorageInterface {
->condition('alias', $this->connection->escapeLike($alias), 'LIKE')
->condition('langcode', $langcode);
if (!empty($source)) {
$query->condition('source', $this->connection->escapeLike($source), 'NOT LIKE');
$query->condition('path', $this->connection->escapeLike($source), 'NOT LIKE');
}
$query->addExpression('1');
$query->range(0, 1);
try {
return (bool) $query->execute()->fetchField();
}
catch (\Exception $e) {
$this->catchException($e);
return FALSE;
}
return (bool) $query->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function languageAliasExists() {
try {
return (bool) $this->connection->queryRange('SELECT 1 FROM {url_alias} WHERE langcode <> :langcode', 0, 1, [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])->fetchField();
}
catch (\Exception $e) {
$this->catchException($e);
return FALSE;
}
return (bool) $this->connection->queryRange('SELECT 1 FROM {' . static::TABLE . '} WHERE langcode <> :langcode', 0, 1, [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])->fetchField();
}
/**
@ -332,121 +289,42 @@ class AliasStorage implements AliasStorageInterface {
// Replace wildcards with PDO wildcards.
$query->condition('alias', '%' . preg_replace('!\*+!', '%', $keys) . '%', 'LIKE');
}
try {
return $query
->fields(static::TABLE)
->orderByHeader($header)
->limit(50)
->execute()
->fetchAll();
}
catch (\Exception $e) {
$this->catchException($e);
return [];
}
$query->addField(static::TABLE, 'id', 'pid');
$query->addField(static::TABLE, 'path', 'source');
return $query
->fields(static::TABLE, ['alias', 'langcode'])
->orderByHeader($header)
->limit(50)
->execute()
->fetchAll();
}
/**
* {@inheritdoc}
*/
public function pathHasMatchingAlias($initial_substring) {
$query = $this->connection->select(static::TABLE, 'u');
$query = $this->connection->select(static::TABLE);
$query->addExpression(1);
try {
return (bool) $query
->condition('u.source', $this->connection->escapeLike($initial_substring) . '%', 'LIKE')
->range(0, 1)
->execute()
->fetchField();
}
catch (\Exception $e) {
$this->catchException($e);
return FALSE;
}
return (bool) $query
->condition('path', $this->connection->escapeLike($initial_substring) . '%', 'LIKE')
->range(0, 1)
->execute()
->fetchField();
}
/**
* Check if the table exists and create it if not.
* Returns the path alias entity storage handler.
*
* We can not store it in the constructor because that leads to a circular
* dependency in the service container.
*
* @return \Drupal\Core\Entity\EntityStorageInterface
* The path alias entity storage.
*/
protected function ensureTableExists() {
try {
$database_schema = $this->connection->schema();
if (!$database_schema->tableExists(static::TABLE)) {
$schema_definition = $this->schemaDefinition();
$database_schema->createTable(static::TABLE, $schema_definition);
return TRUE;
}
}
// If another process has already created the table, attempting to recreate
// it will throw an exception. In this case just catch the exception and do
// nothing.
catch (DatabaseException $e) {
return TRUE;
}
return FALSE;
}
/**
* Act on an exception when url_alias might be stale.
*
* If the table does not yet exist, that's fine, but if the table exists and
* yet the query failed, then the url_alias is stale and the exception needs
* to propagate.
*
* @param $e
* The exception.
*
* @throws \Exception
*/
protected function catchException(\Exception $e) {
if ($this->connection->schema()->tableExists(static::TABLE)) {
throw $e;
}
}
/**
* Defines the schema for the {url_alias} table.
*
* @internal
*/
public static function schemaDefinition() {
return [
'description' => 'A list of URL aliases for Drupal paths; a user may visit either the source or destination path.',
'fields' => [
'pid' => [
'description' => 'A unique path alias identifier.',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
'source' => [
'description' => 'The Drupal path this alias is for; e.g. node/12.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'alias' => [
'description' => 'The alias for this path; e.g. title-of-the-story.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'langcode' => [
'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.",
'type' => 'varchar_ascii',
'length' => 12,
'not null' => TRUE,
'default' => '',
],
],
'primary key' => ['pid'],
'indexes' => [
'alias_langcode_pid' => ['alias', 'langcode', 'pid'],
'source_langcode_pid' => ['source', 'langcode', 'pid'],
],
];
protected function getPathAliasEntityStorage() {
return $this->entityTypeManager->getStorage('path_alias');
}
}

View File

@ -0,0 +1,148 @@
<?php
namespace Drupal\Core\Path\Entity;
use Drupal\Core\Entity\ContentEntityBase;
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\Core\Path\PathAliasInterface;
/**
* Defines the path_alias entity class.
*
* @ContentEntityType(
* id = "path_alias",
* label = @Translation("Path alias"),
* label_collection = @Translation("Path aliases"),
* label_singular = @Translation("path alias"),
* label_plural = @Translation("path aliases"),
* label_count = @PluralTranslation(
* singular = "@count path alias",
* plural = "@count path aliases"
* ),
* handlers = {
* "storage" = "Drupal\Core\Path\PathAliasStorage",
* "storage_schema" = "Drupal\Core\Path\PathAliasStorageSchema",
* },
* base_table = "path_alias",
* revision_table = "path_alias_revision",
* entity_keys = {
* "id" = "id",
* "revision" = "revision_id",
* "langcode" = "langcode",
* "uuid" = "uuid",
* },
* admin_permission = "administer url aliases",
* list_cache_tags = { "route_match" },
* )
*/
class PathAlias extends ContentEntityBase implements PathAliasInterface {
/**
* {@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);
$fields['alias'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('Path alias'))
->setDescription(new TranslatableMarkup('An alias used with this path.'))
->setRequired(TRUE)
->setRevisionable(TRUE);
$fields['langcode']->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED);
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->original->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,48 @@
<?php
namespace Drupal\Core\Path;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Provides an interface defining a path_alias entity.
*/
interface PathAliasInterface extends ContentEntityInterface {
/**
* 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,50 @@
<?php
namespace Drupal\Core\Path;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* Defines the storage handler class for path_alias entities.
*/
class PathAliasStorage extends SqlContentEntityStorage {
/**
* {@inheritdoc}
*/
protected function invokeHook($hook, EntityInterface $entity) {
parent::invokeHook($hook, $entity);
// Invoke the deprecated hook_path_OPERATION() hooks.
if ($hook === 'insert' || $hook === 'update' || $hook === 'delete') {
$values = [
'pid' => $entity->id(),
'source' => $entity->getPath(),
'alias' => $entity->getAlias(),
'langcode' => $entity->language()->getId(),
];
if ($hook === 'update') {
$values['original'] = [
'pid' => $entity->id(),
'source' => $entity->original->getPath(),
'alias' => $entity->original->getAlias(),
'langcode' => $entity->original->language()->getId(),
];
}
$this->moduleHandler()->invokeAllDeprecated("It will be removed before Drupal 9.0.0. Use hook_ENTITY_TYPE_{$hook}() for the 'path_alias' entity type instead. See https://www.drupal.org/node/3013865.", 'path_' . $hook, [$values]);
}
}
/**
* {@inheritdoc}
*/
public function createWithSampleValues($bundle = FALSE, array $values = []) {
$entity = parent::createWithSampleValues($bundle, ['path' => '/<front>'] + $values);
$entity->set('alias', '/' . $entity->get('alias')->value);
return $entity;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Drupal\Core\Path;
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);
$schema[$this->storage->getBaseTable()]['indexes'] += [
'path_alias__alias_langcode_id' => ['alias', 'langcode', 'id'],
'path_alias__path_langcode_id' => ['path', 'langcode', 'id'],
];
return $schema;
}
}

View File

@ -36,10 +36,11 @@ class UpdateServiceProvider implements ServiceProviderInterface, ServiceModifier
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
// Prevent the alias-based path processor, which requires a path_alias db
// table, from being registered to the path processor manager. We do this by
// removing the tags that the compiler pass looks for. This means the url
// generator can safely be used during the database update process.
// The alias-based processor requires the path_alias entity schema to be
// installed, so we prevent it from being registered to the path processor
// manager. We do this by removing the tags that the compiler pass looks
// for. This means that the URL generator can safely be used during the
// database update process.
if ($container->hasDefinition('path_processor_alias')) {
$container->getDefinition('path_processor_alias')
->clearTag('path_processor_inbound')

View File

@ -129,8 +129,11 @@ class EntityTypeInfo implements ContainerInjectionInterface {
*/
public function entityTypeAlter(array &$entity_types) {
foreach ($entity_types as $entity_type_id => $entity_type) {
// The ContentModerationState entity type should never be moderated.
if ($entity_type->isRevisionable() && !$entity_type->isInternal()) {
// Internal entity types should never be moderated, and the 'path_alias'
// entity type needs to be excluded for now.
// @todo Enable moderation for path aliases after they become publishable
// in https://www.drupal.org/project/drupal/issues/3007669.
if ($entity_type->isRevisionable() && !$entity_type->isInternal() && $entity_type_id !== 'path_alias') {
$entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type);
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Drupal\Tests\jsonapi\Functional;
use Drupal\Core\Path\Entity\PathAlias;
use Drupal\Core\Url;
/**
* JSON:API integration test for the "PathAlias" content entity type.
*
* @group jsonapi
*/
class PathAliasTest extends ResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'path_alias';
/**
* {@inheritdoc}
*/
protected static $resourceTypeName = 'path_alias--path_alias';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [];
/**
* {@inheritdoc}
*
* @var \Drupal\user\RoleInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer url aliases']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$path_alias = PathAlias::create([
'alias' => '/frontpage1',
'path' => '/<front>',
'langcode' => 'en',
]);
$path_alias->save();
return $path_alias;
}
/**
* {@inheritdoc}
*/
protected function getExpectedDocument() {
$self_url = Url::fromUri('base:/jsonapi/path_alias/path_alias/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
return [
'jsonapi' => [
'meta' => [
'links' => [
'self' => ['href' => 'http://jsonapi.org/format/1.0/'],
],
],
'version' => '1.0',
],
'links' => [
'self' => ['href' => $self_url],
],
'data' => [
'id' => $this->entity->uuid(),
'type' => static::$resourceTypeName,
'links' => [
'self' => ['href' => $self_url],
],
'attributes' => [
'alias' => '/frontpage1',
'path' => '/<front>',
'langcode' => 'en',
'drupal_internal__id' => 1,
'drupal_internal__revision_id' => 1,
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getPostDocument() {
return [
'data' => [
'type' => static::$resourceTypeName,
'attributes' => [
'alias' => '/frontpage1',
'path' => '/<front>',
'langcode' => 'en',
],
],
];
}
}

View File

@ -2050,7 +2050,9 @@ abstract class ResourceTestBase extends BrowserTestBase {
$doc = $this->getModifiedEntityForPostTesting();
$doc['data']['id'] = $uuid;
$label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
$doc['data']['attributes'][$label_field] = [['value' => $this->randomMachineName()]];
if (isset($label_field)) {
$doc['data']['attributes'][$label_field] = [['value' => $this->randomMachineName()]];
}
$request_options[RequestOptions::BODY] = Json::encode($doc);
$response = $this->request('POST', $url, $request_options);
@ -2060,7 +2062,9 @@ abstract class ResourceTestBase extends BrowserTestBase {
$doc = $this->getModifiedEntityForPostTesting();
$new_uuid = \Drupal::service('uuid')->generate();
$doc['data']['id'] = $new_uuid;
$doc['data']['attributes'][$label_field] = [['value' => $this->randomMachineName()]];
if (isset($label_field)) {
$doc['data']['attributes'][$label_field] = [['value' => $this->randomMachineName()]];
}
$request_options[RequestOptions::BODY] = Json::encode($doc);
$response = $this->request('POST', $url, $request_options);
@ -2094,7 +2098,9 @@ abstract class ResourceTestBase extends BrowserTestBase {
$unparseable_request_body = '!{>}<';
$parseable_valid_request_body = Json::encode($this->getPatchDocument());
/* $parseable_valid_request_body_2 = Json::encode($this->getNormalizedPatchEntity()); */
$parseable_invalid_request_body = Json::encode($this->makeNormalizationInvalid($this->getPatchDocument(), 'label'));
if ($this->entity->getEntityType()->hasKey('label')) {
$parseable_invalid_request_body = Json::encode($this->makeNormalizationInvalid($this->getPatchDocument(), 'label'));
}
$parseable_invalid_request_body_2 = Json::encode(NestedArray::mergeDeep(['data' => ['attributes' => ['field_rest_test' => $this->randomString()]]], $this->getPatchDocument()));
// The 'field_rest_test' field does not allow 'view' access, so does not end
// up in the JSON:API document. Even when we explicitly add it to the JSON
@ -2340,7 +2346,7 @@ abstract class ResourceTestBase extends BrowserTestBase {
// Ensure that PATCHing an entity that is not the latest revision is
// unsupported.
if (!$this->entity->getEntityType()->isRevisionable() || !$this->entity instanceof FieldableEntityInterface) {
if (!$this->entity->getEntityType()->isRevisionable() || !$this->entity->getEntityType()->hasHandlerClass('moderation') || !$this->entity instanceof FieldableEntityInterface) {
return;
}
assert($this->entity instanceof RevisionableInterface);

View File

@ -9,8 +9,8 @@
* Implements hook_install().
*/
function menu_link_content_install() {
// Add a higher weight so that menu_link_content_path_update() is called after
// system_path_update() clears the path alias cache.
// Add a higher weight so that menu_link_content_path_alias_update() is called
// after system_path_alias_update() clears the path alias cache.
// @todo remove this when the cache clearing is moved to path module or if
// caching is removed for path aliases due to
// https://www.drupal.org/node/1965074

View File

@ -7,6 +7,7 @@
use Drupal\Core\Url;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Path\PathAliasInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\system\MenuInterface;
@ -50,10 +51,10 @@ function menu_link_content_menu_delete(MenuInterface $menu) {
}
/**
* Implements hook_path_insert().
* Implements hook_ENTITY_TYPE_insert() for 'path_alias'.
*/
function menu_link_content_path_insert($path) {
_menu_link_content_update_path_alias($path['alias']);
function menu_link_content_path_alias_insert(PathAliasInterface $path_alias) {
_menu_link_content_update_path_alias($path_alias->getAlias());
}
/**
@ -75,23 +76,23 @@ function _menu_link_content_update_path_alias($path) {
}
/**
* Implements hook_path_update().
* Implements hook_ENTITY_TYPE_update() for 'path_alias'.
*/
function menu_link_content_path_update($path) {
if ($path['alias'] != $path['original']['alias']) {
_menu_link_content_update_path_alias($path['alias']);
_menu_link_content_update_path_alias($path['original']['alias']);
function menu_link_content_path_alias_update(PathAliasInterface $path_alias) {
if ($path_alias->getAlias() != $path_alias->original->getAlias()) {
_menu_link_content_update_path_alias($path_alias->getAlias());
_menu_link_content_update_path_alias($path_alias->original->getAlias());
}
elseif ($path['source'] != $path['original']['source']) {
_menu_link_content_update_path_alias($path['alias']);
elseif ($path_alias->getPath() != $path_alias->original->getPath()) {
_menu_link_content_update_path_alias($path_alias->getAlias());
}
}
/**
* Implements hook_path_delete().
* Implements hook_ENTITY_TYPE_delete() for 'path_alias'.
*/
function menu_link_content_path_delete($path) {
_menu_link_content_update_path_alias($path['alias']);
function menu_link_content_path_alias_delete(PathAliasInterface $path_alias) {
_menu_link_content_update_path_alias($path_alias->getAlias());
}
/**

View File

@ -28,6 +28,7 @@ class PathAliasMenuLinkContentTest extends KernelTestBase {
$this->installEntitySchema('user');
$this->installEntitySchema('menu_link_content');
$this->installEntitySchema('path_alias');
// Ensure that the weight of module_link_content is higher than system.
// @see menu_link_content_install()

View File

@ -42,6 +42,7 @@ trait CreateTestContentEntitiesTrait {
$this->installEntitySchema('file');
$this->installEntitySchema('menu_link_content');
$this->installEntitySchema('node');
$this->installEntitySchema('path_alias');
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('user');
}

View File

@ -86,6 +86,7 @@ class Upgrade6Test extends MigrateUpgradeExecuteTestBase {
'shortcut_set' => 1,
'action' => 25,
'menu' => 8,
'path_alias' => 8,
'taxonomy_term' => 15,
'taxonomy_vocabulary' => 7,
'tour' => 5,

View File

@ -90,6 +90,7 @@ class Upgrade7Test extends MigrateUpgradeExecuteTestBase {
'menu' => 6,
'taxonomy_term' => 24,
'taxonomy_vocabulary' => 7,
'path_alias' => 8,
'tour' => 5,
'user' => 4,
'user_role' => 3,

View File

@ -17,7 +17,10 @@
* The array structure is identical to that of the return value of
* \Drupal\Core\Path\AliasStorageInterface::save().
*
* @see \Drupal\Core\Path\AliasStorageInterface::save()
* @deprecated in drupal:8.8.0 and will be removed from drupal:9.0.0. Use
* hook_path_alias_insert() instead.
*
* @see https://www.drupal.org/node/3013865
*/
function hook_path_insert($path) {
\Drupal::database()->insert('mytable')
@ -35,7 +38,10 @@ function hook_path_insert($path) {
* The array structure is identical to that of the return value of
* \Drupal\Core\Path\AliasStorageInterface::save().
*
* @see \Drupal\Core\Path\AliasStorageInterface::save()
* @deprecated in drupal:8.8.0 and will be removed from drupal:9.0.0. Use
* hook_path_alias_update() instead.
*
* @see https://www.drupal.org/node/3013865
*/
function hook_path_update($path) {
if ($path['alias'] != $path['original']['alias']) {
@ -53,7 +59,10 @@ function hook_path_update($path) {
* The array structure is identical to that of the return value of
* \Drupal\Core\Path\AliasStorageInterface::save().
*
* @see \Drupal\Core\Path\AliasStorageInterface::delete()
* @deprecated in drupal:8.8.0 and will be removed from drupal:9.0.0. Use
* hook_path_alias_delete() instead.
*
* @see https://www.drupal.org/node/3013865
*/
function hook_path_delete($path) {
\Drupal::database()->delete('mytable')

View File

@ -368,7 +368,11 @@ class PathAliasTest extends PathTestBase {
* Integer representing the path ID.
*/
public function getPID($alias) {
return Database::getConnection()->query("SELECT pid FROM {url_alias} WHERE alias = :alias", [':alias' => $alias])->fetchField();
$result = \Drupal::entityTypeManager()->getStorage('path_alias')->getQuery()
->condition('alias', $alias, '=')
->accessCheck(FALSE)
->execute();
return reset($result);
}
/**

View File

@ -31,6 +31,7 @@ class MigrateUrlAliasTest extends MigrateDrupal6TestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('path_alias');
$this->installConfig(['node']);
$this->installSchema('node', ['node_access']);
$this->migrateUsers(FALSE);

View File

@ -31,6 +31,9 @@ class MigrateUrlAliasTest extends MigrateDrupal7TestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('path_alias');
$this->installConfig('node');
$this->installSchema('node', ['node_access']);
$this->migrateUsers(FALSE);

View File

@ -29,6 +29,7 @@ class PathItemTest extends KernelTestBase {
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('path_alias');
$this->installSchema('node', ['node_access']);

View File

@ -901,9 +901,10 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 422 when invalid entity: multiple values sent for single-value field.
$response = $this->request('POST', $url, $request_options);
$label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
$label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
if ($label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName) {
$label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
@ -988,7 +989,9 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// 500 when creating an entity with a duplicate UUID.
$normalized_entity = $this->getModifiedEntityForPostTesting();
$normalized_entity[$created_entity->getEntityType()->getKey('uuid')] = [['value' => $created_entity->uuid()]];
$normalized_entity[$label_field] = [['value' => $this->randomMachineName()]];
if ($label_field) {
$normalized_entity[$label_field] = [['value' => $this->randomMachineName()]];
}
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalized_entity, static::$format);
$response = $this->request('POST', $url, $request_options);
@ -999,7 +1002,9 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$normalized_entity = $this->getModifiedEntityForPostTesting();
$new_uuid = \Drupal::service('uuid')->generate();
$normalized_entity[$created_entity->getEntityType()->getKey('uuid')] = [['value' => $new_uuid]];
$normalized_entity[$label_field] = [['value' => $this->randomMachineName()]];
if ($label_field) {
$normalized_entity[$label_field] = [['value' => $this->randomMachineName()]];
}
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalized_entity, static::$format);
$response = $this->request('POST', $url, $request_options);
@ -1130,9 +1135,10 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 422 when invalid entity: multiple values sent for single-value field.
$response = $this->request('PATCH', $url, $request_options);
$label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
$label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
if ($label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName) {
$label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
@ -1499,8 +1505,9 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
switch ($entity_key) {
case 'label':
// Add a second label to this entity to make it invalid.
$label_field = $entity_type->hasKey('label') ? $entity_type->getKey('label') : static::$labelFieldName;
$normalization[$label_field][1]['value'] = 'Second Title';
if ($label_field = $entity_type->hasKey('label') ? $entity_type->getKey('label') : static::$labelFieldName) {
$normalization[$label_field][1]['value'] = 'Second Title';
}
break;
case 'id':
$normalization[$entity_type->getKey('id')][0]['value'] = $this->anotherEntity->id();

View File

@ -3,6 +3,7 @@
namespace Drupal\Tests\rest\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\rest\RestPermissions;
/**
* Tests that existing sites continue to use permissions for EntityResource.
@ -33,16 +34,17 @@ class EntityResourcePermissionsUpdateTest extends UpdatePathTestBase {
* Tests rest_update_8203().
*/
public function testBcEntityResourcePermissionSettingAdded() {
$permission_handler = $this->container->get('user.permissions');
$is_rest_resource_permission = function ($permission) {
return $permission['provider'] === 'rest' && (string) $permission['title'] !== 'Administer REST resource configuration';
};
// Make sure we have the expected values before the update.
$rest_settings = $this->config('rest.settings');
$this->assertFalse(array_key_exists('bc_entity_resource_permissions', $rest_settings->getRawData()));
$this->assertEqual([], array_filter($permission_handler->getPermissions(), $is_rest_resource_permission));
// We can not use the 'user.permissions' service here because some
// permissions include generated URLs inside their description, thus
// requiring the path alias system, which is not guaranteed to be working
// before running the database updates.
$rest_permissions_callback = \Drupal::service('controller_resolver')->getControllerFromDefinition(RestPermissions::class . '::permissions');
$rest_permissions = array_keys(call_user_func($rest_permissions_callback));
$this->assertEquals([], $rest_permissions);
$this->runUpdates();
@ -50,8 +52,10 @@ class EntityResourcePermissionsUpdateTest extends UpdatePathTestBase {
$rest_settings = $this->config('rest.settings');
$this->assertTrue(array_key_exists('bc_entity_resource_permissions', $rest_settings->getRawData()));
$this->assertTrue($rest_settings->get('bc_entity_resource_permissions'));
$rest_permissions = array_keys(array_filter($permission_handler->getPermissions(), $is_rest_resource_permission));
$this->assertEqual(['restful delete entity:node', 'restful get entity:node', 'restful patch entity:node', 'restful post entity:node'], $rest_permissions);
$rest_permissions_callback = \Drupal::service('controller_resolver')->getControllerFromDefinition(RestPermissions::class . '::permissions');
$rest_permissions = array_keys(call_user_func($rest_permissions_callback));
$this->assertEquals(['restful get entity:node', 'restful post entity:node', 'restful delete entity:node', 'restful patch entity:node'], $rest_permissions);
}
}

View File

@ -386,10 +386,10 @@ EOD;
}
if ($container->hasDefinition('path_processor_alias')) {
// Prevent the alias-based path processor, which requires a url_alias db
// table, from being registered to the path processor manager. We do this
// by removing the tags that the compiler pass looks for. This means the
// url generator can safely be used within tests.
// The alias-based processor requires the path_alias entity schema to be
// installed, so we prevent it from being registered to the path processor
// manager. We do this by removing the tags that the compiler pass looks
// for. This means that the URL generator can safely be used within tests.
$definition = $container->getDefinition('path_processor_alias');
$definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound');
}

View File

@ -1,96 +0,0 @@
<?php
namespace Drupal\system\Tests\Path;
use Drupal\Core\Database\Connection;
use Drupal\Core\Path\AliasStorage;
/**
* Utility methods to generate sample data, database configuration, etc.
*/
class UrlAliasFixtures {
/**
* Create the tables required for the sample data.
*
* @param \Drupal\Core\Database\Connection $connection
* The connection to use to create the tables.
*/
public function createTables(Connection $connection) {
$tables = $this->tableDefinition();
$schema = $connection->schema();
foreach ($tables as $name => $table) {
$schema->dropTable($name);
$schema->createTable($name, $table);
}
}
/**
* Drop the tables used for the sample data.
*
* @param \Drupal\Core\Database\Connection $connection
* The connection to use to drop the tables.
*/
public function dropTables(Connection $connection) {
$tables = $this->tableDefinition();
$schema = $connection->schema();
foreach ($tables as $name => $table) {
$schema->dropTable($name);
}
}
/**
* Returns an array of URL aliases for testing.
*
* @return array of URL alias definitions.
*/
public function sampleUrlAliases() {
return [
[
'source' => '/node/1',
'alias' => '/alias_for_node_1_en',
'langcode' => 'en',
],
[
'source' => '/node/2',
'alias' => '/alias_for_node_2_en',
'langcode' => 'en',
],
[
'source' => '/node/1',
'alias' => '/alias_for_node_1_fr',
'langcode' => 'fr',
],
[
'source' => '/node/1',
'alias' => '/alias_for_node_1_und',
'langcode' => 'und',
],
];
}
/**
* Returns the table definition for the URL alias fixtures.
*
* @return array
* Table definitions.
*/
public function tableDefinition() {
$tables = [];
// Prime the drupal_get_filename() cache with the location of the system
// module as its location is known and shouldn't change.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml');
module_load_install('system');
$schema = system_schema();
$tables['url_alias'] = AliasStorage::schemaDefinition();
$tables['key_value'] = $schema['key_value'];
return $tables;
}
}

View File

@ -13,13 +13,16 @@ use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Path\AliasStorage;
use Drupal\Core\Path\Entity\PathAlias;
use Drupal\Core\Path\PathAliasStorage;
use Drupal\Core\Path\PathAliasStorageSchema;
use Drupal\Core\Site\Settings;
use Drupal\Core\StreamWrapper\PrivateStream;
use Drupal\Core\StreamWrapper\PublicStream;
@ -1229,11 +1232,6 @@ function system_schema() {
],
];
// Create the url_alias table. The alias_storage service can auto-create its
// table, but this relies on exceptions being thrown. These exceptions will be
// thrown every request until an alias is created.
$schema['url_alias'] = AliasStorage::schemaDefinition();
return $schema;
}
@ -2359,3 +2357,118 @@ function system_update_8802() {
->save(TRUE);
}
}
/**
* Install the 'path_alias' entity type.
*/
function system_update_8803() {
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
if (!$entity_definition_update_manager->getEntityType('path_alias')) {
$entity_type = new ContentEntityType([
'id' => 'path_alias',
'class' => PathAlias::class,
'label' => new TranslatableMarkup('Path alias'),
'handlers' => [
'storage' => PathAliasStorage::class,
'storage_schema' => PathAliasStorageSchema::class,
],
'base_table' => 'path_alias',
'revision_table' => 'path_alias_revision',
'entity_keys' => [
'id' => 'id',
'revision' => 'revision_id',
'langcode' => 'langcode',
'uuid' => 'uuid',
],
]);
$entity_definition_update_manager->installEntityType($entity_type);
return t('The "path_alias" entity type has been installed.');
}
}
/**
* Convert path aliases to entities.
*/
function system_update_8804(&$sandbox = NULL) {
// Bail out early if the entity type is not using the default storage class.
$storage = \Drupal::entityTypeManager()->getStorage('path_alias');
if (!$storage instanceof PathAliasStorage) {
return;
}
if (!isset($sandbox['current_id'])) {
// This must be the first run. Initialize the sandbox.
$sandbox['progress'] = 0;
$sandbox['current_id'] = 0;
}
$database = \Drupal::database();
$step_size = 200;
$url_aliases = $database->select('url_alias', 't')
->condition('t.pid', $sandbox['current_id'], '>')
->fields('t')
->orderBy('pid', 'ASC')
->range(0, $step_size)
->execute()
->fetchAll();
if ($url_aliases) {
/** @var \Drupal\Component\Uuid\UuidInterface $uuid */
$uuid = \Drupal::service('uuid');
$base_table_insert = $database->insert('path_alias');
$base_table_insert->fields(['id', 'revision_id', 'uuid', 'path', 'alias', 'langcode']);
$revision_table_insert = $database->insert('path_alias_revision');
$revision_table_insert->fields(['id', 'revision_id', 'path', 'alias', 'langcode', 'revision_default']);
foreach ($url_aliases as $url_alias) {
$values = [
'id' => $url_alias->pid,
'revision_id' => $url_alias->pid,
'uuid' => $uuid->generate(),
'path' => $url_alias->source,
'alias' => $url_alias->alias,
'langcode' => $url_alias->langcode,
];
$base_table_insert->values($values);
unset($values['uuid']);
$values['revision_default'] = 1;
$revision_table_insert->values($values);
}
$base_table_insert->execute();
$revision_table_insert->execute();
$sandbox['progress'] += count($url_aliases);
$last_url_alias = end($url_aliases);
$sandbox['current_id'] = $last_url_alias->pid;
// If we're not in maintenance mode, the number of path aliases could change
// at any time so make sure that we always use the latest record count.
$missing = $database->select('url_alias', 't')
->condition('t.pid', $sandbox['current_id'], '>')
->orderBy('pid', 'ASC')
->countQuery()
->execute()
->fetchField();
$sandbox['#finished'] = $missing ? $sandbox['progress'] / ($sandbox['progress'] + (int) $missing) : 1;
}
else {
$sandbox['#finished'] = 1;
}
if ($sandbox['#finished'] >= 1) {
// Keep a backup of the old 'url_alias' table if requested.
if (Settings::get('entity_update_backup', TRUE)) {
$old_table_name = 'old_' . substr(uniqid(), 0, 6) . '_url_alias';
if (!$database->schema()->tableExists($old_table_name)) {
$database->schema()->renameTable('url_alias', $old_table_name);
}
}
else {
$database->schema()->dropTable('url_alias');
}
return t('Path aliases have been converted to entities.');
}
}

View File

@ -1421,29 +1421,6 @@ function system_block_view_system_main_block_alter(array &$build, BlockPluginInt
unset($build['#contextual_links']);
}
/**
* Implements hook_path_update().
*/
function system_path_update($path) {
$alias_manager = \Drupal::service('path.alias_manager');
$alias_manager->cacheClear($path['source']);
$alias_manager->cacheClear($path['original']['source']);
}
/**
* Implements hook_path_insert().
*/
function system_path_insert($path) {
\Drupal::service('path.alias_manager')->cacheClear($path['source']);
}
/**
* Implements hook_path_delete().
*/
function system_path_delete($path) {
\Drupal::service('path.alias_manager')->cacheClear($path['source']);
}
/**
* Implements hook_query_TAG_alter() for entity reference selection handlers.
*/

View File

@ -0,0 +1,46 @@
<?php
// @codingStandardsIgnoreFile
/**
* @file
* Contains database additions to drupal-8.filled.standard.php.gz for testing
* the upgrade path of https://www.drupal.org/node/2336597.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Add a few more url aliases with various language codes.
$connection->insert('url_alias')
->fields([
'pid',
'source',
'alias',
'langcode',
])
->values([
'pid' => '2',
'source' => '/node/1',
'alias' => '/test-article-new-alias',
'langcode' => 'und',
])
->values([
'pid' => '3',
'source' => '/node/8',
'alias' => '/test-alias-for-any-language',
'langcode' => 'und',
])
->values([
'pid' => '4',
'source' => '/node/8',
'alias' => '/test-alias-in-english',
'langcode' => 'en',
])
->values([
'pid' => '5',
'source' => '/node/8',
'alias' => '/test-alias-in-spanish',
'langcode' => 'es',
])
->execute();

View File

@ -0,0 +1,6 @@
name: 'Path deprecated test'
type: module
description: 'Support module for testing deprecated functionality for path aliases.'
package: Testing
version: VERSION
core: 8.x

View File

@ -0,0 +1,27 @@
<?php
/**
* @file
* Support module for testing deprecated functionality for path aliases.
*/
/**
* Implements hook_path_insert().
*/
function path_deprecated_test_path_insert($path) {
// Nothing to do here.
}
/**
* Implements hook_path_update().
*/
function path_deprecated_test_path_update($path) {
// Nothing to do here.
}
/**
* Implements hook_path_delete().
*/
function path_deprecated_test_path_delete($path) {
// Nothing to do here.
}

View File

@ -1,6 +0,0 @@
name: 'Hook path tests'
type: module
description: 'Support module for path hook testing.'
package: Testing
version: VERSION
core: 8.x

View File

@ -1,22 +0,0 @@
<?php
/**
* @file
* Helper module for the path tests.
*/
/**
* Resets the path test results.
*/
function path_test_reset() {
\Drupal::state()->set('path_test.results', []);
}
/**
* Implements hook_path_update().
*/
function path_test_path_update($path) {
$results = \Drupal::state()->get('path_test.results') ?: [];
$results['hook_path_update'] = $path;
\Drupal::state()->set('path_test.results', $results);
}

View File

@ -26,8 +26,8 @@ class UrlAlterFunctionalTest extends BrowserTestBase {
* Test that URL altering works and that it occurs in the correct order.
*/
public function testUrlAlter() {
// Ensure that the url_alias table exists after Drupal installation.
$this->assertTrue(Database::getConnection()->schema()->tableExists('url_alias'), 'The url_alias table exists after Drupal installation.');
// 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.

View File

@ -0,0 +1,103 @@
<?php
namespace Drupal\Tests\system\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests the conversion of path aliases to entities.
*
* @group Update
* @group legacy
*/
class PathAliasToEntityUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../tests/fixtures/update/drupal-8.filled.standard.php.gz',
__DIR__ . '/../../../../tests/fixtures/update/drupal-8.convert-path-aliases-to-entities-2336597.php',
];
}
/**
* Tests the conversion of path aliases to entities.
*
* @see system_update_8803()
* @see system_update_8804()
*/
public function testConversionToEntities() {
$database = \Drupal::database();
$schema = $database->schema();
$this->assertTrue($schema->tableExists('url_alias'));
$query = $database->select('url_alias');
$query->addField('url_alias', 'pid', 'id');
$query->addField('url_alias', 'source', 'path');
$query->addField('url_alias', 'alias');
$query->addField('url_alias', 'langcode');
$original_records = $query->execute()->fetchAllAssoc('id');
// drupal-8.filled.standard.php.gz contains one URL alias and
// drupal-8.convert-path-aliases-to-entities-2336597.php adds another four.
$url_alias_count = 5;
$this->assertCount($url_alias_count, $original_records);
$this->runUpdates();
// Check that the 'path_alias' entity tables have been created and the
// 'url_alias' table has been deleted.
$this->assertTrue($schema->tableExists('path_alias'));
$this->assertTrue($schema->tableExists('path_alias_revision'));
$this->assertFalse($schema->tableExists('url_alias'));
// Check that we have a backup of the old table.
$this->assertCount(1, $schema->findTables('old_%_url_alias'));
$path_alias_count = \Drupal::entityTypeManager()->getStorage('path_alias')->loadMultiple();
$this->assertCount($url_alias_count, $path_alias_count);
// Make sure that existing aliases still work.
$assert_session = $this->assertSession();
$this->drupalGet('test-article');
$assert_session->responseContains('/node/1');
$assert_session->pageTextContains('Test Article - New title');
$this->drupalGet('test-article-new-alias');
$assert_session->responseContains('/node/1');
$assert_session->pageTextContains('Test Article - New title');
$this->drupalGet('test-alias-for-any-language');
$assert_session->responseContains('/node/8');
$assert_session->pageTextContains('Test title');
$this->drupalGet('test-alias-in-english');
$assert_session->responseContains('/node/8');
$assert_session->pageTextContains('Test title');
$spanish = \Drupal::languageManager()->getLanguage('es');
$this->drupalGet('test-alias-for-any-language', ['language' => $spanish]);
$assert_session->responseContains('/es/node/8');
$assert_session->pageTextContains('Test title Spanish');
$this->drupalGet('test-alias-in-spanish', ['language' => $spanish]);
$assert_session->responseContains('/es/node/8');
$assert_session->pageTextContains('Test title Spanish');
// Check that correct data was written in both the base and the revision
// tables.
$base_table_records = $database->select('path_alias')
->fields('path_alias', ['id', 'path', 'alias', 'langcode'])
->execute()->fetchAllAssoc('id');
$this->assertEquals($original_records, $base_table_records);
$revision_table_records = $database->select('path_alias_revision')
->fields('path_alias_revision', ['id', 'path', 'alias', 'langcode'])
->execute()->fetchAllAssoc('id');
$this->assertEquals($original_records, $revision_table_records);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Drupal\Tests\system\Kernel;
use Drupal\Core\Language\LanguageInterface;
use Drupal\KernelTests\KernelTestBase;
/**
* @coversDefaultClass \Drupal\Core\Path\AliasStorage
*
* @group path
* @group legacy
*/
class DeprecatedPathHooksTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['system', 'path_deprecated_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('path_alias');
}
/**
* @covers ::save
*
* @expectedDeprecation The deprecated hook hook_path_insert() is implemented in these functions: path_deprecated_test_path_insert(). It will be removed before Drupal 9.0.0. Use hook_ENTITY_TYPE_insert() for the 'path_alias' entity type instead. See https://www.drupal.org/node/3013865.
*/
public function testInsert() {
$source = '/' . $this->randomMachineName();
$alias = '/' . $this->randomMachineName();
$alias_storage = \Drupal::service('path.alias_storage');
$alias_storage->save($source, $alias);
}
/**
* @covers ::save
*
* @expectedDeprecation The deprecated hook hook_path_update() is implemented in these functions: path_deprecated_test_path_update(). It will be removed before Drupal 9.0.0. Use hook_ENTITY_TYPE_update() for the 'path_alias' entity type instead. See https://www.drupal.org/node/3013865.
*/
public function testUpdate() {
$source = '/' . $this->randomMachineName();
$alias = '/' . $this->randomMachineName();
$alias_storage = \Drupal::service('path.alias_storage');
$alias_storage->save($source, $alias);
$new_source = '/' . $this->randomMachineName();
$path = $alias_storage->load(['source' => $source]);
$alias_storage->save($new_source, $alias, LanguageInterface::LANGCODE_NOT_SPECIFIED, $path['pid']);
}
/**
* @covers ::delete
*
* @expectedDeprecation The deprecated hook hook_path_delete() is implemented in these functions: path_deprecated_test_path_delete(). It will be removed before Drupal 9.0.0. Use hook_ENTITY_TYPE_delete() for the 'path_alias' entity type instead. See https://www.drupal.org/node/3013865.
*/
public function testDelete() {
$source = '/' . $this->randomMachineName();
$alias = '/' . $this->randomMachineName();
$alias_storage = \Drupal::service('path.alias_storage');
$alias_storage->save($source, $alias);
$path = $alias_storage->load(['source' => $source]);
$alias_storage->delete(['pid' => $path['pid']]);
}
}

View File

@ -18,13 +18,22 @@ class PathHooksTest extends KernelTestBase {
public static $modules = ['system'];
/**
* Test system_path_*() correctly clears caches.
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('path_alias');
}
/**
* Test system_path_alias_*() correctly clears caches.
*/
public function testPathHooks() {
$source = '/' . $this->randomMachineName();
$alias = '/' . $this->randomMachineName();
// Check system_path_insert();
// Check system_path_alias_insert();
$alias_manager = $this->prophesize(AliasManagerInterface::class);
$alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(1);
$alias_manager->cacheClear($source)->shouldBeCalledTimes(1);
@ -35,7 +44,7 @@ class PathHooksTest extends KernelTestBase {
$new_source = '/' . $this->randomMachineName();
$path = $alias_storage->load(['source' => $source]);
// Check system_path_update();
// Check system_path_alias_update();
$alias_manager = $this->prophesize(AliasManagerInterface::class);
$alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(2);
$alias_manager->cacheClear($source)->shouldBeCalledTimes(1);
@ -43,7 +52,7 @@ class PathHooksTest extends KernelTestBase {
\Drupal::getContainer()->set('path.alias_manager', $alias_manager->reveal());
$alias_storage->save($new_source, $alias, LanguageInterface::LANGCODE_NOT_SPECIFIED, $path['pid']);
// Check system_path_delete();
// Check system_path_alias_delete();
$alias_manager = $this->prophesize(AliasManagerInterface::class);
$alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(1);
$alias_manager->cacheClear($new_source)->shouldBeCalledTimes(1);

View File

@ -46,7 +46,14 @@ class ExposedFilterBlocksUpdateTest extends UpdatePathTestBase {
// the config schema checker ignore the block.
static::$configSchemaCheckerExclusions[] = 'block.block.seven_secondary_local_tasks';
$this->container->get('module_installer')->uninstall(['block']);
// We need to uninstall the menu_link_content module because
// menu_link_content_entity_predelete() invokes alias processing and we
// don't have a working path alias system until system_update_8803() runs.
// Note that path alias processing is disabled during the regular database
// update process, so this only happens because we uninstall the Block
// module before running the updates.
// @see \Drupal\Core\Update\UpdateServiceProvider::alter()
$this->container->get('module_installer')->uninstall(['menu_link_content', 'block']);
$this->runUpdates();
}

View File

@ -0,0 +1,29 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group hal
*/
class PathAliasHalJsonAnonTest extends PathAliasHalJsonTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class PathAliasHalJsonBasicAuthTest extends PathAliasHalJsonTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal', 'basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class PathAliasHalJsonCookieTest extends PathAliasHalJsonTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View File

@ -0,0 +1,56 @@
<?php
namespace Drupal\FunctionalTests\Hal;
use Drupal\FunctionalTests\Rest\PathAliasResourceTestBase;
use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
/**
* Base hal_json test class for the path_alias entity type.
*/
abstract class PathAliasHalJsonTestBase extends PathAliasResourceTestBase {
use HalEntityNormalizationTrait;
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$default_normalization = parent::getExpectedNormalizedEntity();
$normalization = $this->applyHalFieldNormalization($default_normalization);
return $normalization + [
'_links' => [
'self' => [
'href' => '',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/path_alias/path_alias',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return parent::getNormalizedPostEntity() + [
'_links' => [
'type' => [
'href' => $this->baseUrl . '/rest/type/path_alias/path_alias',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return [
'url.site',
'user.permissions',
];
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,119 @@
<?php
namespace Drupal\FunctionalTests\Rest;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Path\Entity\PathAlias;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
* Base class for path_alias EntityResource tests.
*/
abstract class PathAliasResourceTestBase extends EntityResourceTestBase {
use BcTimestampNormalizerUnixTestTrait;
/**
* {@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',
],
],
'uuid' => [
[
'value' => $this->entity->uuid(),
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'path' => [
[
'value' => '/<front>',
],
],
'alias' => [
[
'value' => '/frontpage1',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return ['user.permissions'];
}
}

View File

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

View File

@ -0,0 +1,38 @@
<?php
namespace Drupal\FunctionalTests\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
*/
class PathAliasXmlBasicAuthTest extends PathAliasResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@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,33 @@
<?php
namespace Drupal\FunctionalTests\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* Test path_alias entities for XML requests.
*
* @group path
*/
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';
}

View File

@ -93,6 +93,7 @@ class DbDumpTest extends KernelTestBase {
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installEntitySchema('menu_link_content');
$this->installEntitySchema('path_alias');
$this->installSchema('system', 'sequences');
// Place some sample config to test for in the export.
@ -107,7 +108,7 @@ class DbDumpTest extends KernelTestBase {
$account = User::create(['mail' => 'q\'uote$dollar@example.com', 'name' => '$dollar']);
$account->save();
// Create url_alias (this will create 'url_alias').
// Create a path alias.
$this->container->get('path.alias_storage')->save('/user/' . $account->id(), '/user/example');
// Create a cache table (this will create 'cache_discovery').
@ -134,7 +135,8 @@ class DbDumpTest extends KernelTestBase {
'menu_link_content_field_revision',
'sequences',
'sessions',
'url_alias',
'path_alias',
'path_alias_revision',
'user__roles',
'users',
'users_field_data',

View File

@ -40,6 +40,7 @@ class CreateSampleEntityTest extends KernelTestBase {
$this->installEntitySchema('file');
$this->installEntitySchema('comment');
$this->installEntitySchema('comment_type');
$this->installEntitySchema('path_alias');
$this->installEntitySchema('taxonomy_vocabulary');
$this->installEntitySchema('taxonomy_term');
$this->entityTypeManager = $this->container->get('entity_type.manager');

View File

@ -27,6 +27,7 @@ class AliasStorageTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->installEntitySchema('path_alias');
$this->storage = $this->container->get('path.alias_storage');
}

View File

@ -4,39 +4,51 @@ namespace Drupal\KernelTests\Core\Path;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Cache\MemoryCounterBackend;
use Drupal\Core\Path\AliasStorage;
use Drupal\Core\Database\Database;
use Drupal\Core\Path\AliasManager;
use Drupal\Core\Path\AliasWhitelist;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests path alias CRUD and lookup functionality.
*
* @group Path
*/
class AliasTest extends PathUnitTestBase {
class AliasTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// The alias whitelist 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');
}
public function testCRUD() {
// Prepare database table.
$connection = Database::getConnection();
$this->fixtures->createTables($connection);
// Create Path object.
$aliasStorage = new AliasStorage($connection, $this->container->get('module_handler'));
$aliasStorage = $this->container->get('path.alias_storage');
$aliases = $this->fixtures->sampleUrlAliases();
$aliases = $this->sampleUrlAliases();
// Create a few aliases
foreach ($aliases as $idx => $alias) {
$aliasStorage->save($alias['source'], $alias['alias'], $alias['langcode']);
$result = $connection->query('SELECT * FROM {url_alias} WHERE source = :source AND alias= :alias AND langcode = :langcode', [':source' => $alias['source'], ':alias' => $alias['alias'], ':langcode' => $alias['langcode']]);
$result = $connection->query('SELECT * FROM {path_alias} WHERE path = :path AND alias= :alias AND langcode = :langcode', [':path' => $alias['source'], ':alias' => $alias['alias'], ':langcode' => $alias['langcode']]);
$rows = $result->fetchAll();
$this->assertEqual(count($rows), 1, new FormattableMarkup('Created an entry for %alias.', ['%alias' => $alias['alias']]));
// Cache the pid for further tests.
$aliases[$idx]['pid'] = $rows[0]->pid;
$aliases[$idx]['pid'] = $rows[0]->id;
}
// Load a few aliases
@ -56,7 +68,7 @@ class AliasTest extends PathUnitTestBase {
$this->assertEqual($alias['alias'], $fields['original']['alias']);
$result = $connection->query('SELECT pid FROM {url_alias} WHERE source = :source AND alias= :alias AND langcode = :langcode', [':source' => $alias['source'], ':alias' => $alias['alias'] . '_updated', ':langcode' => $alias['langcode']]);
$result = $connection->query('SELECT id FROM {path_alias} WHERE path = :path AND alias= :alias AND langcode = :langcode', [':path' => $alias['source'], ':alias' => $alias['alias'] . '_updated', ':langcode' => $alias['langcode']]);
$pid = $result->fetchField();
$this->assertEqual($pid, $alias['pid'], new FormattableMarkup('Updated entry for pid %pid.', ['%pid' => $pid]));
@ -67,21 +79,47 @@ class AliasTest extends PathUnitTestBase {
$pid = $alias['pid'];
$aliasStorage->delete(['pid' => $pid]);
$result = $connection->query('SELECT * FROM {url_alias} WHERE pid = :pid', [':pid' => $pid]);
$result = $connection->query('SELECT * FROM {path_alias} WHERE id = :id', [':id' => $pid]);
$rows = $result->fetchAll();
$this->assertEqual(count($rows), 0, new FormattableMarkup('Deleted entry with pid %pid.', ['%pid' => $pid]));
}
}
public function testLookupPath() {
// Prepare database table.
$connection = Database::getConnection();
$this->fixtures->createTables($connection);
/**
* Returns an array of URL aliases for testing.
*
* @return array of URL alias definitions.
*/
protected function sampleUrlAliases() {
return [
[
'source' => '/node/1',
'alias' => '/alias_for_node_1_en',
'langcode' => 'en',
],
[
'source' => '/node/2',
'alias' => '/alias_for_node_2_en',
'langcode' => 'en',
],
[
'source' => '/node/1',
'alias' => '/alias_for_node_1_fr',
'langcode' => 'fr',
],
[
'source' => '/node/1',
'alias' => '/alias_for_node_1_und',
'langcode' => 'und',
],
];
}
public function testLookupPath() {
// Create AliasManager and Path object.
$aliasManager = $this->container->get('path.alias_manager');
$aliasStorage = new AliasStorage($connection, $this->container->get('module_handler'));
$aliasStorage = $this->container->get('path.alias_storage');
// Test the situation where the source is the same for multiple aliases.
// Start with a language-neutral alias, which we will override.
@ -157,14 +195,10 @@ class AliasTest extends PathUnitTestBase {
* Tests the alias whitelist.
*/
public function testWhitelist() {
// Prepare database table.
$connection = Database::getConnection();
$this->fixtures->createTables($connection);
$memoryCounterBackend = new MemoryCounterBackend();
// Create AliasManager and Path object.
$aliasStorage = new AliasStorage($connection, $this->container->get('module_handler'));
$aliasStorage = $this->container->get('path.alias_storage');
$whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $aliasStorage);
$aliasManager = new AliasManager($aliasStorage, $whitelist, $this->container->get('language_manager'), $memoryCounterBackend);
@ -221,14 +255,10 @@ class AliasTest extends PathUnitTestBase {
* Tests situation where the whitelist cache is deleted mid-request.
*/
public function testWhitelistCacheDeletionMidRequest() {
// Prepare database table.
$connection = Database::getConnection();
$this->fixtures->createTables($connection);
$memoryCounterBackend = new MemoryCounterBackend();
// Create AliasManager and Path object.
$aliasStorage = new AliasStorage($connection, $this->container->get('module_handler'));
$aliasStorage = $this->container->get('path.alias_storage');
$whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $aliasStorage);
$aliasManager = new AliasManager($aliasStorage, $whitelist, $this->container->get('language_manager'), $memoryCounterBackend);

View File

@ -1,33 +0,0 @@
<?php
namespace Drupal\KernelTests\Core\Path;
use Drupal\Core\Database\Database;
use Drupal\KernelTests\KernelTestBase;
use Drupal\system\Tests\Path\UrlAliasFixtures;
/**
* Base class for Path/URL alias integration tests.
*/
abstract class PathUnitTestBase extends KernelTestBase {
/**
* @var \Drupal\system\Tests\Path\UrlAliasFixtures
*/
protected $fixtures;
protected function setUp() {
parent::setUp();
$this->fixtures = new UrlAliasFixtures();
// The alias whitelist expects that the menu path roots are set by a
// menu router rebuild.
\Drupal::state()->set('router.path_roots', ['user', 'admin']);
}
protected function tearDown() {
$this->fixtures->dropTables(Database::getConnection());
parent::tearDown();
}
}

View File

@ -19,6 +19,15 @@ class ContentNegotiationRoutingTest extends KernelTestBase {
*/
public static $modules = ['conneg_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('path_alias');
}
/**
* {@inheritdoc}
*/

View File

@ -87,6 +87,7 @@ class RouteProviderTest extends KernelTestBase {
$this->cache = new MemoryBackend();
$this->pathProcessor = \Drupal::service('path_processor_manager');
$this->cacheTagsInvalidator = \Drupal::service('cache_tags.invalidator');
$this->installEntitySchema('path_alias');
}
/**

View File

@ -539,10 +539,10 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
}
if ($container->hasDefinition('path_processor_alias')) {
// Prevent the alias-based path processor, which requires a url_alias db
// table, from being registered to the path processor manager. We do this
// by removing the tags that the compiler pass looks for. This means the
// url generator can safely be used within tests.
// The alias-based processor requires the path_alias entity schema to be
// installed, so we prevent it from being registered to the path processor
// manager. We do this by removing the tags that the compiler pass looks
// for. This means that the URL generator can safely be used within tests.
$container->getDefinition('path_processor_alias')
->clearTag('path_processor_inbound')
->clearTag('path_processor_outbound');