Commit 70e40101 authored by catch's avatar catch
Browse files

Issue #2957381 by amateescu, GaëlG, alexpott, douggreen, jibran, catch: Data...

Issue #2957381 by amateescu, GaëlG, alexpott, douggreen, jibran, catch: Data model problems with Vocabulary hierarchy
parent 9444c6a2
......@@ -7,5 +7,4 @@ dependencies:
name: Forums
vid: forums
description: 'Forum navigation vocabulary'
hierarchy: 1
weight: -10
......@@ -27,9 +27,6 @@ taxonomy.vocabulary.*:
description:
type: label
label: 'Description'
hierarchy:
type: integer
label: 'Hierarchy'
weight:
type: integer
label: 'Weight'
......
......@@ -25,7 +25,6 @@ process:
label: name
name: name
description: description
hierarchy: hierarchy
weight: weight
destination:
plugin: entity:taxonomy_vocabulary
......@@ -23,7 +23,6 @@ process:
label: name
name: name
description: description
hierarchy: hierarchy
weight: weight
destination:
plugin: entity:taxonomy_vocabulary
......@@ -53,7 +53,6 @@
* "name",
* "vid",
* "description",
* "hierarchy",
* "weight",
* }
* )
......@@ -81,18 +80,6 @@ class Vocabulary extends ConfigEntityBundleBase implements VocabularyInterface {
*/
protected $description;
/**
* The type of hierarchy allowed within the vocabulary.
*
* Possible values:
* - VocabularyInterface::HIERARCHY_DISABLED: No parents.
* - VocabularyInterface::HIERARCHY_SINGLE: Single parent.
* - VocabularyInterface::HIERARCHY_MULTIPLE: Multiple parents.
*
* @var int
*/
protected $hierarchy = VocabularyInterface::HIERARCHY_DISABLED;
/**
* The weight of this vocabulary in relation to other vocabularies.
*
......@@ -104,14 +91,16 @@ class Vocabulary extends ConfigEntityBundleBase implements VocabularyInterface {
* {@inheritdoc}
*/
public function getHierarchy() {
return $this->hierarchy;
@trigger_error('\Drupal\taxonomy\VocabularyInterface::getHierarchy() is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.x. Use \Drupal\taxonomy\TermStorage::getVocabularyHierarchyType() instead.', E_USER_DEPRECATED);
return $this->entityTypeManager()->getStorage('taxonomy_term')->getVocabularyHierarchyType($this->id());
}
/**
* {@inheritdoc}
*/
public function setHierarchy($hierarchy) {
$this->hierarchy = $hierarchy;
@trigger_error('\Drupal\taxonomy\VocabularyInterface::setHierarchy() is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.x. Reset the cache of the taxonomy_term storage controller instead.', E_USER_DEPRECATED);
$this->entityTypeManager()->getStorage('taxonomy_term')->resetCache();
return $this;
}
......
......@@ -112,6 +112,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular
global $pager_page_array, $pager_total, $pager_total_items;
$form_state->set(['taxonomy', 'vocabulary'], $taxonomy_vocabulary);
$vocabulary_hierarchy = $this->storageController->getVocabularyHierarchyType($taxonomy_vocabulary->id());
$parent_fields = FALSE;
$page = $this->getRequest()->query->get('page') ?: 0;
......@@ -277,7 +278,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular
'#title' => $term->getName(),
'#url' => $term->urlInfo(),
];
if ($taxonomy_vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) {
if ($vocabulary_hierarchy != VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) {
$parent_fields = TRUE;
$form['terms'][$key]['term']['tid'] = [
'#type' => 'hidden',
......@@ -384,7 +385,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular
];
}
if (($taxonomy_vocabulary->getHierarchy() !== VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) && $change_weight_access->isAllowed()) {
if (($vocabulary_hierarchy !== VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) && $change_weight_access->isAllowed()) {
$form['actions'] = ['#type' => 'actions', '#tree' => FALSE];
$form['actions']['submit'] = [
'#type' => 'submit',
......@@ -425,9 +426,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
uasort($form_state->getValue('terms'), ['Drupal\Component\Utility\SortArray', 'sortByWeightElement']);
$vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
// Update the current hierarchy type as we go.
$hierarchy = VocabularyInterface::HIERARCHY_DISABLED;
$changed_terms = [];
$tree = $this->storageController->loadTree($vocabulary->id(), 0, NULL, TRUE);
......@@ -444,7 +442,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$changed_terms[$term->id()] = $term;
}
$weight++;
$hierarchy = $term->parents[0] != 0 ? VocabularyInterface::HIERARCHY_SINGLE : $hierarchy;
$term = $tree[$weight];
}
......@@ -471,7 +468,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$term->parent->target_id = $values['term']['parent'];
$changed_terms[$term->id()] = $term;
}
$hierarchy = $term->parents[0] != 0 ? VocabularyInterface::HIERARCHY_SINGLE : $hierarchy;
$weight++;
}
}
......@@ -484,7 +480,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$term->setWeight($weight);
$changed_terms[$term->id()] = $term;
}
$hierarchy = $term->parents[0] != 0 ? VocabularyInterface::HIERARCHY_SINGLE : $hierarchy;
}
// Save all updated terms.
......@@ -492,11 +487,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
$term->save();
}
// Update the vocabulary hierarchy to flat or single hierarchy.
if ($vocabulary->getHierarchy() != $hierarchy) {
$vocabulary->setHierarchy($hierarchy);
$vocabulary->save();
}
$this->messenger()->addStatus($this->t('The configuration options have been saved.'));
}
......
......@@ -2,7 +2,6 @@
namespace Drupal\taxonomy\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Entity\ContentEntityDeleteForm;
use Drupal\Core\Url;
......@@ -43,21 +42,4 @@ protected function getDeletionMessage() {
return $this->t('Deleted term %name.', ['%name' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
/** @var \Drupal\Core\Entity\ContentEntityInterface $term */
$term = $this->getEntity();
if ($term->isDefaultTranslation()) {
$storage = $this->entityManager->getStorage('taxonomy_vocabulary');
$vocabulary = $storage->load($this->entity->bundle());
// @todo Move to storage http://drupal.org/node/1988712
taxonomy_check_vocabulary_hierarchy($vocabulary, ['tid' => $term->id()]);
}
}
}
......@@ -18,6 +18,7 @@ class TermForm extends ContentEntityForm {
public function form(array $form, FormStateInterface $form_state) {
$term = $this->entity;
$vocab_storage = $this->entityManager->getStorage('taxonomy_vocabulary');
/** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
$taxonomy_storage = $this->entityManager->getStorage('taxonomy_term');
$vocabulary = $vocab_storage->load($term->bundle());
......@@ -28,7 +29,7 @@ public function form(array $form, FormStateInterface $form_state) {
$form['relations'] = [
'#type' => 'details',
'#title' => $this->t('Relations'),
'#open' => $vocabulary->getHierarchy() == VocabularyInterface::HIERARCHY_MULTIPLE,
'#open' => $taxonomy_storage->getVocabularyHierarchyType($vocabulary->id()) == VocabularyInterface::HIERARCHY_MULTIPLE,
'#weight' => 10,
];
......@@ -142,26 +143,11 @@ public function save(array $form, FormStateInterface $form_state) {
}
$current_parent_count = count($form_state->getValue('parent'));
$previous_parent_count = count($form_state->get(['taxonomy', 'parent']));
// Root doesn't count if it's the only parent.
if ($current_parent_count == 1 && $form_state->hasValue(['parent', 0])) {
$current_parent_count = 0;
$form_state->setValue('parent', []);
}
// If the number of parents has been reduced to one or none, do a check on the
// parents of every term in the vocabulary value.
$vocabulary = $form_state->get(['taxonomy', 'vocabulary']);
if ($current_parent_count < $previous_parent_count && $current_parent_count < 2) {
taxonomy_check_vocabulary_hierarchy($vocabulary, $form_state->getValues());
}
// If we've increased the number of parents and this is a single or flat
// hierarchy, update the vocabulary immediately.
elseif ($current_parent_count > $previous_parent_count && $vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE) {
$vocabulary->setHierarchy($current_parent_count == 1 ? VocabularyInterface::HIERARCHY_SINGLE : VocabularyInterface::HIERARCHY_MULTIPLE);
$vocabulary->save();
}
$form_state->setValue('tid', $term->id());
$form_state->set('tid', $term->id());
}
......
......@@ -4,6 +4,7 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Entity\Sql\TableMappingInterface;
/**
* Defines a Controller class for taxonomy terms.
......@@ -46,6 +47,19 @@ class TermStorage extends SqlContentEntityStorage implements TermStorageInterfac
*/
protected $ancestors;
/**
* The type of hierarchy allowed within a vocabulary.
*
* Possible values:
* - VocabularyInterface::HIERARCHY_DISABLED: No parents.
* - VocabularyInterface::HIERARCHY_SINGLE: Single parent.
* - VocabularyInterface::HIERARCHY_MULTIPLE: Multiple parents.
*
* @var int[]
* An array of one the possible values above, keyed by vocabulary ID.
*/
protected $vocabularyHierarchyType;
/**
* {@inheritdoc}
*
......@@ -72,6 +86,7 @@ public function resetCache(array $ids = NULL) {
$this->treeParents = [];
$this->treeTerms = [];
$this->trees = [];
$this->vocabularyHierarchyType = [];
parent::resetCache($ids);
}
......@@ -357,13 +372,50 @@ public function getNodeTerms(array $nids, array $vocabs = [], $langcode = NULL)
return $terms;
}
/**
* {@inheritdoc}
*/
public function getVocabularyHierarchyType($vid) {
// Return early if we already computed this value.
if (isset($this->vocabularyHierarchyType[$vid])) {
return $this->vocabularyHierarchyType[$vid];
}
$parent_field_storage = $this->entityManager->getFieldStorageDefinitions($this->entityTypeId)['parent'];
$table_mapping = $this->getTableMapping();
$target_id_column = $table_mapping->getFieldColumnName($parent_field_storage, 'target_id');
$delta_column = $table_mapping->getFieldColumnName($parent_field_storage, TableMappingInterface::DELTA);
$query = $this->database->select($table_mapping->getFieldTableName('parent'), 'p');
$query->addExpression("MAX($target_id_column)", 'max_parent_id');
$query->addExpression("MAX($delta_column)", 'max_delta');
$query->condition('bundle', $vid);
$result = $query->execute()->fetchAll();
// If all the terms have the same parent, the parent can only be root (0).
if ((int) $result[0]->max_parent_id === 0) {
$this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_DISABLED;
}
// If no term has a delta higher than 0, no term has multiple parents.
elseif ((int) $result[0]->max_delta === 0) {
$this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_SINGLE;
}
else {
$this->vocabularyHierarchyType[$vid] = VocabularyInterface::HIERARCHY_MULTIPLE;
}
return $this->vocabularyHierarchyType[$vid];
}
/**
* {@inheritdoc}
*/
public function __sleep() {
$vars = parent::__sleep();
// Do not serialize static cache.
unset($vars['ancestors'], $vars['treeChildren'], $vars['treeParents'], $vars['treeTerms'], $vars['trees']);
unset($vars['ancestors'], $vars['treeChildren'], $vars['treeParents'], $vars['treeTerms'], $vars['trees'], $vars['vocabularyHierarchyType']);
return $vars;
}
......@@ -378,6 +430,7 @@ public function __wakeup() {
$this->treeParents = [];
$this->treeTerms = [];
$this->trees = [];
$this->vocabularyHierarchyType = [];
}
}
......@@ -126,4 +126,19 @@ public function resetWeights($vid);
*/
public function getNodeTerms(array $nids, array $vocabs = [], $langcode = NULL);
/**
* Returns the hierarchy type for a specific vocabulary ID.
*
* @param string $vid
* Vocabulary ID to retrieve the hierarchy type for.
*
* @return int
* The vocabulary hierarchy.
* Possible values:
* - VocabularyInterface::HIERARCHY_DISABLED: No parents.
* - VocabularyInterface::HIERARCHY_SINGLE: Single parent.
* - VocabularyInterface::HIERARCHY_MULTIPLE: Multiple parents.
*/
public function getVocabularyHierarchyType($vid);
}
......@@ -108,4 +108,29 @@ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $st
return $schema;
}
/**
* {@inheritdoc}
*/
protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
$dedicated_table_schema = parent::getDedicatedTableSchema($storage_definition, $entity_type);
// Add an index on 'bundle', 'delta' and 'parent_target_id' columns to
// increase the performance of the query from
// \Drupal\taxonomy\TermStorage::getVocabularyHierarchyType().
if ($storage_definition->getName() === 'parent') {
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $this->storage->getTableMapping();
$dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
unset($dedicated_table_schema[$dedicated_table_name]['indexes']['bundle']);
$dedicated_table_schema[$dedicated_table_name]['indexes']['bundle_delta_target_id'] = [
'bundle',
'delta',
$table_mapping->getFieldColumnName($storage_definition, 'target_id'),
];
}
return $dedicated_table_schema;
}
}
......@@ -29,6 +29,9 @@ interface VocabularyInterface extends ConfigEntityInterface {
*
* @return int
* The vocabulary hierarchy.
*
* @deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.x. Use
* \Drupal\taxonomy\TermStorage::getVocabularyHierarchyType() instead.
*/
public function getHierarchy();
......@@ -43,6 +46,9 @@ public function getHierarchy();
* - VocabularyInterface::HIERARCHY_MULTIPLE: Multiple parents.
*
* @return $this
*
* @deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.x. Reset
* the cache of the taxonomy_term storage handler instead.
*/
public function setHierarchy($hierarchy);
......
......@@ -186,3 +186,12 @@ function taxonomy_update_8601() {
return t('The publishing status field has been added to taxonomy terms.');
}
/**
* Add an index on the 'taxonomy_term__parent' field table.
*/
function taxonomy_update_8701() {
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$storage_definition = $entity_definition_update_manager->getFieldStorageDefinition('parent', 'taxonomy_term');
$entity_definition_update_manager->updateFieldStorageDefinition($storage_definition);
}
......@@ -81,8 +81,9 @@ function taxonomy_help($route_name, RouteMatchInterface $route_match) {
case 'entity.taxonomy_vocabulary.overview_form':
$vocabulary = $route_match->getParameter('taxonomy_vocabulary');
$vocabulary_hierarchy = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getVocabularyHierarchyType($vocabulary->id());
if (\Drupal::currentUser()->hasPermission('administer taxonomy') || \Drupal::currentUser()->hasPermission('edit terms in ' . $vocabulary->id())) {
switch ($vocabulary->getHierarchy()) {
switch ($vocabulary_hierarchy) {
case VocabularyInterface::HIERARCHY_DISABLED:
return '<p>' . t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '</p>';
case VocabularyInterface::HIERARCHY_SINGLE:
......@@ -92,7 +93,7 @@ function taxonomy_help($route_name, RouteMatchInterface $route_match) {
}
}
else {
switch ($vocabulary->getHierarchy()) {
switch ($vocabulary_hierarchy) {
case VocabularyInterface::HIERARCHY_DISABLED:
return '<p>' . t('%capital_name contains the following terms.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
case VocabularyInterface::HIERARCHY_SINGLE:
......@@ -156,11 +157,10 @@ function taxonomy_theme() {
}
/**
* Checks and updates the hierarchy flag of a vocabulary.
* Checks the hierarchy flag of a vocabulary.
*
* Checks the current parents of all terms in a vocabulary and updates the
* vocabulary's hierarchy setting to the lowest possible level. If no term
* has parent terms then the vocabulary will be given a hierarchy of
* Checks the current parents of all terms in a vocabulary. If no term has
* parent terms then the vocabulary will be given a hierarchy of
* VocabularyInterface::HIERARCHY_DISABLED. If any term has a single parent then
* the vocabulary will be given a hierarchy of
* VocabularyInterface::HIERARCHY_SINGLE. If any term has multiple parents then
......@@ -172,33 +172,15 @@ function taxonomy_theme() {
* @param $changed_term
* An array of the term structure that was updated.
*
* @return
* @return int
* An integer that represents the level of the vocabulary's hierarchy.
*
* @deprecated in Drupal 8.7.x. Will be removed before Drupal 9.0.0. Use
* \Drupal\taxonomy\TermStorage::getVocabularyHierarchyType() instead.
*/
function taxonomy_check_vocabulary_hierarchy(VocabularyInterface $vocabulary, $changed_term) {
$tree = \Drupal::entityManager()->getStorage('taxonomy_term')->loadTree($vocabulary->id());
$hierarchy = VocabularyInterface::HIERARCHY_DISABLED;
foreach ($tree as $term) {
// Update the changed term with the new parent value before comparison.
if ($term->tid == $changed_term['tid']) {
$term = (object) $changed_term;
$term->parents = $term->parent;
}
// Check this term's parent count.
if (count($term->parents) > 1) {
$hierarchy = VocabularyInterface::HIERARCHY_MULTIPLE;
break;
}
elseif (count($term->parents) == 1 && !isset($term->parents[0])) {
$hierarchy = VocabularyInterface::HIERARCHY_SINGLE;
}
}
if ($hierarchy != $vocabulary->getHierarchy()) {
$vocabulary->setHierarchy($hierarchy);
$vocabulary->save();
}
return $hierarchy;
@trigger_error('taxonomy_check_vocabulary_hierarchy() is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.x. Use \Drupal\taxonomy\TermStorage::getVocabularyHierarchyType() instead.', E_USER_DEPRECATED);
return \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getVocabularyHierarchyType($vocabulary->id());
}
/**
......
......@@ -125,3 +125,12 @@ function taxonomy_post_update_handle_publishing_status_addition_in_views(&$sandb
return TRUE;
});
}
/**
* Remove the 'hierarchy' property from vocabularies.
*/
function taxonomy_post_update_remove_hierarchy_from_vocabularies(&$sandbox = NULL) {
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'taxonomy_vocabulary', function () {
return TRUE;
});
}
......@@ -54,7 +54,6 @@ protected function getExpectedNormalizedEntity() {
'dependencies' => [],
'name' => 'Llama',
'description' => NULL,
'hierarchy' => 0,
'weight' => 0,
];
}
......
......@@ -90,11 +90,11 @@ public function testTaxonomyTermHierarchy() {
$term2 = $this->createTerm($this->vocabulary);
// Get the taxonomy storage.
$taxonomy_storage = $this->container->get('entity.manager')->getStorage('taxonomy_term');
/** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_storage */
$taxonomy_storage = $this->container->get('entity_type.manager')->getStorage('taxonomy_term');
// Check that hierarchy is flat.
$vocabulary = Vocabulary::load($this->vocabulary->id());
$this->assertEqual(0, $vocabulary->getHierarchy(), 'Vocabulary is flat.');
$this->assertEquals(0, $taxonomy_storage->getVocabularyHierarchyType($this->vocabulary->id()), 'Vocabulary is flat.');
// Edit $term2, setting $term1 as parent.
$edit = [];
......
<?php
namespace Drupal\Tests\taxonomy\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests that the 'hierarchy' property is removed from vocabularies.
*
* @group taxonomy
* @group Update
* @group legacy
*/
class TaxonomyVocabularyHierarchyUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8-rc1.filled.standard.php.gz',
];
}
/**
* Tests that the 'hierarchy' property is removed from vocabularies.
*
* @see taxonomy_post_update_remove_hierarchy_from_vocabularies()
* @see taxonomy_update_8701()
*/
public function testTaxonomyUpdateParents() {
$hierarchy = \Drupal::config('taxonomy.vocabulary.test_vocabulary')->get('hierarchy');
$this->assertSame(1, $hierarchy);
// Run updates.
$this->runUpdates();
$hierarchy = \Drupal::config('taxonomy.vocabulary.test_vocabulary')->get('hierarchy');
$this->assertNull($hierarchy);
$database = \Drupal::database();
$this->assertFalse($database->schema()->indexExists('taxonomy_term__parent', 'bundle'));
$this->assertTrue($database->schema()->indexExists('taxonomy_term__parent', 'bundle_delta_target_id'));
}
}
......@@ -35,13 +35,11 @@ public function testTaxonomyVocabulary() {
$this->assertSame($this->getMigration('d6_taxonomy_vocabulary')->getIdMap()->lookupDestinationId([$j]), [$vocabulary->id()]);
$this->assertSame("vocabulary $j (i=$i)", $vocabulary->label());
$this->assertSame("description of vocabulary $j (i=$i)", $vocabulary->getDescription());
$this->assertSame($i, $vocabulary->getHierarchy());
$this->assertSame(4 + $i, $vocabulary->get('weight'));
}
$vocabulary = Vocabulary::load('vocabulary_name_much_longer_than');
$this->assertSame('vocabulary name much longer than thirty two characters', $vocabulary->label());
$this->assertSame('description of vocabulary name much longer than thirty two characters', $vocabulary->getDescription());
$this->assertSame(3, $vocabulary->getHierarchy());
$this->assertSame(7, $vocabulary->get('weight'));
}
......
......@@ -35,18 +35,15 @@ protected function setUp() {
* The label the migrated entity should have.
* @param $expected_description
* The description the migrated entity should have.
* @param $expected_hierarchy
* The hierarchy setting the migrated entity should have.
* @param $expected_weight
* The weight the migrated entity should have.
*/
protected function assertEntity($id, $expected_label, $expected_description, $expected_hierarchy, $expected_weight) {
protected function assertEntity($id, $expected_label, $expected_description, $expected_weight) {
/** @var \Drupal\taxonomy\VocabularyInterface $entity */
$entity = Vocabulary::load($id);
$this->assertTrue($entity instanceof VocabularyInterface);
$this->assertIdentical($expected_label, $entity->label());