Commit a2cc58de authored by catch's avatar catch

Issue #2846554 by Berdir, tedbow, amateescu, dawehner, himanshu-dixit, Wim...

Issue #2846554 by Berdir, tedbow, amateescu, dawehner, himanshu-dixit, Wim Leers, badrange, damiankloip, catch: Make the PathItem field type actually computed and auto-load stored aliases
parent 4416f7bb
......@@ -39,6 +39,7 @@ class NodeHalJsonAnonTest extends NodeResourceTestBase {
'changed',
'promote',
'sticky',
'path',
'revision_uid',
];
......@@ -54,7 +55,7 @@ protected function getExpectedNormalizedEntity() {
return $normalization + [
'_links' => [
'self' => [
'href' => $this->baseUrl . '/node/1?_format=hal_json',
'href' => $this->baseUrl . '/llama?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/node/camelids',
......
......@@ -40,7 +40,7 @@ protected function getExpectedNormalizedEntity() {
return $normalization + [
'_links' => [
'self' => [
'href' => $this->baseUrl . '/taxonomy/term/1?_format=hal_json',
'href' => $this->baseUrl . '/llama?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids',
......
......@@ -5,6 +5,7 @@
* Enables users to rename URLs.
*/
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
......@@ -76,3 +77,17 @@ function path_entity_base_field_info(EntityTypeInterface $entity_type) {
return $fields;
}
}
/**
* Implements hook_entity_translation_create().
*/
function path_entity_translation_create(ContentEntityInterface $translation) {
foreach ($translation->getFieldDefinitions() as $field_name => $field_definition) {
if ($field_definition->getType() === 'path' && $translation->get($field_name)->pid) {
// If there are values and a path ID, update the langcode and unset the
// path ID to save this as a new alias.
$translation->get($field_name)->langcode = $translation->language()->getId();
$translation->get($field_name)->pid = NULL;
}
}
}
......@@ -34,4 +34,42 @@ public function delete() {
\Drupal::service('path.alias_storage')->delete($conditions);
}
/**
* {@inheritdoc}
*/
public function getValue($include_computed = FALSE) {
$this->ensureLoaded();
return parent::getValue($include_computed);
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$this->ensureLoaded();
return parent::isEmpty();
}
/**
* {@inheritdoc}
*/
public function getIterator() {
$this->ensureLoaded();
return parent::getIterator();
}
/**
* Automatically create the first item for computed fields.
*
* This ensures that ::getValue() and ::isEmpty() calls will behave like a
* non-computed field.
*
* @todo: Move this to the base class in https://www.drupal.org/node/2392845.
*/
protected function ensureLoaded() {
if (!isset($this->list[0]) && $this->definition->isComputed()) {
$this->list[0] = $this->createItem(0);
}
}
}
......@@ -22,6 +22,13 @@
*/
class PathItem extends FieldItemBase {
/**
* Whether the alias has been loaded from the alias storage service yet.
*
* @var bool
*/
protected $isLoaded = FALSE;
/**
* {@inheritdoc}
*/
......@@ -30,9 +37,19 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
->setLabel(t('Path alias'));
$properties['pid'] = DataDefinition::create('integer')
->setLabel(t('Path id'));
$properties['langcode'] = DataDefinition::create('string')
->setLabel(t('Language Code'));
return $properties;
}
/**
* {@inheritdoc}
*/
public function __get($name) {
$this->ensureLoaded();
return parent::__get($name);
}
/**
* {@inheritdoc}
*/
......@@ -40,6 +57,30 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
return [];
}
/**
* {@inheritdoc}
*/
public function getValue() {
$this->ensureLoaded();
return parent::getValue();
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$this->ensureLoaded();
return parent::isEmpty();
}
/**
* {@inheritdoc}
*/
public function getIterator() {
$this->ensureLoaded();
return parent::getIterator();
}
/**
* {@inheritdoc}
*/
......@@ -47,6 +88,28 @@ public function preSave() {
$this->alias = trim($this->alias);
}
/**
* {@inheritdoc}
*/
public function __set($name, $value) {
// Also ensure that existing values are loaded when setting a value, this
// ensures that it is possible to set a new value immediately after loading
// an entity.
$this->ensureLoaded();
parent::__set($name, $value);
}
/**
* {@inheritdoc}
*/
public function set($property_name, $value, $notify = TRUE) {
// Also ensure that existing values are loaded when setting a value, this
// ensures that it is possible to set a new value immediately after loading
// an entity.
$this->ensureLoaded();
return parent::set($property_name, $value, $notify);
}
/**
* {@inheritdoc}
*/
......@@ -88,4 +151,38 @@ public static function mainPropertyName() {
return 'alias';
}
/**
* Ensures the alias properties are loaded if available.
*
* This ensures that the properties will always be loaded and act like
* non-computed fields when calling ::__get() and getValue().
*
* @todo: Determine if this should be moved to the base class in
* https://www.drupal.org/node/2392845.
*/
protected function ensureLoaded() {
if (!$this->isLoaded) {
$entity = $this->getEntity();
if (!$entity->isNew()) {
// @todo Support loading languge neutral aliases in
// https://www.drupal.org/node/2511968.
$alias = \Drupal::service('path.alias_storage')->load([
'source' => '/' . $entity->toUrl()->getInternalPath(),
'langcode' => $this->getLangcode(),
]);
if ($alias) {
$this->setValue($alias);
}
else {
// If there is no existing alias, default the langcode to the current
// language.
// @todo Set the langcode to not specified for untranslatable fields
// in https://www.drupal.org/node/2689459.
$this->langcode = $this->getLangcode();
}
}
$this->isLoaded = TRUE;
}
}
}
......@@ -5,7 +5,6 @@
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
/**
......@@ -26,23 +25,6 @@ class PathWidget extends WidgetBase {
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$entity = $items->getEntity();
$path = [];
if (!$entity->isNew()) {
$conditions = ['source' => '/' . $entity->urlInfo()->getInternalPath()];
if ($items->getLangcode() != LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$conditions['langcode'] = $items->getLangcode();
}
$path = \Drupal::service('path.alias_storage')->load($conditions);
if ($path === FALSE) {
$path = [];
}
}
$path += [
'pid' => NULL,
'source' => !$entity->isNew() ? '/' . $entity->urlInfo()->getInternalPath() : NULL,
'alias' => '',
'langcode' => $items->getLangcode(),
];
$element += [
'#element_validate' => [[get_class($this), 'validateFormElement']],
......@@ -50,22 +32,22 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
$element['alias'] = [
'#type' => 'textfield',
'#title' => $element['#title'],
'#default_value' => $path['alias'],
'#default_value' => $items[$delta]->alias,
'#required' => $element['#required'],
'#maxlength' => 255,
'#description' => $this->t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page.'),
];
$element['pid'] = [
'#type' => 'value',
'#value' => $path['pid'],
'#value' => $items[$delta]->pid,
];
$element['source'] = [
'#type' => 'value',
'#value' => $path['source'],
];
'#value' => !$entity->isNew() ? '/' . $entity->toUrl()->getInternalPath() : NULL,
];
$element['langcode'] = [
'#type' => 'value',
'#value' => $path['langcode'],
'#value' => $items[$delta]->langcode,
];
return $element;
}
......
<?php
namespace Drupal\Tests\path\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
/**
* Tests loading and storing data using PathItem.
*
* @group path
*/
class PathItemTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['path', 'node', 'user', 'system', 'language', 'content_translation'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installSchema('node', ['node_access']);
$node_type = NodeType::create(['type' => 'foo']);
$node_type->save();
$this->installConfig(['language']);
ConfigurableLanguage::createFromLangcode('de')->save();
}
/**
* Test creating, loading, updating and deleting aliases through PathItem.
*/
public function testPathItem() {
/** @var \Drupal\Core\Path\AliasStorageInterface $alias_storage */
$alias_storage = \Drupal::service('path.alias_storage');
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$node = Node::create([
'title' => 'Testing create()',
'type' => 'foo',
'path' => ['alias' => '/foo'],
]);
$this->assertFalse($node->get('path')->isEmpty());
$this->assertEquals('/foo', $node->get('path')->alias);
$node->save();
$this->assertFalse($node->get('path')->isEmpty());
$this->assertEquals('/foo', $node->get('path')->alias);
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foo', $stored_alias);
$node_storage->resetCache();
/** @var \Drupal\node\NodeInterface $loaded_node */
$loaded_node = $node_storage->load($node->id());
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/foo', $loaded_node->get('path')->alias);
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$values = $loaded_node->get('path')->getValue();
$this->assertEquals('/foo', $values[0]['alias']);
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$this->assertEquals('/foo', $loaded_node->path->alias);
// Add a translation, verify it is being saved as expected.
$translation = $loaded_node->addTranslation('de', $loaded_node->toArray());
$translation->get('path')->alias = '/furchtbar';
$translation->save();
// Assert the alias on the English node, the German translation, and the
// stored aliases.
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$this->assertEquals('/foo', $loaded_node->path->alias);
$translation = $loaded_node->getTranslation('de');
$this->assertEquals('/furchtbar', $translation->path->alias);
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foo', $stored_alias);
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $translation->language()->getId());
$this->assertEquals('/furchtbar', $stored_alias);
$loaded_node->get('path')->alias = '/bar';
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$loaded_node->save();
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$loaded_node->get('path')->alias = '/bar';
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$loaded_node->save();
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/bar', $loaded_node->get('path')->alias);
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/bar', $stored_alias);
$old_alias = $alias_storage->lookupPathSource('/foo', $node->language()->getId());
$this->assertFalse($old_alias);
// Reload the node to make sure that it is possible to set a value
// immediately after loading.
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$loaded_node->get('path')->alias = '/foobar';
$loaded_node->save();
$node_storage->resetCache();
$loaded_node = $node_storage->load($node->id());
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/foobar', $loaded_node->get('path')->alias);
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foobar', $stored_alias);
$old_alias = $alias_storage->lookupPathSource('/bar', $node->language()->getId());
$this->assertFalse($old_alias);
$loaded_node->get('path')->alias = '';
$this->assertEquals('', $loaded_node->get('path')->alias);
$loaded_node->save();
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertFalse($stored_alias);
// Check that reading, updating and reading the computed alias again in the
// same request works without clearing any caches in between.
$loaded_node = $node_storage->load($node->id());
$loaded_node->get('path')->alias = '/foo';
$loaded_node->save();
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/foo', $loaded_node->get('path')->alias);
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foo', $stored_alias);
$loaded_node->get('path')->alias = '/foobar';
$loaded_node->save();
$this->assertFalse($loaded_node->get('path')->isEmpty());
$this->assertEquals('/foobar', $loaded_node->get('path')->alias);
$stored_alias = $alias_storage->lookupPathAlias('/' . $node->toUrl()->getInternalPath(), $node->language()->getId());
$this->assertEquals('/foobar', $stored_alias);
}
}
......@@ -15,7 +15,7 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node'];
public static $modules = ['node', 'path'];
/**
* {@inheritdoc}
......@@ -32,6 +32,7 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
'changed',
'promote',
'sticky',
'path',
];
/**
......@@ -51,6 +52,10 @@ protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['access content', 'create camelids content']);
break;
case 'PATCH':
// Do not grant the 'create url aliases' permission to test the case
// when the path field is protected/not accessible, see
// \Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase
// for a positive test.
$this->grantPermissionsToTestedRole(['access content', 'edit any camelids content']);
break;
case 'DELETE':
......@@ -79,6 +84,7 @@ protected function createEntity() {
->setCreatedTime(123456789)
->setChangedTime(123456789)
->setRevisionCreationTime(123456789)
->set('path', '/llama')
->save();
return $node;
......@@ -167,6 +173,13 @@ protected function getExpectedNormalizedEntity() {
],
],
'revision_log' => [],
'path' => [
[
'alias' => '/llama',
'pid' => 1,
'langcode' => 'en',
],
],
];
}
......
......@@ -14,7 +14,7 @@ abstract class TermResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['taxonomy'];
public static $modules = ['taxonomy', 'path'];
/**
* {@inheritdoc}
......@@ -44,8 +44,12 @@ protected function setUpAuthorization($method) {
case 'POST':
case 'PATCH':
case 'DELETE':
// Grant the 'create url aliases' permission to test the case when
// the path field is accessible, see
// \Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase
// for a negative test.
// @todo Update once https://www.drupal.org/node/2824408 lands.
$this->grantPermissionsToTestedRole(['administer taxonomy']);
$this->grantPermissionsToTestedRole(['administer taxonomy', 'create url aliases']);
break;
}
}
......@@ -67,7 +71,8 @@ protected function createEntity() {
// Create a "Llama" taxonomy term.
$term = Term::create(['vid' => $vocabulary->id()])
->setName('Llama')
->setChangedTime(123456789);
->setChangedTime(123456789)
->set('path', '/llama');
$term->save();
return $term;
......@@ -117,6 +122,13 @@ protected function getExpectedNormalizedEntity() {
'value' => TRUE,
],
],
'path' => [
[
'alias' => '/llama',
'pid' => 1,
'langcode' => 'en',
],
],
];
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment