diff --git a/core/modules/forum/forum.views.inc b/core/modules/forum/forum.views.inc
index 5f67af5..1565962 100644
--- a/core/modules/forum/forum.views.inc
+++ b/core/modules/forum/forum.views.inc
@@ -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,
diff --git a/core/modules/forum/tests/src/Functional/ForumTest.php b/core/modules/forum/tests/src/Functional/ForumTest.php
index 9ba4be7..2bc6a45 100644
--- a/core/modules/forum/tests/src/Functional/ForumTest.php
+++ b/core/modules/forum/tests/src/Functional/ForumTest.php
@@ -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);
diff --git a/core/modules/hal/hal.services.yml b/core/modules/hal/hal.services.yml
index a877163..1330180 100644
--- a/core/modules/hal/hal.services.yml
+++ b/core/modules/hal/hal.services.yml
@@ -1,7 +1,7 @@
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:
diff --git a/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
index fbcae8c..1a4ef54 100644
--- a/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/EntityReferenceItemNormalizer.php
@@ -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.
*
@@ -34,31 +40,41 @@ class EntityReferenceItemNormalizer extends FieldItemNormalizer implements UuidR
protected $entityResolver;
/**
+ * The entity type manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
* Constructs an EntityReferenceItemNormalizer object.
*
* @param \Drupal\hal\LinkManager\LinkManagerInterface $link_manager
* 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.
@@ -92,6 +108,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}
*/
protected function constructValue($data, $context) {
@@ -108,6 +157,22 @@ protected function constructValue($data, $context) {
/**
* {@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}
+ */
public function getUuid($data) {
if (isset($data['uuid'])) {
$uuid = $data['uuid'];
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php
index 9b0cee2..3edd9b1 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php
@@ -25,11 +25,11 @@ class CommentHalJsonAnonTest extends CommentHalJsonTestBase {
* @see ::setUpAuthorization
*/
protected static $patchProtectedFieldNames = [
- 'entity_id',
'changed',
'thread',
'entity_type',
'field_name',
+ 'entity_id',
];
}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php
index 3deb0ea..1939e04 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php
@@ -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}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
index 2de6539..e218a73 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
@@ -33,6 +33,19 @@ class NodeHalJsonAnonTest extends NodeResourceTestBase {
/**
* {@inheritdoc}
*/
+ protected static $patchProtectedFieldNames = [
+ 'revision_timestamp',
+ 'created',
+ 'changed',
+ 'promote',
+ 'sticky',
+ 'path',
+ 'revision_uid',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
protected function getExpectedNormalizedEntity() {
$default_normalization = parent::getExpectedNormalizedEntity();
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php
index 73a1549..e19fb1b 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Term/TermHalJsonAnonTest.php
@@ -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,
],
];
}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
index 85a4b1a..ce60904 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
@@ -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' => "
It is a little known fact that llamas cannot count higher than seven.
\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 (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]
+ ],
+ ];
+ }
+
}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php b/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php
index 962d8e9..b69394b 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/XmlEntityNormalizationQuirksTrait.php
@@ -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];
}
}
diff --git a/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizer.php
index ea2e020..2d3c6f0 100644
--- a/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizer.php
@@ -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;
}
diff --git a/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizerTrait.php b/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizerTrait.php
new file mode 100644
index 0000000..1acfd70
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/EntityReferenceFieldItemNormalizerTrait.php
@@ -0,0 +1,30 @@
+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';
+ }
+
+}
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
index 5cc6467..9320d17 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
@@ -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();
@@ -142,12 +149,59 @@ public function testNormalize() {
/**
* @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
+ */
public function testNormalizeWithNoEntity() {
$entity_reference = $this->prophesize(TypedDataInterface::class);
$entity_reference->getValue()
->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();
diff --git a/core/modules/system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.php b/core/modules/system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.php
new file mode 100644
index 0000000..70e0ad4
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.php
@@ -0,0 +1,63 @@
+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();
+
+ $tids[] = $tid;
+}
+
+$hierarchy = [
+ // Term with tid 1 has terms with tids 2 and 3 as parents.
+ 1 => [2, 3],
+ 2 => [3, 0],
+ 3 => [0],
+];
+
+$query = $connection->insert('taxonomy_term_hierarchy')->fields(['tid', 'parent']);
+
+foreach ($hierarchy as $tid => $parents) {
+ foreach ($parents as $parent) {
+ $query->values(['tid' => $tids[$tid], 'parent' => $tids[$parent]]);
+ }
+}
+
+$query->execute();
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_parent.yml b/core/modules/system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.yml
similarity index 97%
copy from core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_parent.yml
copy to core/modules/system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.yml
index 256f618..65df6a8 100644
--- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_parent.yml
+++ b/core/modules/system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.yml
@@ -124,8 +124,8 @@ display:
relationships:
parent:
id: parent
- table: taxonomy_term_hierarchy
- field: parent
+ table: taxonomy_term__parent
+ field: parent_target_id
relationship: none
group_type: group
admin_label: Parent
diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php
index b8eac0a..3a14073 100644
--- a/core/modules/taxonomy/src/Entity/Term.php
+++ b/core/modules/taxonomy/src/Entity/Term.php
@@ -64,22 +64,28 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti
// See if any of the term's children are about to be become orphans.
$orphans = [];
- foreach (array_keys($entities) as $tid) {
- if ($children = $storage->loadChildren($tid)) {
+ /** @var \Drupal\taxonomy\TermInterface $term */
+ foreach ($entities as $tid => $term) {
+ if ($children = $storage->getChildren($term)) {
+ /** @var \Drupal\taxonomy\TermInterface $child */
foreach ($children as $child) {
+ $parent = $child->get('parent');
+ // Update child parents item list.
+ $parent->filter(function ($item) use ($tid) {
+ return $item->target_id != $tid;
+ });
+
// If the term has multiple parents, we don't delete it.
- $parents = $storage->loadParents($child->id());
- if (empty($parents)) {
+ if ($parent->count()) {
+ $child->save();
+ }
+ else {
$orphans[] = $child;
}
}
}
}
- // Delete term hierarchy information after looking up orphans but before
- // deleting them so that their children/parent information is consistent.
- $storage->deleteTermHierarchy(array_keys($entities));
-
if (!empty($orphans)) {
$storage->delete($orphans);
}
@@ -88,14 +94,11 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti
/**
* {@inheritdoc}
*/
- public function postSave(EntityStorageInterface $storage, $update = TRUE) {
- parent::postSave($storage, $update);
-
- // Only change the parents if a value is set, keep the existing values if
- // not.
- if (isset($this->parent->target_id)) {
- $storage->deleteTermHierarchy([$this->id()]);
- $storage->updateTermHierarchy($this);
+ public function preSave(EntityStorageInterface $storage) {
+ parent::preSave($storage);
+ // Terms with no parents are mandatory children of .
+ if (!$this->get('parent')->count()) {
+ $this->parent->target_id = 0;
}
}
@@ -156,8 +159,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
->setLabel(t('Term Parents'))
->setDescription(t('The parents of this term.'))
->setSetting('target_type', 'taxonomy_term')
- ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
- ->setCustomStorage(TRUE);
+ ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
diff --git a/core/modules/taxonomy/src/Plugin/views/argument/IndexTidDepth.php b/core/modules/taxonomy/src/Plugin/views/argument/IndexTidDepth.php
index 02b5995..8eeb1f9 100644
--- a/core/modules/taxonomy/src/Plugin/views/argument/IndexTidDepth.php
+++ b/core/modules/taxonomy/src/Plugin/views/argument/IndexTidDepth.php
@@ -111,18 +111,19 @@ public function query($group_by = FALSE) {
$last = "tn";
if ($this->options['depth'] > 0) {
- $subquery->leftJoin('taxonomy_term_hierarchy', 'th', "th.tid = tn.tid");
+ $subquery->leftJoin('taxonomy_term__parent', 'th', "th.entity_id = tn.tid");
$last = "th";
foreach (range(1, abs($this->options['depth'])) as $count) {
- $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.parent = th$count.tid");
- $where->condition("th$count.tid", $tids, $operator);
+ $subquery->leftJoin('taxonomy_term__parent', "th$count", "$last.parent_target_id = th$count.entity_id");
+ $where->condition("th$count.entity_id", $tids, $operator);
$last = "th$count";
}
}
elseif ($this->options['depth'] < 0) {
foreach (range(1, abs($this->options['depth'])) as $count) {
- $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.tid = th$count.parent");
- $where->condition("th$count.tid", $tids, $operator);
+ $field = $count == 1 ? 'tid' : 'entity_id';
+ $subquery->leftJoin('taxonomy_term__parent', "th$count", "$last.$field = th$count.parent_target_id");
+ $where->condition("th$count.entity_id", $tids, $operator);
$last = "th$count";
}
}
diff --git a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTidDepth.php b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTidDepth.php
index 802786e..ebb5b51 100644
--- a/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTidDepth.php
+++ b/core/modules/taxonomy/src/Plugin/views/filter/TaxonomyIndexTidDepth.php
@@ -77,18 +77,19 @@ public function query() {
$last = "tn";
if ($this->options['depth'] > 0) {
- $subquery->leftJoin('taxonomy_term_hierarchy', 'th', "th.tid = tn.tid");
+ $subquery->leftJoin('taxonomy_term__parent', 'th', "th.entity_id = tn.tid");
$last = "th";
foreach (range(1, abs($this->options['depth'])) as $count) {
- $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.parent = th$count.tid");
- $where->condition("th$count.tid", $this->value, $operator);
+ $subquery->leftJoin('taxonomy_term__parent', "th$count", "$last.parent_target_id = th$count.entity_id");
+ $where->condition("th$count.entity_id", $this->value, $operator);
$last = "th$count";
}
}
elseif ($this->options['depth'] < 0) {
foreach (range(1, abs($this->options['depth'])) as $count) {
- $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.tid = th$count.parent");
- $where->condition("th$count.tid", $this->value, $operator);
+ $field = $count == 1 ? 'tid' : 'entity_id';
+ $subquery->leftJoin('taxonomy_term__parent', "th$count", "$last.$field = th$count.parent_target_id");
+ $where->condition("th$count.entity_id", $this->value, $operator);
$last = "th$count";
}
}
diff --git a/core/modules/taxonomy/src/TermStorage.php b/core/modules/taxonomy/src/TermStorage.php
index 0a00c13..f3e8ecb 100644
--- a/core/modules/taxonomy/src/TermStorage.php
+++ b/core/modules/taxonomy/src/TermStorage.php
@@ -2,8 +2,8 @@
namespace Drupal\taxonomy;
-use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* Defines a Controller class for taxonomy terms.
@@ -11,27 +11,6 @@
class TermStorage extends SqlContentEntityStorage implements TermStorageInterface {
/**
- * Array of loaded parents keyed by child term ID.
- *
- * @var array
- */
- protected $parents = [];
-
- /**
- * Array of all loaded term ancestry keyed by ancestor term ID.
- *
- * @var array
- */
- protected $parentsAll = [];
-
- /**
- * Array of child terms keyed by parent term ID.
- *
- * @var array
- */
- protected $children = [];
-
- /**
* Array of term parents keyed by vocabulary ID and child term ID.
*
* @var array
@@ -60,6 +39,14 @@ class TermStorage extends SqlContentEntityStorage implements TermStorageInterfac
protected $trees = [];
/**
+ * Array of all loaded term ancestry keyed by ancestor term ID, keyed by term
+ * ID.
+ *
+ * @var \Drupal\taxonomy\TermInterface[][]
+ */
+ protected $ancestors;
+
+ /**
* {@inheritdoc}
*
* @param array $values
@@ -80,9 +67,7 @@ public function create(array $values = []) {
*/
public function resetCache(array $ids = NULL) {
drupal_static_reset('taxonomy_term_count_nodes');
- $this->parents = [];
- $this->parentsAll = [];
- $this->children = [];
+ $this->ancestors = [];
$this->treeChildren = [];
$this->treeParents = [];
$this->treeTerms = [];
@@ -93,100 +78,125 @@ public function resetCache(array $ids = NULL) {
/**
* {@inheritdoc}
*/
- public function deleteTermHierarchy($tids) {
- $this->database->delete('taxonomy_term_hierarchy')
- ->condition('tid', $tids, 'IN')
- ->execute();
- }
+ public function deleteTermHierarchy($tids) {}
/**
* {@inheritdoc}
*/
- public function updateTermHierarchy(EntityInterface $term) {
- $query = $this->database->insert('taxonomy_term_hierarchy')
- ->fields(['tid', 'parent']);
-
- foreach ($term->parent as $parent) {
- $query->values([
- 'tid' => $term->id(),
- 'parent' => (int) $parent->target_id,
- ]);
- }
- $query->execute();
- }
+ public function updateTermHierarchy(EntityInterface $term) {}
/**
* {@inheritdoc}
*/
public function loadParents($tid) {
- if (!isset($this->parents[$tid])) {
- $parents = [];
- $query = $this->database->select('taxonomy_term_field_data', 't');
- $query->join('taxonomy_term_hierarchy', 'h', 'h.parent = t.tid');
- $query->addField('t', 'tid');
- $query->condition('h.tid', $tid);
- $query->condition('t.default_langcode', 1);
- $query->addTag('taxonomy_term_access');
- $query->orderBy('t.weight');
- $query->orderBy('t.name');
- if ($ids = $query->execute()->fetchCol()) {
- $parents = $this->loadMultiple($ids);
+ $terms = [];
+ /** @var \Drupal\taxonomy\TermInterface $term */
+ if ($tid && $term = $this->load($tid)) {
+ foreach ($this->getParents($term) as $id => $parent) {
+ // This method currently doesn't return the parent.
+ // @see https://www.drupal.org/node/2019905
+ if (!empty($id)) {
+ $terms[$id] = $parent;
+ }
+ }
+ }
+
+ return $terms;
+ }
+
+ /**
+ * Returns a list of parents of this term.
+ *
+ * @return \Drupal\taxonomy\TermInterface[]
+ * The parent taxonomy term entities keyed by term ID. If this term has a
+ * parent, that item is keyed with 0 and will have NULL as value.
+ *
+ * @internal
+ * @todo Refactor away when TreeInterface is introduced.
+ */
+ protected function getParents(TermInterface $term) {
+ $parents = $ids = [];
+ // Cannot use $this->get('parent')->referencedEntities() here because that
+ // strips out the '0' reference.
+ foreach ($term->get('parent') as $item) {
+ if ($item->target_id == 0) {
+ // The parent.
+ $parents[0] = NULL;
+ continue;
}
- $this->parents[$tid] = $parents;
+ $ids[] = $item->target_id;
}
- return $this->parents[$tid];
+
+ // @todo Better way to do this? AND handle the NULL/0 parent?
+ // Querying the terms again so that the same access checks are run when
+ // getParents() is called as in Drupal version prior to 8.3.
+ $loaded_parents = [];
+
+ if ($ids) {
+ $query = \Drupal::entityQuery('taxonomy_term')
+ ->condition('tid', $ids, 'IN');
+
+ $loaded_parents = static::loadMultiple($query->execute());
+ }
+
+ return $parents + $loaded_parents;
}
/**
* {@inheritdoc}
*/
public function loadAllParents($tid) {
- if (!isset($this->parentsAll[$tid])) {
- $parents = [];
- if ($term = $this->load($tid)) {
- $parents[$term->id()] = $term;
- $terms_to_search[] = $term->id();
-
- while ($tid = array_shift($terms_to_search)) {
- if ($new_parents = $this->loadParents($tid)) {
- foreach ($new_parents as $new_parent) {
- if (!isset($parents[$new_parent->id()])) {
- $parents[$new_parent->id()] = $new_parent;
- $terms_to_search[] = $new_parent->id();
- }
- }
+ /** @var \Drupal\taxonomy\TermInterface $term */
+ return (!empty($tid) && $term = $this->load($tid)) ? $this->getAncestors($term) : [];
+ }
+
+ /**
+ * Returns all ancestors of this term.
+ *
+ * @return \Drupal\taxonomy\TermInterface[]
+ * A list of ancestor taxonomy term entities keyed by term ID.
+ *
+ * @internal
+ * @todo Refactor away when TreeInterface is introduced.
+ */
+ protected function getAncestors(TermInterface $term) {
+ if (!isset($this->ancestors[$term->id()])) {
+ $this->ancestors[$term->id()] = [$term->id() => $term];
+ $search[] = $term->id();
+
+ while ($tid = array_shift($search)) {
+ foreach ($this->getParents(static::load($tid)) as $id => $parent) {
+ if ($parent && !isset($this->ancestors[$term->id()][$id])) {
+ $this->ancestors[$term->id()][$id] = $parent;
+ $search[] = $id;
}
}
}
-
- $this->parentsAll[$tid] = $parents;
}
- return $this->parentsAll[$tid];
+ return $this->ancestors[$term->id()];
}
/**
* {@inheritdoc}
*/
public function loadChildren($tid, $vid = NULL) {
- if (!isset($this->children[$tid])) {
- $children = [];
- $query = $this->database->select('taxonomy_term_field_data', 't');
- $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
- $query->addField('t', 'tid');
- $query->condition('h.parent', $tid);
- if ($vid) {
- $query->condition('t.vid', $vid);
- }
- $query->condition('t.default_langcode', 1);
- $query->addTag('taxonomy_term_access');
- $query->orderBy('t.weight');
- $query->orderBy('t.name');
- if ($ids = $query->execute()->fetchCol()) {
- $children = $this->loadMultiple($ids);
- }
- $this->children[$tid] = $children;
- }
- return $this->children[$tid];
+ /** @var \Drupal\taxonomy\TermInterface $term */
+ return (!empty($tid) && $term = $this->load($tid)) ? $this->getChildren($term) : [];
+ }
+
+ /**
+ * Returns all children terms of this term.
+ *
+ * @return \Drupal\taxonomy\TermInterface[]
+ * A list of children taxonomy term entities keyed by term ID.
+ *
+ * @internal
+ * @todo Refactor away when TreeInterface is introduced.
+ */
+ public function getChildren(TermInterface $term) {
+ $query = \Drupal::entityQuery('taxonomy_term')
+ ->condition('parent', $term->id());
+ return static::loadMultiple($query->execute());
}
/**
@@ -202,11 +212,11 @@ public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities =
$this->treeParents[$vid] = [];
$this->treeTerms[$vid] = [];
$query = $this->database->select('taxonomy_term_field_data', 't');
- $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
+ $query->join('taxonomy_term__parent', 'p', 't.tid = p.entity_id');
+ $query->addExpression('parent_target_id', 'parent');
$result = $query
->addTag('taxonomy_term_access')
->fields('t')
- ->fields('h', ['parent'])
->condition('t.vid', $vid)
->condition('t.default_langcode', 1)
->orderBy('t.weight')
@@ -254,7 +264,9 @@ public function loadTree($vid, $parent = 0, $max_depth = NULL, $load_entities =
$term = clone $term;
}
$term->depth = $depth;
- unset($term->parent);
+ if (!$load_entities) {
+ unset($term->parent);
+ }
$tid = $load_entities ? $term->id() : $term->tid;
$term->parents = $this->treeParents[$vid][$tid];
$tree[] = $term;
@@ -351,7 +363,7 @@ public function getNodeTerms(array $nids, array $vocabs = [], $langcode = NULL)
public function __sleep() {
$vars = parent::__sleep();
// Do not serialize static cache.
- unset($vars['parents'], $vars['parentsAll'], $vars['children'], $vars['treeChildren'], $vars['treeParents'], $vars['treeTerms'], $vars['trees']);
+ unset($vars['treeChildren'], $vars['treeParents'], $vars['treeTerms'], $vars['trees']);
return $vars;
}
@@ -361,9 +373,7 @@ public function __sleep() {
public function __wakeup() {
parent::__wakeup();
// Initialize static caches.
- $this->parents = [];
- $this->parentsAll = [];
- $this->children = [];
+ $this->ancestors = [];
$this->treeChildren = [];
$this->treeParents = [];
$this->treeTerms = [];
diff --git a/core/modules/taxonomy/src/TermStorageInterface.php b/core/modules/taxonomy/src/TermStorageInterface.php
index 4ab2d2d..4d7b5cc 100644
--- a/core/modules/taxonomy/src/TermStorageInterface.php
+++ b/core/modules/taxonomy/src/TermStorageInterface.php
@@ -15,6 +15,10 @@
*
* @param array $tids
* Array of terms that need to be removed from hierarchy.
+ *
+ * @todo Remove this method in Drupal 9.0.x. Now the parent references are
+ * automatically cleared when deleting a taxonomy term.
+ * https://www.drupal.org/node/2785693
*/
public function deleteTermHierarchy($tids);
@@ -23,6 +27,10 @@ public function deleteTermHierarchy($tids);
*
* @param \Drupal\Core\Entity\EntityInterface $term
* Term entity that needs to be added to term hierarchy information.
+ *
+ * @todo remove this method Drupal 9.0.x. Now the parent references are
+ * automatically updates when when a taxonomy term is added/updated.
+ * https://www.drupal.org/node/2785693
*/
public function updateTermHierarchy(EntityInterface $term);
diff --git a/core/modules/taxonomy/src/TermStorageSchema.php b/core/modules/taxonomy/src/TermStorageSchema.php
index 5bcb088..2b49ee2 100644
--- a/core/modules/taxonomy/src/TermStorageSchema.php
+++ b/core/modules/taxonomy/src/TermStorageSchema.php
@@ -22,36 +22,6 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res
'taxonomy_term__vid_name' => ['vid', 'name'],
];
- $schema['taxonomy_term_hierarchy'] = [
- 'description' => 'Stores the hierarchical relationship between terms.',
- 'fields' => [
- 'tid' => [
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'default' => 0,
- 'description' => 'Primary Key: The {taxonomy_term_data}.tid of the term.',
- ],
- 'parent' => [
- 'type' => 'int',
- 'unsigned' => TRUE,
- 'not null' => TRUE,
- 'default' => 0,
- 'description' => "Primary Key: The {taxonomy_term_data}.tid of the term's parent. 0 indicates no parent.",
- ],
- ],
- 'indexes' => [
- 'parent' => ['parent'],
- ],
- 'foreign keys' => [
- 'taxonomy_term_data' => [
- 'table' => 'taxonomy_term_data',
- 'columns' => ['tid' => 'tid'],
- ],
- ],
- 'primary key' => ['tid', 'parent'],
- ];
-
$schema['taxonomy_index'] = [
'description' => 'Maintains denormalized information about node/term relationships.',
'fields' => [
diff --git a/core/modules/taxonomy/src/TermViewsData.php b/core/modules/taxonomy/src/TermViewsData.php
index 5e98bfe..6d47f3f 100644
--- a/core/modules/taxonomy/src/TermViewsData.php
+++ b/core/modules/taxonomy/src/TermViewsData.php
@@ -36,7 +36,7 @@ public function getViewsData() {
$data['taxonomy_term_field_data']['tid']['filter']['id'] = 'taxonomy_index_tid';
$data['taxonomy_term_field_data']['tid']['filter']['title'] = $this->t('Term');
$data['taxonomy_term_field_data']['tid']['filter']['help'] = $this->t('Taxonomy term chosen from autocomplete or select widget.');
- $data['taxonomy_term_field_data']['tid']['filter']['hierarchy table'] = 'taxonomy_term_hierarchy';
+ $data['taxonomy_term_field_data']['tid']['filter']['hierarchy table'] = 'taxonomy_term__parent';
$data['taxonomy_term_field_data']['tid']['filter']['numeric'] = TRUE;
$data['taxonomy_term_field_data']['tid_raw'] = [
@@ -146,8 +146,8 @@ public function getViewsData() {
'left_field' => 'nid',
'field' => 'nid',
],
- 'taxonomy_term_hierarchy' => [
- 'left_field' => 'tid',
+ 'taxonomy_term__parent' => [
+ 'left_field' => 'entity_id',
'field' => 'tid',
],
];
@@ -181,7 +181,7 @@ public function getViewsData() {
'filter' => [
'title' => $this->t('Has taxonomy term'),
'id' => 'taxonomy_index_tid',
- 'hierarchy table' => 'taxonomy_term_hierarchy',
+ 'hierarchy table' => 'taxonomy_term__parent',
'numeric' => TRUE,
'skip base' => 'taxonomy_term_field_data',
'allow empty' => TRUE,
@@ -223,40 +223,15 @@ public function getViewsData() {
],
];
- $data['taxonomy_term_hierarchy']['table']['group'] = $this->t('Taxonomy term');
- $data['taxonomy_term_hierarchy']['table']['provider'] = 'taxonomy';
-
- $data['taxonomy_term_hierarchy']['table']['join'] = [
- 'taxonomy_term_hierarchy' => [
- // Link to self through left.parent = right.tid (going down in depth).
- 'left_field' => 'tid',
- 'field' => 'parent',
- ],
- 'taxonomy_term_field_data' => [
- // Link directly to taxonomy_term_field_data via tid.
- 'left_field' => 'tid',
- 'field' => 'tid',
- ],
+ // Link to self through left.parent = right.tid (going down in depth).
+ $data['taxonomy_term__parent']['table']['join']['taxonomy_term__parent'] = [
+ 'left_field' => 'entity_id',
+ 'field' => 'parent_target_id',
];
- $data['taxonomy_term_hierarchy']['parent'] = [
- 'title' => $this->t('Parent term'),
- 'help' => $this->t('The parent term of the term. This can produce duplicate entries if you are using a vocabulary that allows multiple parents.'),
- 'relationship' => [
- 'base' => 'taxonomy_term_field_data',
- 'field' => 'parent',
- 'label' => $this->t('Parent'),
- 'id' => 'standard',
- ],
- 'filter' => [
- 'help' => $this->t('Filter the results of "Taxonomy: Term" by the parent pid.'),
- 'id' => 'numeric',
- ],
- 'argument' => [
- 'help' => $this->t('The parent term of the term.'),
- 'id' => 'taxonomy',
- ],
- ];
+ $data['taxonomy_term__parent']['parent_target_id']['help'] = $this->t('The parent term of the term. This can produce duplicate entries if you are using a vocabulary that allows multiple parents.');
+ $data['taxonomy_term__parent']['parent_target_id']['relationship']['label'] = $this->t('Parent');
+ $data['taxonomy_term__parent']['parent_target_id']['argument']['id'] = 'taxonomy';
return $data;
}
diff --git a/core/modules/taxonomy/src/VocabularyStorage.php b/core/modules/taxonomy/src/VocabularyStorage.php
index bdbf893..ce90189 100644
--- a/core/modules/taxonomy/src/VocabularyStorage.php
+++ b/core/modules/taxonomy/src/VocabularyStorage.php
@@ -21,7 +21,12 @@ public function resetCache(array $ids = NULL) {
* {@inheritdoc}
*/
public function getToplevelTids($vids) {
- return db_query('SELECT t.tid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_term_hierarchy} th ON th.tid = t.tid WHERE t.vid IN ( :vids[] ) AND th.parent = 0', [':vids[]' => $vids])->fetchCol();
+ $tids = \Drupal::entityQuery('taxonomy_term')
+ ->condition('vid', $vids, 'IN')
+ ->condition('parent.target_id', 0)
+ ->execute();
+
+ return array_values($tids);
}
}
diff --git a/core/modules/taxonomy/taxonomy.install b/core/modules/taxonomy/taxonomy.install
new file mode 100644
index 0000000..e5f4fe3
--- /dev/null
+++ b/core/modules/taxonomy/taxonomy.install
@@ -0,0 +1,132 @@
+getFieldStorageDefinition('parent', 'taxonomy_term');
+ $field_storage_definition->setCustomStorage(FALSE);
+ $definition_update_manager->updateFieldStorageDefinition($field_storage_definition);
+}
+
+/**
+ * Copy hierarchy from {taxonomy_term_hierarchy} to {taxonomy_term__parent}.
+ */
+function taxonomy_update_8502(&$sandbox) {
+ $database = \Drupal::database();
+
+ if (!isset($sandbox['current'])) {
+ // Set batch ops sandbox.
+ $sandbox['current'] = 0;
+ $sandbox['max'] = $database->select('taxonomy_term_hierarchy')
+ ->countQuery()
+ ->execute()
+ ->fetchField();
+ }
+
+ // Save the hierarchy.
+ $select = $database->select('taxonomy_term_hierarchy', 'h');
+ $select->join('taxonomy_term_data', 'd', 'h.tid = d.tid');
+ $hierarchy = $select
+ ->fields('h', ['tid', 'parent'])
+ ->fields('d', ['vid', 'langcode'])
+ ->range($sandbox['current'], $sandbox['current'] + 100)
+ ->execute()
+ ->fetchAll();
+
+ // Restore data.
+ $insert = $database->insert('taxonomy_term__parent')
+ ->fields(['bundle', 'entity_id', 'revision_id', 'langcode', 'delta', 'parent_target_id']);
+ $tid = -1;
+
+ foreach ($hierarchy as $row) {
+ if ($row->tid !== $tid) {
+ $delta = 0;
+ $tid = $row->tid;
+ }
+
+ $insert->values([
+ 'bundle' => $row->vid,
+ 'entity_id' => $row->tid,
+ 'revision_id' => $row->tid,
+ 'langcode' => $row->langcode,
+ 'delta' => $delta,
+ 'parent_target_id' => $row->parent,
+ ]);
+
+ $delta++;
+ $sandbox['current']++;
+ }
+
+ $insert->execute();
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['current'] / $sandbox['max']);
+
+ if ($sandbox['#finished'] >= 1) {
+ // Update the entity type because the 'taxonomy_term_hierarchy' table is no
+ // longer part of its shared tables schema.
+ $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+ $definition_update_manager->updateEntityType($definition_update_manager->getEntityType('taxonomy_term'));
+
+ // \Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::onEntityTypeUpdate()
+ // only deletes *known* entity tables (i.e. the base, data and revision
+ // tables), so we have to drop it manually.
+ $database->schema()->dropTable('taxonomy_term_hierarchy');
+
+ return t('Taxonomy term hierarchy has been converted to default entity reference storage.');
+ }
+}
+
+/**
+ * Update views to use {taxonomy_term__parent} in relationships.
+ */
+function taxonomy_update_8503() {
+ $config_factory = \Drupal::configFactory();
+ foreach ($config_factory->listAll('views.view.') as $id) {
+ $view = $config_factory->getEditable($id);
+ $changed = FALSE;
+
+ foreach (array_keys($view->get('display')) as $display_id) {
+ $base_path = "display.$display_id.display_options.relationships.parent";
+ $table_path = $base_path . '.table';
+ $field_path = $base_path . '.field';
+
+ if (!empty($table = $view->get($table_path)) && $table == 'taxonomy_term_hierarchy') {
+ $view->set($table_path, 'taxonomy_term__parent');
+ $view->set($field_path, 'parent_target_id');
+
+ $base_path = "display.{$display_id}.display_options.filters.parent";
+ $table_path = $base_path . '.table';
+ $field_path = $base_path . '.field';
+
+ if (!empty($table = $view->get($table_path)) && $table == 'taxonomy_term_hierarchy') {
+ $view->set($table_path, 'taxonomy_term__parent');
+ $view->set($field_path, 'parent_target_id');
+ }
+
+ $base_path = "display.{$display_id}.display_options.arguments.parent.table";
+ $table_path = $base_path . '.table';
+ $field_path = $base_path . '.field';
+
+ if (!empty($table = $view->get($table_path)) && $table == 'taxonomy_term_hierarchy') {
+ $view->set($table_path, 'taxonomy_term__parent');
+ $view->set($field_path, 'parent_target_id');
+ }
+
+ $changed = TRUE;
+ }
+ }
+
+ if ($changed) {
+ $view->save(TRUE);
+ }
+ }
+}
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_parent.yml b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_parent.yml
index 256f618..65df6a8 100644
--- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_parent.yml
+++ b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_parent.yml
@@ -124,8 +124,8 @@ display:
relationships:
parent:
id: parent
- table: taxonomy_term_hierarchy
- field: parent
+ table: taxonomy_term__parent
+ field: parent_target_id
relationship: none
group_type: group
admin_label: Parent
diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_term_relationship.yml b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_term_relationship.yml
index 80295b6..664ae27 100644
--- a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_term_relationship.yml
+++ b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_taxonomy_term_relationship.yml
@@ -186,8 +186,8 @@ display:
plugin_id: standard
parent:
id: parent
- table: taxonomy_term_hierarchy
- field: parent
+ table: taxonomy_term__parent
+ field: parent_target_id
relationship: none
group_type: group
admin_label: Parent
diff --git a/core/modules/taxonomy/tests/src/Functional/TaxonomyQueryAlterTest.php b/core/modules/taxonomy/tests/src/Functional/TaxonomyQueryAlterTest.php
index 85310e9..81bd679 100644
--- a/core/modules/taxonomy/tests/src/Functional/TaxonomyQueryAlterTest.php
+++ b/core/modules/taxonomy/tests/src/Functional/TaxonomyQueryAlterTest.php
@@ -50,12 +50,12 @@ public function testTaxonomyQueryAlter() {
$this->setupQueryTagTestHooks();
$loaded_terms = $term_storage->loadParents($terms[2]->id());
$this->assertEqual(count($loaded_terms), 1, 'All parent terms were loaded');
- $this->assertQueryTagTestResult(2, 1, 'TermStorage::loadParents()');
+ $this->assertQueryTagTestResult(3, 1, 'TermStorage::loadParents()');
$this->setupQueryTagTestHooks();
$loaded_terms = $term_storage->loadChildren($terms[1]->id());
$this->assertEqual(count($loaded_terms), 1, 'All child terms were loaded');
- $this->assertQueryTagTestResult(2, 1, 'TermStorage::loadChildren()');
+ $this->assertQueryTagTestResult(3, 1, 'TermStorage::loadChildren()');
$this->setupQueryTagTestHooks();
$query = db_select('taxonomy_term_data', 't');
diff --git a/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyParentUpdateTest.php b/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyParentUpdateTest.php
new file mode 100644
index 0000000..68e84e0
--- /dev/null
+++ b/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyParentUpdateTest.php
@@ -0,0 +1,80 @@
+db = $this->container->get('database');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDatabaseDumpFiles() {
+ $this->databaseDumpFiles = [
+ __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8-rc1.bare.standard.php.gz',
+ __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.php',
+ ];
+ }
+
+ /**
+ * Tests taxonomy term parents update.
+ *
+ * @see taxonomy_update_8501()
+ * @see taxonomy_update_8502()
+ * @see taxonomy_update_8503()
+ */
+ public function testTaxonomyUpdateParents() {
+ // Run updates.
+ $this->runUpdates();
+
+ /** @var \Drupal\taxonomy\TermInterface $term */
+ $term = Term::load(1);
+ $parents = [2, 3];
+ $this->assertCount(2, $term->parent);
+ $this->assertTrue(in_array($term->parent[0]->entity->id(), $parents));
+ $this->assertTrue(in_array($term->parent[1]->entity->id(), $parents));
+
+ $term = Term::load(2);
+ $parents = [0, 3];
+ $this->assertCount(2, $term->parent);
+ $this->assertTrue(in_array($term->parent[0]->target_id, $parents));
+ $this->assertTrue(in_array($term->parent[1]->target_id, $parents));
+
+ $term = Term::load(3);
+ $this->assertCount(1, $term->parent);
+ // Target ID is returned as string.
+ $this->assertSame((int) $term->get('parent')[0]->target_id, 0);
+
+ // Test if the view has been converted to use the {taxonomy_term__parent}
+ // table instead of the {taxonomy_term_hierarchy} table.
+ $view = $this->config("views.view.test_taxonomy_parent");
+ $path = 'display.default.display_options.relationships.parent.table';
+ $this->assertSame($view->get($path), 'taxonomy_term__parent');
+
+ // The {taxonomy_term_hierarchy} table has been removed.
+ $this->assertFalse($this->db->schema()->tableExists('taxonomy_term_hierarchy'));
+ }
+
+}
diff --git a/core/modules/taxonomy/tests/src/Functional/Views/TaxonomyRelationshipTest.php b/core/modules/taxonomy/tests/src/Functional/Views/TaxonomyRelationshipTest.php
index 03be3e1..3299964 100644
--- a/core/modules/taxonomy/tests/src/Functional/Views/TaxonomyRelationshipTest.php
+++ b/core/modules/taxonomy/tests/src/Functional/Views/TaxonomyRelationshipTest.php
@@ -61,24 +61,24 @@ public function testTaxonomyRelationships() {
$this->assertEqual($views_data['table']['join']['taxonomy_term_field_data']['field'], 'tid');
$this->assertEqual($views_data['table']['join']['node_field_data']['left_field'], 'nid');
$this->assertEqual($views_data['table']['join']['node_field_data']['field'], 'nid');
- $this->assertEqual($views_data['table']['join']['taxonomy_term_hierarchy']['left_field'], 'tid');
- $this->assertEqual($views_data['table']['join']['taxonomy_term_hierarchy']['field'], 'tid');
+ $this->assertEqual($views_data['table']['join']['taxonomy_term__parent']['left_field'], 'entity_id');
+ $this->assertEqual($views_data['table']['join']['taxonomy_term__parent']['field'], 'tid');
- // Check the generated views data of taxonomy_term_hierarchy.
- $views_data = Views::viewsData()->get('taxonomy_term_hierarchy');
+ // Check the generated views data of taxonomy_term__parent.
+ $views_data = Views::viewsData()->get('taxonomy_term__parent');
// Check the table join data.
- $this->assertEqual($views_data['table']['join']['taxonomy_term_hierarchy']['left_field'], 'tid');
- $this->assertEqual($views_data['table']['join']['taxonomy_term_hierarchy']['field'], 'parent');
+ $this->assertEqual($views_data['table']['join']['taxonomy_term__parent']['left_field'], 'entity_id');
+ $this->assertEqual($views_data['table']['join']['taxonomy_term__parent']['field'], 'parent_target_id');
$this->assertEqual($views_data['table']['join']['taxonomy_term_field_data']['left_field'], 'tid');
- $this->assertEqual($views_data['table']['join']['taxonomy_term_field_data']['field'], 'tid');
+ $this->assertEqual($views_data['table']['join']['taxonomy_term_field_data']['field'], 'entity_id');
// Check the parent relationship data.
- $this->assertEqual($views_data['parent']['relationship']['base'], 'taxonomy_term_field_data');
- $this->assertEqual($views_data['parent']['relationship']['field'], 'parent');
- $this->assertEqual($views_data['parent']['relationship']['label'], t('Parent'));
- $this->assertEqual($views_data['parent']['relationship']['id'], 'standard');
+ $this->assertEqual($views_data['parent_target_id']['relationship']['base'], 'taxonomy_term_field_data');
+ $this->assertEqual($views_data['parent_target_id']['relationship']['base field'], 'tid');
+ $this->assertEqual($views_data['parent_target_id']['relationship']['label'], t('Parent'));
+ $this->assertEqual($views_data['parent_target_id']['relationship']['id'], 'standard');
// Check the parent filter and argument data.
- $this->assertEqual($views_data['parent']['filter']['id'], 'numeric');
- $this->assertEqual($views_data['parent']['argument']['id'], 'taxonomy');
+ $this->assertEqual($views_data['parent_target_id']['filter']['id'], 'numeric');
+ $this->assertEqual($views_data['parent_target_id']['argument']['id'], 'taxonomy');
// Check an actual test view.
$view = Views::getView('test_taxonomy_term_relationship');
@@ -95,7 +95,7 @@ public function testTaxonomyRelationships() {
if (!$index) {
$this->assertTrue($row->_relationship_entities['parent'] instanceof TermInterface);
$this->assertEqual($row->_relationship_entities['parent']->id(), $this->term2->id());
- $this->assertEqual($row->taxonomy_term_field_data_taxonomy_term_hierarchy_tid, $this->term2->id());
+ $this->assertEqual($row->taxonomy_term_field_data_taxonomy_term__parent_tid, $this->term2->id());
}
$this->assertTrue($row->_relationship_entities['nid'] instanceof NodeInterface);
$this->assertEqual($row->_relationship_entities['nid']->id(), $this->nodes[$index]->id());
diff --git a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTaxonomyTermTest.php b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTaxonomyTermTest.php
index 14fba56..75c7924 100644
--- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTaxonomyTermTest.php
+++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTaxonomyTermTest.php
@@ -92,7 +92,7 @@ public function testTaxonomyTerms() {
$this->assertSame($values['vid'], $term->vid->target_id);
$this->assertSame((string) $values['weight'], $term->weight->value);
if ($values['parent'] === [0]) {
- $this->assertNull($term->parent->target_id);
+ $this->assertSame(0, (int) $term->parent->target_id);
}
else {
$parents = [];