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 = [];