diff --git a/composer.json b/composer.json
index 3bfb0c7779..646fd94203 100644
--- a/composer.json
+++ b/composer.json
@@ -8,7 +8,7 @@
"wikimedia/composer-merge-plugin": "^1.4"
},
"replace": {
- "drupal/core": "^8.5"
+ "drupal/core": "^8.6"
},
"minimum-stability": "dev",
"prefer-stable": true,
diff --git a/composer.lock b/composer.lock
index e42d75b760..3b126299e4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "content-hash": "97de1708c79f6205a295cfc9808c0c72",
+ "content-hash": "fdc8e2afb49b5917c7bb593a3a8746d3",
"packages": [
{
"name": "asm89/stack-cors",
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index 1697ff62a2..61982c3885 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -82,7 +82,7 @@ class Drupal {
/**
* The current system version.
*/
- const VERSION = '8.5.0-dev';
+ const VERSION = '8.6.0-dev';
/**
* Core API compatibility.
diff --git a/core/modules/forum/forum.views.inc b/core/modules/forum/forum.views.inc
index 5f67af59a0..156596234b 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 9ba4be7b2a..2bc6a45d84 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 a877163c08..13301804ff 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 fbcae8c758..1a4ef54e12 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.
*
@@ -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}
*/
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 9b0cee24a0..3edd9b1609 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 3deb0eaba5..1939e04364 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 2de6539fe4..e218a73a42 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
@@ -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}
*/
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 73a1549e0a..e19fb1baab 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 85a4b1a107..ce6090419e 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 962d8e9d2e..b69394ba14 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 ea2e020bf0..2d3c6f012b 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 0000000000..1acfd70a7a
--- /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 5cc6467e8b..9320d17ba2 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();
@@ -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();
diff --git a/core/modules/settings_tray/js/settings_tray.es6.js b/core/modules/settings_tray/js/settings_tray.es6.js
index 6487690c7b..a739e9718c 100644
--- a/core/modules/settings_tray/js/settings_tray.es6.js
+++ b/core/modules/settings_tray/js/settings_tray.es6.js
@@ -171,6 +171,14 @@
}
instance.options.data.dialogOptions.settingsTrayActiveEditableId = $(instance.element).parents('.settings-tray-editable').attr('id');
instance.progress = { type: 'fullscreen' };
+
+ if (instance.hasOwnProperty('element')) {
+ const element = $(instance.element);
+ // If the ajax element is within a overridden block remove the link.
+ if (element.closest('[data-settings-tray-overridden]').length === 1) {
+ element.remove();
+ }
+ }
});
}
diff --git a/core/modules/settings_tray/js/settings_tray.js b/core/modules/settings_tray/js/settings_tray.js
index 7a83e156ca..d4649d72f4 100644
--- a/core/modules/settings_tray/js/settings_tray.js
+++ b/core/modules/settings_tray/js/settings_tray.js
@@ -102,6 +102,14 @@
}
instance.options.data.dialogOptions.settingsTrayActiveEditableId = $(instance.element).parents('.settings-tray-editable').attr('id');
instance.progress = { type: 'fullscreen' };
+
+ if (instance.hasOwnProperty('element')) {
+ var element = $(instance.element);
+
+ if (element.closest('[data-settings-tray-overridden]').length === 1) {
+ element.remove();
+ }
+ }
});
}
diff --git a/core/modules/settings_tray/settings_tray.module b/core/modules/settings_tray/settings_tray.module
index 48b08caaab..04a5cadd32 100644
--- a/core/modules/settings_tray/settings_tray.module
+++ b/core/modules/settings_tray/settings_tray.module
@@ -11,6 +11,8 @@
use Drupal\settings_tray\Block\BlockEntityOffCanvasForm;
use Drupal\settings_tray\Form\SystemBrandingOffCanvasForm;
use Drupal\settings_tray\Form\SystemMenuOffCanvasForm;
+use Drupal\block\BlockInterface;
+use Drupal\block\Entity\Block;
/**
* Implements hook_help().
@@ -54,6 +56,23 @@ function settings_tray_contextual_links_view_alter(&$element, $items) {
}
}
+/**
+ * Checks if a block has overrides.
+ *
+ * @param \Drupal\block\BlockInterface $block
+ * The block to check for overrides.
+ *
+ * @return bool
+ * TRUE if the block has overrides otherwise FALSE.
+ *
+ * @internal
+ */
+function _settings_tray_has_block_overrides(BlockInterface $block) {
+ // @todo Replace the following with $block->hasOverrides() in https://www.drupal.org/project/drupal/issues/2910353
+ // and remove the need for this function.
+ return \Drupal::config($block->getEntityType()->getConfigPrefix() . '.' . $block->id())->hasOverrides();
+}
+
/**
* Implements hook_block_view_alter().
*/
@@ -93,10 +112,25 @@ function settings_tray_preprocess_block(&$variables) {
$block_plugin_manager = \Drupal::service('plugin.manager.block');
/** @var \Drupal\Core\Block\BlockPluginInterface $block_plugin */
$block_plugin = $block_plugin_manager->createInstance($variables['plugin_id']);
- if ($access_checker->accessBlockPlugin($block_plugin)->isAllowed()) {
- // Add class and attributes to all blocks to allow Javascript to target.
- $variables['attributes']['class'][] = 'settings-tray-editable';
- $variables['attributes']['data-drupal-settingstray'] = 'editable';
+ if (isset($variables['elements']['#contextual_links']['block']['route_parameters']['block'])) {
+ $block_id = $variables['elements']['#contextual_links']['block']['route_parameters']['block'];
+ $block = Block::load($block_id);
+ if ($access_checker->accessBlockPlugin($block_plugin)->isAllowed()) {
+ if (!_settings_tray_has_block_overrides($block)) {
+ // Add class and attributes to all blocks to allow Javascript to target.
+ $variables['attributes']['class'][] = 'settings-tray-editable';
+ $variables['attributes']['data-drupal-settingstray'] = 'editable';
+ }
+ else {
+ // If the block is overridden then set an attribute which will be used
+ // in settings_tray.es6.js to remove the "Quick edit" link.
+ // It not possible to remove the contextual link with
+ // hook_contextual_links_alter or hook_contextual_links_view_alter
+ // because they don't get called every time the block will be rendered
+ // with possibly different configuration overrides.
+ $variables['attributes']['data-settings-tray-overridden'] = TRUE;
+ }
+ }
}
}
diff --git a/core/modules/settings_tray/src/Form/SystemBrandingOffCanvasForm.php b/core/modules/settings_tray/src/Form/SystemBrandingOffCanvasForm.php
index 0b4290e2fa..94c8052648 100644
--- a/core/modules/settings_tray/src/Form/SystemBrandingOffCanvasForm.php
+++ b/core/modules/settings_tray/src/Form/SystemBrandingOffCanvasForm.php
@@ -2,6 +2,7 @@
namespace Drupal\settings_tray\Form;
+use Drupal\Core\Access\AccessResult;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormStateInterface;
@@ -63,10 +64,13 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
unset($form['block_branding']['use_site_name']['#description'], $form['block_branding']['use_site_slogan']['#description']);
$site_config = $this->configFactory->getEditable('system.site');
+ // Load the immutable config to load the overrides.
+ $site_config_immutable = $this->configFactory->get('system.site');
$form['site_information'] = [
'#type' => 'details',
'#title' => t('Site details'),
'#open' => TRUE,
+ '#access' => AccessResult::allowedIf(!$site_config_immutable->hasOverrides('name') && !$site_config_immutable->hasOverrides('slogan')),
];
$form['site_information']['site_name'] = [
'#type' => 'textfield',
@@ -95,11 +99,15 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
- $site_info = $form_state->getValue('site_information');
- $this->configFactory->getEditable('system.site')
- ->set('name', $site_info['site_name'])
- ->set('slogan', $site_info['site_slogan'])
- ->save();
+ $site_config = $this->configFactory->get('system.site');
+ if (AccessResult::allowedIf(!$site_config->hasOverrides('name') && !$site_config->hasOverrides('slogan'))->isAllowed()) {
+ $site_info = $form_state->getValue('site_information');
+ $this->configFactory->getEditable('system.site')
+ ->set('name', $site_info['site_name'])
+ ->set('slogan', $site_info['site_slogan'])
+ ->save();
+ }
+
$this->plugin->submitConfigurationForm($form, $form_state);
}
diff --git a/core/modules/settings_tray/src/Form/SystemMenuOffCanvasForm.php b/core/modules/settings_tray/src/Form/SystemMenuOffCanvasForm.php
index 15d19a87f9..d755d092d0 100644
--- a/core/modules/settings_tray/src/Form/SystemMenuOffCanvasForm.php
+++ b/core/modules/settings_tray/src/Form/SystemMenuOffCanvasForm.php
@@ -3,6 +3,7 @@
namespace Drupal\settings_tray\Form;
use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -87,6 +88,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
'#type' => 'details',
'#title' => $this->t('Edit menu %label', ['%label' => $this->menu->label()]),
'#open' => TRUE,
+ '#access' => AccessResult::allowedIf(!$this->hasMenuOverrides()),
];
$form['entity_form'] += $this->getEntityForm($this->menu)->buildForm([], $form_state);
@@ -115,7 +117,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->plugin->validateConfigurationForm($form, $form_state);
- $this->getEntityForm($this->menu)->validateForm($form, $form_state);
+ if (!$this->hasMenuOverrides()) {
+ $this->getEntityForm($this->menu)->validateForm($form, $form_state);
+ }
}
/**
@@ -123,8 +127,10 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->plugin->submitConfigurationForm($form, $form_state);
- $this->getEntityForm($this->menu)->submitForm($form, $form_state);
- $this->menu->save();
+ if (!$this->hasMenuOverrides()) {
+ $this->getEntityForm($this->menu)->submitForm($form, $form_state);
+ $this->menu->save();
+ }
}
/**
@@ -147,7 +153,15 @@ protected function getEntityForm(MenuInterface $menu) {
*/
public function setPlugin(PluginInspectionInterface $plugin) {
$this->plugin = $plugin;
- $this->menu = $this->menuStorage->load($this->plugin->getDerivativeId());
+ $this->menu = $this->menuStorage->loadOverrideFree($this->plugin->getDerivativeId());
+ }
+
+ /**
+ * @return bool
+ */
+ protected function hasMenuOverrides() {
+ return \Drupal::config($this->menu->getEntityType()
+ ->getConfigPrefix() . '.' . $this->menu->id())->hasOverrides();
}
}
diff --git a/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.info.yml b/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.info.yml
new file mode 100644
index 0000000000..89f9732feb
--- /dev/null
+++ b/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.info.yml
@@ -0,0 +1,7 @@
+name: 'Configuration override test for Settings Tray'
+type: module
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+ - settings_tray
diff --git a/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.services.yml b/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.services.yml
new file mode 100644
index 0000000000..6e5cb75731
--- /dev/null
+++ b/core/modules/settings_tray/tests/modules/settings_tray_override_test/settings_tray_override_test.services.yml
@@ -0,0 +1,5 @@
+services:
+ settings_tray_override_test.overrider:
+ class: Drupal\settings_tray_override_test\ConfigOverrider
+ tags:
+ - { name: config.factory.override }
diff --git a/core/modules/settings_tray/tests/modules/settings_tray_override_test/src/ConfigOverrider.php b/core/modules/settings_tray/tests/modules/settings_tray_override_test/src/ConfigOverrider.php
new file mode 100644
index 0000000000..6921fa8e0d
--- /dev/null
+++ b/core/modules/settings_tray/tests/modules/settings_tray_override_test/src/ConfigOverrider.php
@@ -0,0 +1,60 @@
+get('settings_tray_override_test.block')) {
+ $overrides = $overrides + ['block.block.overridden_block' => ['settings' => ['label' => 'Now this will be the label.']]];
+ }
+ }
+ if (in_array('system.site', $names)) {
+ if (\Drupal::state()->get('settings_tray_override_test.site_name')) {
+ $overrides = $overrides + ['system.site' => ['name' => 'Llama Fan Club']];
+ }
+ }
+ if (in_array('system.menu.main', $names)) {
+ if (\Drupal::state()->get('settings_tray_override_test.menu')) {
+ $overrides = $overrides + ['system.menu.main' => ['label' => 'Labely label']];
+ }
+ }
+ return $overrides;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheSuffix() {
+ return 'ConfigOverrider';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION) {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheableMetadata($name) {
+ return new CacheableMetadata();
+ }
+
+}
diff --git a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php
index fcfecde400..b935175eb8 100644
--- a/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php
+++ b/core/modules/settings_tray/tests/src/FunctionalJavascript/SettingsTrayBlockFormTest.php
@@ -5,6 +5,7 @@
use Drupal\block\Entity\Block;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
+use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\settings_tray_test\Plugin\Block\SettingsTrayFormAnnotationIsClassBlock;
use Drupal\settings_tray_test\Plugin\Block\SettingsTrayFormAnnotationNoneBlock;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
@@ -43,6 +44,9 @@ class SettingsTrayBlockFormTest extends OffCanvasTestBase {
// cause test failures.
'settings_tray_test_css',
'settings_tray_test',
+ 'settings_tray_override_test',
+ 'menu_ui',
+ 'menu_link_content',
];
/**
@@ -75,7 +79,7 @@ public function testBlocks($theme, $block_plugin, $new_page_text, $element_selec
$page = $this->getSession()->getPage();
$this->enableTheme($theme);
$block = $this->placeBlock($block_plugin);
- $block_selector = str_replace('_', '-', $this->getBlockSelector($block));
+ $block_selector = $this->getBlockSelector($block);
$block_id = $block->id();
$this->drupalGet('user');
@@ -267,8 +271,10 @@ protected function assertOffCanvasBlockFormIsValid() {
* @param string $contextual_link_container
* The element that contains the contextual links. If none provide the
* $block_selector will be used.
+ * @param bool $has_confirm_form
+ * Determines if the block form should be confirmed.
*/
- protected function openBlockForm($block_selector, $contextual_link_container = '') {
+ protected function openBlockForm($block_selector, $contextual_link_container = '', $has_confirm_form = TRUE) {
if (!$contextual_link_container) {
$contextual_link_container = $block_selector;
}
@@ -283,7 +289,9 @@ protected function openBlockForm($block_selector, $contextual_link_container = '
$this->assertSession()->assertWaitOnAjaxRequest();
$this->click($block_selector);
$this->waitForOffCanvasToOpen();
- $this->assertOffCanvasBlockFormIsValid();
+ if ($has_confirm_form) {
+ $this->assertOffCanvasBlockFormIsValid();
+ }
}
/**
@@ -321,7 +329,7 @@ public function testQuickEditLinks() {
$this->enableTheme($theme);
$block = $this->placeBlock($block_plugin);
- $block_selector = str_replace('_', '-', $this->getBlockSelector($block));
+ $block_selector = $this->getBlockSelector($block);
// Load the same page twice.
foreach ([1, 2] as $page_load_times) {
$this->drupalGet('node/' . $node->id());
@@ -531,7 +539,7 @@ public function testCustomBlockLinks() {
* The CSS selector.
*/
public function getBlockSelector(Block $block) {
- return '#block-' . $block->id();
+ return '#block-' . str_replace('_', '-', $block->id());
}
/**
@@ -577,4 +585,138 @@ protected function getTestThemes() {
});
}
+ /**
+ * Tests that blocks with configuration overrides are disabled.
+ */
+ public function testOverriddenBlock() {
+ $web_assert = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $overridden_block = $this->placeBlock('system_powered_by_block', [
+ 'id' => 'overridden_block',
+ 'label_display' => 1,
+ 'label' => 'This will be overridden.',
+ ]);
+ $this->drupalGet('user');
+ $block_selector = $this->getBlockSelector($overridden_block);
+ // Confirm the block is marked as Settings Tray editable.
+ $this->assertEquals('editable', $page->find('css', $block_selector)->getAttribute('data-drupal-settingstray'));
+ // Confirm the label is not overridden.
+ $web_assert->elementContains('css', $block_selector, 'This will be overridden.');
+ $this->enableEditMode();
+ $this->openBlockForm($block_selector);
+
+
+ // Confirm the block Settings Tray functionality is disabled when block is
+ // overridden.
+ $this->container->get('state')->set('settings_tray_override_test.block', TRUE);
+ $overridden_block->save();
+ $block_config = \Drupal::configFactory()->getEditable('block.block.overridden_block');
+ $block_config->set('settings', $block_config->get('settings'))->save();
+
+ $this->drupalGet('user');
+ $this->assertOverriddenBlockDisabled($overridden_block, 'Now this will be the label.');
+
+ // Test a non-overridden block does show the form in the off-canvas dialog.
+ $block = $this->placeBlock('system_powered_by_block', [
+ 'label_display' => 1,
+ 'label' => 'Labely label',
+ ]);
+ $this->drupalGet('user');
+ $block_selector = $this->getBlockSelector($block);
+ // Confirm the block is marked as Settings Tray editable.
+ $this->assertEquals('editable', $page->find('css', $block_selector)->getAttribute('data-drupal-settingstray'));
+ // Confirm the label is not overridden.
+ $web_assert->elementContains('css', $block_selector, 'Labely label');
+ $this->openBlockForm($block_selector);
+ }
+
+ /**
+ * Test blocks with overridden related configuration removed when overridden.
+ */
+ public function testOverriddenConfigurationRemoved() {
+ $web_assert = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Confirm the branding block does include 'site_information' section when
+ // the site name is not overridden.
+ $branding_block = $this->placeBlock('system_branding_block');
+ $this->drupalGet('user');
+ $this->enableEditMode();
+ $this->openBlockForm($this->getBlockSelector($branding_block));
+ $web_assert->fieldExists('settings[site_information][site_name]');
+ // Confirm the branding block does not include 'site_information' section
+ // when the site name is overridden.
+ $this->container->get('state')->set('settings_tray_override_test.site_name', TRUE);
+ $this->drupalGet('user');
+ $this->openBlockForm($this->getBlockSelector($branding_block));
+ $web_assert->fieldNotExists('settings[site_information][site_name]');
+ $page->pressButton('Save Site branding');
+ $this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)');
+ $web_assert->assertWaitOnAjaxRequest();
+ // Confirm we did not save changes to the configuration.
+ $this->assertEquals('Llama Fan Club', \Drupal::configFactory()->get('system.site')->get('name'));
+ $this->assertEquals('Drupal', \Drupal::configFactory()->getEditable('system.site')->get('name'));
+
+ // Add a link or the menu will not render.
+ $menu_link_content = MenuLinkContent::create([
+ 'title' => 'This is on the menu',
+ 'menu_name' => 'main',
+ 'link' => ['uri' => 'route:'],
+ ]);
+ $menu_link_content->save();
+ // Confirm the menu block does include menu section when the menu is not
+ // overridden.
+ $menu_block = $this->placeBlock('system_menu_block:main');
+ $web_assert->assertWaitOnAjaxRequest();
+ $this->drupalGet('user');
+ $web_assert->pageTextContains('This is on the menu');
+ $this->openBlockForm($this->getBlockSelector($menu_block));
+ $web_assert->elementExists('css', '#menu-overview');
+
+ // Confirm the menu block does not include menu section when the menu is
+ // overridden.
+ $this->container->get('state')->set('settings_tray_override_test.menu', TRUE);
+ $this->drupalGet('user');
+ $web_assert->pageTextContains('This is on the menu');
+ $menu_with_overrides = \Drupal::configFactory()->get('system.menu.main')->get();
+ $menu_without_overrides = \Drupal::configFactory()->getEditable('system.menu.main')->get();
+ $this->openBlockForm($this->getBlockSelector($menu_block));
+ $web_assert->elementNotExists('css', '#menu-overview');
+ $page->pressButton('Save Main navigation');
+ $this->assertElementVisibleAfterWait('css', 'div:contains(The block configuration has been saved)');
+ $web_assert->assertWaitOnAjaxRequest();
+ // Confirm we did not save changes to the configuration.
+ $this->assertEquals('Labely label', \Drupal::configFactory()->get('system.menu.main')->get('label'));
+ $this->assertEquals('Main navigation', \Drupal::configFactory()->getEditable('system.menu.main')->get('label'));
+ $this->assertEquals($menu_with_overrides, \Drupal::configFactory()->get('system.menu.main')->get());
+ $this->assertEquals($menu_without_overrides, \Drupal::configFactory()->getEditable('system.menu.main')->get());
+ $web_assert->pageTextContains('This is on the menu');
+ }
+ /**
+ * Asserts that an overridden block has Settings Tray disabled.
+ *
+ * @param \Drupal\block\Entity\Block $overridden_block
+ * The overridden block.
+ * @param string $override_text
+ * The override text that should appear in the block.
+ */
+ protected function assertOverriddenBlockDisabled(Block $overridden_block, $override_text) {
+ $web_assert = $this->assertSession();
+ $page = $this->getSession()->getPage();
+ $block_selector = $this->getBlockSelector($overridden_block);
+ $block_id = $overridden_block->id();
+ // Confirm the block does not have a quick edit link.
+ $contextual_links = $page->findAll('css', "$block_selector .contextual-links li a");
+ $this->assertNotEmpty($contextual_links);
+ foreach ($contextual_links as $link) {
+ $this->assertNotContains("/admin/structure/block/manage/$block_id/off-canvas", $link->getAttribute('href'));
+ }
+ // Confirm the block is not marked as Settings Tray editable.
+ $this->assertFalse($page->find('css', $block_selector)
+ ->hasAttribute('data-drupal-settingstray'));
+
+ // Confirm the text is actually overridden.
+ $web_assert->elementContains('css', $this->getBlockSelector($overridden_block), $override_text);
+ }
+
}
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 0000000000..70e0ad4964
--- /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/system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.yml b/core/modules/system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.yml
new file mode 100644
index 0000000000..de8f63d7c4
--- /dev/null
+++ b/core/modules/system/tests/fixtures/update/drupal-8.views-taxonomy-parent-2543726.yml
@@ -0,0 +1,222 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - taxonomy
+ - user
+id: test_taxonomy_parent
+label: test_taxonomy_parent
+module: views
+description: ''
+tag: ''
+base_table: taxonomy_term_data
+base_field: tid
+core: 8.x
+display:
+ default:
+ display_plugin: default
+ id: default
+ display_title: Master
+ position: 0
+ display_options:
+ access:
+ type: perm
+ options:
+ perm: 'access content'
+ cache:
+ type: tag
+ options: { }
+ query:
+ type: views_query
+ options:
+ disable_sql_rewrite: false
+ distinct: false
+ replica: false
+ query_comment: ''
+ query_tags: { }
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Apply
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: full
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 25, 50'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: '‹ Previous'
+ next: 'Next ›'
+ first: '« First'
+ last: 'Last »'
+ quantity: 9
+ style:
+ type: default
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ uses_fields: false
+ row:
+ type: fields
+ options:
+ inline: { }
+ separator: ''
+ hide_empty: false
+ default_field_elements: true
+ fields:
+ name:
+ id: name
+ table: taxonomy_term_data
+ field: name
+ label: ''
+ alter:
+ alter_text: false
+ make_link: false
+ absolute: false
+ trim: false
+ word_boundary: false
+ ellipsis: false
+ strip_tags: false
+ html: false
+ hide_empty: false
+ empty_zero: false
+ relationship: none
+ group_type: group
+ admin_label: ''
+ exclude: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: true
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_alter_empty: true
+ plugin_id: field
+ type: string
+ settings:
+ link_to_entity: true
+ entity_type: taxonomy_term
+ entity_field: name
+ filters:
+ parent:
+ id: parent
+ table: taxonomy_term_hierarchy
+ field: parent
+ relationship: field_tags
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value:
+ min: ''
+ max: ''
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: parent_op
+ label: 'Parent term'
+ description: ''
+ use_operator: false
+ operator: parent_op
+ identifier: parent
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ placeholder: ''
+ min_placeholder: ''
+ max_placeholder: ''
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ plugin_id: numeric
+ parent_1:
+ id: parent_1
+ table: taxonomy_term_hierarchy
+ field: parent
+ relationship: field_tags
+ group_type: group
+ admin_label: ''
+ operator: '='
+ value:
+ min: ''
+ max: ''
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: parent_1_op
+ label: 'Parent term'
+ description: ''
+ use_operator: false
+ operator: parent_1_op
+ identifier: parent_1
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ placeholder: ''
+ min_placeholder: ''
+ max_placeholder: ''
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ plugin_id: numeric
+ sorts: { }
+ header: { }
+ footer: { }
+ empty: { }
+ relationships:
+ parent:
+ id: parent
+ table: taxonomy_term__parent
+ field: parent_target_id
+ relationship: none
+ group_type: group
+ admin_label: Parent
+ required: true
+ plugin_id: standard
+ arguments: { }
diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php
index b8eac0acc3..3a140731c7 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 02b5995426..8eeb1f9b6a 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 802786e467..ebb5b51f22 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 0a00c13de5..f3e8ecb341 100644
--- a/core/modules/taxonomy/src/TermStorage.php
+++ b/core/modules/taxonomy/src/TermStorage.php
@@ -2,35 +2,14 @@
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.
*/
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.
*
@@ -59,6 +38,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}
*
@@ -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 4ab2d2deaf..4d7b5cc1dd 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 5bcb088db4..2b49ee247b 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 5e98bfe9b2..6d47f3ffb6 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 bdbf8938f7..ce90189b58 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 0000000000..c1a18bcb2c
--- /dev/null
+++ b/core/modules/taxonomy/taxonomy.install
@@ -0,0 +1,128 @@
+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);
+
+ foreach (array_keys($view->get('display')) as $display_id) {
+ $changed = FALSE;
+
+ foreach (['relationships', 'filters', 'arguments'] as $handler_type) {
+ $base_path = "display.$display_id.display_options.$handler_type";
+ $handlers = $view->get($base_path);
+
+ if (!$handlers) {
+ continue;
+ }
+
+ foreach ($handlers as $handler_key => $handler_config) {
+ $table_path = "$base_path.$handler_key.table";
+ $field_path = "$base_path.$handler_key.field";
+ $table = $view->get($table_path);
+ $field = $view->get($field_path);
+
+ if (($table && ($table === 'taxonomy_term_hierarchy')) && ($field && ($field === 'parent'))) {
+ $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 256f618991..65df6a80e8 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 80295b6ea2..664ae27cf0 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 85310e9aa9..81bd6796b5 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 0000000000..1cbe8b49ac
--- /dev/null
+++ b/core/modules/taxonomy/tests/src/Functional/Update/TaxonomyParentUpdateTest.php
@@ -0,0 +1,90 @@
+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");
+
+ $relationship_base_path = 'display.default.display_options.relationships.parent';
+ $this->assertSame($view->get("$relationship_base_path.table"), 'taxonomy_term__parent');
+ $this->assertSame($view->get("$relationship_base_path.field"), 'parent_target_id');
+
+ $filters_base_path_1 = 'display.default.display_options.filters.parent';
+ $this->assertSame($view->get("$filters_base_path_1.table"), 'taxonomy_term__parent');
+ $this->assertSame($view->get("$filters_base_path_1.field"), 'parent_target_id');
+
+ $filters_base_path_2 = 'display.default.display_options.filters.parent';
+ $this->assertSame($view->get("$filters_base_path_2.table"), 'taxonomy_term__parent');
+ $this->assertSame($view->get("$filters_base_path_2.field"), 'parent_target_id');
+
+ // 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 03be3e15bb..3299964909 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 14fba563b0..75c79241e0 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 = [];