Unverified Commit 8640a9df authored by larowlan's avatar larowlan

Issue #2543726 by damiankloip, Wim Leers, claudiu.cristea, voleger, dagmar,...

Issue #2543726 by damiankloip, Wim Leers, claudiu.cristea, voleger, dagmar, dawehner, amateescu, tedbow, FredCorreia, mgalalm, caseylau, jibran, dbjpanda, pcambra, Grimreaper, idebr, dmouse, larowlan, Berdir, chx, andypost, yched, fgm: Make $term->parent behave like any other entity reference field, to fix REST and Migrate support and de-customize its Views integration
parent f724db34
......@@ -74,7 +74,7 @@ function forum_views_data() {
'filter' => [
'title' => t('Has taxonomy term'),
'id' => 'taxonomy_index_tid',
'hierarchy table' => 'taxonomy_term_hierarchy',
'hierarchy table' => 'taxonomy_term__parent',
'numeric' => TRUE,
'skip base' => 'taxonomy_term_data',
'allow empty' => TRUE,
......
......@@ -433,7 +433,7 @@ public function createForum($type, $parent = 0) {
// Verify forum hierarchy.
$tid = $term['tid'];
$parent_tid = db_query("SELECT t.parent FROM {taxonomy_term_hierarchy} t WHERE t.tid = :tid", [':tid' => $tid])->fetchField();
$parent_tid = db_query("SELECT t.parent_target_id FROM {taxonomy_term__parent} t WHERE t.entity_id = :tid", [':tid' => $tid])->fetchField();
$this->assertTrue($parent == $parent_tid, 'The ' . $type . ' is linked to its container');
$forum = $this->container->get('entity.manager')->getStorage('taxonomy_term')->load($tid);
......
services:
serializer.normalizer.entity_reference_item.hal:
class: Drupal\hal\Normalizer\EntityReferenceItemNormalizer
arguments: ['@hal.link_manager', '@serializer.entity_resolver']
arguments: ['@hal.link_manager', '@serializer.entity_resolver', '@entity_type.manager']
tags:
- { name: normalizer, priority: 10 }
serializer.normalizer.field_item.hal:
......
......@@ -2,16 +2,22 @@
namespace Drupal\hal\Normalizer;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\hal\LinkManager\LinkManagerInterface;
use Drupal\serialization\EntityResolver\EntityResolverInterface;
use Drupal\serialization\EntityResolver\UuidReferenceInterface;
use Drupal\serialization\Normalizer\EntityReferenceFieldItemNormalizerTrait;
/**
* Converts the Drupal entity reference item object to HAL array structure.
*/
class EntityReferenceItemNormalizer extends FieldItemNormalizer implements UuidReferenceInterface {
use EntityReferenceFieldItemNormalizerTrait;
/**
* The interface or class that this Normalizer supports.
*
......@@ -33,6 +39,13 @@ class EntityReferenceItemNormalizer extends FieldItemNormalizer implements UuidR
*/
protected $entityResolver;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs an EntityReferenceItemNormalizer object.
*
......@@ -40,25 +53,28 @@ class EntityReferenceItemNormalizer extends FieldItemNormalizer implements UuidR
* The hypermedia link manager.
* @param \Drupal\serialization\EntityResolver\EntityResolverInterface $entity_Resolver
* The entity resolver.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface|null $entity_type_manager
* The entity type manager.
*/
public function __construct(LinkManagerInterface $link_manager, EntityResolverInterface $entity_Resolver) {
public function __construct(LinkManagerInterface $link_manager, EntityResolverInterface $entity_Resolver, EntityTypeManagerInterface $entity_type_manager = NULL) {
$this->linkManager = $link_manager;
$this->entityResolver = $entity_Resolver;
$this->entityTypeManager = $entity_type_manager ?: \Drupal::service('entity_type.manager');
}
/**
* {@inheritdoc}
*/
public function normalize($field_item, $format = NULL, array $context = []) {
/** @var $field_item \Drupal\Core\Field\FieldItemInterface */
$target_entity = $field_item->get('entity')->getValue();
// If this is not a content entity, let the parent implementation handle it,
// only content entities are supported as embedded resources.
if (!($target_entity instanceof FieldableEntityInterface)) {
// If this is not a fieldable entity, let the parent implementation handle
// it, only fieldable entities are supported as embedded resources.
if (!$this->targetEntityIsFieldable($field_item)) {
return parent::normalize($field_item, $format, $context);
}
/** @var $field_item \Drupal\Core\Field\FieldItemInterface */
$target_entity = $field_item->get('entity')->getValue();
// If the parent entity passed in a langcode, unset it before normalizing
// the target entity. Otherwise, untranslatable fields of the target entity
// will include the langcode.
......@@ -91,6 +107,39 @@ public function normalize($field_item, $format = NULL, array $context = []) {
];
}
/**
* Checks whether the referenced entity is of a fieldable entity type.
*
* @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item
* The reference field item whose target entity needs to be checked.
*
* @return bool
* TRUE when the referenced entity is of a fieldable entity type.
*/
protected function targetEntityIsFieldable(EntityReferenceItem $item) {
$target_entity = $item->get('entity')->getValue();
if ($target_entity !== NULL) {
return $target_entity instanceof FieldableEntityInterface;
}
$referencing_entity = $item->getEntity();
$target_entity_type_id = $item->getFieldDefinition()->getSetting('target_type');
// If the entity type is the same as the parent, we can check that. This is
// just a shortcut to avoid getting the entity type defintition and checking
// the class.
if ($target_entity_type_id === $referencing_entity->getEntityTypeId()) {
return $referencing_entity instanceof FieldableEntityInterface;
}
// Otherwise, we need to get the class for the type.
$target_entity_type = $this->entityTypeManager->getDefinition($target_entity_type_id);
$target_entity_type_class = $target_entity_type->getClass();
return is_a($target_entity_type_class, FieldableEntityInterface::class, TRUE);
}
/**
* {@inheritdoc}
*/
......@@ -105,6 +154,22 @@ protected function constructValue($data, $context) {
return NULL;
}
/**
* {@inheritdoc}
*/
protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
// Normalize root reference values here so we don't need to deal with hal's
// nested data structure for field items. This will be called from
// \Drupal\hal\Normalizer\FieldItemNormalizer::normalize. Which will only
// be called from this class for entities that are not fieldable.
$normalized = parent::normalizedFieldValues($field_item, $format, $context);
$this->normalizeRootReferenceValue($normalized, $field_item);
return $normalized;
}
/**
* {@inheritdoc}
*/
......
......@@ -25,11 +25,11 @@ class CommentHalJsonAnonTest extends CommentHalJsonTestBase {
* @see ::setUpAuthorization
*/
protected static $patchProtectedFieldNames = [
'entity_id',
'changed',
'thread',
'entity_type',
'field_name',
'entity_id',
];
}
......@@ -26,6 +26,25 @@
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*
* The HAL+JSON format causes different PATCH-protected fields. For some
* reason, the 'pid' and 'homepage' fields are NOT PATCH-protected, even
* though they are for non-HAL+JSON serializations.
*
* @todo fix in https://www.drupal.org/node/2824271
*/
protected static $patchProtectedFieldNames = [
'status',
'created',
'changed',
'thread',
'entity_type',
'field_name',
'entity_id',
'uid',
];
/**
* {@inheritdoc}
......
......@@ -30,6 +30,19 @@ class NodeHalJsonAnonTest extends NodeResourceTestBase {
*/
protected static $mimeType = 'application/hal+json';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
'revision_timestamp',
'created',
'changed',
'promote',
'sticky',
'path',
'revision_uid',
];
/**
* {@inheritdoc}
*/
......
......@@ -2,6 +2,7 @@
namespace Drupal\Tests\hal\Functional\EntityResource\Term;
use Drupal\taxonomy\Entity\Term;
use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase;
......@@ -37,6 +38,114 @@ protected function getExpectedNormalizedEntity() {
$normalization = $this->applyHalFieldNormalization($default_normalization);
// We test with multiple parent terms, and combinations thereof.
// @see ::createEntity()
// @see ::testGet()
// @see ::testGetTermWithParent()
// @see ::providerTestGetTermWithParent()
// @see ::testGetTermWithParent()
$parent_term_ids = [];
for ($i = 0; $i < $this->entity->get('parent')->count(); $i++) {
$parent_term_ids[$i] = (int) $this->entity->get('parent')[$i]->target_id;
}
$expected_parent_normalization_links = FALSE;
$expected_parent_normalization_embedded = FALSE;
switch ($parent_term_ids) {
case [0]:
$expected_parent_normalization_links = [
NULL,
];
$expected_parent_normalization_embedded = [
NULL,
];
break;
case [2]:
$expected_parent_normalization_links = [
[
'href' => $this->baseUrl . '/taxonomy/term/2?_format=hal_json',
],
];
$expected_parent_normalization_embedded = [
[
'_links' => [
'self' => [
'href' => $this->baseUrl . '/taxonomy/term/2?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids',
],
],
'uuid' => [
['value' => Term::load(2)->uuid()],
],
],
];
break;
case [0, 2]:
$expected_parent_normalization_links = [
NULL,
[
'href' => $this->baseUrl . '/taxonomy/term/2?_format=hal_json',
],
];
$expected_parent_normalization_embedded = [
NULL,
[
'_links' => [
'self' => [
'href' => $this->baseUrl . '/taxonomy/term/2?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids',
],
],
'uuid' => [
['value' => Term::load(2)->uuid()],
],
],
];
break;
case [3, 2]:
$expected_parent_normalization_links = [
[
'href' => $this->baseUrl . '/taxonomy/term/3?_format=hal_json',
],
[
'href' => $this->baseUrl . '/taxonomy/term/2?_format=hal_json',
],
];
$expected_parent_normalization_embedded = [
[
'_links' => [
'self' => [
'href' => $this->baseUrl . '/taxonomy/term/3?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids',
],
],
'uuid' => [
['value' => Term::load(3)->uuid()],
],
],
[
'_links' => [
'self' => [
'href' => $this->baseUrl . '/taxonomy/term/2?_format=hal_json',
],
'type' => [
'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids',
],
],
'uuid' => [
['value' => Term::load(2)->uuid()],
],
],
];
break;
}
return $normalization + [
'_links' => [
'self' => [
......@@ -45,6 +154,10 @@ protected function getExpectedNormalizedEntity() {
'type' => [
'href' => $this->baseUrl . '/rest/type/taxonomy_term/camelids',
],
$this->baseUrl . '/rest/relation/taxonomy_term/camelids/parent' => $expected_parent_normalization_links,
],
'_embedded' => [
$this->baseUrl . '/rest/relation/taxonomy_term/camelids/parent' => $expected_parent_normalization_embedded,
],
];
}
......
......@@ -92,6 +92,66 @@ protected function createEntity() {
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
// We test with multiple parent terms, and combinations thereof.
// @see ::createEntity()
// @see ::testGet()
// @see ::testGetTermWithParent()
// @see ::providerTestGetTermWithParent()
$parent_term_ids = [];
for ($i = 0; $i < $this->entity->get('parent')->count(); $i++) {
$parent_term_ids[$i] = (int) $this->entity->get('parent')[$i]->target_id;
}
$expected_parent_normalization = FALSE;
switch ($parent_term_ids) {
case [0]:
$expected_parent_normalization = [
[
'target_id' => NULL,
],
];
break;
case [2]:
$expected_parent_normalization = [
[
'target_id' => 2,
'target_type' => 'taxonomy_term',
'target_uuid' => Term::load(2)->uuid(),
'url' => base_path() . 'taxonomy/term/2',
],
];
break;
case [0, 2]:
$expected_parent_normalization = [
[
'target_id' => NULL,
],
[
'target_id' => 2,
'target_type' => 'taxonomy_term',
'target_uuid' => Term::load(2)->uuid(),
'url' => base_path() . 'taxonomy/term/2',
],
];
break;
case [3, 2]:
$expected_parent_normalization = [
[
'target_id' => 3,
'target_type' => 'taxonomy_term',
'target_uuid' => Term::load(3)->uuid(),
'url' => base_path() . 'taxonomy/term/3',
],
[
'target_id' => 2,
'target_type' => 'taxonomy_term',
'target_uuid' => Term::load(2)->uuid(),
'url' => base_path() . 'taxonomy/term/2',
],
];
break;
}
return [
'tid' => [
['value' => 1],
......@@ -116,7 +176,7 @@ protected function getExpectedNormalizedEntity() {
'processed' => "<p>It is a little known fact that llamas cannot count higher than seven.</p>\n",
],
],
'parent' => [],
'parent' => $expected_parent_normalization,
'weight' => [
['value' => 0],
],
......@@ -238,4 +298,54 @@ protected function getExpectedCacheContexts() {
return Cache::mergeContexts(['url.site'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
}
/**
* Tests GETting a term with a parent term other than the default <root> (0).
*
* @see ::getExpectedNormalizedEntity()
*
* @dataProvider providerTestGetTermWithParent
*/
public function testGetTermWithParent(array $parent_term_ids) {
// Create all possible parent terms.
Term::create(['vid' => Vocabulary::load('camelids')->id()])
->setName('Lamoids')
->save();
Term::create(['vid' => Vocabulary::load('camelids')->id()])
->setName('Wimoids')
->save();
// Modify the entity under test to use the provided parent terms.
$this->entity->set('parent', $parent_term_ids)->save();
$this->initAuthentication();
$url = $this->getEntityResourceUrl();
$url->setOption('query', ['_format' => static::$format]);
$request_options = $this->getAuthenticationRequestOptions('GET');
$this->provisionEntityResource();
$this->setUpAuthorization('GET');
$response = $this->request('GET', $url, $request_options);
$expected = $this->getExpectedNormalizedEntity();
static::recursiveKSort($expected);
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
static::recursiveKSort($actual);
$this->assertSame($expected, $actual);
}
public function providerTestGetTermWithParent() {
return [
'root parent: [0] (= no parent)' => [
[0]
],
'non-root parent: [2]' => [
[2]
],
'multiple parents: [0,2] (root + non-root parent)' => [
[0, 2]
],
'multiple parents: [3,2] (both non-root parents)' => [
[3, 2]
],
];
}
}
......@@ -99,7 +99,7 @@ protected function applyXmlFieldDecodingQuirks(array $normalization) {
}
}
if (!empty($normalization[$field_name])) {
if (count($normalization[$field_name]) === 1) {
$normalization[$field_name] = $normalization[$field_name][0];
}
}
......
......@@ -12,6 +12,8 @@
*/
class EntityReferenceFieldItemNormalizer extends FieldItemNormalizer {
use EntityReferenceFieldItemNormalizerTrait;
/**
* The interface or class that this Normalizer supports.
*
......@@ -42,6 +44,8 @@ public function __construct(EntityRepositoryInterface $entity_repository) {
public function normalize($field_item, $format = NULL, array $context = []) {
$values = parent::normalize($field_item, $format, $context);
$this->normalizeRootReferenceValue($values, $field_item);
/** @var \Drupal\Core\Entity\EntityInterface $entity */
if ($entity = $field_item->get('entity')->getValue()) {
$values['target_type'] = $entity->getEntityTypeId();
......@@ -55,6 +59,7 @@ public function normalize($field_item, $format = NULL, array $context = []) {
$values['url'] = $url;
}
}
return $values;
}
......
<?php
namespace Drupal\serialization\Normalizer;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
/**
* Converts empty reference values for entity reference items.
*/
trait EntityReferenceFieldItemNormalizerTrait {
protected function normalizeRootReferenceValue(&$values, EntityReferenceItem $field_item) {
// @todo Generalize for all tree-structured entity types.
if ($this->fieldItemReferencesTaxonomyTerm($field_item) && empty($values['target_id'])) {
$values['target_id'] = NULL;
}
}
/**
* Determines if a field item references a taxonomy term.
*
* @param \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $field_item
*
* @return bool
*/
protected function fieldItemReferencesTaxonomyTerm(EntityReferenceItem $field_item) {
return $field_item->getFieldDefinition()->getSetting('target_type') === 'taxonomy_term';
}
}
......@@ -120,6 +120,13 @@ public function testNormalize() {
->willReturn($entity->reveal())
->shouldBeCalled();
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$field_definition->getSetting('target_type')
->willReturn('test_type');
$this->fieldItem->getFieldDefinition()
->willReturn($field_definition->reveal());
$this->fieldItem->get('entity')
->willReturn($entity_reference)
->shouldBeCalled();
......@@ -139,6 +146,46 @@ public function testNormalize() {
$this->assertSame($expected, $normalized);
}
/**
* @covers ::normalize
*/
public function testNormalizeWithEmptyTaxonomyTermReference() {
// Override the serializer prophecy from setUp() to return a zero value.
$this->serializer = $this->prophesize(Serializer::class);
// Set up the serializer to return an entity property.
$this->serializer->normalize(Argument::cetera())
->willReturn(0);
$this->normalizer->setSerializer($this->serializer->reveal());
$entity_reference = $this->prophesize(TypedDataInterface::class);
$entity_reference->getValue()
->willReturn(NULL)
->shouldBeCalled();
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$field_definition->getSetting('target_type')
->willReturn('taxonomy_term');
$this->fieldItem->getFieldDefinition()
->willReturn($field_definition->reveal());
$this->fieldItem->get('entity')
->willReturn($entity_reference)
->shouldBeCalled();
$this->fieldItem->getProperties(TRUE)
->willReturn(['target_id' => $this->getTypedDataProperty(FALSE)])
->shouldBeCalled();
$normalized = $this->normalizer->normalize($this->fieldItem->reveal());
$expected = [
'target_id' => NULL,
];
$this->assertSame($expected, $normalized);
}
/**
* @covers ::normalize
*/
......@@ -148,6 +195,13 @@ public function testNormalizeWithNoEntity() {
->willReturn(NULL)
->shouldBeCalled();
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$field_definition->getSetting('target_type')
->willReturn('test_type');
$this->fieldItem->getFieldDefinition()
->willReturn($field_definition->reveal());
$this->fieldItem->get('entity')
->willReturn($entity_reference->reveal())
->shouldBeCalled();
......
<?php
/**
* @file
* Contains database additions to drupal-8.bare.standard.php.gz for testing the
* upgrade path of https://www.drupal.org/node/2455125.
*/
use Drupal\Component\Uuid\Php;
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
$view_file = __DIR__ . '/drupal-8.views-taxonomy-parent-2543726.yml';
$view_config = Yaml::decode(file_get_contents($view_file));
$connection->insert('config')
->fields(['collection', 'name', 'data'])
->values([
'collection' => '',
'name' => "views.view.test_taxonomy_parent",
'data' => serialize($view_config),
])
->execute();
$uuid = new Php();
// The root tid.
$tids = [0];
for ($i = 0; $i < 4; $i++) {
$name = $this->randomString();
$tid = $connection->insert('taxonomy_term_data')
->fields(['vid', 'uuid', 'langcode'])
->values(['vid' => 'tags', 'uuid' => $uuid->generate(), 'langcode' => 'en'])
->execute();
$connection->insert('taxonomy_term_field_data')
->fields(['tid', 'vid', 'langcode', 'name', 'weight', 'changed', 'default_langcode'])
->values(['tid' => $tid, 'vid' => 'tags', 'langcode' => 'en', 'name' => $name, 'weight' => 0, 'changed' => REQUEST_TIME, 'default_langcode' => 1])
->execute();